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.
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 asButtonSmallorButtonLarge. - Every dropdown-style value-picker trigger (shows current value, opens a picker) takes its chrome from
dropdownTriggerVariantsinpackages/ui/src/components/ui/dropdown-trigger.ts(sizes:smdense h-6,defaultforms h-8; nativeSelectTriggerconsumes 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
- Code display ->
syntax.* - Error/warning/success/info ->
status.* - Primary CTA ->
primary.* - Hover/pressed/focus ->
interactive.* - Selected/active state ->
interactive.selection* - 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: 360degis not a cheap synonym fortransform: rotate(360deg). Prefer thetransformform.- 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, andsteps()timing do not make a non-composited property cheap. Only changing the property does.- Verify with
bun run profile:animationrather than reasoning about it; add a variant toscripts/perf/animation-fixture.htmlfor a technique not covered. Seescripts/perf/DOCUMENTATION.md.
Verification
- Animations are limited to
transformandopacity, 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.