@opsydyn/elysia-spectral 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.1](https://github.com/opsydyn/elysia-spectral/compare/v1.3.0...v1.3.1) (2026-04-28)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **dashboard:** stop double-encoding inlined CSS and JS assets ([a4457bf](https://github.com/opsydyn/elysia-spectral/commit/a4457bf87fc30e5a0e68a580796085f39aa49dbe))
9
+
10
+ ## [1.3.0](https://github.com/opsydyn/elysia-spectral/compare/v1.2.0...v1.3.0) (2026-04-28)
11
+
12
+
13
+ ### Features
14
+
15
+ * **dashboard:** add theme switcher and extract dashboard assets ([08217c8](https://github.com/opsydyn/elysia-spectral/commit/08217c8e786220ca1f5b613ac18e2d266ee8d928))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **dashboard:** label theme switcher visibly ([fb74c82](https://github.com/opsydyn/elysia-spectral/commit/fb74c828d31063cb4d667578789811fcb1f35642))
21
+
3
22
  ## [1.2.0](https://github.com/opsydyn/elysia-spectral/compare/v1.1.1...v1.2.0) (2026-04-28)
4
23
 
5
24
 
package/README.md CHANGED
@@ -386,6 +386,12 @@ What it surfaces:
386
386
 
387
387
  Keyboard shortcuts: `r` re-runs, `/` focuses the filter, `Enter`/`Space` toggles the focused severity chip.
388
388
 
389
+ Three dark themes ship with the dashboard, selectable from the header dropdown and persisted per-browser via `localStorage`:
390
+
391
+ - **Astro Houston** (default) — purple / blue gradient
392
+ - **Tron Legacy** — cyan neon on near-black
393
+ - **Detroit 808** — TR-808 amber, red, and yellow
394
+
389
395
  Append `?fresh=1` to force a fresh lint run instead of returning the cached result.
390
396
 
391
397
  | State | Screenshot |
@@ -717,7 +723,7 @@ That example uses `startup.mode: 'report'`, so the app still boots while the pac
717
723
  ### Package API
718
724
 
719
725
  ```ts
720
- // ── Vocabulary types ──────────────────────────────────────────────────────────
726
+ // ── Ubiquitous language types ──────────────────────────────────────────────────────────
721
727
 
722
728
  type PresetName = 'recommended' | 'server' | 'strict'
723
729
  type LintSeverity = 'error' | 'warn' | 'info' | 'hint'
package/dist/index.mjs CHANGED
@@ -1,5 +1,11 @@
1
1
  import { a as enforceThreshold, c as strict, d as RulesetLoadError, f as defaultRulesetResolvers, g as lintOpenApi, h as recommended, i as OpenApiLintThresholdError, l as server, m as loadRuleset, n as createOpenApiLintRuntime, o as shouldFail, p as loadResolvedRuleset, r as resolveStartupMode, s as presets, t as OpenApiLintArtifactWriteError, u as resolveReporter } from "./core-BLJeXQ15.mjs";
2
2
  import { Elysia } from "elysia";
3
+ //#region \0inline-text:3.mjs
4
+ var _inline_text_3_default = "(() => {\n const THEME_KEY = 'elysia-spectral-theme';\n const THEMES = ['astro', 'tron', '808'];\n let stored = null;\n try {\n stored = localStorage.getItem(THEME_KEY);\n } catch {}\n const initial = THEMES.includes(stored) ? stored : 'astro';\n document.documentElement.dataset.theme = initial;\n\n const themeSel = document.querySelector('[data-theme-switcher]');\n if (themeSel) {\n themeSel.value = initial;\n themeSel.addEventListener('change', () => {\n const next = THEMES.includes(themeSel.value) ? themeSel.value : 'astro';\n document.documentElement.dataset.theme = next;\n try {\n localStorage.setItem(THEME_KEY, next);\n } catch {}\n });\n }\n\n const rel = (iso) => {\n const t = Date.parse(iso);\n if (Number.isNaN(t)) return '';\n const s = Math.round((Date.now() - t) / 1000);\n if (s < 60) return s + 's ago';\n if (s < 3600) return Math.round(s / 60) + 'm ago';\n if (s < 86400) return Math.round(s / 3600) + 'h ago';\n return Math.round(s / 86400) + 'd ago';\n };\n for (const el of document.querySelectorAll('[data-relative-time]')) {\n el.textContent = rel(el.getAttribute('data-relative-time'));\n }\n\n const rows = Array.from(document.querySelectorAll('[data-findings] tr'));\n const search = document.querySelector('[data-search]');\n const empty = document.querySelector('[data-empty-findings]');\n const chips = Array.from(document.querySelectorAll('[data-filter]'));\n let activeSeverity = 'all';\n let query = '';\n\n const apply = () => {\n let visible = 0;\n for (const tr of rows) {\n const sev = tr.getAttribute('data-severity');\n const hay = tr.getAttribute('data-haystack') || '';\n const sevOk = activeSeverity === 'all' || sev === activeSeverity;\n const qOk = !query || hay.includes(query);\n const show = sevOk && qOk;\n tr.classList.toggle('is-hidden', !show);\n if (show) visible += 1;\n }\n if (empty)\n empty.classList.toggle('hidden', visible !== 0 || rows.length === 0);\n };\n\n for (const chip of chips) {\n const select = () => {\n activeSeverity = chip.getAttribute('data-filter') || 'all';\n for (const c of chips) c.classList.toggle('is-active', c === chip);\n apply();\n };\n chip.addEventListener('click', select);\n chip.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n select();\n }\n });\n }\n\n if (search) {\n search.addEventListener('input', () => {\n query = search.value.trim().toLowerCase();\n apply();\n });\n }\n\n for (const btn of document.querySelectorAll('[data-copy]')) {\n btn.addEventListener('click', async () => {\n const value = btn.getAttribute('data-copy') || '';\n try {\n await navigator.clipboard.writeText(value);\n btn.classList.add('copied');\n btn.textContent = 'copied';\n setTimeout(() => {\n btn.classList.remove('copied');\n btn.textContent = 'copy';\n }, 1200);\n } catch {}\n });\n }\n\n document.addEventListener('keydown', (e) => {\n if (\n e.target &&\n (e.target.tagName === 'INPUT' ||\n e.target.tagName === 'TEXTAREA' ||\n e.target.tagName === 'SELECT')\n )\n return;\n if (e.key === 'r') {\n const link = document.querySelector('[data-refresh]');\n if (link) link.click();\n }\n if (e.key === '/' && search) {\n e.preventDefault();\n search.focus();\n }\n });\n})();\n";
5
+ //#endregion
6
+ //#region \0inline-text:2.mjs
7
+ var _inline_text_2_default = ":root {\n color-scheme: dark;\n --bg: #17191e;\n --fg: #eef0f9;\n --muted: #8a93a0;\n --line: #262a33;\n --surface: #1d2027;\n --accent: #ad5dca;\n --accent-soft: #2b7eca;\n --pass: #23d18b;\n --fail: #dc3657;\n --warn: #ffc368;\n --info: #54b9ff;\n --hint: #545864;\n --mono:\n ui-monospace, SFMono-Regular, \"JetBrains Mono\", Menlo, Monaco, Consolas,\n \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n[data-theme=\"tron\"] {\n --bg: #06080d;\n --fg: #e6f7ff;\n --muted: #5d8aa3;\n --line: #0e2c3d;\n --surface: #0a1320;\n --accent: #38f3ff;\n --accent-soft: #1ea7ff;\n --pass: #7de8ff;\n --fail: #ff3c5f;\n --warn: #a8f5ff;\n --info: #73d8ff;\n --hint: #273746;\n}\n\n[data-theme=\"808\"] {\n --bg: #202020;\n --fg: #ffffff;\n --muted: #9a958a;\n --line: #2a2a2a;\n --surface: #262626;\n --accent: #f8a125;\n --accent-soft: #e72e2e;\n --pass: #f1f827;\n --fail: #e72e2e;\n --warn: #f8a125;\n --info: #f1f827;\n --hint: #404040;\n}\n\n* {\n box-sizing: border-box;\n}\n\nbody {\n margin: 0;\n font-family: var(--mono);\n font-feature-settings:\n \"calt\" 0,\n \"liga\" 0,\n \"ss01\";\n background: var(--bg);\n color: var(--fg);\n font-size: 13px;\n line-height: 1.5;\n}\n\nheader {\n position: sticky;\n top: 0;\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 24px;\n border-bottom: 1px solid var(--line);\n background: color-mix(in srgb, var(--bg) 88%, transparent);\n backdrop-filter: blur(8px);\n overflow: hidden;\n}\n\nheader::before {\n content: \"\";\n position: absolute;\n inset: 0;\n z-index: 0;\n background:\n radial-gradient(900px 220px at 12% -30%, var(--accent), transparent 60%),\n radial-gradient(\n 700px 200px at 88% -20%,\n var(--accent-soft),\n transparent 65%\n );\n opacity: 0.45;\n pointer-events: none;\n}\n\nheader > * {\n position: relative;\n z-index: 1;\n}\n\n.brand {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--accent);\n box-shadow:\n 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent),\n 0 0 18px color-mix(in srgb, var(--accent) 60%, transparent);\n}\n\nh1 {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n}\n\n.actions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.visually-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0 0 0 0);\n}\n\n.theme-switch {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.theme-label {\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--muted);\n}\n\n.theme-switch select {\n font-family: var(--mono);\n font-size: 11px;\n padding: 5px 24px 5px 10px;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--surface)\n url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='none' stroke='%238a93a0' stroke-width='1.4' d='M1 1l4 4 4-4'/></svg>\")\n no-repeat right 8px center;\n color: var(--fg);\n -webkit-appearance: none;\n appearance: none;\n cursor: pointer;\n}\n.theme-switch select:hover {\n border-color: var(--muted);\n}\n.theme-switch select:focus {\n outline: none;\n border-color: var(--accent);\n}\n\nkbd {\n font-family: var(--mono);\n font-size: 11px;\n padding: 2px 6px;\n border: 1px solid var(--line);\n border-bottom-width: 2px;\n border-radius: 4px;\n color: var(--muted);\n background: var(--surface);\n}\n\n.refresh {\n color: var(--fg);\n text-decoration: none;\n padding: 6px 12px;\n border: 1px solid var(--line);\n border-radius: 6px;\n font-size: 12px;\n font-family: var(--mono);\n background: var(--surface);\n transition:\n border-color 0.15s,\n transform 0.05s;\n}\n.refresh:hover {\n border-color: var(--accent);\n}\n.refresh:active {\n transform: translateY(1px);\n}\n\nmain {\n padding: 24px;\n max-width: 1100px;\n margin: 0 auto;\n}\n\n.banner {\n padding: 12px 16px;\n border-radius: 8px;\n margin-bottom: 16px;\n font-size: 13px;\n}\n.banner-pass {\n background: color-mix(in srgb, var(--pass) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--pass) 60%, var(--line));\n}\n.banner-fail {\n background: color-mix(in srgb, var(--fail) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--fail) 60%, var(--line));\n}\n.tagline {\n display: block;\n margin-top: 6px;\n font-weight: 700;\n letter-spacing: 0.12em;\n color: var(--pass);\n text-transform: uppercase;\n font-size: 11px;\n}\n\n.meta {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 8px;\n margin: 0 0 16px;\n padding: 0;\n}\n.meta div {\n background: var(--surface);\n border: 1px solid var(--line);\n border-radius: 6px;\n padding: 8px 12px;\n}\n.meta dt {\n color: var(--muted);\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n margin-bottom: 4px;\n}\n.meta dd {\n margin: 0;\n font-size: 12px;\n}\n.muted-line {\n display: block;\n color: var(--muted);\n font-size: 11px;\n margin-top: 2px;\n}\n\n.summary {\n list-style: none;\n padding: 0;\n margin: 0 0 24px;\n display: flex;\n gap: 6px;\n flex-wrap: wrap;\n}\n.summary li {\n padding: 6px 12px;\n border: 1px solid var(--line);\n border-radius: 999px;\n font-size: 12px;\n color: var(--muted);\n cursor: pointer;\n user-select: none;\n transition:\n border-color 0.12s,\n background 0.12s;\n}\n.summary li:hover {\n border-color: var(--muted);\n}\n.summary li:focus {\n outline: 2px solid color-mix(in srgb, var(--accent) 60%, transparent);\n outline-offset: 2px;\n}\n.summary li.is-active {\n background: color-mix(in srgb, var(--accent) 14%, transparent);\n border-color: var(--accent);\n color: var(--fg);\n}\n.summary li span {\n color: var(--fg);\n font-weight: 600;\n margin-right: 6px;\n}\n.summary .sev-error span {\n color: var(--fail);\n}\n.summary .sev-warn span {\n color: var(--warn);\n}\n.summary .sev-info span {\n color: var(--info);\n}\n\nsection {\n margin-top: 24px;\n}\nh2 {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--muted);\n margin: 0 0 12px;\n font-weight: 600;\n}\n\n.findings-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n margin-bottom: 8px;\n}\n.findings-head h2 {\n margin: 0;\n}\n\n[data-search] {\n font-family: var(--mono);\n font-size: 12px;\n padding: 6px 10px;\n min-width: 240px;\n background: var(--surface);\n color: var(--fg);\n border: 1px solid var(--line);\n border-radius: 6px;\n}\n[data-search]:focus {\n outline: none;\n border-color: var(--accent);\n}\n\n.artifacts {\n list-style: none;\n padding: 0;\n margin: 0;\n}\n.artifacts li {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 6px 0;\n border-bottom: 1px solid var(--line);\n font-size: 12px;\n}\n.artifacts code {\n color: var(--muted);\n min-width: 160px;\n}\n.artifacts .path {\n flex: 1;\n word-break: break-all;\n}\n\n.copy {\n font-family: var(--mono);\n font-size: 11px;\n padding: 2px 8px;\n border: 1px solid var(--line);\n background: var(--surface);\n color: var(--muted);\n border-radius: 4px;\n cursor: pointer;\n}\n.copy:hover {\n color: var(--fg);\n border-color: var(--muted);\n}\n.copy.copied {\n color: var(--pass);\n border-color: var(--pass);\n}\n\ntable {\n width: 100%;\n border-collapse: collapse;\n font-size: 12px;\n}\nth,\ntd {\n text-align: left;\n padding: 8px 12px;\n border-bottom: 1px solid var(--line);\n vertical-align: top;\n}\nth {\n color: var(--muted);\n font-weight: 500;\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n}\ntbody tr:hover {\n background: color-mix(in srgb, var(--fg) 4%, transparent);\n}\ntr.is-hidden {\n display: none;\n}\n\n.badge {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 4px;\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n font-family: var(--mono);\n}\n.sev-error .badge {\n background: color-mix(in srgb, var(--fail) 22%, transparent);\n color: var(--fail);\n}\n.sev-warn .badge {\n background: color-mix(in srgb, var(--warn) 22%, transparent);\n color: var(--warn);\n}\n.sev-info .badge {\n background: color-mix(in srgb, var(--info) 22%, transparent);\n color: var(--info);\n}\n.sev-hint .badge {\n background: color-mix(in srgb, var(--hint) 22%, transparent);\n color: var(--hint);\n}\n\n.recommendation {\n margin: 4px 0 0;\n color: var(--muted);\n font-size: 11px;\n}\n.pointer {\n display: block;\n margin-top: 4px;\n color: var(--muted);\n font-size: 11px;\n}\n.empty {\n color: var(--muted);\n font-size: 12px;\n}\n.hidden {\n display: none;\n}\n";
8
+ //#endregion
3
9
  //#region src/output/dashboard.ts
4
10
  const renderDashboard = (input) => {
5
11
  const { result, threshold, cached, error, refreshPath } = input;
@@ -10,7 +16,7 @@ const renderDashboard = (input) => {
10
16
  <meta charset="utf-8" />
11
17
  <meta name="viewport" content="width=device-width,initial-scale=1" />
12
18
  <title>Elysia Spectral Lint Dashboard</title>
13
- <style>${styles}</style>
19
+ <style>${_inline_text_2_default}</style>
14
20
  </head>
15
21
  <body>
16
22
  <header>
@@ -19,12 +25,20 @@ const renderDashboard = (input) => {
19
25
  <h1>Elysia Spectral Lint</h1>
20
26
  </div>
21
27
  <div class="actions">
28
+ <label class="theme-switch" title="Switch dashboard theme">
29
+ <span class="theme-label">Theme</span>
30
+ <select data-theme-switcher aria-label="Dashboard theme">
31
+ <option value="astro">Astro Houston</option>
32
+ <option value="tron">Tron Legacy</option>
33
+ <option value="808">Detroit 808</option>
34
+ </select>
35
+ </label>
22
36
  <kbd title="Press r to re-run">r</kbd>
23
37
  <a class="refresh" href="${`${escapeAttr(refreshPath)}?fresh=1`}" data-refresh>Re-run</a>
24
38
  </div>
25
39
  </header>
26
40
  <main>${body}</main>
27
- <script>${script}<\/script>
41
+ <script>${_inline_text_3_default}<\/script>
28
42
  </body>
29
43
  </html>`;
30
44
  };
@@ -82,130 +96,6 @@ const renderFindings = (findings) => {
82
96
  };
83
97
  const escapeText = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
84
98
  const escapeAttr = escapeText;
85
- const styles = `
86
- :root { color-scheme: light dark; --bg:#0b0d10; --fg:#e6e6e6; --muted:#8a93a0; --line:#1f242b; --surface:#11151a; --pass:#3ddc97; --fail:#ff5d5d; --warn:#f5a623; --info:#5dade2; --hint:#bdc3c7; --mono: ui-monospace, SFMono-Regular, "JetBrains Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
87
- @media (prefers-color-scheme: light) { :root { --bg:#fafafa; --fg:#1a1a1a; --muted:#6b7380; --line:#e3e6eb; --surface:#ffffff; } }
88
- * { box-sizing: border-box; }
89
- body { margin:0; font-family: var(--mono); font-feature-settings: "calt" 0, "liga" 0, "ss01"; background:var(--bg); color:var(--fg); font-size:13px; line-height:1.5; }
90
- header { position:sticky; top:0; z-index:10; display:flex; align-items:center; justify-content:space-between; padding:12px 24px; border-bottom:1px solid var(--line); background:color-mix(in srgb, var(--bg) 92%, transparent); backdrop-filter: blur(8px); }
91
- .brand { display:flex; align-items:center; gap:10px; }
92
- .dot { width:8px; height:8px; border-radius:50%; background:var(--pass); box-shadow:0 0 0 3px color-mix(in srgb, var(--pass) 25%, transparent); }
93
- h1 { margin:0; font-size:13px; font-weight:600; letter-spacing:.02em; text-transform:uppercase; }
94
- .actions { display:flex; align-items:center; gap:10px; }
95
- kbd { font-family:var(--mono); font-size:11px; padding:2px 6px; border:1px solid var(--line); border-bottom-width:2px; border-radius:4px; color:var(--muted); background:var(--surface); }
96
- .refresh { color:var(--fg); text-decoration:none; padding:6px 12px; border:1px solid var(--line); border-radius:6px; font-size:12px; font-family:var(--mono); background:var(--surface); transition:border-color .15s, transform .05s; }
97
- .refresh:hover { border-color:var(--muted); }
98
- .refresh:active { transform: translateY(1px); }
99
- main { padding:24px; max-width:1100px; margin:0 auto; }
100
- .banner { padding:12px 16px; border-radius:8px; margin-bottom:16px; font-size:13px; }
101
- .banner-pass { background: color-mix(in srgb, var(--pass) 14%, transparent); border:1px solid color-mix(in srgb, var(--pass) 60%, var(--line)); }
102
- .banner-fail { background: color-mix(in srgb, var(--fail) 14%, transparent); border:1px solid color-mix(in srgb, var(--fail) 60%, var(--line)); }
103
- .tagline { display:block; margin-top:6px; font-weight:700; letter-spacing:.12em; color:var(--pass); text-transform:uppercase; font-size:11px; }
104
- .meta { display:grid; grid-template-columns: repeat(auto-fit, minmax(180px,1fr)); gap:8px; margin:0 0 16px; padding:0; }
105
- .meta div { background:var(--surface); border:1px solid var(--line); border-radius:6px; padding:8px 12px; }
106
- .meta dt { color:var(--muted); font-size:10px; text-transform:uppercase; letter-spacing:.06em; margin-bottom:4px; }
107
- .meta dd { margin:0; font-size:12px; }
108
- .muted-line { display:block; color:var(--muted); font-size:11px; margin-top:2px; }
109
- .summary { list-style:none; padding:0; margin:0 0 24px; display:flex; gap:6px; flex-wrap:wrap; }
110
- .summary li { padding:6px 12px; border:1px solid var(--line); border-radius:999px; font-size:12px; color:var(--muted); cursor:pointer; user-select:none; transition:border-color .12s, background .12s; }
111
- .summary li:hover { border-color:var(--muted); }
112
- .summary li:focus { outline:2px solid color-mix(in srgb, var(--info) 60%, transparent); outline-offset:2px; }
113
- .summary li.is-active { background: color-mix(in srgb, var(--fg) 8%, transparent); border-color:var(--muted); color:var(--fg); }
114
- .summary li span { color:var(--fg); font-weight:600; margin-right:6px; }
115
- .summary .sev-error span { color:var(--fail); }
116
- .summary .sev-warn span { color:var(--warn); }
117
- .summary .sev-info span { color:var(--info); }
118
- section { margin-top:24px; }
119
- h2 { font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin:0 0 12px; font-weight:600; }
120
- .findings-head { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:8px; }
121
- .findings-head h2 { margin:0; }
122
- [data-search] { font-family:var(--mono); font-size:12px; padding:6px 10px; min-width:240px; background:var(--surface); color:var(--fg); border:1px solid var(--line); border-radius:6px; }
123
- [data-search]:focus { outline:none; border-color:var(--muted); }
124
- .artifacts { list-style:none; padding:0; margin:0; }
125
- .artifacts li { display:flex; align-items:center; gap:12px; padding:6px 0; border-bottom:1px solid var(--line); font-size:12px; }
126
- .artifacts code { color:var(--muted); min-width:160px; }
127
- .artifacts .path { flex:1; word-break:break-all; }
128
- .copy { font-family:var(--mono); font-size:11px; padding:2px 8px; border:1px solid var(--line); background:var(--surface); color:var(--muted); border-radius:4px; cursor:pointer; }
129
- .copy:hover { color:var(--fg); border-color:var(--muted); }
130
- .copy.copied { color:var(--pass); border-color:var(--pass); }
131
- table { width:100%; border-collapse:collapse; font-size:12px; }
132
- th, td { text-align:left; padding:8px 12px; border-bottom:1px solid var(--line); vertical-align:top; }
133
- th { color:var(--muted); font-weight:500; font-size:10px; text-transform:uppercase; letter-spacing:.06em; }
134
- tbody tr:hover { background: color-mix(in srgb, var(--fg) 4%, transparent); }
135
- tr.is-hidden { display:none; }
136
- .badge { display:inline-block; padding:2px 8px; border-radius:4px; font-size:10px; text-transform:uppercase; letter-spacing:.06em; font-family:var(--mono); }
137
- .sev-error .badge { background: color-mix(in srgb, var(--fail) 22%, transparent); color:var(--fail); }
138
- .sev-warn .badge { background: color-mix(in srgb, var(--warn) 22%, transparent); color:var(--warn); }
139
- .sev-info .badge { background: color-mix(in srgb, var(--info) 22%, transparent); color:var(--info); }
140
- .sev-hint .badge { background: color-mix(in srgb, var(--hint) 22%, transparent); color:var(--hint); }
141
- .recommendation { margin:4px 0 0; color:var(--muted); font-size:11px; }
142
- .pointer { display:block; margin-top:4px; color:var(--muted); font-size:11px; }
143
- .empty { color:var(--muted); font-size:12px; }
144
- .hidden { display:none; }
145
- `;
146
- const script = `
147
- (() => {
148
- const rel = (iso) => {
149
- const t = Date.parse(iso); if (Number.isNaN(t)) return '';
150
- const s = Math.round((Date.now() - t) / 1000);
151
- if (s < 60) return s + 's ago';
152
- if (s < 3600) return Math.round(s/60) + 'm ago';
153
- if (s < 86400) return Math.round(s/3600) + 'h ago';
154
- return Math.round(s/86400) + 'd ago';
155
- };
156
- for (const el of document.querySelectorAll('[data-relative-time]')) {
157
- el.textContent = rel(el.getAttribute('data-relative-time'));
158
- }
159
-
160
- const rows = Array.from(document.querySelectorAll('[data-findings] tr'));
161
- const search = document.querySelector('[data-search]');
162
- const empty = document.querySelector('[data-empty-findings]');
163
- const chips = Array.from(document.querySelectorAll('[data-filter]'));
164
- let activeSeverity = 'all';
165
- let query = '';
166
-
167
- const apply = () => {
168
- let visible = 0;
169
- for (const tr of rows) {
170
- const sev = tr.getAttribute('data-severity');
171
- const hay = tr.getAttribute('data-haystack') || '';
172
- const sevOk = activeSeverity === 'all' || sev === activeSeverity;
173
- const qOk = !query || hay.includes(query);
174
- const show = sevOk && qOk;
175
- tr.classList.toggle('is-hidden', !show);
176
- if (show) visible += 1;
177
- }
178
- if (empty) empty.classList.toggle('hidden', visible !== 0 || rows.length === 0);
179
- };
180
-
181
- for (const chip of chips) {
182
- const select = () => {
183
- activeSeverity = chip.getAttribute('data-filter') || 'all';
184
- for (const c of chips) c.classList.toggle('is-active', c === chip);
185
- apply();
186
- };
187
- chip.addEventListener('click', select);
188
- chip.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); select(); } });
189
- }
190
-
191
- if (search) {
192
- search.addEventListener('input', () => { query = search.value.trim().toLowerCase(); apply(); });
193
- }
194
-
195
- for (const btn of document.querySelectorAll('[data-copy]')) {
196
- btn.addEventListener('click', async () => {
197
- const value = btn.getAttribute('data-copy') || '';
198
- try { await navigator.clipboard.writeText(value); btn.classList.add('copied'); btn.textContent = 'copied'; setTimeout(() => { btn.classList.remove('copied'); btn.textContent = 'copy'; }, 1200); } catch {}
199
- });
200
- }
201
-
202
- document.addEventListener('keydown', (e) => {
203
- if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA')) return;
204
- if (e.key === 'r') { const link = document.querySelector('[data-refresh]'); if (link) link.click(); }
205
- if (e.key === '/' && search) { e.preventDefault(); search.focus(); }
206
- });
207
- })();
208
- `;
209
99
  //#endregion
210
100
  //#region src/plugin.ts
211
101
  const spectralPlugin = (options = {}) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opsydyn/elysia-spectral",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Thin Elysia plugin that lints generated OpenAPI documents with Spectral.",
5
5
  "packageManager": "bun@1.3.11",
6
6
  "publishConfig": {