diff --git a/web/package.json b/web/package.json index fc86e78..2d2d8d9 100644 --- a/web/package.json +++ b/web/package.json @@ -7,7 +7,7 @@ "dev": "vite", "build": "tsc --noEmit && vite build", "icons": "node scripts/icons.mjs", - "test": "node scripts/test-graphcols.mjs" + "test": "node scripts/test-graphcols.mjs && node scripts/test-css-imports.mjs" }, "dependencies": { "@xterm/addon-fit": "^0.10.0", diff --git a/web/scripts/test-css-imports.mjs b/web/scripts/test-css-imports.mjs new file mode 100644 index 0000000..0ae1ae4 --- /dev/null +++ b/web/scripts/test-css-imports.mjs @@ -0,0 +1,20 @@ +// node scripts/test-css-imports.mjs — every stylesheet the UI imports is one somebody has read. +// A third-party stylesheet usually carries its demo page's global rules (body, *, button); on +// 2026-10-09 one restyled every button in the app. Add a file here only once its rules are +// known to touch nothing but its own component. +import { readdirSync, readFileSync, statSync } from 'node:fs' +import { join, relative } from 'node:path' + +const allowed = new Set(['./styles.css', '@xterm/xterm/css/xterm.css']) +const src = new URL('../src', import.meta.url).pathname +const files = [] +const walk = (d) => { for (const n of readdirSync(d)) { const p = join(d, n); if (statSync(p).isDirectory()) { if (n !== 'vendor') walk(p) } else if (/\.(ts|tsx)$/.test(n)) files.push(p) } } +walk(src) +const fails = [] +for (const f of files) { + for (const m of readFileSync(f, 'utf8').matchAll(/import\s+(?:[^'"]*from\s+)?['"]([^'"]+\.css)['"]/g)) { + if (!allowed.has(m[1])) fails.push(`${relative(src, f)} imports ${m[1]}`) + } +} +if (fails.length) { console.error('css imports: not allowed:\n ' + fails.join('\n ')); process.exit(1) } +console.log(`css imports: ok (${files.length} files)`) diff --git a/web/src/VgaConsole.tsx b/web/src/VgaConsole.tsx index 5fea277..ec906e6 100644 --- a/web/src/VgaConsole.tsx +++ b/web/src/VgaConsole.tsx @@ -4,7 +4,9 @@ import { useEffect, useRef, useState } from 'react' import { wsURLPath } from './api' import { Btn, useEscape } from './ui' -import './vendor/spice-html5/spice.css' +// No spice.css: upstream's stylesheet is for its own demo page and sets body, *, button and +// canvas for the whole document. Imported here it restyled every button in Ham Lab. The +// console is styled by the .vga rules in styles.css. type Spice = typeof import('./vendor/spice-html5/src/main.js') diff --git a/web/src/vendor/spice-html5/README-hamlab.md b/web/src/vendor/spice-html5/README-hamlab.md index 5a0ca3a..977cbe0 100644 --- a/web/src/vendor/spice-html5/README-hamlab.md +++ b/web/src/vendor/spice-html5/README-hamlab.md @@ -1,9 +1,16 @@ # spice-html5, vendored Upstream: https://gitlab.freedesktop.org/spice/spice-html5 — commit b372aab (2026-09-21), -copied unchanged on 2026-10-09 (`src/`, `thirdparty/`, `spice.css`). LGPL-3.0-or-later; the +copied unchanged on 2026-10-09 (`src/`, `thirdparty/`). LGPL-3.0-or-later; the licence text is beside this file. It draws a VM's SPICE display into a canvas from a websocket carrying the raw SPICE stream — which is what Incus's VGA console websocket is — so Ham Lab's daemon bridges the two and this does the rest. Loaded lazily by `VgaConsole.tsx`, so the main bundle does not carry it. Not on npm in a current form (the `@spice-project/spice-html5` package stopped at 0.2.1 in 2019), hence the copy. + +Upstream's `spice.css` is deliberately **not** here. It is the stylesheet of spice-html5's +demo page, with rules for `body`, `*`, `button` and `canvas` that apply to the whole +document. It was copied and imported on 2026-10-09 and turned every button in Ham Lab into +a gradient pill shifted off its place (v0.1.0-188 to -191). The console is styled by the +`.vga` rules in `src/styles.css`; `scripts/test-css-imports.mjs` fails on any new +stylesheet import that has not been looked at. diff --git a/web/src/vendor/spice-html5/spice.css b/web/src/vendor/spice-html5/spice.css deleted file mode 100644 index 90aecd6..0000000 --- a/web/src/vendor/spice-html5/spice.css +++ /dev/null @@ -1,168 +0,0 @@ -body -{ - background-color: #999999; - color: #000000; - margin: 0; - padding: 0; - font-family: "Lucida Grande", "Lucida Sans Unicode", "Helvetica Neue", Helvetica, Arial, Verdana, sans-serif; - font-size: 12pt; - line-height: 1.5em; -} - -* { margin: 0; } - -button -{ - cursor: pointer; - color: #000000; - background-color: #999999; - border: 1px solid #4a4a4a; - min-width: 150px; - border-radius: 10px; - background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#24414e)); - background: -moz-linear-gradient(top, #fff, #24414e); - -ms-transform: translate(15%, -8%); - transform: translate(15%, -8%); - font-size: .90em; - font-weight: bolder; - padding: 0px 0px 3px 0px; - margin-top: 5px; -} -button:focus -{ - outline: none; -} -button:hover -{ - background-color: #24414e -} -button:active -{ - background-color: #24414e; - transform: translate(14.85%, -7%); -} - -.SidenavClosed -{ - height: 100%; - width: 0; - position: fixed; - z-index: 1; - top: 0; - left: 0; - background-color: #bbbbbb; - box-shadow: 1px 0px #00000040; - overflow-x: hidden; - transition: 0.5s; - padding-top: 60px; -} -.SidenavOpen -{ - height: 100%; - width: 200px !important; - position: fixed; - z-index: 1; - top: 0; - left: 0; - background-color: #bbbbbb; - box-shadow: 1px 0px #00000040; - overflow-x: hidden; - transition: 0.5s; - padding-top: 60px; -} -#Sidenav label -{ - color: #000000; - margin-left: 3%; - text-shadow: 1px 1px 0px rgba(175, 210, 220, 0.8); - position: absolute; - font-size: .9em; -} -#Sidenav input -{ - padding: 3px; - background-color: #fAfAfA; - border: 1px inset #999999; - outline: none; - float: right; - margin-right: 3%; - -moz-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; - box-sizing: border-box; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - position: relative; - max-width: 110px; -} -#Sidenav .closebtn -{ - position: absolute; - top: 10px; - right: 10px; - font-size: 25px; - margin-left: 50px; - cursor: pointer; - color: #000000; -} - -canvas -{ - border: 1px solid #000000; -} - -#login -{ - background: #bbbbbb; - z-index: 1; - padding-top: 2px; - border-bottom: 2px solid #00000040; -} -#login p -{ - margin-left: 27%; - display: inline; - font-size: large; -} - -#spice-area -{ - height: 100%; - width: 100%; - margin-left: auto; - margin-right: auto; -} -.spice-screen -{ - min-height: 600px; - height: 100%; - text-align: center; - padding: 0; - background-color: #999999; -} -.spice-message -{ - width: 90%; - height: 40%; - overflow: auto; - margin-left: auto; - margin-right: auto; - padding: 10px; - font-size: 8pt; - line-height: 1.1em; - font-family: 'Andale Mono', monospace; - background-color: #fAfAfA; - border: 1px inset #999999; - border-radius: 3px; - box-sizing: border-box; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; -} -.spice-message p { - margin-bottom: 0em; - margin-top: 0em; -} -.spice-message-warning { - color: orange; -} -.spice-message-error { - color: red; -} \ No newline at end of file