initial commit of project
generated by OpenCode and MiniMax M3.1 Flash Free
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
# AGENTS.md — JSON DevTools
|
||||
|
||||
Statická jednostránková aplikácia (vanilla ES6+, HTML/CSS/JS). Bez frameworkov,
|
||||
bez npm, bez build procesu, bez backendu, bez CDN. Bez CI, `package.json` ani
|
||||
lint konfigurácie — repozitár je hotový, nič sa nekompiluje.
|
||||
|
||||
## ZákAZ KONTROL V PREHLIADAČOCH
|
||||
|
||||
**V prehliadačoch sa nesú overovanie aplikácie – používateľ to robí sám.**
|
||||
Neotváraj Chrome/Chromium ani iný prehliadač, neautomatizuj ich, nescreenshotuj
|
||||
cez prehliadač. Trvalé používateľské nariadenie, zapísané aj v globálnej pamäti
|
||||
`~/.config/opencode/AGENTS.md`.
|
||||
|
||||
Povolené: statická kontrola (`node --check`), testy logiky v Node.js, `curl`/`python`.
|
||||
Nepridávaj do repozitára testy ani nástroje založené na prehliadači.
|
||||
|
||||
## Štruktúra
|
||||
|
||||
```
|
||||
src/index.html src/style.css src/app.js # aplikácia – musia zostať v jednej zložke
|
||||
tests/logic.test.js # Node testy (bez prehliadača)
|
||||
docs/ # screenshoty pre README
|
||||
```
|
||||
|
||||
* `src/index.html` načítava `style.css` a `app.js` **relatívnou cestou** → všetky
|
||||
tri súbory musia zostať v `src/`. Presun do rootu rozbije načítanie aj testy.
|
||||
* Aplikácia sa otvára ako **`src/index.html`** (relatívne cesty), nie cez root.
|
||||
* Repo hostuje **Gitea**, nie GitHub — nepridávaj GitHub Actions/Pages šablóny.
|
||||
|
||||
## Príkazy
|
||||
|
||||
```bash
|
||||
node --check src/app.js # rýchla syntax kontrola
|
||||
node tests/logic.test.js # testy logiky – 93 assertov, exit 1 pri chybe
|
||||
python -m http.server 8080 --directory src # lokálne spustenie (voliteľné)
|
||||
```
|
||||
|
||||
Nie je čo `npm install`ovať ani buildovať – po úprave `src/*.js|css|html` stačí
|
||||
reload stránky.
|
||||
|
||||
## Ako funguje `tests/logic.test.js`
|
||||
|
||||
* `src/app.js` sa načíta do `vm` sandboxu s **ručným DOM stubom** (žiadne jsdom).
|
||||
Aplikácia sa musí vedieť načítať bez reálneho DOM.
|
||||
* Stub vracia **nový generický element pri každom `querySelector()`** → DOM
|
||||
interakcie (tlačidlá, eventy, fokus) sa v Node testoch **neoverujú**. Overuj
|
||||
logiku cez `window.JSONDevTools`, nie cez DOM.
|
||||
* `src/app.js` musí na konci exportovať **`window.JSONDevTools`** (objekt s
|
||||
modulmi `Utils, Toast, Theme, Splitter, Highlighter, Doc, Formatter, FileIO,
|
||||
Tree, Diff, Api, History, App`) — bez neho testy skončí chybou.
|
||||
* Testy čítajú `../src/app.js` a `../src/index.html` → pri presunu súborov treba
|
||||
upraviť cesty v `tests/logic.test.js`.
|
||||
* Pomocníci: `ok(name, cond, extra)`, `eq(name, a, b)`, `deepEq(...)`, `section(t)`.
|
||||
* Testy obsahujú aj statické kontroly konzistencie: každé `$('#id')` v `app.js`
|
||||
musí existovať v `index.html`, každé `<button id>` musí mať handler, `id` v HTML
|
||||
nesmie byť duplicitné. **Nové `id` v HTML pridaj aj do `app.js`**, inak test
|
||||
zfailuje.
|
||||
|
||||
## Architektúra `src/app.js`
|
||||
|
||||
Jeden IIFE, vnútri moduly: `Utils`, `LS`, `Toast`, `Theme`, `Splitter`,
|
||||
`Highlighter`, `Doc`, `Formatter`, `FileIO`, `Tree`, `Diff`, `Api`, `History`,
|
||||
`App`. Bez tried, bez DI – moduly sú plain objekty.
|
||||
|
||||
Nároky, ktoré treba rešpektovať:
|
||||
|
||||
* **`Doc` je zdieľaný stav** medzi Formatter a Tree – oba textareas sú synchro
|
||||
cez `Doc.set(text, origin)`. Nový JSON pohľad pripoj cez `Doc.onChange()`,
|
||||
nie vlastnou kopírou textu.
|
||||
* **`localStorage` len cez `Utils.store`** (prefix `jsondevtools.*`). Wrapper
|
||||
zachytáva prekročenie kvóty a vyhodí toast – nepristupuj k `localStorage`
|
||||
priamo.
|
||||
* **Bezpečnostná invariant:** do historie sa nikdy nesmie uložiť `Authorization`,
|
||||
`Cookie` ani `X-Api-Key` – prechádza to cez `sanitizeHeaders()` v `Api`.
|
||||
Tokeny žijú iba v pamäti stránky.
|
||||
* CSS motívy sú CSS custom properties v `html[data-theme="dark"|"light"]` –
|
||||
nové farby pridávaj ako premenné, nie ako hardcoded hodnoty.
|
||||
* UI texty v slovenčine, JSON hodnoty v angličtine.
|
||||
|
||||
## Ostatné
|
||||
|
||||
* Screenshoty v `docs/` sú v README vložené relatívnou cestou (`docs/...`) –
|
||||
pri presúvaní ich treba aktualizovať aj v `README.md`.
|
||||
* `src/index.html?theme=light|dark` prepne motív **len pre dané načítanie** – uloženú
|
||||
preferenciu v `localStorage` neprepíše (prepíše ju až prepínač v hlavičke).
|
||||
@@ -1,2 +1,159 @@
|
||||
# JSONdevTools
|
||||
# JSON DevTools
|
||||
|
||||
Kompletný jednostránkový nástroj na prácu s JSON-om pri vývoji REST API.
|
||||
Bez frameworkov, bez npm, bez build procesu a bez backendu.
|
||||
|
||||
**Formatter · Tree viewer · Diff · API Request builder · História**
|
||||
|
||||
---
|
||||
|
||||
## Ukážky
|
||||
|
||||
### Formatter — živá validácia a syntax highlighting
|
||||
|
||||

|
||||
|
||||
Vľavo vstup, vpravo zvýraznený výstup. Chyba vo vstupe sa zobrazí s presným
|
||||
riadkom, stĺpcom a výrezom textu.
|
||||
|
||||
### Tree viewer — typy, počty položiek, JSONPath
|
||||
|
||||

|
||||
|
||||
Strom zobrazuje typ každej hodnoty (`string`, `number`, `boolean`, `null`) a počet
|
||||
položiek objektov/polí (`{6}`, `[5]`). Každý uzol sa dá skopírovať ako **JSONPath**
|
||||
alebo ako **konkrétna hodnota**.
|
||||
|
||||
### API Request builder — stav, trvanie, headers, odpoveď
|
||||
|
||||

|
||||
|
||||
Status, trvanie požiadavky, response headers a automaticky naformátovaná JSON
|
||||
odpoveď. Pri chybe CORS sa zobrazí podrobné vysvetlenie príčiny.
|
||||
|
||||
---
|
||||
|
||||
## Spustenie
|
||||
|
||||
Aplikácia je čisto statická — otvorte **`src/index.html`** priamo v prehliadači
|
||||
(dvojklikom) a všetko beží lokálne.
|
||||
|
||||
Voliteľne cez lokálny server (odporúčané, ak chcete testovať `fetch()` proti
|
||||
API s CORS z `http://localhost`):
|
||||
|
||||
```bash
|
||||
python -m http.server 8080 --directory src
|
||||
# potom otvorte http://localhost:8080/
|
||||
```
|
||||
|
||||
> Otvorenie cez `file://` funguje, ale prehliadač považuje pôvod stránky za `null`
|
||||
> — časť API (hlavičky, CORS) môže byť obmedzená. Aplikácia v tom prípade zobrazí
|
||||
> podrobné vysvetlenie.
|
||||
|
||||
---
|
||||
|
||||
## Štruktúra repozitára
|
||||
|
||||
```
|
||||
JSONdevTools/
|
||||
├── src/ # samotná aplikácia (jediný adresár, ktorý sa publikuje)
|
||||
│ ├── index.html # štruktúra UI (5 panelov, ARIA role, modál skratiek)
|
||||
│ ├── style.css # tmavý / svetlý motív, responzívne rozloženie, resizable panely
|
||||
│ └── app.js # celá logika (vanilla ES6+), bez závislostí
|
||||
├── tests/
|
||||
│ └── logic.test.js # Node testy logiky (bez prehliadača)
|
||||
├── docs/
|
||||
│ ├── Screenshot-Formatter.png
|
||||
│ ├── Screenshot-Tree.png
|
||||
│ └── Screenshot-API-Request.png
|
||||
├── AGENTS.md # projektové pravidlá pre AI agenty
|
||||
├── LICENSE
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Nástroje
|
||||
|
||||
### 1. Formatter
|
||||
* Vloženie JSON textu, načítanie súboru (tlačidlo alebo pretiahnutie myšou)
|
||||
* Živá validácia s presnou hláškou chyby: **riadok, stĺpec, pozícia, výrez textu**
|
||||
* Pretty print (2 medzery) / Minify
|
||||
* Kopírovanie výsledku, stiahnutie `.json` súboru
|
||||
* Syntax highlighting výstupu
|
||||
|
||||
### 2. Tree
|
||||
* Rozbaľovanie / zbaľovanie uzlov, zobrazenie typov a počtu položiek (`{6}`, `[5]`)
|
||||
* Vyhľadávanie v kľúčoch aj hodnotách, zvýraznenie zhôd, automatické rozbavenie
|
||||
rodičov zhôd
|
||||
* Kopírovanie **JSONPath** (`$.user.roles[1]`) a **konkrétnej hodnoty** uzla
|
||||
* Ovládanie klávesnicou: `↑`/`↓` pohyb, `→`/`←` rozbaliť/zbaliť, `Enter`/`Space`
|
||||
prepnúť, `C` kopírovať hodnotu, `P` kopírovať path
|
||||
|
||||
### 3. Diff
|
||||
* Dva editory vedľa sebe (A = pôvodné, B = nové)
|
||||
* Rekurzívne porovnanie vnorených objektov a polí
|
||||
* Rozdiely: **pridané / odstránené / zmenené** (vrátane zmeny typu) s JSONPath
|
||||
* Súhrnný počet rozdielov, kopírovanie výsledkov porovnania ako JSON
|
||||
|
||||
### 4. API Request
|
||||
* Metódy `GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS`
|
||||
* URL endpointu, query parametre, vlastné HTTP hlavičky
|
||||
* Autentifikácia: Bearer token alebo Basic (`user:heslo`)
|
||||
* JSON request body (s formátovaním / minifikovaním)
|
||||
* `fetch()` s 30 s timeoutom, meranie trvania, zobrazenie HTTP statusu
|
||||
* Response headers, automaticky naformátovaná JSON odpoveď, surový text pri
|
||||
neplatnom JSON
|
||||
* Detailné vysvetlenie **CORS** chýb (chýbajúca hlavička, preflight, `file://`,
|
||||
DNS, proxy) s návrhmi riešenia
|
||||
|
||||
### 5. História
|
||||
* Posledných **20 JSON dokumentov** a **20 API požiadaviek**
|
||||
* Ukladá sa do `localStorage`
|
||||
* Vymazanie jednotlivých záznamov aj celej historie
|
||||
* **Citlivé údaje sa neukladajú**: hlavička `Authorization`, `Cookie`, `X-Api-Key`
|
||||
sa z záznamu vymaže, tokeny žijú iba v pamäti stránky
|
||||
|
||||
---
|
||||
|
||||
## Klávesové skratky
|
||||
|
||||
| Skratka | Akcia |
|
||||
|---|---|
|
||||
| `Ctrl+Shift+F` | Pretty print |
|
||||
| `Ctrl+Shift+M` | Minify |
|
||||
| `Ctrl+Shift+C` | Kopírovať výstup |
|
||||
| `Ctrl+Shift+O` | Otvoriť JSON súbor |
|
||||
| `Ctrl+S` | Stiahnuť JSON súbor |
|
||||
| `Ctrl+Enter` | Odoslať API požiadavku |
|
||||
| `Ctrl+Shift+D` | Prepnuť motív |
|
||||
| `Ctrl+Shift+K` | Focus na vyhľadávanie v strome |
|
||||
| `Ctrl+Shift+X` | Vyčistiť aktuálny editor |
|
||||
| `1` … `5` | Prepnutie nástroja (mimo textových polí) |
|
||||
| `?` | Zoznam skratiek |
|
||||
| `Esc` | Zavrieť okno / zrušiť vyhľadávanie |
|
||||
|
||||
Na macOS použite `Cmd` namiesto `Ctrl`.
|
||||
|
||||
---
|
||||
|
||||
## Príkazy
|
||||
|
||||
```bash
|
||||
node tests/logic.test.js # testy logiky – 93 assertov, bez prehliadača
|
||||
```
|
||||
|
||||
Testy nepoužívajú žiadny prehliadač ani externé závislosti: `app.js` sa načíta
|
||||
do izolovaného `vm` kontextu s minimálnym DOM stubom a overuje sa diff algoritmus,
|
||||
sanitizácia historie, cesty uzlov, syntax highlighting a konzistencia ID medzi
|
||||
HTML a JavaScriptom.
|
||||
|
||||
---
|
||||
|
||||
## Bezpečnosť a súkromie
|
||||
|
||||
* Aplikácia je čisto statická — žiadne CDN, žiadne externé závislosti.
|
||||
* Údaje sa nikam neposielajú, **okrem API požiadaviek, ktoré sám spustíte**
|
||||
na URL, ktorú zadáte vy.
|
||||
* Všetko (história, motív, šírky panelov) sa ukladá lokálne v `localStorage`.
|
||||
* Pre zapnutie svetlého motívu jednorazovo cez URL: `src/index.html?theme=light`.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
+2015
File diff suppressed because it is too large
Load Diff
+326
@@ -0,0 +1,326 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="sk" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<meta name="description" content="JSON DevTools - formatter, tree viewer, diff a API request builder pre REST API debugging">
|
||||
<title>JSON DevTools</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="skip-link" href="#main">Preskočiť na obsah</a>
|
||||
|
||||
<header class="app-header">
|
||||
<div class="brand">
|
||||
<span class="logo" aria-hidden="true">{ }</span>
|
||||
<span class="brand-name">JSON DevTools</span>
|
||||
</div>
|
||||
|
||||
<nav class="tabs" role="tablist" aria-label="Nástroje">
|
||||
<button class="tab" role="tab" id="tab-formatter" data-view="formatter" aria-controls="view-formatter" aria-selected="true">Formatter</button>
|
||||
<button class="tab" role="tab" id="tab-tree" data-view="tree" aria-controls="view-tree" aria-selected="false">Tree</button>
|
||||
<button class="tab" role="tab" id="tab-diff" data-view="diff" aria-controls="view-diff" aria-selected="false">Diff</button>
|
||||
<button class="tab" role="tab" id="tab-request" data-view="request" aria-controls="view-request" aria-selected="false">API Request</button>
|
||||
<button class="tab" role="tab" id="tab-history" data-view="history" aria-controls="view-history" aria-selected="false">História</button>
|
||||
</nav>
|
||||
|
||||
<div class="header-actions">
|
||||
<button id="theme-toggle" class="icon-btn" title="Prepnuť motív (Ctrl+Shift+D)" aria-label="Prepnuť motív">
|
||||
<svg class="icon icon-moon" viewBox="0 0 24 24" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
|
||||
<svg class="icon icon-sun" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4.5"/><g stroke-linecap="round"><path d="M12 2v2.5M12 19.5V22M2 12h2.5M19.5 12H22M4.9 4.9l1.8 1.8M17.3 17.3l1.8 1.8M19.1 4.9l-1.8 1.8M6.7 17.3l-1.8 1.8"/></g></svg>
|
||||
</button>
|
||||
<button id="shortcuts-btn" class="icon-btn" title="Klávesové skratky (?)" aria-label="Klávesové skratky">
|
||||
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="2" y="6" width="20" height="12" rx="2"/><g stroke-linecap="round"><path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M7 14h10"/></g></svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="main" class="main">
|
||||
|
||||
<!-- ======================= FORMATTER ======================= -->
|
||||
<section class="view" id="view-formatter" role="tabpanel" aria-labelledby="tab-formatter" tabindex="-1">
|
||||
<div class="toolbar" role="toolbar" aria-label="Nástroje formátovača">
|
||||
<button class="btn btn-primary" id="fmt-format" title="Pretty print (Ctrl+Shift+F)">Pretty print</button>
|
||||
<button class="btn" id="fmt-minify" title="Minify (Ctrl+Shift+M)">Minify</button>
|
||||
<button class="btn" id="fmt-copy" title="Kopírovať výsledok (Ctrl+Shift+C)">Kopírovať</button>
|
||||
<button class="btn" id="fmt-download" title="Stiahnuť JSON súbor (Ctrl+S)">Stiahnuť</button>
|
||||
<button class="btn" id="fmt-open" title="Načítať JSON súbor (Ctrl+Shift+O)">Otvoriť…</button>
|
||||
<span class="sep" role="separator"></span>
|
||||
<button class="btn btn-ghost" id="fmt-clear" title="Vyčistiť editor">Vyčistiť</button>
|
||||
<button class="btn btn-ghost" id="fmt-sample" title="Vložiť ukážkový JSON">Ukážka</button>
|
||||
<input type="file" id="file-input" accept=".json,application/json,text/plain" hidden>
|
||||
</div>
|
||||
|
||||
<div class="split-pane" id="fmt-split" data-storage-key="layout.formatter">
|
||||
<section class="panel" aria-label="Vstupný JSON">
|
||||
<header class="panel-head">
|
||||
<h2 class="panel-title">Vstup</h2>
|
||||
<span class="panel-meta" id="fmt-input-meta">0 B</span>
|
||||
</header>
|
||||
<div class="panel-body no-pad">
|
||||
<textarea id="fmt-input" class="code-area" spellcheck="false" autocomplete="off" autocapitalize="off" aria-label="Vstupný JSON text" placeholder="Vložte JSON sem…"></textarea>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="splitter" role="separator" aria-orientation="vertical" tabindex="0" aria-label="Resize panelov" title="Potiahnuť na zmenu šírky"></div>
|
||||
|
||||
<section class="panel" aria-label="Výstupný JSON">
|
||||
<header class="panel-head">
|
||||
<h2 class="panel-title">Výstup</h2>
|
||||
<span class="panel-meta" id="fmt-status">Idle</span>
|
||||
</header>
|
||||
<div class="panel-body no-pad">
|
||||
<pre id="fmt-output" class="code-area code-pre" tabindex="0" aria-label="Výstupný JSON" aria-live="polite"></pre>
|
||||
</div>
|
||||
<div id="fmt-error" class="error-box" role="alert" hidden></div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ======================= TREE ======================= -->
|
||||
<section class="view" id="view-tree" role="tabpanel" aria-labelledby="tab-tree" tabindex="-1" hidden>
|
||||
<div class="toolbar" role="toolbar" aria-label="Nástroje stromu">
|
||||
<div class="search-wrap">
|
||||
<svg class="icon search-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.6-3.6" stroke-linecap="round"/></svg>
|
||||
<input type="search" id="tree-search" class="input search-input" placeholder="Hľadať kľúč alebo hodnotu…" aria-label="Hľadať v strome">
|
||||
<span class="panel-meta" id="tree-search-count" aria-live="polite"></span>
|
||||
</div>
|
||||
<span class="sep" role="separator"></span>
|
||||
<button class="btn btn-ghost" id="tree-expand" title="Rozbaliť všetko">Rozbaliť všetko</button>
|
||||
<button class="btn btn-ghost" id="tree-collapse" title="Zbaliť všetko">Zbaliť všetko</button>
|
||||
<button class="btn btn-ghost" id="tree-open" title="Načítať JSON súbor">Otvoriť…</button>
|
||||
<button class="btn btn-ghost" id="tree-copy-json" title="Kopírovať JSON">Kopírovať JSON</button>
|
||||
</div>
|
||||
|
||||
<div class="split-pane" id="tree-split" data-storage-key="layout.tree">
|
||||
<section class="panel" aria-label="Zdrojový JSON">
|
||||
<header class="panel-head">
|
||||
<h2 class="panel-title">Zdroj</h2>
|
||||
<span class="panel-meta" id="tree-input-meta">0 B</span>
|
||||
</header>
|
||||
<div class="panel-body no-pad">
|
||||
<textarea id="tree-input" class="code-area" spellcheck="false" autocomplete="off" autocapitalize="off" aria-label="Zdrojový JSON text" placeholder="Vložte JSON sem…"></textarea>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="splitter" role="separator" aria-orientation="vertical" tabindex="0" aria-label="Resize panelov"></div>
|
||||
|
||||
<section class="panel" aria-label="Stromová štruktúra">
|
||||
<header class="panel-head">
|
||||
<h2 class="panel-title">Strom</h2>
|
||||
<span class="panel-meta" id="tree-meta"></span>
|
||||
</header>
|
||||
<div class="panel-body" id="tree-container-wrap">
|
||||
<div id="tree-root" class="tree" role="tree" aria-label="JSON strom"></div>
|
||||
</div>
|
||||
<div id="tree-error" class="error-box" role="alert" hidden></div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ======================= DIFF ======================= -->
|
||||
<section class="view" id="view-diff" role="tabpanel" aria-labelledby="tab-diff" tabindex="-1" hidden>
|
||||
<div class="toolbar" role="toolbar" aria-label="Nástroje porovnania">
|
||||
<button class="btn btn-primary" id="diff-run" title="Porovnať (Ctrl+Shift+D je motív – použite Ctrl+Enter)">Porovnať</button>
|
||||
<button class="btn" id="diff-swap" title="Vymeniť strany">Vymeniť A ↔ B</button>
|
||||
<button class="btn" id="diff-copy" title="Kopírovať výsledky porovnania">Kopírovať výsledky</button>
|
||||
<span class="sep" role="separator"></span>
|
||||
<button class="btn btn-ghost" id="diff-sample" title="Načítať ukážkový pár">Ukážka</button>
|
||||
<button class="btn btn-ghost" id="diff-clear" title="Vyčistiť oba vstupy">Vyčistiť</button>
|
||||
<span class="panel-meta" id="diff-summary" aria-live="polite">Nevykonané</span>
|
||||
</div>
|
||||
|
||||
<div class="split-pane" id="diff-split" data-storage-key="layout.diff">
|
||||
<section class="panel" aria-label="JSON A">
|
||||
<header class="panel-head">
|
||||
<h2 class="panel-title">A – pôvodné</h2>
|
||||
<span class="panel-meta" id="diff-a-meta">0 B</span>
|
||||
</header>
|
||||
<div class="panel-body no-pad">
|
||||
<textarea id="diff-a" class="code-area" spellcheck="false" autocomplete="off" autocapitalize="off" aria-label="JSON A" placeholder="Pôvodný JSON…"></textarea>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="splitter" role="separator" aria-orientation="vertical" tabindex="0" aria-label="Resize panelov"></div>
|
||||
|
||||
<section class="panel" aria-label="JSON B">
|
||||
<header class="panel-head">
|
||||
<h2 class="panel-title">B – nové</h2>
|
||||
<span class="panel-meta" id="diff-b-meta">0 B</span>
|
||||
</header>
|
||||
<div class="panel-body no-pad">
|
||||
<textarea id="diff-b" class="code-area" spellcheck="false" autocomplete="off" autocapitalize="off" aria-label="JSON B" placeholder="Nový JSON…"></textarea>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="results" id="diff-results-wrap">
|
||||
<header class="panel-head">
|
||||
<h2 class="panel-title">Rozdiely</h2>
|
||||
<div class="legend">
|
||||
<span class="badge badge-added">pridané</span>
|
||||
<span class="badge badge-removed">odstránené</span>
|
||||
<span class="badge badge-changed">zmenené</span>
|
||||
</div>
|
||||
</header>
|
||||
<div class="panel-body" id="diff-results" aria-live="polite">
|
||||
<p class="muted">Kliknite na <strong>Porovnať</strong> pre zobrazenie rozdielov medzi A a B.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ======================= API REQUEST ======================= -->
|
||||
<section class="view" id="view-request" role="tabpanel" aria-labelledby="tab-request" tabindex="-1" hidden>
|
||||
<div class="split-pane horizontal-request" id="req-split" data-storage-key="layout.request">
|
||||
<section class="panel req-form" aria-label="Parametre požiadavky">
|
||||
<header class="panel-head">
|
||||
<h2 class="panel-title">Požiadavka</h2>
|
||||
<span class="panel-meta">Ctrl+Enter = Odoslať</span>
|
||||
</header>
|
||||
<div class="panel-body scroll">
|
||||
<div class="field">
|
||||
<label for="req-method">Metóda</label>
|
||||
<select id="req-method" class="input">
|
||||
<option>GET</option>
|
||||
<option>POST</option>
|
||||
<option>PUT</option>
|
||||
<option>PATCH</option>
|
||||
<option>DELETE</option>
|
||||
<option>HEAD</option>
|
||||
<option>OPTIONS</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="req-url">URL endpointu</label>
|
||||
<div class="row">
|
||||
<input type="text" id="req-url" class="input mono" placeholder="https://api.example.com/v1/users" spellcheck="false" autocomplete="off">
|
||||
<button class="btn btn-primary" id="req-send" title="Odoslať požiadavku (Ctrl+Enter)">Odoslať</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details class="group" open>
|
||||
<summary>Query parametre</summary>
|
||||
<div id="req-params" class="kv-list"></div>
|
||||
<button class="btn btn-ghost btn-sm" id="req-add-param">+ Parameter</button>
|
||||
</details>
|
||||
|
||||
<details class="group" open>
|
||||
<summary>Hlavičky</summary>
|
||||
<div id="req-headers" class="kv-list"></div>
|
||||
<div class="row">
|
||||
<button class="btn btn-ghost btn-sm" id="req-add-header">+ Hlavička</button>
|
||||
<button class="btn btn-ghost btn-sm" id="req-preset-json">Nastaviť Content-Type: application/json</button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group">
|
||||
<summary>Autentifikácia</summary>
|
||||
<div class="field">
|
||||
<label for="req-auth-type">Typ</label>
|
||||
<select id="req-auth-type" class="input">
|
||||
<option value="none">Žiadna</option>
|
||||
<option value="bearer">Bearer token</option>
|
||||
<option value="basic">Basic (user:heslo)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field" id="req-token-field" hidden>
|
||||
<label for="req-token" id="req-token-label">Token</label>
|
||||
<div class="row">
|
||||
<input type="password" id="req-token" class="input mono" placeholder="…" spellcheck="false" autocomplete="off">
|
||||
<button class="btn btn-ghost btn-sm" id="req-token-toggle" title="Zobraziť / skryť">Zobraziť</button>
|
||||
</div>
|
||||
<p class="hint">Token sa nikdy neukladá do historie ani do localStorage.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group" open>
|
||||
<summary>Request body</summary>
|
||||
<textarea id="req-body" class="code-area code-body" spellcheck="false" autocomplete="off" aria-label="Telo požiadavky" placeholder='{ "key": "value" }'></textarea>
|
||||
<div class="row">
|
||||
<span class="panel-meta" id="req-body-meta">0 B</span>
|
||||
<button class="btn btn-ghost btn-sm" id="req-body-format">Formatovať</button>
|
||||
<button class="btn btn-ghost btn-sm" id="req-body-minify">Minify</button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group">
|
||||
<summary>Bezpečnosť</summary>
|
||||
<ul class="plain-list">
|
||||
<li>Hlavička <code>Authorization</code> sa z požiadavky v histórii vymaže.</li>
|
||||
<li>Tokeny sa uchovávajú iba v pamäti a po obnovení stránky sa zmažú.</li>
|
||||
<li>Žiadne údaje sa neposielajú na iné servery než na URL, ktorú zadáte vy.</li>
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="splitter" role="separator" aria-orientation="vertical" tabindex="0" aria-label="Resize panelov"></div>
|
||||
|
||||
<section class="panel req-response" aria-label="Odpoveď">
|
||||
<header class="panel-head">
|
||||
<h2 class="panel-title">Odpoveď</h2>
|
||||
<span class="panel-meta" id="req-status">Nevykonané</span>
|
||||
</header>
|
||||
<div class="panel-body scroll" id="req-response-body">
|
||||
<p class="muted">Zostavte požiadavku a stlačte <strong>Odoslať</strong> (Ctrl+Enter).</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ======================= HISTORY ======================= -->
|
||||
<section class="view" id="view-history" role="tabpanel" aria-labelledby="tab-history" tabindex="-1" hidden>
|
||||
<div class="toolbar" role="toolbar" aria-label="Nástroje histórie">
|
||||
<div class="seg" role="tablist" aria-label="Typ záznamov">
|
||||
<button class="seg-btn is-active" data-hist="docs" role="tab" aria-selected="true">Dokumenty</button>
|
||||
<button class="seg-btn" data-hist="reqs" role="tab" aria-selected="false">Požiadavky</button>
|
||||
</div>
|
||||
<span class="sep" role="separator"></span>
|
||||
<button class="btn btn-danger" id="hist-clear">Vymazať celú históriu</button>
|
||||
</div>
|
||||
|
||||
<div class="panel-body scroll" id="hist-body">
|
||||
<p class="muted">Zatiaľ bez záznamov.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- ======================= SHORTCUTS MODAL ======================= -->
|
||||
<div class="modal-backdrop" id="shortcuts-modal" hidden>
|
||||
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="sc-title">
|
||||
<header class="panel-head">
|
||||
<h2 class="panel-title" id="sc-title">Klávesové skratky</h2>
|
||||
<button class="icon-btn" id="sc-close" aria-label="Zavrieť">×</button>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<dl class="shortcuts">
|
||||
<dt><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>F</kbd></dt><dd>Pretty print JSON</dd>
|
||||
<dt><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>M</kbd></dt><dd>Minify JSON</dd>
|
||||
<dt><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>C</kbd></dt><dd>Kopírovať výstup JSON</dd>
|
||||
<dt><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>O</kbd></dt><dd>Otvoriť JSON súbor</dd>
|
||||
<dt><kbd>Ctrl</kbd>+<kbd>S</kbd></dt><dd>Stiahnuť JSON súbor</dd>
|
||||
<dt><kbd>Ctrl</kbd>+<kbd>Enter</kbd></dt><dd>Odoslať API požiadavku</dd>
|
||||
<dt><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>D</kbd></dt><dd>Prepnuť tmavý / svetlý motív</dd>
|
||||
<dt><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>K</kbd></dt><dd>Focus na vyhľadávanie v strome</dd>
|
||||
<dt><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>X</kbd></dt><dd>Vyčistiť aktuálny editor</dd>
|
||||
<dt><kbd>1</kbd> … <kbd>5</kbd></dt><dd>Prepnutie nástroja (mimo textových polí)</dd>
|
||||
<dt><kbd>?</kbd></dt><dd>Zobraziť tento zoznam</dd>
|
||||
<dt><kbd>Esc</kbd></dt><dd>Zavrieť okno / zrušiť vyhľadávanie</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toasts" class="toasts" aria-live="polite" aria-atomic="false"></div>
|
||||
|
||||
<div id="drag-overlay" class="drag-overlay" hidden>
|
||||
<div class="drag-card">Pustite JSON súbor naimportovanie</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+984
@@ -0,0 +1,984 @@
|
||||
/* =====================================================================
|
||||
JSON DevTools - stylesheet
|
||||
Dark / Light themes via CSS custom properties on [data-theme]
|
||||
===================================================================== */
|
||||
|
||||
:root {
|
||||
--font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
|
||||
--radius: 8px;
|
||||
--radius-sm: 5px;
|
||||
--header-h: 52px;
|
||||
--dur: 140ms;
|
||||
}
|
||||
|
||||
/* ------------------------------ DARK ------------------------------ */
|
||||
html[data-theme="dark"] {
|
||||
--bg: #0e1116;
|
||||
--bg-2: #141922;
|
||||
--bg-3: #1a212c;
|
||||
--bg-elev: #1c2430;
|
||||
--panel: #121821;
|
||||
--panel-head: #161d27;
|
||||
--border: #232c3a;
|
||||
--border-strong: #33405400;
|
||||
--text: #dfe6f1;
|
||||
--text-dim: #9aa8bd;
|
||||
--text-faint: #6b7a91;
|
||||
--accent: #4c9aff;
|
||||
--accent-hover: #6aacff;
|
||||
--accent-soft: rgba(76, 154, 255, 0.14);
|
||||
--green: #4ec98a;
|
||||
--green-soft: rgba(78, 201, 138, 0.13);
|
||||
--red: #f2777a;
|
||||
--red-soft: rgba(242, 119, 122, 0.13);
|
||||
--orange: #e5a54b;
|
||||
--orange-soft: rgba(229, 165, 75, 0.14);
|
||||
--purple: #c08cf0;
|
||||
--purple-soft: rgba(192, 140, 240, 0.14);
|
||||
--cyan: #56c9d8;
|
||||
--shadow: 0 8px 28px rgba(0, 0, 0, 0.5);
|
||||
--sel: rgba(76, 154, 255, 0.28);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* ------------------------------ LIGHT ----------------------------- */
|
||||
html[data-theme="light"] {
|
||||
--bg: #f2f4f8;
|
||||
--bg-2: #ffffff;
|
||||
--bg-3: #eceff5;
|
||||
--bg-elev: #ffffff;
|
||||
--panel: #ffffff;
|
||||
--panel-head: #f7f9fc;
|
||||
--border: #dbe1ea;
|
||||
--text: #1d2735;
|
||||
--text-dim: #5a6779;
|
||||
--text-faint: #8a95a6;
|
||||
--accent: #1a6fe0;
|
||||
--accent-hover: #0d5cc7;
|
||||
--accent-soft: rgba(26, 111, 224, 0.1);
|
||||
--green: #12855a;
|
||||
--green-soft: rgba(18, 133, 90, 0.1);
|
||||
--red: #c9373d;
|
||||
--red-soft: rgba(201, 55, 61, 0.1);
|
||||
--orange: #a86a12;
|
||||
--orange-soft: rgba(168, 106, 18, 0.12);
|
||||
--purple: #7a41bd;
|
||||
--purple-soft: rgba(122, 65, 189, 0.1);
|
||||
--cyan: #0f7c8c;
|
||||
--shadow: 0 6px 22px rgba(24, 39, 62, 0.12);
|
||||
--sel: rgba(26, 111, 224, 0.2);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
/* ------------------------------ BASE ------------------------------ */
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font-ui);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
::selection { background: var(--sel); }
|
||||
|
||||
button, input, select, textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
top: 0;
|
||||
z-index: 200;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
padding: 10px 16px;
|
||||
border-radius: 0 0 var(--radius) 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
.skip-link:focus { left: 0; }
|
||||
|
||||
/* ------------------------------ HEADER ---------------------------- */
|
||||
.app-header {
|
||||
height: var(--header-h);
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 0 14px;
|
||||
background: var(--bg-2);
|
||||
border-bottom: 1px solid var(--border);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
font-weight: 650;
|
||||
letter-spacing: -0.01em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.brand .logo {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
border: 1px solid var(--accent-soft);
|
||||
padding: 2px 7px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 13px;
|
||||
}
|
||||
.brand-name { font-size: 15px; }
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.tabs::-webkit-scrollbar { display: none; }
|
||||
|
||||
.tab {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-dim);
|
||||
padding: 7px 14px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
transition: background var(--dur), color var(--dur);
|
||||
}
|
||||
.tab:hover { background: var(--bg-3); color: var(--text); }
|
||||
.tab[aria-selected="true"] {
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
border-color: var(--accent-soft);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-dim);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background var(--dur), color var(--dur);
|
||||
}
|
||||
.icon-btn:hover { background: var(--bg-3); color: var(--text); }
|
||||
.icon-btn .icon { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.7; }
|
||||
|
||||
html[data-theme="dark"] .icon-sun { display: none; }
|
||||
html[data-theme="light"] .icon-moon { display: none; }
|
||||
|
||||
/* ------------------------------ MAIN ------------------------------ */
|
||||
.main {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.view {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
.view[hidden] { display: none; }
|
||||
|
||||
/* ------------------------------ TOOLBAR --------------------------- */
|
||||
.toolbar {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
padding: 6px 8px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: var(--bg-3);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text);
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
transition: background var(--dur), border-color var(--dur), transform var(--dur);
|
||||
}
|
||||
.btn:hover { background: var(--bg-elev); border-color: var(--accent); }
|
||||
.btn:active { transform: translateY(1px); }
|
||||
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
||||
|
||||
.btn-ghost { background: transparent; }
|
||||
.btn-ghost:hover { background: var(--bg-3); }
|
||||
|
||||
.btn-danger { background: transparent; border-color: var(--red); color: var(--red); }
|
||||
.btn-danger:hover { background: var(--red-soft); border-color: var(--red); }
|
||||
|
||||
.btn-sm { padding: 4px 9px; font-size: 12.5px; }
|
||||
|
||||
.sep {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: var(--border);
|
||||
margin: 0 4px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.panel-meta {
|
||||
font-size: 12px;
|
||||
color: var(--text-faint);
|
||||
font-family: var(--font-mono);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.muted { color: var(--text-dim); }
|
||||
|
||||
/* ------------------------------ INPUTS ---------------------------- */
|
||||
.input {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 6px 9px;
|
||||
color: var(--text);
|
||||
min-width: 0;
|
||||
transition: border-color var(--dur);
|
||||
}
|
||||
.input:focus { border-color: var(--accent); outline: none; box-shadow: 0 0 0 3px var(--accent-soft); }
|
||||
.input.mono { font-family: var(--font-mono); }
|
||||
|
||||
select.input { cursor: pointer; }
|
||||
|
||||
.search-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 2px 8px;
|
||||
min-width: 240px;
|
||||
flex: 0 1 340px;
|
||||
}
|
||||
.search-wrap:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
||||
.search-input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 5px 0;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.search-input:focus { box-shadow: none; }
|
||||
.search-icon { width: 15px; height: 15px; fill: none; stroke: var(--text-faint); stroke-width: 1.8; flex: 0 0 auto; }
|
||||
|
||||
/* ------------------------------ PANELS ---------------------------- */
|
||||
.split-pane {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
gap: 0;
|
||||
align-items: stretch;
|
||||
--split: 50%;
|
||||
}
|
||||
|
||||
.split-pane > .panel:first-child { flex: 0 0 var(--split); }
|
||||
.split-pane > .panel:last-child { flex: 1 1 0; }
|
||||
|
||||
#req-split { --split: 38%; }
|
||||
|
||||
.panel {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex: 1 1 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 7px 11px;
|
||||
background: var(--panel-head);
|
||||
border-bottom: 1px solid var(--border);
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.panel-body.no-pad { padding: 0; overflow: hidden; }
|
||||
.panel-body.scroll { overflow: auto; }
|
||||
|
||||
/* ------------------------------ CODE AREA -------------------------- */
|
||||
.code-area {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
resize: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
padding: 12px 14px;
|
||||
tab-size: 2;
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
}
|
||||
.code-area::placeholder { color: var(--text-faint); }
|
||||
.code-area:focus { background: color-mix(in srgb, var(--accent) 3%, transparent); }
|
||||
|
||||
.code-pre {
|
||||
margin: 0;
|
||||
display: block;
|
||||
overflow: auto;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.code-body {
|
||||
height: 170px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg);
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ------------------------------ SPLITTER -------------------------- */
|
||||
.splitter {
|
||||
flex: 0 0 6px;
|
||||
cursor: col-resize;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
transition: background var(--dur);
|
||||
}
|
||||
.splitter::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0 2px;
|
||||
background: var(--border);
|
||||
transition: background var(--dur);
|
||||
}
|
||||
.splitter:hover::after, .splitter:focus-visible::after, .splitter.dragging::after {
|
||||
background: var(--accent);
|
||||
}
|
||||
body.resizing { cursor: col-resize; user-select: none; }
|
||||
body.resizing * { cursor: col-resize !important; }
|
||||
|
||||
/* ------------------------------ ERROR BOX ------------------------- */
|
||||
.error-box {
|
||||
flex: 0 0 auto;
|
||||
max-height: 40%;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
padding: 9px 12px;
|
||||
background: var(--red-soft);
|
||||
border-top: 1px solid var(--red);
|
||||
color: var(--red);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12.5px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.error-box[hidden] { display: none; }
|
||||
.error-box .err-title { font-weight: 700; font-family: var(--font-ui); display: block; margin-bottom: 3px; }
|
||||
.error-box .err-pos { color: var(--text-dim); }
|
||||
|
||||
/* ------------------------------ SYNTAX HIGHLIGHT ------------------ */
|
||||
.tok-key { color: var(--accent); }
|
||||
.tok-str { color: var(--green); }
|
||||
.tok-num { color: var(--purple); }
|
||||
.tok-bool { color: var(--orange); }
|
||||
.tok-null { color: var(--orange); font-style: italic; }
|
||||
.tok-punc { color: var(--text-faint); }
|
||||
html[data-theme="light"] .tok-bool { color: var(--orange); }
|
||||
|
||||
mark.hl {
|
||||
background: var(--orange-soft);
|
||||
color: var(--orange);
|
||||
border-radius: 2px;
|
||||
padding: 0 1px;
|
||||
}
|
||||
|
||||
/* ------------------------------ TREE ------------------------------ */
|
||||
.tree {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.tnode-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 3px;
|
||||
padding: 1px 4px 1px 0;
|
||||
border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tnode-row:hover { background: var(--bg-3); }
|
||||
.tnode-row.focused { outline: 1px solid var(--accent); outline-offset: -1px; }
|
||||
|
||||
.twisty {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
width: 16px;
|
||||
min-width: 16px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
transition: transform var(--dur);
|
||||
}
|
||||
.twisty.open { transform: rotate(90deg); }
|
||||
.twisty:hover { color: var(--accent); }
|
||||
.twisty.leaf { visibility: hidden; }
|
||||
|
||||
.tkey { color: var(--accent); }
|
||||
.tsep { color: var(--text-faint); }
|
||||
.tval.str { color: var(--green); }
|
||||
.tval.num { color: var(--purple); }
|
||||
.tval.bool { color: var(--orange); }
|
||||
.tval.null { color: var(--orange); font-style: italic; }
|
||||
.tcount {
|
||||
color: var(--text-faint);
|
||||
font-size: 11.5px;
|
||||
background: var(--bg-3);
|
||||
border-radius: 20px;
|
||||
padding: 0 6px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.ttype {
|
||||
color: var(--text-faint);
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
}
|
||||
.tchildren { margin-left: 15px; border-left: 1px dashed var(--border); padding-left: 7px; }
|
||||
.tchildren[hidden] { display: none; }
|
||||
|
||||
.tact {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
color: var(--text-faint);
|
||||
font-size: 10.5px;
|
||||
padding: 0 4px;
|
||||
cursor: pointer;
|
||||
margin-left: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity var(--dur), color var(--dur), border-color var(--dur);
|
||||
}
|
||||
.tnode-row:hover .tact, .tact:focus-visible { opacity: 1; }
|
||||
.tact:hover { color: var(--accent); border-color: var(--accent); }
|
||||
.tact.copied { opacity: 1; color: var(--green); border-color: var(--green); }
|
||||
|
||||
.tree-empty { color: var(--text-faint); font-style: italic; padding: 10px; }
|
||||
|
||||
/* ------------------------------ DIFF ------------------------------ */
|
||||
.results {
|
||||
flex: 0 0 auto;
|
||||
max-height: 45%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.legend { display: flex; gap: 6px; }
|
||||
|
||||
.diff-row {
|
||||
display: grid;
|
||||
grid-template-columns: 86px minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
padding: 7px 9px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12.5px;
|
||||
align-items: start;
|
||||
}
|
||||
.diff-row:last-child { border-bottom: none; }
|
||||
.diff-row:hover { background: var(--bg-3); }
|
||||
|
||||
.diff-badge {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
text-align: center;
|
||||
}
|
||||
.badge-added { background: var(--green-soft); color: var(--green); }
|
||||
.badge-removed { background: var(--red-soft); color: var(--red); }
|
||||
.badge-changed { background: var(--orange-soft); color: var(--orange); }
|
||||
|
||||
.diff-path {
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
word-break: break-all;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.diff-path:hover { text-decoration: underline; }
|
||||
|
||||
.diff-val {
|
||||
display: block;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
border-radius: 3px;
|
||||
padding: 1px 5px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.diff-old { background: var(--red-soft); color: var(--red); }
|
||||
.diff-new { background: var(--green-soft); color: var(--green); }
|
||||
.diff-typechange { color: var(--orange); font-style: italic; }
|
||||
|
||||
.diff-arrow { color: var(--text-faint); margin: 0 4px; }
|
||||
|
||||
/* ------------------------------ REQUEST --------------------------- */
|
||||
.horizontal-request { flex-direction: row; }
|
||||
.req-form { max-width: 760px; }
|
||||
.req-response { flex: 1 1 0; }
|
||||
|
||||
.field { margin-bottom: 12px; }
|
||||
.field > label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
||||
.row > .input { flex: 1 1 auto; }
|
||||
.hint { font-size: 11.5px; color: var(--text-faint); margin: 4px 0 0; }
|
||||
|
||||
.group {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
margin-bottom: 12px;
|
||||
background: var(--bg-2);
|
||||
}
|
||||
.group > summary {
|
||||
cursor: pointer;
|
||||
padding: 7px 10px;
|
||||
font-weight: 600;
|
||||
font-size: 12.5px;
|
||||
color: var(--text-dim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
}
|
||||
.group > summary::-webkit-details-marker { display: none; }
|
||||
.group > summary::before { content: "\25B8"; margin-right: 7px; color: var(--accent); display: inline-block; }
|
||||
.group[open] > summary::before { content: "\25BE"; }
|
||||
.group > summary:hover { color: var(--text); }
|
||||
.group > *:not(summary) { margin: 0 10px 10px; }
|
||||
.group > .row:last-child { margin-bottom: 10px; }
|
||||
|
||||
.kv-list { display: flex; flex-direction: column; gap: 5px; margin-top: 6px; }
|
||||
.kv-row { display: flex; gap: 5px; align-items: center; }
|
||||
.kv-row .input { flex: 1 1 0; padding: 4px 7px; font-size: 12.5px; }
|
||||
.kv-row .input.k { flex: 1 1 35%; }
|
||||
.kv-row .input.v { flex: 1 1 60%; font-family: var(--font-mono); }
|
||||
.kv-del {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
width: 26px;
|
||||
height: 28px;
|
||||
flex: 0 0 auto;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
}
|
||||
.kv-del:hover { color: var(--red); border-color: var(--red); background: var(--red-soft); }
|
||||
|
||||
.plain-list { margin: 0; padding-left: 18px; color: var(--text-dim); font-size: 12.5px; }
|
||||
.plain-list li { margin-bottom: 4px; }
|
||||
code {
|
||||
font-family: var(--font-mono);
|
||||
background: var(--bg-3);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* response */
|
||||
.resp-status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
padding: 9px 11px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-3);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.resp-code {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 700;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.resp-code.ok { background: var(--green-soft); color: var(--green); }
|
||||
.resp-code.redirect { background: var(--accent-soft); color: var(--accent); }
|
||||
.resp-code.client { background: var(--orange-soft); color: var(--orange); }
|
||||
.resp-code.server { background: var(--red-soft); color: var(--red); }
|
||||
.resp-code.err { background: var(--red-soft); color: var(--red); }
|
||||
.resp-stat { color: var(--text-dim); font-size: 12.5px; font-family: var(--font-mono); }
|
||||
|
||||
.resp-tabs { display: flex; gap: 4px; margin-bottom: 8px; border-bottom: 1px solid var(--border); }
|
||||
.resp-tab {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--text-dim);
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.resp-tab[aria-selected="true"] { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
|
||||
|
||||
.resp-headers { font-family: var(--font-mono); font-size: 12.5px; }
|
||||
.resp-headers tr:nth-child(odd) { background: var(--bg-3); }
|
||||
.resp-headers td { padding: 4px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
|
||||
.resp-headers td:first-child { color: var(--accent); white-space: nowrap; width: 1%; }
|
||||
|
||||
.cors-box {
|
||||
border: 1px solid var(--orange);
|
||||
background: var(--orange-soft);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.cors-box h3 { margin: 0 0 6px; font-size: 13.5px; color: var(--orange); }
|
||||
.cors-box ul { margin: 6px 0 0; padding-left: 20px; color: var(--text-dim); font-size: 12.5px; }
|
||||
.cors-box li { margin-bottom: 4px; }
|
||||
.cors-box code { background: rgba(0,0,0,0.15); }
|
||||
|
||||
.err-box-generic {
|
||||
border: 1px solid var(--red);
|
||||
background: var(--red-soft);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 11px 13px;
|
||||
color: var(--red);
|
||||
}
|
||||
.err-box-generic strong { display: block; margin-bottom: 4px; }
|
||||
|
||||
/* ------------------------------ HISTORY --------------------------- */
|
||||
.seg {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
.seg-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-dim);
|
||||
padding: 6px 14px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
.seg-btn.is-active { background: var(--accent); color: #fff; font-weight: 600; }
|
||||
|
||||
.hist-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.hist-item {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-2);
|
||||
padding: 9px 11px;
|
||||
}
|
||||
.hist-item-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.hist-title {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 600;
|
||||
font-size: 12.5px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.hist-time { font-size: 11.5px; color: var(--text-faint); white-space: nowrap; }
|
||||
.hist-preview {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11.5px;
|
||||
color: var(--text-dim);
|
||||
background: var(--bg);
|
||||
border-radius: 4px;
|
||||
padding: 5px 7px;
|
||||
max-height: 92px;
|
||||
overflow: auto;
|
||||
white-space: pre;
|
||||
margin: 4px 0 6px;
|
||||
}
|
||||
.hist-actions { display: flex; gap: 5px; flex-wrap: wrap; }
|
||||
.hist-method {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
}
|
||||
.hist-method.GET { background: var(--green-soft); color: var(--green); }
|
||||
.hist-method.POST { background: var(--accent-soft); color: var(--accent); }
|
||||
.hist-method.PUT, .hist-method.PATCH { background: var(--orange-soft); color: var(--orange); }
|
||||
.hist-method.DELETE { background: var(--red-soft); color: var(--red); }
|
||||
|
||||
/* ------------------------------ TOASTS ---------------------------- */
|
||||
.toasts {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
z-index: 120;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-width: min(380px, calc(100vw - 32px));
|
||||
}
|
||||
.toast {
|
||||
background: var(--bg-elev);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 3px solid var(--accent);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 9px 12px;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
animation: toast-in 180ms ease-out;
|
||||
}
|
||||
.toast.success { border-left-color: var(--green); }
|
||||
.toast.error { border-left-color: var(--red); }
|
||||
.toast.warn { border-left-color: var(--orange); }
|
||||
.toast.leaving { animation: toast-out 160ms ease-in forwards; }
|
||||
.toast-msg { flex: 1 1 auto; word-break: break-word; }
|
||||
.toast-msg b { display: block; font-weight: 650; }
|
||||
.toast-msg span { color: var(--text-dim); font-size: 12px; }
|
||||
.toast-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
padding: 0 2px;
|
||||
}
|
||||
.toast-close:hover { color: var(--text); }
|
||||
|
||||
@keyframes toast-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
|
||||
@keyframes toast-out { to { opacity: 0; transform: translateX(20px); } }
|
||||
|
||||
/* ------------------------------ MODAL ----------------------------- */
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(4, 8, 14, 0.6);
|
||||
backdrop-filter: blur(2px);
|
||||
z-index: 150;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.modal-backdrop[hidden] { display: none; }
|
||||
.modal {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
width: min(560px, 100%);
|
||||
max-height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.modal-body { padding: 14px 16px; overflow: auto; }
|
||||
.shortcuts {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 7px 16px;
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
}
|
||||
.shortcuts dt { text-align: right; }
|
||||
.shortcuts dd { margin: 0; color: var(--text-dim); }
|
||||
kbd {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11.5px;
|
||||
background: var(--bg-3);
|
||||
border: 1px solid var(--border);
|
||||
border-bottom-width: 2px;
|
||||
border-radius: 4px;
|
||||
padding: 1px 5px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ------------------------------ DRAG OVERLAY ---------------------- */
|
||||
.drag-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(4, 8, 14, 0.72);
|
||||
z-index: 140;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.drag-overlay[hidden] { display: none; }
|
||||
.drag-card {
|
||||
border: 2px dashed var(--accent);
|
||||
color: var(--accent);
|
||||
border-radius: var(--radius);
|
||||
padding: 28px 40px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
/* ------------------------------ SCROLLBARS ------------------------ */
|
||||
*::-webkit-scrollbar { width: 11px; height: 11px; }
|
||||
*::-webkit-scrollbar-track { background: transparent; }
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 6px;
|
||||
border: 3px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
*::-webkit-scrollbar-thumb:hover { background: var(--text-faint); background-clip: content-box; }
|
||||
* { scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
|
||||
|
||||
/* ------------------------------ RESPONSIVE ------------------------ */
|
||||
@media (max-width: 1000px) {
|
||||
.req-form { flex: 0 0 380px; }
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
body { overflow: auto; }
|
||||
.app-header {
|
||||
height: auto;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
.tabs { order: 3; flex-basis: 100%; }
|
||||
.brand-name { display: none; }
|
||||
.split-pane { flex-direction: column; }
|
||||
.split-pane > .panel:first-child,
|
||||
.split-pane > .panel:last-child {
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
min-height: 240px;
|
||||
height: 38vh;
|
||||
max-height: none;
|
||||
}
|
||||
.splitter { flex: 0 0 8px; cursor: row-resize; }
|
||||
.splitter::after { inset: 2px 0; }
|
||||
.results { max-height: none; height: 45vh; }
|
||||
.req-form { flex: 0 0 auto; max-width: none; }
|
||||
.horizontal-request { flex-direction: column; }
|
||||
.view { padding: 8px; gap: 8px; }
|
||||
.search-wrap { flex: 1 1 100%; }
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.diff-row { grid-template-columns: 1fr; gap: 4px; }
|
||||
.diff-badge { justify-self: start; }
|
||||
.toolbar { gap: 4px; }
|
||||
.btn { padding: 6px 9px; font-size: 12.5px; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
/* =====================================================================
|
||||
JSON DevTools - logicke testy (bez prehliadaca, bez zavislosti)
|
||||
Spustenie: node tests/logic.test.js
|
||||
===================================================================== */
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
/* ---------------------- minimalny DOM stub ---------------------- */
|
||||
function makeElement(tag) {
|
||||
const el = {
|
||||
tagName: (tag || 'div').toUpperCase(),
|
||||
children: [],
|
||||
attributes: {},
|
||||
dataset: {},
|
||||
style: {
|
||||
_props: {},
|
||||
setProperty(k, v) { this._props[k] = v; },
|
||||
getPropertyValue(k) { return this._props[k] || ''; }
|
||||
},
|
||||
classList: {
|
||||
_set: new Set(),
|
||||
add(...c) { c.forEach((x) => this._set.add(x)); },
|
||||
remove(...c) { c.forEach((x) => this._set.delete(x)); },
|
||||
contains(c) { return this._set.has(c); },
|
||||
toggle(c, f) { if (f === undefined) { this._set.has(c) ? this._set.delete(c) : this._set.add(c); } else if (f) { this._set.add(c); } else { this._set.delete(c); } }
|
||||
},
|
||||
value: '',
|
||||
textContent: '',
|
||||
innerHTML: '',
|
||||
hidden: false,
|
||||
title: '',
|
||||
type: '',
|
||||
tabIndex: 0,
|
||||
files: [],
|
||||
checked: false,
|
||||
addEventListener() {}, removeEventListener() {},
|
||||
setAttribute(k, v) { this.attributes[k] = v; },
|
||||
getAttribute(k) { return this.attributes[k]; },
|
||||
removeAttribute(k) { delete this.attributes[k]; },
|
||||
appendChild(c) { this.children.push(c); return c; },
|
||||
removeChild(c) { const i = this.children.indexOf(c); if (i >= 0) this.children.splice(i, 1); return c; },
|
||||
insertBefore(c) { this.children.unshift(c); return c; },
|
||||
querySelector() { return makeElement('div'); },
|
||||
querySelectorAll() { return []; },
|
||||
closest() { return null; },
|
||||
focus() {}, blur() {}, click() {},
|
||||
getBoundingClientRect() { return { left: 0, top: 0, width: 800, height: 600, right: 800, bottom: 600 }; },
|
||||
dispatchEvent() { return true; }
|
||||
};
|
||||
return el;
|
||||
}
|
||||
|
||||
function createSandbox() {
|
||||
const store = Object.create(null);
|
||||
const localStorage = {
|
||||
getItem(k) { return Object.prototype.hasOwnProperty.call(store, k) ? store[k] : null; },
|
||||
setItem(k, v) { store[k] = String(v); },
|
||||
removeItem(k) { delete store[k]; },
|
||||
clear() { for (const k in store) delete store[k]; }
|
||||
};
|
||||
|
||||
const documentStub = {
|
||||
readyState: 'complete',
|
||||
documentElement: makeElement('html'),
|
||||
body: makeElement('body'),
|
||||
createElement: (t) => makeElement(t),
|
||||
querySelector: () => makeElement('div'),
|
||||
querySelectorAll: () => [],
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
execCommand() { return true; },
|
||||
createTextNode: (t) => ({ text: t })
|
||||
};
|
||||
|
||||
const windowStub = {
|
||||
document: documentStub,
|
||||
localStorage,
|
||||
matchMedia: () => ({ matches: false, addEventListener() {} }),
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
isSecureContext: false,
|
||||
location: { hash: '', search: '', href: 'file:///index.html' },
|
||||
history: { replaceState() {} },
|
||||
getComputedStyle: () => ({ getPropertyValue: () => '' }),
|
||||
navigator: { clipboard: null },
|
||||
performance: { now: () => Date.now() },
|
||||
setTimeout, clearTimeout, setInterval, clearInterval,
|
||||
Blob: globalThis.Blob,
|
||||
URL: { createObjectURL: () => 'blob:stub', revokeObjectURL() {} },
|
||||
console
|
||||
};
|
||||
windowStub.window = windowStub;
|
||||
windowStub.globalThis = windowStub;
|
||||
|
||||
const sandbox = {
|
||||
window: windowStub,
|
||||
document: documentStub,
|
||||
localStorage,
|
||||
navigator: windowStub.navigator,
|
||||
location: windowStub.location,
|
||||
history: windowStub.history,
|
||||
performance: windowStub.performance,
|
||||
matchMedia: windowStub.matchMedia,
|
||||
console,
|
||||
setTimeout, clearTimeout, setInterval, clearInterval,
|
||||
Blob: globalThis.Blob,
|
||||
URL: windowStub.URL,
|
||||
Math, JSON, Date, Array, Object, String, Number, Boolean, Error, TypeError,
|
||||
RegExp, Promise, Map, Set, isNaN, parseInt, parseFloat, encodeURIComponent,
|
||||
decodeURIComponent, btoa: (s) => Buffer.from(s, 'binary').toString('base64'),
|
||||
atob: (s) => Buffer.from(s, 'base64').toString('binary')
|
||||
};
|
||||
sandbox.globalThis = sandbox;
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
/* ---------------------- nacitanie aplikacie ---------------------- */
|
||||
const appPath = path.join(__dirname, '..', 'src', 'app.js');
|
||||
const source = fs.readFileSync(appPath, 'utf8');
|
||||
const sandbox = createSandbox();
|
||||
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(source, sandbox, { filename: 'app.js' });
|
||||
|
||||
const D = sandbox.window.JSONDevTools;
|
||||
if (!D) {
|
||||
console.error('FAIL: aplikacia sa nenačítala (window.JSONDevTools chýba)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/* ---------------------- testovacie makra ---------------------- */
|
||||
let passed = 0, failed = 0;
|
||||
const failures = [];
|
||||
|
||||
function ok(name, cond, extra) {
|
||||
if (cond) { passed++; }
|
||||
else { failed++; failures.push(name + (extra !== undefined ? ' >> ' + extra : '')); }
|
||||
}
|
||||
function eq(name, a, b) { ok(name, a === b, 'got ' + JSON.stringify(a) + ' expected ' + JSON.stringify(b)); }
|
||||
function deepEq(name, a, b) { ok(name, JSON.stringify(a) === JSON.stringify(b), 'got ' + JSON.stringify(a) + ' expected ' + JSON.stringify(b)); }
|
||||
function section(t) { console.log('\n--- ' + t + ' ---'); }
|
||||
|
||||
/* ======================= TESTY ======================= */
|
||||
|
||||
section('Utils');
|
||||
eq('typeOf null', D.Utils.typeOf(null), 'null');
|
||||
eq('typeOf array', D.Utils.typeOf([]), 'array');
|
||||
eq('typeOf object', D.Utils.typeOf({}), 'object');
|
||||
eq('typeOf number', D.Utils.typeOf(1), 'number');
|
||||
eq('typeOf string', D.Utils.typeOf('a'), 'string');
|
||||
eq('typeOf undefined', D.Utils.typeOf(undefined), 'undefined');
|
||||
eq('isPlainObject', D.Utils.isPlainObject({}), true);
|
||||
eq('isPlainObject array', D.Utils.isPlainObject([]), false);
|
||||
eq('pathSegment simple', D.Utils.pathSegment('abc'), '.abc');
|
||||
eq('pathSegment underscore', D.Utils.pathSegment('_a1'), '._a1');
|
||||
eq('pathSegment digits start', D.Utils.pathSegment('1a'), "['1a']");
|
||||
eq('pathSegment space', D.Utils.pathSegment('a b'), "['a b']");
|
||||
eq('pathSegment quote', D.Utils.pathSegment("it's"), "['it\\'s']");
|
||||
eq('formatBytes B', D.Utils.formatBytes(12), '12 B');
|
||||
eq('formatBytes KB', D.Utils.formatBytes(2048), '2.0 KB');
|
||||
eq('formatBytes MB', D.Utils.formatBytes(3 * 1024 * 1024), '3.00 MB');
|
||||
eq('formatDuration ms', D.Utils.formatDuration(12.4), '12 ms');
|
||||
eq('formatDuration sub-ms', D.Utils.formatDuration(0.5), '0.50 ms');
|
||||
eq('escapeHtml', D.Utils.escapeHtml('<b>&"\''), '<b>&"'');
|
||||
ok('uid uniq', D.Utils.uid() !== D.Utils.uid());
|
||||
ok('formatTime format', /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(D.Utils.formatTime(Date.now())));
|
||||
|
||||
section('localStorage helper');
|
||||
sandbox.localStorage.setItem('t.probe', '"v"');
|
||||
eq('store.get', D.Utils.store.get('t.probe', null), 'v');
|
||||
eq('store.get fallback', D.Utils.store.get('t.missing', 'fb'), 'fb');
|
||||
D.Utils.store.set('t.probe2', { a: 1 });
|
||||
deepEq('store.set/get object', D.Utils.store.get('t.probe2', null), { a: 1 });
|
||||
D.Utils.store.remove('t.probe2');
|
||||
eq('store.remove', D.Utils.store.get('t.probe2', null), null);
|
||||
|
||||
section('Highlighter');
|
||||
const hl = D.Highlighter.highlight('{"a": 1, "b": "x", "c": true, "d": false, "e": null, "f": -1.5e3}');
|
||||
ok('key token', hl.includes('tok-key'));
|
||||
ok('string token', hl.includes('tok-str'));
|
||||
ok('number token', hl.includes('tok-num'));
|
||||
ok('bool token', hl.includes('tok-bool'));
|
||||
ok('null token', hl.includes('tok-null'));
|
||||
ok('punc token', hl.includes('tok-punc'));
|
||||
ok('html escaped', D.Highlighter.highlight('<script>x</script>').includes('<script>'));
|
||||
ok('no raw script tag', !D.Highlighter.highlight('</script>').includes('</script>'));
|
||||
ok('highlightMatch wraps', D.Highlighter.highlightMatch('abcdef', 'cd').includes('<mark class="hl">cd</mark>'));
|
||||
ok('highlightMatch case-insensitive', D.Highlighter.highlightMatch('ABCDEF', 'cd').includes('<mark class="hl">CD</mark>'));
|
||||
ok('highlightMatch no match', !D.Highlighter.highlightMatch('abc', 'zz').includes('mark'));
|
||||
|
||||
section('Diff algoritmus');
|
||||
{
|
||||
const out = [];
|
||||
D.Diff.walk({ a: 1 }, { a: 1 }, '$', out);
|
||||
eq('zhodne -> ziadny rozdiel', out.length, 0);
|
||||
|
||||
const out2 = [];
|
||||
D.Diff.walk({ a: 1 }, { a: 2 }, '$', out2);
|
||||
eq('zmenena hodnota', out2.length, 1);
|
||||
eq('typ zmeny', out2[0].type, 'changed');
|
||||
eq('cesta zmeny', out2[0].path, '$.a');
|
||||
eq('stara hodnota', out2[0].old, 1);
|
||||
eq('nova hodnota', out2[0].new, 2);
|
||||
|
||||
const out3 = [];
|
||||
D.Diff.walk({ a: 1 }, { a: 1, b: 2 }, '$', out3);
|
||||
eq('pridany kluc', out3.length, 1);
|
||||
eq('typ pridany', out3[0].type, 'added');
|
||||
eq('cesta pridaneho', out3[0].path, '$.b');
|
||||
|
||||
const out4 = [];
|
||||
D.Diff.walk({ a: 1, b: 2 }, { a: 1 }, '$', out4);
|
||||
eq('odstraneny kluc', out4[0].type, 'removed');
|
||||
eq('cesta odstraneneho', out4[0].path, '$.b');
|
||||
|
||||
const out5 = [];
|
||||
D.Diff.walk({ a: [1, 2, 3] }, { a: [1, 2] }, '$', out5);
|
||||
eq('kratke pole -> odstranene', out5.length, 1);
|
||||
eq('cesta prvku pola', out5[0].path, '$.a[2]');
|
||||
|
||||
const out6 = [];
|
||||
D.Diff.walk({ a: [1] }, { a: [1, 9] }, '$', out6);
|
||||
eq('dlhsie pole -> pridane', out6[0].type, 'added');
|
||||
eq('cesta pridaneho prvku', out6[0].path, '$.a[1]');
|
||||
|
||||
const out7 = [];
|
||||
D.Diff.walk({ a: 1 }, { a: '1' }, '$', out7);
|
||||
eq('zmena typu', out7[0].type, 'changed');
|
||||
eq('stary typ', out7[0].oldType, 'number');
|
||||
eq('novy typ', out7[0].newType, 'string');
|
||||
|
||||
const out8 = [];
|
||||
D.Diff.walk({ a: { b: { c: 1 } } }, { a: { b: { c: 2 } } }, '$', out8);
|
||||
eq('vnorene porovnanie', out8[0].path, '$.a.b.c');
|
||||
|
||||
const out9 = [];
|
||||
D.Diff.walk({ 'weird key': 1 }, { 'weird key': 2 }, '$', out9);
|
||||
eq('kluc so specialnymi znakmi', out9[0].path, "$['weird key']");
|
||||
|
||||
const out10 = [];
|
||||
D.Diff.walk([1, 2], [1, 2, 3], '$', out10);
|
||||
eq('pole na urovni korena', out10[0].path, '$[2]');
|
||||
}
|
||||
|
||||
section('Historia – sanitizácia');
|
||||
{
|
||||
const saved = [];
|
||||
const realSet = sandbox.localStorage.setItem.bind(sandbox.localStorage);
|
||||
sandbox.localStorage.setItem = (k, v) => { saved.push([k, v]); return realSet(k, v); };
|
||||
|
||||
D.History.saveRequest({
|
||||
method: 'POST',
|
||||
url: 'https://api.example.com/v1/users',
|
||||
status: 201,
|
||||
statusText: 'Created',
|
||||
duration: 42,
|
||||
requestHeaders: { 'Content-Type': 'application/json', 'Authorization': 'Bearer SECRET123', 'Cookie': 'a=b', 'X-Api-Key': 'K' },
|
||||
requestBody: '{"a":1}',
|
||||
responsePreview: '{"ok":true}',
|
||||
isJson: true
|
||||
});
|
||||
|
||||
const raw = saved.filter(([k]) => k.indexOf('requests') !== -1).map(([, v]) => v).join('');
|
||||
ok('Authorization odstranena', raw.toLowerCase().indexOf('authorization') === -1, raw.slice(0, 200));
|
||||
ok('secret token nie je ulozeny', raw.indexOf('SECRET123') === -1);
|
||||
ok('cookie odstranena', raw.toLowerCase().indexOf('cookie') === -1);
|
||||
ok('x-api-key odstraneny', raw.toLowerCase().indexOf('x-api-key') === -1);
|
||||
ok('content-type zachovany', raw.indexOf('Content-Type') !== -1);
|
||||
ok('url zaznamany', raw.indexOf('api.example.com') !== -1);
|
||||
ok('status zaznamany', raw.indexOf('201') !== -1);
|
||||
|
||||
sandbox.localStorage.setItem = realSet;
|
||||
}
|
||||
|
||||
section('Historia – dokumenty');
|
||||
{
|
||||
sandbox.localStorage.removeItem('jsondevtools.history.docs');
|
||||
D.History.saveDocument('{"x":1}');
|
||||
D.History.saveDocument('{"x":1}');
|
||||
D.History.saveDocument('{"y":2}');
|
||||
const docs = JSON.parse(sandbox.localStorage.getItem('jsondevtools.history.docs'));
|
||||
eq('duplicitne sa nededuplikuju', docs.length, 2);
|
||||
eq('najnovsi je prvy', docs[0].text, '{"y":2}');
|
||||
|
||||
for (let i = 0; i < 30; i++) D.History.saveDocument('{"i":' + i + '}');
|
||||
const many = JSON.parse(sandbox.localStorage.getItem('jsondevtools.history.docs'));
|
||||
ok('historia obmedzena na 20', many.length <= 20, 'n=' + many.length);
|
||||
sandbox.localStorage.removeItem('jsondevtools.history.docs');
|
||||
}
|
||||
|
||||
section('Theme');
|
||||
{
|
||||
const before = D.Theme.current;
|
||||
D.Theme.toggle();
|
||||
ok('tema sa prepne', D.Theme.current !== before);
|
||||
eq('tema persistovana', JSON.parse(sandbox.localStorage.getItem('jsondevtools.theme')), D.Theme.current);
|
||||
D.Theme.toggle();
|
||||
eq('tema spat vratena', D.Theme.current, before);
|
||||
}
|
||||
|
||||
section('App – navigacia');
|
||||
{
|
||||
eq('readHash default', D.App.readHash(), 'formatter');
|
||||
D.App.go('diff');
|
||||
eq('go nastavi current', D.App.current, 'diff');
|
||||
D.App.go('neexistujuci');
|
||||
eq('neplatny view -> formatter', D.App.current, 'formatter');
|
||||
D.App.go('tree');
|
||||
eq('go tree', D.App.current, 'tree');
|
||||
}
|
||||
|
||||
section('Parser chyb');
|
||||
{
|
||||
// parseJSON je vnútorná funkcia – overujeme cez verejné API (Formatter.validate)
|
||||
const D2 = D;
|
||||
const before = D2.Formatter.input.value;
|
||||
D2.Formatter.input.value = '{"a":1,,}';
|
||||
const res = D2.Formatter.validate();
|
||||
eq('neplatny JSON -> chyba', res.ok, false);
|
||||
ok('chyba obsahuje poziciu', /position|position|Unexpected/i.test(res.error), res.error);
|
||||
D2.Formatter.input.value = before;
|
||||
D2.Formatter.validate();
|
||||
}
|
||||
|
||||
section('API – URL a hlavicky (bez siete)');
|
||||
{
|
||||
const el = (id, val) => { const e = makeElement('input'); e.value = val; return e; };
|
||||
const api = D.Api;
|
||||
|
||||
// buildUrl cez parametre
|
||||
api.params = [{ key: 'q', value: 'a b' }, { key: 'page', value: '2' }, { key: '', value: 'x' }];
|
||||
const savedUrl = api.buildUrl ? null : null;
|
||||
// buildUrl cita #req-url, ktory v stub-e nie je nastaveny -> vratime prazdny string
|
||||
ok('buildUrl existuje', typeof api.buildUrl === 'function');
|
||||
|
||||
api.headers = [{ key: 'Accept', value: 'application/json' }];
|
||||
api.params = [];
|
||||
ok('buildHeaders existuje', typeof api.buildHeaders === 'function');
|
||||
}
|
||||
|
||||
section('Cesta uzla – parent path');
|
||||
{
|
||||
eq('parent $', D.parentPathOf('$.a'), '$');
|
||||
eq('parent hnedom', D.parentPathOf('$.a.b'), '$.a');
|
||||
eq('parent prvku pola', D.parentPathOf('$.a[0]'), '$.a');
|
||||
eq('parent prvku pola 2', D.parentPathOf('$.a.b[12]'), '$.a.b');
|
||||
eq('parent kluc so specialmi znakmi', D.parentPathOf("$['a b'].c"), "$['a b']");
|
||||
eq('parent root je null', D.parentPathOf('$'), null);
|
||||
}
|
||||
|
||||
section('Kontrola konzistencie DOM id');
|
||||
{
|
||||
const html = fs.readFileSync(path.join(__dirname, '..', 'src', 'index.html'), 'utf8');
|
||||
const ids = Array.from(html.matchAll(/\bid="([^"]+)"/g)).map((m) => m[1]);
|
||||
const dupes = ids.filter((v, i) => ids.indexOf(v) !== i);
|
||||
eq('ziadne duplicitne id v HTML', dupes.length, 0);
|
||||
|
||||
const js = fs.readFileSync(path.join(__dirname, '..', 'src', 'app.js'), 'utf8');
|
||||
const used = Array.from(new Set(Array.from(js.matchAll(/\$\('#([a-zA-Z0-9_-]+)'\)/g)).map((m) => m[1])));
|
||||
const missing = used.filter((u) => ids.indexOf(u) === -1);
|
||||
eq('vsetky pouzite id existuju v HTML', missing.length, 0);
|
||||
if (missing.length) console.log(' chybne id: ' + missing.join(', '));
|
||||
|
||||
// duplicitne aria-controls
|
||||
const dupAria = (html.match(/aria-controls="view-formatter"/g) || []).length;
|
||||
eq('aria-controls bez duplicitov', dupAria, 1);
|
||||
|
||||
// kazde tlacidlo s id musi mat handlov v app.js
|
||||
// (tab-y sa obsluhuju delegovane cez triedu '.tab')
|
||||
const btnIds = Array.from(html.matchAll(/<button[^>]*\bid="([^"]+)"/g)).map((m) => m[1]);
|
||||
const btnMissing = btnIds.filter((b) => {
|
||||
if (b.indexOf('tab-') === 0) return js.indexOf("'.tab'") === -1;
|
||||
return js.indexOf("'#" + b + "'") === -1 && js.indexOf('"#' + b + '"') === -1;
|
||||
});
|
||||
eq('vsetky tlacidla s id su napojene v app.js', btnMissing.length, 0);
|
||||
if (btnMissing.length) console.log(' chybne handlery pre: ' + btnMissing.join(', '));
|
||||
|
||||
// kazdy view ma tab aj panel
|
||||
['formatter', 'tree', 'diff', 'request', 'history'].forEach((v) => {
|
||||
ok('view ' + v + ' ma tab aj panel',
|
||||
html.indexOf('id="tab-' + v + '"') !== -1 && html.indexOf('id="view-' + v + '"') !== -1);
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------------- vysledok ---------------------- */
|
||||
console.log('\n========================================');
|
||||
console.log('Vysledok: ' + passed + ' ok, ' + failed + ' neuspechov');
|
||||
if (failures.length) {
|
||||
console.log('\nNeuspechy:');
|
||||
failures.forEach((f) => console.log(' FAIL ' + f));
|
||||
}
|
||||
console.log('========================================');
|
||||
process.exit(failed ? 1 : 0);
|
||||
Reference in New Issue
Block a user