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
791 lines
38 KiB
Markdown
791 lines
38 KiB
Markdown
# 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?
|