Files
openchamber/.agents/skills/theme-system/SKILL.md
T
Bohdan Triapitsyn ca31157584 perf(tooling): add an animation cost profiler and document the harness
Adds `bun run profile:animation`: it serves an isolated fixture and measures
each animation variant directly, so comparing techniques takes seconds instead
of an application rebuild plus a streamed response.

The result is unambiguous and does not vary with element count, measured from 1
to 32: transform, opacity and filter cost zero extra style recalculations, while
the individual rotate property, background-position, border-color and box-shadow
each recalculate style 60 times a second, and geometry properties add layout on
top. Notably `rotate: 360deg` is not a cheap synonym for
`transform: rotate(360deg)`, and will-change, wrapper elements, containment and
stepped timing do not make a non-composited property cheap.

`scripts/perf/DOCUMENTATION.md` documents all four capture commands, how to
stand up a production build to measure against, how to read the artifacts, the
validity guarantees the scripts enforce, and the methodology rules, so this can
be handed to an agent as the entry point for measuring performance. It is linked
from the root guide's documentation anchors.

The theme skill gains an animation contract carrying the measured table, and the
performance skill points at the tooling documentation.
2026-08-03 18:38:58 +03:00

4.8 KiB

name, description
name description
theme-system Use when creating or modifying OpenChamber UI components, styling, colors, buttons, visual states, themes, or icons.

Theme System

Core Rules

  • Use semantic OpenChamber theme tokens; never hardcode hex colors or generic Tailwind palette colors.
  • Use shared UI primitives before introducing feature-local controls.
  • Use the shared Button; do not create button wrappers such as ButtonSmall or ButtonLarge.
  • Every dropdown-style value-picker trigger (shows current value, opens a picker) takes its chrome from dropdownTriggerVariants in packages/ui/src/components/ui/dropdown-trigger.ts (sizes: sm dense h-6, default forms h-8; native SelectTrigger consumes it). Call sites add layout classes only (width/truncation) — never re-declare border/radius/bg/hover. Deliberately chrome-less pickers (chat composer, headers) are the only exception.
  • Use the sprite-based Icon; never import icons directly from @remixicon/react.
  • Apply hover tokens only to interactive elements.
  • Use status colors only for actual status/feedback.
  • Use selection tokens for selected state and primary tokens for primary actions.

Load References By Task

Task Required reference
Choosing colors/tokens or reviewing styled examples references/tokens-and-examples.md
Adding, converting, storing, or generating icons references/icons.md
Adding built-in or custom themes references/adding-themes.md

Load every matching reference before editing. Settings work must also load settings-ui-patterns; user-facing or accessible text must load locale-ui-patterns.

Token Decision

  1. Code display -> syntax.*
  2. Error/warning/success/info -> status.*
  3. Primary CTA -> primary.*
  4. Hover/pressed/focus -> interactive.*
  5. Selected/active state -> interactive.selection*
  6. Background/text/border layer -> surface.* and semantic utility classes

Prefer CSS variables/classes for component styling. Use useThemeSystem() only when an API requires resolved color values.

Button Contract

Use Button from packages/ui/src/components/ui/button.tsx.

Variant Use
default Primary local action
outline Visible secondary action
secondary Soft secondary action
ghost Quiet row/toolbar action
destructive Destructive action
chip Compact selectable option with aria-pressed
link Rare inline text action
Size Use
xs Dense row/list control
sm Compact action
default Standard action
lg Prominent action
icon Icon-only square action

Do not hardcode button height/padding when a size variant exists. Do not recreate selection/destructive styling with ad-hoc classes.

Icon Contract

import { Icon } from '@/components/icon/Icon';

<Icon name="check" className="size-4" />

Use IconName for icon values stored in arrays, objects, state, or config. Icon has no size prop. Run bun run icons:generate when introducing a sprite name, and never edit sprite.ts manually. Load references/icons.md for the complete workflow.

Animation Contract

Animate only transform and opacity. The compositor drives those; every other property recalculates style on each frame for as long as the animation runs, and geometry properties add layout on top. Measured on this repository's fixture, identical at any element count from 1 to 32:

Animated property Style recalculations/sec Layouts/sec
transform, opacity, filter 0 0
rotate (the individual property) 60 0
background-position, border-color, box-shadow 60 0
width and other geometry 60 60
  • rotate: 360deg is not a cheap synonym for transform: rotate(360deg). Prefer the transform form.
  • Cost applies for the entire time an animation runs, so an indicator tied to a long-running operation pays it continuously. An indicator that is not conveying anything should not be animating.
  • will-change, wrapper elements, contain, and steps() timing do not make a non-composited property cheap. Only changing the property does.
  • Verify with bun run profile:animation rather than reasoning about it; add a variant to scripts/perf/animation-fixture.html for a technique not covered. See scripts/perf/DOCUMENTATION.md.

Verification

  • Animations are limited to transform and opacity, or their cost was measured and accepted.
  • No hardcoded/palette colors were introduced.
  • Buttons use shared variants and sizes.
  • Icons use Icon/IconName, and generated sprite changes are intentional.
  • Hover, selection, primary, and status semantics are distinct.
  • Light/dark/high-contrast and long-text states remain legible.
  • Relevant type-check, visual/runtime validation, and generated-asset checks ran.