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