@denindka/picker
v0.1.0
Published
Reusable vanilla color picker: zero-DOM core logic/store + DOM view binder (delegated listeners + semantic callbacks). Shared across products on the same vanilla-TS stack.
Downloads
104
Maintainers
Readme
@denindka/picker
Reusable color picker (and the plumbing for other dropdowns) for our vanilla-TS shell stack (the F1 shell + its twin product). No React, no other frameworks — by design.
This stack does not bundle browser code: it builds one HTML document and inlines client JS as
strings into a single <script>. So this package is consumed at shell-generation time (Node):
you call its string-builders and drop the results into your HTML. The browser only ever runs the
inlined string — it never imports anything.
Two layers:
./core— zero-DOM logic, data, and the state store. Pure, unit-tested../ui— the DOM binder (delegated listeners +attachColorDialog) and the client-script builder.
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/picker";
// code stays in this repo → from "../../packages/picker/index.js"
// published to a private registry → from "@denindka/picker"
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— done, smoke-tested (the smoke builds the IIFE, runs it in a mock browser, and asserts a click firesonChange). - ⬜
renderColorDialogHtml(props): string— a packaged builder for the dialog markup (step 1 above). Planned; it's a mechanical port of the shell's existingrenderFormatColorDialog. Until then, produce the markup yourself per the DOM-contract table.
Other dropdowns (font-family, …)
Same idea: build their client script with the shared primitive bindDelegated(root, eventMap) and a
small store. Only the event map + store change:
bindDelegated(root, {
"click [data-grid-font-family-option]": (el) => store.setFont(el.getAttribute("data-grid-font-family-option")),
"input [data-grid-font-family-search]": (el) => store.filter(el.value ?? ""),
});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:
- This is consumed in Node (shell generation), not in browser code. The browser runs only the
inlined string — a bare
import "@denindka/picker"in browser code does NOT work on this stack (no bundler). - 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);
on this stack you consume it as source (relative import in the same repo) or npm install from a
private registry. Test: npx tsx packages/picker/smoke.ts.
Architecture
Part of the reusable-picker plan (project memory project_reusable_picker_package).
