diff --git a/SKILL.md b/SKILL.md index 7f3c8005..13803ac2 100644 --- a/SKILL.md +++ b/SKILL.md @@ -407,6 +407,17 @@ import DevupUI from "@devup-ui/vite-plugin"; export default defineConfig({ plugins: [react(), DevupUI()] }); ``` +> **The Vite plugin does not mean the project is a Vite SPA.** `vinext` runs +> Next.js App Router *on* Vite, so an App Router project has a `vite.config.ts`, +> no `next.config.ts`, and uses this plugin. Decide from `package.json`: a +> `vinext` or `next` dependency means file routing under `src/app/`, and there +> is no `main.tsx` or `index.html` to create. + +```ts +// vinext project - both plugins belong here +plugins: [DevupUI(), vinext({ nextConfig: { output: "export" } })]; +``` + ### Next.js ```ts @@ -451,6 +462,77 @@ DevupUI({ }) ``` +## Never Author a CSS File + +Devup UI extracts styling at build time. A hand-written stylesheet is invisible +to it: it cannot be checked, themed, or ordered in the cascade, and it competes +silently with the classes the plugin generated. + +| Need | Use | +|------|-----| +| Reset / normalize | `resetCss()` from `@devup-ui/reset-css` | +| Document-level rules (`body`, `*`, `@font-face`) | `globalCss({ ... })` | +| Component styling | Style props, or `css({ ... })` | +| A genuinely runtime value | A style prop - the plugin emits a CSS variable | + +The only acceptable CSS import is a stylesheet **shipped by an installed +package you do not author**, such as an offline webfont package. + +### `@devup-ui/reset-css` + +It is a package, so the plugin has to be told to process it or its classes are +never emitted. With Vite the two resolver settings are needed as well: + +```ts +plugins: [DevupUI({ include: ["@devup-ui/reset-css"] })], +optimizeDeps: { exclude: ["@devup-ui/reset-css"] }, +ssr: { noExternal: ["@devup-ui/reset-css"] }, +``` + +## What Decides Static Extraction + +One rule explains `Dynamic Values = CSS Variables`, `$token Scope` and +`Inline Variant Pattern` below: + +> Devup UI extracts at build time only what it can prove is constant **at the +> JSX prop site**. A value reached through a variable is treated as possibly +> mutated at runtime - TypeScript's types are not a runtime guarantee - so it +> falls back to a CSS variable. + +| Form | Result | +|------|--------| +| `` | Static class | +| `` | Static class per value - **preferred** | +| `` where `colors` is declared elsewhere | CSS variable | +| `` | CSS variable (genuinely dynamic - correct) | +| `const s = { a: css({ ... }) }` then `className={s[v]}` | Neither - see below | + +**How to tell which one you got:** inspect the rendered element. Inline CSS +variables mean the values were not extracted. + +```html +
+
+``` + +### The `css()` exception + +An external object holding **`css()` results** is not the same thing and is +fine. `css()` runs at build time and returns a className string, so extraction +already happened at the `css()` call; the object carries strings and no style +prop is involved. + +```tsx +// FINE - extraction happened inside css(); this object holds classNames +const variantStyles = { + primary: css({ bg: "$primary", color: "#FFF" }), + secondary: css({ bg: "$gray100", color: "$text" }), +}; +; +``` + +The rule is about **style prop values**, not about objects. + ## $token Scope `$token` values (colors, length, shadow) only work in **JSX props**. Use `var(--token)` in external objects. @@ -485,8 +567,16 @@ Use inline object indexing instead of external config objects: // AVOID - external config object (becomes dynamic, uses CSS variables) const sizeStyles = { lg: { h: '48px' }, md: { h: '40px' } } + +// AVOID - the flat form is the same trap. The compiler cannot prove `colors` +// was not mutated before this runs, so every value becomes a CSS variable. +const colors = { red: '#f00', blue: '#00f' } + ``` +Why, and how to detect it: see [What Decides Static Extraction](#what-decides-static-extraction). +An external object of `css()` results is a different thing and is fine. + ## Anti-Patterns (NEVER do) | Wrong | Right | Why | @@ -503,3 +593,6 @@ const sizeStyles = { lg: { h: '48px' }, md: { h: '40px' } } | `width="100%"` | `w="100%"` | Always use shorthands | | `styled("div", {...})` | `` | Use Box component with props, not styled() | | `stylex.create({...})` | `` | Use Box component with props, not stylex | +| Authoring `styles.css` / `styles.scss` | `globalCss({...})` or style props | A hand-written stylesheet is invisible to extraction | +| `import "./styles.css"` | `resetCss()` / `globalCss()` | Only a stylesheet shipped by an installed package is acceptable | +| `` (external object) | `` | Reached through a variable, so it becomes a CSS variable |