@denindka/fast-ui
v0.2.2
Published
A reusable, framework-agnostic vanilla-TS UI component library — a growing home for shared UI components (dumb components: render + attach + CSS + one-call mount, over a zero-DOM core). Shared across products; built to be extended. First components: colou
Maintainers
Readme
@denindka/fast-ui
A reusable, framework-agnostic UI component library for our vanilla-TS stack — the F1/fastsheet shell and its fastdoc twin. It's the single home for shared UI components, designed to grow: any developer adds a new one by following the same pattern (→ AUTHORING.md). No React, no other frameworks — by design.
The components shipped so far are an advanced colour dialog, a Word/Excel colour dropdown,
and a font-family menu — but the library and its pattern are the product, not those particular
components; expect more (and feel free to add them). Every component is a dumb component (like a
controlled React component, but vanilla): renderXxxHtml(props) + attachXxx(root, callbacks) +
XXX_CSS (+ a one-call mountXxx), over a zero-DOM core.
Two kinds of consumer, one source:
- Bundler apps (fastdoc — esbuild / Vite / webpack): import components straight from
@denindka/fast-ui/ui(and/core); use themount*helpers orrender*+attach*. → see INTEGRATION.md. - No-bundler shells (F1 — inlines client JS as strings): import the markup builders
(
renderXxxHtml) at Node shell-generation time and inline the behaviour as an IIFE from@denindka/fast-ui/node(createXxxClientScript()). Markup + CSS are byte-identical to the bundler path — same single source. (This path is detailed below.)
Layers: ./core (zero-DOM logic/data/store, unit-tested) · ./ui (render + attach* + CSS +
mount* — browser-safe) · ./node (Node-only client-script builders).
Start here — who reads what
| You are… | Start with |
|---|---|
| Using these components in an app (e.g. fastdoc) | INTEGRATION.md — install, mount* helpers (colour + font), the boundary |
| Creating / maintaining a shared component (F1/fastsheet) | AUTHORING.md — the extract→verify→swap recipe — and docs/UI_COMPONENT_PLACEMENT.md for where it belongs |
| Publishing a release | PUBLISHING.md |
The rest of this README is the overview + the no-bundler (F1) consumption path.
How you use it (this stack)
In your shell generator (the Node file that builds the HTML), import the builders and wire 3 spots:
import { createColorPickerClientScript } from "@denindka/fast-ui";
// in-repo (the F1 shell) → from "../../packages/fast-ui/index.js"
// every other consumer → from "@denindka/fast-ui" (npm install)
const html = `
...your shell...
<!-- 1) the dialog markup (must carry the data-grid-* attributes from the contract below) -->
<div data-grid-format-color-dialog> ... swatches / tabs / preview / OK / Cancel ... </div>
<script>
${createColorPickerClientScript()} /* 2) the picker runtime, inlined as an IIFE */
/* 3) wire YOUR product's callbacks: */
__layersPicker.attachColorDialog(
document.querySelector("[data-grid-format-color-dialog]"),
{
defaultValue: "#FF2600",
onChange: (hex) => preview(hex), // live, on every change
onCommit: (hex) => applyFill(hex), // OK pressed
onClose: () => hideDialog(),
}
);
</script>
`;That's the whole integration. You write markup + one inlined runtime + your callbacks. You never
write addEventListener, never touch the data-grid-* wiring, never write color math.
createColorPickerClientScript() builds that IIFE on demand with esbuild (a build-time dep) from
the real ui/core modules — so there's no duplicated logic. After it runs, the page has
window.__layersPicker.attachColorDialog(root, handlers).
DOM contract (what your markup must expose)
| Attribute | Element | Behavior |
|-----------|---------|----------|
| data-grid-format-color-dialog | dialog root | passed to attachColorDialog |
| data-grid-format-color-dialog-choice="#RRGGBB" | swatch / pencil | click → set color |
| data-grid-format-color-dialog-tab="<tab>" | tab button | click → switch tab |
| data-grid-format-color-dialog-hex-input | text input | input → set from hex |
| data-grid-format-color-dialog-spectrum | spectrum surface | pointerdown → pick by x/y |
| data-grid-format-color-dialog-apply | OK button | click → commit |
| data-grid-format-color-dialog-cancel | Cancel button | click → cancel |
| data-grid-format-color-dialog-preview | swatch preview | patched: --format-dialog-current-color |
| data-grid-format-color-dialog-name | label | patched: resolved color name |
| data-grid-format-color-dialog-pane="<tab>" | tab pane | patched: hidden toggled by active tab |
<tab> ∈ wheel | sliders | palettes | image | pencils. This matches the F1 shell exactly, so its
existing markup + e2e proofs keep working.
API
createColorPickerClientScript({ minify? }): string — the browser runtime as an inline-<script>
IIFE. Exposes window.__layersPicker.attachColorDialog.
attachColorDialog(root, options): controller (the function exposed on the global):
options: {
value? | defaultValue?: string; // seed color (uncontrolled — the binder owns state)
defaultTab?: "wheel"|"sliders"|"palettes"|"image"|"pencils";
recents?: string[];
translate?: (key, fallback) => string; // for color names (i18n)
rememberOnCommit?: boolean; // push committed color into recents (default true)
onChange?(hex, source); onCommit?(hex); onCancel?(); onClose?(); onTabChange?(tab); onRecentAdd?(hex);
}
controller: { setColor(hex); getColor(); getState(); subscribe(fn); destroy(); }onChange = live (every click/drag); onCommit = confirmed (OK). Call destroy() on teardown.
./core (pure, for the markup side / tests): createColorPickerStore, hslToHex/hsvToHex/
rgbToHex/hexToRgb/normalizeHex, spectrumPointerToColor, pencilShade/renderColorPencilSvg,
resolveColorName, and data (NAMED_COLORS, PENCIL_COLORS, WEB_SAFE_COLORS, PASTEL_COLORS,
DEVELOPER_COLORS, PALETTE_COLLECTIONS).
Status / roadmap
- ✅
createColorPickerClientScript()+attachColorDialog+core— smoke-tested (the smoke builds the IIFE, runs it in a mock browser, and asserts a click firesonChange). - ✅
renderColorDialogHtml(props)+COLOR_DIALOG_CSS— the dialog markup + CSS builders (verbatim fromrenderFormatColorDialog); F1 renders from them, byte-identical. Same for the palette dropdown and the font-family menu (renderFontFamilyMenuHtml/attachFontFamilyMenu/mountFontFamilyMenu) — both products render the same component. - ✅
mount*one-call helpers — consumption proven live in fastdoc (colour + font menus).
Other components
The font-family menu is already a full shared component — renderFontFamilyMenuHtml +
attachFontFamilyMenu + FONT_FAMILY_MENU_CSS (+ mountFontFamilyMenu), the exact menu F1 renders.
The Word/Excel colour dropdown is renderColorPaletteMenuHtml + attachColorPaletteMenu +
COLOR_PALETTE_MENU_CSS. To add a NEW one, follow AUTHORING.md; the reusable
primitive underneath is bindDelegated(root, { "click [selector]": (el) => … }) over a data-grid-*
contract plus a core store.
For AI assistants
To add the color picker to a shell on this stack:
- Emit markup with the
data-grid-format-color-dialog-*attributes (see the contract table). - Inline
${createColorPickerClientScript()}inside the shell's<script>block. - Call
__layersPicker.attachColorDialog(root, { onCommit, onChange, ... })with the product's callbacks.
Rules:
- On the no-bundler F1 stack this is consumed in Node (shell generation); the browser there runs
only the inlined string. Bundler apps (fastdoc) DO import
@denindka/fast-ui/ui//coredirectly in browser code — both paths are first-class (see INTEGRATION.md). Only@denindka/fast-ui/node(esbuild builders) must never reach browser code. - Consumers pass callbacks, never DOM listeners; never reimplement color math/state (that's
core). uiuses structural DOM interfaces (PickerElement,PickerDomEvent), neverdocument/HTMLElement/CustomEvent.
Build / publish
See PUBLISHING.md. It's a real npm package (npm run build → dist/ with .d.ts):
the in-repo F1 shell consumes it by relative source import; every other consumer runs
npm install @denindka/fast-ui. Test: npx tsx packages/fast-ui/smoke.ts.
Architecture
Part of the reusable-picker plan (project memory project_reusable_picker_package).
