# UI System This document defines the responsibility boundary for the shared visual-system package under `packages/ui/*`. ## Purpose The shared UI system package exists to hold product-facing visual foundations that are reused across renderer surfaces. The current package is: - `packages/ui/system` It is organized into two public component layers: - `base` Basic visual primitives and foundations such as tokens, icons, Button, Input, Dialog, Select, Card, Badge, and Toast. - `business` Multi-end reusable Tutti business display components. These components may expose domain display props such as workspace, file, task, or agent state, and should compose `base` primitives instead of recreating them. The package owns: - design tokens - shared theme styles - icon exports - presentation primitives - reusable business display components It does not own: - business logic or host-side side effects - app-specific workflows - daemon, Electron, router, or host adapter calls ## Sortable foundation `Sortable`, `SortableContent`, `SortableItem`, `SortableItemHandle`, and `SortableOverlay` are stable `base` primitives installed from the Dice UI shadcn registry and narrowly adapted to Tutti's input, accessibility, and reduced-motion contracts. The primitive owns sensors, drag state, layout strategies, handles, overlays, and caller-provided announcements. Consumers own business ordering, persistence, optimistic updates, localized copy, and error recovery. The adopted Dice UI revision and the Radix/Lucide source-derived helpers are recorded in the package `NOTICE`. Update that provenance whenever registry source is refreshed. Storyboard coverage is the visual inventory for enabled, disabled, keyboard-focus, and overlay states. ## Avatar foundation `Avatar` is the single high-level identity-image primitive. It owns decorative image loading, rendered-box-aware image transformation, broken-image fallback, initial or empty fallback modes, loading presentation, named or numeric sizing, and a caller-owned overlay slot through `children`. Sources are requested unchanged with a `no-referrer` request policy by default; callers may override that policy through `imageProps` for a source that requires it. When the caller passes `transform`, HTTP(S) sources request a bucketed two-times-size WebP variant with `fit=inside`, while preserving the original query string. A failed variant retries the original source once. Consumers own identity resolution, directly consumable image URLs, transformation capability, labels, fallback colors, presence semantics, and overlay content. They never pass buckets or object keys to the component. The underlying Radix/shadcn parts are intentionally not public. `fallbackColor` is the narrow exception for caller-owned dynamic identity data; component defaults still use semantic tokens. Initial fallback content appears only when both the delivered image and its original fallback are unavailable, never while a valid image URL is still loading. `NativeAvatar` is the React Native counterpart for the same identity-image contract. It uses Native semantic tokens and finite control-backed sizes, keeps the image decorative, shows loading feedback while a valid source is pending, and falls back to the caller's stable label when the source is absent or fails. `NativeProgressBar` is the React Native progress primitive. It clamps determinate values to the inclusive zero-to-one range, exposes progressbar accessibility semantics, and accepts `null` for an indeterminate task. Product phase labels, byte counts, cancellation, and workflow state stay caller-owned. `NativeControlGlyph` is the decorative React Native control-glyph primitive. Its finite variants cover add, back, chevron, send, status, and stop shapes; callers supply the semantic token color and keep the localized accessible name on the owning control. Chevron direction is explicit, and glyph content remains hidden from the accessibility tree. ## Current Package Role `@tutti-os/ui-system` is the single source of truth for: - CSS token definitions - structural shared workbench CSS in `packages/workbench/surface/src/styles/workbench.css` - structural shared terminal CSS in `packages/workspace/terminal/src/styles/terminal.css` - Tailwind-facing semantic theme variables - shared SVG and icon APIs - shadcn-derived React primitives Desktop renderer code should consume this package instead of defining a second token or primitive layer in `apps/desktop`. The visual language that this package should serve is defined in [Desktop Visual Language](../../../docs/conventions/desktop-visual-language.md). ## Public API `@tutti-os/ui-system` should expose a small, stable surface: - `@tutti-os/ui-system` Root runtime entry for shared primitives, icon components, and the small set of utility exports that primitives depend on - `@tutti-os/ui-system/styles.css` Shared stylesheet entry loaded by renderer shells - `@tutti-os/ui-system/native.css` NativeWind stylesheet entry loaded once by React Native renderer shells - `@tutti-os/ui-system/native` React Native primitive and semantic-token entry; this is separate from the DOM component entry and must not be imported by desktop renderers. Native consumers provide React Native; `react-dom` is optional for this platform-specific entry. - `@tutti-os/ui-system/components` Stable component barrel for tooling and rare category-focused imports - `@tutti-os/ui-system/icons` Stable icon barrel for tooling and rare category-focused imports - `@tutti-os/ui-system/utils` Stable utility entry for shadcn CLI integration and primitive support code - `@tutti-os/ui-system/metadata` Tooling entry for component metadata used by storyboard, dev server, and agent skills - `@tutti-os/ui-system/dev-vite` Development-tooling entry for external Vite apps that opt into local source sync; this is not a runtime component API Default consumption rules: - application code should prefer importing primitives and icons from `@tutti-os/ui-system` - renderer entrypoints should import `@tutti-os/ui-system/styles.css` once - React Native renderer entrypoints should import `@tutti-os/ui-system/native.css` once - React Native components and screens should import Native primitives and semantic tokens from `@tutti-os/ui-system/native`, never from a private app palette - shadcn monorepo aliases may target `@tutti-os/ui-system/components` and `@tutti-os/ui-system/utils` - application runtime code must not import `@tutti-os/ui-system/dev-vite` - consumers must not deep import `@tutti-os/ui-system/src/*` or component file paths; use the root package and stable subpaths instead ## Automated Enforcement The repository enforces the shared UI boundary with: - `pnpm check:ui-boundaries` - `pnpm check:ui-boundaries:staged` Use the script output as the source of truth for the mechanically-checkable rules. - `check:ui-boundaries:staged` is the fast local hook variant for staged files - `check:ui-boundaries` is the full-repository variant for `pre-push` and CI - `@tutti-os/workbench-surface/styles.css`, `@tutti-os/workspace-terminal/styles.css`, and `@tutti-os/agent-gui/styles.css` are the only non-UI-system package stylesheets allowed by the boundary check - `@tutti-os/workbench-surface/styles.css` and `@tutti-os/workspace-terminal/styles.css` should remain structural and variable-driven, not product-branded - `@tutti-os/agent-gui/styles.css` is a deliberate package contract for the carried agent GUI and workspace-agent panel selectors that still rely on package-owned class names Keep this check aligned with the package exports and file boundary rules: - if a new stable public subpath is intentionally added, update both `packages/ui/system/package.json` and the import-check script - do not "fix" the script by broadening the allowed list unless the package boundary itself is intentionally changing ## Metadata Rules Every public UI-system metadata entry must have a stable `id`. - `id` is the user-facing tooling identifier for storyboard anchors, dev server metadata, and agent skill lookups - `id` must be globally unique and use readable kebab-case, for example `button`, `dialog-content`, `button-variants`, or `styles-css` - `id` should not be derived at runtime by consumers; read it from `@tutti-os/ui-system/metadata` - `name` and `export` remain the TypeScript API identity, while `id` is the stable human-readable inventory identity - `layer` must be either `base` or `business` - `business` components may use business display nouns in their props, but should still receive data, labels, status, and callbacks from the consuming host ## Component Promotion Protocol Use this protocol when moving UI from an application or business package into `@tutti-os/ui-system`. The UI system follows a shadcn-like self-owned source model: - Radix or equivalent headless primitives provide accessible interaction behavior where a proven primitive exists - CVA-style variant definitions keep component variants explicit and typed - Tailwind classes consume shared semantic tokens instead of local palettes - Tutti owns the checked-in source, public exports, metadata, storyboard examples, and boundary validation Before extracting a component, decide whether the target is `base`, `business`, or not suitable for UI-system promotion. Promote to `base` when the component is a foundation primitive: - it has no product workflow or domain noun in its public contract - props describe presentation, interaction, accessibility, variants, refs, slots, class names, or children - the component can be reused by unrelated surfaces without explaining a business concept - a known shadcn or Radix primitive can provide the starting point, or the exception is documented in review Promote to `business` when the component is a reusable business display unit: - it represents a cross-surface Tutti concept such as workspace, file, task, agent, run, project, or account display state - it receives all business data, labels, statuses, permissions, and callbacks from the host through props - it receives user-visible copy through props, labels, or children instead of introducing new hardcoded product strings inside the shared component body - it composes `base` components for buttons, fields, dialogs, cards, icons, and overlays instead of rebuilding those primitives locally - it can be rendered in storyboard with controlled sample data and no daemon, router, store, or host adapter Business promotion uses a copy-first workflow. Move the existing application component structure into `@tutti-os/ui-system` as intact as possible, preserve the source DOM hierarchy, visual states, and interaction layout, then remove host-owned dependencies and standardize the API incrementally. Do not start by inventing a cleaner abstraction or new visual treatment. Storyboard should first prove source/screenshot parity; only then should props be generalized into stable data, labels, actions, variants, and slots. Promotion is not a redesign. The promoted UI must preserve the original design unless the user explicitly approves a visual change. Do not add decoration, layout chrome, controls, icons, copy, motion, states, spacing, or hierarchy that cannot be traced to the source component or source screenshot. UI-system tokens and primitives should replace local styling only when they keep the same observed design and interaction path. Copy-first promotion includes the candidate's dependent display subcomponents and third-party-library wrappers. Pure presentational helpers should migrate with the business component. Reusable wrappers around Radix, floating UI, resizable panels, drag/drop, virtualization, editor shells, or similar libraries should become or reuse `base` primitives before the business component composes them. Host-coupled children must be split into caller-owned data, labels, callbacks, or slots rather than rewritten from memory inside the promoted component. Do not promote a component when it owns: - daemon, Electron, filesystem, router, or host-adapter calls - data fetching, cache mutation, persistence, or polling - global app store ownership - workspace registration, navigation, onboarding, or other app workflow orchestration - product copy that cannot be supplied by props or children For every promoted component, define this contract before editing code: - component `id` and `layer` - source usage being replaced - intended reuse surfaces - public props and callbacks - host-owned state and side effects that remain outside the component - stable export path - metadata entry - storyboard states and examples - validation commands The bundled `packages/ui/system/agent/tutti-ui-system/SKILL.md` skill is the standard prompt-level workflow for this judgment-heavy promotion step. Boundary scripts are still required, but they only catch mechanical violations. ## API Shape And Composition The public API must be derived from the source state matrix and intended reuse surfaces, not from every conditional branch in the original component. - Avoid boolean prop proliferation for rendering modes. Standard UI booleans such as `disabled`, `loading`, `selected`, `open`, `required`, or `invalid` are acceptable when they represent real state. Mode switches such as `isFoo`, `showBar`, and `withBaz` should usually become a finite variant, discriminated union, explicit component variant, slot, or composed child. - Use explicit variants when combinations would otherwise create impossible states. Prefer a narrow discriminated union or named component variant over a component that accepts unrelated mode booleans. - Prefer `children` and named slots for caller-owned visual regions. Use render props only when the shared component must pass data back to the caller. - User-visible copy must stay caller-owned by default. Do not add hardcoded product strings inside `@tutti-os/ui-system` components when the text can be supplied by `labels`, `title`, `description`, `children`, or other explicit props. If a legacy-compatible fallback string must exist temporarily, treat it as compatibility debt rather than the preferred API pattern. - Use compound components and context only when the component is complex enough that consumers need to assemble subparts while sharing state. Do not add a provider for a simple card, row, badge, or button wrapper. - If shared state is necessary, make the context contract narrow and injectable: `state`, `actions`, and `meta`. State implementation remains outside the visual subcomponents, and host-owned daemon, Electron, router, store, query, persistence, filesystem, or workflow behavior remains outside the UI-system package. - For new components, follow the repository's React 19 baseline, including `ref` as a prop where a ref is part of the public contract. Do not rewrite shadcn or Radix-acquired components only to chase API style parity. Document the API shape decision before promotion: which state axes became variants, props, slots, children, compound subcomponents, provider state, or host-owned caller logic. ## Token Rules - `src/tokens/renderer-theme.json` is the single semantic-token manifest for the shared Web/Native subset. It generates Web CSS renderer variables and Native light/dark palettes; renderer-specific literal values may differ when their color syntaxes or current visual languages require it. Do not duplicate these values in application code. - `NativeThemeProvider` resolves the operating-system scheme by default, or a caller-provided `"light" | "dark"` product preference. Native primitives and Mobile composition consume `useNativeTheme()` rather than application-local palettes. `nativeTheme` remains only as a dark compatibility value for non-rendering callers; this does not yet promise full Desktop/Mobile visual parity. - `pnpm check:ui-boundaries` verifies generated renderer-token outputs stay in sync with the manifest. Regenerate them with `node tools/scripts/check-ui-renderer-tokens.mjs --write` before formatting and committing a manifest change. - Shared theme styles must support both host-managed `html[data-theme="light" | "dark"]` and workspace-app CSS `prefers-color-scheme`; explicit `data-theme` values should override system preference when both are present. - Tailwind utilities should consume the same token layer rather than defining a parallel color system - prefer semantic token names such as `background`, `foreground`, `primary`, `muted`, and `destructive` over raw palette leakage in public APIs - keep tutti-specific token extensions additive and minimal - Build primitives for a calm workbench shell, not for marketing-card theatrics. ### Native renderer review The DOM storyboard cannot render React Native primitives faithfully. Each public Native primitive must therefore have a concrete example in the Mobile development gallery and be reviewed on a Native renderer before it is promoted from experimental status. The gallery must import only the stable `@tutti-os/ui-system/native` entrypoint and render the primitive's public states; it is the Native counterpart of a DOM storyboard example, not a second component implementation. `NativeSheet` uses React Native's window-level `Modal` for its controlled overlay and does not require a portal provider. It owns the semantic scrim, bottom-aligned panel, system-back dismissal, screen-reader escape gesture, accessible backdrop dismissal, keyboard avoidance, and optional fixed `height`. Its window-level keyboard container uses padding on iOS and height reduction on Android, with no caller-supplied keyboard height. Callers must supply the localized `closeAccessibilityLabel`; one fixed height replaces the former single-value `snapPoints` usage. Keep `@gorhom/bottom-sheet` app-owned for genuinely complex gesture, animated keyboard, or multi-snap-point sheets rather than routing the shared compact sheet through its React 19-incompatible portal. For cross-surface stacking, use shared semantic `z-index` tokens instead of local magic numbers. Current global layer tokens live in `packages/ui/system/src/styles/theme.css` and should be the default source of truth for: - workbench chrome overlays - popovers and menus - toasts - full-panel overlays - dialogs and their backdrops When a surface needs a new global layer, add a responsibility-named token to the shared theme rather than introducing another raw `z-[12345]` value in app code. Small component-internal stacking such as `z-1` on a pseudo-element can stay local when it does not participate in global overlay ordering. ### Z-Index Design Rules Treat `z-index` as an ordering system, not as a per-component escape hatch. Use these rules: - use shared global tokens when a layer can overlap content owned by another component, feature, portal, or renderer surface - use package-local or component-local variables when the layer only needs to order parts inside one isolated surface - keep local decorative layering simple; values such as `0`, `1`, `2`, or `3` are acceptable when they never compete with global overlays - do not introduce new raw high values such as `9999`, `10000`, or `z-[12345]` in app code - prefer a new responsibility-named token over “one bigger number” when an existing global layer is not sufficient Questions to ask before adding or changing a layer: 1. Can this element ever overlap a portal, popover, toast, dialog, or another feature-owned overlay? 2. Is this a global interaction layer or only internal ordering inside one component? 3. Would another engineer understand the layer’s purpose from its token name alone? If the answer to the first question is yes, the layer should almost always use a shared global token. ### Current Global Layers The current shared global `z-index` tokens are: - `--z-workbench-chrome` Top or bottom workbench chrome rendered above window content but below global popovers and dialogs. - `--z-workbench-genie` The genie animation layer that must stay above ordinary workbench chrome. - `--z-popover` Cross-feature floating UI such as menus, switchers, and layout popovers. - `--z-panel` Full-panel overlays such as workspace settings surfaces. - `--z-panel-popover` Popovers or menus that are portaled from within a full-panel overlay and must stay above the panel but below dialog backdrops. - `--z-dialog-overlay` Dialog backdrops that should dim or block panel surfaces beneath them. - `--z-dialog` Dialog content rendered above dialog backdrops. - `--z-dialog-popover` Floating controls or host-owned preview windows that must stay above dialog content and backdrop while remaining below tooltips. - `--z-toast` Toast notifications that must remain visible above dialogs and their portaled controls while staying below tooltips. - `--z-tooltip` Short hover/focus guidance that should stay above panels, drawers, and their popovers so clipped text can be inspected across overlay boundaries. These tokens are intentionally semantic and ordered by responsibility, not by visual implementation detail. ### Local Layering Rules Local layers should stay local when they only solve ordering inside one bounded surface. Good examples: - a selected tile border inside one settings grid - a resize handle above adjacent pane content inside one file manager surface - a tooltip above its own dock icon inside one workbench dock - background decorations behind launcher content For these cases: - prefer package-local variables such as `--workbench-z-dock-tooltip` or `--workspace-file-manager-dialog-overlay-z-index` - keep the numeric scale tight and relative to the owning surface - do not promote a local layer into the global theme unless another surface needs to reason about it ### Migration Rules When touching existing code: - replace raw high `z-index` values with the nearest existing semantic token when the layer is globally meaningful - if no existing token matches, add a new one in `packages/ui/system/src/styles/theme.css` and document it here - if the layer is local-only, prefer a package or component variable instead of a new global token - remove transitional duplicate declarations such as a Tailwind `z-*` class plus an overriding inline `style.zIndex` ## Reusable Package Styling Rules Reusable packages outside `packages/ui/*` should not create their own visual systems. They should consume `@tutti-os/ui-system` primitives, icons, tokens, and token-backed Tailwind utilities as their default styling model. For reusable packages that render Tailwind utility classes, consumers are responsible for including the published package output in Tailwind source scanning. For example, a consumer of `@tutti-os/workbench-surface` should include that package's built output in its Tailwind entrypoint or equivalent build configuration: ```css @source "../node_modules/@tutti-os/workbench-surface/dist"; ``` Within this monorepo, reusable packages that require consumer Tailwind scanning should declare that requirement in their `package.json`: ```json { "tutti": { "tailwindSourceRoot": "src" } } ``` `pnpm check:ui-boundaries` validates that the desktop renderer entrypoint `apps/desktop/src/renderer/src/style.css` includes matching `@source` directives for any imported workspace package that declares `tutti.tailwindSourceRoot`. It also validates that the path matches the declared source root and reports the exact `@source` line to add or replace. Tailwind source troubleshooting checklist: - if a reusable package introduces new utility classes but the desktop UI does not change at runtime, confirm the package declares `tutti.tailwindSourceRoot` when it renders runtime Tailwind classes - confirm the desktop renderer Tailwind entrypoint includes the package source path through `@source` - re-run `pnpm check:ui-boundaries` before assuming the issue is a hot-reload or build-cache problem Package-local CSS in reusable packages is an exception, not the default. Add it only when the package needs selectors, keyframes, or structural behavior that is awkward to express through UI system primitives and Tailwind utilities. When package-local CSS is necessary: - use UI system CSS variables or Tailwind-facing semantic tokens - keep the CSS structural and package-responsibility-specific - do not define raw palette values, a second token layer, or app-specific visual roles - do not include product styling, product copy, or app-specific state concepts - document the public stylesheet entrypoint in the package README and package release docs If a reusable package repeatedly needs visual primitives or new semantic tokens, prefer moving that foundation into `@tutti-os/ui-system` before adding more package-local CSS. ## Design Foundation Compliance All components promoted into `@tutti-os/ui-system` must fully follow the existing design foundations owned by this package. Promotion must align with the shared token model, theme variables, spacing rhythm, radius scale, typography, surface language, focus states, disabled states, and the existing `base` primitive vocabulary. Do not introduce a second visual language during promotion. Avoid raw palette values, ad hoc spacing scales, local radius conventions, duplicate button or field treatments, or component-specific CSS that should be expressed through existing tokens or primitives. Current shared control contracts: - routine `Button` and `Input` surfaces use `32px` height, the shared `6px` `--radius-md`, token-backed text and surface colors, and no decorative shadows - chrome icon buttons stay compact, transparent by default, and use the shared chrome text/hover states rather than app-local canvas colors - text fields use `--transparency-block` for the default surface and `--transparency-hover` for hover/focus surfaces - destructive controls use semantic danger tokens such as `--state-danger` and `--state-danger-hover` - `UnderlineTabs` labels and counts use `font-medium`, so English contexts render at `font-weight: 500`; CJK contexts remain aligned through the global CJK emphasis rule After a component is promoted, start an independent design-foundation review subagent before reporting completion. Give the subagent the promoted component files, source usage, selected states, storyboard entry, metadata entry, and this document. The subagent should verify that the component follows the design foundation and report any drift. If a subagent cannot be started in the current environment, report the verification as blocked instead of claiming full design-foundation compliance. ## Component Rules - `base` primitives should stay low-level and presentation-focused - `business` components may include reusable business display semantics, but must stay host-agnostic and side-effect-free - `packages/ui/system` is the repository's shared Radix and shadcn host package - for primitives that exist in the shadcn registry, start from shadcn CLI output targeted at `packages/ui/system`; do not hand-author a fresh component body when the upstream primitive can be downloaded - keep `packages/ui/system/components.json` healthy enough that `pnpm dlx shadcn@latest add -c packages/ui/system` remains the default acquisition path - treat CLI-generated source as the canonical starting point; repository-specific edits should stay narrow and mechanical, such as package import aliases, stable barrel exports, icon-layer routing, and token-backed class adjustments required by boundary checks - if a desired primitive is not available from shadcn, prefer composing directly from `radix-ui` inside `packages/ui/system` and document that exception in the change review or follow-up docs - if the current package structure makes CLI acquisition awkward, fix the host package structure or configuration first instead of silently replacing the workflow with a handwritten primitive - keep primitive APIs close to upstream shadcn patterns unless product-specific constraints require a deviation - do not place app-specific workflows such as launcher flows, workspace registration, or route-owned panels in the shared package - do not add new hardcoded user-visible copy inside `@tutti-os/ui-system` components when the text can be supplied by props, `labels`, `title`, `description`, or `children`; keep translation lookup and copy selection in the caller - export UI-system components through stable package barrels instead of exposing per-file component paths - only move a component into `@tutti-os/ui-system` when it is a real visual-system primitive or reusable business display component with more than one plausible consumer - every public component, icon, utility, style entry, or tooling-visible UI export must have metadata in `packages/ui/system/src/metadata/components.json` - metadata `source` paths must point at existing files under `packages/ui/system/src` and `from` must use a stable public entrypoint - run `node tools/scripts/check-ui-metadata.mjs` or `pnpm check:ui-boundaries` after adding, removing, or renaming UI-system public exports ### Primitive Sourcing Workflow Use this workflow when adding or replacing a shared primitive: 1. Run shadcn CLI against `packages/ui/system` when the primitive exists in the registry. 2. Keep the downloaded component body as the baseline implementation. 3. Apply only the minimum package-specific adaptation required to satisfy repository rules. 4. Export the primitive through the stable `@tutti-os/ui-system` barrels. 5. Re-run `pnpm check:ui-boundaries` after the adaptation pass. Repository-specific adaptation is allowed for: - replacing direct third-party icon imports with `@tutti-os/ui-system` icon exports when the UI boundary check requires it - switching import aliases to package-local `#components`, `#icons`, or `#lib` paths - aligning classes with shared CSS tokens or other repository-owned boundary rules Repository-specific adaptation is not a reason to skip CLI acquisition. The rule of thumb is: - download first - adapt second - do not handwrite the primitive body from scratch unless there is no upstream shadcn primitive to start from ### Business Component Workflow Use this workflow when promoting reusable business UI: 1. Read existing `@tutti-os/ui-system/metadata` and storyboard entries first. 2. Reuse existing `base` and `business` components when they cover the need. 3. Keep host state, side effects, data loading, routing, and daemon calls in the original app or package. 4. Extract only the reusable display surface and typed callback contract. 5. Compose `base` primitives for controls, overlays, cards, icons, and layout affordances. 6. Add metadata with `layer: "business"` and a readable stable `id`. 7. Add storyboard examples that cover empty, loading, normal, disabled, and error-like display states when those states are part of the public contract. 8. Replace the original duplicated UI with a stable public import from `@tutti-os/ui-system`. Business components should look like controlled React components: ```tsx ``` They should not reach back into the host: ```tsx useWorkspaceStore(); useNavigate(); invokeDaemon(); fetch("/api/workspaces"); localStorage.setItem("workspace", id); ``` ## Promotion Review Gate Use this gate for every base or business component promotion. It adapts frontend design review practice to Tutti's product standard; do not import general marketing, portfolio, or decorative frontend heuristics into the shared workbench system. ### Frictionless - The migrated consumer preserves the original task path and interaction count unless the user approved a behavior change. - Primary, secondary, destructive, cancel, and recovery actions keep their visual hierarchy and remain reachable by keyboard. - The shared component does not introduce dead ends, hidden required steps, or new caller-owned state requirements. ### Quality Craft - Selected states have before/after visual parity evidence from the source route, storyboard, or smallest reproducible view. - The implementation uses existing `@tutti-os/ui-system` primitives and canonical tokens before adding component-local CSS. - Light, dark, focus-visible, hover, disabled, invalid, selected, loading, and reduced-motion states are covered when they exist in the public contract. - Layout, density, spacing, radius, typography, icon sizing, border, color, opacity, shadow, and responsive behavior have no unapproved drift. - Any intentional delta is named, justified, and reported as approved rather than hidden inside the promotion. ### Trustworthy - Loading, empty, disabled, error-like, and permission-limited states keep clear labels and actionable recovery where the source had them. - AI-generated or inferred content keeps provenance, confidence, or disclaimer treatment host-owned and visible when applicable. - Error copy and status labels are supplied by the caller or labels props; the shared component must not invent product policy or workflow meaning. - New user-visible copy should enter the component boundary through props, labels, or children rather than new hardcoded strings inside the shared component. - The component boundary leaves daemon, Electron, router, persistence, query, filesystem, polling, i18n lookup, and workflow orchestration in the host. Report the promotion review in this structure: - context: component id/layer, source usage, user task, selected states, and intended reuse surfaces - summary: pass, needs work, or blocked - pillar assessment: Frictionless, Quality craft, Trustworthy - design-system compliance: tokens, primitives, metadata, storyboard, stable exports, and public imports - issues: blocking, major, and minor, with concrete file references - validation: exact commands and results - risks: uncovered states, unavailable visual evidence, unresolved subagent review, or approved visual deltas. ## Agent Skill Rules Use the bundled `tutti-ui-system` skill for prompt-level work involving `@tutti-os/ui-system`. The source lives under `packages/ui/system/agent/tutti-ui-system/SKILL.md` so it can ship with the UI system package. The skill should route internally across these scenarios: - use an existing UI-system component - extract a new `base` component - extract a new `business` component - maintain metadata, ids, exports, or storyboard coverage The skill must treat this document, `packages/ui/AGENTS.md`, metadata, and boundary scripts as the source of truth. It should not duplicate long copies of the rules; it should point the coding agent to the right files, force the base/business decision, and require validation. External business repositories that promote UI into the shared system should add a short agent instruction such as: ```md When promoting business UI into @tutti-os/ui-system, use the tutti-ui-system skill and follow packages/ui/system/ui-system.md. ``` After installing `@tutti-os/ui-system`, external repositories can configure the bundled skill with: ```bash pnpm exec tutti-ui-system-install-skill ``` The command copies the bundled skill into `.codex/skills/tutti-ui-system` in the current repository. When `.tutti-ui-system-dev/` is present, the installer prefers the synced source checkout so the skill and bundled UI-system rules stay aligned with the current local UI-system source. It refuses to overwrite local changes unless run with `--force`. ## Storyboard Rules `apps/ui-storyboard` is the local component inventory and example surface for `@tutti-os/ui-system`. - the component list, categories, statuses, and inventory counts must come from `@tutti-os/ui-system/metadata` - storyboard navigation should group component stories by `layer` - examples may stay hand-written so they can show realistic composition and edge states - visible component stories should display the component `id` prominently and support copying it from the UI - the storyboard should import public UI-system entrypoints and metadata, not private component file paths - keep the storyboard as a development surface; it should not become a product shell or marketing page ## External Dev Server Rules The UI-system dev server exists only for local external development. It lets an external app keep normal `@tutti-os/ui-system` imports while temporarily resolving the stable entrypoints to a generated local cache. - start it with `pnpm --filter @tutti-os/ui-system dev:server` - external Vite apps opt in with `tuttiUISystemDev` from `@tutti-os/ui-system/dev-vite` - when the server is unavailable, external apps must fall back to their installed package in `node_modules` - the generated `.tutti-ui-system-dev/` cache belongs in the external app's `.gitignore` - Tailwind consumers must include both the installed package output and the generated dev cache in source scanning, for example `@source "../node_modules/@tutti-os/ui-system/dist";` and `@source "../.tutti-ui-system-dev";` - do not make CI, production builds, or package publishing depend on the dev server - `@tutti-os/ui-system/dev-vite` may be imported only from bundler config or tooling files ## Icon Rules - renderer-visible icons should be exported through the root package or the stable `@tutti-os/ui-system/icons` barrel - promoted components and storyboard examples must consume icons from `@tutti-os/ui-system/icons`; do not leave inline SVG/data URI icons, app-local icon assets, or direct third-party icon imports in promoted UI - generic system icons may wrap a third-party icon set - product marks and custom status glyphs should live as local SVG components in the package - icons should default to `currentColor` unless a specific token-driven treatment is required ## Review Heuristics When reviewing a change under `packages/ui/*`, prefer these checks: - is the new export responsibility-named and stable enough to support for a while - could a consumer do the same work through the root package instead of a new subpath - is a proposed helper really part of primitive support, or should it stay local to one app - does the change make the package easier to consume without exposing its folder layout ## Review Questions When reviewing a change under `packages/ui/*`, ask: 1. Is this a visual-system concern or an app-specific component? 2. Does this change preserve CSS tokens as the source of truth? 3. Is the package API still narrow, stable, and responsibility-named? 4. Would this be clearer if it stayed local to one app instead? 5. Are consumers being nudged toward the root package and stable barrels instead of internal paths?