@tenphi/tasty 2.6.5 → 2.7.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 (30) hide show
  1. package/dist/{collector-c00_hT9R.js → collector-BWvvN7_y.js} +3 -3
  2. package/dist/{collector-c00_hT9R.js.map → collector-BWvvN7_y.js.map} +1 -1
  3. package/dist/{config-IzenlK2R.js → config-DF2QZQEW.js} +144 -32
  4. package/dist/{config-IzenlK2R.js.map → config-DF2QZQEW.js.map} +1 -1
  5. package/dist/core/index.js +5 -5
  6. package/dist/{core-J9U8fXzr.js → core-BbdGIKAK.js} +5 -5
  7. package/dist/{core-J9U8fXzr.js.map → core-BbdGIKAK.js.map} +1 -1
  8. package/dist/{css-writer-CCyaR6ZM.js → css-writer-Bh05D6KI.js} +3 -3
  9. package/dist/{css-writer-CCyaR6ZM.js.map → css-writer-Bh05D6KI.js.map} +1 -1
  10. package/dist/{format-rules-CCb7qNPt.js → format-rules-DCI2lomx.js} +2 -2
  11. package/dist/{format-rules-CCb7qNPt.js.map → format-rules-DCI2lomx.js.map} +1 -1
  12. package/dist/{hydrate-DEsdmcdy.js → hydrate-DsFfFPVK.js} +2 -2
  13. package/dist/{hydrate-DEsdmcdy.js.map → hydrate-DsFfFPVK.js.map} +1 -1
  14. package/dist/index.js +6 -6
  15. package/dist/{keyframes-DxAp6xwG.js → keyframes-Dg95rDpN.js} +2 -2
  16. package/dist/{keyframes-DxAp6xwG.js.map → keyframes-Dg95rDpN.js.map} +1 -1
  17. package/dist/{merge-styles-Ugifi570.js → merge-styles-Bnn6j_SA.js} +2 -2
  18. package/dist/{merge-styles-Ugifi570.js.map → merge-styles-Bnn6j_SA.js.map} +1 -1
  19. package/dist/{resolve-recipes-DEmQhiop.js → resolve-recipes-CBQaQ3tD.js} +3 -3
  20. package/dist/{resolve-recipes-DEmQhiop.js.map → resolve-recipes-CBQaQ3tD.js.map} +1 -1
  21. package/dist/ssr/astro-client.js +1 -1
  22. package/dist/ssr/astro.js +3 -3
  23. package/dist/ssr/index.js +3 -3
  24. package/dist/ssr/next.js +4 -4
  25. package/dist/static/index.js +1 -1
  26. package/dist/zero/babel.js +4 -4
  27. package/dist/zero/index.js +1 -1
  28. package/docs/dsl.md +109 -0
  29. package/docs/pipeline.md +40 -2
  30. package/package.json +5 -5
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-recipes-DEmQhiop.js","names":[],"sources":["../src/utils/resolve-recipes.ts"],"sourcesContent":["/**\n * Recipe resolution utility.\n *\n * Resolves `recipe` style properties by looking up predefined recipe styles\n * from global configuration and merging them with the component's own styles.\n *\n * Resolution order per level (top-level and each sub-element independently):\n * base_recipe_1 base_recipe_2 → component styles → post_recipe_1 post_recipe_2\n *\n * The `/` separator splits base recipes (before component styles)\n * from post recipes (after component styles). All merges use mergeStyles\n * semantics: primitives and state maps with '' key fully replace;\n * state maps without '' key extend the existing value.\n *\n * Returns the same object reference if no recipes are present (zero overhead).\n */\n\nimport { getGlobalRecipes } from '../config';\nimport { isSelector } from '../pipeline';\nimport type { RecipeStyles, Styles } from '../styles/types';\n\nimport { isDevEnv } from './is-dev-env';\nimport { mergeStyles } from './merge-styles';\n\nconst devMode = isDevEnv();\n\ninterface ParsedRecipeGroups {\n base: string[] | null;\n post: string[] | null;\n}\n\n/**\n * Parse a recipe string into base and post recipe name groups.\n *\n * Syntax: `'base1 base2 / post1 post2'`\n * - Names are space-separated within each group\n * - `/` separates base (before component) from post (after component) groups\n * - `/` is optional; if absent, all names are base\n * - `none` as the sole base value means \"no base recipes\"\n *\n * Returns `{ base: null, post: null }` if the string is empty or invalid.\n */\nfunction parseRecipeNames(value: unknown): ParsedRecipeGroups {\n const empty: ParsedRecipeGroups = { base: null, post: null };\n\n if (typeof value !== 'string') return empty;\n const trimmed = value.trim();\n if (trimmed === '') return empty;\n\n const slashIndex = trimmed.indexOf('/');\n\n if (slashIndex === -1) {\n if (trimmed === 'none') return empty;\n const names = splitNames(trimmed);\n return { base: names, post: null };\n }\n\n const basePart = trimmed.slice(0, slashIndex);\n const postPart = trimmed.slice(slashIndex + 1);\n\n return {\n base: basePart.trim() === 'none' ? null : splitNames(basePart),\n post: splitNames(postPart),\n };\n}\n\nfunction splitNames(s: string): string[] | null {\n const names = s.split(/\\s+/).filter(Boolean);\n return names.length > 0 ? names : null;\n}\n\n/**\n * Collect merged styles for a list of recipe names.\n * Each recipe is flat-spread on top of the previous.\n */\nfunction collectRecipeStyles(\n names: string[],\n recipes: Record<string, RecipeStyles>,\n): Record<string, unknown> {\n let merged: Record<string, unknown> = {};\n\n for (const name of names) {\n const recipeStyles = recipes[name];\n\n if (!recipeStyles) {\n if (devMode) {\n console.warn(\n `[Tasty] Recipe \"${name}\" not found. ` +\n `Make sure it is defined in configure({ recipes: { ... } }).`,\n );\n }\n continue;\n }\n\n merged = { ...merged, ...(recipeStyles as Record<string, unknown>) };\n }\n\n return merged;\n}\n\n/**\n * Resolve recipe references in a flat styles object (no sub-elements).\n * Returns null if no `recipe` key is present.\n *\n * Resolution: base recipes → component styles → post recipes (all via mergeStyles)\n */\nfunction resolveRecipesForLevel(\n styles: Record<string, unknown>,\n recipes: Record<string, RecipeStyles>,\n): Record<string, unknown> | null {\n if (!('recipe' in styles)) return null;\n\n const { base, post } = parseRecipeNames(styles.recipe);\n\n // Separate selector keys (sub-elements) from flat style properties.\n // mergeStyles handles selectors with its own semantics (e.g. false = delete),\n // but at this level we only want recipe merging on flat properties.\n\n const { recipe: _recipe, ...allRest } = styles;\n const flatStyles: Record<string, unknown> = {};\n const selectorStyles: Record<string, unknown> = {};\n\n for (const key of Object.keys(allRest)) {\n if (isSelector(key)) {\n selectorStyles[key] = allRest[key];\n } else {\n flatStyles[key] = allRest[key];\n }\n }\n\n if (!base && !post) {\n return allRest;\n }\n\n // 1. Merge base recipes, then component styles on top (via mergeStyles)\n let result: Record<string, unknown>;\n\n if (base) {\n const baseStyles = collectRecipeStyles(base, recipes);\n result = mergeStyles(baseStyles as Styles, flatStyles as Styles) as Record<\n string,\n unknown\n >;\n } else {\n result = { ...flatStyles };\n }\n\n // 2. Apply post recipes via mergeStyles (state map extend semantics)\n if (post) {\n const postStyles = collectRecipeStyles(post, recipes);\n result = mergeStyles(result as Styles, postStyles as Styles) as Record<\n string,\n unknown\n >;\n }\n\n // Re-attach selector keys unchanged\n for (const key of Object.keys(selectorStyles)) {\n result[key] = selectorStyles[key];\n }\n\n return result;\n}\n\n/**\n * Resolve all `recipe` style properties in a styles object.\n *\n * Handles both top-level and sub-element recipe references.\n * Returns the same object reference if no recipes are present anywhere\n * (zero overhead for the common case).\n *\n * @param styles - The styles object potentially containing `recipe` keys\n * @returns Resolved styles with recipe values merged in, or the original object if unchanged\n */\nexport function resolveRecipes(styles: Styles): Styles {\n const recipes = getGlobalRecipes();\n\n // Fast path: no recipes configured globally\n if (!recipes) return styles;\n\n let changed = false;\n\n // Resolve top-level recipe\n const topResolved = resolveRecipesForLevel(\n styles as Record<string, unknown>,\n recipes,\n );\n\n let result: Record<string, unknown>;\n\n if (topResolved) {\n changed = true;\n result = topResolved;\n } else {\n // Keep reference; a shallow copy is deferred until a sub-element actually changes\n result = styles as Record<string, unknown>;\n }\n\n // Resolve sub-element recipes\n const keys = Object.keys(result);\n\n for (const key of keys) {\n if (!isSelector(key)) continue;\n\n const subStyles = result[key];\n\n if (\n !subStyles ||\n typeof subStyles !== 'object' ||\n Array.isArray(subStyles)\n ) {\n continue;\n }\n\n const subRecord = subStyles as Record<string, unknown>;\n\n if (!('recipe' in subRecord)) continue;\n\n const subResolved = resolveRecipesForLevel(subRecord, recipes);\n\n if (subResolved) {\n if (!changed) {\n // First change in sub-elements -- need to shallow-copy the top level\n changed = true;\n result = { ...(styles as Record<string, unknown>) };\n }\n result[key] = subResolved;\n }\n }\n\n return changed ? (result as Styles) : styles;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAwBA,MAAM,UAAU,UAAU;;;;;;;;;;;;AAkB1B,SAAS,iBAAiB,OAAoC;CAC5D,MAAM,QAA4B;EAAE,MAAM;EAAM,MAAM;EAAM;CAE5D,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,MAAM,UAAU,MAAM,MAAM;CAC5B,IAAI,YAAY,IAAI,OAAO;CAE3B,MAAM,aAAa,QAAQ,QAAQ,IAAI;CAEvC,IAAI,eAAe,IAAI;EACrB,IAAI,YAAY,QAAQ,OAAO;EAE/B,OAAO;GAAE,MADK,WAAW,QACL;GAAE,MAAM;GAAM;;CAGpC,MAAM,WAAW,QAAQ,MAAM,GAAG,WAAW;CAC7C,MAAM,WAAW,QAAQ,MAAM,aAAa,EAAE;CAE9C,OAAO;EACL,MAAM,SAAS,MAAM,KAAK,SAAS,OAAO,WAAW,SAAS;EAC9D,MAAM,WAAW,SAAS;EAC3B;;AAGH,SAAS,WAAW,GAA4B;CAC9C,MAAM,QAAQ,EAAE,MAAM,MAAM,CAAC,OAAO,QAAQ;CAC5C,OAAO,MAAM,SAAS,IAAI,QAAQ;;;;;;AAOpC,SAAS,oBACP,OACA,SACyB;CACzB,IAAI,SAAkC,EAAE;CAExC,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,eAAe,QAAQ;EAE7B,IAAI,CAAC,cAAc;GACjB,IAAI,SACF,QAAQ,KACN,mBAAmB,KAAK,0EAEzB;GAEH;;EAGF,SAAS;GAAE,GAAG;GAAQ,GAAI;GAA0C;;CAGtE,OAAO;;;;;;;;AAST,SAAS,uBACP,QACA,SACgC;CAChC,IAAI,EAAE,YAAY,SAAS,OAAO;CAElC,MAAM,EAAE,MAAM,SAAS,iBAAiB,OAAO,OAAO;CAMtD,MAAM,EAAE,QAAQ,SAAS,GAAG,YAAY;CACxC,MAAM,aAAsC,EAAE;CAC9C,MAAM,iBAA0C,EAAE;CAElD,KAAK,MAAM,OAAO,OAAO,KAAK,QAAQ,EACpC,IAAI,WAAW,IAAI,EACjB,eAAe,OAAO,QAAQ;MAE9B,WAAW,OAAO,QAAQ;CAI9B,IAAI,CAAC,QAAQ,CAAC,MACZ,OAAO;CAIT,IAAI;CAEJ,IAAI,MAEF,SAAS,YADU,oBAAoB,MAAM,QACd,EAAY,WAAqB;MAKhE,SAAS,EAAE,GAAG,YAAY;CAI5B,IAAI,MAAM;EACR,MAAM,aAAa,oBAAoB,MAAM,QAAQ;EACrD,SAAS,YAAY,QAAkB,WAAqB;;CAO9D,KAAK,MAAM,OAAO,OAAO,KAAK,eAAe,EAC3C,OAAO,OAAO,eAAe;CAG/B,OAAO;;;;;;;;;;;;AAaT,SAAgB,eAAe,QAAwB;CACrD,MAAM,UAAU,kBAAkB;CAGlC,IAAI,CAAC,SAAS,OAAO;CAErB,IAAI,UAAU;CAGd,MAAM,cAAc,uBAClB,QACA,QACD;CAED,IAAI;CAEJ,IAAI,aAAa;EACf,UAAU;EACV,SAAS;QAGT,SAAS;CAIX,MAAM,OAAO,OAAO,KAAK,OAAO;CAEhC,KAAK,MAAM,OAAO,MAAM;EACtB,IAAI,CAAC,WAAW,IAAI,EAAE;EAEtB,MAAM,YAAY,OAAO;EAEzB,IACE,CAAC,aACD,OAAO,cAAc,YACrB,MAAM,QAAQ,UAAU,EAExB;EAGF,MAAM,YAAY;EAElB,IAAI,EAAE,YAAY,YAAY;EAE9B,MAAM,cAAc,uBAAuB,WAAW,QAAQ;EAE9D,IAAI,aAAa;GACf,IAAI,CAAC,SAAS;IAEZ,UAAU;IACV,SAAS,EAAE,GAAI,QAAoC;;GAErD,OAAO,OAAO;;;CAIlB,OAAO,UAAW,SAAoB"}
1
+ {"version":3,"file":"resolve-recipes-CBQaQ3tD.js","names":[],"sources":["../src/utils/resolve-recipes.ts"],"sourcesContent":["/**\n * Recipe resolution utility.\n *\n * Resolves `recipe` style properties by looking up predefined recipe styles\n * from global configuration and merging them with the component's own styles.\n *\n * Resolution order per level (top-level and each sub-element independently):\n * base_recipe_1 base_recipe_2 → component styles → post_recipe_1 post_recipe_2\n *\n * The `/` separator splits base recipes (before component styles)\n * from post recipes (after component styles). All merges use mergeStyles\n * semantics: primitives and state maps with '' key fully replace;\n * state maps without '' key extend the existing value.\n *\n * Returns the same object reference if no recipes are present (zero overhead).\n */\n\nimport { getGlobalRecipes } from '../config';\nimport { isSelector } from '../pipeline';\nimport type { RecipeStyles, Styles } from '../styles/types';\n\nimport { isDevEnv } from './is-dev-env';\nimport { mergeStyles } from './merge-styles';\n\nconst devMode = isDevEnv();\n\ninterface ParsedRecipeGroups {\n base: string[] | null;\n post: string[] | null;\n}\n\n/**\n * Parse a recipe string into base and post recipe name groups.\n *\n * Syntax: `'base1 base2 / post1 post2'`\n * - Names are space-separated within each group\n * - `/` separates base (before component) from post (after component) groups\n * - `/` is optional; if absent, all names are base\n * - `none` as the sole base value means \"no base recipes\"\n *\n * Returns `{ base: null, post: null }` if the string is empty or invalid.\n */\nfunction parseRecipeNames(value: unknown): ParsedRecipeGroups {\n const empty: ParsedRecipeGroups = { base: null, post: null };\n\n if (typeof value !== 'string') return empty;\n const trimmed = value.trim();\n if (trimmed === '') return empty;\n\n const slashIndex = trimmed.indexOf('/');\n\n if (slashIndex === -1) {\n if (trimmed === 'none') return empty;\n const names = splitNames(trimmed);\n return { base: names, post: null };\n }\n\n const basePart = trimmed.slice(0, slashIndex);\n const postPart = trimmed.slice(slashIndex + 1);\n\n return {\n base: basePart.trim() === 'none' ? null : splitNames(basePart),\n post: splitNames(postPart),\n };\n}\n\nfunction splitNames(s: string): string[] | null {\n const names = s.split(/\\s+/).filter(Boolean);\n return names.length > 0 ? names : null;\n}\n\n/**\n * Collect merged styles for a list of recipe names.\n * Each recipe is flat-spread on top of the previous.\n */\nfunction collectRecipeStyles(\n names: string[],\n recipes: Record<string, RecipeStyles>,\n): Record<string, unknown> {\n let merged: Record<string, unknown> = {};\n\n for (const name of names) {\n const recipeStyles = recipes[name];\n\n if (!recipeStyles) {\n if (devMode) {\n console.warn(\n `[Tasty] Recipe \"${name}\" not found. ` +\n `Make sure it is defined in configure({ recipes: { ... } }).`,\n );\n }\n continue;\n }\n\n merged = { ...merged, ...(recipeStyles as Record<string, unknown>) };\n }\n\n return merged;\n}\n\n/**\n * Resolve recipe references in a flat styles object (no sub-elements).\n * Returns null if no `recipe` key is present.\n *\n * Resolution: base recipes → component styles → post recipes (all via mergeStyles)\n */\nfunction resolveRecipesForLevel(\n styles: Record<string, unknown>,\n recipes: Record<string, RecipeStyles>,\n): Record<string, unknown> | null {\n if (!('recipe' in styles)) return null;\n\n const { base, post } = parseRecipeNames(styles.recipe);\n\n // Separate selector keys (sub-elements) from flat style properties.\n // mergeStyles handles selectors with its own semantics (e.g. false = delete),\n // but at this level we only want recipe merging on flat properties.\n\n const { recipe: _recipe, ...allRest } = styles;\n const flatStyles: Record<string, unknown> = {};\n const selectorStyles: Record<string, unknown> = {};\n\n for (const key of Object.keys(allRest)) {\n if (isSelector(key)) {\n selectorStyles[key] = allRest[key];\n } else {\n flatStyles[key] = allRest[key];\n }\n }\n\n if (!base && !post) {\n return allRest;\n }\n\n // 1. Merge base recipes, then component styles on top (via mergeStyles)\n let result: Record<string, unknown>;\n\n if (base) {\n const baseStyles = collectRecipeStyles(base, recipes);\n result = mergeStyles(baseStyles as Styles, flatStyles as Styles) as Record<\n string,\n unknown\n >;\n } else {\n result = { ...flatStyles };\n }\n\n // 2. Apply post recipes via mergeStyles (state map extend semantics)\n if (post) {\n const postStyles = collectRecipeStyles(post, recipes);\n result = mergeStyles(result as Styles, postStyles as Styles) as Record<\n string,\n unknown\n >;\n }\n\n // Re-attach selector keys unchanged\n for (const key of Object.keys(selectorStyles)) {\n result[key] = selectorStyles[key];\n }\n\n return result;\n}\n\n/**\n * Resolve all `recipe` style properties in a styles object.\n *\n * Handles both top-level and sub-element recipe references.\n * Returns the same object reference if no recipes are present anywhere\n * (zero overhead for the common case).\n *\n * @param styles - The styles object potentially containing `recipe` keys\n * @returns Resolved styles with recipe values merged in, or the original object if unchanged\n */\nexport function resolveRecipes(styles: Styles): Styles {\n const recipes = getGlobalRecipes();\n\n // Fast path: no recipes configured globally\n if (!recipes) return styles;\n\n let changed = false;\n\n // Resolve top-level recipe\n const topResolved = resolveRecipesForLevel(\n styles as Record<string, unknown>,\n recipes,\n );\n\n let result: Record<string, unknown>;\n\n if (topResolved) {\n changed = true;\n result = topResolved;\n } else {\n // Keep reference; a shallow copy is deferred until a sub-element actually changes\n result = styles as Record<string, unknown>;\n }\n\n // Resolve sub-element recipes\n const keys = Object.keys(result);\n\n for (const key of keys) {\n if (!isSelector(key)) continue;\n\n const subStyles = result[key];\n\n if (\n !subStyles ||\n typeof subStyles !== 'object' ||\n Array.isArray(subStyles)\n ) {\n continue;\n }\n\n const subRecord = subStyles as Record<string, unknown>;\n\n if (!('recipe' in subRecord)) continue;\n\n const subResolved = resolveRecipesForLevel(subRecord, recipes);\n\n if (subResolved) {\n if (!changed) {\n // First change in sub-elements -- need to shallow-copy the top level\n changed = true;\n result = { ...(styles as Record<string, unknown>) };\n }\n result[key] = subResolved;\n }\n }\n\n return changed ? (result as Styles) : styles;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAwBA,MAAM,UAAU,UAAU;;;;;;;;;;;;AAkB1B,SAAS,iBAAiB,OAAoC;CAC5D,MAAM,QAA4B;EAAE,MAAM;EAAM,MAAM;EAAM;CAE5D,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,MAAM,UAAU,MAAM,MAAM;CAC5B,IAAI,YAAY,IAAI,OAAO;CAE3B,MAAM,aAAa,QAAQ,QAAQ,IAAI;CAEvC,IAAI,eAAe,IAAI;EACrB,IAAI,YAAY,QAAQ,OAAO;EAE/B,OAAO;GAAE,MADK,WAAW,QACL;GAAE,MAAM;GAAM;;CAGpC,MAAM,WAAW,QAAQ,MAAM,GAAG,WAAW;CAC7C,MAAM,WAAW,QAAQ,MAAM,aAAa,EAAE;CAE9C,OAAO;EACL,MAAM,SAAS,MAAM,KAAK,SAAS,OAAO,WAAW,SAAS;EAC9D,MAAM,WAAW,SAAS;EAC3B;;AAGH,SAAS,WAAW,GAA4B;CAC9C,MAAM,QAAQ,EAAE,MAAM,MAAM,CAAC,OAAO,QAAQ;CAC5C,OAAO,MAAM,SAAS,IAAI,QAAQ;;;;;;AAOpC,SAAS,oBACP,OACA,SACyB;CACzB,IAAI,SAAkC,EAAE;CAExC,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,eAAe,QAAQ;EAE7B,IAAI,CAAC,cAAc;GACjB,IAAI,SACF,QAAQ,KACN,mBAAmB,KAAK,0EAEzB;GAEH;;EAGF,SAAS;GAAE,GAAG;GAAQ,GAAI;GAA0C;;CAGtE,OAAO;;;;;;;;AAST,SAAS,uBACP,QACA,SACgC;CAChC,IAAI,EAAE,YAAY,SAAS,OAAO;CAElC,MAAM,EAAE,MAAM,SAAS,iBAAiB,OAAO,OAAO;CAMtD,MAAM,EAAE,QAAQ,SAAS,GAAG,YAAY;CACxC,MAAM,aAAsC,EAAE;CAC9C,MAAM,iBAA0C,EAAE;CAElD,KAAK,MAAM,OAAO,OAAO,KAAK,QAAQ,EACpC,IAAI,WAAW,IAAI,EACjB,eAAe,OAAO,QAAQ;MAE9B,WAAW,OAAO,QAAQ;CAI9B,IAAI,CAAC,QAAQ,CAAC,MACZ,OAAO;CAIT,IAAI;CAEJ,IAAI,MAEF,SAAS,YADU,oBAAoB,MAAM,QACd,EAAY,WAAqB;MAKhE,SAAS,EAAE,GAAG,YAAY;CAI5B,IAAI,MAAM;EACR,MAAM,aAAa,oBAAoB,MAAM,QAAQ;EACrD,SAAS,YAAY,QAAkB,WAAqB;;CAO9D,KAAK,MAAM,OAAO,OAAO,KAAK,eAAe,EAC3C,OAAO,OAAO,eAAe;CAG/B,OAAO;;;;;;;;;;;;AAaT,SAAgB,eAAe,QAAwB;CACrD,MAAM,UAAU,kBAAkB;CAGlC,IAAI,CAAC,SAAS,OAAO;CAErB,IAAI,UAAU;CAGd,MAAM,cAAc,uBAClB,QACA,QACD;CAED,IAAI;CAEJ,IAAI,aAAa;EACf,UAAU;EACV,SAAS;QAGT,SAAS;CAIX,MAAM,OAAO,OAAO,KAAK,OAAO;CAEhC,KAAK,MAAM,OAAO,MAAM;EACtB,IAAI,CAAC,WAAW,IAAI,EAAE;EAEtB,MAAM,YAAY,OAAO;EAEzB,IACE,CAAC,aACD,OAAO,cAAc,YACrB,MAAM,QAAQ,UAAU,EAExB;EAGF,MAAM,YAAY;EAElB,IAAI,EAAE,YAAY,YAAY;EAE9B,MAAM,cAAc,uBAAuB,WAAW,QAAQ;EAE9D,IAAI,aAAa;GACf,IAAI,CAAC,SAAS;IAEZ,UAAU;IACV,SAAS,EAAE,GAAI,QAAoC;;GAErD,OAAO,OAAO;;;CAIlB,OAAO,UAAW,SAAoB"}
@@ -1,4 +1,4 @@
1
- import { n as hydrateTastyClasses } from "../hydrate-DEsdmcdy.js";
1
+ import { n as hydrateTastyClasses } from "../hydrate-DsFfFPVK.js";
2
2
  //#region src/ssr/astro-client.ts
3
3
  /**
4
4
  * Client-side cache hydration for Astro islands.
package/dist/ssr/astro.js CHANGED
@@ -1,6 +1,6 @@
1
- import { n as getConfig } from "../config-IzenlK2R.js";
2
- import { a as registerSSRCollectorGetterGlobal } from "../format-rules-CCb7qNPt.js";
3
- import { t as ServerStyleCollector } from "../collector-c00_hT9R.js";
1
+ import { n as getConfig } from "../config-DF2QZQEW.js";
2
+ import { a as registerSSRCollectorGetterGlobal } from "../format-rules-DCI2lomx.js";
3
+ import { t as ServerStyleCollector } from "../collector-BWvvN7_y.js";
4
4
  import { n as runWithCollector, t as getSSRCollector } from "../async-storage-B7_o6FKt.js";
5
5
  //#region src/ssr/astro.ts
6
6
  /**
package/dist/ssr/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { a as registerSSRCollectorGetterGlobal } from "../format-rules-CCb7qNPt.js";
2
- import { t as ServerStyleCollector } from "../collector-c00_hT9R.js";
1
+ import { a as registerSSRCollectorGetterGlobal } from "../format-rules-DCI2lomx.js";
2
+ import { t as ServerStyleCollector } from "../collector-BWvvN7_y.js";
3
3
  import { n as runWithCollector, t as getSSRCollector } from "../async-storage-B7_o6FKt.js";
4
- import { n as hydrateTastyClasses, t as hydrateTastyCache } from "../hydrate-DEsdmcdy.js";
4
+ import { n as hydrateTastyClasses, t as hydrateTastyCache } from "../hydrate-DsFfFPVK.js";
5
5
  //#region src/ssr/index.ts
6
6
  registerSSRCollectorGetterGlobal(getSSRCollector);
7
7
  //#endregion
package/dist/ssr/next.js CHANGED
@@ -1,9 +1,9 @@
1
1
  "use client";
2
- import { n as getConfig } from "../config-IzenlK2R.js";
3
- import { i as registerSSRCollectorGetter } from "../format-rules-CCb7qNPt.js";
2
+ import { n as getConfig } from "../config-DF2QZQEW.js";
3
+ import { i as registerSSRCollectorGetter } from "../format-rules-DCI2lomx.js";
4
4
  import { t as getTastySSRContext } from "../context-CkSg-kDT.js";
5
- import { t as ServerStyleCollector } from "../collector-c00_hT9R.js";
6
- import { n as hydrateTastyClasses } from "../hydrate-DEsdmcdy.js";
5
+ import { t as ServerStyleCollector } from "../collector-BWvvN7_y.js";
6
+ import { n as hydrateTastyClasses } from "../hydrate-DsFfFPVK.js";
7
7
  import { Fragment, createElement, useState } from "react";
8
8
  import { useServerInsertedHTML } from "next/navigation";
9
9
  //#region src/ssr/next.ts
@@ -1,4 +1,4 @@
1
- import { t as mergeStyles } from "../merge-styles-Ugifi570.js";
1
+ import { t as mergeStyles } from "../merge-styles-Bnn6j_SA.js";
2
2
  //#region src/static/types.ts
3
3
  /**
4
4
  * Create a StaticStyle object.
@@ -1,7 +1,7 @@
1
- import { i as getGlobalConfigTokens, t as configure, u as getGlobalStyles, v as resetConfig } from "../config-IzenlK2R.js";
2
- import { t as mergeStyles } from "../merge-styles-Ugifi570.js";
3
- import { t as resolveRecipes } from "../resolve-recipes-DEmQhiop.js";
4
- import { a as extractPropertiesFromStyles, c as setExtractorNamePrefix, i as extractKeyframesFromStyles, n as extractCounterStyleFromStyles, o as extractStylesForSelector, r as extractFontFaceFromStyles, s as extractStylesWithChunks, t as CSSWriter } from "../css-writer-CCyaR6ZM.js";
1
+ import { i as getGlobalConfigTokens, t as configure, u as getGlobalStyles, v as resetConfig } from "../config-DF2QZQEW.js";
2
+ import { t as mergeStyles } from "../merge-styles-Bnn6j_SA.js";
3
+ import { t as resolveRecipes } from "../resolve-recipes-CBQaQ3tD.js";
4
+ import { a as extractPropertiesFromStyles, c as setExtractorNamePrefix, i as extractKeyframesFromStyles, n as extractCounterStyleFromStyles, o as extractStylesForSelector, r as extractFontFaceFromStyles, s as extractStylesWithChunks, t as CSSWriter } from "../css-writer-Bh05D6KI.js";
5
5
  import * as fs from "fs";
6
6
  import * as path from "path";
7
7
  import { declare } from "@babel/helper-plugin-utils";
@@ -1,2 +1,2 @@
1
- import { o as extractStylesForSelector, s as extractStylesWithChunks, t as CSSWriter } from "../css-writer-CCyaR6ZM.js";
1
+ import { o as extractStylesForSelector, s as extractStylesWithChunks, t as CSSWriter } from "../css-writer-Bh05D6KI.js";
2
2
  export { CSSWriter, extractStylesForSelector, extractStylesWithChunks };
package/docs/dsl.md CHANGED
@@ -41,6 +41,20 @@ fill: {
41
41
  }
42
42
  ```
43
43
 
44
+ #### Default State Ordering
45
+
46
+ Key order sets priority — later keys win and turn off earlier ones via negation. The bare default (`''`) is the lowest-priority state, so it **must be the first key**. If it appears after other states, Tasty moves it to the front and emits a dev warning (`MISPLACED_DEFAULT_STATE`); otherwise it would override every state above it.
47
+
48
+ ```jsx
49
+ // Correct — default first
50
+ color: { '': '#text', hovered: '#accent' }
51
+
52
+ // Auto-corrected with a warning — '' moved to the front
53
+ color: { hovered: '#accent', '': '#text' }
54
+ ```
55
+
56
+ The bare `''` default still **receives negation** (it is turned off when a higher-priority state matches). For a value that must always apply as a guaranteed floor — even where a query is *unknown* — use the [`_` fallback floor](#_--fallback-floor) instead. The two can coexist: `''` is the negated default, `_` is the always-on floor. If a map contains only `_` and `''` (no other states), the `''` default is redundant — Tasty keeps the `_` value and drops `''` with a `REDUNDANT_DEFAULT_STATE` warning.
57
+
44
58
  ### Sub-element
45
59
 
46
60
  Element styled using a capitalized key. Identified by `data-element` attribute:
@@ -288,11 +302,21 @@ const SimpleButton = tasty(Button, {
288
302
  | `@parent` | Parent/ancestor element states | `@parent(hovered)` |
289
303
  | `@own` | Sub-element's own state | `@own(hovered)` |
290
304
  | `@starting` | Entry animation | `@starting` |
305
+ | `_` | Fallback floor (always-on, never negated) | `_` |
291
306
  | `:is()` | CSS `:is()` structural pseudo-class | `:is(fieldset > label)` |
292
307
  | `:has()` | CSS `:has()` relational pseudo-class | `:has(> Icon)` |
293
308
  | `:not()` | CSS `:not()` negation (prefer `!:is()`) | `:not(:first-child)` |
294
309
  | `:where()` | CSS `:where()` (zero specificity) | `:where(Section)` |
295
310
 
311
+ > **Specificity.** All state selectors Tasty generates (modifiers, pseudo-classes,
312
+ > `:is()`/`:not()` groups, and `@root` / `@parent` context) are wrapped in
313
+ > `:where(...)` so they carry **zero specificity**. The only specificity anchors
314
+ > are the doubled component class (`.t0.t0`) and sub-element `[data-element]`
315
+ > attributes. This means overlapping rules (e.g. a `_` fallback floor and the
316
+ > states layered over it) resolve purely by **source order** — Tasty emits
317
+ > lower-priority states first and higher-priority states last so the cascade
318
+ > produces the intended winner.
319
+
296
320
  ### `@media(...)` — Media Queries
297
321
 
298
322
  Media queries support dimension shorthands and custom unit expansion:
@@ -374,6 +398,91 @@ display: {
374
398
  }
375
399
  ```
376
400
 
401
+ ### `_` — Fallback Floor
402
+
403
+ By default Tasty makes states **mutually exclusive**: a higher-priority state
404
+ turns the lower-priority ones off via negation, so exactly one branch applies.
405
+ This relies on `A | !A` always being true. CSS feature/container queries break
406
+ that assumption: `@supports(...)` and `@(...)` queries can be **unknown** (not
407
+ just true/false), and `not(unknown)` is also unknown — so a negated default
408
+ branch silently never applies. The classic case is `scroll-state`: a browser can
409
+ support `container-type: scroll-state` while a specific `scroll-state(...)` query
410
+ is unknown, leaving *no* branch active.
411
+
412
+ The `_` fallback floor solves this. Use `_` as a **standalone key** and its value
413
+ **always applies** as a guaranteed floor: it never receives negation, and
414
+ higher-priority states simply layer over it via the cascade.
415
+
416
+ ```jsx
417
+ inset: {
418
+ _: '0 top',
419
+ '@supports(container-type: scroll-state) & @(scroll-state(scrolled: block-end))':
420
+ '-80x top',
421
+ }
422
+ ```
423
+
424
+ ```css
425
+ .t0.t0 { inset: 0 ...; }
426
+ @container scroll-state(scrolled: block-end) {
427
+ @supports (container-type: scroll-state) {
428
+ .t0.t0 { inset: -80px ...; }
429
+ }
430
+ }
431
+ ```
432
+
433
+ The floor is emitted as a bare rule (no negated `@supports` / container branches),
434
+ so it always applies. When the override matches it wins because it is emitted
435
+ later and all state selectors share the same specificity (see the note on
436
+ `:where()` below).
437
+
438
+ Notes:
439
+
440
+ - `_` is a **standalone key only** — it defines a single map-wide floor and
441
+ cannot be combined with state logic. Keys like `_ & hovered` or `_ | focused`
442
+ are ignored with an `INVALID_FALLBACK_KEY` dev warning.
443
+ - `_` can coexist with the bare `''` default when other states exist: `''` is the
444
+ negated default, `_` is the always-on floor. With only `_` and `''` (no other
445
+ states) the `''` default is redundant — Tasty keeps the `_` value and drops
446
+ `''` with a `REDUNDANT_DEFAULT_STATE` warning.
447
+ - `_` is position-independent: it is always placed at the lowest priority
448
+ regardless of where it appears in the map.
449
+
450
+ #### `_` vs the `''` default
451
+
452
+ In simple maps where every other state is a plain modifier (always cleanly
453
+ on/off), `_` and `''` produce the **same visible result** — the base value shows
454
+ whenever no other state wins. They diverge in three situations, and the root
455
+ cause is always the same: **`''` is mutually exclusive (turned off by negation),
456
+ while `_` is always on (layered underneath).**
457
+
458
+ - **Unknown / invalid branches.** If a higher-priority branch sits behind a query
459
+ that can be *unknown* (`@supports`, container, `scroll-state`) or uses a
460
+ selector the browser drops, the negated `''` default disappears along with it
461
+ (`not(unknown) = unknown`), leaving the property with no rule. The `_` floor
462
+ survives because it is an unconditional bare rule. This is the case `_` exists
463
+ for.
464
+
465
+ - **Empty / "unset" states.** A state can intentionally produce *no* output (an
466
+ empty value) to leave a property unset while that state is active. With `''`
467
+ this works — the default is negated away and nothing replaces it, so the
468
+ property unsets. With `_` the floor can never be turned off, so its value keeps
469
+ applying and the "unset on this state" intent is lost.
470
+
471
+ ```jsx
472
+ // '' : color unsets while loading. '_' : color stays #text while loading.
473
+ color: { '': '#text', loading: '' }
474
+ ```
475
+
476
+ - **States with different output shapes.** Because `_` layers additively, when
477
+ different states emit *different sets* of declarations, the floor's
478
+ declarations bleed through wherever the winning state does not override the
479
+ same property — which can produce inconsistent combinations. A `''` default is
480
+ swapped out cleanly by its mutually-exclusive condition.
481
+
482
+ Rule of thumb: **use `''` for the normal default** (clean mutual exclusivity,
483
+ supports unsetting), and reach for `_` only when a value must survive an
484
+ *unknown* higher-priority branch.
485
+
377
486
  ### `@root(...)` — Root Element States
378
487
 
379
488
  Root states generate selectors on the `:root` element. They are useful for theme modes, feature flags, and other page-level conditions:
package/docs/pipeline.md CHANGED
@@ -76,7 +76,7 @@ Output: CSSRule[]
76
76
 
77
77
  **Simplification** (`simplifyCondition` in `simplify.ts`) is not a separate numbered stage. It runs inside OR expansion, exclusive building, `expandExclusiveOrs` branch cleanup, combination ANDs, merge-by-value ORs, and materialization. Every call is cached by condition unique-id, so the repetition is cheap.
78
78
 
79
- **Post-pass:** After `processStyles` collects rules from every handler, `runPipeline` (`index.ts:188`) filters duplicates using a key of `selector|declarations|atRules|rootPrefix|startingStyle` and then reorders rules so every `@starting-style` rule is emitted **after** all normal rules. This ordering is cascade-critical: `@starting-style` rules share specificity with their normal counterparts, and source order decides which value wins.
79
+ **Post-pass:** After `processStyles` collects rules from every handler, `runPipeline` (`index.ts`) filters duplicates using a key of `selector|declarations|atRules|rootPrefix|startingStyle`, then **stable-sorts rules by their cascade `order` hint (ascending source priority)** so lower-priority rules come first and higher-priority rules win, and finally emits every `@starting-style` rule **after** all normal rules. The `order` hint is the highest source priority among the entries that produced each rule (threaded `ComputedRule → CSSRule`). This ordering is cascade-critical because Stage 7 equalizes specificity with `:where()`: once every state selector carries zero specificity, source order alone decides which of two overlapping rules wins — most importantly the `_` fallback floor (low order, emitted first) versus the states layered over it, and `@starting-style` versus its normal counterpart. Mutually-exclusive rules are unaffected by ordering (they never both match), so reordering them is safe.
80
80
 
81
81
  ---
82
82
 
@@ -120,6 +120,8 @@ Removing don't-care dimensions before parsing prevents combinatorial blowup in l
120
120
 
121
121
  Converts each state key in a style value map (like `'hovered & !disabled'`, `'@media(w < 768px)'`) into `ConditionNode` trees. `parseStyleEntries` walks the object keys in source order and assigns priorities; `parseStateKey` parses a single key string.
122
122
 
123
+ `parseStyleEntries` also handles the bare `''` default and the standalone `_` fallback floor before the priority order is finalized. The bare `''` default only behaves correctly as the lowest-priority state, so a misplaced one is moved to the front (`MISPLACED_DEFAULT_STATE` warning). The `_` key is pulled out as a separate floor entry (`floor: true`, `TrueCondition`) and always assigned the lowest priority. If a map defines only `_` and a bare `''` (no other states), the `''` default is redundant — it is dropped in favor of the `_` value (`REDUNDANT_DEFAULT_STATE` warning). A `_` combined with state logic (`_ & hovered`, `_ | x`) is ignored (`INVALID_FALLBACK_KEY` warning).
124
+
123
125
  ### How It Works
124
126
 
125
127
  1. **Tokenization**: The state key is split into tokens using a regex pattern that recognizes:
@@ -293,6 +295,32 @@ C: C & !A & !B (applies only when neither A nor B)
293
295
 
294
296
  Each exclusive condition is passed through `simplifyCondition`. Entries that simplify to `FALSE` (impossible) are filtered out. The default state (`''` → `TrueCondition`) is not added to the “prior” list for negation (see `buildExclusiveConditions`).
295
297
 
298
+ ### `_` fallback floor
299
+
300
+ An entry flagged `floor: true` (from the standalone `_` key) is the one
301
+ exception to the negation cascade above. `buildExclusiveConditions` **pulls the
302
+ floor entries out** before the negation pass, builds the remaining entries with a
303
+ plain unconditional negation loop, then re-appends each floor with its own
304
+ (`TRUE`) condition as the exclusive condition. So the floor **always applies**,
305
+ never receives `!prior`, and never negates lower-priority entries. Because its
306
+ condition is `TRUE`, Stage 1 `mergeEntriesByValue` already leaves it untouched
307
+ (same guard as the bare `''` default), so no floor-specific merge handling is
308
+ needed.
309
+
310
+ This exists to fix the *unknown-query hole*: CSS three-valued logic makes
311
+ `not(unknown) = unknown`, so a negated `@supports(...)` / container-query default
312
+ branch silently never applies. A `_` floor sidesteps negation entirely and lets
313
+ the positive override layer on top via the cascade (see Stage 7's `:where()`
314
+ equalization and the ascending-priority post-pass).
315
+
316
+ Because the floor is **additive** (always emitted, never negated) rather than
317
+ mutually exclusive, it differs observably from the `''` default in two ways
318
+ beyond the unknown-query fix: an "empty" higher-priority state (one that emits no
319
+ declarations) cannot unset the property — the floor still applies — and when
320
+ states emit different declaration sets, the floor's declarations bleed through
321
+ wherever a winning state does not override the same property. The `''` default,
322
+ being negated, swaps out cleanly. See the DSL guide's "`_` vs the `''` default".
323
+
296
324
  ### Why
297
325
 
298
326
  This eliminates CSS specificity wars. Instead of relying on cascade order, each CSS rule matches in exactly one scenario. Benefits:
@@ -477,7 +505,15 @@ Converts condition trees into actual CSS selectors and at-rules.
477
505
 
478
506
  3. **Contradiction detection**: During variant merging, impossible combinations are dropped (e.g. conflicting media, root, or modifier negations).
479
507
 
480
- 4. **`materializeComputedRule`**: Groups variants by sorted at-rules plus root-prefix key; within each group, `mergeVariantsIntoSelectorGroups` merges variants that differ only in flat modifier/pseudo parts; builds selector strings and emits one or more `CSSRule` objects.
508
+ 4. **`materializeComputedRule`**: Groups variants by sorted at-rules plus root-prefix key; within each group, `mergeVariantsIntoSelectorGroups` merges variants that differ only in flat modifier/pseudo parts; builds selector strings and emits one or more `CSSRule` objects. The rule's `order` hint (cascade priority) is propagated here for the post-pass sort.
509
+
510
+ 5. **`:where()` specificity equalization**: Every stateful part of the selector is wrapped in `:where(...)` so it contributes **zero specificity**:
511
+ - the flat root modifiers + pseudos and the root element's `:is()`/`:not()` groups are combined into one `:where(...)` (`buildSelectorFromVariant`);
512
+ - the sub-element (`@own`) groups are wrapped in a **separate** `:where(...)` so the sub-element's structural `[data-element="X"]` specificity is preserved while its states are zeroed;
513
+ - `@parent` ancestors (`parentGroupsToCSS`) become `:where(... *)` / `:where(:not(... *))`;
514
+ - the `@root` context prefix (`rootGroupsToCSS`) becomes `:where(:root...)` (zeroing the `:root` pseudo too).
515
+
516
+ The only specificity anchors that remain are the doubled component class (`.tXX.tXX`, added by the injector) and sub-element `[data-element]` attributes. Because Stage 2b already makes ordinary rules mutually exclusive, zeroing specificity never changes *which* rule matches — it only makes additive layering (`_` fallback floor, `@starting-style`) resolve deterministically by source order (see the post-pass). Canonical sorting/dedup/subsumption still run first; `:where()` is only the final wrapper, and both output paths (injector and direct-selector / SSR) consume the same wrapped fragments.
481
517
 
482
518
  ### Why
483
519
 
@@ -498,6 +534,8 @@ interface CSSRule {
498
534
  declarations: string; // CSS declarations (e.g. 'color: red;')
499
535
  atRules?: string[]; // Wrapping at-rules
500
536
  rootPrefix?: string; // Root state prefix
537
+ startingStyle?: boolean; // Wrap declarations in @starting-style
538
+ order?: number; // Internal cascade order hint (ascending source priority); stripped before injection
501
539
  }
502
540
  ```
503
541
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenphi/tasty",
3
- "version": "2.6.5",
3
+ "version": "2.7.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",
@@ -165,13 +165,13 @@
165
165
  "name": "main (import *)",
166
166
  "path": "dist/index.js",
167
167
  "import": "*",
168
- "limit": "52 kB"
168
+ "limit": "53 kB"
169
169
  },
170
170
  {
171
171
  "name": "core (import *)",
172
172
  "path": "dist/core/index.js",
173
173
  "import": "*",
174
- "limit": "49.5 kB"
174
+ "limit": "50.5 kB"
175
175
  },
176
176
  {
177
177
  "name": "static",
@@ -188,7 +188,7 @@
188
188
  "path",
189
189
  "crypto"
190
190
  ],
191
- "limit": "31 kB"
191
+ "limit": "32 kB"
192
192
  },
193
193
  {
194
194
  "name": "babel-plugin",
@@ -199,7 +199,7 @@
199
199
  "path",
200
200
  "crypto"
201
201
  ],
202
- "limit": "45.65 kB"
202
+ "limit": "46.65 kB"
203
203
  }
204
204
  ],
205
205
  "scripts": {