Skip to content

Latest commit

 

History

History
174 lines (125 loc) · 14.3 KB

File metadata and controls

174 lines (125 loc) · 14.3 KB

AI Agent Reference — Cube UI Kit

Entry point for AI agents working on @cube-dev/ui-kit.

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 in src/stories/CreateComponent.docs.mdx (Getting Started / Create Component). Update these whenever you add components, change the API surface, or modify tokens/presets.

Before You Start

Run this at the start of every task, before reading code, running tests, or trusting any type error:

pnpm install && pnpm rebuild esbuild

A 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/glaze

Rules

Project-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.tsx and .docs.mdx authoring, including the play-function rule below
  • documentation.md.docs.mdx structure + update flow
  • tests.md — Vitest + React Testing Library patterns
  • probe.mdpnpm 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

Changesets

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 .md file.
  • Use patch for bug fixes and small changes; minor for new features and noticeable breaking changes.
  • Keep the summary concise and user-focused ("@cube-dev/ui-kit": patch|minor frontmatter).
  • 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.

Stories: Interaction-Only States

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.

Inspecting Rendered HTML & CSS — pnpm probe

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.

Project Structure

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.

Commands

  • pnpm storybook — start Storybook on port 6060
  • pnpm build — build library (tsdown, unbundled ESM)
  • pnpm test — run all tests (Vitest); add -- ComponentName to filter, -u to update snapshots
  • pnpm test:browser — run the *.browser.test.tsx specs 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:browser for computed values, geometry and screenshots. See Inspecting Rendered HTML & CSS
  • pnpm fix — lint + format (Oxlint + Prettier)
  • pnpm size — check bundle size limits
  • pnpm chromatic — visual regression
  • pnpm add-icons — add new icons from tabler
  • pnpm 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 test fails 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/tasty or @tenphi/glaze to the latest version. Pass --version=X.Y.Z to pin a specific version.

Environment

  • Node 24, pinned in .nvmrc and 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 to pnpm@10.34.5 via packageManager; no Corepack involved. Note the published package still declares engines.node >=22.0.0 — that is the floor for consumers, not for building this repo.
  • After pnpm install, run pnpm rebuild esbuild (postinstall is blocked in pnpm-workspace.yaml). Do this at the start of every task — see Before You Start.
  • Husky hooks: pre-commit runs pnpm lint-staged; pre-push runs pnpm test. Skip only intentionally (--no-verify or HUSKY=0).
  • No external services or databases required for local development.

Tasty Documentation

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:

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.

Glaze Documentation

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.

Stack

  • Styling: @tenphi/tasty (declarative token-aware CSS-in-JS)
  • Accessibility: react-aria + react-stately
  • Icons: @tabler/icons-react + custom icons in src/icons/
  • Testing: Vitest + React Testing Library + Chromatic
  • Build: tsdown (unbundled ESM, es2022)
  • Storybook: v10 (@storybook/react-vite)
  • React: 18 and 19 supported

Creating Components

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.

Design System Reference

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.

i18n

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 through useI18n(): 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-US is the source of truth; locale-parity.test.ts fails 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.

TypeScript & Exports

  • Module augmentation: src/tasty-augment.d.ts extends @tenphi/tasty with project-specific color tokens, preset names, and theme names.
  • Props naming: Cube{ComponentName}Props. Extend BaseProps/AllBaseProps from @tenphi/tasty; mix in style-prop interfaces (ContainerStyleProps, OuterStyleProps, ColorStyleProps, …) as needed. Form types live in src/shared/.
  • Barrel exports: every category has an index.ts; everything re-exports through src/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*Props from react-aria silently resolve to any. tsconfig.json sets preserveSymlinks: true, so TS resolves react-aria's re-exports from the symlink path and never finds the @react-aria/* subpackages (they are not direct dependencies); skipLibCheck then hides the failure. Consequences: interface X extends AriaFooProps contributes no members (keyof X drops them), while Omit<AriaFooProps, …> becomes an index signature that accepts anything. Either way those props are unchecked. So declare the Aria props a component genuinely supports — see ToggleSelectionProps in src/shared/form.ts, which restores onChange for Switch/Checkbox. Removing preserveSymlinks is the real fix but surfaces ~320 previously-hidden errors across ~60 files, so it needs its own migration.

Testing

  • Helpers: renderWithRoot (wraps with <Root>), renderWithForm (returns { formInstance, ...renderResult }).
  • QA selectors: qa prop → data-qa attribute → screen.getByTestId('name') (testIdAttribute is configured to data-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 probe instead of writing a spec to delete afterwards.
  • Patterns: see docs/rules/tests.md.