@deepnode/hub-ui (0.1.0)
Installation
@deepnode:registry=npm install @deepnode/hub-ui@0.1.0"@deepnode/hub-ui": "0.1.0"About this package
@deepnode/hub-ui
The Deepnode interface kit's base: the tokens, the base CSS and the components layer taken from Review Hub's
interface (b3b2220), the fonts, and the shared helpers. A product installs it with exactly one area package
(@deepnode/hub-ui-reviewhub or @deepnode/hub-ui-crmhub) at the same version, and @material/web 2.5.0.
Entries
@deepnode/hub-ui: the constants (THEME_KEY,THEME_EVENT,BAR_COLOURS),escandel, and every part.@deepnode/hub-ui/markup: the pure markup helpers (esc,eland the parts' markup).@deepnode/hub-ui/parts/<part>: one part.@deepnode/hub-ui/testing: the sizes, the looks,setLookandwalkRoutesfor a product's Playwright tests (see Testing entry).@deepnode/hub-ui/styles.css: one stylesheet, its first statement@layer tokens, base, components, area, product;. Load it before the area'sstyles.css; a product's own rules go in@layer product.@deepnode/hub-ui/fonts/*: the self-hosted faces, referenced bystyles.cssas./fonts/<file>.
Testing entry
@deepnode/hub-ui/testing is for a product's Playwright tests, in Node. The kit does not install Playwright: the product adds
@playwright/test 1.63.0 as a devDependency, exactly:
npm install --save-dev --save-exact @playwright/test@1.63.0
When ./testing loads, it refuses any other @playwright/test version, or none, naming the one it found.
Consumer build
Merge the preset into your vite.config:
import preset from "@deepnode/hub-ui/vite-preset";
export default preset; // or mergeConfig(preset, { ...your own settings })
The preset sets build.assetsInlineLimit: 0, so every font and image is emitted as a file and never inlined as a
data: URL (a Content-Security-Policy without data: loads it).
Default Vite build keeps the layer order: yes
(Found on Vite 8.2.2, 2026-10-18: a starter-shaped consumer importing @deepnode/hub-ui/styles.css and then its own
@layer product file, built with Vite's default settings, emits one minified stylesheet whose layer statement is
folded into the blocks, @layer tokens{…}@layer base{…}@layer components{…}@layer area;@layer product{…}: the five
names first appear in the kit's order with product last, so the order holds and the preset needs no
cssMinify: false.)
Links the kit draws: the export rule and the same-origin rule
Every href a part draws from data is checked by the kit. Two rules do it, and they are not one rule:
- The export rule (part
export:exportButtonandsetExportHref): anhttps:address with a host and no user name or password, on any host, drawn as its parsed canonical form; or a relative reference (no scheme, not starting//), drawn as given. Either form is refused when it holds, anywhere, a backslash or a code point U+0000-U+0020 or U+007F (an ASCII control character, the ASCII space or DEL); no other code point is refused for itself, so a non-ASCII space or an invisible character (U+0085, U+00A0, U+200B, U+2028, U+FEFF) can stay in a relative reference drawn as given. Any otherhref, or one longer than 8192 UTF-16 code units (counted on thehrefas given, before parsing), draws the link with nohref. - The same-origin rule (parts
editandleave: the edit dialog's refusal link and the leave guard): anhttp:orhttps:address on the page's own origin with no user name or password, resolved againstdocument.baseURI, drawn as its canonical absolute form and checked again when pressed. Any otherhrefdraws no link and does not navigate.
The two rules answer the same href differently in four ways:
- An
https:address on another host: the export rule draws it; the same-origin rule refuses it. - An
http:address on the page's own origin: the export rule refuses it; the same-origin rule accepts it. - A relative reference: the export rule checks it against a fixed root and draws it as given, so the browser resolves it
against the page's base when the link is pressed (a
<base href>on another origin moves it there); the same-origin rule resolves it againstdocument.baseURIwhen it checks, and checks again at the press. - A percent-encoded slash (
/%2Fhost/x,/%5Chost/x): both rules keep it as a path on the page's own origin; a server or proxy that decodes%2For merges slashes before it redirects could turn it into//host/x.
An export link that must stay on the product's own origin is built from the product's own fixed paths.
Updating an export link. A screen that changes a drawn export link's address (the history filter's CSV link, #csv,
after its filters change) calls setExportHref(link, csvHref(base, filters, extra, allowExtra)), never link.href = … or
setAttribute("href", …): setExportHref runs the export rule and removes the href when the rule refuses it. csvHref
throws a TypeError for a base that is not a string or holds ? or #; for an allowExtra that is not an array of
strings; for an extra key that is also a filter key (level, kind, from, to), declared or not; and for an extra
key that allowExtra does not name, compared as the same string (Company is not company; the default [] refuses every
extra key, an empty value included). A screen whose base is built from data catches that error and calls
setExportHref(link, null), so a refused address never leaves the link's previous href in place.
Strict Content-Security-Policy
The preset also keeps the bundle whole under a strict Content-Security-Policy: style-src 'self' with no
'unsafe-inline'. @material/web 2.5.0's menus, selects and text fields style themselves through lit's styleMap,
which writes a style attribute on its first render, and such a policy blocks that attribute. The preset's plugin
cspStyleMap() answers every import of lit/directives/style-map.js and lit-html/directives/style-map.js in
vite build with a styleMap that writes the same properties through the CSSOM only, never the attribute. A product
that does not merge the preset adds the plugin to its own config:
import { cspStyleMap } from "@deepnode/hub-ui/vite-preset";
export default { plugins: [cspStyleMap()] };
The plugin works in vite build only: the dev server's pre-bundled dependencies keep lit's own styleMap.
Licences
The preset turns on Vite's build.license: every build writes licenses.txt to the root of its output directory,
naming every bundled dependency (lit and @material/web among them) with its version, its licence and its licence
text. Serve it with the bundle. A product that publishes its notices another way turns it off in its own config:
mergeConfig(preset, { build: { license: false } }).
The self-hosted fonts' licences ship in this package beside the faces: dist/fonts/LICENSE-Inter-OFL.txt and
dist/fonts/LICENSE-JetBrainsMono-OFL.txt (SIL Open Font License 1.1) and dist/fonts/LICENSE-material-symbols.txt
(Apache License 2.0). Vite copies the faces into a build as assets, but not these files: a product that serves the
fonts serves their licences too.
KPI meters
kpi() draws a meter's fill with its share as data-share (a percentage), never as a style attribute, so the tile
stays whole under a strict Content-Security-Policy. After the tiles are in the document, call paintKpiMeters(root)
from @deepnode/hub-ui/parts/kpi (or @deepnode/hub-ui): it sets the width of every fill under root through the
CSSOM and returns how many fills it painted. Call it again after drawing new tiles.
The styleMap guard
cspStyleMap() also fails the build when lit's own styleMap reaches the build's modules by another route: a
resolve.alias of lit or lit-html (to a directory, or to a CDN URL whatever its name), an import of the module
by its file path, a copy of its code under another name (vendored, minified, aliased, a virtual module or a ?raw
import), or another plugin that resolves or loads it before the substitute; whenever lit, @material/web or the
kit is left external; and whenever lit's, @material/web's or the kit's own code imports, directly or through other
bundled modules, a module that is not in the build. The build then ends with the error lit's own styleMap module "<id>" reached the build instead of the CSSOM-only substitute or, for an external, lit, @material/web or the kit is external in this build ("<id>") or "<importer>" imports "<id>", which is not in the build, naming the module,
and writes no output. The fix is in the product's own config: import the directive only as
lit/directives/style-map.js or lit-html/directives/style-map.js, and keep lit, @material/web and the kit
bundled.
The guard knows lit's styleMap three ways. By its code, lit-html's camelCase-to-dash-case regular expression (its
development, production and node builds and a minified copy all keep it), read in every bundled module whatever its
name, path or virtual id. By its path: lit/directives/style-map.js or lit-html/directives/style-map.js in a bare
id, a file path or a URL, with or without a version after the package name, jsdelivr's /+esm ending or a ?query,
in any lit version. The code check is proved on lit-html 3.3.3, the version the kit installs. An external has no
code in the build, so any external whose id names lit as a word (lit, lit-html,
https://esm.sh/*lit-html@3.3.3/directives/style-map.js, /vendor/lit-3.3.3.min.js,
https://cdn.jsdelivr.net/npm/lit@3/+esm) or whose id holds lit's directive path directives/style-map (in any
letter case, whatever follows it: .js, .min.js, /+esm, /index.js, a ?query; the kit's own import becomes
https://cdn.example.com/a/directives/style-map.js under an alias of lit to https://cdn.example.com/a) fails
the build, and so do externals that only resemble it (lit-element, @lit/reactive-element, a lit.dev URL,
another package's directives/style-map): serve lit from the bundle, never from a CDN or an import map. And by the
build's structure: lit's, @material/web's and the kit's own bundled code (a module whose nearest package.json
names lit, lit-html, lit-element, @lit/*, @material/web or @deepnode/hub-ui*, wherever it lies:
installed, linked, a workspace or an alias to its directory) may import only modules in the build, directly or
through any bundled module between them, so an alias, an external or a stand-in of any of their imports, to a URL or
a path of any name (lit/directives/style-map.js to https://cdn.example.com/x.js, lit/directives to a CDN, or
to a product's file or a plugin's virtual module that re-exports a URL), fails the build, naming lit's, Material's
or the kit's module that reaches it and the import. So does a build that leaves a dependency of Material (tslib)
external, and any external whose id names @material/web or @deepnode/hub-ui (in any letter case, path or
version) fails the build whoever imports it: there is no exception by name, so keep them bundled. A product's own
externals pass (React from a CDN).
What the guard defends is the build's modules against a product's own configuration: its aliases, file paths,
externals, copies and the plugins it adds. It reads the build's modules, not the files the build writes, so it does
not see lit's code that reaches the output another way: an asset made from it (?url, new URL(..., import.meta.url)), a worker that Vite builds (a worker gets neither the substitute nor the guard), a file another
plugin emits, rewrites or writes, a file under public/, or a compressed copy of any of these. Keep lit's style-map
code out of all of them. An external that only the product's own code imports is judged by its module id, so it does
not see one whose id neither names lit nor holds directives/style-map: an import-map name, or an external that
build.rolldownOptions.output.paths remaps to a lit URL (output.paths rewrites only the import written into a
chunk, never the module's id, so an external lit remapped to another URL still fails the build, while an external
smx remapped to lit's URL passes). Nor does it see code a page loads at run time from outside the build (a
<script src> to a CDN in index.html, an import() of a URL computed at run time), or a hostile program on the
build machine. A copy of lit, Material or the kit that is disguised on purpose (its package.json renamed, its code
rewritten) is the product's own code. The product's own Content-Security-Policy is the final control: style-src 'self' blocks the style attribute lit's styleMap writes, script-src 'self' blocks code from another origin, and
a product whose policy allows more owns that risk.
The guard refuses @material/web or the kit left external because it cannot read the lit that such a module would
load from outside the bundle: a product that externalises Material or the kit fails the build. The guard does not
reach the dev server and Vitest: the plugin runs in vite build only, so a policy a team relaxes for the dev server
must never be copied into production. vite build --ssr leaves dependencies external, so an SSR build that imports
lit, Material or the kit fails the build unless it bundles them (ssr.noExternal); no product renders on a server.
The substitute imports lit-html and lit-html/directive.js from the Vite root, so a build whose root cannot resolve
lit-html (a strict, non-hoisted tree) fails with Rolldown failed to resolve import "lit-html": the build fails, it never
silently drops the substitute. A second lit-html copy in the tree works, at the cost of that copy in the bundle.
Dependencies
Peer dependencies
| ID | Version |
|---|---|
| @material/web | 2.5.0 |