@tenphi/tasty 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,10 +12,6 @@ import { Tokens } from "../types.js";
12
12
  * @returns CSSProperties object or undefined if no tokens to process
13
13
  */
14
14
  declare function processTokens(tokens: Tokens | undefined): CSSProperties | undefined;
15
- /**
16
- * Stringify tokens for memoization key.
17
- */
18
- declare function stringifyTokens(tokens: Tokens | undefined): string;
19
15
  //#endregion
20
- export { processTokens, stringifyTokens };
16
+ export { processTokens };
21
17
  //# sourceMappingURL=process-tokens.d.ts.map
@@ -77,14 +77,7 @@ function processTokens(tokens) {
77
77
  }
78
78
  return result;
79
79
  }
80
- /**
81
- * Stringify tokens for memoization key.
82
- */
83
- function stringifyTokens(tokens) {
84
- if (!tokens) return "";
85
- return JSON.stringify(tokens);
86
- }
87
80
  //#endregion
88
- export { processTokens, stringifyTokens };
81
+ export { processTokens };
89
82
 
90
83
  //# sourceMappingURL=process-tokens.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"process-tokens.js","names":[],"sources":["../../src/utils/process-tokens.ts"],"sourcesContent":["import type { Tokens, TokenValue } from '../types';\n\nimport type { CSSProperties } from './css-types';\n\nimport { getColorSpaceComponents, getColorSpaceSuffix } from './color-space';\nimport { normalizeColorTokenValue, parseStyle } from './styles';\n\nexport { hslToRgbValues } from './color-math';\n\nconst devMode = process.env.NODE_ENV !== 'production';\n\n/**\n * Extract color components in the configured color space.\n * Returns a CSS variable reference for token colors, or decomposed\n * components as a space-separated string.\n */\nfunction extractColorSpaceValue(\n colorValue: string,\n parsedOutput: string,\n): string {\n const suffix = getColorSpaceSuffix();\n\n // If the parsed output references a color variable, use the companion variant\n const varMatch = parsedOutput.match(/var\\(--([a-z0-9-]+)-color\\)/);\n if (varMatch) {\n return `var(--${varMatch[1]}-color-${suffix})`;\n }\n\n // Try the original color value first, then parsed output\n const components = getColorSpaceComponents(colorValue);\n if (components !== colorValue) return components;\n\n const componentsFromParsed = getColorSpaceComponents(parsedOutput);\n if (componentsFromParsed !== parsedOutput) return componentsFromParsed;\n\n // Fallback: return the parsed output\n return parsedOutput;\n}\n\n/**\n * Check if a value is a valid token value (string, number, or boolean - not object).\n * Returns false for `false` values (they mean \"skip this token\").\n */\nfunction isValidTokenValue(\n value: unknown,\n): value is Exclude<TokenValue, undefined | null | false> {\n if (value === undefined || value === null || value === false) {\n return false;\n }\n\n if (typeof value === 'object') {\n if (devMode) {\n console.warn(\n 'Tasty: Object values are not allowed in tokens prop. ' +\n 'Tokens do not support state-based styling. Use a primitive value instead.',\n );\n }\n return false;\n }\n\n return (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n );\n}\n\n/**\n * Process a single token value through the tasty parser.\n * Numbers are converted to strings; 0 stays as \"0\".\n */\nfunction processTokenValue(value: string | number): string {\n if (typeof value === 'number') {\n // 0 should remain as \"0\", not converted to any unit\n if (value === 0) {\n return '0';\n }\n return parseStyle(String(value)).output;\n }\n return parseStyle(value).output;\n}\n\n/**\n * Process tokens object into inline style properties.\n * - $name -> --name with parsed value\n * - #name -> --name-color AND --name-color-{colorSpace} with parsed values\n *\n * @param tokens - The tokens object to process\n * @returns CSSProperties object or undefined if no tokens to process\n */\nexport function processTokens(\n tokens: Tokens | undefined,\n): CSSProperties | undefined {\n if (!tokens) {\n return undefined;\n }\n\n const keys = Object.keys(tokens);\n if (keys.length === 0) {\n return undefined;\n }\n\n let result: Record<string, string> | undefined;\n\n for (const key of keys) {\n const value = tokens[key as keyof Tokens];\n\n // Skip undefined/null values\n if (!isValidTokenValue(value)) {\n continue;\n }\n\n if (key.startsWith('$')) {\n // Custom property token: $name -> --name\n const propName = `--${key.slice(1)}`;\n // Boolean true for custom properties converts to empty string (valid CSS value)\n const effectiveValue = value === true ? '' : value;\n const processedValue = processTokenValue(effectiveValue);\n\n if (!result) result = {};\n result[propName] = processedValue;\n } else if (key.startsWith('#')) {\n const colorName = key.slice(1);\n const suffix = getColorSpaceSuffix();\n\n // Normalize color token value (true → 'transparent', false is already filtered by isValidTokenValue)\n const effectiveValue = normalizeColorTokenValue(value);\n // Skip if normalized to null (shouldn't happen since false is filtered by isValidTokenValue)\n if (effectiveValue === null) continue;\n\n const originalValue =\n typeof effectiveValue === 'number'\n ? String(effectiveValue)\n : effectiveValue;\n const lowerValue = originalValue.toLowerCase();\n const processedValue = processTokenValue(effectiveValue);\n\n if (!result) result = {};\n result[`--${colorName}-color`] = processedValue;\n\n // Skip component generation for #current values (currentcolor is dynamic, cannot decompose)\n if (/^#current(?:\\.|$)/i.test(lowerValue)) {\n continue;\n }\n\n result[`--${colorName}-color-${suffix}`] = extractColorSpaceValue(\n originalValue,\n processedValue,\n );\n }\n }\n\n return result as CSSProperties | undefined;\n}\n\n/**\n * Stringify tokens for memoization key.\n */\nexport function stringifyTokens(tokens: Tokens | undefined): string {\n if (!tokens) return '';\n return JSON.stringify(tokens);\n}\n"],"mappings":";;;;;;;;;AAgBA,SAAS,uBACP,YACA,cACQ;CACR,MAAM,SAAS,qBAAqB;CAGpC,MAAM,WAAW,aAAa,MAAM,8BAA8B;AAClE,KAAI,SACF,QAAO,SAAS,SAAS,GAAG,SAAS,OAAO;CAI9C,MAAM,aAAa,wBAAwB,WAAW;AACtD,KAAI,eAAe,WAAY,QAAO;CAEtC,MAAM,uBAAuB,wBAAwB,aAAa;AAClE,KAAI,yBAAyB,aAAc,QAAO;AAGlD,QAAO;;;;;;AAOT,SAAS,kBACP,OACwD;AACxD,KAAI,UAAU,KAAA,KAAa,UAAU,QAAQ,UAAU,MACrD,QAAO;AAGT,KAAI,OAAO,UAAU,UAAU;AAE3B,UAAQ,KACN,iIAED;AAEH,SAAO;;AAGT,QACE,OAAO,UAAU,YACjB,OAAO,UAAU,YACjB,OAAO,UAAU;;;;;;AAQrB,SAAS,kBAAkB,OAAgC;AACzD,KAAI,OAAO,UAAU,UAAU;AAE7B,MAAI,UAAU,EACZ,QAAO;AAET,SAAO,WAAW,OAAO,MAAM,CAAC,CAAC;;AAEnC,QAAO,WAAW,MAAM,CAAC;;;;;;;;;;AAW3B,SAAgB,cACd,QAC2B;AAC3B,KAAI,CAAC,OACH;CAGF,MAAM,OAAO,OAAO,KAAK,OAAO;AAChC,KAAI,KAAK,WAAW,EAClB;CAGF,IAAI;AAEJ,MAAK,MAAM,OAAO,MAAM;EACtB,MAAM,QAAQ,OAAO;AAGrB,MAAI,CAAC,kBAAkB,MAAM,CAC3B;AAGF,MAAI,IAAI,WAAW,IAAI,EAAE;GAEvB,MAAM,WAAW,KAAK,IAAI,MAAM,EAAE;GAGlC,MAAM,iBAAiB,kBADA,UAAU,OAAO,KAAK,MACW;AAExD,OAAI,CAAC,OAAQ,UAAS,EAAE;AACxB,UAAO,YAAY;aACV,IAAI,WAAW,IAAI,EAAE;GAC9B,MAAM,YAAY,IAAI,MAAM,EAAE;GAC9B,MAAM,SAAS,qBAAqB;GAGpC,MAAM,iBAAiB,yBAAyB,MAAM;AAEtD,OAAI,mBAAmB,KAAM;GAE7B,MAAM,gBACJ,OAAO,mBAAmB,WACtB,OAAO,eAAe,GACtB;GACN,MAAM,aAAa,cAAc,aAAa;GAC9C,MAAM,iBAAiB,kBAAkB,eAAe;AAExD,OAAI,CAAC,OAAQ,UAAS,EAAE;AACxB,UAAO,KAAK,UAAU,WAAW;AAGjC,OAAI,qBAAqB,KAAK,WAAW,CACvC;AAGF,UAAO,KAAK,UAAU,SAAS,YAAY,uBACzC,eACA,eACD;;;AAIL,QAAO;;;;;AAMT,SAAgB,gBAAgB,QAAoC;AAClE,KAAI,CAAC,OAAQ,QAAO;AACpB,QAAO,KAAK,UAAU,OAAO"}
1
+ {"version":3,"file":"process-tokens.js","names":[],"sources":["../../src/utils/process-tokens.ts"],"sourcesContent":["import type { Tokens, TokenValue } from '../types';\n\nimport type { CSSProperties } from './css-types';\n\nimport { getColorSpaceComponents, getColorSpaceSuffix } from './color-space';\nimport { normalizeColorTokenValue, parseStyle } from './styles';\n\nexport { hslToRgbValues } from './color-math';\n\nconst devMode = process.env.NODE_ENV !== 'production';\n\n/**\n * Extract color components in the configured color space.\n * Returns a CSS variable reference for token colors, or decomposed\n * components as a space-separated string.\n */\nfunction extractColorSpaceValue(\n colorValue: string,\n parsedOutput: string,\n): string {\n const suffix = getColorSpaceSuffix();\n\n // If the parsed output references a color variable, use the companion variant\n const varMatch = parsedOutput.match(/var\\(--([a-z0-9-]+)-color\\)/);\n if (varMatch) {\n return `var(--${varMatch[1]}-color-${suffix})`;\n }\n\n // Try the original color value first, then parsed output\n const components = getColorSpaceComponents(colorValue);\n if (components !== colorValue) return components;\n\n const componentsFromParsed = getColorSpaceComponents(parsedOutput);\n if (componentsFromParsed !== parsedOutput) return componentsFromParsed;\n\n // Fallback: return the parsed output\n return parsedOutput;\n}\n\n/**\n * Check if a value is a valid token value (string, number, or boolean - not object).\n * Returns false for `false` values (they mean \"skip this token\").\n */\nfunction isValidTokenValue(\n value: unknown,\n): value is Exclude<TokenValue, undefined | null | false> {\n if (value === undefined || value === null || value === false) {\n return false;\n }\n\n if (typeof value === 'object') {\n if (devMode) {\n console.warn(\n 'Tasty: Object values are not allowed in tokens prop. ' +\n 'Tokens do not support state-based styling. Use a primitive value instead.',\n );\n }\n return false;\n }\n\n return (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n );\n}\n\n/**\n * Process a single token value through the tasty parser.\n * Numbers are converted to strings; 0 stays as \"0\".\n */\nfunction processTokenValue(value: string | number): string {\n if (typeof value === 'number') {\n // 0 should remain as \"0\", not converted to any unit\n if (value === 0) {\n return '0';\n }\n return parseStyle(String(value)).output;\n }\n return parseStyle(value).output;\n}\n\n/**\n * Process tokens object into inline style properties.\n * - $name -> --name with parsed value\n * - #name -> --name-color AND --name-color-{colorSpace} with parsed values\n *\n * @param tokens - The tokens object to process\n * @returns CSSProperties object or undefined if no tokens to process\n */\nexport function processTokens(\n tokens: Tokens | undefined,\n): CSSProperties | undefined {\n if (!tokens) {\n return undefined;\n }\n\n const keys = Object.keys(tokens);\n if (keys.length === 0) {\n return undefined;\n }\n\n let result: Record<string, string> | undefined;\n\n for (const key of keys) {\n const value = tokens[key as keyof Tokens];\n\n // Skip undefined/null values\n if (!isValidTokenValue(value)) {\n continue;\n }\n\n if (key.startsWith('$')) {\n // Custom property token: $name -> --name\n const propName = `--${key.slice(1)}`;\n // Boolean true for custom properties converts to empty string (valid CSS value)\n const effectiveValue = value === true ? '' : value;\n const processedValue = processTokenValue(effectiveValue);\n\n if (!result) result = {};\n result[propName] = processedValue;\n } else if (key.startsWith('#')) {\n const colorName = key.slice(1);\n const suffix = getColorSpaceSuffix();\n\n // Normalize color token value (true → 'transparent', false is already filtered by isValidTokenValue)\n const effectiveValue = normalizeColorTokenValue(value);\n // Skip if normalized to null (shouldn't happen since false is filtered by isValidTokenValue)\n if (effectiveValue === null) continue;\n\n const originalValue =\n typeof effectiveValue === 'number'\n ? String(effectiveValue)\n : effectiveValue;\n const lowerValue = originalValue.toLowerCase();\n const processedValue = processTokenValue(effectiveValue);\n\n if (!result) result = {};\n result[`--${colorName}-color`] = processedValue;\n\n // Skip component generation for #current values (currentcolor is dynamic, cannot decompose)\n if (/^#current(?:\\.|$)/i.test(lowerValue)) {\n continue;\n }\n\n result[`--${colorName}-color-${suffix}`] = extractColorSpaceValue(\n originalValue,\n processedValue,\n );\n }\n }\n\n return result as CSSProperties | undefined;\n}\n"],"mappings":";;;;;;;;;AAgBA,SAAS,uBACP,YACA,cACQ;CACR,MAAM,SAAS,qBAAqB;CAGpC,MAAM,WAAW,aAAa,MAAM,8BAA8B;AAClE,KAAI,SACF,QAAO,SAAS,SAAS,GAAG,SAAS,OAAO;CAI9C,MAAM,aAAa,wBAAwB,WAAW;AACtD,KAAI,eAAe,WAAY,QAAO;CAEtC,MAAM,uBAAuB,wBAAwB,aAAa;AAClE,KAAI,yBAAyB,aAAc,QAAO;AAGlD,QAAO;;;;;;AAOT,SAAS,kBACP,OACwD;AACxD,KAAI,UAAU,KAAA,KAAa,UAAU,QAAQ,UAAU,MACrD,QAAO;AAGT,KAAI,OAAO,UAAU,UAAU;AAE3B,UAAQ,KACN,iIAED;AAEH,SAAO;;AAGT,QACE,OAAO,UAAU,YACjB,OAAO,UAAU,YACjB,OAAO,UAAU;;;;;;AAQrB,SAAS,kBAAkB,OAAgC;AACzD,KAAI,OAAO,UAAU,UAAU;AAE7B,MAAI,UAAU,EACZ,QAAO;AAET,SAAO,WAAW,OAAO,MAAM,CAAC,CAAC;;AAEnC,QAAO,WAAW,MAAM,CAAC;;;;;;;;;;AAW3B,SAAgB,cACd,QAC2B;AAC3B,KAAI,CAAC,OACH;CAGF,MAAM,OAAO,OAAO,KAAK,OAAO;AAChC,KAAI,KAAK,WAAW,EAClB;CAGF,IAAI;AAEJ,MAAK,MAAM,OAAO,MAAM;EACtB,MAAM,QAAQ,OAAO;AAGrB,MAAI,CAAC,kBAAkB,MAAM,CAC3B;AAGF,MAAI,IAAI,WAAW,IAAI,EAAE;GAEvB,MAAM,WAAW,KAAK,IAAI,MAAM,EAAE;GAGlC,MAAM,iBAAiB,kBADA,UAAU,OAAO,KAAK,MACW;AAExD,OAAI,CAAC,OAAQ,UAAS,EAAE;AACxB,UAAO,YAAY;aACV,IAAI,WAAW,IAAI,EAAE;GAC9B,MAAM,YAAY,IAAI,MAAM,EAAE;GAC9B,MAAM,SAAS,qBAAqB;GAGpC,MAAM,iBAAiB,yBAAyB,MAAM;AAEtD,OAAI,mBAAmB,KAAM;GAE7B,MAAM,gBACJ,OAAO,mBAAmB,WACtB,OAAO,eAAe,GACtB;GACN,MAAM,aAAa,cAAc,aAAa;GAC9C,MAAM,iBAAiB,kBAAkB,eAAe;AAExD,OAAI,CAAC,OAAQ,UAAS,EAAE;AACxB,UAAO,KAAK,UAAU,WAAW;AAGjC,OAAI,qBAAqB,KAAK,WAAW,CACvC;AAGF,UAAO,KAAK,UAAU,SAAS,YAAY,uBACzC,eACA,eACD;;;AAIL,QAAO"}
@@ -256,6 +256,54 @@ The `tokens` prop sets `style="--progress: 75%"` on the DOM element. The `$progr
256
256
 
257
257
  Design tokens (via `configure({ tokens })`) are injected as CSS custom properties on `:root`. Replace tokens (via `configure({ replaceTokens })`) are resolved at parse time and baked into the generated CSS. The `tokens` prop on components is resolved at render time via inline CSS custom properties. Use design tokens for design-system constants, replace tokens for value aliases, and the `tokens` prop for truly dynamic per-instance values.
258
258
 
259
+ ### tokenProps
260
+
261
+ `tokenProps` expose token keys as top-level component props — the token equivalent of `styleProps` and `modProps`. Use them when a component has a fixed set of known dynamic token values.
262
+
263
+ #### Array form
264
+
265
+ Prop names are plain camelCase identifiers. Names ending in `Color` map to `#` color tokens; everything else maps to `$` custom property tokens:
266
+
267
+ ```tsx
268
+ const ProgressBar = tasty({
269
+ tokenProps: ['progress', 'accentColor'] as const,
270
+ styles: { width: '$progress', fill: '#accent' },
271
+ });
272
+
273
+ // Clean prop API — no tokens object needed
274
+ <ProgressBar progress="75%" accentColor="#purple" />
275
+
276
+ // Conversion:
277
+ // 'progress' → $progress → --progress
278
+ // 'accentColor' → #accent → --accent-color + --accent-color-oklch
279
+ ```
280
+
281
+ #### Object form
282
+
283
+ Keys are prop names; values are `$`/`#`-prefixed token keys. No suffix convention needed — the prefix in the value is explicit:
284
+
285
+ ```tsx
286
+ const Card = tasty({
287
+ tokenProps: {
288
+ size: '$card-size',
289
+ color: '#card-accent',
290
+ },
291
+ styles: { padding: '$card-size', fill: '#card-accent' },
292
+ });
293
+
294
+ <Card size="4x" color="#purple" />
295
+ ```
296
+
297
+ #### Merge order
298
+
299
+ When all three token sources are present, values merge with increasing priority:
300
+
301
+ 1. `tokens` option in `tasty({...})` — default tokens (lowest)
302
+ 2. `tokens` prop on the component instance — runtime overrides
303
+ 3. `tokenProps`-derived values — highest priority (explicit named props win)
304
+
305
+ The `tokens` prop remains available for ad-hoc or dynamic tokens alongside `tokenProps`.
306
+
259
307
  ---
260
308
 
261
309
  ## styles prop vs style prop
@@ -458,7 +506,8 @@ See [Configuration](configuration.md) for the full `configure()` API.
458
506
  - **Use `elements` prop** to declare typed sub-components for compound components
459
507
  - **Use `styleProps`** to define what product engineers can customize
460
508
  - **Use `modProps`** to expose known modifier states as clean component props
461
- - **Use `tokens` prop** for per-instance dynamic values (progress, user color)
509
+ - **Use `tokenProps`** to expose known token keys as clean component props
510
+ - **Use `tokens` prop** for ad-hoc or dynamic per-instance token values (progress, user color)
462
511
  - **Use modifiers** (`mods` or `modProps`) for state-driven style changes instead of runtime `styles` prop changes
463
512
 
464
513
  ### Avoid
package/docs/runtime.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Runtime API
2
2
 
3
- The React-specific `tasty()` component factory, component props, and hooks. For the shared style language (state maps, tokens, units, extending semantics), see [Style DSL](dsl.md). For global configuration, see [Configuration](configuration.md). For the broader docs map, see the [Docs Hub](README.md).
3
+ The React-specific `tasty()` component factory, component props, and hooks. `tasty()` components are hook-free and compatible with React Server Components — no `'use client'` directive needed. For the shared style language (state maps, tokens, units, extending semantics), see [Style DSL](dsl.md). For global configuration, see [Configuration](configuration.md). For the broader docs map, see the [Docs Hub](README.md).
4
4
 
5
5
  ---
6
6
 
@@ -154,6 +154,75 @@ For architecture guidance on when to use modifiers vs `styleProps`, see [Methodo
154
154
 
155
155
  ---
156
156
 
157
+ ## Token Props
158
+
159
+ Use `tokenProps` to expose token keys as direct component props instead of requiring the `tokens` object:
160
+
161
+ ```jsx
162
+ // Before: tokens object
163
+ <ProgressBar tokens={{ $progress: '75%', '#accent': '#purple' }} />
164
+
165
+ // After: token props
166
+ <ProgressBar progress="75%" accentColor="#purple" />
167
+ ```
168
+
169
+ ### Array form
170
+
171
+ List prop names. Names ending in `Color` map to `#` color tokens; everything else maps to `$` custom property tokens:
172
+
173
+ ```jsx
174
+ const ProgressBar = tasty({
175
+ tokenProps: ['progress', 'accentColor'] as const,
176
+ styles: { width: '$progress', fill: '#accent' },
177
+ });
178
+
179
+ <ProgressBar progress="75%" accentColor="#purple" />
180
+ // 'progress' → $progress → --progress
181
+ // 'accentColor' → #accent → --accent-color + --accent-color-oklch
182
+ ```
183
+
184
+ ### Object form
185
+
186
+ Map prop names to explicit `$`/`#`-prefixed token keys:
187
+
188
+ ```tsx
189
+ const Card = tasty({
190
+ tokenProps: {
191
+ size: '$card-size',
192
+ color: '#card-accent',
193
+ },
194
+ styles: { padding: '$card-size', fill: '#card-accent' },
195
+ });
196
+
197
+ <Card size="4x" color="#purple" />
198
+ ```
199
+
200
+ ### Merge with `tokens`
201
+
202
+ Token props and the `tokens` prop can be used together. Token props take precedence over `tokens`, which takes precedence over default `tokens` in `tasty({...})`:
203
+
204
+ ```jsx
205
+ const Bar = tasty({
206
+ tokenProps: ['progress'] as const,
207
+ tokens: { $progress: '0%' }, // default
208
+ });
209
+
210
+ <Bar tokens={{ $progress: '50%' }} progress="90%" />
211
+ // progress="90%" wins (from token prop)
212
+ ```
213
+
214
+ ### When to use `tokenProps` vs `tokens`
215
+
216
+ | Use case | Recommendation |
217
+ |---|---|
218
+ | Component has a fixed set of known token keys | `tokenProps` — cleaner API, better TypeScript autocomplete |
219
+ | Component needs arbitrary/dynamic token values | `tokens` — open-ended `Record<string, TokenValue>` |
220
+ | Both fixed and dynamic | Combine: `tokenProps` for known keys, `tokens` for ad-hoc |
221
+
222
+ For architecture guidance, see [Methodology — tokenProps](methodology.md#tokenprops).
223
+
224
+ ---
225
+
157
226
  ## Variants
158
227
 
159
228
  Define named style variations. Only CSS for variants actually used at runtime is injected:
@@ -264,11 +333,29 @@ For the mental model behind sub-elements — how they share root state context a
264
333
 
265
334
  ---
266
335
 
336
+ ## computeStyles
337
+
338
+ Hook-free, synchronous style computation. Can be used anywhere — including React Server Components, plain functions, and non-React code:
339
+
340
+ ```tsx
341
+ import { computeStyles } from '@tenphi/tasty';
342
+
343
+ const { className } = computeStyles({
344
+ padding: '2x',
345
+ fill: '#surface',
346
+ radius: '1r',
347
+ });
348
+ ```
349
+
350
+ On the client, CSS is injected synchronously into the DOM (idempotent via the injector cache). On the server, CSS is collected via the SSR collector if one is available. This is the same function that `tasty()` components use internally.
351
+
352
+ ---
353
+
267
354
  ## Hooks
268
355
 
269
356
  ### useStyles
270
357
 
271
- Generate a className from a style object:
358
+ Generate a className from a style object. Thin wrapper around `computeStyles()` that adds React context-based SSR collector discovery for backward compatibility:
272
359
 
273
360
  ```tsx
274
361
  import { useStyles } from '@tenphi/tasty';
@@ -469,7 +556,7 @@ function useCounterStyle(
469
556
  ### Troubleshooting
470
557
 
471
558
  - Styles are not updating: make sure `configure()` runs before first render, and verify the generated class name or global rule with [Debug Utilities](debug.md).
472
- - SSR output looks wrong: check the [SSR guide](ssr.md) because the hooks integrate with SSR collectors differently than the client-only runtime path.
559
+ - SSR output looks wrong: check the [SSR guide](ssr.md) for collector setup. `computeStyles()` discovers the SSR collector via `AsyncLocalStorage` or the global getter registered by `TastyRegistry`.
473
560
  - Animation/custom property issues: prefer `useKeyframes()` and `useProperty()` over raw CSS when you want Tasty to manage injection and SSR collection for you.
474
561
 
475
562
  ---
package/docs/ssr.md CHANGED
@@ -18,16 +18,16 @@ The Astro integration (`@tenphi/tasty/ssr/astro`) has no additional dependencies
18
18
 
19
19
  ## How It Works
20
20
 
21
- When the environment can execute runtime React code during server rendering, the same `tasty()` and `useStyles()` calls can run there too. In Next.js, generic React SSR, and Astro islands, Tasty simply changes where that runtime-generated CSS goes: `useStyles()` detects a `ServerStyleCollector` and collects CSS into it instead of trying to access the DOM. The collector accumulates all styles, serializes them as `<style>` tags and a cache state script in the HTML. On the client, `hydrateTastyCache()` pre-populates the injector cache so that `useStyles()` skips the rendering pipeline entirely during hydration.
21
+ `tasty()` components are hook-free and use `computeStyles()` internally a synchronous, framework-agnostic function. On the server, `computeStyles()` detects a `ServerStyleCollector` (via `AsyncLocalStorage` or an explicit option) and collects CSS into it instead of trying to access the DOM. On the client, CSS is injected synchronously into the DOM during render; the injector's content-based cache makes this idempotent. The collector accumulates all styles, serializes them as `<style>` tags and a cache state script in the HTML. On the client, `hydrateTastyCache()` pre-populates the injector cache so that `computeStyles()` skips the rendering pipeline entirely during hydration.
22
22
 
23
23
  ```
24
24
  Server Client
25
25
  ────── ──────
26
26
  tasty() renders hydrateTastyCache() pre-populates cache
27
- └─ useStyles() └─ cacheKey → className map ready
27
+ └─ computeStyles() └─ cacheKey → className map ready
28
28
  └─ collector.collect()
29
29
  tasty() renders
30
- After render: └─ useStyles()
30
+ After render: └─ computeStyles()
31
31
  <style data-tasty-ssr> └─ cache hit → skip pipeline
32
32
  <script data-tasty-cache> └─ no CSS re-injection
33
33
  ```
@@ -83,14 +83,14 @@ That's it. All `tasty()` components inside the tree automatically get SSR suppor
83
83
  ### How it works
84
84
 
85
85
  - `TastyRegistry` is a `'use client'` component, but Next.js still server-renders it on initial page load.
86
- - During SSR, `useStyles()` finds the collector via React context and pushes CSS rules to it.
86
+ - During SSR, `computeStyles()` finds the collector (registered globally by `TastyRegistry`) and collects CSS rules into it.
87
87
  - `TastyRegistry` uses `useServerInsertedHTML` to flush collected CSS into the HTML stream as `<style data-tasty-ssr>` tags. This is fully streaming-compatible -- styles are injected alongside each Suspense boundary as it resolves.
88
88
  - A companion `<script>` tag transfers the `cacheKey → className` mapping to the client.
89
- - When the module loads on the client, `hydrateTastyCache()` runs automatically and pre-populates the injector cache. During hydration, `useStyles()` hits the cache and skips the entire pipeline.
89
+ - When the module loads on the client, `hydrateTastyCache()` runs automatically and pre-populates the injector cache. During hydration, `computeStyles()` hits the cache and skips the entire pipeline.
90
90
 
91
91
  ### Using tasty() in Server Components
92
92
 
93
- `tasty()` components use React hooks internally, so they require `'use client'`. However, this does **not** prevent them from being used in Server Component pages. In Next.js, `'use client'` components are still server-rendered on initial load. Dynamic `styleProps` like `<Grid flow="column">` work normally when a `tasty()` component is imported into a Server Component page.
93
+ `tasty()` components are hook-free and do not require `'use client'`. They can be used directly in React Server Components. Dynamic `styleProps` like `<Grid flow="column">` work normally in server components. During SSR, the `TastyRegistry` registers its collector globally so that `computeStyles()` can discover it without React context.
94
94
 
95
95
  ### Options
96
96
 
@@ -161,10 +161,10 @@ import Card from '../components/Card.tsx';
161
161
 
162
162
  ### How it works
163
163
 
164
- Astro's `@astrojs/react` renderer calls `renderToString()` for each React component without wrapping the tree in a provider. The middleware uses `AsyncLocalStorage` to make the collector available to all `useStyles()` calls within the request.
164
+ Astro's `@astrojs/react` renderer calls `renderToString()` for each React component without wrapping the tree in a provider. The middleware uses `AsyncLocalStorage` to make the collector available to all `computeStyles()` calls within the request.
165
165
 
166
166
  - **Static components** (no `client:*`): Styles are collected during `renderToString` and injected into `</head>`. No JavaScript is shipped for these components.
167
- - **Islands** (`client:load`, `client:visible`, etc.): Styles are collected during SSR the same way. On the client, importing `@tenphi/tasty/ssr/astro` auto-hydrates the cache from `<script data-tasty-cache>`. The island's `useStyles()` calls hit the cache during hydration.
167
+ - **Islands** (`client:load`, `client:visible`, etc.): Styles are collected during SSR the same way. On the client, importing `@tenphi/tasty/ssr/astro` auto-hydrates the cache from `<script data-tasty-cache>`. The island's `computeStyles()` calls hit the cache during hydration.
168
168
 
169
169
  ### Client-side hydration for islands
170
170
 
@@ -325,7 +325,7 @@ Server-safe style collector. One instance per request.
325
325
 
326
326
  ### `TastySSRContext`
327
327
 
328
- React context (`createContext<ServerStyleCollector | null>(null)`). Used by `useStyles()` to find the collector during SSR.
328
+ React context (`createContext<ServerStyleCollector | null>(null)`). Used by the `useStyles()` hook to find the collector during SSR. Not needed when using `computeStyles()` directly (which discovers the collector via `AsyncLocalStorage` or the global getter registered by `TastyRegistry`).
329
329
 
330
330
  ### `TastyRegistry`
331
331
 
@@ -350,7 +350,7 @@ Pre-populate the client injector cache. When called without arguments, reads fro
350
350
 
351
351
  ### `runWithCollector(collector, fn)`
352
352
 
353
- Run a function with a `ServerStyleCollector` bound to the current async context via `AsyncLocalStorage`. All `useStyles()` calls within `fn` (and async continuations) will find this collector.
353
+ Run a function with a `ServerStyleCollector` bound to the current async context via `AsyncLocalStorage`. All `computeStyles()` and `useStyles()` calls within `fn` (and async continuations) will find this collector.
354
354
 
355
355
  ---
356
356
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenphi/tasty",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "A design-system-integrated styling system and DSL for concise, state-aware UI styling",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -150,7 +150,7 @@
150
150
  "name": "main (import *)",
151
151
  "path": "dist/index.js",
152
152
  "import": "*",
153
- "limit": "47 kB"
153
+ "limit": "48 kB"
154
154
  },
155
155
  {
156
156
  "name": "core (import *)",