Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

791 lines
38 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <component> -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
<WorkspaceSummaryCard
workspace={workspace}
status={workspaceStatus}
disabled={!canOpenWorkspace}
onOpen={handleOpenWorkspace}
/>
```
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?