Max

@deepnode/hub-ui (0.1.0)

Published 2026-11-04 06:17:08 +00:00 by Max

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), esc and el, and every part.
  • @deepnode/hub-ui/markup: the pure markup helpers (esc, el and the parts' markup).
  • @deepnode/hub-ui/parts/<part>: one part.
  • @deepnode/hub-ui/testing: the sizes, the looks, setLook and walkRoutes for 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's styles.css; a product's own rules go in @layer product.
  • @deepnode/hub-ui/fonts/*: the self-hosted faces, referenced by styles.css as ./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.)

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: exportButton and setExportHref): an https: 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 other href, or one longer than 8192 UTF-16 code units (counted on the href as given, before parsing), draws the link with no href.
  • The same-origin rule (parts edit and leave: the edit dialog's refusal link and the leave guard): an http: or https: address on the page's own origin with no user name or password, resolved against document.baseURI, drawn as its canonical absolute form and checked again when pressed. Any other href draws no link and does not navigate.

The two rules answer the same href differently in four ways:

  1. An https: address on another host: the export rule draws it; the same-origin rule refuses it.
  2. An http: address on the page's own origin: the export rule refuses it; the same-origin rule accepts it.
  3. 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 against document.baseURI when it checks, and checks again at the press.
  4. 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 %2F or 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
Details
npm
2026-11-04 06:17:08 +00:00
44
UNLICENSED
331 KiB
Assets (1)
Versions (6) View all
0.3.0 2026-11-15
0.2.0 2026-11-10
0.1.1 2026-11-05
0.1.0 2026-11-04
0.1.0-preview.2 2026-10-26