Entry point for AI agents working on @cube-dev/ui-kit.
- Package:
@cube-dev/ui-kit - Repository: cube-js/cube-ui-kit
- Storybook: cube-ui-kit.vercel.app
- Styling engine: Tasty (
@tenphi/tasty) - Palette engine: Glaze (
@tenphi/glaze)
Maintenance note: The design-system reference (tokens, presets, colors, modifiers, state syntax, form system, icons) lives in
src/stories/Usage.docs.mdx(Storybook → Getting Started / Usage). The component creation guide lives insrc/stories/CreateComponent.docs.mdx(Getting Started / Create Component). Update these whenever you add components, change the API surface, or modify tokens/presets.
Run this at the start of every task, before reading code, running tests, or trusting any type error:
pnpm install && pnpm rebuild esbuildA working copy can sit idle across dependency bumps, so node_modules may not match pnpm-lock.yaml. A stale tree does not fail loudly — it silently inverts results. Tests pass locally and fail on CI (or the reverse), and tsc reports errors that do not exist on the pinned version. Anything you conclude from a stale tree is unreliable, including the conclusion that a failure is "pre-existing".
When local results and CI disagree, suspect the dependency tree first. Compare the installed version against the lockfile before theorising about anything else:
pnpm list @tenphi/tasty @tenphi/glazeProject-specific working rules for AI agents. Not published with the package.
- coding.md — development flow, code style, knowledge maintenance
- input-components.md — form-attachable input components (
useFieldProps, validation props,wrapWithField) - storybook.md —
.stories.tsxand.docs.mdxauthoring, including theplay-function rule below - documentation.md —
.docs.mdxstructure + update flow - tests.md — Vitest + React Testing Library patterns
- probe.md —
pnpm probe: inspecting the HTML, CSS and tokens a render actually produces - commit-changes.md — commit message convention
- eslint-plugin.md — the shipped lint rule + the defaults registry it checks against
When making code changes that affect end users or the public API, always add a new changeset or update an existing one in .changeset/ as part of the same task. Do not wait to be asked.
- Prefer updating an existing open changeset that already covers the same work/PR; otherwise add a new
.mdfile. - Use
patchfor bug fixes and small changes;minorfor new features and noticeable breaking changes. - Keep the summary concise and user-focused (
"@cube-dev/ui-kit": patch|minorfrontmatter). - Skip changesets for docs-only, test-only, Storybook-only, or internal tooling that does not affect package consumers. Also skip fixes for issues introduced and resolved within the same PR.
- Add changeset manually (no CLI) — full guidelines:
.cursor/commands/add-changeset.md.
A state that only exists during an interaction — an open tooltip, a hover or focus style, an expanded overlay — is invisible to Chromatic unless a play function puts the story into it. Chromatic runs play before it snapshots, so a story whose point is such a state must drive it with play and end on a waitFor assertion for the state itself (that assertion is also the wait Chromatic needs). Drive one element per story — only the final state is captured. For tooltips, copy the recipe verbatim: timeout(250) (the trigger is wired in a mount effect), unhover before hover (React Aria ignores a hover until a mouse move sets the pointer modality), and delay: 0 in the tooltip config. Both caveats fail silently, and a local render test can pass while the story does not — Chromatic is the check. Full pattern: storybook.md.
Answering "what CSS does this actually produce?" is one call, not a throwaway vitest spec: pnpm probe styles '{"fill":"#purple"}' (also tokens, render, globals). It loads the real config — the configure() and setGlobalPredefinedStates() from src/components/Root.tsx and the live Glaze palette. Never verify a style through node -e "import {renderStyles} from '@tenphi/tasty'" — that path loads none of it, so units, recipes, presets and tokens go unresolved and the output looks authoritative while being wrong.
Use it as freely as you like, either tier: it is a throwaway question-answering tool, so nothing here needs justifying. It is also the honest way to check a styling claim before you write it into a doc or a review. pnpm probe:browser adds the four things jsdom cannot do — computed values, geometry, pointer behaviour, screenshots — at the cost of a browser binary and a slower start.
Read docs/rules/probe.md for the modes, the jsdom blind spots that make an answer jsdom's rather than the truth, the one-time pnpm exec playwright install chromium, and how the *.browser.test.tsx suite differs (that one is in CI, and the probe's freedom does not extend to it).
The DOM helpers behind it live in src/probe/ and ship as the @cube-dev/ui-kit/probe entry, which Cube Cloud's own yarn probe imports. Changing them is a change to published API.
src/
├── components/ # actions, content, fields, form, layout, navigation,
│ # organisms, overlays, status, helpers, portal, other, shared
├── icons/ # 130+ icon components
├── shared/ # Form types (FieldBaseProps, FormBaseProps, FieldCoreProps)
├── tokens/ # Design tokens (colors, typography, spacing, sizes, shadows, layout)
├── stories/ # Storybook guides and documentation pages
├── _internal/ # Internal hooks (useEvent, etc.)
├── tasty-augment.d.ts # TypeScript module augmentation for tasty
└── index.ts # Public barrel export
Each component lives in src/components/{category}/{ComponentName}/ and ships ComponentName.tsx, .stories.tsx, .docs.mdx, .test.tsx, and index.tsx.
pnpm storybook— start Storybook on port 6060pnpm build— build library (tsdown, unbundled ESM)pnpm test— run all tests (Vitest); add-- ComponentNameto filter,-uto update snapshotspnpm test:browser— run the*.browser.test.tsxspecs in real Chromium (layout, pointer, observers)pnpm probe— print the HTML, CSS or tokens a snippet actually produces, with the real config loaded.pnpm probe:browserfor computed values, geometry and screenshots. See Inspecting Rendered HTML & CSSpnpm fix— lint + format (Oxlint + Prettier)pnpm size— check bundle size limitspnpm chromatic— visual regressionpnpm add-icons— add new icons from tablerpnpm audit-docs— audit component API ↔ docs ↔ argTypes sync. Options:--component=Name,--fix-stories,--fix-docs,--json,--verbose,--all-props. Run after changing a component's API or adding a new component.pnpm audit-defaults— regenerate the lint plugin's defaults registry (src/eslint-plugin/defaults.generated.ts). Run whenever you change a default prop value.pnpm testfails until the registry matches what the components actually render — see eslint-plugin.md.pnpm run update-tasty/pnpm run update-glaze— bump and pin@tenphi/tastyor@tenphi/glazeto the latest version. Pass--version=X.Y.Zto pin a specific version.
- Node 24, pinned in
.nvmrcand used by every CI job including publish — OIDC trusted publishing requires npm ≥ 11.5.1, which Node 24 ships and Node 22 (npm 10.x) does not. pnpm^10, pinned topnpm@10.34.5viapackageManager; no Corepack involved. Note the published package still declaresengines.node >=22.0.0— that is the floor for consumers, not for building this repo. - After
pnpm install, runpnpm rebuild esbuild(postinstall is blocked inpnpm-workspace.yaml). Do this at the start of every task — see Before You Start. - Husky hooks:
pre-commitrunspnpm lint-staged;pre-pushrunspnpm test. Skip only intentionally (--no-verifyorHUSKY=0). - No external services or databases required for local development.
Bundled in docs/tasty/ (symlinked from node_modules/@tenphi/tasty/docs in dev, copied at pack time by scripts/prepare-docs.mjs). Consult these when authoring components or working with style props:
getting-started.md,methodology.md,design-system.md,react-api.md,dsl.md,styles.md,configuration.md,debug.md
Internal-only references (pipeline.md, injector.md), positioning material (comparison.md, adoption.md), and modes the UI Kit does not ship (tasty-static.md, ssr.md) are intentionally omitted; read them directly in docs/tasty/ if needed.
Bundled in docs/glaze/ (same symlink/copy mechanism). Consult when working on color tokens, theme generation, or contrast tuning — Glaze powers src/tokens/palette.ts.
- Styling:
@tenphi/tasty(declarative token-aware CSS-in-JS) - Accessibility:
react-aria+react-stately - Icons:
@tabler/icons-react+ custom icons insrc/icons/ - Testing: Vitest + React Testing Library + Chromatic
- Build: tsdown (unbundled ESM,
es2022) - Storybook: v10 (
@storybook/react-vite) - React: 18 and 19 supported
See src/stories/CreateComponent.docs.mdx (Storybook → Getting Started / Create Component) for the full guide: styleProps vs extractStyles, filterBaseProps, modifiers, sub-elements, React Aria integration, variants, useEvent, and complete examples.
See src/stories/Usage.docs.mdx (Storybook → Getting Started / Usage) for units, base/spacing/size/shadow/layout tokens, color tokens, typography presets, themes, recipes, modifiers, state syntax, icons, and the form system.
Full rules in src/i18n/README.md. The short version:
- Scope: strings a component renders. Anything the component itself puts in
front of a user — visible text,
aria-label,aria-roledescription, live-region announcements,title— goes throughuseI18n():t('component.key', 'English default'). The inline English stays as a belt-and-braces fallback. - Not for stories, docs, or tests. Storybook stories,
.docs.mdx, and specs are demo and fixture copy, not product UI. Use plain literals there — a locale key that exists only to feed a story is noise in twelve files, and a test that reads its expectation from the bundle asserts nothing about the string. - Component props that expose a label stay overrides that win over the
translated default:
emptyLabel = t('...', 'No items'). - All 12 locales, every time.
en-USis the source of truth;locale-parity.test.tsfails CI if any locale's key set or{{interpolation}}tokens diverge. Interpolation is{{double}}braces with no ICU, so plurals need separate keys rather than a plural rule. - If a string doubles as a DOM selector, build the selector from the same
t(...)value so the two cannot drift when the language changes.
- Module augmentation:
src/tasty-augment.d.tsextends@tenphi/tastywith project-specific color tokens, preset names, and theme names. - Props naming:
Cube{ComponentName}Props. ExtendBaseProps/AllBasePropsfrom@tenphi/tasty; mix in style-prop interfaces (ContainerStyleProps,OuterStyleProps,ColorStyleProps, …) as needed. Form types live insrc/shared/. - Barrel exports: every category has an
index.ts; everything re-exports throughsrc/index.ts. - Compound components:
Object.assign(Button, { Group: ButtonGroup, Split: ButtonSplit }). - Tasty re-exports: only types are re-exported. Runtime imports (
tasty,extractStyles,filterBaseProps) come directly from@tenphi/tasty. Aria*Propsfromreact-ariasilently resolve toany.tsconfig.jsonsetspreserveSymlinks: true, so TS resolvesreact-aria's re-exports from the symlink path and never finds the@react-aria/*subpackages (they are not direct dependencies);skipLibCheckthen hides the failure. Consequences:interface X extends AriaFooPropscontributes no members (keyof Xdrops them), whileOmit<AriaFooProps, …>becomes an index signature that accepts anything. Either way those props are unchecked. So declare the Aria props a component genuinely supports — seeToggleSelectionPropsinsrc/shared/form.ts, which restoresonChangeforSwitch/Checkbox. RemovingpreserveSymlinksis the real fix but surfaces ~320 previously-hidden errors across ~60 files, so it needs its own migration.
- Helpers:
renderWithRoot(wraps with<Root>),renderWithForm(returns{ formInstance, ...renderResult }). - QA selectors:
qaprop →data-qaattribute →screen.getByTestId('name')(testIdAttributeis configured todata-qa). - Tasty snapshots:
toMatchTastySnapshot()captures markup + CSS together. - Not every question needs a spec. If you only want to see what something renders, run
pnpm probeinstead of writing a spec to delete afterwards. - Patterns: see docs/rules/tests.md.