Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 21 additions & 24 deletions .agents/skills/rcr-frontend/SKILL.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -80,43 +80,42 @@ 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)

`display`, `primary`, `secondary`, `muted`, `symbol`, `accent`, `accentActive`, `background`, `surface`, `container`, `float`, `divider`, `border`, `edge`, `focus`, `mist`, `splash`, `flood`, `main`, `mainActive`, `mainContrast`.

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
<styled.span css={{ colorPalette: 'error', backgroundColor: 'splash', color: 'accent' }}>
<styled.span css={{ palette: 'error', chromatic: true, backgroundColor: 'splash', color: 'accent' }}>
Error
</styled.span>

<styled.section css={{ colorPalette: 'info', layerStyle: 'chromatic' }}>
{/* Uses the chromatic variant of the `info` palette inside */}
<styled.section css={{ palette: 'info', chromatic: true }}>
{/* Everything inside uses the chromatic variant of the `info` palette */}
</styled.section>
```

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

Expand All @@ -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=<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`):

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aias/red-cliff-record",
"version": "0.5.10",
"version": "0.5.11",
"license": "MIT",
"author": {
"name": "Nick Trombley",
Expand Down
20 changes: 1 addition & 19 deletions panda.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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: {
Expand All @@ -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',
Expand Down
3 changes: 2 additions & 1 deletion src/app/components/button/button.recipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export const buttonRecipe = defineRecipe({
boxSize: '[1.15em]',
},
_invalid: {
colorPalette: 'error',
palette: 'error',
chromatic: true,
},
_focusVisible: {
borderColor: 'focus',
Expand Down
4 changes: 2 additions & 2 deletions src/app/components/dropdown-menu/dropdown-menu.recipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 2 additions & 2 deletions src/app/components/input/input.recipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ export const inputRecipe = defineRecipe({
outlineWidth: '2px',
},
_invalid: {
layerStyle: 'chromatic',
colorPalette: 'error',
palette: 'error',
chromatic: true,
},
_selection: {
backgroundColor: 'main',
Expand Down
4 changes: 2 additions & 2 deletions src/app/components/media-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ export const MediaUpload = ({
backgroundColor: 'main/10',
},
'&[data-error]': {
layerStyle: 'chromatic',
colorPalette: 'error',
palette: 'error',
chromatic: true,
borderColor: 'accent',
},
_loading: {
Expand Down
3 changes: 2 additions & 1 deletion src/app/components/textarea/textarea.recipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export const textareaRecipe = defineRecipe({
color: 'muted',
},
_invalid: {
colorPalette: 'error',
palette: 'error',
chromatic: true,
},
_disabled: {
opacity: '50%',
Expand Down
4 changes: 2 additions & 2 deletions src/app/components/toggle/toggle.recipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion src/app/routes/-app-components/app-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const AppLayout = ({ children, currentTheme, onThemeChange }: AppLayoutPr
<LinkButton
variant="solid"
css={{
layerStyle: 'chromatic',
chromatic: true,
_childIcon: {
opacity: '75%',
},
Expand Down
3 changes: 1 addition & 2 deletions src/app/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,7 @@ function RootDocument({
return (
<html
className={css({
colorPalette: 'artifact',
layerStyle: 'neutral',
palette: 'artifact',
})}
data-color-scheme={appearance}
data-theme-transitioning={isTransitioning || undefined}
Expand Down
2 changes: 1 addition & 1 deletion src/app/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function Home() {
/>
)}
{isError && (
<styled.p css={{ colorPalette: 'error', layerStyle: 'chromatic', color: 'accent' }}>
<styled.p css={{ palette: 'error', chromatic: true, color: 'accent' }}>
Error loading records.
</styled.p>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/app/routes/records/$recordId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ function RouteComponent() {
<styled.div
css={{ display: 'flex', flex: '1', alignItems: 'center', justifyContent: 'center' }}
>
<styled.div css={{ textAlign: 'center', colorPalette: 'error', layerStyle: 'chromatic' }}>
<styled.div css={{ textAlign: 'center', palette: 'error', chromatic: true }}>
<styled.div css={{ marginBlockEnd: '2', color: 'accent' }}>Record not found</styled.div>
<styled.div css={{ textStyle: 'sm', color: 'muted' }}>
This record may have been deleted or moved.
Expand Down
8 changes: 4 additions & 4 deletions src/app/routes/records/-components/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,8 @@ export function RecordForm({
{field.state.meta.errors && (
<styled.p
css={{
colorPalette: 'error',
layerStyle: 'chromatic',
palette: 'error',
chromatic: true,
textStyle: 'sm',
}}
>
Expand Down Expand Up @@ -515,8 +515,8 @@ export function RecordForm({
{field.state.meta.errors && (
<styled.p
css={{
colorPalette: 'error',
layerStyle: 'chromatic',
palette: 'error',
chromatic: true,
textStyle: 'sm',
}}
>
Expand Down
2 changes: 1 addition & 1 deletion src/app/routes/records/-components/record-metabar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export const Metabar = ({ recordId, onDelete, css: cssProp, ...props }: MetabarP
render={
<Button
variant="solid"
css={{ colorPalette: 'error', layerStyle: 'chromatic' }}
css={{ palette: 'error', chromatic: true }}
onClick={handleDelete}
>
Continue
Expand Down
37 changes: 21 additions & 16 deletions src/app/styles/borders.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { defineTokens } from '@pandacss/dev';

const semanticBorders = ['divider', 'border', 'edge', 'focus'] as const;

export const borders = defineTokens.borders({
none: { value: 'none' },
divider: {
DEFAULT: { value: '1px solid {colors.divider}' },
thin: { value: '0.5px solid {colors.divider}' },
},
border: {
DEFAULT: { value: '1px solid {colors.border}' },
thin: { value: '0.5px solid {colors.border}' },
},
edge: {
DEFAULT: { value: '1px solid {colors.edge}' },
thin: { value: '0.5px solid {colors.edge}' },
},
focus: {
DEFAULT: { value: '1px solid {colors.focus}' },
thin: { value: '0.5px solid {colors.focus}' },
},
...Object.fromEntries(
semanticBorders.map((name) => [
name,
{
DEFAULT: { value: `1px solid {colors.${name}}` },
thin: { value: `0.5px solid {colors.${name}}` },
},
])
),
});

// Border tokens wrap semantic colors, and custom properties bake their var()
// substitutions at the declaring element — so theme boundaries must re-declare
// them alongside the color formulas to re-resolve against the new context.
export const borderDeclarations: Record<`--${string}`, string> = Object.fromEntries(
semanticBorders.flatMap((name) => [
[`--borders-${name}`, `1px solid var(--colors-${name})`],
[`--borders-${name}-thin`, `0.5px solid var(--colors-${name})`],
])
);
Loading