@pantoken/web-components
Shadow-DOM custom elements built over the @pantoken/components CSS. A wrapper(tag, css, render, display) covers pure-presentational elements; behavioral elements are bespoke classes.
Runtime graph must stay Node-free
This package ships to the browser (the docs client registers it). Anything in the runtime graph is bundled into the client. A value import of a postcss-based @cssdoc/* package makes rolldown inject a createRequire/node:module shim into dist/index.mjs, which throws createRequire is not a function at app init and kills all client JS. Keep @cssdoc/* value imports out of src/ — type-only imports are fine (erased at build); value imports belong in build scripts and tests.
Shadow CSS is co-located .css → ?inline
- Shadow CSS lives in co-located
src/**/*.cssfiles — the lint, format, and docs source of truth. - Elements import their co-located CSS with
?inline; Vite+ bundles@tsdown/css, which returns the processed stylesheet as a tree-shakeable string for the shadow<style>. - The CSS finalizer strips doc comments and minifies the inline string, so cssdoc records stay in the source
.cssfiles without shipping as runtime bytes. - Use
?inline, not?raw:?inlineis the shared Vite/@tsdown/csscontract and works in pack, dev, and test.
register() and the tag prefix
register(target?, { prefix }). A prefix is always applied because a custom-element name must contain a hyphen (<icon> is invalid). A non-empty string wins ({prefix:"x"} → <x-icon>); anything falsy or whitespace falls back to DEFAULT_PREFIX = "instui" — it never drops elements. Implementation is low-churn: internal code keeps its canonical instui-<base> literals, and register shadows the registry with a scoped adapter that rewrites instui-<base> → ${prefix}-<base>. Cross-element nesting uses ctx.tag("calendar") so the prefix propagates.
register() also injects foundationCss() once into document.head as <style id="pantoken-foundation"> — a :root block built from elevationDeclarations() + focusOutlineDeclarations(). Those tokens aren't in the plain token sheet, so without it a consumer loading only the module + @pantoken/css gets no shadows or focus rings (custom props pierce the shadow boundary). It's guarded on typeof document so the registry-only test path skips it, and exported for SSR inlining.
Structure
src/index.ts is a barrel. Elements are src/elements/<name>.ts, one ElementDefinition each, added to DEFINITIONS[] and ELEMENTS[] in load-bearing order (a test asserts ELEMENTS matches the register() define order — append new tags at the end). register() builds one RegisterContext ({ registry, tag, I:{prefix:"instui"}, invokerSupported, onCommand, wrapper, variantClass, iconSvg }) and hands it to each def.define(ctx).
The JS-behavior tier
Behavior pure CSS can't cover: tooltip, drilldown, pages, drawer-layout, an interactive calendar, and date/datetime pickers. The consistent declarative layer is the Invoker Commands API — light-DOM <button command="…" commandfor="element-id">:
instui-modal:--show/--close/--toggle(the shadow<dialog>can't be acommandfortarget, hence host-level custom commands).instui-drilldown:--goto(readsdata-page) /--back.instui-pages:--push/--back.instui-drawer-layout:--toggle/--open/--close.instui-popover/instui-tray/instui-context-view: already native[popover], so the built-incommand="toggle-popover"works with no code.
A shared onCommand(target, handler) helper adds the command listener and, when the API is absent (!("command" in HTMLButtonElement.prototype)), falls back to a click delegate reading the same attributes. It listens on target.getRootNode() so it covers a light-DOM host (document) and the calendar grid (shadow root). Date helpers use local dates (avoid toISOString UTC shift).
Universal spacing
Every element supports margin/padding (InstUI 1–4-value shorthand) and per-side margin-<side>/padding-<side> (physical or logical edges), each value a keyword → token or a raw CSS length. lib/helpers.ts exports resolveSpace, spacingValue, and applySpacing(host); a withSpacing(Ctor) mixin subclasses each ctor and the scoped registry wraps every ctor with it in define, so it's universal with zero per-element code (a per-host MutationObserver re-applies on attr change).
Testing
The harness has no DOM lib (happy-dom/jsdom absent), so tests/index.test.ts only asserts registration + pure iconSvg. Verify behavior in a real browser. .focus() fires no focus events when document.hasFocus() is false (headless) — dispatch a composed focusin to exercise focus-show.