# AI Agent Resources Source: https://plumeria.dev/docs/ai-agent-resources Plumeria's documentation is available as plain Markdown for Claude, Codex, Gemini, ChatGPT, and other coding agents. Start with installation, then add only the references the task needs. Always load the installation guide before asking an agent to write Plumeria. The compiler and bundler integration are part of the programming model—not optional setup around it. ## Start here [#start-here] Copy this into your agent's prompt. It establishes the correct reading order and gives the agent the complete Plumeria rule set before it edits code. ```text title="Prompt for your coding agent" Read these Plumeria documents in order before making changes: 1. Installation — https://plumeria.dev/docs/getting-started/installation.md 2. Rules for AI agents — https://plumeria.dev/docs/ai.md 3. Introduction and core usage — https://plumeria.dev/docs.md Follow AI.md as authoritative. Inspect the existing project's bundler and framework, then read the matching integration guide before changing configuration. Do not apply runtime CSS-in-JS conventions to Plumeria: its styles are compiled statically. ``` The installation guide covers the package, styling-prop declaration, ESLint setup, and the compiler integration required for Next.js and Vite. For another bundler, append its guide from [Integrations](#integrations). ```text title="Minimum context" https://plumeria.dev/docs/getting-started/installation.md https://plumeria.dev/docs/ai.md https://plumeria.dev/docs.md ``` ## Add context for the task [#add-context-for-the-task] Keep the three documents above, then append one of these focused sets. ### Writing components and styles [#writing-components-and-styles] Use this set for everyday component work, selectors, composition, dynamic values, themes, animations, and style props. ```text title="Authoring context" https://plumeria.dev/docs/getting-started/selector-rules.md https://plumeria.dev/docs/reference/specificity.md https://plumeria.dev/docs/api-reference/javascript/create.md https://plumeria.dev/docs/api-reference/javascript/createStatic.md https://plumeria.dev/docs/api-reference/javascript/createTheme.md https://plumeria.dev/docs/api-reference/javascript/keyframes.md https://plumeria.dev/docs/api-reference/javascript/marker.md https://plumeria.dev/docs/api-reference/javascript/use.md https://plumeria.dev/docs/api-reference/javascript/viewTransition.md ``` ### Tooling, testing, and migration [#tooling-testing-and-migration] Use this set when configuring linting, testing compiler output, or migrating an existing codebase. ```text title="Tooling context" https://plumeria.dev/docs/getting-started/editor-integration.md https://plumeria.dev/docs/testing.md https://plumeria.dev/docs/api-reference/plugins/eslint-plugin.md https://plumeria.dev/docs/api-reference/plugins/eslint-plugin/optional-rules.md https://plumeria.dev/docs/api-reference/codemod.md ``` ### Components and distribution [#components-and-distribution] Use this set for component libraries, framework components, or development inspection. ```text title="Component context" https://plumeria.dev/docs/integration/frameworks.md https://plumeria.dev/docs/integration/component-library.md https://plumeria.dev/docs/api-reference/components/headlessui.md https://plumeria.dev/docs/api-reference/components/inspector.md ``` ## Integrations [#integrations] Load the guide that matches the project. Each page includes installation and a working configuration. ```text title="Bundler and framework integrations" Next.js https://plumeria.dev/docs/integration/next.md Vite https://plumeria.dev/docs/integration/vite.md Bun https://plumeria.dev/docs/integration/bun.md esbuild https://plumeria.dev/docs/integration/esbuild.md Farm https://plumeria.dev/docs/integration/farm.md Rollup https://plumeria.dev/docs/integration/rollup.md Rolldown https://plumeria.dev/docs/integration/rolldown.md Rspack https://plumeria.dev/docs/integration/rspack.md webpack https://plumeria.dev/docs/integration/webpack.md Frameworks https://plumeria.dev/docs/integration/frameworks.md ``` The lower-level plugin references document every configuration option: ```text title="Plugin references" Next.js plugin https://plumeria.dev/docs/api-reference/plugins/next-plugin.md Unplugin https://plumeria.dev/docs/api-reference/plugins/unplugin.md ESLint plugin https://plumeria.dev/docs/api-reference/plugins/eslint-plugin.md ``` ## Complete documentation map [#complete-documentation-map] Use this map when an agent needs to discover material outside the focused sets above. It covers every documentation section, from concepts through API and CSS references. | Area | What it contains | Markdown | | :---------------- | :-------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- | | Getting started | Installation, selector rules, editor and ESLint setup | [Open section](/docs/getting-started) · [Markdown](/docs/getting-started.md) | | Core concepts | Why Plumeria, compile-time behavior, composition | [Introduction](/docs.md) · [Why Plumeria?](/docs/why-plumeria.md) · [Category theory](/docs/category.md) | | JavaScript API | `create`, static styles, themes, keyframes, markers, transitions, and `use` | [Open section](/docs/api-reference#javascript-api) · [Markdown](/docs/api-reference.md) | | Components | Headless UI integration and the development inspector | [Open section](/docs/api-reference#components) · [Markdown](/docs/api-reference.md#components) | | Plugins and tools | Next.js, unplugin, ESLint, and codemods | [Open section](/docs/api-reference#plugins) · [Markdown](/docs/api-reference.md#plugins) | | Integrations | Nine bundlers, frameworks, and component-library distribution | [Open section](/docs/integration) · [Markdown](/docs/integration.md) | | Testing | Compiler, component, jsdom, and type testing | [Open page](/docs/testing) · [Markdown](/docs/testing.md) | | CSS reference | Specificity, colors, and supported pseudo-selectors | [Open section](/docs/reference) · [Markdown](/docs/reference.md) | ## Fetch Markdown [#fetch-markdown] Add `.md` to any documentation URL to get the page as raw Markdown. The response includes its title and canonical URL. ```text title="Any page as Markdown" https://plumeria.dev/docs/testing.md https://plumeria.dev/docs/api-reference/javascript/create.md https://plumeria.dev/docs/integration/webpack.md ``` For broad research, use the generated machine-readable indexes: ## Working examples [#working-examples] The [`examples/` directory](https://github.com/zss-in-js/plumeria/tree/main/examples) contains compiling projects for Next.js, Vite, Rollup, Rspack, webpack, esbuild, Bun, Farm, and Rolldown, plus React Router, Vue, Svelte, Solid, Qwik, Preact, Astro, and Storybook. Point an agent to the matching example when it needs a complete configuration rather than an isolated snippet. --- # AI.md Source: https://plumeria.dev/docs/ai This document is for AI assistants (Claude, Gemini, ChatGPT, and others) generating code with Plumeria. When writing Plumeria code, **always** follow the rules and patterns described here. ## Critical Rules (Quick Reference) [#critical-rules-quick-reference] The complete rule set, distilled. Each rule is explained with examples in the sections below. **MUST:** * Call `css.create()` at module top level, never inside a component. (→ Forbidden Patterns) * Bind styles with the `classStyle` prop, not `className`. (→ Mental Model) * Import `@plumeria/core` in every file that uses Plumeria styles, including files that only consume imported styles. (→ Core Usage) * Start nested selector keys with `:` (pseudo) or `[` (attribute). (→ Selector Rules) * Explicitly declare a compound selector when simultaneous pseudo-class states set the same property. (→ Selector Rules) * Compose with arrays; the right side always wins. (→ Core Usage) **NEVER:** * Pass a raw object to `classStyle`, e.g. `classStyle={{ color: 'red' }}`. (→ Forbidden Patterns) * Call `css.create()` inside a component body. (→ Forbidden Patterns) * Use the `&` self-reference character. (→ Selector Rules) * Use `:has()`, `:is()`, or `:where()` — use `css.marker()` / `css.extended()` instead. (→ Selector Rules, Advanced APIs) * Use child or descendant selectors (`.title`, `> div`) — style child elements directly. (→ Selector Rules) * Nest a media/container query inside a pseudo-selector. The reverse (pseudo inside media) is allowed once. (→ Selector Rules) * Mix `className` and `classStyle` on the same element. (→ Forbidden Patterns) * Merge `css.use()` output with the inline `style` prop. (→ Dynamic Styling) * Route a function key through `css.use()`, inline or through a `Style` prop. (→ Dynamic Styling) * Pass a received `Style` prop on to another component. Apply it on the element the component renders. (→ Styling Custom Components) ## Mental Model [#mental-model] AI assistants often misapply runtime CSS-in-JS habits to Plumeria. Correct your mental model: * **Styles are type schemas, not runtime objects.** Plumeria definitions are static schemas read by the compiler; they do not exist as JavaScript objects at runtime. * **`css.create()` is a compiler directive, not an object factory.** Everything inside it is statically analyzed — the compiler must be able to read all styling information without executing code. * **`classStyle` is NOT `className`.** Plumeria binds styles through its own `classStyle` prop. Never reach for `className` reflexively. * **Zero-byte CSS in the JS bundle.** All CSS is resolved at build time. The abstraction layer (your definitions) and the output layer (generated CSS) are completely separated, so Plumeria has no side effects and high referential transparency. ## Core Usage [#core-usage] Define styles at module top level with `css.create()`, bind them with `classStyle`: ```tsx import * as css from '@plumeria/core'; const styles = css.create({ container: { display: 'flex', padding: '16px', }, active: { backgroundColor: 'blue', }, }); export const MyComponent = ({ isActive }) => { return (
Hello Plumeria
); }; ``` **Right-wins composition.** `classStyle` accepts arrays, ternaries, and conditional expressions. The right-most style always takes precedence: above, `styles.active` overrides `styles.container` when `isActive` is true. `css.use()` follows the same rule. Within a single style, do not rely on source order to resolve overlapping pseudo-class states; declare their compound selector explicitly (see Selector Rules). **Cross-file imports.** Styles can be imported across files, but the consuming component file MUST contain `import "@plumeria/core";` — the import is how the compiler finds the file. ## Forbidden Patterns [#forbidden-patterns] Never generate these patterns. They break static analysis or cause runtime errors. ❌ **Passing an object directly to `classStyle`:** ```tsx Small Text ``` ❌ **Creating styles inside a component body:** ```tsx export const BadComponent = ({ dynamicColor }) => { const dynamicStyles = css.create({ dynamic: { color: dynamicColor }, }); // ... }; ``` ❌ **Mixing `className` and `classStyle` on the same element:** ```tsx Text ``` ❌ **Complex JS expressions in style values.** Only simple ternary operators are allowed, and only when necessary. Never put complex logic or function calls inside style values. ## Selector Rules [#selector-rules] Plumeria supports nesting for pseudo-classes, pseudo-elements, and attribute selectors — under strict rules: * **NO `&`.** Plumeria does not use the `&` self-reference character. * **Nested keys MUST start with `:` or `[`.** Pseudo-classes/elements start with `:`; attribute selectors start with `[`. * **NO `:has()`, `:is()`, `:where()`.** These are strictly forbidden. Use the paired `css.marker()` / `css.extended()` APIs (see Advanced APIs) for context-aware, parent-state, or descendant styling — they cover every use case of these pseudo-classes without breaking CSS atomicity. * **NO child selectors.** Keys like `.title` or `> div` are not supported. Apply Plumeria styles directly to child elements instead. * **Declare overlapping states explicitly.** Pseudo-classes such as `:hover`, `:focus`, and `:active` can match simultaneously. When two states set the same property, declare their compound selector with the value the intersection should take. If they set different properties, no compound selector is needed. * **Media/container query nesting is one-directional.** A pseudo-selector may be nested inside a media/container query exactly once. The reverse — a media/container query inside a pseudo-selector — is forbidden and causes compiler/type errors. ```tsx const styles = css.create({ button: { backgroundColor: 'white', padding: '16px', // ✅ Pseudo-classes and elements (starts with :) ':hover': { boxShadow: '0 4px 12px rgba(0,0,0,0.1)', }, ':active': { boxShadow: '0 1px 4px rgba(0,0,0,0.2)', }, // ✅ :hover and :active can match together; the intersection gets its own value ':hover:active': { boxShadow: '0 2px 6px rgba(0,0,0,0.3)', }, '::before': { content: '""', display: 'block', }, // ✅ Attribute selectors (starts with [) '[data-active="true"]': { border: '2px solid blue', }, // ✅ Pseudo-selector nested inside a media query (allowed once) '@media (max-width: 768px)': { padding: '8px', ':hover': { boxShadow: 'none', }, }, }, }); // ❌ INVALID: media query nested inside a pseudo-selector const badStyles = css.create({ button: { ':hover': { '@media (max-width: 768px)': { backgroundColor: 'blue', }, }, }, }); ``` ## Dynamic Styling [#dynamic-styling] ### Function keys [#function-keys] For dynamic values (which would normally require React's inline `style` prop), define a **function key** in `css.create()`. Function keys compile to CSS variables — so unlike inline styles, they fully support `@media` and `@container` queries: ```tsx const styles = css.create({ palette: (color: string) => ({ backgroundColor: color, // Media/container queries work on dynamic styles '@media (max-width: 600px)': { backgroundColor: 'blue', }, }), }); // Usage:
``` ### Bracket notation (variants) [#bracket-notation-variants] The `css.variants()` API has been removed in favor of standard bracket notation (`styles[variant]`). The compiler turns these expressions into static lookup tables at build time: ```tsx import * as css from '@plumeria/core'; // 1. Define variants in a dedicated, minimal css.create() const sizeStyles = css.create({ small: { fontSize: '12px' }, medium: { fontSize: '16px' }, large: { fontSize: '20px' }, }); // 2. Type the variant (string literal union or keyof typeof) type Size = keyof typeof sizeStyles; interface ButtonProps { size: Size; children: React.ReactNode; } // 3. Resolve dynamically with bracket notation export const Button = ({ size, children }: ButtonProps) => { return ; }; ``` Best practices: * ⚠️ **Keep variant `css.create()` calls minimal.** Every key in a `css.create()` used with bracket notation is compiled into the generated lookup table (e.g. `{"small":"...","medium":"..."}[size]`). Separate variants into dedicated calls; don't mix them with unrelated static styles. * **Local variable assignment is supported.** `const currentStyle = sizeStyles[size];
` works — the compiler traces local style variables and inlines them during JSX extraction. ### `css.use()` returns a static string [#cssuse-returns-a-static-string] `css.use()` compiles to a static class name string. It has **no** integration with the inline `style` prop — never attempt to merge or combine `css.use()` output with `style`. A function key cannot go through it, inline or through a `Style` prop: the value reaches the element as a CSS variable on `style`, which `css.use()` never sets. ## Advanced APIs [#advanced-apis] ### `css.marker()` and `css.extended()` (paired descendant styling) [#cssmarker-and-cssextended-paired-descendant-styling] These paired APIs enable context-aware styling (e.g. styling a child when the parent is hovered) without DOM combinators or `:has()`/`:is()`/`:where()`: * **`css.marker(id, pseudo)`** — sets a CSS variable marker on the parent when the pseudo state is active. MUST be spread into the parent style: `...css.marker(...)`. * **`css.extended(id, pseudo)`** — applies styles to descendants while the linked marker is active. Used as a computed key: `[css.extended(...)]`. ```tsx import * as css from '@plumeria/core'; const styles = css.create({ parent: { // 1. Set a marker with the unique id 'card' for the ':hover' state ...css.marker('card', ':hover'), padding: '24px', border: '1px solid #ccc', }, child: { transition: 'color 0.3s ease', // 2. React to the 'card' marker being active [css.extended('card', ':hover')]: { color: 'blue', // Further pseudo-classes can be nested here ':hover': { color: 'darkblue', }, }, }, }); export const Card = () => { return (
Hover parent to make me blue!
); }; ``` At build time this compiles to an atomic container style query (`@container style(--x1f9k2q1-card-hover: 1)`) — zero runtime overhead, zero dependency on DOM hierarchy. ### `css.createStatic()` [#csscreatestatic] Defines static variables (such as media query strings) that are inlined at build time, typically used as computed keys in `css.create()`: ```tsx import * as css from '@plumeria/core'; export const breakpoints = css.createStatic({ xs: '@media (max-width: 480px)', sm: '@media (max-width: 640px)', md: '@media (max-width: 768px)', lg: '@media (max-width: 1024px)', }); export const styles = css.create({ container: { [breakpoints.sm]: { padding: 16, }, [breakpoints.lg]: { padding: 32, }, }, }); ``` ### `css.createTheme()` [#csscreatetheme] Defines scoped CSS variables for themes. First argument: the selector that activates the theme. Second argument: an object of `{ default, theme }` value pairs. Generated variable names are prefixed with a unique hash, so name collisions are impossible. Theme values are only compiled when actually used in a styling block. ```tsx import * as css from '@plumeria/core'; // Class-based theme export const theme = css.createTheme('.dark', { text: { default: '#333', theme: '#eaeaea', }, background: { default: 'white', theme: 'black', }, }); // Attribute selectors and media queries are also supported: // css.createTheme('[data-theme="dark"]', { ... }); // css.createTheme('@media (prefers-color-scheme: dark)', { ... }); ``` ### `css.keyframes()` [#csskeyframes] Generates a unique `@keyframes` animation name (a hash inlined at build time) for use inside style rules: ```tsx import * as css from '@plumeria/core'; const fadeIn = css.keyframes({ from: { opacity: 0 }, to: { opacity: 1 }, }); const styles = css.create({ card: { transition: 'transform 0.3s ease', ':hover': { animationName: fadeIn, animationDuration: '0.5s', }, }, }); ``` ### `css.viewTransition()` [#cssviewtransition] Generates a unique `view-transition-name`, accepting animation definitions (`group`, `imagePair`, `new`, `old`) to customize View Transitions: ```tsx import * as css from '@plumeria/core'; const fadeIn = css.keyframes({ from: { opacity: 0 }, to: { opacity: 1 }, }); const fadeOut = css.keyframes({ from: { opacity: 1 }, to: { opacity: 0 }, }); const longCrossFade = css.viewTransition({ old: { animationName: fadeOut, animationDuration: '1.2s', }, new: { animationName: fadeIn, animationDuration: '1.2s', }, }); export const transition = css.create({ name: { viewTransitionName: longCrossFade, }, }); // React usage: // ... ``` ## Styling Custom Components [#styling-custom-components] There are exactly **3 patterns** for applying Plumeria styles to custom components. In all of them, compilation happens at the `classStyle` / `css.use()` call sites; the component itself just passes the compiled `className` / `style` through to the DOM. > **Core principle**: Custom props typed as `Style` are statically traced by the compiler, so styles pass seamlessly across component boundaries. ### Pattern 1: Direct `classStyle` inside the component [#pattern-1-direct-classstyle-inside-the-component] The component imports its own styles and applies them internally. Simplest pattern — fully self-contained: ```tsx import React from 'react'; import * as css from '@plumeria/core'; const styles = css.create({ button: { padding: '10px', backgroundColor: 'navy', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', }, }); export const Button = ({ children }: { children: React.ReactNode }) => { return ( // The compiler transforms classStyle → className here ); }; // Usage ; ``` ### Pattern 2: Passing a `Style` prop [#pattern-2-passing-a-style-prop] `classStyle` itself is compiled away at the call site, so it cannot be used as a prop name on a custom component. Instead, define a custom prop (e.g. `styleArray`) typed as `css.Style`. The compiler statically traces it across component boundaries and resolves it into the internal element's `classStyle`. Composing as `[styles.text, styleArray]` lets call-site styles override the component's base styles (right-wins — see Core Usage): below, the call site's `fontSize: '24px'` overrides the internal `fontSize: '12px'`. ```tsx // --- Button.tsx --- import React from 'react'; import * as css from '@plumeria/core'; type ButtonProps = { children: React.ReactNode; styleArray?: css.Style; }; // base style const styles = css.create({ text: { fontSize: '12px', }, }); // Pass styleArray directly to the inner element's classStyle export const Button = ({ children, styleArray }: ButtonProps) => { return ; }; // --- Usage (call site) --- import * as css from '@plumeria/core'; import { Button } from './Button'; const styles = css.create({ primary: { padding: '10px', backgroundColor: 'navy', color: '#fff', }, text: { fontSize: '24px', }, }); // The compiler traces styleArray and resolves it statically ; ``` ### Pattern 3: `className` bypass with `css.use()` [#pattern-3-classname-bypass-with-cssuse] The component resolves the `Style` prop into a class name string with `css.use()` and passes it to `className`. The compiler traces the prop into the `css.use()` call the same way: ```tsx // --- Button.tsx --- import React from 'react'; import * as css from '@plumeria/core'; type ButtonProps = { children: React.ReactNode; styleArray?: css.Style; }; // base style const styles = css.create({ text: { fontSize: '12px', }, }); // Pass styleArray into css.use() to generate the className export const Button = ({ children, styleArray }: ButtonProps) => { return ( ); }; // --- Usage (call site) --- import * as css from '@plumeria/core'; import { Button } from './Button'; const styles = css.create({ primary: { padding: '10px', backgroundColor: 'navy', color: '#fff', }, text: { fontSize: '24px', }, }); // The compiler traces styleArray inside css.use() and resolves it statically ; ``` This pattern cannot carry a function key. The call site's value reaches the element as a CSS variable, and `css.use()` returns a class name with nowhere to put it: ```tsx // ❌ INVALID: palette is a function key, so its variable has no element to land on ; ``` ``` Plumeria: "styleArray" carries a dynamic function key, and css.use() returns only a class name. Apply it to classStyle on the element instead. ``` Use Pattern 2 for a prop that may carry one: `classStyle` sets the variable next to the class. ### Anti-pattern: relaying a `Style` prop [#anti-pattern-relaying-a-style-prop] A component that receives a `Style` prop MUST apply it — to `classStyle` or through `css.use()` — on an element it renders. A style prop that is never applied is a build error, which is what happens when it is handed to another component instead. What decides this is **where the style ends up**, not how many style props a component takes. Passing styles in is always fine: ```tsx // ✅ VALID: every value is a style, so any number of style props is fine ``` Forwarding one onward instead of applying it is not: ```tsx // ❌ INVALID: headerStyle reaches no element of this component export const Layout = ({ headerStyle }: { headerStyle?: css.Style }) => { return
; }; ``` ``` Plumeria: "headerStyle" is a style received through a prop but is never applied to classStyle or css.use() here. Apply it on an element this component renders; a style prop cannot be passed on to another component. ``` Write one of these instead: ```tsx // ✅ Apply it on an element this component renders export const Layout = ({ headerStyle }: { headerStyle?: css.Style }) => { return
...
; }; // ✅ Or drop the middle component and style the one that owns the element
; ``` Merging under a base style — `[styles.base, headerStyle]` — is the supported shape. What is rejected is the *relay*: a prop that is only forwarded, never applied. So the valid call site above stays valid only while `Layout` applies all three props itself. If it routes them onward to a grandchild, each unapplied prop is an error — which is the signal that the styles belong on the components that own those elements. Prop drilling is an anti-pattern in React on its own terms; here it is also unresolvable, because the compiler would have to trace a style across an arbitrary chain of wrappers to build the lookup table. Rejecting it keeps the boundary decidable and the generated CSS exact. ### Summary [#summary] | Pattern | Compilation site | Component's role | | ---------------------- | -------------------------------- | --------------------------------- | | 1. Direct `classStyle` | Inside the component | Self-contained styles | | 2. `Style` prop | Traced and compiled | Receives and applies `Style` | | 3. `className` bypass | Inside the component (`css.use`) | Resolves `Style` into `className` | In every pattern the component **applies** the style it receives. None of them forward it. ## Migrating from Tailwind CSS [#migrating-from-tailwind-css] Tailwind's model and Plumeria's do not map one-to-one. A utility-by-utility rewrite produces code that fails to compile, silently breaks, or reads like Tailwind reimplemented. Migrate one component at a time, under three rules. **Resolve values from the compiled stylesheet, never from memory.** Utility values come from the project's `@theme`, not from a fixed table: `p-4` is `calc(var(--spacing) * 4)`, `text-sm` sets `font-size` *and* a paired `line-height`, and the color scale is whatever the project defines. Build the project and read the declarations out of the emitted CSS. **NEVER carry a `--tw-*` variable chain across.** `shadow-lg` compiles to `--tw-shadow: …` plus `box-shadow: var(--tw-inset-shadow), …, var(--tw-shadow)`, and those variables hold a value only because Tailwind registers them with `@property`. Copied literally, the declaration is invalid and the shadow disappears. Resolve the chain to its final value instead. **Name keys after what the element is, not after the utilities it carried.** The class list is the input to the migration, not the output. ```tsx // ❌ Tailwind reimplemented — these names carry no more meaning than the class list did const styles = css.create({ hoverBgBlue500: { ':hover': { backgroundColor: '#3b82f6' } }, px4: { paddingInline: '1rem' }, }); // ✅ One key per thing the component has const styles = css.create({ submitButton: { paddingInline: '1rem', backgroundColor: '#2563eb', ':hover': { backgroundColor: '#3b82f6' }, }, }); ``` **Three variants need restructuring rather than translation.** Breakpoints (`md:`) and `data-*` variants map directly, to `@media` and attribute-selector keys. `dark:` depends on the project: it is `@media (prefers-color-scheme: dark)` by default, but `@custom-variant dark` commonly redefines it as a `.dark` class selector — which belongs in the restructuring set below, not the direct one. Check the compiled output before assuming which. | Tailwind | Compiles to | Write instead | | ----------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `hover:`, `focus:` | `&:hover { @media (hover: hover) { … } }` | `':hover': { … }`, dropping the `@media (hover: hover)` wrapper — Plumeria forbids a query inside a pseudo. The styles then apply on touch devices too. | | `group-*`, `peer-*` | `&:is(:where(.group):hover *)` | `css.marker()` on the parent and `css.extended()` on the descendant. The `.group` / `.peer` class disappears. | | `space-x-*`, `divide-*` | `:where(& > :not(:last-child))` | The margin or border applied to the children directly — Plumeria has no child selectors. | ### `tv()` and `twMerge()` [#tv-and-twmerge] | Tailwind Variants | Plumeria | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------- | | `base` | Static keys in the component's own `css.create()` | | `variants` | One dedicated `css.create()` per variant group, read with bracket notation (keep these calls minimal — see Bracket notation) | | `defaultVariants` | Default parameter values | | `compoundVariants` | An explicit condition in the `classStyle` array | | `slots` | One `css.create()` per element, not one call carrying every slot | **An external `className` override becomes a `Style` prop, not a `className`.** `twMerge(button(...), className)` exists to let the call site win; Pattern 2 does the same job, because array position is right-wins. **NEVER** resolve the override away, force it through `css.use()`, or change the component's public API to avoid the mixing rule. **NEVER guess a class name that was built at runtime.** A template literal or a value read from props or state is not statically knowable. Leave the component on Tailwind and report it; a guessed utility compiles and is silently wrong. ```tsx // --- Before --- const button = tv({ base: 'inline-flex rounded font-medium', variants: { size: { sm: 'h-8 px-3 text-sm', lg: 'h-12 px-6 text-lg' }, tone: { primary: 'bg-blue-500 text-white', danger: 'bg-red-500 text-white' }, }, compoundVariants: [{ size: 'lg', tone: 'primary', class: 'shadow-lg' }], defaultVariants: { size: 'sm', tone: 'primary' }, }); ); }; ``` Function keys (dynamic styles) within the style object are ignored during bracket notation lookup table compilation. ### The last source wins [#the-last-source-wins] An element takes a list, and a property set by more than one entry keeps the value from the one written last. That is what lets a variant override the base above. The rule holds whatever the entries are — a plain style, a condition, a bracket group. Swapping two of them swaps which one applies: ```tsx const styles = css.create({ base: { color: 'white' }, muted: { color: 'slategray' }, }); const toneStyles = css.create({ brand: { color: 'royalblue' }, danger: { color: 'crimson' }, }); // muted wins — it is written last