diff --git a/.agents/skills/rcr-frontend/SKILL.md b/.agents/skills/rcr-frontend/SKILL.md index 8f465c47..8bc16324 100644 --- a/.agents/skills/rcr-frontend/SKILL.md +++ b/.agents/skills/rcr-frontend/SKILL.md @@ -1,6 +1,6 @@ --- name: rcr-frontend -description: Component and styling conventions for Red Cliff Record. Use when writing, reviewing, or refactoring any frontend code under `src/app/` — components, routes, styles, forms, icons, animations. Triggers on `.tsx`/`.css` edits in this project, design tokens, Panda CSS recipes, Base UI primitives, TanStack Form/Router, Lucide icons, color palettes (`data-palette`, `colorPalette`), or questions like "how do I style X in RCR". Also invoke proactively before UI work to ensure Panda-first styling and Base UI primitives, and to clean up legacy Tailwind/Radix in any file you touch. +description: Component and styling conventions for Red Cliff Record. Use when writing, reviewing, or refactoring any frontend code under `src/app/` — components, routes, styles, forms, icons, animations. Triggers on `.tsx`/`.css` edits in this project, design tokens, Panda CSS recipes, Base UI primitives, TanStack Form/Router, Lucide icons, color palettes (`palette`, `chromatic`, `mode`), or questions like "how do I style X in RCR". Also invoke proactively before UI work to ensure Panda-first styling and Base UI primitives, and to clean up legacy Tailwind/Radix in any file you touch. --- # RCR Frontend @@ -80,14 +80,14 @@ export function Toolbar({ css: cssProp }: { css?: SystemStyleObject }) { Everything lives under `src/app/styles/`: -| File | Contents | -| ---------------------------------------------------------- | -------------------------------------------------------------- | -| `colors.ts` | Semantic tokens + semantic palettes + Radix color scales | -| `typography.ts` | `textStyle` values, font families, sizes, weights | -| `dimensions.ts` | `spacing` and `sizes` (multiples of `0.25rem`, plus fractions) | -| `borders.ts` / `radii.ts` / `shadows.ts` / `animations.ts` | Their namesakes | -| `conditions.ts` | Custom Panda conditions | -| `plugins.ts` | Custom utilities (`animateIn`, `fadeIn`, `translateCenter`, …) | +| File | Contents | +| ---------------------------------------------------------- | ----------------------------------------------------------------- | +| `colors.ts` | Color engine: Radix hue ramps, palettes, semantic formulas | +| `typography.ts` | `textStyle` values, font families, sizes, weights | +| `dimensions.ts` | `spacing` and `sizes` (multiples of `0.25rem`, plus fractions) | +| `borders.ts` / `radii.ts` / `shadows.ts` / `animations.ts` | Their namesakes | +| `conditions.ts` | Custom Panda conditions | +| `plugins.ts` | Custom utilities (`palette`, `chromatic`, `mode`, `animateIn`, …) | ### Semantic color tokens (use these, not Radix scales directly) @@ -95,28 +95,27 @@ Everything lives under `src/app/styles/`: Use them as token names in Panda (`color: 'primary'`, `backgroundColor: 'float'`). The `c-*` names (`bg-c-paper`, `text-c-primary`) are the Tailwind aliases — only relevant while removing Tailwind from legacy components. In Panda code, `c-paper` doesn't exist; the equivalent is `float` or `surface` depending on intent (check existing usage). -### Palette-aware components via `colorPalette` and `layerStyle` +### Theme boundaries via `palette`, `chromatic`, and `mode` -Two style properties steer semantic tokens within a subtree. Use them as Panda CSS properties — not HTML attributes. +Three inherited axes steer every semantic color token. Set any of them anywhere in the tree and the whole subtree re-resolves against the new context until a descendant sets that axis again. -| Property | Values | Effect | -| -------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -| `colorPalette` | `artifact`, `entity`, `concept`, `error`, `success`, `info` | Remaps every semantic color under this element to the chosen palette (tokens like `accent`, `main`, `splash` resolve through that palette). | -| `layerStyle` | `chromatic`, `neutral` | Picks the chromatic or neutral variant of the active palette. Activates the `_chromatic` / `_neutral` conditions inside recipes. | +| Utility | Values | Effect | +| ----------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `palette` | `artifact`, `entity`, `concept`, `error`, `success`, `info` | Selects the hue ramps (chromatic + associated neutral) that semantic tokens like `accent`, `main`, `splash` resolve through. | +| `chromatic` | `true`, `false` | Switches every semantic token between its chromatic and neutral variant. | +| `mode` | `dark`, `light`, `inverted`, `normal` | Sets `color-scheme`; all colors are `light-dark()` pairs, so the subtree flips instantly. | ```tsx - + Error - - {/* Uses the chromatic variant of the `info` palette inside */} + + {/* Everything inside uses the chromatic variant of the `info` palette */} ``` -Inside a recipe, gate a palette swap on a variant or condition — e.g. `_invalid: { colorPalette: 'error' }` on button/textarea input states. - -**Escape hatch — `data-palette` / `data-chromatic`.** These HTML attributes still work (they're wired up in `src/app/styles/theme.css` and `conditions.ts`) and are fine when the element is rendered outside Panda's reach: third-party components that only accept HTML attributes, markdown-generated DOM, server-rendered bootstrap wrappers, etc. For anything authored with `css({ ... })` or `styled.*`, prefer `colorPalette` and `layerStyle`. +They work identically inside recipes — e.g. `_invalid: { palette: 'error', chromatic: true }` on input states. Each boundary also re-asserts `color: primary` at base-layer precedence, so plain inherited text adapts across the boundary while any explicit `color` still wins. ### Text styles @@ -140,12 +139,10 @@ Numeric tokens are `0.25rem` multiples: `0, 0.25, 0.5, 0.75, 1, 1.5, 2, 2.5, 3, | ------------------------------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `_dark` | `:where([data-color-scheme="dark"], .dark) &` | Color-scheme-scoped overrides | | `_light` | `:where([data-color-scheme="light"], .light) &` | Color-scheme-scoped overrides | -| `_neutral` | `&[data-neutral]` (+ scope variants) | Triggered by `layerStyle: 'neutral'` (or `data-neutral` as escape hatch). | -| `_chromatic` | `&[data-chromatic]` (+ scope variants) | Triggered by `layerStyle: 'chromatic'` (or `data-chromatic` as escape hatch). | | `_childIcon` | `& :where(svg, .icon, .lucide)` | Size/color icons inside a container. Prefer over `& svg`. | | `_sideBottom` / `_sideTop` / `_sideLeft` / `_sideRight` | `&[data-side=]` | Radix/popper-positioned content (popover, dropdown, hover-card) — directional slide-in animation per placement. Use these instead of raw `'&[data-side=bottom]'` selectors. | -Reach for a defined condition over a raw `[data-*]` selector — but **first check Panda's built-ins so you don't duplicate one**: `_vertical` / `_horizontal` (`[data-orientation]`), `_open` / `_closed` (`[data-state]`), `_hover`, `_focusVisible`, `_disabled`, `_invalid`, `_placeholder`, and the `_group*` / `_peer*` families all ship out of the box. Only when Panda has nothing (e.g. the project's `_side*`, `_chromatic` / `_neutral`) add it to `styles/conditions.ts` (and `bun run stylegen`) rather than inlining the attribute selector in a recipe. +Reach for a defined condition over a raw `[data-*]` selector — but **first check Panda's built-ins so you don't duplicate one**: `_vertical` / `_horizontal` (`[data-orientation]`), `_open` / `_closed` (`[data-state]`), `_hover`, `_focusVisible`, `_disabled`, `_invalid`, `_placeholder`, and the `_group*` / `_peer*` families all ship out of the box. Only when Panda has nothing (e.g. the project's `_side*`, `_childIcon`) add it to `styles/conditions.ts` (and `bun run stylegen`) rather than inlining the attribute selector in a recipe. Useful utilities (full list in `src/app/styles/plugins.ts`): diff --git a/package.json b/package.json index e18e1f29..b5d9aa6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aias/red-cliff-record", - "version": "0.5.10", + "version": "0.5.11", "license": "MIT", "author": { "name": "Nick Trombley", diff --git a/panda.config.ts b/panda.config.ts index 98659894..b559b9bd 100644 --- a/panda.config.ts +++ b/panda.config.ts @@ -22,7 +22,7 @@ import { tooltipRecipe } from '@/app/components/tooltip/tooltip.recipe'; import { easings, durations, keyframes } from '@/app/styles/animations'; import { blurs } from '@/app/styles/blurs'; import { borders } from '@/app/styles/borders'; -import { colors, semanticColors } from '@/app/styles/colors'; +import { colors } from '@/app/styles/colors'; import { conditionsPreset } from '@/app/styles/conditions'; import { spacing, sizes } from '@/app/styles/dimensions'; import { globalStyles } from '@/app/styles/globals'; @@ -51,16 +51,6 @@ export default defineConfig({ presets: [conditionsPreset], globalCss: globalStyles, - staticCss: { - css: [ - { - properties: { - layerStyle: ['chromatic', 'neutral'], - palette: ['artifact', 'entity', 'concept', 'error', 'success', 'info'], - }, - }, - ], - }, theme: { extend: { tokens: { @@ -78,17 +68,9 @@ export default defineConfig({ sizes: sizes, spacing: spacing, }, - semanticTokens: { - colors: semanticColors, - }, }, keyframes: keyframes, textStyles: textStyles, - layerStyles: { - // Empty declarations, only for autocomplete and class name generation - chromatic: { value: {} }, - neutral: { value: {} }, - }, breakpoints: { sm: '40rem', md: '48rem', diff --git a/src/app/components/button/button.recipe.ts b/src/app/components/button/button.recipe.ts index 4cae9161..a887858a 100644 --- a/src/app/components/button/button.recipe.ts +++ b/src/app/components/button/button.recipe.ts @@ -28,7 +28,8 @@ export const buttonRecipe = defineRecipe({ boxSize: '[1.15em]', }, _invalid: { - colorPalette: 'error', + palette: 'error', + chromatic: true, }, _focusVisible: { borderColor: 'focus', diff --git a/src/app/components/dropdown-menu/dropdown-menu.recipe.ts b/src/app/components/dropdown-menu/dropdown-menu.recipe.ts index 7e9fa992..61c5985f 100644 --- a/src/app/components/dropdown-menu/dropdown-menu.recipe.ts +++ b/src/app/components/dropdown-menu/dropdown-menu.recipe.ts @@ -88,8 +88,8 @@ export const dropdownMenuRecipe = defineSlotRecipe({ paddingInline: '2', '&[data-inset]': { paddingInlineStart: '8' }, '&[data-variant=destructive]': { - colorPalette: 'error', - layerStyle: 'chromatic', + palette: 'error', + chromatic: true, color: 'accent', _highlighted: { backgroundColor: 'splash', diff --git a/src/app/components/input/input.recipe.ts b/src/app/components/input/input.recipe.ts index 580c7a03..f4614ee9 100644 --- a/src/app/components/input/input.recipe.ts +++ b/src/app/components/input/input.recipe.ts @@ -39,8 +39,8 @@ export const inputRecipe = defineRecipe({ outlineWidth: '2px', }, _invalid: { - layerStyle: 'chromatic', - colorPalette: 'error', + palette: 'error', + chromatic: true, }, _selection: { backgroundColor: 'main', diff --git a/src/app/components/media-upload.tsx b/src/app/components/media-upload.tsx index 7519e86d..74ceefe4 100644 --- a/src/app/components/media-upload.tsx +++ b/src/app/components/media-upload.tsx @@ -197,8 +197,8 @@ export const MediaUpload = ({ backgroundColor: 'main/10', }, '&[data-error]': { - layerStyle: 'chromatic', - colorPalette: 'error', + palette: 'error', + chromatic: true, borderColor: 'accent', }, _loading: { diff --git a/src/app/components/textarea/textarea.recipe.ts b/src/app/components/textarea/textarea.recipe.ts index 9eeedac5..ce3b3cbd 100644 --- a/src/app/components/textarea/textarea.recipe.ts +++ b/src/app/components/textarea/textarea.recipe.ts @@ -23,7 +23,8 @@ export const textareaRecipe = defineRecipe({ color: 'muted', }, _invalid: { - colorPalette: 'error', + palette: 'error', + chromatic: true, }, _disabled: { opacity: '50%', diff --git a/src/app/components/toggle/toggle.recipe.ts b/src/app/components/toggle/toggle.recipe.ts index fbc8730a..72e30be7 100644 --- a/src/app/components/toggle/toggle.recipe.ts +++ b/src/app/components/toggle/toggle.recipe.ts @@ -25,12 +25,12 @@ export const toggleRecipe = defineRecipe({ _pressed: { backgroundColor: 'splash', color: 'accent', - layerStyle: 'chromatic', + chromatic: true, }, _disabled: { pointerEvents: 'none', opacity: '50%', - layerStyle: 'neutral', + chromatic: false, }, _focusVisible: { borderColor: 'focus', diff --git a/src/app/routes/-app-components/app-layout.tsx b/src/app/routes/-app-components/app-layout.tsx index 61475583..df0c2bda 100644 --- a/src/app/routes/-app-components/app-layout.tsx +++ b/src/app/routes/-app-components/app-layout.tsx @@ -52,7 +52,7 @@ export const AppLayout = ({ children, currentTheme, onThemeChange }: AppLayoutPr )} {isError && ( - + Error loading records. )} diff --git a/src/app/routes/records/$recordId.tsx b/src/app/routes/records/$recordId.tsx index 11e09eb5..1e96600e 100644 --- a/src/app/routes/records/$recordId.tsx +++ b/src/app/routes/records/$recordId.tsx @@ -334,7 +334,7 @@ function RouteComponent() { - + Record not found This record may have been deleted or moved. diff --git a/src/app/routes/records/-components/form.tsx b/src/app/routes/records/-components/form.tsx index 0950371b..4aea03b2 100644 --- a/src/app/routes/records/-components/form.tsx +++ b/src/app/routes/records/-components/form.tsx @@ -336,8 +336,8 @@ export function RecordForm({ {field.state.meta.errors && ( @@ -515,8 +515,8 @@ export function RecordForm({ {field.state.meta.errors && ( diff --git a/src/app/routes/records/-components/record-metabar.tsx b/src/app/routes/records/-components/record-metabar.tsx index 0e8abaa5..b5436446 100644 --- a/src/app/routes/records/-components/record-metabar.tsx +++ b/src/app/routes/records/-components/record-metabar.tsx @@ -144,7 +144,7 @@ export const Metabar = ({ recordId, onDelete, css: cssProp, ...props }: MetabarP render={