@tenphi/tasty 3.9.11 → 3.11.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.
Files changed (92) hide show
  1. package/README.md +399 -380
  2. package/dist/babel-types-BKDTT4--.d.ts +18 -0
  3. package/dist/chunks/async-storage-f9j9JRtq.js +37 -0
  4. package/dist/chunks/async-storage-f9j9JRtq.js.map +1 -0
  5. package/dist/chunks/build-config-5d8WlQbq.js +60 -0
  6. package/dist/chunks/build-config-5d8WlQbq.js.map +1 -0
  7. package/dist/chunks/{collector-D2ARGT2L.js → collector-CjCak4iE.js} +8 -6
  8. package/dist/chunks/collector-CjCak4iE.js.map +1 -0
  9. package/dist/chunks/{config-engine-DEmmPPdp.js → config-engine-uOFYZidB.js} +4 -4
  10. package/dist/chunks/{config-engine-DEmmPPdp.js.map → config-engine-uOFYZidB.js.map} +1 -1
  11. package/dist/chunks/config-file-DlZXJMj8.js +25 -0
  12. package/dist/chunks/config-file-DlZXJMj8.js.map +1 -0
  13. package/dist/chunks/{css-definitions-DotDkIXt.js → css-definitions-ubrHP97m.js} +130 -96
  14. package/dist/chunks/css-definitions-ubrHP97m.js.map +1 -0
  15. package/dist/chunks/{debug-D8pg9RGI.js → debug-CJ31U_WE.js} +6 -6
  16. package/dist/chunks/{debug-D8pg9RGI.js.map → debug-CJ31U_WE.js.map} +1 -1
  17. package/dist/chunks/{dsl-DRhFWQzo.js → dsl-DYSJc1-T.js} +11 -6
  18. package/dist/chunks/dsl-DYSJc1-T.js.map +1 -0
  19. package/dist/chunks/{react-runtime-BpxlOTbO.js → react-runtime-CyyCI02N.js} +190 -63
  20. package/dist/chunks/react-runtime-CyyCI02N.js.map +1 -0
  21. package/dist/chunks/{runtime-engine-CKFXUH-A.js → runtime-engine-BQmOu5-W.js} +69 -53
  22. package/dist/chunks/runtime-engine-BQmOu5-W.js.map +1 -0
  23. package/dist/chunks/{runtime-state-BxaLhVdP.js → runtime-state-DEsyUMU-.js} +28 -3
  24. package/dist/chunks/runtime-state-DEsyUMU-.js.map +1 -0
  25. package/dist/chunks/{style-engine-BBbBuSkT.js → style-engine-BX2LshDZ.js} +309 -103
  26. package/dist/chunks/style-engine-BX2LshDZ.js.map +1 -0
  27. package/dist/chunks/{zero-engine-gJrKBRiw.js → zero-engine-Dw0P4yGM.js} +62 -27
  28. package/dist/chunks/zero-engine-Dw0P4yGM.js.map +1 -0
  29. package/dist/{collector-w4mBj5gd.d.ts → collector-CTU-nl0g.d.ts} +2 -2
  30. package/dist/{config-CZWCzBr6.d.ts → config-CcxqSZI2.d.ts} +35 -16
  31. package/dist/core/index.d.ts +4 -4
  32. package/dist/core/index.js +7 -7
  33. package/dist/hydrate-CRyjjAyl.d.ts +22 -0
  34. package/dist/{index-G9JuwM9p.d.ts → index-xtey-KDu.d.ts} +43 -13
  35. package/dist/index.d.ts +4 -4
  36. package/dist/index.js +8 -8
  37. package/dist/{merge-styles-VXwJ0Frr.d.ts → merge-styles-DSkVc6CD.d.ts} +2 -2
  38. package/dist/ssr/astro.js +4 -4
  39. package/dist/ssr/client.d.ts +2 -0
  40. package/dist/ssr/client.js +2 -0
  41. package/dist/ssr/index.d.ts +3 -22
  42. package/dist/ssr/index.js +4 -4
  43. package/dist/ssr/index.js.map +1 -1
  44. package/dist/ssr/next-config.d.ts +1 -1
  45. package/dist/ssr/next-config.js +13 -18
  46. package/dist/ssr/next-config.js.map +1 -1
  47. package/dist/ssr/next.d.ts +1 -1
  48. package/dist/ssr/next.js +3 -3
  49. package/dist/static/index.d.ts +2 -2
  50. package/dist/{types-JEY8PVYP.d.ts → types-DJEM_BOJ.d.ts} +6 -6
  51. package/dist/zero/babel.d.ts +67 -2
  52. package/dist/zero/babel.js +47 -44
  53. package/dist/zero/babel.js.map +1 -1
  54. package/dist/zero/index.d.ts +58 -3
  55. package/dist/zero/index.js +24 -2
  56. package/dist/zero/index.js.map +1 -0
  57. package/dist/zero/next.d.ts +2 -1
  58. package/dist/zero/next.js +3 -7
  59. package/dist/zero/next.js.map +1 -1
  60. package/docs/README.md +4 -1
  61. package/docs/adoption.md +25 -17
  62. package/docs/ai-agents.md +37 -34
  63. package/docs/comparison.md +9 -8
  64. package/docs/configuration.md +174 -59
  65. package/docs/debug.md +3 -2
  66. package/docs/design-system.md +117 -15
  67. package/docs/dsl.md +122 -81
  68. package/docs/getting-started.md +81 -24
  69. package/docs/injector.md +39 -40
  70. package/docs/methodology.md +45 -20
  71. package/docs/migration-v3.md +2 -2
  72. package/docs/pipeline.md +20 -17
  73. package/docs/plugins.md +20 -40
  74. package/docs/react-api.md +28 -8
  75. package/docs/runtime-benchmarks.md +4 -4
  76. package/docs/ssr.md +83 -63
  77. package/docs/styles.md +27 -17
  78. package/docs/tasty-static.md +142 -55
  79. package/package.json +24 -10
  80. package/dist/babel-rZcEwPLM.d.ts +0 -82
  81. package/dist/chunks/async-storage-CG5HnUT5.js +0 -20
  82. package/dist/chunks/async-storage-CG5HnUT5.js.map +0 -1
  83. package/dist/chunks/build-config-CL4eRu2z.js +0 -42
  84. package/dist/chunks/build-config-CL4eRu2z.js.map +0 -1
  85. package/dist/chunks/collector-D2ARGT2L.js.map +0 -1
  86. package/dist/chunks/css-definitions-DotDkIXt.js.map +0 -1
  87. package/dist/chunks/dsl-DRhFWQzo.js.map +0 -1
  88. package/dist/chunks/react-runtime-BpxlOTbO.js.map +0 -1
  89. package/dist/chunks/runtime-engine-CKFXUH-A.js.map +0 -1
  90. package/dist/chunks/runtime-state-BxaLhVdP.js.map +0 -1
  91. package/dist/chunks/style-engine-BBbBuSkT.js.map +0 -1
  92. package/dist/chunks/zero-engine-gJrKBRiw.js.map +0 -1
package/dist/zero/next.js CHANGED
@@ -1,5 +1,5 @@
1
+ import { t as loadConfigFile } from "../chunks/config-file-DlZXJMj8.js";
1
2
  import * as path from "path";
2
- import { createJiti } from "jiti";
3
3
  import { createRequire } from "module";
4
4
  import { fileURLToPath } from "url";
5
5
  //#region src/zero/next.ts
@@ -64,12 +64,8 @@ function withTastyZero(options = {}) {
64
64
  injectImport: true,
65
65
  ...mode ? { mode } : {}
66
66
  };
67
- if (wpAbsoluteConfigFile) {
68
- const jiti = createJiti(wpProjectDir, { moduleCache: false });
69
- babelPluginOptions.config = () => {
70
- return jiti(wpAbsoluteConfigFile);
71
- };
72
- } else if (tastyConfig) babelPluginOptions.config = tastyConfig;
67
+ if (wpAbsoluteConfigFile) babelPluginOptions.config = () => loadConfigFile(wpAbsoluteConfigFile, wpProjectDir);
68
+ else if (tastyConfig) babelPluginOptions.config = tastyConfig;
73
69
  if (wpAllDeps.length > 0) babelPluginOptions.configDeps = wpAllDeps;
74
70
  const babelPluginConfig = [babelPluginPath, babelPluginOptions];
75
71
  const existingRule = config.module?.rules?.find((rule) => rule.use?.loader === "babel-loader" || rule.use?.some?.((u) => u.loader === "babel-loader"));
@@ -1 +1 @@
1
- {"version":3,"file":"next.js","names":[],"sources":["../../src/zero/next.ts"],"sourcesContent":["/**\n * Next.js configuration wrapper for tasty-zero.\n *\n * Supports both webpack and Turbopack bundlers:\n * - **webpack**: Injects a babel-loader rule with the tasty-zero Babel plugin\n * via `webpack()` config hook. Config is passed as a jiti factory function.\n * - **Turbopack**: Adds a `turbopack.rules` entry with babel-loader and\n * JSON-serializable options (`configFile` path instead of a function).\n * The Babel plugin loads the config internally via jiti.\n *\n * The generated CSS is injected automatically — `@tenphi/tasty/static`\n * imports are replaced with an import of the output CSS file at build time.\n * No manual CSS import in layout files is needed.\n *\n * @example\n * ```javascript\n * // next.config.js\n * const { withTastyZero } = require('@tenphi/tasty/zero/next');\n *\n * module.exports = withTastyZero({\n * output: 'public/tasty.css',\n * configFile: './app/tasty-zero.config.ts',\n * })({\n * // your Next.js config\n * });\n * ```\n */\n\nimport { createRequire } from 'module';\nimport * as path from 'path';\nimport { fileURLToPath } from 'url';\n\nimport { createJiti } from 'jiti';\n\nimport type { TastyZeroBabelOptions, TastyZeroConfig } from './babel';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n// Next.js types (inline to avoid requiring next as a dependency)\ninterface WebpackConfigContext {\n isServer: boolean;\n dev: boolean;\n buildId: string;\n dir: string;\n}\n\n/* eslint-disable @typescript-eslint/no-explicit-any -- webpack/Next.js config types are complex */\ninterface TurbopackLoaderItem {\n loader: string;\n options?: Record<string, unknown>;\n}\n\ninterface TurbopackRuleConfig {\n loaders: (string | TurbopackLoaderItem)[];\n as?: string;\n condition?: unknown;\n}\n\ninterface TurbopackConfig {\n rules?: Record<string, TurbopackRuleConfig | TurbopackRuleConfig[]>;\n [key: string]: unknown;\n}\n\ninterface NextConfig {\n webpack?: (config: any, context: WebpackConfigContext) => any;\n turbopack?: TurbopackConfig;\n [key: string]: unknown;\n}\n\nexport interface TastyZeroNextOptions {\n /**\n * Output path for CSS relative to project root.\n * @default 'public/tasty.css'\n */\n output?: string;\n\n /**\n * Whether to enable the plugin.\n * @default true\n */\n enabled?: boolean;\n\n /**\n * Tasty configuration for build-time processing.\n * For static configs that don't change during dev.\n *\n * For configs that depend on theme files, use `configFile` instead.\n */\n config?: TastyZeroConfig;\n\n /**\n * Path to a TypeScript/JavaScript module that exports the tasty zero config\n * as its default export. The module is re-evaluated on each\n * compilation, enabling hot reload when the file (or its imports) change.\n *\n * @example './app/tasty-zero.config.ts'\n */\n configFile?: string;\n\n /**\n * Extra file paths (relative to project root) that the config depends on.\n * When any of these files change, the Babel cache is invalidated and\n * the config is re-evaluated.\n *\n * The `configFile` itself is always tracked automatically.\n * Use this for transitive dependencies that aren't directly imported\n * by the config file, or when using `config` instead of `configFile`.\n *\n * @example ['./app/theme.ts']\n */\n configDeps?: string[];\n\n /**\n * Output mode for extracted CSS.\n *\n * - `'file'` (default): CSS is written to a single output file.\n * - `'inject'`: CSS is embedded inline in JS and injected at runtime.\n * No CSS file is written. Best for reusable components and extensions.\n *\n * When `mode` is `'inject'`, `output` is ignored.\n *\n * @default 'file'\n */\n mode?: 'file' | 'inject';\n}\n\n/**\n * Next.js configuration wrapper for tasty-zero.\n * Configures both webpack and Turbopack bundlers automatically.\n */\nexport function withTastyZero(options: TastyZeroNextOptions = {}) {\n const {\n output = 'public/tasty.css',\n enabled = true,\n config: tastyConfig,\n configFile,\n configDeps = [],\n mode,\n } = options;\n\n return (nextConfig: NextConfig = {}): NextConfig => {\n if (!enabled) {\n return nextConfig;\n }\n\n const projectDir = process.cwd();\n const absoluteOutput = path.resolve(projectDir, output);\n const babelPluginPath = path.resolve(__dirname, 'babel.js');\n\n const absoluteConfigFile = configFile\n ? path.resolve(projectDir, configFile)\n : undefined;\n\n const allDeps = [\n ...(absoluteConfigFile ? [absoluteConfigFile] : []),\n ...configDeps.map((dep) => path.resolve(projectDir, dep)),\n ];\n\n // --- Turbopack configuration ---\n // Turbopack loader options must be JSON-serializable (no functions).\n // The Babel plugin loads config internally via `configFile` path + jiti.\n const turbopackBabelOptions: Record<string, unknown> = {\n babelrc: false,\n configFile: false,\n parserOpts: {\n plugins: ['typescript', 'jsx', 'decorators-legacy'],\n },\n plugins: [\n [\n babelPluginPath,\n {\n output: absoluteOutput,\n injectImport: true,\n ...(mode ? { mode } : {}),\n ...(absoluteConfigFile\n ? { configFile: absoluteConfigFile }\n : tastyConfig\n ? { config: tastyConfig }\n : {}),\n ...(allDeps.length > 0 ? { configDeps: allDeps } : {}),\n },\n ],\n ],\n };\n\n const existingTurbopack = nextConfig.turbopack || {};\n const existingRules = existingTurbopack.rules || {};\n\n const existingExperimental =\n (nextConfig.experimental as Record<string, unknown>) || {};\n\n return {\n ...nextConfig,\n\n experimental: {\n ...existingExperimental,\n turbopackUseBuiltinBabel: true,\n },\n\n turbopack: {\n ...existingTurbopack,\n rules: {\n ...existingRules,\n '*.{ts,tsx,js,jsx}': {\n condition: { not: 'foreign' },\n loaders: [\n {\n loader: 'babel-loader',\n options: turbopackBabelOptions,\n },\n ],\n },\n },\n },\n\n webpack(config: any, context: WebpackConfigContext) {\n const { dir } = context;\n\n const wpProjectDir = dir || projectDir;\n const wpAbsoluteOutput = path.resolve(wpProjectDir, output);\n const projectRequire = createRequire(\n path.resolve(wpProjectDir, 'package.json'),\n );\n\n const wpAbsoluteConfigFile = configFile\n ? path.resolve(wpProjectDir, configFile)\n : undefined;\n\n const wpAllDeps = [\n ...(wpAbsoluteConfigFile ? [wpAbsoluteConfigFile] : []),\n ...configDeps.map((dep) => path.resolve(wpProjectDir, dep)),\n ];\n\n const babelPluginOptions: TastyZeroBabelOptions = {\n output: wpAbsoluteOutput,\n injectImport: true,\n ...(mode ? { mode } : {}),\n };\n\n if (wpAbsoluteConfigFile) {\n const jiti = createJiti(wpProjectDir, {\n moduleCache: false,\n });\n\n babelPluginOptions.config = () => {\n return jiti(wpAbsoluteConfigFile) as TastyZeroConfig;\n };\n } else if (tastyConfig) {\n babelPluginOptions.config = tastyConfig;\n }\n\n if (wpAllDeps.length > 0) {\n babelPluginOptions.configDeps = wpAllDeps;\n }\n\n const babelPluginConfig = [babelPluginPath, babelPluginOptions];\n\n const existingRule = config.module?.rules?.find(\n (rule: any) =>\n rule.use?.loader === 'babel-loader' ||\n rule.use?.some?.((u: any) => u.loader === 'babel-loader'),\n );\n\n if (existingRule) {\n const babelUse = Array.isArray(existingRule.use)\n ? existingRule.use.find((u: any) => u.loader === 'babel-loader')\n : existingRule.use;\n\n if (babelUse?.options) {\n babelUse.options.plugins = babelUse.options.plugins || [];\n babelUse.options.plugins.push(babelPluginConfig);\n }\n } else {\n config.module = config.module || {};\n config.module.rules = config.module.rules || [];\n config.module.rules.push({\n test: /\\.(tsx?|jsx?)$/,\n exclude: /node_modules/,\n use: [\n {\n loader: projectRequire.resolve('babel-loader'),\n options: {\n babelrc: false,\n configFile: false,\n parserOpts: {\n plugins: ['typescript', 'jsx', 'decorators-legacy'],\n },\n plugins: [babelPluginConfig],\n },\n },\n ],\n });\n }\n\n if (typeof nextConfig.webpack === 'function') {\n return nextConfig.webpack(config, context);\n }\n\n return config;\n },\n };\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"next.js","names":[],"sources":["../../src/zero/next.ts"],"sourcesContent":["/**\n * Next.js configuration wrapper for tasty-zero.\n *\n * Supports both webpack and Turbopack bundlers:\n * - **webpack**: Injects a babel-loader rule with the tasty-zero Babel plugin\n * via `webpack()` config hook. Config is passed as a jiti factory function.\n * - **Turbopack**: Adds a `turbopack.rules` entry with babel-loader and\n * JSON-serializable options (`configFile` path instead of a function).\n * The Babel plugin loads the config internally via jiti.\n *\n * The generated CSS is injected automatically — `@tenphi/tasty/static`\n * imports are replaced with an import of the output CSS file at build time.\n * No manual CSS import in layout files is needed.\n *\n * @example\n * ```javascript\n * // next.config.js\n * const { withTastyZero } = require('@tenphi/tasty/zero/next');\n *\n * module.exports = withTastyZero({\n * output: 'public/tasty.css',\n * configFile: './app/tasty-zero.config.ts',\n * })({\n * // your Next.js config\n * });\n * ```\n */\n\nimport { createRequire } from 'module';\nimport * as path from 'path';\nimport { fileURLToPath } from 'url';\n\nimport type { TastyZeroBabelOptions, TastyZeroConfig } from './babel';\nimport { loadConfigFile } from './config-file';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n// Next.js types (inline to avoid requiring next as a dependency)\ninterface WebpackConfigContext {\n isServer: boolean;\n dev: boolean;\n buildId: string;\n dir: string;\n}\n\n/* eslint-disable @typescript-eslint/no-explicit-any -- webpack/Next.js config types are complex */\ninterface TurbopackLoaderItem {\n loader: string;\n options?: Record<string, unknown>;\n}\n\ninterface TurbopackRuleConfig {\n loaders: (string | TurbopackLoaderItem)[];\n as?: string;\n condition?: unknown;\n}\n\ninterface TurbopackConfig {\n rules?: Record<string, TurbopackRuleConfig | TurbopackRuleConfig[]>;\n [key: string]: unknown;\n}\n\ninterface NextConfig {\n webpack?: (config: any, context: WebpackConfigContext) => any;\n turbopack?: TurbopackConfig;\n [key: string]: unknown;\n}\n\nexport interface TastyZeroNextOptions {\n /**\n * Output path for CSS relative to project root.\n * @default 'public/tasty.css'\n */\n output?: string;\n\n /**\n * Whether to enable the plugin.\n * @default true\n */\n enabled?: boolean;\n\n /**\n * Tasty configuration for build-time processing.\n * For static configs that don't change during dev.\n *\n * For configs that depend on theme files, use `configFile` instead.\n */\n config?: TastyZeroConfig;\n\n /**\n * Path to a TypeScript/JavaScript module that exports the tasty zero config\n * as its default export. The module is re-evaluated on each\n * compilation, enabling hot reload when the file (or its imports) change.\n *\n * @example './app/tasty-zero.config.ts'\n */\n configFile?: string;\n\n /**\n * Extra file paths (relative to project root) that the config depends on.\n * When any of these files change, the Babel cache is invalidated and\n * the config is re-evaluated.\n *\n * The `configFile` itself is always tracked automatically.\n * Use this for transitive dependencies that aren't directly imported\n * by the config file, or when using `config` instead of `configFile`.\n *\n * @example ['./app/theme.ts']\n */\n configDeps?: string[];\n\n /**\n * Output mode for extracted CSS.\n *\n * - `'file'` (default): CSS is written to a single output file.\n * - `'inject'`: CSS is embedded inline in JS and injected at runtime.\n * No CSS file is written. Best for reusable components and extensions.\n *\n * When `mode` is `'inject'`, `output` is ignored.\n *\n * @default 'file'\n */\n mode?: 'file' | 'inject';\n}\n\n/**\n * Next.js configuration wrapper for tasty-zero.\n * Configures both webpack and Turbopack bundlers automatically.\n */\nexport function withTastyZero(options: TastyZeroNextOptions = {}) {\n const {\n output = 'public/tasty.css',\n enabled = true,\n config: tastyConfig,\n configFile,\n configDeps = [],\n mode,\n } = options;\n\n return (nextConfig: NextConfig = {}): NextConfig => {\n if (!enabled) {\n return nextConfig;\n }\n\n const projectDir = process.cwd();\n const absoluteOutput = path.resolve(projectDir, output);\n const babelPluginPath = path.resolve(__dirname, 'babel.js');\n\n const absoluteConfigFile = configFile\n ? path.resolve(projectDir, configFile)\n : undefined;\n\n const allDeps = [\n ...(absoluteConfigFile ? [absoluteConfigFile] : []),\n ...configDeps.map((dep) => path.resolve(projectDir, dep)),\n ];\n\n // --- Turbopack configuration ---\n // Turbopack loader options must be JSON-serializable (no functions).\n // The Babel plugin loads config internally via `configFile` path + jiti.\n const turbopackBabelOptions: Record<string, unknown> = {\n babelrc: false,\n configFile: false,\n parserOpts: {\n plugins: ['typescript', 'jsx', 'decorators-legacy'],\n },\n plugins: [\n [\n babelPluginPath,\n {\n output: absoluteOutput,\n injectImport: true,\n ...(mode ? { mode } : {}),\n ...(absoluteConfigFile\n ? { configFile: absoluteConfigFile }\n : tastyConfig\n ? { config: tastyConfig }\n : {}),\n ...(allDeps.length > 0 ? { configDeps: allDeps } : {}),\n },\n ],\n ],\n };\n\n const existingTurbopack = nextConfig.turbopack || {};\n const existingRules = existingTurbopack.rules || {};\n\n const existingExperimental =\n (nextConfig.experimental as Record<string, unknown>) || {};\n\n return {\n ...nextConfig,\n\n experimental: {\n ...existingExperimental,\n turbopackUseBuiltinBabel: true,\n },\n\n turbopack: {\n ...existingTurbopack,\n rules: {\n ...existingRules,\n '*.{ts,tsx,js,jsx}': {\n condition: { not: 'foreign' },\n loaders: [\n {\n loader: 'babel-loader',\n options: turbopackBabelOptions,\n },\n ],\n },\n },\n },\n\n webpack(config: any, context: WebpackConfigContext) {\n const { dir } = context;\n\n const wpProjectDir = dir || projectDir;\n const wpAbsoluteOutput = path.resolve(wpProjectDir, output);\n const projectRequire = createRequire(\n path.resolve(wpProjectDir, 'package.json'),\n );\n\n const wpAbsoluteConfigFile = configFile\n ? path.resolve(wpProjectDir, configFile)\n : undefined;\n\n const wpAllDeps = [\n ...(wpAbsoluteConfigFile ? [wpAbsoluteConfigFile] : []),\n ...configDeps.map((dep) => path.resolve(wpProjectDir, dep)),\n ];\n\n const babelPluginOptions: TastyZeroBabelOptions = {\n output: wpAbsoluteOutput,\n injectImport: true,\n ...(mode ? { mode } : {}),\n };\n\n if (wpAbsoluteConfigFile) {\n babelPluginOptions.config = () =>\n loadConfigFile<TastyZeroConfig>(wpAbsoluteConfigFile, wpProjectDir);\n } else if (tastyConfig) {\n babelPluginOptions.config = tastyConfig;\n }\n\n if (wpAllDeps.length > 0) {\n babelPluginOptions.configDeps = wpAllDeps;\n }\n\n const babelPluginConfig = [babelPluginPath, babelPluginOptions];\n\n const existingRule = config.module?.rules?.find(\n (rule: any) =>\n rule.use?.loader === 'babel-loader' ||\n rule.use?.some?.((u: any) => u.loader === 'babel-loader'),\n );\n\n if (existingRule) {\n const babelUse = Array.isArray(existingRule.use)\n ? existingRule.use.find((u: any) => u.loader === 'babel-loader')\n : existingRule.use;\n\n if (babelUse?.options) {\n babelUse.options.plugins = babelUse.options.plugins || [];\n babelUse.options.plugins.push(babelPluginConfig);\n }\n } else {\n config.module = config.module || {};\n config.module.rules = config.module.rules || [];\n config.module.rules.push({\n test: /\\.(tsx?|jsx?)$/,\n exclude: /node_modules/,\n use: [\n {\n loader: projectRequire.resolve('babel-loader'),\n options: {\n babelrc: false,\n configFile: false,\n parserOpts: {\n plugins: ['typescript', 'jsx', 'decorators-legacy'],\n },\n plugins: [babelPluginConfig],\n },\n },\n ],\n });\n }\n\n if (typeof nextConfig.webpack === 'function') {\n return nextConfig.webpack(config, context);\n }\n\n return config;\n },\n };\n };\n}\n"],"mappings":"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"}
package/docs/README.md CHANGED
@@ -16,7 +16,7 @@ Use this hub to move from the core idea to a working component, then into design
16
16
 
17
17
  - **Application developer using an existing design system**: [Getting Started](getting-started.md), then [React API](react-api.md).
18
18
  - **Design-system author**: [Methodology](methodology.md), [Building a Design System](design-system.md), [Configuration](configuration.md), and [Adoption Guide](adoption.md).
19
- - **Platform or tooling engineer**: [Configuration](configuration.md), [Build-Time Extraction (`tastyStatic`)](tasty-static.md), [Server-Side Rendering](ssr.md), and [Debug Utilities](debug.md).
19
+ - **Platform or tooling engineer**: [Configuration](configuration.md), [Plugins & Extension Points](plugins.md), [Build-Time Extraction (`tastyStatic`)](tasty-static.md), [Server-Side Rendering](ssr.md), and [Debug Utilities](debug.md).
20
20
 
21
21
  ## By Styling Approach
22
22
 
@@ -29,6 +29,9 @@ Use this hub to move from the core idea to a working component, then into design
29
29
  ## By Task
30
30
 
31
31
  - **Learn the style language**: [Style DSL](dsl.md)
32
+ - **Set up linting and autocomplete**: [Getting Started — ESLint plugin](getting-started.md#eslint-plugin) and [TypeScript autocomplete](getting-started.md#typescript-autocomplete)
33
+ - **Design a component's public API** (`styleProps`, `modProps`, `tokenProps`, variants, sub-elements): [React API](react-api.md), then [Methodology](methodology.md)
34
+ - **Generate a color palette with contrast solving**: [Glaze](../README.md#glaze)
32
35
  - **Brief an AI agent (or yourself) on writing correct styles**: [Style Rules for AI Agents](ai-agents.md)
33
36
  - **Look up a property handler**: [Style Properties](styles.md)
34
37
  - **Define tokens, units, recipes, keyframes, properties, functions, or polyfills globally**: [Configuration](configuration.md)
package/docs/adoption.md CHANGED
@@ -24,7 +24,8 @@ Product code
24
24
  - The DSL and value parser (tokens, custom units, auto-calc, color opacity)
25
25
  - State compilation (mutually exclusive selectors from state maps)
26
26
  - Style injection and deduplication (runtime or build-time)
27
- - The `tasty()` component factory and hooks API
27
+ - The `tasty()` component factory and style functions (`useStyles`, `useGlobalStyles`, …)
28
+ - The lint rules and TypeScript extension interfaces that check usage against your vocabulary
28
29
 
29
30
  **What the DS team owns:**
30
31
 
@@ -64,16 +65,18 @@ For a detailed comparison with Tailwind, Panda CSS, vanilla-extract, StyleX, Sti
64
65
 
65
66
  Tasty provides the engine. The DS team defines the language that runs on it. Here is what that typically involves:
66
67
 
67
- | Layer | What you define | Where |
68
- | ------------------ | ---------------------------------------------------------- | ------------------------------------------ |
69
- | **Tokens** | Color names, spacing scale, border widths, radii | `configure({ tokens })` |
70
- | **Units** | Custom multiplier units (`x`, `r`, `bw`, or your own) | `configure({ units })` |
71
- | **State aliases** | Responsive breakpoints, theme modes, feature flags | `configure({ states })` |
72
- | **Recipes** | Reusable style bundles (card, elevated, input-reset) | `configure({ recipes })` |
73
- | **Typography** | Preset definitions (h1-h6, t1-t4, etc.) | `configure({ presets: { ... } })` |
74
- | **Style props** | Which CSS properties each component exposes as React props | `styleProps` in each component |
75
- | **Sub-elements** | Inner parts of compound components (Title, Icon, Content) | `elements` + capitalized keys in `styles` |
76
- | **Override rules** | How product engineers extend or constrain components | Styled wrappers via `tasty(Base, { ... })` |
68
+ | Layer | What you define | Where |
69
+ | ------------------ | ----------------------------------------------------------------------------------- | --------------------------------------------------- |
70
+ | **Tokens** | Color names and named values (`#surface`, `$sidebar-width`) | `configure({ tokens })` |
71
+ | **Units** | Spacing step, radius, border and outline widths (`x`, `r`, `bw`, `ow`, or your own) | `configure({ units })` |
72
+ | **State aliases** | Responsive breakpoints, theme modes, feature flags | `configure({ states })` |
73
+ | **Recipes** | Reusable style bundles (card, elevated, input-reset) | `configure({ recipes })` |
74
+ | **Typography** | Preset definitions (h1-h6, t1-t4, etc.) | `configure({ presets: { ... } })` |
75
+ | **Style props** | Which CSS properties each component exposes as React props | `styleProps` in each component |
76
+ | **Sub-elements** | Inner parts of compound components (Title, Icon, Content) | `elements` + capitalized keys in `styles` |
77
+ | **Component API** | Typed props for states, values, and looks | `modProps`, `tokenProps`, `variants` |
78
+ | **Override rules** | How product engineers extend or constrain components | Styled wrappers via `tasty(Base, { ... })` |
79
+ | **Guardrails** | The names the linter and TypeScript check against | `tasty.config.ts`, `declare module '@tenphi/tasty'` |
77
80
 
78
81
  The same engine can power a minimal design system with a handful of tokens:
79
82
 
@@ -155,18 +158,23 @@ configure({
155
158
  '#surface': '#white',
156
159
  '#text': '#111',
157
160
  '$card-padding': '4x',
161
+ $gap: '8px',
162
+ $radius: '6px',
163
+ '$border-width': '1px',
158
164
  },
159
- // Common units (x, r, bw, ow, cr) are built-in.
160
- // A DS typically redefines them to use CSS custom properties
161
- // so that the actual scale is controlled via CSS, not JS:
165
+ // Built-in units (x, r, cr, bw, ow) compile to fixed values by default.
166
+ // A DS often points them at the custom properties above, so the scale
167
+ // is controlled by CSS at runtime rather than fixed at build time:
162
168
  units: {
163
- x: 'var(--gap)', // 2x → calc(var(--gap) * 2)
169
+ x: 'var(--gap)', // 2x → calc(2 * var(--gap))
164
170
  r: 'var(--radius)',
165
171
  bw: 'var(--border-width)',
166
172
  },
167
173
  });
168
174
  ```
169
175
 
176
+ Turn on the guardrails in the same step. Add [`@tenphi/eslint-plugin-tasty`](https://github.com/tenphi/eslint-plugin-tasty) with `tasty.configs.recommended` and a `tasty.config.ts` that lists the tokens, units, states, presets, and recipes as you define them, and register the same color and preset names for TypeScript autocomplete (`TastyNamedColors`, `TastyPresetNames`). Every component you migrate from here on is checked against the vocabulary. See [Getting Started — ESLint plugin](getting-started.md#eslint-plugin).
177
+
170
178
  ### Phase 3 -- State aliases and recipes
171
179
 
172
180
  Define the state vocabulary your components will share. This is where you start encoding your team's conventions.
@@ -175,7 +183,7 @@ Define the state vocabulary your components will share. This is where you start
175
183
  configure({
176
184
  states: {
177
185
  '@mobile': '@media(w < 768px)',
178
- '@tablet': '@media(w < 1024px)',
186
+ '@tablet': '@media(768px <= w < 1024px)',
179
187
  '@dark':
180
188
  '@root(schema=dark) | (!@root(schema) & @media(prefers-color-scheme: dark))',
181
189
  },
@@ -252,7 +260,7 @@ const Card = tasty({
252
260
 
253
261
  ### Phase 7 -- Expand to full DS coverage
254
262
 
255
- Migrate the remaining components, add the [ESLint plugin](https://github.com/tenphi/eslint-plugin-tasty) to enforce style conventions at lint time, and choose between [server-only `tasty()`](ssr.md) and [`tastyStatic()` build-time extraction](tasty-static.md) when a page should ship no Tasty styling runtime.
263
+ Migrate the remaining components, switch the [ESLint plugin](https://github.com/tenphi/eslint-plugin-tasty) to `tasty.configs.strict` to also catch unknown `$` tokens and state aliases, and choose between [server-only `tasty()`](ssr.md) and [`tastyStatic()` build-time extraction](tasty-static.md) when a page should ship no Tasty styling runtime.
256
264
 
257
265
  ---
258
266
 
package/docs/ai-agents.md CHANGED
@@ -64,19 +64,20 @@ The last row goes the other way — longhands over the shorthand — because `fl
64
64
 
65
65
  ### Tokens
66
66
 
67
- | Syntax | Meaning |
68
- | --------------------- | ------------------------------------------------------------------------- |
69
- | `#name` | color token → `var(--name-color)` |
70
- | `#name.50` | token at 50% opacity (`0`–`100`) |
71
- | `#name.$opacity` | opacity from a custom property |
72
- | `#clear` / `#current` | `transparent` / `currentcolor` |
73
- | `$name` | custom property → `var(--name)` |
74
- | `(#a, #b)` | fallback chain |
75
- | `$$name` / `##name` | the custom property _name_ (not its value) — inside `transition` |
76
- | `$$name(…)` | call a CSS `@function` declared via `@function`, `useFunction`, or config |
77
-
78
- - ❌ `#f5f5f5`, `rgb(0 0 0)`, `oklch(…)`, `okhsl(…)`, `red` → ✅ `#surface` (add the token to the config if it doesn't exist).
67
+ | Syntax | Meaning |
68
+ | --------------------- | -------------------------------------------------------------------------------------------------- |
69
+ | `#name` | color token → `var(--name-color)` |
70
+ | `#name.50` | token at 50% opacity — digits are a decimal fraction: `.5` = `.50` = 50%, `.05` = 5%, `.100` = 10% |
71
+ | `#name.$opacity` | opacity from a custom property |
72
+ | `#clear` / `#current` | `transparent` / `currentcolor` |
73
+ | `$name` | custom property → `var(--name)` |
74
+ | `(#a, #b)` | fallback chain |
75
+ | `$$name` / `##name` | the custom property _name_ (not its value) — inside `transition` |
76
+ | `$$name(…)` | call a CSS `@function` declared via `@function`, `useFunction`, or config |
77
+
78
+ - ❌ `#f5f5f5`, `rgb(0 0 0)`, `oklch(…)`, `okhsl(…)`, `okhst(…)`, `red` → ✅ `#surface` (add the token to the config if it doesn't exist).
79
79
  - ❌ `var(--gap)` → ✅ `$gap` · ❌ `$accent-color` → ✅ `#accent` · ❌ `transparent` → ✅ `#clear` · ❌ `currentColor` → ✅ `#current`.
80
+ - `#clear` is a default replace token for `transparent`, so `#clear.5` and `#clear.$opacity` stay transparent. It does not read `--clear-color`; override it through `replaceTokens` if needed.
80
81
  - A `$name` must be declared as a `'$name': value` key in the same styles object, or in the config.
81
82
  - Names are case-sensitive and start lowercase — `$myVar` → `--myVar`; a leading capital folds (`$Foo` → `--foo`). Prefer kebab-case.
82
83
  - A `$name` reference is untyped: it fills the first free slot in a shorthand (`border: '1bw $my-style'` → the line style). Write colors as `#name`; `$name-color` is a fallback for pointing at a raw CSS custom property that holds a color.
@@ -84,7 +85,7 @@ The last row goes the other way — longhands over the shorthand — because `fl
84
85
 
85
86
  ### Units
86
87
 
87
- Tasty units: `x` (gap multiple), `r` (radius), `cr` (card radius), `bw` (border width), `ow` (outline width), `sf` (`minmax(0, Nfr)`). All CSS units work too, plus anything in `units` in the config. Any other unit is an error.
88
+ Tasty units and their defaults: `x` 8px spacing step, `r` 6px radius, `cr` 10px card radius, `bw` 1px border width, `ow` 3px outline width, `sf` → `minmax(0, Nfr)`. They compile to literal values; change them through `units` in the config, not with `$gap`/`$radius` tokens. All CSS units work too, plus anything in `units` in the config. Any other unit is an error. Units are not resolved inside `@media(…)` / `@(…)` conditions — write query lengths in `px`, `em`, or `rem`.
88
89
 
89
90
  Prefer units over raw pixels: `8px` → `1x` … `64px` → `8x`; `radius: '6px'` → `'1r'`; `border: '1px …'` → `'1bw …'`.
90
91
 
@@ -94,7 +95,7 @@ Prefer units over raw pixels: `8px` → `1x` … `64px` → `8x`; `radius: '6px'
94
95
 
95
96
  ### `true`
96
97
 
97
- `true` means "the design-system default" and is accepted **only** by: `border`, `radius`, `outline`, `shadow`, `padding`, `margin`, `gap`, `inset`, `width`, `height`, `fill`, `color`, `fade`, `preset`, `font`, `scrollbar`, `scrollMargin`, `scrollPadding`, and the enhanced logical category styles listed above, plus `hide`. Anywhere else it is an error.
98
+ `true` means "the design-system default" and is accepted **only** by: `border`, `radius`, `outline`, `shadow`, `padding`, `margin`, `gap`, `inset`, `width`, `height`, `color`, `fade`, `preset`, `font`, `scrollbar`, `scrollMargin`, `scrollPadding`, and the enhanced logical category styles listed above, plus `hide`. Anywhere else it is an error.
98
99
 
99
100
  ### `!important`
100
101
 
@@ -104,21 +105,23 @@ Never use it. Tasty owns specificity through doubled selectors and state orderin
104
105
 
105
106
  A value is `[values…] [modifiers…]`, and several groups can be comma-separated (later groups override earlier ones). A group that names **direction** modifiers takes a **single** value, applied to every direction it names — per-side values come from comma groups. A group naming no direction keeps plain CSS shorthand order (1–4 values). (`inset` + `dock` is the one exception: a second value insets the spanned sides.) Only the modifiers a property knows are valid:
106
107
 
107
- | Property | Modifiers |
108
- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
109
- | `padding`, `margin`, `inset`, `fade` | `top` `right` `bottom` `left` |
110
- | `border` | the four directions + `solid` `dashed` `dotted` `double` `groove` `ridge` `inset` `outset` `none` `hidden` |
111
- | logical axis spacing/inset styles | `start` `end` |
112
- | `blockBorder`, `inlineBorder` | `start` `end` + the border style keywords above |
113
- | `outline` | the style keywords above |
114
- | `radius` | `top` `right` `bottom` `left` + shapes `round` `ellipse` `leaf` `backleaf` |
115
- | `width`, `height`, `blockSize`, `inlineSize` | `min` `max` `fixed` |
116
- | `flow` | `row` `column` `row-reverse` `column-reverse` `wrap` `nowrap` `dense` |
117
- | `overflow` | `visible` `hidden` `scroll` `clip` `auto` `overlay` |
118
- | `position` | `static` `relative` `absolute` `fixed` `sticky` |
119
- | `shadow` | `inset` |
120
- | `preset` | `name / strong` (or `bold`) `italic` `icon` `tight` |
121
- | box properties | `longhand` — emit CSS longhands instead of the shorthand |
108
+ | Property | Modifiers |
109
+ | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
110
+ | `padding`, `margin`, `fade` | `top` `right` `bottom` `left` |
111
+ | `inset` | the four directions + `dock` |
112
+ | `border` | the four directions + `solid` `dashed` `dotted` `double` `groove` `ridge` `inset` `outset` `none` `hidden` |
113
+ | logical axis spacing/inset styles | `start` `end` |
114
+ | `blockBorder`, `inlineBorder` | `start` `end` + the border style keywords above |
115
+ | `outline` | the style keywords above |
116
+ | `radius` | `top` `right` `bottom` `left`, corners `top-left` `top-right` `bottom-right` `bottom-left`, `inherit` + shapes `round` `ellipse` `leaf` `backleaf` |
117
+ | `width`, `height`, `blockSize`, `inlineSize` | `min` `max` `fixed` |
118
+ | `flow` | `row` `column` `row-reverse` `column-reverse` `wrap` `nowrap` `dense` |
119
+ | `overflow` | `visible` `hidden` `scroll` `clip` `auto` `overlay` |
120
+ | `position` | `static` `relative` `absolute` `fixed` `sticky` |
121
+ | `shadow` | `inset` |
122
+ | `preset` | `name / strong` (or `bold`) `italic` `icon` `tight` `normal` |
123
+ | `scrollbar` | `thin` `auto` `none` `stable` `both-edges` `always` |
124
+ | box properties | `longhand` — emit CSS longhands instead of the shorthand |
122
125
 
123
126
  Directional modifiers beat placeholder zeros:
124
127
 
@@ -128,11 +131,11 @@ Directional modifiers beat placeholder zeros:
128
131
  - ❌ `padding: '2x 4x top right'` — a directional group takes one value, so `4x` is dropped → ✅ `padding: '2x top, 4x right'`
129
132
  - ❌ `fade: '3x 1x top bottom'` → ✅ `fade: '3x top, 1x bottom'`
130
133
 
131
- Value-only properties reject both colors and modifiers: `gap`, `columnGap`, `rowGap`, `opacity`, `zIndex`, `order`, `flexGrow`, `flexShrink`, `flexBasis`, `aspectRatio`, `lineClamp`, and `tabSize`. Logical axis category styles accept one or two values in start/end order and the `start`, `end`, and `longhand` modifiers. `fill` and `color` take a color (plus `none` / `transparent`); `caretColor` and `accentColor` take a color only.
134
+ Value-only properties reject both colors and modifiers: `gap`, `columnGap`, `rowGap`, `opacity`, `zIndex`, `order`, `flexGrow`, `flexShrink`, `flexBasis`, `aspectRatio`, `lineClamp`, and `tabSize`. Logical axis category styles accept one or two values in start/end order and the `start`, `end`, and `longhand` modifiers. `fill`, `color`, `caretColor`, and `accentColor` take a color only (use `#clear` for transparent).
132
135
 
133
136
  ### `transition`
134
137
 
135
- Use semantic names, not CSS property names: `fade` `fill` `color` `theme` `border` `radius` `shadow` `outline` `preset` `text` `gap` `opacity` `translate` `rotate` `scale` `filter` `image` `background` `width` `height` `zIndex` `inset` `flow` `dimension`.
138
+ Use semantic names, not CSS property names: `fade` `fill` `color` `theme` `border` `radius` `shadow` `outline` `preset` `text` `gap` `opacity` `translate` `rotate` `scale` `filter` `image` `background` `width` `height` `zIndex` `inset`.
136
139
 
137
140
  ❌ `transition: 'background-color 0.2s'` → ✅ `transition: 'fill'`
138
141
 
@@ -168,13 +171,13 @@ Precedence `!` > `^` > `|` > `&`; use parentheses to override.
168
171
 
169
172
  Rules:
170
173
 
171
- 1. **`''` comes first.** The bare default is the lowest-priority state; placing it later would override everything above it.
174
+ 1. **`''` comes first.** The bare default is the lowest-priority state. Written later, it triggers a `MISPLACED_DEFAULT_STATE` warning and Tasty moves it to the front.
172
175
  2. **Every state map needs `''` or `_`** — except when extending (`tasty(Parent, …)`), where omitting `''` merges into the parent's states and including `''` replaces them wholesale.
173
176
  3. **`_` is standalone-only** and always first (with `''` right after it, if present). `_` is a never-negated fallback floor for cases where a higher-priority branch may be _unknown_ (`@supports`, container queries). If a map contains only `_` and `''`, drop the `''`.
174
177
  4. **No nested maps:** ❌ `{ hovered: { pressed: 'x' } }` → ✅ `{ 'hovered & pressed': 'x' }`
175
178
  5. **State keys never sit at the top level** of a styles object — `:hover`, `.active`, `[open]` belong inside a property value.
176
179
  6. **Write keys Tasty can fully read.** Names start with a letter (a single letter is fine: `x` → `[data-x]`); join states with an operator (`hovered & focused`, not `hovered focused`); no space between `@media` and `(`. Tasty warns (`INVALID_STATE_KEY`) and skips what it cannot read.
177
- 7. **Prefer automatic native states:** use `disabled` and `checked` in Tasty component styles instead of `[disabled]` and `[checked]`. The concise states track the corresponding native prop or attribute automatically.
180
+ 7. **Prefer automatic native states:** use `disabled`, `checked`, and `hidden` in Tasty component styles instead of `[disabled]`, `[checked]`, and `[hidden]`. Tasty sets `data-disabled`, `data-checked`, and `data-hidden` from the native props automatically.
178
181
 
179
182
  ```jsx
180
183
  color: { '': '#text', hovered: '#accent', disabled: '#text.40' }
@@ -204,7 +207,7 @@ When extending a parent's state map: `'@inherit'` reuses the parent's value for
204
207
  A **capitalized** key targets `[data-element="Name"]`, and its value must be a style object.
205
208
 
206
209
  ```jsx
207
- styles: { Title: { preset: 'h3' }, Icon: { $: '>@:last-child', color: '#accent' } }
210
+ styles: { Title: { preset: 'h3' }, Icon: { $: '>', color: { '': '#text', '@own(:last-child)': '#accent' } } }
208
211
  ```
209
212
 
210
213
  ❌ nested-selector keys (`'& .title'`, `'&:hover'`) → ✅ sub-elements and state maps. Use the `$` affix property inside a sub-element to control how its selector attaches (`>` direct child, `@` placeholder for the element itself, `&::before` for a root pseudo-element).
@@ -6,7 +6,7 @@ Tasty is CSS-in-JS for React design systems. It is built for the point where reu
6
6
 
7
7
  Its distinctive guarantee is narrow and concrete: each property’s state map compiles into selectors that cannot compete, so one declared branch wins regardless of CSS source order.
8
8
 
9
- That does not require a big upfront configuration step. Tasty's built-in units and normal CSS color values work out of the box, and `okhsl(...)` is available immediately as the recommended path for color authoring. The extra setup comes later if a team wants shared tokens, aliases, recipes, or stricter conventions.
9
+ That does not require a big upfront configuration step. Tasty's built-in units and normal CSS color values work out of the box, and the built-in `okhsl(...)` and `okhst(...)` color functions are available immediately as the recommended path for color authoring. The extra setup comes later if a team wants shared tokens, aliases, recipes, or stricter conventions.
10
10
 
11
11
  Most styling tools focus on one of these layers:
12
12
 
@@ -95,11 +95,12 @@ That makes Tasty less of a "better way to write CSS objects" and more of a **sta
95
95
 
96
96
  Beyond state resolution, Tasty also provides several structural capabilities that reinforce the design-system layer:
97
97
 
98
- - **CSS properties as typed React props** — `styleProps` lets a component expose selected style properties as normal props (`<Button placeSelf="end">`), including state maps for responsive values. This keeps layout and composition controls inside a governed component API instead of pushing teams back to ad hoc styling escapes.
99
- - **Sub-element styling** — Compound components declare inner parts via capitalized keys in `styles` and `data-element` attributes. States, tokens, and recipes apply across the entire element tree from a single definition. See [React API — Sub-element Styling](react-api.md#sub-element-styling).
98
+ - **Typed component APIs** — `styleProps` exposes selected style properties as normal props (`<Button placeSelf="end">`, with state maps for responsive values), `modProps` turns modifiers into typed props (`size: ['sm', 'md', 'lg'] as const`), `tokenProps` passes per-instance values without generating new CSS, and `variants` gives a typed `variant` prop. Layout and composition controls stay inside a governed component API instead of pushing teams back to ad hoc styling escapes.
99
+ - **Sub-element styling** — Compound components declare inner parts via capitalized keys in `styles` and the `elements` option, which produces typed sub-components (`<Card.Title>`) that share the root's state context. States, tokens, and recipes apply across the entire element tree from a single definition. See [React API — Sub-element Styling](react-api.md#sub-element-styling).
100
+ - **Modern CSS as state keys** — Container queries, `@supports`, `:has()`, root and parent states, and `@starting-style` live in the same state maps as `:hover`, and `@keyframes`, `@property`, `@font-face`, `@counter-style`, and `@function` are built in.
100
101
  - **Auto-inferred `@property`** — When a custom property is assigned a concrete value, Tasty infers the CSS `@property` syntax and registers it automatically, enabling smooth transitions on custom properties without manual declarations.
101
102
  - **AI-friendly style definitions** — Style definitions are declarative, self-contained, and structurally consistent. AI tools can read, refactor, and generate Tasty styles as confidently as a human — no hidden cascade logic or implicit ordering to second-guess.
102
- - **Companion ecosystem** — An [ESLint plugin](https://github.com/tenphi/eslint-plugin-tasty) with 27 lint rules, a [VS Code extension](https://github.com/tenphi/tasty-vscode-extension) for syntax highlighting, and [Glaze](https://github.com/tenphi/glaze) for OKHSL color theme generation with automatic WCAG contrast solving.
103
+ - **Companion ecosystem** — An [ESLint and oxlint plugin](https://github.com/tenphi/eslint-plugin-tasty) with 40+ rules that validate styles against your project's `tasty.config.ts`, TypeScript augmentation for autocompleting your own token and preset names, a [VS Code extension](https://github.com/tenphi/tasty-vscode-extension) for syntax highlighting and snippets, and [Glaze](https://github.com/tenphi/glaze) for OKHST palette generation with WCAG and APCA contrast solving.
103
104
 
104
105
  ---
105
106
 
@@ -184,7 +185,7 @@ Every new state doubles the selector count. Miss one intersection and you ship a
184
185
  )}>
185
186
  ```
186
187
 
187
- The `theme` branch is runtime JS, not CSS. Intersections like `disabled + hover` need manual `!important` or extra utilities to override correctly.
188
+ The `theme` branch is runtime JS, not CSS. Tailwind resolves `hover:` against `disabled:` by its global variant order, not per component; when your priority differs from that order, or two base utilities conflict (`bg-primary` vs `bg-danger`), you need `tailwind-merge`, `!` modifiers, or more conditional class logic.
188
189
 
189
190
  **Tasty** — each property declares all its states in one map. The engine generates mutually exclusive selectors:
190
191
 
@@ -196,10 +197,10 @@ const Button = tasty({
196
197
  '': '#primary',
197
198
  ':hover': '#primary-hover',
198
199
  ':active': '#primary-pressed',
199
- disabled: '#surface',
200
200
  'theme=danger': '#danger',
201
201
  'theme=danger & :hover': '#danger-hover',
202
202
  'theme=danger & :active': '#danger-pressed',
203
+ disabled: '#surface',
203
204
  },
204
205
  color: {
205
206
  '': '#on-primary',
@@ -213,7 +214,7 @@ const Button = tasty({
213
214
  });
214
215
  ```
215
216
 
216
- `disabled` always wins over `:hover` because Tasty emits negation selectors — no source-order dependence, no manual intersection management, no `!important`.
217
+ `disabled` is declared last, so it wins over `:hover`, `:active`, and `theme=danger`: Tasty guards every earlier branch with `:not([data-disabled])` — no source-order dependence, no manual intersection management, no `!important`.
217
218
 
218
219
  ---
219
220
 
@@ -328,7 +329,7 @@ That makes it narrower in audience, but deeper in architectural ambition.
328
329
 
329
330
  There is also a fundamental architectural difference: Emotion and styled-components rely on React context and hooks internally, which means they require `'use client'` in modern React and cannot run as React Server Components. Tasty's style functions and `tasty()` components are hook-free, so they work as server components by default and ship no Tasty styling runtime in server-only contexts. This is not a minor compatibility detail — it means Tasty-based components stay as server components until _your_ code needs interactivity, while Emotion and styled-components force the client boundary at the styling layer.
330
331
 
331
- For teams evaluating runtime styling at scale, Tasty also documents its runtime benchmarks and caching model in the main [README](../README.md#performance). That matters, but it is still secondary to the core question of whether you want Tasty's deterministic selector model.
332
+ For teams evaluating runtime styling at scale, Tasty publishes reproducible [Runtime Benchmarks](runtime-benchmarks.md) and summarizes its caching model in the [README](../README.md#performance). That matters, but it is still secondary to the core question of whether you want Tasty's deterministic selector model.
332
333
 
333
334
  ---
334
335