Files
ditto.site/.opencode/skills/css-to-tailwind/SKILL.md
T
2026-06-29 15:11:48 -07:00

129 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: css-to-tailwind
description: Rules for converting resolved computed CSS from the capture into Tailwind v3 classes on Next.js components. Decides when to use arbitrary values vs extending tailwind.config.ts, how to handle gradients / shadows / filters / clamp / variable fonts / container queries, and when to bail to globals.css. Load this in every Stage 1 or Stage 2 generation run.
user-invocable: false
---
# CSS → Tailwind
The generate agent receives a DOM fragment with computed styles already resolved per node. Your job is to turn those styles into Tailwind classes on the JSX output — no cascade reasoning required.
## Core decision: utility vs arbitrary vs config vs CSS file
| Situation | Approach |
| ---------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| Value matches a default Tailwind scale step (e.g. `p-4`, `gap-8`) | Use the utility |
| Value is used 1-2 times site-wide and does not match scale | Arbitrary: `w-[347px]` |
| Value is used 3+ times site-wide | Extend `tailwind.config.ts`, then use the token |
| Cannot be expressed in Tailwind (complex `filter`, `@property`, `@supports`, dynamic `attr()`) | Write to `globals.css` |
The manifest's `design_tokens` object is already deduplicated by frequency — use it as the source of truth for what goes into `tailwind.config.ts`.
## Specific rules
### Heights and widths — use intent over resolved px
The capture's computed styles resolve `vh`/`vw`/`%`/`calc()` to literal pixel values at the capture viewport (1280×720). **Do not blindly transcribe these.** The manifest gives you better signals:
1. **`section.vh_relative === true`**: the section's height is viewport-height-derived. Map `section.vh_value` to Tailwind:
- `vh_value` ≈ 100 → `min-h-screen` (or `h-screen` if the source had `height` not `min-height`)
- `vh_value` between 50 and 99 → `min-h-[<vh>vh]` arbitrary, e.g. `min-h-[88vh]`
- `vh_value` < 50 → still `min-h-[<vh>vh]` but consider whether content alone determines the height (if so, omit the height entirely and let content size it)
2. **`section.full_width === true`**: use `w-full` (or `w-screen` for true full-bleed elements that escape parent padding). Never write `w-[1280px]`.
3. **`capture/css-rules/<vp>.json`** has the source intent verbatim. When in doubt for a specific element, read the rule that matched its selector. Look for `100vh`, `100%`, `calc(...)`, `clamp(...)`, `aspect-ratio` — these are signals to NOT use the literal px value.
Heuristic for elements not in `vh_relative_elements` but suspected vh-relative: if `bounding_box.height` is close to `viewport_height * N` for `N ∈ {0.5, 0.75, 1.0}` and the element is a top-level section with no other content driving height, prefer the vh form. Cheaper to over-flag here than to leave a brittle px height.
### Colors
- Every color in `manifest.design_tokens.colors` gets promoted into `theme.extend.colors` under its semantic name.
- Reference as `bg-primary`, `text-primary`, `border-accent-1`, etc. in class output.
- Unseen one-off colors: arbitrary, e.g. `text-[#3a3a3a]`.
### Typography
- Font families: promote to `theme.extend.fontFamily`. Use `next/font/local` in `layout.tsx` with the downloaded woff2 files and expose via CSS variable, then reference as `['var(--font-display)']` etc.
- Font sizes: promote clusters to `theme.extend.fontSize` as named keys (`display`, `h1`, `body`, `small`). Value is `[fontSize, { lineHeight, letterSpacing, fontWeight }]`.
- One-off sizes: `text-[32px]/[1.2]` arbitrary.
### Spacing
- Infer the scale from `manifest.design_tokens.spacing[]`. Often 4px / 8px / 16px / 24px ... but match what the site actually uses.
- Extend Tailwind's spacing scale only for values not already close to a default step.
- Use `gap-*`, `p-*`, `m-*`, `space-y-*` utilities.
### Gradients
- Linear: `bg-gradient-to-r from-[#...] via-[#...] to-[#...]` for simple 2-3 stops. For more stops or angled variants, use arbitrary `bg-[linear-gradient(135deg,#a_0%,#b_50%,#c_100%)]`.
- Radial: `bg-[radial-gradient(...)]` arbitrary.
- Conic: `bg-[conic-gradient(...)]` arbitrary.
- Mesh / multi-layer: wrap in `bg-[image:...]` syntax with escaped commas.
### Shadows
- Single-layer shadows that match scale: `shadow-md`, `shadow-xl`.
- Multi-layer shadows: arbitrary with escaped commas: `shadow-[0_1px_2px_rgba(0,0,0,0.05),_0_4px_8px_rgba(0,0,0,0.08)]`.
- Promote 3+ occurrences of the same shadow to `theme.extend.boxShadow`.
### Filters and backdrop-filter
- Built-in: `blur-*`, `brightness-*`, `contrast-*`, `grayscale-*`, `backdrop-blur-*`, `backdrop-saturate-*`.
- Arbitrary: `filter-[blur(16px)_saturate(140%)]`, `backdrop-filter-[blur(20px)_saturate(140%)]`.
- Complex filter chains (4+ functions): promote to a CSS utility class in `globals.css` under `@layer utilities`.
### clamp() / min() / max()
- Preserve verbatim in arbitrary: `text-[clamp(24px,4vw,48px)]`, `w-[min(100%,1200px)]`.
- Do not try to approximate with `md:` breakpoints — the whole point of `clamp()` is continuous scaling.
### Variable fonts
- `font-variation-settings: "wght" 420, "ital" 0` → arbitrary `[font-variation-settings:'wght'_420,'ital'_0]`.
- If the site uses variable weights fluidly (e.g. on hover), pair with `transition-[font-variation-settings]`.
### Container queries
- Use the `@tailwindcss/container-queries` plugin. Add to `tailwind.config.ts` plugins.
- Mark the container: `@container`.
- Query: `@md:grid-cols-2`, `@lg:text-lg`, etc.
### clip-path and mask
- Built-in utilities cover basic cases (`rounded-full`). Anything custom: arbitrary.
- `clip-path: polygon(...)``[clip-path:polygon(0_0,100%_0,100%_80%,0_100%)]`.
- `mask-image``[mask-image:linear-gradient(...)]` + matching `[-webkit-mask-image:...]` if Safari support needed.
### Transforms
- Scale / rotate / translate cover common cases. Chained transforms: Tailwind's `transform` + individual utilities, or arbitrary `[transform:perspective(800px)_rotateY(12deg)]`.
### Transitions
- Simple: `transition-colors`, `duration-200`, `ease-in-out`.
- Multi-property transitions: arbitrary `[transition:transform_400ms_cubic-bezier(0.22,1,0.36,1),_opacity_300ms]`.
### Keyframes / `@keyframes`
- Always bail to `globals.css` under `@layer utilities` or a named keyframe in `tailwind.config.ts` `theme.extend.keyframes` + `animation`.
- Then reference in JSX: `animate-fade-up`.
## When to bail to `globals.css`
Write rules to `src/app/globals.css` (not to the component) when:
- The rule is `@property` — Tailwind cannot express it.
- The rule is `@supports` — conditional CSS outside Tailwind's reach.
- The rule is `@font-face` — prefer `next/font/local`, but if the font is served from a remote URL that cannot be downloaded, `@font-face` in `globals.css` is acceptable.
- A selector you cannot express as a class (e.g. `:has()`, `:nth-child(odd) > :first-of-type`).
- Complex multi-rule animations (keyframes + state variants + JS hooks).
Keep `globals.css` under 200 lines. If it grows beyond, group related rules into separate files imported from `globals.css`.
## Do not
- Do not re-derive cascade from the source stylesheets. The computed styles on each node already resolve cascade. Copy those.
- Do not use `!important`. If a style is not applying, you have a selector specificity problem — fix it by structuring the JSX, not by nuking the cascade.
- Do not add classes that are not represented in the captured computed styles. If the source does not have `box-shadow`, do not add one "for polish".
- Do not add `@apply` in component files. `@apply` only belongs in `globals.css` when consolidating a repeated utility cluster.
- Do not leave TODO comments in component files — the cloner is meant to be done work, not a scaffold.