@tenphi/tasty 0.0.0-snapshot.b3fbcbb → 0.0.0-snapshot.bae20d4

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 (163) hide show
  1. package/README.md +130 -7
  2. package/dist/chunks/cacheKey.js +16 -8
  3. package/dist/chunks/cacheKey.js.map +1 -1
  4. package/dist/chunks/definitions.js +1 -2
  5. package/dist/chunks/definitions.js.map +1 -1
  6. package/dist/chunks/renderChunk.js +31 -32
  7. package/dist/chunks/renderChunk.js.map +1 -1
  8. package/dist/config.d.ts +45 -10
  9. package/dist/config.js +75 -13
  10. package/dist/config.js.map +1 -1
  11. package/dist/core/index.d.ts +9 -8
  12. package/dist/core/index.js +7 -6
  13. package/dist/hooks/useGlobalStyles.d.ts +3 -0
  14. package/dist/hooks/useGlobalStyles.js +28 -1
  15. package/dist/hooks/useGlobalStyles.js.map +1 -1
  16. package/dist/hooks/useKeyframes.js +18 -3
  17. package/dist/hooks/useKeyframes.js.map +1 -1
  18. package/dist/hooks/useProperty.js +36 -13
  19. package/dist/hooks/useProperty.js.map +1 -1
  20. package/dist/hooks/useRawCSS.js +13 -1
  21. package/dist/hooks/useRawCSS.js.map +1 -1
  22. package/dist/hooks/useStyles.d.ts +5 -0
  23. package/dist/hooks/useStyles.js +87 -7
  24. package/dist/hooks/useStyles.js.map +1 -1
  25. package/dist/index.d.ts +9 -8
  26. package/dist/index.js +7 -6
  27. package/dist/injector/index.d.ts +9 -1
  28. package/dist/injector/index.js +9 -1
  29. package/dist/injector/index.js.map +1 -1
  30. package/dist/injector/injector.d.ts +9 -0
  31. package/dist/injector/injector.js +21 -17
  32. package/dist/injector/injector.js.map +1 -1
  33. package/dist/injector/sheet-manager.d.ts +1 -0
  34. package/dist/injector/sheet-manager.js +1 -0
  35. package/dist/injector/sheet-manager.js.map +1 -1
  36. package/dist/parser/classify.js +2 -1
  37. package/dist/parser/classify.js.map +1 -1
  38. package/dist/parser/parser.js +1 -1
  39. package/dist/pipeline/index.d.ts +1 -1
  40. package/dist/pipeline/index.js +24 -14
  41. package/dist/pipeline/index.js.map +1 -1
  42. package/dist/pipeline/materialize.js +380 -119
  43. package/dist/pipeline/materialize.js.map +1 -1
  44. package/dist/pipeline/parseStateKey.d.ts +1 -1
  45. package/dist/pipeline/parseStateKey.js +20 -11
  46. package/dist/pipeline/parseStateKey.js.map +1 -1
  47. package/dist/plugins/okhsl-plugin.js +2 -275
  48. package/dist/plugins/okhsl-plugin.js.map +1 -1
  49. package/dist/plugins/types.d.ts +9 -2
  50. package/dist/properties/index.js +14 -28
  51. package/dist/properties/index.js.map +1 -1
  52. package/dist/properties/property-type-resolver.js +12 -0
  53. package/dist/properties/property-type-resolver.js.map +1 -1
  54. package/dist/ssr/astro.d.ts +29 -0
  55. package/dist/ssr/astro.js +65 -0
  56. package/dist/ssr/astro.js.map +1 -0
  57. package/dist/ssr/async-storage.d.ts +17 -0
  58. package/dist/ssr/async-storage.js +35 -0
  59. package/dist/ssr/async-storage.js.map +1 -0
  60. package/dist/ssr/collect-auto-properties.js +40 -0
  61. package/dist/ssr/collect-auto-properties.js.map +1 -0
  62. package/dist/ssr/collector.d.ts +85 -0
  63. package/dist/ssr/collector.js +183 -0
  64. package/dist/ssr/collector.js.map +1 -0
  65. package/dist/ssr/context.d.ts +8 -0
  66. package/dist/ssr/context.js +14 -0
  67. package/dist/ssr/context.js.map +1 -0
  68. package/dist/ssr/format-global-rules.js +22 -0
  69. package/dist/ssr/format-global-rules.js.map +1 -0
  70. package/dist/ssr/format-keyframes.js +70 -0
  71. package/dist/ssr/format-keyframes.js.map +1 -0
  72. package/dist/ssr/format-property.js +50 -0
  73. package/dist/ssr/format-property.js.map +1 -0
  74. package/dist/ssr/format-rules.js +70 -0
  75. package/dist/ssr/format-rules.js.map +1 -0
  76. package/dist/ssr/hydrate.d.ts +22 -0
  77. package/dist/ssr/hydrate.js +50 -0
  78. package/dist/ssr/hydrate.js.map +1 -0
  79. package/dist/ssr/index.d.ts +5 -0
  80. package/dist/ssr/index.js +12 -0
  81. package/dist/ssr/index.js.map +1 -0
  82. package/dist/ssr/next.d.ts +45 -0
  83. package/dist/ssr/next.js +71 -0
  84. package/dist/ssr/next.js.map +1 -0
  85. package/dist/ssr/ssr-collector-ref.js +12 -0
  86. package/dist/ssr/ssr-collector-ref.js.map +1 -0
  87. package/dist/states/index.js +10 -257
  88. package/dist/states/index.js.map +1 -1
  89. package/dist/styles/color.js +9 -5
  90. package/dist/styles/color.js.map +1 -1
  91. package/dist/styles/createStyle.js +24 -21
  92. package/dist/styles/createStyle.js.map +1 -1
  93. package/dist/styles/index.js +1 -1
  94. package/dist/styles/predefined.d.ts +0 -2
  95. package/dist/styles/predefined.js +0 -3
  96. package/dist/styles/predefined.js.map +1 -1
  97. package/dist/styles/preset.js +1 -1
  98. package/dist/styles/preset.js.map +1 -1
  99. package/dist/styles/scrollbar.d.ts +9 -5
  100. package/dist/styles/scrollbar.js +25 -89
  101. package/dist/styles/scrollbar.js.map +1 -1
  102. package/dist/styles/transition.js +1 -1
  103. package/dist/styles/transition.js.map +1 -1
  104. package/dist/styles/types.d.ts +24 -14
  105. package/dist/tasty.d.ts +20 -21
  106. package/dist/tasty.js +24 -11
  107. package/dist/tasty.js.map +1 -1
  108. package/dist/types.d.ts +1 -1
  109. package/dist/utils/cache-wrapper.js +4 -8
  110. package/dist/utils/cache-wrapper.js.map +1 -1
  111. package/dist/utils/color-math.d.ts +45 -0
  112. package/dist/utils/color-math.js +749 -0
  113. package/dist/utils/color-math.js.map +1 -0
  114. package/dist/utils/color-space.d.ts +5 -0
  115. package/dist/utils/color-space.js +228 -0
  116. package/dist/utils/color-space.js.map +1 -0
  117. package/dist/utils/colors.js +3 -1
  118. package/dist/utils/colors.js.map +1 -1
  119. package/dist/utils/has-keys.js +13 -0
  120. package/dist/utils/has-keys.js.map +1 -0
  121. package/dist/utils/mod-attrs.js +1 -1
  122. package/dist/utils/process-tokens.d.ts +3 -13
  123. package/dist/utils/process-tokens.js +17 -97
  124. package/dist/utils/process-tokens.js.map +1 -1
  125. package/dist/utils/selector-transform.js +32 -0
  126. package/dist/utils/selector-transform.js.map +1 -0
  127. package/dist/utils/styles.d.ts +2 -79
  128. package/dist/utils/styles.js +26 -554
  129. package/dist/utils/styles.js.map +1 -1
  130. package/dist/utils/typography.d.ts +24 -13
  131. package/dist/utils/typography.js +6 -16
  132. package/dist/utils/typography.js.map +1 -1
  133. package/dist/zero/babel.d.ts +48 -6
  134. package/dist/zero/babel.js +63 -5
  135. package/dist/zero/babel.js.map +1 -1
  136. package/dist/zero/next.d.ts +44 -30
  137. package/dist/zero/next.js +102 -38
  138. package/dist/zero/next.js.map +1 -1
  139. package/docs/PIPELINE.md +519 -0
  140. package/docs/adoption.md +286 -0
  141. package/docs/comparison.md +413 -0
  142. package/docs/configuration.md +263 -0
  143. package/docs/debug.md +505 -0
  144. package/docs/design-system.md +401 -0
  145. package/docs/dsl.md +540 -0
  146. package/docs/getting-started.md +201 -0
  147. package/docs/injector.md +528 -0
  148. package/docs/methodology.md +501 -0
  149. package/docs/runtime.md +291 -0
  150. package/docs/ssr.md +382 -0
  151. package/docs/styles.md +574 -0
  152. package/docs/tasty-static.md +420 -0
  153. package/package.json +50 -30
  154. package/dist/styles/styledScrollbar.d.ts +0 -47
  155. package/dist/styles/styledScrollbar.js +0 -38
  156. package/dist/styles/styledScrollbar.js.map +0 -1
  157. package/dist/tokens/typography.d.ts +0 -19
  158. package/dist/tokens/typography.js +0 -237
  159. package/dist/tokens/typography.js.map +0 -1
  160. package/dist/utils/hsl-to-rgb.js +0 -38
  161. package/dist/utils/hsl-to-rgb.js.map +0 -1
  162. package/dist/utils/okhsl-to-rgb.js +0 -296
  163. package/dist/utils/okhsl-to-rgb.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"parseStateKey.js","names":[],"sources":["../../src/pipeline/parseStateKey.ts"],"sourcesContent":["/**\n * State Key Parser\n *\n * Parses state notation strings (like 'hovered & !disabled', '@media(w < 768px)')\n * into ConditionNode trees for processing in the pipeline.\n */\n\nimport { Lru } from '../parser/lru';\nimport type { StateParserContext } from '../states';\nimport {\n expandDimensionShorthands,\n expandTastyUnits,\n findTopLevelComma,\n resolvePredefinedState,\n} from '../states';\nimport { camelToKebab } from '../utils/case-converter';\n\nimport type { ConditionNode, NumericBound } from './conditions';\nimport { emitWarning } from './warnings';\nimport {\n and,\n createContainerDimensionCondition,\n createContainerRawCondition,\n createContainerStyleCondition,\n createMediaDimensionCondition,\n createMediaFeatureCondition,\n createMediaTypeCondition,\n createModifierCondition,\n createOwnCondition,\n createParentCondition,\n createPseudoCondition,\n createRootCondition,\n createStartingCondition,\n createSupportsCondition,\n not,\n or,\n trueCondition,\n} from './conditions';\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/**\n * Maximum XOR operands before emitting a performance warning.\n * A ^ B ^ C ^ D = 8 OR branches (2^(n-1)), so chains above 4\n * risk exponential blowup in downstream processing.\n */\nconst MAX_XOR_CHAIN_LENGTH = 4;\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface ParseStateKeyOptions {\n context?: StateParserContext;\n isSubElement?: boolean;\n}\n\n// ============================================================================\n// Caching\n// ============================================================================\n\n// Cache for parsed state keys (key -> ConditionNode)\nconst parseCache = new Lru<string, ConditionNode>(5000);\n\n// ============================================================================\n// Tokenizer Patterns\n// ============================================================================\n\n/**\n * Pattern for tokenizing state notation.\n * Matches: operators, parentheses, @-prefixed states, value mods, boolean mods,\n * pseudo-classes, class selectors, and attribute selectors.\n *\n * Note: For @supports and @(...) container queries we need to handle nested parentheses\n * like @supports($, :has(*)) or @(scroll-state(stuck: top)).\n * We use a pattern that allows one level of nesting: [^()]*(?:\\([^)]*\\))?[^)]*\n */\nconst STATE_TOKEN_PATTERN =\n /([&|!^])|([()])|(@media:[a-z]+)|(@media\\([^)]+\\))|(@supports\\([^()]*(?:\\([^)]*\\))?[^)]*\\))|(@root\\([^)]+\\))|(@parent\\([^)]+\\))|(@own\\([^)]+\\))|(@\\([^()]*(?:\\([^)]*\\))?[^)]*\\))|(@starting)|(@[A-Za-z][A-Za-z0-9-]*)|([a-z][a-z0-9-]*(?:\\^=|\\$=|\\*=|=)(?:\"[^\"]*\"|'[^']*'|[^\\s&|!^()]+))|([a-z][a-z0-9-]+)|(:[-a-z][a-z0-9-]*(?:\\([^)]+\\))?)|(\\.[a-z][a-z0-9-]+)|(\\[[^\\]]+\\])/gi;\n\n// ============================================================================\n// Token Types\n// ============================================================================\n\ntype TokenType = 'AND' | 'OR' | 'NOT' | 'XOR' | 'LPAREN' | 'RPAREN' | 'STATE';\n\ninterface Token {\n type: TokenType;\n value: string;\n raw: string;\n}\n\n// ============================================================================\n// Tokenizer\n// ============================================================================\n\n/**\n * Tokenize a state notation string\n */\nfunction tokenize(stateKey: string): Token[] {\n const tokens: Token[] = [];\n let match: RegExpExecArray | null;\n\n // Replace commas with | outside of parentheses (for compatibility)\n const normalized = replaceCommasOutsideParens(stateKey);\n\n STATE_TOKEN_PATTERN.lastIndex = 0;\n while ((match = STATE_TOKEN_PATTERN.exec(normalized)) !== null) {\n const fullMatch = match[0];\n\n if (match[1]) {\n // Operator: &, |, !, ^\n switch (fullMatch) {\n case '&':\n tokens.push({ type: 'AND', value: '&', raw: fullMatch });\n break;\n case '|':\n tokens.push({ type: 'OR', value: '|', raw: fullMatch });\n break;\n case '!':\n tokens.push({ type: 'NOT', value: '!', raw: fullMatch });\n break;\n case '^':\n tokens.push({ type: 'XOR', value: '^', raw: fullMatch });\n break;\n }\n } else if (match[2]) {\n // Parenthesis\n if (fullMatch === '(') {\n tokens.push({ type: 'LPAREN', value: '(', raw: fullMatch });\n } else {\n tokens.push({ type: 'RPAREN', value: ')', raw: fullMatch });\n }\n } else {\n // State token (all other capture groups)\n tokens.push({ type: 'STATE', value: fullMatch, raw: fullMatch });\n }\n }\n\n return tokens;\n}\n\n/**\n * Replace commas with | only outside of parentheses\n */\nfunction replaceCommasOutsideParens(str: string): string {\n let result = '';\n let depth = 0;\n\n for (const char of str) {\n if (char === '(') {\n depth++;\n result += char;\n } else if (char === ')') {\n depth--;\n result += char;\n } else if (char === ',' && depth === 0) {\n result += '|';\n } else {\n result += char;\n }\n }\n\n return result;\n}\n\n// ============================================================================\n// Recursive Descent Parser\n// ============================================================================\n\n/**\n * Parser state\n */\nclass Parser {\n private tokens: Token[];\n private pos = 0;\n private options: ParseStateKeyOptions;\n\n constructor(tokens: Token[], options: ParseStateKeyOptions) {\n this.tokens = tokens;\n this.options = options;\n }\n\n parse(): ConditionNode {\n if (this.tokens.length === 0) {\n return trueCondition();\n }\n const result = this.parseExpression();\n return result;\n }\n\n private current(): Token | undefined {\n return this.tokens[this.pos];\n }\n\n private advance(): Token | undefined {\n return this.tokens[this.pos++];\n }\n\n private match(type: TokenType): boolean {\n if (this.current()?.type === type) {\n this.advance();\n return true;\n }\n return false;\n }\n\n /**\n * Parse expression with operator precedence:\n * ! (NOT) > ^ (XOR) > | (OR) > & (AND)\n */\n private parseExpression(): ConditionNode {\n return this.parseAnd();\n }\n\n private parseAnd(): ConditionNode {\n let left = this.parseOr();\n\n while (this.current()?.type === 'AND') {\n this.advance();\n const right = this.parseOr();\n left = and(left, right);\n }\n\n return left;\n }\n\n private parseOr(): ConditionNode {\n let left = this.parseXor();\n\n while (this.current()?.type === 'OR') {\n this.advance();\n const right = this.parseXor();\n left = or(left, right);\n }\n\n return left;\n }\n\n private parseXor(): ConditionNode {\n let left = this.parseUnary();\n let operandCount = 1;\n\n while (this.current()?.type === 'XOR') {\n this.advance();\n const right = this.parseUnary();\n operandCount++;\n\n if (operandCount > MAX_XOR_CHAIN_LENGTH) {\n emitWarning(\n 'XOR_CHAIN_TOO_LONG',\n `XOR chain with ${operandCount} operands produces ${Math.pow(2, operandCount - 1)} OR branches. ` +\n `Consider breaking into smaller expressions to avoid exponential growth.`,\n );\n }\n\n // XOR: (A & !B) | (!A & B)\n left = or(and(left, not(right)), and(not(left), right));\n }\n\n return left;\n }\n\n private parseUnary(): ConditionNode {\n if (this.match('NOT')) {\n const operand = this.parseUnary();\n return not(operand);\n }\n return this.parsePrimary();\n }\n\n private parsePrimary(): ConditionNode {\n // Handle parentheses\n if (this.match('LPAREN')) {\n const expr = this.parseExpression();\n this.match('RPAREN'); // Consume closing paren (lenient if missing)\n return expr;\n }\n\n // Handle state tokens\n const token = this.current();\n if (token?.type === 'STATE') {\n this.advance();\n return this.parseStateToken(token.value);\n }\n\n // Fallback for empty/invalid - return TRUE\n return trueCondition();\n }\n\n /**\n * Parse a state token into a ConditionNode\n */\n private parseStateToken(value: string): ConditionNode {\n // @starting\n if (value === '@starting') {\n return createStartingCondition(false, value);\n }\n\n // @media:type (e.g., @media:print)\n if (value.startsWith('@media:')) {\n const mediaType = value.slice(7) as 'print' | 'screen' | 'all' | 'speech';\n return createMediaTypeCondition(mediaType, false, value);\n }\n\n // @media(...) - media query\n if (value.startsWith('@media(')) {\n return this.parseMediaQuery(value);\n }\n\n // @supports(...) - feature/selector support query\n if (value.startsWith('@supports(')) {\n return this.parseSupportsQuery(value);\n }\n\n // @root(...) - root state\n if (value.startsWith('@root(')) {\n return this.parseRootState(value);\n }\n\n // @parent(...) - parent element state\n if (value.startsWith('@parent(')) {\n return this.parseParentState(value);\n }\n\n // @own(...) - own state (sub-element)\n if (value.startsWith('@own(')) {\n return this.parseOwnState(value);\n }\n\n // @(...) - container query\n if (value.startsWith('@(')) {\n return this.parseContainerQuery(value);\n }\n\n // @name - predefined state\n if (value.startsWith('@') && /^@[A-Za-z][A-Za-z0-9-]*$/.test(value)) {\n return this.parsePredefinedState(value);\n }\n\n // Pseudo-class (e.g., :hover, :focus-visible, :nth-child(2n))\n if (value.startsWith(':')) {\n return createPseudoCondition(value, false, value);\n }\n\n // Class selector (e.g., .active)\n if (value.startsWith('.')) {\n return createPseudoCondition(value, false, value);\n }\n\n // Attribute selector (e.g., [disabled], [data-state=\"active\"])\n if (value.startsWith('[')) {\n return createPseudoCondition(value, false, value);\n }\n\n // Value modifier (e.g., theme=danger, size=large)\n if (value.includes('=')) {\n return this.parseValueModifier(value);\n }\n\n // Boolean modifier (e.g., hovered, disabled)\n return this.parseBooleanModifier(value);\n }\n\n /**\n * Parse @media(...) query\n */\n private parseMediaQuery(raw: string): ConditionNode {\n const content = raw.slice(7, -1); // Remove '@media(' and ')'\n if (!content.trim()) {\n return trueCondition();\n }\n\n // Expand shorthands and units\n let condition = expandDimensionShorthands(content);\n condition = expandTastyUnits(condition);\n\n // Check for feature queries (contains ':' but not dimension comparison)\n if (\n condition.includes(':') &&\n !condition.includes('<') &&\n !condition.includes('>') &&\n !condition.includes('=')\n ) {\n // Feature query: @media(prefers-contrast: high)\n const colonIdx = condition.indexOf(':');\n const feature = condition.slice(0, colonIdx).trim();\n const featureValue = condition.slice(colonIdx + 1).trim();\n return createMediaFeatureCondition(feature, featureValue, false, raw);\n }\n\n // Boolean feature query: @media(prefers-reduced-motion)\n if (\n !condition.includes('<') &&\n !condition.includes('>') &&\n !condition.includes('=')\n ) {\n return createMediaFeatureCondition(\n condition.trim(),\n undefined,\n false,\n raw,\n );\n }\n\n // Dimension query - parse bounds\n const { dimension, lowerBound, upperBound } =\n this.parseDimensionCondition(condition);\n\n if (!dimension) {\n // Fallback for unparseable - treat as pseudo\n return createPseudoCondition(raw, false, raw);\n }\n\n return createMediaDimensionCondition(\n dimension as 'width' | 'height',\n lowerBound,\n upperBound,\n false,\n raw,\n );\n }\n\n /**\n * Parse dimension condition string (e.g., \"width < 768px\", \"600px <= width < 1200px\")\n */\n private parseDimensionCondition(condition: string): {\n dimension?: string;\n lowerBound?: NumericBound;\n upperBound?: NumericBound;\n } {\n // Range syntax: \"600px <= width < 1200px\"\n const rangeMatch = condition.match(\n /^(.+?)\\s*(<=|<)\\s*(width|height|inline-size|block-size)\\s*(<=|<)\\s*(.+)$/,\n );\n if (rangeMatch) {\n const [, lowerValue, lowerOp, dimension, upperOp, upperValue] =\n rangeMatch;\n return {\n dimension,\n lowerBound: {\n value: lowerValue.trim(),\n valueNumeric: parseNumericValue(lowerValue.trim()),\n inclusive: lowerOp === '<=',\n },\n upperBound: {\n value: upperValue.trim(),\n valueNumeric: parseNumericValue(upperValue.trim()),\n inclusive: upperOp === '<=',\n },\n };\n }\n\n // Simple comparison: \"width < 768px\"\n const simpleMatch = condition.match(\n /^(width|height|inline-size|block-size)\\s*(<=|>=|<|>|=)\\s*(.+)$/,\n );\n if (simpleMatch) {\n const [, dimension, operator, value] = simpleMatch;\n const numeric = parseNumericValue(value.trim());\n\n if (operator === '<' || operator === '<=') {\n return {\n dimension,\n upperBound: {\n value: value.trim(),\n valueNumeric: numeric,\n inclusive: operator === '<=',\n },\n };\n } else if (operator === '>' || operator === '>=') {\n return {\n dimension,\n lowerBound: {\n value: value.trim(),\n valueNumeric: numeric,\n inclusive: operator === '>=',\n },\n };\n } else if (operator === '=') {\n // Exact match: both bounds are the same and inclusive\n return {\n dimension,\n lowerBound: {\n value: value.trim(),\n valueNumeric: numeric,\n inclusive: true,\n },\n upperBound: {\n value: value.trim(),\n valueNumeric: numeric,\n inclusive: true,\n },\n };\n }\n }\n\n // Reversed: \"768px > width\"\n const reversedMatch = condition.match(\n /^(.+?)\\s*(<=|>=|<|>|=)\\s*(width|height|inline-size|block-size)$/,\n );\n if (reversedMatch) {\n const [, value, operator, dimension] = reversedMatch;\n const numeric = parseNumericValue(value.trim());\n\n // Reverse the operator\n if (operator === '<' || operator === '<=') {\n return {\n dimension,\n lowerBound: {\n value: value.trim(),\n valueNumeric: numeric,\n inclusive: operator === '<=',\n },\n };\n } else if (operator === '>' || operator === '>=') {\n return {\n dimension,\n upperBound: {\n value: value.trim(),\n valueNumeric: numeric,\n inclusive: operator === '>=',\n },\n };\n }\n }\n\n return {};\n }\n\n /**\n * Parse @root(...) state\n */\n private parseInnerCondition(\n raw: string,\n prefixLen: number,\n wrap: (inner: ConditionNode) => ConditionNode,\n ): ConditionNode {\n const content = raw.slice(prefixLen, -1);\n if (!content.trim()) return trueCondition();\n return wrap(parseStateKey(content, this.options));\n }\n\n private parseRootState(raw: string): ConditionNode {\n return this.parseInnerCondition(raw, 6, (inner) =>\n createRootCondition(inner, false, raw),\n );\n }\n\n /**\n * Parse @parent(...) state\n *\n * Syntax:\n * @parent(hovered) → :is([data-hovered] *)\n * @parent(theme=dark) → :is([data-theme=\"dark\"] *)\n * @parent(hovered, >) → :is([data-hovered] > *) (direct parent)\n * @parent(.my-class) → :is(.my-class *)\n */\n private parseParentState(raw: string): ConditionNode {\n const content = raw.slice(8, -1);\n if (!content.trim()) {\n return trueCondition();\n }\n\n let condition = content.trim();\n let direct = false;\n\n const lastCommaIdx = condition.lastIndexOf(',');\n if (lastCommaIdx !== -1) {\n const afterComma = condition.slice(lastCommaIdx + 1).trim();\n if (afterComma === '>') {\n direct = true;\n condition = condition.slice(0, lastCommaIdx).trim();\n }\n }\n\n const innerCondition = parseStateKey(condition, this.options);\n return createParentCondition(innerCondition, direct, false, raw);\n }\n\n /**\n * Parse @supports(...) query\n *\n * Syntax:\n * @supports(display: grid) → @supports (display: grid)\n * @supports($, :has(*)) → @supports selector(:has(*))\n */\n private parseSupportsQuery(raw: string): ConditionNode {\n const content = raw.slice(10, -1); // Remove '@supports(' and ')'\n if (!content.trim()) {\n return trueCondition();\n }\n\n // Check for selector syntax: @supports($, :has(*))\n if (content.startsWith('$,')) {\n const selector = content.slice(2).trim(); // Remove '$,' prefix\n return createSupportsCondition('selector', selector, false, raw);\n }\n\n // Feature syntax: @supports(display: grid)\n return createSupportsCondition('feature', content, false, raw);\n }\n\n private parseOwnState(raw: string): ConditionNode {\n return this.parseInnerCondition(raw, 5, (inner) =>\n createOwnCondition(inner, false, raw),\n );\n }\n\n /**\n * Parse @(...) container query\n */\n private parseContainerQuery(raw: string): ConditionNode {\n const content = raw.slice(2, -1); // Remove '@(' and ')'\n if (!content.trim()) {\n return trueCondition();\n }\n\n // Check for named container: @(layout, w < 600px)\n // Use parentheses-aware comma search so inner commas (e.g., scroll-state(a, b)) are skipped\n const commaIdx = findTopLevelComma(content);\n let containerName: string | undefined;\n let condition: string;\n\n if (commaIdx !== -1) {\n containerName = content.slice(0, commaIdx).trim();\n condition = content.slice(commaIdx + 1).trim();\n } else {\n condition = content.trim();\n }\n\n // Check for style query shorthand: @($variant=primary)\n if (condition.startsWith('$')) {\n const styleQuery = condition.slice(1); // Remove '$'\n const eqIdx = styleQuery.indexOf('=');\n\n if (eqIdx === -1) {\n // Existence check: @($variant)\n return createContainerStyleCondition(\n styleQuery,\n undefined,\n containerName,\n false,\n raw,\n );\n }\n\n const property = styleQuery.slice(0, eqIdx).trim();\n let propertyValue = styleQuery.slice(eqIdx + 1).trim();\n\n // Remove quotes if present\n if (\n (propertyValue.startsWith('\"') && propertyValue.endsWith('\"')) ||\n (propertyValue.startsWith(\"'\") && propertyValue.endsWith(\"'\"))\n ) {\n propertyValue = propertyValue.slice(1, -1);\n }\n\n return createContainerStyleCondition(\n property,\n propertyValue,\n containerName,\n false,\n raw,\n );\n }\n\n // Check for function-like syntax: scroll-state(...), style(...), etc.\n // Passes the condition through to CSS verbatim.\n if (/^[a-zA-Z][\\w-]*\\s*\\(/.test(condition)) {\n return createContainerRawCondition(condition, containerName, false, raw);\n }\n\n // Dimension query\n let expandedCondition = expandDimensionShorthands(condition);\n expandedCondition = expandTastyUnits(expandedCondition);\n\n const { dimension, lowerBound, upperBound } =\n this.parseDimensionCondition(expandedCondition);\n\n if (!dimension) {\n // Fallback\n return createPseudoCondition(raw, false, raw);\n }\n\n return createContainerDimensionCondition(\n dimension as 'width' | 'height',\n lowerBound,\n upperBound,\n containerName,\n false,\n raw,\n );\n }\n\n /**\n * Parse predefined state (@mobile, @dark, etc.)\n */\n private parsePredefinedState(raw: string): ConditionNode {\n const ctx = this.options.context;\n if (!ctx) {\n // No context - can't resolve predefined states\n return createPseudoCondition(raw, false, raw);\n }\n\n const resolved = resolvePredefinedState(raw, ctx);\n if (!resolved) {\n // Undefined predefined state - treat as modifier\n return createModifierCondition(\n `data-${camelToKebab(raw.slice(1))}`,\n undefined,\n '=',\n false,\n raw,\n );\n }\n\n // Parse the resolved value recursively\n return parseStateKey(resolved, this.options);\n }\n\n /**\n * Parse value modifier (e.g., theme=danger, size^=sm)\n */\n private parseValueModifier(raw: string): ConditionNode {\n // Match operators: =, ^=, $=, *=\n const opMatch = raw.match(/^([a-z][a-z0-9-]*)(\\^=|\\$=|\\*=|=)(.+)$/i);\n if (!opMatch) {\n return createModifierCondition(\n `data-${camelToKebab(raw)}`,\n undefined,\n '=',\n false,\n raw,\n );\n }\n\n const [, key, operator, value] = opMatch;\n let cleanValue = value;\n\n // Remove quotes if present\n if (\n (cleanValue.startsWith('\"') && cleanValue.endsWith('\"')) ||\n (cleanValue.startsWith(\"'\") && cleanValue.endsWith(\"'\"))\n ) {\n cleanValue = cleanValue.slice(1, -1);\n }\n\n return createModifierCondition(\n `data-${camelToKebab(key)}`,\n cleanValue,\n operator as '=' | '^=' | '$=' | '*=',\n false,\n raw,\n );\n }\n\n /**\n * Parse boolean modifier (e.g., hovered, disabled)\n */\n private parseBooleanModifier(raw: string): ConditionNode {\n return createModifierCondition(\n `data-${camelToKebab(raw)}`,\n undefined,\n '=',\n false,\n raw,\n );\n }\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Parse a numeric value from a CSS value string\n */\nfunction parseNumericValue(value: string): number | null {\n const match = value.match(/^(\\d+(?:\\.\\d+)?)(px|em|rem|vh|vw|%)?$/);\n if (match) {\n return parseFloat(match[1]);\n }\n return null;\n}\n\n// ============================================================================\n// Main Export\n// ============================================================================\n\n/**\n * Parse a state key string into a ConditionNode\n */\nexport function parseStateKey(\n stateKey: string,\n options: ParseStateKeyOptions = {},\n): ConditionNode {\n // Handle empty/default state\n if (!stateKey || !stateKey.trim()) {\n return trueCondition();\n }\n\n const trimmed = stateKey.trim();\n\n // Build cache key including local predefined states (they affect parsing)\n // Global predefined states are set once at initialization and don't change\n const ctx = options.context;\n const localStatesKey =\n ctx && Object.keys(ctx.localPredefinedStates).length > 0\n ? JSON.stringify(ctx.localPredefinedStates)\n : '';\n const cacheKey = JSON.stringify([\n trimmed,\n options.isSubElement,\n localStatesKey,\n ]);\n\n // Check cache\n const cached = parseCache.get(cacheKey);\n if (cached) {\n return cached;\n }\n\n // Tokenize and parse\n const tokens = tokenize(trimmed);\n const parser = new Parser(tokens, options);\n const result = parser.parse();\n\n // Cache result\n parseCache.set(cacheKey, result);\n\n return result;\n}\n\n/**\n * Clear the parse cache (for testing)\n */\nexport function clearParseCache(): void {\n parseCache.clear();\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"parseStateKey.js","names":[],"sources":["../../src/pipeline/parseStateKey.ts"],"sourcesContent":["/**\n * State Key Parser\n *\n * Parses state notation strings (like 'hovered & !disabled', '@media(w < 768px)')\n * into ConditionNode trees for processing in the pipeline.\n */\n\nimport { Lru } from '../parser/lru';\nimport type { StateParserContext } from '../states';\nimport {\n expandDimensionShorthands,\n expandTastyUnits,\n findTopLevelComma,\n resolvePredefinedState,\n} from '../states';\nimport { camelToKebab } from '../utils/case-converter';\nimport { transformSelectorContent } from '../utils/selector-transform';\n\nimport type { ConditionNode, NumericBound } from './conditions';\nimport { emitWarning } from './warnings';\nimport {\n and,\n createContainerDimensionCondition,\n createContainerRawCondition,\n createContainerStyleCondition,\n createMediaDimensionCondition,\n createMediaFeatureCondition,\n createMediaTypeCondition,\n createModifierCondition,\n createOwnCondition,\n createParentCondition,\n createPseudoCondition,\n createRootCondition,\n createStartingCondition,\n createSupportsCondition,\n not,\n or,\n trueCondition,\n} from './conditions';\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/**\n * Maximum XOR operands before emitting a performance warning.\n * A ^ B ^ C ^ D = 8 OR branches (2^(n-1)), so chains above 4\n * risk exponential blowup in downstream processing.\n */\nconst MAX_XOR_CHAIN_LENGTH = 4;\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface ParseStateKeyOptions {\n context?: StateParserContext;\n isSubElement?: boolean;\n}\n\n// ============================================================================\n// Caching\n// ============================================================================\n\n// Cache for parsed state keys (key -> ConditionNode)\nconst parseCache = new Lru<string, ConditionNode>(5000);\n\n// ============================================================================\n// Tokenizer Patterns\n// ============================================================================\n\n/**\n * Pattern for tokenizing state notation.\n * Matches: operators, parentheses, @-prefixed states, value mods, boolean mods,\n * pseudo-classes, class selectors, and attribute selectors.\n *\n * Note: For @supports, @(...) container queries, and :is/:has/:not/:where\n * we need to handle nested parentheses.\n * We use a pattern that allows up to 2 levels of nesting:\n * [^()]*(?:\\([^()]*(?:\\([^)]*\\))?[^)]*\\))*[^)]*\n */\nconst STATE_TOKEN_PATTERN =\n /([&|!^])|([()])|(@media:[a-z]+)|(@media\\([^)]+\\))|(@supports\\([^()]*(?:\\([^)]*\\))?[^)]*\\))|(@root\\([^)]+\\))|(@parent\\([^)]+\\))|(@own\\([^)]+\\))|(@\\([^()]*(?:\\([^)]*\\))?[^)]*\\))|(@starting)|(@[A-Za-z][A-Za-z0-9-]*)|([a-z][a-z0-9-]*(?:\\^=|\\$=|\\*=|=)(?:\"[^\"]*\"|'[^']*'|[^\\s&|!^()]+))|([a-z][a-z0-9-]+)|(:(?:is|has|not|where)\\([^()]*(?:\\([^()]*(?:\\([^)]*\\))?[^)]*\\))*[^)]*\\))|(:[-a-z][a-z0-9-]*(?:\\([^)]+\\))?)|(\\.[a-z][a-z0-9-]+)|(\\[[^\\]]+\\])/gi;\n\n// ============================================================================\n// Token Types\n// ============================================================================\n\ntype TokenType = 'AND' | 'OR' | 'NOT' | 'XOR' | 'LPAREN' | 'RPAREN' | 'STATE';\n\ninterface Token {\n type: TokenType;\n value: string;\n raw: string;\n}\n\n// ============================================================================\n// Tokenizer\n// ============================================================================\n\n/**\n * Tokenize a state notation string\n */\nfunction tokenize(stateKey: string): Token[] {\n const tokens: Token[] = [];\n let match: RegExpExecArray | null;\n\n // Replace commas with | outside of parentheses (for compatibility)\n const normalized = replaceCommasOutsideParens(stateKey);\n\n STATE_TOKEN_PATTERN.lastIndex = 0;\n while ((match = STATE_TOKEN_PATTERN.exec(normalized)) !== null) {\n const fullMatch = match[0];\n\n if (match[1]) {\n // Operator: &, |, !, ^\n switch (fullMatch) {\n case '&':\n tokens.push({ type: 'AND', value: '&', raw: fullMatch });\n break;\n case '|':\n tokens.push({ type: 'OR', value: '|', raw: fullMatch });\n break;\n case '!':\n tokens.push({ type: 'NOT', value: '!', raw: fullMatch });\n break;\n case '^':\n tokens.push({ type: 'XOR', value: '^', raw: fullMatch });\n break;\n }\n } else if (match[2]) {\n // Parenthesis\n if (fullMatch === '(') {\n tokens.push({ type: 'LPAREN', value: '(', raw: fullMatch });\n } else {\n tokens.push({ type: 'RPAREN', value: ')', raw: fullMatch });\n }\n } else {\n // State token (all other capture groups)\n tokens.push({ type: 'STATE', value: fullMatch, raw: fullMatch });\n }\n }\n\n return tokens;\n}\n\n/**\n * Replace commas with | only outside of parentheses\n */\nfunction replaceCommasOutsideParens(str: string): string {\n let result = '';\n let depth = 0;\n\n for (const char of str) {\n if (char === '(') {\n depth++;\n result += char;\n } else if (char === ')') {\n depth--;\n result += char;\n } else if (char === ',' && depth === 0) {\n result += '|';\n } else {\n result += char;\n }\n }\n\n return result;\n}\n\n// ============================================================================\n// Recursive Descent Parser\n// ============================================================================\n\n/**\n * Parser state\n */\nclass Parser {\n private tokens: Token[];\n private pos = 0;\n private options: ParseStateKeyOptions;\n\n constructor(tokens: Token[], options: ParseStateKeyOptions) {\n this.tokens = tokens;\n this.options = options;\n }\n\n parse(): ConditionNode {\n if (this.tokens.length === 0) {\n return trueCondition();\n }\n const result = this.parseExpression();\n return result;\n }\n\n private current(): Token | undefined {\n return this.tokens[this.pos];\n }\n\n private advance(): Token | undefined {\n return this.tokens[this.pos++];\n }\n\n private match(type: TokenType): boolean {\n if (this.current()?.type === type) {\n this.advance();\n return true;\n }\n return false;\n }\n\n /**\n * Parse expression with operator precedence:\n * ! (NOT) > ^ (XOR) > | (OR) > & (AND)\n */\n private parseExpression(): ConditionNode {\n return this.parseAnd();\n }\n\n private parseAnd(): ConditionNode {\n let left = this.parseOr();\n\n while (this.current()?.type === 'AND') {\n this.advance();\n const right = this.parseOr();\n left = and(left, right);\n }\n\n return left;\n }\n\n private parseOr(): ConditionNode {\n let left = this.parseXor();\n\n while (this.current()?.type === 'OR') {\n this.advance();\n const right = this.parseXor();\n left = or(left, right);\n }\n\n return left;\n }\n\n private parseXor(): ConditionNode {\n let left = this.parseUnary();\n let operandCount = 1;\n\n while (this.current()?.type === 'XOR') {\n this.advance();\n const right = this.parseUnary();\n operandCount++;\n\n if (operandCount > MAX_XOR_CHAIN_LENGTH) {\n emitWarning(\n 'XOR_CHAIN_TOO_LONG',\n `XOR chain with ${operandCount} operands produces ${Math.pow(2, operandCount - 1)} OR branches. ` +\n `Consider breaking into smaller expressions to avoid exponential growth.`,\n );\n }\n\n // XOR: (A & !B) | (!A & B)\n left = or(and(left, not(right)), and(not(left), right));\n }\n\n return left;\n }\n\n private parseUnary(): ConditionNode {\n if (this.match('NOT')) {\n const operand = this.parseUnary();\n return not(operand);\n }\n return this.parsePrimary();\n }\n\n private parsePrimary(): ConditionNode {\n // Handle parentheses\n if (this.match('LPAREN')) {\n const expr = this.parseExpression();\n this.match('RPAREN'); // Consume closing paren (lenient if missing)\n return expr;\n }\n\n // Handle state tokens\n const token = this.current();\n if (token?.type === 'STATE') {\n this.advance();\n return this.parseStateToken(token.value);\n }\n\n // Fallback for empty/invalid - return TRUE\n return trueCondition();\n }\n\n /**\n * Parse a state token into a ConditionNode\n */\n private parseStateToken(value: string): ConditionNode {\n // @starting\n if (value === '@starting') {\n return createStartingCondition(false, value);\n }\n\n // @media:type (e.g., @media:print)\n if (value.startsWith('@media:')) {\n const mediaType = value.slice(7) as 'print' | 'screen' | 'all' | 'speech';\n return createMediaTypeCondition(mediaType, false, value);\n }\n\n // @media(...) - media query\n if (value.startsWith('@media(')) {\n return this.parseMediaQuery(value);\n }\n\n // @supports(...) - feature/selector support query\n if (value.startsWith('@supports(')) {\n return this.parseSupportsQuery(value);\n }\n\n // @root(...) - root state\n if (value.startsWith('@root(')) {\n return this.parseRootState(value);\n }\n\n // @parent(...) - parent element state\n if (value.startsWith('@parent(')) {\n return this.parseParentState(value);\n }\n\n // @own(...) - own state (sub-element)\n if (value.startsWith('@own(')) {\n return this.parseOwnState(value);\n }\n\n // @(...) - container query\n if (value.startsWith('@(')) {\n return this.parseContainerQuery(value);\n }\n\n // @name - predefined state\n if (value.startsWith('@') && /^@[A-Za-z][A-Za-z0-9-]*$/.test(value)) {\n return this.parsePredefinedState(value);\n }\n\n // Enhanced pseudo-classes: :is(), :has(), :not(), :where()\n // Transform capitalized words to [data-element=\"...\"] selectors,\n // auto-complete trailing combinators with *, and\n // normalize :not(X) → negated :is(X) for deduplication.\n if (value.startsWith(':')) {\n const enhancedMatch = /^:(is|has|not|where)\\(/.exec(value);\n if (enhancedMatch) {\n const fn = enhancedMatch[1];\n const prefix = enhancedMatch[0];\n let content = transformSelectorContent(value.slice(prefix.length, -1));\n\n // Auto-complete trailing combinator: :has(Icon >) → :has(... > *)\n content = content.replace(/([>+~])\\s*$/, '$1 *');\n\n if (fn === 'not') {\n return createPseudoCondition(`:is(${content})`, true, value);\n }\n\n return createPseudoCondition(`:${fn}(${content})`, false, value);\n }\n\n return createPseudoCondition(value, false, value);\n }\n\n // Class selector (e.g., .active)\n if (value.startsWith('.')) {\n return createPseudoCondition(value, false, value);\n }\n\n // Attribute selector (e.g., [disabled], [data-state=\"active\"])\n if (value.startsWith('[')) {\n return createPseudoCondition(value, false, value);\n }\n\n // Value modifier (e.g., theme=danger, size=large)\n if (value.includes('=')) {\n return this.parseValueModifier(value);\n }\n\n // Boolean modifier (e.g., hovered, disabled)\n return this.parseBooleanModifier(value);\n }\n\n /**\n * Parse @media(...) query\n */\n private parseMediaQuery(raw: string): ConditionNode {\n const content = raw.slice(7, -1); // Remove '@media(' and ')'\n if (!content.trim()) {\n return trueCondition();\n }\n\n // Expand shorthands and units\n let condition = expandDimensionShorthands(content);\n condition = expandTastyUnits(condition);\n\n // Check for feature queries (contains ':' but not dimension comparison)\n if (\n condition.includes(':') &&\n !condition.includes('<') &&\n !condition.includes('>') &&\n !condition.includes('=')\n ) {\n // Feature query: @media(prefers-contrast: high)\n const colonIdx = condition.indexOf(':');\n const feature = condition.slice(0, colonIdx).trim();\n const featureValue = condition.slice(colonIdx + 1).trim();\n return createMediaFeatureCondition(feature, featureValue, false, raw);\n }\n\n // Boolean feature query: @media(prefers-reduced-motion)\n if (\n !condition.includes('<') &&\n !condition.includes('>') &&\n !condition.includes('=')\n ) {\n return createMediaFeatureCondition(\n condition.trim(),\n undefined,\n false,\n raw,\n );\n }\n\n // Dimension query - parse bounds\n const { dimension, lowerBound, upperBound } =\n this.parseDimensionCondition(condition);\n\n if (!dimension) {\n // Fallback for unparseable - treat as pseudo\n return createPseudoCondition(raw, false, raw);\n }\n\n return createMediaDimensionCondition(\n dimension as 'width' | 'height',\n lowerBound,\n upperBound,\n false,\n raw,\n );\n }\n\n /**\n * Parse dimension condition string (e.g., \"width < 768px\", \"600px <= width < 1200px\")\n */\n private parseDimensionCondition(condition: string): {\n dimension?: string;\n lowerBound?: NumericBound;\n upperBound?: NumericBound;\n } {\n // Range syntax: \"600px <= width < 1200px\"\n const rangeMatch = condition.match(\n /^(.+?)\\s*(<=|<)\\s*(width|height|inline-size|block-size)\\s*(<=|<)\\s*(.+)$/,\n );\n if (rangeMatch) {\n const [, lowerValue, lowerOp, dimension, upperOp, upperValue] =\n rangeMatch;\n return {\n dimension,\n lowerBound: {\n value: lowerValue.trim(),\n valueNumeric: parseNumericValue(lowerValue.trim()),\n inclusive: lowerOp === '<=',\n },\n upperBound: {\n value: upperValue.trim(),\n valueNumeric: parseNumericValue(upperValue.trim()),\n inclusive: upperOp === '<=',\n },\n };\n }\n\n // Simple comparison: \"width < 768px\"\n const simpleMatch = condition.match(\n /^(width|height|inline-size|block-size)\\s*(<=|>=|<|>|=)\\s*(.+)$/,\n );\n if (simpleMatch) {\n const [, dimension, operator, value] = simpleMatch;\n const numeric = parseNumericValue(value.trim());\n\n if (operator === '<' || operator === '<=') {\n return {\n dimension,\n upperBound: {\n value: value.trim(),\n valueNumeric: numeric,\n inclusive: operator === '<=',\n },\n };\n } else if (operator === '>' || operator === '>=') {\n return {\n dimension,\n lowerBound: {\n value: value.trim(),\n valueNumeric: numeric,\n inclusive: operator === '>=',\n },\n };\n } else if (operator === '=') {\n // Exact match: both bounds are the same and inclusive\n return {\n dimension,\n lowerBound: {\n value: value.trim(),\n valueNumeric: numeric,\n inclusive: true,\n },\n upperBound: {\n value: value.trim(),\n valueNumeric: numeric,\n inclusive: true,\n },\n };\n }\n }\n\n // Reversed: \"768px > width\"\n const reversedMatch = condition.match(\n /^(.+?)\\s*(<=|>=|<|>|=)\\s*(width|height|inline-size|block-size)$/,\n );\n if (reversedMatch) {\n const [, value, operator, dimension] = reversedMatch;\n const numeric = parseNumericValue(value.trim());\n\n // Reverse the operator\n if (operator === '<' || operator === '<=') {\n return {\n dimension,\n lowerBound: {\n value: value.trim(),\n valueNumeric: numeric,\n inclusive: operator === '<=',\n },\n };\n } else if (operator === '>' || operator === '>=') {\n return {\n dimension,\n upperBound: {\n value: value.trim(),\n valueNumeric: numeric,\n inclusive: operator === '>=',\n },\n };\n }\n }\n\n return {};\n }\n\n /**\n * Parse @root(...) state\n */\n private parseInnerCondition(\n raw: string,\n prefixLen: number,\n wrap: (inner: ConditionNode) => ConditionNode,\n ): ConditionNode {\n const content = raw.slice(prefixLen, -1);\n if (!content.trim()) return trueCondition();\n return wrap(parseStateKey(content, this.options));\n }\n\n private parseRootState(raw: string): ConditionNode {\n return this.parseInnerCondition(raw, 6, (inner) =>\n createRootCondition(inner, false, raw),\n );\n }\n\n /**\n * Parse @parent(...) state\n *\n * Syntax:\n * @parent(hovered) → :is([data-hovered] *)\n * @parent(theme=dark) → :is([data-theme=\"dark\"] *)\n * @parent(hovered, >) → :is([data-hovered] > *) (direct parent)\n * @parent(.my-class) → :is(.my-class *)\n */\n private parseParentState(raw: string): ConditionNode {\n const content = raw.slice(8, -1);\n if (!content.trim()) {\n return trueCondition();\n }\n\n let condition = content.trim();\n let direct = false;\n\n const lastCommaIdx = condition.lastIndexOf(',');\n if (lastCommaIdx !== -1) {\n const afterComma = condition.slice(lastCommaIdx + 1).trim();\n if (afterComma === '>') {\n direct = true;\n condition = condition.slice(0, lastCommaIdx).trim();\n }\n }\n\n const innerCondition = parseStateKey(condition, this.options);\n return createParentCondition(innerCondition, direct, false, raw);\n }\n\n /**\n * Parse @supports(...) query\n *\n * Syntax:\n * @supports(display: grid) → @supports (display: grid)\n * @supports($, :has(*)) → @supports selector(:has(*))\n */\n private parseSupportsQuery(raw: string): ConditionNode {\n const content = raw.slice(10, -1); // Remove '@supports(' and ')'\n if (!content.trim()) {\n return trueCondition();\n }\n\n // Check for selector syntax: @supports($, :has(*))\n if (content.startsWith('$,')) {\n const selector = content.slice(2).trim(); // Remove '$,' prefix\n return createSupportsCondition('selector', selector, false, raw);\n }\n\n // Feature syntax: @supports(display: grid)\n return createSupportsCondition('feature', content, false, raw);\n }\n\n private parseOwnState(raw: string): ConditionNode {\n return this.parseInnerCondition(raw, 5, (inner) =>\n createOwnCondition(inner, false, raw),\n );\n }\n\n /**\n * Parse @(...) container query\n */\n private parseContainerQuery(raw: string): ConditionNode {\n const content = raw.slice(2, -1); // Remove '@(' and ')'\n if (!content.trim()) {\n return trueCondition();\n }\n\n // Check for named container: @(layout, w < 600px)\n // Use parentheses-aware comma search so inner commas (e.g., scroll-state(a, b)) are skipped\n const commaIdx = findTopLevelComma(content);\n let containerName: string | undefined;\n let condition: string;\n\n if (commaIdx !== -1) {\n containerName = content.slice(0, commaIdx).trim();\n condition = content.slice(commaIdx + 1).trim();\n } else {\n condition = content.trim();\n }\n\n // Check for style query shorthand: @($variant=primary)\n if (condition.startsWith('$')) {\n const styleQuery = condition.slice(1); // Remove '$'\n const eqIdx = styleQuery.indexOf('=');\n\n if (eqIdx === -1) {\n // Existence check: @($variant)\n return createContainerStyleCondition(\n styleQuery,\n undefined,\n containerName,\n false,\n raw,\n );\n }\n\n const property = styleQuery.slice(0, eqIdx).trim();\n let propertyValue = styleQuery.slice(eqIdx + 1).trim();\n\n // Remove quotes if present\n if (\n (propertyValue.startsWith('\"') && propertyValue.endsWith('\"')) ||\n (propertyValue.startsWith(\"'\") && propertyValue.endsWith(\"'\"))\n ) {\n propertyValue = propertyValue.slice(1, -1);\n }\n\n return createContainerStyleCondition(\n property,\n propertyValue,\n containerName,\n false,\n raw,\n );\n }\n\n // Check for function-like syntax: scroll-state(...), style(...), etc.\n // Passes the condition through to CSS verbatim.\n if (/^[a-zA-Z][\\w-]*\\s*\\(/.test(condition)) {\n return createContainerRawCondition(condition, containerName, false, raw);\n }\n\n // Dimension query\n let expandedCondition = expandDimensionShorthands(condition);\n expandedCondition = expandTastyUnits(expandedCondition);\n\n const { dimension, lowerBound, upperBound } =\n this.parseDimensionCondition(expandedCondition);\n\n if (!dimension) {\n // Fallback\n return createPseudoCondition(raw, false, raw);\n }\n\n return createContainerDimensionCondition(\n dimension as 'width' | 'height',\n lowerBound,\n upperBound,\n containerName,\n false,\n raw,\n );\n }\n\n /**\n * Parse predefined state (@mobile, @dark, etc.)\n */\n private parsePredefinedState(raw: string): ConditionNode {\n const ctx = this.options.context;\n if (!ctx) {\n // No context - can't resolve predefined states\n return createPseudoCondition(raw, false, raw);\n }\n\n const resolved = resolvePredefinedState(raw, ctx);\n if (!resolved) {\n // Undefined predefined state - treat as modifier\n return createModifierCondition(\n `data-${camelToKebab(raw.slice(1))}`,\n undefined,\n '=',\n false,\n raw,\n );\n }\n\n // Parse the resolved value recursively\n return parseStateKey(resolved, this.options);\n }\n\n /**\n * Parse value modifier (e.g., theme=danger, size^=sm)\n */\n private parseValueModifier(raw: string): ConditionNode {\n // Match operators: =, ^=, $=, *=\n const opMatch = raw.match(/^([a-z][a-z0-9-]*)(\\^=|\\$=|\\*=|=)(.+)$/i);\n if (!opMatch) {\n return createModifierCondition(\n `data-${camelToKebab(raw)}`,\n undefined,\n '=',\n false,\n raw,\n );\n }\n\n const [, key, operator, value] = opMatch;\n let cleanValue = value;\n\n // Remove quotes if present\n if (\n (cleanValue.startsWith('\"') && cleanValue.endsWith('\"')) ||\n (cleanValue.startsWith(\"'\") && cleanValue.endsWith(\"'\"))\n ) {\n cleanValue = cleanValue.slice(1, -1);\n }\n\n return createModifierCondition(\n `data-${camelToKebab(key)}`,\n cleanValue,\n operator as '=' | '^=' | '$=' | '*=',\n false,\n raw,\n );\n }\n\n /**\n * Parse boolean modifier (e.g., hovered, disabled)\n */\n private parseBooleanModifier(raw: string): ConditionNode {\n return createModifierCondition(\n `data-${camelToKebab(raw)}`,\n undefined,\n '=',\n false,\n raw,\n );\n }\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Parse a numeric value from a CSS value string\n */\nfunction parseNumericValue(value: string): number | null {\n const match = value.match(/^(\\d+(?:\\.\\d+)?)(px|em|rem|vh|vw|%)?$/);\n if (match) {\n return parseFloat(match[1]);\n }\n return null;\n}\n\n// ============================================================================\n// Main Export\n// ============================================================================\n\n/**\n * Parse a state key string into a ConditionNode\n */\nexport function parseStateKey(\n stateKey: string,\n options: ParseStateKeyOptions = {},\n): ConditionNode {\n // Handle empty/default state\n if (!stateKey || !stateKey.trim()) {\n return trueCondition();\n }\n\n const trimmed = stateKey.trim();\n\n // Build cache key including local predefined states (they affect parsing)\n // Global predefined states are set once at initialization and don't change\n const ctx = options.context;\n const localStatesKey =\n ctx && Object.keys(ctx.localPredefinedStates).length > 0\n ? JSON.stringify(ctx.localPredefinedStates)\n : '';\n const cacheKey =\n trimmed + '\\0' + (options.isSubElement ? '1' : '0') + '\\0' + localStatesKey;\n\n // Check cache\n const cached = parseCache.get(cacheKey);\n if (cached) {\n return cached;\n }\n\n // Tokenize and parse\n const tokens = tokenize(trimmed);\n const parser = new Parser(tokens, options);\n const result = parser.parse();\n\n // Cache result\n parseCache.set(cacheKey, result);\n\n return result;\n}\n\n/**\n * Clear the parse cache (for testing)\n */\nexport function clearParseCache(): void {\n parseCache.clear();\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { Lru } from "../parser/lru.js";
2
+ import { okhslToSrgb } from "../utils/color-math.js";
2
3
 
3
4
  //#region src/plugins/okhsl-plugin.ts
4
5
  /**
@@ -12,283 +13,9 @@ import { Lru } from "../parser/lru.js";
12
13
  * okhsl(240.5deg 50% 50%)
13
14
  * okhsl(0.25turn 50% 50%)
14
15
  * okhsl(1.57rad 50% 50%)
15
- *
16
- * Conversion math inlined from @texel/color to avoid external dependencies.
17
16
  */
18
17
  const conversionCache = new Lru(500);
19
- const OKLab_to_LMS_M = [
20
- [
21
- 1,
22
- .3963377773761749,
23
- .2158037573099136
24
- ],
25
- [
26
- 1,
27
- -.1055613458156586,
28
- -.0638541728258133
29
- ],
30
- [
31
- 1,
32
- -.0894841775298119,
33
- -1.2914855480194092
34
- ]
35
- ];
36
- const LMS_to_linear_sRGB_M = [
37
- [
38
- 4.076741636075959,
39
- -3.307711539258062,
40
- .2309699031821041
41
- ],
42
- [
43
- -1.2684379732850313,
44
- 2.6097573492876878,
45
- -.3413193760026569
46
- ],
47
- [
48
- -.004196076138675526,
49
- -.703418617935936,
50
- 1.7076146940746113
51
- ]
52
- ];
53
- const OKLab_to_linear_sRGB_coefficients = [
54
- [[-1.8817030993265873, -.8093650129914302], [
55
- 1.19086277,
56
- 1.76576728,
57
- .59662641,
58
- .75515197,
59
- .56771245
60
- ]],
61
- [[1.8144407988010998, -1.194452667805235], [
62
- .73956515,
63
- -.45954404,
64
- .08285427,
65
- .12541073,
66
- -.14503204
67
- ]],
68
- [[.13110757611180954, 1.813339709266608], [
69
- 1.35733652,
70
- -.00915799,
71
- -1.1513021,
72
- -.50559606,
73
- .00692167
74
- ]]
75
- ];
76
- const TAU = 2 * Math.PI;
77
18
  const clamp = (value, min, max) => Math.max(Math.min(value, max), min);
78
- const constrainAngle = (angle) => (angle % 360 + 360) % 360;
79
- const sRGBLinearToGamma = (val) => {
80
- const sign = val < 0 ? -1 : 1;
81
- const abs = Math.abs(val);
82
- return abs > .0031308 ? sign * (1.055 * Math.pow(abs, 1 / 2.4) - .055) : 12.92 * val;
83
- };
84
- const dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
85
- const transform = (input, matrix) => [
86
- dot3(input, matrix[0]),
87
- dot3(input, matrix[1]),
88
- dot3(input, matrix[2])
89
- ];
90
- const cubed3 = (lms) => [
91
- lms[0] * lms[0] * lms[0],
92
- lms[1] * lms[1] * lms[1],
93
- lms[2] * lms[2] * lms[2]
94
- ];
95
- const OKLabToLinearSRGB = (lab) => {
96
- return transform(cubed3(transform(lab, OKLab_to_LMS_M)), LMS_to_linear_sRGB_M);
97
- };
98
- const K1 = .206;
99
- const K2 = .03;
100
- const K3 = (1 + K1) / (1 + K2);
101
- const toeInv = (x) => (x ** 2 + K1 * x) / (K3 * (x + K2));
102
- const dotXY = (a, b) => a[0] * b[0] + a[1] * b[1];
103
- const computeMaxSaturationOKLC = (a, b) => {
104
- const okCoeff = OKLab_to_linear_sRGB_coefficients;
105
- const lmsToRgb = LMS_to_linear_sRGB_M;
106
- const tmp2 = [a, b];
107
- const tmp3 = [
108
- 0,
109
- a,
110
- b
111
- ];
112
- let chnlCoeff;
113
- let chnlLMS;
114
- if (dotXY(okCoeff[0][0], tmp2) > 1) {
115
- chnlCoeff = okCoeff[0][1];
116
- chnlLMS = lmsToRgb[0];
117
- } else if (dotXY(okCoeff[1][0], tmp2) > 1) {
118
- chnlCoeff = okCoeff[1][1];
119
- chnlLMS = lmsToRgb[1];
120
- } else {
121
- chnlCoeff = okCoeff[2][1];
122
- chnlLMS = lmsToRgb[2];
123
- }
124
- const [k0, k1, k2, k3, k4] = chnlCoeff;
125
- const [wl, wm, ws] = chnlLMS;
126
- let sat = k0 + k1 * a + k2 * b + k3 * (a * a) + k4 * a * b;
127
- const dotYZ = (mat, vec) => mat[1] * vec[1] + mat[2] * vec[2];
128
- const kl = dotYZ(OKLab_to_LMS_M[0], tmp3);
129
- const km = dotYZ(OKLab_to_LMS_M[1], tmp3);
130
- const ks = dotYZ(OKLab_to_LMS_M[2], tmp3);
131
- const l_ = 1 + sat * kl;
132
- const m_ = 1 + sat * km;
133
- const s_ = 1 + sat * ks;
134
- const l = l_ * l_ * l_;
135
- const m = m_ * m_ * m_;
136
- const s = s_ * s_ * s_;
137
- const lds = 3 * kl * (l_ * l_);
138
- const mds = 3 * km * (m_ * m_);
139
- const sds = 3 * ks * (s_ * s_);
140
- const lds2 = 6 * (kl * kl) * l_;
141
- const mds2 = 6 * (km * km) * m_;
142
- const sds2 = 6 * (ks * ks) * s_;
143
- const f = wl * l + wm * m + ws * s;
144
- const f1 = wl * lds + wm * mds + ws * sds;
145
- const f2 = wl * lds2 + wm * mds2 + ws * sds2;
146
- sat = sat - f * f1 / (f1 * f1 - .5 * f * f2);
147
- return sat;
148
- };
149
- const findCuspOKLCH = (a, b) => {
150
- const S_cusp = computeMaxSaturationOKLC(a, b);
151
- const rgb_at_max = OKLabToLinearSRGB([
152
- 1,
153
- S_cusp * a,
154
- S_cusp * b
155
- ]);
156
- const L_cusp = Math.cbrt(1 / Math.max(Math.max(rgb_at_max[0], rgb_at_max[1]), Math.max(rgb_at_max[2], 0)));
157
- return [L_cusp, L_cusp * S_cusp];
158
- };
159
- const findGamutIntersectionOKLCH = (a, b, l1, c1, l0, cusp) => {
160
- const lmsToRgb = LMS_to_linear_sRGB_M;
161
- const tmp3 = [
162
- 0,
163
- a,
164
- b
165
- ];
166
- const floatMax = Number.MAX_VALUE;
167
- let t;
168
- const dotYZ = (mat, vec) => mat[1] * vec[1] + mat[2] * vec[2];
169
- const dotXYZ = (vec, x, y, z) => vec[0] * x + vec[1] * y + vec[2] * z;
170
- if ((l1 - l0) * cusp[1] - (cusp[0] - l0) * c1 <= 0) {
171
- const denom = c1 * cusp[0] + cusp[1] * (l0 - l1);
172
- t = denom === 0 ? 0 : cusp[1] * l0 / denom;
173
- } else {
174
- const denom = c1 * (cusp[0] - 1) + cusp[1] * (l0 - l1);
175
- t = denom === 0 ? 0 : cusp[1] * (l0 - 1) / denom;
176
- const dl = l1 - l0;
177
- const dc = c1;
178
- const kl = dotYZ(OKLab_to_LMS_M[0], tmp3);
179
- const km = dotYZ(OKLab_to_LMS_M[1], tmp3);
180
- const ks = dotYZ(OKLab_to_LMS_M[2], tmp3);
181
- const ldt_ = dl + dc * kl;
182
- const mdt_ = dl + dc * km;
183
- const sdt_ = dl + dc * ks;
184
- const L = l0 * (1 - t) + t * l1;
185
- const C = t * c1;
186
- const l_ = L + C * kl;
187
- const m_ = L + C * km;
188
- const s_ = L + C * ks;
189
- const l = l_ * l_ * l_;
190
- const m = m_ * m_ * m_;
191
- const s = s_ * s_ * s_;
192
- const ldt = 3 * ldt_ * l_ * l_;
193
- const mdt = 3 * mdt_ * m_ * m_;
194
- const sdt = 3 * sdt_ * s_ * s_;
195
- const ldt2 = 6 * ldt_ * ldt_ * l_;
196
- const mdt2 = 6 * mdt_ * mdt_ * m_;
197
- const sdt2 = 6 * sdt_ * sdt_ * s_;
198
- const r_ = dotXYZ(lmsToRgb[0], l, m, s) - 1;
199
- const r1 = dotXYZ(lmsToRgb[0], ldt, mdt, sdt);
200
- const r2 = dotXYZ(lmsToRgb[0], ldt2, mdt2, sdt2);
201
- const ur = r1 / (r1 * r1 - .5 * r_ * r2);
202
- let tr = -r_ * ur;
203
- const g_ = dotXYZ(lmsToRgb[1], l, m, s) - 1;
204
- const g1 = dotXYZ(lmsToRgb[1], ldt, mdt, sdt);
205
- const g2 = dotXYZ(lmsToRgb[1], ldt2, mdt2, sdt2);
206
- const ug = g1 / (g1 * g1 - .5 * g_ * g2);
207
- let tg = -g_ * ug;
208
- const b_ = dotXYZ(lmsToRgb[2], l, m, s) - 1;
209
- const b1 = dotXYZ(lmsToRgb[2], ldt, mdt, sdt);
210
- const b2 = dotXYZ(lmsToRgb[2], ldt2, mdt2, sdt2);
211
- const ub = b1 / (b1 * b1 - .5 * b_ * b2);
212
- let tb = -b_ * ub;
213
- tr = ur >= 0 ? tr : floatMax;
214
- tg = ug >= 0 ? tg : floatMax;
215
- tb = ub >= 0 ? tb : floatMax;
216
- t += Math.min(tr, Math.min(tg, tb));
217
- }
218
- return t;
219
- };
220
- const computeSt = (cusp) => {
221
- const l = cusp[0];
222
- const c = cusp[1];
223
- return [c / l, c / (1 - l)];
224
- };
225
- const computeStMid = (a, b) => {
226
- return [.11516993 + 1 / (7.4477897 + 4.1590124 * b + a * (-2.19557347 + 1.75198401 * b + a * (-2.13704948 - 10.02301043 * b + a * (-4.24894561 + 5.38770819 * b + 4.69891013 * a)))), .11239642 + 1 / (1.6132032 - .68124379 * b + a * (.40370612 + .90148123 * b + a * (-.27087943 + .6122399 * b + a * (.00299215 - .45399568 * b - .14661872 * a))))];
227
- };
228
- const getCs = (L, a, b, cusp) => {
229
- const cMax = findGamutIntersectionOKLCH(a, b, L, 1, L, cusp);
230
- const stMax = computeSt(cusp);
231
- const k = cMax / Math.min(L * stMax[0], (1 - L) * stMax[1]);
232
- const stMid = computeStMid(a, b);
233
- let ca = L * stMid[0];
234
- let cb = (1 - L) * stMid[1];
235
- const cMid = .9 * k * Math.sqrt(Math.sqrt(1 / (1 / ca ** 4 + 1 / cb ** 4)));
236
- ca = L * .4;
237
- cb = (1 - L) * .8;
238
- return [
239
- Math.sqrt(1 / (1 / ca ** 2 + 1 / cb ** 2)),
240
- cMid,
241
- cMax
242
- ];
243
- };
244
- const OKHSLToOKLab = (hsl) => {
245
- let h = hsl[0];
246
- const s = hsl[1];
247
- const l = hsl[2];
248
- const L = toeInv(l);
249
- let a = 0;
250
- let b = 0;
251
- h = constrainAngle(h) / 360;
252
- if (L !== 0 && L !== 1 && s !== 0) {
253
- const a_ = Math.cos(TAU * h);
254
- const b_ = Math.sin(TAU * h);
255
- const [c0, cMid, cMax] = getCs(L, a_, b_, findCuspOKLCH(a_, b_));
256
- const mid = .8;
257
- const midInv = 1.25;
258
- let t, k0, k1, k2;
259
- if (s < mid) {
260
- t = midInv * s;
261
- k0 = 0;
262
- k1 = mid * c0;
263
- k2 = 1 - k1 / cMid;
264
- } else {
265
- t = 5 * (s - .8);
266
- k0 = cMid;
267
- k1 = .2 * cMid ** 2 * 1.25 ** 2 / c0;
268
- k2 = 1 - k1 / (cMax - cMid);
269
- }
270
- const c = k0 + t * k1 / (1 - k2 * t);
271
- a = c * a_;
272
- b = c * b_;
273
- }
274
- return [
275
- L,
276
- a,
277
- b
278
- ];
279
- };
280
- const okhslToSRGB = (h, s, l) => {
281
- const linearRGB = OKLabToLinearSRGB(OKHSLToOKLab([
282
- h,
283
- s,
284
- l
285
- ]));
286
- return [
287
- clamp(sRGBLinearToGamma(linearRGB[0]), 0, 1),
288
- clamp(sRGBLinearToGamma(linearRGB[1]), 0, 1),
289
- clamp(sRGBLinearToGamma(linearRGB[2]), 0, 1)
290
- ];
291
- };
292
19
  /**
293
20
  * Parse an angle value with optional unit.
294
21
  * Supports: deg, turn, rad, or unitless (treated as degrees).
@@ -330,7 +57,7 @@ const okhslFunc = (groups) => {
330
57
  const h = parseAngle(tokens[0]);
331
58
  const s = parsePercentage(tokens[1]);
332
59
  const l = parsePercentage(tokens[2]);
333
- const [r, g, b] = okhslToSRGB(h, clamp(s, 0, 1), clamp(l, 0, 1));
60
+ const [r, g, b] = okhslToSrgb(h, clamp(s, 0, 1), clamp(l, 0, 1));
334
61
  const format = (n) => {
335
62
  const pct = n * 100;
336
63
  return parseFloat(pct.toFixed(1)).toString() + "%";
@@ -1 +1 @@
1
- {"version":3,"file":"okhsl-plugin.js","names":[],"sources":["../../src/plugins/okhsl-plugin.ts"],"sourcesContent":["/**\n * OKHSL Plugin for Tasty\n *\n * Converts OKHSL color syntax to RGB notation.\n * Supports angle units: deg, turn, rad, or unitless (degrees).\n *\n * Examples:\n * okhsl(240.5 50% 50%)\n * okhsl(240.5deg 50% 50%)\n * okhsl(0.25turn 50% 50%)\n * okhsl(1.57rad 50% 50%)\n *\n * Conversion math inlined from @texel/color to avoid external dependencies.\n */\n\nimport { Lru } from '../parser/lru';\n\nimport type { StyleDetails } from '../parser/types';\nimport type { TastyPlugin, TastyPluginFactory } from './types';\n\n// Cache for OKHSL to RGB conversions\nconst conversionCache = new Lru<string, string>(500);\n\n// ============================================================================\n// Conversion Matrices (from texel-color)\n// ============================================================================\n\nconst OKLab_to_LMS_M: [number, number, number][] = [\n [1.0, 0.3963377773761749, 0.2158037573099136],\n [1.0, -0.1055613458156586, -0.0638541728258133],\n [1.0, -0.0894841775298119, -1.2914855480194092],\n];\n\nconst LMS_to_linear_sRGB_M: [number, number, number][] = [\n [4.076741636075959, -3.307711539258062, 0.2309699031821041],\n [-1.2684379732850313, 2.6097573492876878, -0.3413193760026569],\n [-0.004196076138675526, -0.703418617935936, 1.7076146940746113],\n];\n\nconst OKLab_to_linear_sRGB_coefficients: [\n [[number, number], number[]],\n [[number, number], number[]],\n [[number, number], number[]],\n] = [\n [\n [-1.8817030993265873, -0.8093650129914302],\n [1.19086277, 1.76576728, 0.59662641, 0.75515197, 0.56771245],\n ],\n [\n [1.8144407988010998, -1.194452667805235],\n [0.73956515, -0.45954404, 0.08285427, 0.12541073, -0.14503204],\n ],\n [\n [0.13110757611180954, 1.813339709266608],\n [1.35733652, -0.00915799, -1.1513021, -0.50559606, 0.00692167],\n ],\n];\n\n// ============================================================================\n// Math Utilities\n// ============================================================================\n\nconst TAU = 2 * Math.PI;\n\nconst clamp = (value: number, min: number, max: number): number =>\n Math.max(Math.min(value, max), min);\n\nconst constrainAngle = (angle: number): number => ((angle % 360) + 360) % 360;\n\nconst copySign = (to: number, from: number): number =>\n Math.sign(to) === Math.sign(from) ? to : -to;\n\nconst _spow = (base: number, exp: number): number =>\n copySign(Math.abs(base) ** exp, base);\n\n// ============================================================================\n// sRGB Gamma Conversion\n// ============================================================================\n\nconst sRGBLinearToGamma = (val: number): number => {\n const sign = val < 0 ? -1 : 1;\n const abs = Math.abs(val);\n return abs > 0.0031308\n ? sign * (1.055 * Math.pow(abs, 1 / 2.4) - 0.055)\n : 12.92 * val;\n};\n\n// ============================================================================\n// Matrix Operations\n// ============================================================================\n\ntype Vec3 = [number, number, number];\n\nconst dot3 = (a: Vec3, b: Vec3): number =>\n a[0] * b[0] + a[1] * b[1] + a[2] * b[2];\n\nconst transform = (input: Vec3, matrix: Vec3[]): Vec3 => [\n dot3(input, matrix[0]),\n dot3(input, matrix[1]),\n dot3(input, matrix[2]),\n];\n\nconst cubed3 = (lms: Vec3): Vec3 => [\n lms[0] * lms[0] * lms[0],\n lms[1] * lms[1] * lms[1],\n lms[2] * lms[2] * lms[2],\n];\n\n// ============================================================================\n// OKLab Conversion\n// ============================================================================\n\nconst OKLabToLinearSRGB = (lab: Vec3): Vec3 => {\n const lms = transform(lab, OKLab_to_LMS_M);\n const cubedLms = cubed3(lms);\n return transform(cubedLms, LMS_to_linear_sRGB_M);\n};\n\n// ============================================================================\n// OKHSL to OKLab Conversion (from texel-color)\n// ============================================================================\n\nconst K1 = 0.206;\nconst K2 = 0.03;\nconst K3 = (1.0 + K1) / (1.0 + K2);\n\nconst _toe = (x: number): number =>\n 0.5 *\n (K3 * x - K1 + Math.sqrt((K3 * x - K1) * (K3 * x - K1) + 4 * K2 * K3 * x));\n\nconst toeInv = (x: number): number => (x ** 2 + K1 * x) / (K3 * (x + K2));\n\nconst dotXY = (a: [number, number], b: [number, number]): number =>\n a[0] * b[0] + a[1] * b[1];\n\nconst computeMaxSaturationOKLC = (a: number, b: number): number => {\n const okCoeff = OKLab_to_linear_sRGB_coefficients;\n const lmsToRgb = LMS_to_linear_sRGB_M;\n const tmp2: [number, number] = [a, b];\n const tmp3: Vec3 = [0, a, b];\n\n let chnlCoeff: number[];\n let chnlLMS: Vec3;\n\n if (dotXY(okCoeff[0][0], tmp2) > 1) {\n chnlCoeff = okCoeff[0][1];\n chnlLMS = lmsToRgb[0];\n } else if (dotXY(okCoeff[1][0], tmp2) > 1) {\n chnlCoeff = okCoeff[1][1];\n chnlLMS = lmsToRgb[1];\n } else {\n chnlCoeff = okCoeff[2][1];\n chnlLMS = lmsToRgb[2];\n }\n\n const [k0, k1, k2, k3, k4] = chnlCoeff;\n const [wl, wm, ws] = chnlLMS;\n\n let sat = k0 + k1 * a + k2 * b + k3 * (a * a) + k4 * a * b;\n\n const dotYZ = (mat: Vec3, vec: Vec3): number =>\n mat[1] * vec[1] + mat[2] * vec[2];\n\n const kl = dotYZ(OKLab_to_LMS_M[0], tmp3);\n const km = dotYZ(OKLab_to_LMS_M[1], tmp3);\n const ks = dotYZ(OKLab_to_LMS_M[2], tmp3);\n\n const l_ = 1.0 + sat * kl;\n const m_ = 1.0 + sat * km;\n const s_ = 1.0 + sat * ks;\n\n const l = l_ * l_ * l_;\n const m = m_ * m_ * m_;\n const s = s_ * s_ * s_;\n\n const lds = 3.0 * kl * (l_ * l_);\n const mds = 3.0 * km * (m_ * m_);\n const sds = 3.0 * ks * (s_ * s_);\n\n const lds2 = 6.0 * (kl * kl) * l_;\n const mds2 = 6.0 * (km * km) * m_;\n const sds2 = 6.0 * (ks * ks) * s_;\n\n const f = wl * l + wm * m + ws * s;\n const f1 = wl * lds + wm * mds + ws * sds;\n const f2 = wl * lds2 + wm * mds2 + ws * sds2;\n\n sat = sat - (f * f1) / (f1 * f1 - 0.5 * f * f2);\n\n return sat;\n};\n\nconst findCuspOKLCH = (a: number, b: number): [number, number] => {\n const _lmsToRgb = LMS_to_linear_sRGB_M;\n const S_cusp = computeMaxSaturationOKLC(a, b);\n\n const lab: Vec3 = [1, S_cusp * a, S_cusp * b];\n const rgb_at_max = OKLabToLinearSRGB(lab);\n\n const L_cusp = Math.cbrt(\n 1 /\n Math.max(\n Math.max(rgb_at_max[0], rgb_at_max[1]),\n Math.max(rgb_at_max[2], 0.0),\n ),\n );\n const C_cusp = L_cusp * S_cusp;\n\n return [L_cusp, C_cusp];\n};\n\nconst findGamutIntersectionOKLCH = (\n a: number,\n b: number,\n l1: number,\n c1: number,\n l0: number,\n cusp: [number, number],\n): number => {\n const lmsToRgb = LMS_to_linear_sRGB_M;\n const tmp3: Vec3 = [0, a, b];\n const floatMax = Number.MAX_VALUE;\n\n let t: number;\n\n const dotYZ = (mat: Vec3, vec: Vec3): number =>\n mat[1] * vec[1] + mat[2] * vec[2];\n const dotXYZ = (vec: Vec3, x: number, y: number, z: number): number =>\n vec[0] * x + vec[1] * y + vec[2] * z;\n\n if ((l1 - l0) * cusp[1] - (cusp[0] - l0) * c1 <= 0.0) {\n const denom = c1 * cusp[0] + cusp[1] * (l0 - l1);\n t = denom === 0 ? 0 : (cusp[1] * l0) / denom;\n } else {\n const denom = c1 * (cusp[0] - 1.0) + cusp[1] * (l0 - l1);\n t = denom === 0 ? 0 : (cusp[1] * (l0 - 1.0)) / denom;\n\n const dl = l1 - l0;\n const dc = c1;\n\n const kl = dotYZ(OKLab_to_LMS_M[0], tmp3);\n const km = dotYZ(OKLab_to_LMS_M[1], tmp3);\n const ks = dotYZ(OKLab_to_LMS_M[2], tmp3);\n\n const ldt_ = dl + dc * kl;\n const mdt_ = dl + dc * km;\n const sdt_ = dl + dc * ks;\n\n const L = l0 * (1.0 - t) + t * l1;\n const C = t * c1;\n\n const l_ = L + C * kl;\n const m_ = L + C * km;\n const s_ = L + C * ks;\n\n const l = l_ * l_ * l_;\n const m = m_ * m_ * m_;\n const s = s_ * s_ * s_;\n\n const ldt = 3 * ldt_ * l_ * l_;\n const mdt = 3 * mdt_ * m_ * m_;\n const sdt = 3 * sdt_ * s_ * s_;\n\n const ldt2 = 6 * ldt_ * ldt_ * l_;\n const mdt2 = 6 * mdt_ * mdt_ * m_;\n const sdt2 = 6 * sdt_ * sdt_ * s_;\n\n const r_ = dotXYZ(lmsToRgb[0], l, m, s) - 1;\n const r1 = dotXYZ(lmsToRgb[0], ldt, mdt, sdt);\n const r2 = dotXYZ(lmsToRgb[0], ldt2, mdt2, sdt2);\n\n const ur = r1 / (r1 * r1 - 0.5 * r_ * r2);\n let tr = -r_ * ur;\n\n const g_ = dotXYZ(lmsToRgb[1], l, m, s) - 1;\n const g1 = dotXYZ(lmsToRgb[1], ldt, mdt, sdt);\n const g2 = dotXYZ(lmsToRgb[1], ldt2, mdt2, sdt2);\n\n const ug = g1 / (g1 * g1 - 0.5 * g_ * g2);\n let tg = -g_ * ug;\n\n const b_ = dotXYZ(lmsToRgb[2], l, m, s) - 1;\n const b1 = dotXYZ(lmsToRgb[2], ldt, mdt, sdt);\n const b2 = dotXYZ(lmsToRgb[2], ldt2, mdt2, sdt2);\n\n const ub = b1 / (b1 * b1 - 0.5 * b_ * b2);\n let tb = -b_ * ub;\n\n tr = ur >= 0.0 ? tr : floatMax;\n tg = ug >= 0.0 ? tg : floatMax;\n tb = ub >= 0.0 ? tb : floatMax;\n\n t += Math.min(tr, Math.min(tg, tb));\n }\n\n return t;\n};\n\nconst computeSt = (cusp: [number, number]): [number, number] => {\n const l = cusp[0];\n const c = cusp[1];\n return [c / l, c / (1 - l)];\n};\n\nconst computeStMid = (a: number, b: number): [number, number] => {\n const s =\n 0.11516993 +\n 1.0 /\n (7.4477897 +\n 4.1590124 * b +\n a *\n (-2.19557347 +\n 1.75198401 * b +\n a *\n (-2.13704948 -\n 10.02301043 * b +\n a * (-4.24894561 + 5.38770819 * b + 4.69891013 * a))));\n\n const t =\n 0.11239642 +\n 1.0 /\n (1.6132032 -\n 0.68124379 * b +\n a *\n (0.40370612 +\n 0.90148123 * b +\n a *\n (-0.27087943 +\n 0.6122399 * b +\n a * (0.00299215 - 0.45399568 * b - 0.14661872 * a))));\n\n return [s, t];\n};\n\nconst getCs = (\n L: number,\n a: number,\n b: number,\n cusp: [number, number],\n): [number, number, number] => {\n const cMax = findGamutIntersectionOKLCH(a, b, L, 1, L, cusp);\n const stMax = computeSt(cusp);\n\n const k = cMax / Math.min(L * stMax[0], (1 - L) * stMax[1]);\n\n const stMid = computeStMid(a, b);\n\n let ca = L * stMid[0];\n let cb = (1.0 - L) * stMid[1];\n const cMid =\n 0.9 * k * Math.sqrt(Math.sqrt(1.0 / (1.0 / ca ** 4 + 1.0 / cb ** 4)));\n\n ca = L * 0.4;\n cb = (1.0 - L) * 0.8;\n const c0 = Math.sqrt(1.0 / (1.0 / ca ** 2 + 1.0 / cb ** 2));\n\n return [c0, cMid, cMax];\n};\n\nconst OKHSLToOKLab = (hsl: Vec3): Vec3 => {\n let h = hsl[0];\n const s = hsl[1];\n const l = hsl[2];\n\n const L = toeInv(l);\n let a = 0;\n let b = 0;\n\n h = constrainAngle(h) / 360.0;\n\n if (L !== 0.0 && L !== 1.0 && s !== 0) {\n const a_ = Math.cos(TAU * h);\n const b_ = Math.sin(TAU * h);\n\n const cusp = findCuspOKLCH(a_, b_);\n const Cs = getCs(L, a_, b_, cusp);\n const [c0, cMid, cMax] = Cs;\n\n const mid = 0.8;\n const midInv = 1.25;\n let t: number, k0: number, k1: number, k2: number;\n\n if (s < mid) {\n t = midInv * s;\n k0 = 0.0;\n k1 = mid * c0;\n k2 = 1.0 - k1 / cMid;\n } else {\n t = 5 * (s - 0.8);\n k0 = cMid;\n k1 = (0.2 * cMid ** 2 * 1.25 ** 2) / c0;\n k2 = 1.0 - k1 / (cMax - cMid);\n }\n\n const c = k0 + (t * k1) / (1.0 - k2 * t);\n\n a = c * a_;\n b = c * b_;\n }\n\n return [L, a, b];\n};\n\n// ============================================================================\n// OKHSL to sRGB Conversion\n// ============================================================================\n\nconst okhslToSRGB = (h: number, s: number, l: number): Vec3 => {\n // h: 0-360, s: 0-1, l: 0-1\n const oklab = OKHSLToOKLab([h, s, l]);\n const linearRGB = OKLabToLinearSRGB(oklab);\n\n // Apply gamma correction and clamp\n return [\n clamp(sRGBLinearToGamma(linearRGB[0]), 0, 1),\n clamp(sRGBLinearToGamma(linearRGB[1]), 0, 1),\n clamp(sRGBLinearToGamma(linearRGB[2]), 0, 1),\n ];\n};\n\n// ============================================================================\n// Parsing Utilities\n// ============================================================================\n\n/**\n * Parse an angle value with optional unit.\n * Supports: deg, turn, rad, or unitless (treated as degrees).\n */\nconst parseAngle = (value: string): number => {\n const match = value.match(/^([+-]?\\d*\\.?\\d+)(deg|turn|rad)?$/);\n if (!match) return 0;\n\n const num = parseFloat(match[1]);\n const unit = match[2];\n\n switch (unit) {\n case 'turn':\n return num * 360;\n case 'rad':\n return (num * 180) / Math.PI;\n case 'deg':\n default:\n return num;\n }\n};\n\n/**\n * Parse a percentage value (e.g., \"50%\") to a 0-1 range.\n */\nconst parsePercentage = (value: string): number => {\n const match = value.match(/^([+-]?\\d*\\.?\\d+)%?$/);\n if (!match) return 0;\n\n const num = parseFloat(match[1]);\n // If the value includes %, divide by 100, otherwise assume 0-1 range\n return value.includes('%') ? num / 100 : num;\n};\n\n// ============================================================================\n// Plugin Implementation\n// ============================================================================\n\n/**\n * The okhsl function handler for tasty parser.\n * Receives parsed style groups and returns an RGB color string.\n */\nconst okhslFunc = (groups: StyleDetails[]): string => {\n // We expect a single group with 3 values: H, S, L\n // and an optional slash-separated alpha part\n if (groups.length === 0 || groups[0].all.length < 3) {\n console.warn('[okhsl] Expected 3 values (H S L), got:', groups);\n return 'rgb(0% 0% 0%)';\n }\n\n const group = groups[0];\n const tokens = group.all;\n\n // Alpha is in the second slash-separated part (e.g., okhsl(240 50% 50% / .5))\n const alpha =\n group.parts.length > 1 && group.parts[1].all.length > 0\n ? group.parts[1].output\n : undefined;\n\n // Create cache key from input tokens\n const cacheKey = tokens.slice(0, 3).join(' ') + (alpha ? ` / ${alpha}` : '');\n const cached = conversionCache.get(cacheKey);\n if (cached) return cached;\n\n const h = parseAngle(tokens[0]);\n const s = parsePercentage(tokens[1]);\n const l = parsePercentage(tokens[2]);\n\n const [r, g, b] = okhslToSRGB(h, clamp(s, 0, 1), clamp(l, 0, 1));\n\n const format = (n: number): string => {\n const pct = n * 100;\n return parseFloat(pct.toFixed(1)).toString() + '%';\n };\n\n const result = alpha\n ? `rgb(${format(r)} ${format(g)} ${format(b)} / ${alpha})`\n : `rgb(${format(r)} ${format(g)} ${format(b)})`;\n\n conversionCache.set(cacheKey, result);\n return result;\n};\n\n/**\n * OKHSL Plugin for Tasty.\n *\n * Adds support for the `okhsl()` color function in tasty styles.\n *\n * @example\n * ```ts\n * import { configure } from '@tenphi/tasty';\n * import { okhslPlugin } from '@tenphi/tasty';\n *\n * configure({\n * plugins: [okhslPlugin()],\n * });\n *\n * // Now you can use okhsl in styles:\n * const Box = tasty({\n * styles: {\n * fill: 'okhsl(240 50% 50%)',\n * },\n * });\n * ```\n */\nexport const okhslPlugin: TastyPluginFactory = (): TastyPlugin => ({\n name: 'okhsl',\n funcs: {\n okhsl: okhslFunc,\n },\n});\n\n// Export the raw function for direct use if needed\nexport { okhslFunc };\n"],"mappings":"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"}
1
+ {"version":3,"file":"okhsl-plugin.js","names":[],"sources":["../../src/plugins/okhsl-plugin.ts"],"sourcesContent":["/**\n * OKHSL Plugin for Tasty\n *\n * Converts OKHSL color syntax to RGB notation.\n * Supports angle units: deg, turn, rad, or unitless (degrees).\n *\n * Examples:\n * okhsl(240.5 50% 50%)\n * okhsl(240.5deg 50% 50%)\n * okhsl(0.25turn 50% 50%)\n * okhsl(1.57rad 50% 50%)\n */\n\nimport { Lru } from '../parser/lru';\nimport { okhslToSrgb } from '../utils/color-math';\n\nimport type { StyleDetails } from '../parser/types';\nimport type { TastyPlugin, TastyPluginFactory } from './types';\n\nconst conversionCache = new Lru<string, string>(500);\n\nconst clamp = (value: number, min: number, max: number): number =>\n Math.max(Math.min(value, max), min);\n\n/**\n * Parse an angle value with optional unit.\n * Supports: deg, turn, rad, or unitless (treated as degrees).\n */\nconst parseAngle = (value: string): number => {\n const match = value.match(/^([+-]?\\d*\\.?\\d+)(deg|turn|rad)?$/);\n if (!match) return 0;\n\n const num = parseFloat(match[1]);\n const unit = match[2];\n\n switch (unit) {\n case 'turn':\n return num * 360;\n case 'rad':\n return (num * 180) / Math.PI;\n case 'deg':\n default:\n return num;\n }\n};\n\n/**\n * Parse a percentage value (e.g., \"50%\") to a 0-1 range.\n */\nconst parsePercentage = (value: string): number => {\n const match = value.match(/^([+-]?\\d*\\.?\\d+)%?$/);\n if (!match) return 0;\n\n const num = parseFloat(match[1]);\n return value.includes('%') ? num / 100 : num;\n};\n\n/**\n * The okhsl function handler for tasty parser.\n * Receives parsed style groups and returns an RGB color string.\n */\nconst okhslFunc = (groups: StyleDetails[]): string => {\n if (groups.length === 0 || groups[0].all.length < 3) {\n console.warn('[okhsl] Expected 3 values (H S L), got:', groups);\n return 'rgb(0% 0% 0%)';\n }\n\n const group = groups[0];\n const tokens = group.all;\n\n const alpha =\n group.parts.length > 1 && group.parts[1].all.length > 0\n ? group.parts[1].output\n : undefined;\n\n const cacheKey = tokens.slice(0, 3).join(' ') + (alpha ? ` / ${alpha}` : '');\n const cached = conversionCache.get(cacheKey);\n if (cached) return cached;\n\n const h = parseAngle(tokens[0]);\n const s = parsePercentage(tokens[1]);\n const l = parsePercentage(tokens[2]);\n\n const [r, g, b] = okhslToSrgb(h, clamp(s, 0, 1), clamp(l, 0, 1));\n\n const format = (n: number): string => {\n const pct = n * 100;\n return parseFloat(pct.toFixed(1)).toString() + '%';\n };\n\n const result = alpha\n ? `rgb(${format(r)} ${format(g)} ${format(b)} / ${alpha})`\n : `rgb(${format(r)} ${format(g)} ${format(b)})`;\n\n conversionCache.set(cacheKey, result);\n return result;\n};\n\n/**\n * OKHSL Plugin for Tasty.\n *\n * Adds support for the `okhsl()` color function in tasty styles.\n *\n * @example\n * ```ts\n * import { configure } from '@tenphi/tasty';\n * import { okhslPlugin } from '@tenphi/tasty';\n *\n * configure({\n * plugins: [okhslPlugin()],\n * });\n *\n * // Now you can use okhsl in styles:\n * const Box = tasty({\n * styles: {\n * fill: 'okhsl(240 50% 50%)',\n * },\n * });\n * ```\n */\nexport const okhslPlugin: TastyPluginFactory = (): TastyPlugin => ({\n name: 'okhsl',\n funcs: {\n okhsl: okhslFunc,\n },\n});\n\nexport { okhslFunc };\n"],"mappings":";;;;;;;;;;;;;;;;AAmBA,MAAM,kBAAkB,IAAI,IAAoB,IAAI;AAEpD,MAAM,SAAS,OAAe,KAAa,QACzC,KAAK,IAAI,KAAK,IAAI,OAAO,IAAI,EAAE,IAAI;;;;;AAMrC,MAAM,cAAc,UAA0B;CAC5C,MAAM,QAAQ,MAAM,MAAM,oCAAoC;AAC9D,KAAI,CAAC,MAAO,QAAO;CAEnB,MAAM,MAAM,WAAW,MAAM,GAAG;AAGhC,SAFa,MAAM,IAEnB;EACE,KAAK,OACH,QAAO,MAAM;EACf,KAAK,MACH,QAAQ,MAAM,MAAO,KAAK;EAE5B,QACE,QAAO;;;;;;AAOb,MAAM,mBAAmB,UAA0B;CACjD,MAAM,QAAQ,MAAM,MAAM,uBAAuB;AACjD,KAAI,CAAC,MAAO,QAAO;CAEnB,MAAM,MAAM,WAAW,MAAM,GAAG;AAChC,QAAO,MAAM,SAAS,IAAI,GAAG,MAAM,MAAM;;;;;;AAO3C,MAAM,aAAa,WAAmC;AACpD,KAAI,OAAO,WAAW,KAAK,OAAO,GAAG,IAAI,SAAS,GAAG;AACnD,UAAQ,KAAK,2CAA2C,OAAO;AAC/D,SAAO;;CAGT,MAAM,QAAQ,OAAO;CACrB,MAAM,SAAS,MAAM;CAErB,MAAM,QACJ,MAAM,MAAM,SAAS,KAAK,MAAM,MAAM,GAAG,IAAI,SAAS,IAClD,MAAM,MAAM,GAAG,SACf;CAEN,MAAM,WAAW,OAAO,MAAM,GAAG,EAAE,CAAC,KAAK,IAAI,IAAI,QAAQ,MAAM,UAAU;CACzE,MAAM,SAAS,gBAAgB,IAAI,SAAS;AAC5C,KAAI,OAAQ,QAAO;CAEnB,MAAM,IAAI,WAAW,OAAO,GAAG;CAC/B,MAAM,IAAI,gBAAgB,OAAO,GAAG;CACpC,MAAM,IAAI,gBAAgB,OAAO,GAAG;CAEpC,MAAM,CAAC,GAAG,GAAG,KAAK,YAAY,GAAG,MAAM,GAAG,GAAG,EAAE,EAAE,MAAM,GAAG,GAAG,EAAE,CAAC;CAEhE,MAAM,UAAU,MAAsB;EACpC,MAAM,MAAM,IAAI;AAChB,SAAO,WAAW,IAAI,QAAQ,EAAE,CAAC,CAAC,UAAU,GAAG;;CAGjD,MAAM,SAAS,QACX,OAAO,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,KAAK,MAAM,KACtD,OAAO,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC;AAE/C,iBAAgB,IAAI,UAAU,OAAO;AACrC,QAAO;;;;;;;;;;;;;;;;;;;;;;;;AAyBT,MAAa,qBAAsD;CACjE,MAAM;CACN,OAAO,EACL,OAAO,WACR;CACF"}
@@ -1,6 +1,6 @@
1
1
  import { StyleDetails, UnitHandler } from "../parser/types.js";
2
2
  import { StyleHandlerDefinition } from "../utils/styles.js";
3
- import { RecipeStyles } from "../styles/types.js";
3
+ import { ConfigTokens, RecipeStyles } from "../styles/types.js";
4
4
 
5
5
  //#region src/plugins/types.d.ts
6
6
  /**
@@ -29,8 +29,15 @@ interface TastyPlugin {
29
29
  * ```
30
30
  */
31
31
  handlers?: Record<string, StyleHandlerDefinition>;
32
+ /**
33
+ * Design tokens injected as CSS custom properties on `:root`.
34
+ * Values are parsed through the Tasty DSL. Supports state maps.
35
+ * - `$name` → `--name` CSS custom property
36
+ * - `#name` → `--name-color` and `--name-color-{colorSpace}`
37
+ */
38
+ tokens?: ConfigTokens;
32
39
  /** Predefined tokens replaced during style parsing (`$name` or `#name`) */
33
- tokens?: Record<`$${string}` | `#${string}`, string | number>;
40
+ replaceTokens?: Record<`$${string}` | `#${string}`, string | number>;
34
41
  /**
35
42
  * Predefined style recipes -- named style bundles that can be applied via `recipe` style property.
36
43
  * Recipe values are flat tasty styles (no sub-element keys).
@@ -1,5 +1,5 @@
1
1
  import { RE_NUMBER, RE_RAW_UNIT } from "../parser/const.js";
2
- import { getRgbValuesFromRgbaString, strToRgb } from "../utils/styles.js";
2
+ import { colorInitialValueToComponents } from "../utils/color-space.js";
3
3
 
4
4
  //#region src/properties/index.ts
5
5
  const PROPERTIES_KEY = "@properties";
@@ -138,24 +138,6 @@ function getEffectiveDefinition(token, userDefinition) {
138
138
  isValid: true
139
139
  };
140
140
  }
141
- /**
142
- * Extract RGB triplet string from a color initial value.
143
- * Used when auto-creating the companion `-rgb` property for color @property definitions.
144
- *
145
- * @param initialValue - The color property's initial value (e.g., 'rgb(255 255 255)', 'transparent', '#fff')
146
- * @returns Space-separated RGB triplet (e.g., '255 255 255'), defaults to '0 0 0'
147
- */
148
- function colorInitialValueToRgb(initialValue) {
149
- if (initialValue == null) return "0 0 0";
150
- const str = String(initialValue).trim();
151
- if (!str || str === "transparent") return "0 0 0";
152
- const rgba = strToRgb(str);
153
- if (rgba) {
154
- const values = getRgbValuesFromRgbaString(rgba);
155
- if (values.length >= 3) return values.slice(0, 3).join(" ");
156
- }
157
- return "0 0 0";
158
- }
159
141
  const UNIT_TO_SYNTAX = {};
160
142
  const LENGTH_UNITS = [
161
143
  "px",
@@ -192,12 +174,12 @@ const ANGLE_UNITS = [
192
174
  ];
193
175
  const TIME_UNITS = ["ms", "s"];
194
176
  for (const u of LENGTH_UNITS) UNIT_TO_SYNTAX[u] = {
195
- syntax: "<length>",
177
+ syntax: "<length-percentage>",
196
178
  initialValue: "0px"
197
179
  };
198
180
  UNIT_TO_SYNTAX["%"] = {
199
- syntax: "<percentage>",
200
- initialValue: "0%"
181
+ syntax: "<length-percentage>",
182
+ initialValue: "0px"
201
183
  };
202
184
  for (const u of ANGLE_UNITS) UNIT_TO_SYNTAX[u] = {
203
185
  syntax: "<angle>",
@@ -209,7 +191,8 @@ for (const u of TIME_UNITS) UNIT_TO_SYNTAX[u] = {
209
191
  };
210
192
  /**
211
193
  * Infer CSS @property syntax from a concrete value.
212
- * Only detects numeric types: \<number\>, \<length\>, \<percentage\>, \<angle\>, \<time\>.
194
+ * Detects numeric types: \<number\>, \<length-percentage\>, \<angle\>, \<time\>.
195
+ * Length and percentage values both map to \<length-percentage\> for maximum flexibility.
213
196
  * Color properties are handled separately via the #name token convention
214
197
  * (--name-color gets \<color\> syntax automatically in getEffectiveDefinition).
215
198
  *
@@ -220,10 +203,13 @@ function inferSyntaxFromValue(value) {
220
203
  if (!value || typeof value !== "string") return null;
221
204
  const trimmed = value.trim();
222
205
  if (!trimmed) return null;
223
- if (RE_NUMBER.test(trimmed)) return {
224
- syntax: "<number>",
225
- initialValue: "0"
226
- };
206
+ if (RE_NUMBER.test(trimmed)) {
207
+ if (parseFloat(trimmed) === 0) return null;
208
+ return {
209
+ syntax: "<number>",
210
+ initialValue: "0"
211
+ };
212
+ }
227
213
  const unitMatch = trimmed.match(RE_RAW_UNIT);
228
214
  if (unitMatch) {
229
215
  const mapping = UNIT_TO_SYNTAX[unitMatch[2]];
@@ -233,5 +219,5 @@ function inferSyntaxFromValue(value) {
233
219
  }
234
220
 
235
221
  //#endregion
236
- export { colorInitialValueToRgb, extractLocalProperties, getEffectiveDefinition, hasLocalProperties, inferSyntaxFromValue, normalizePropertyDefinition };
222
+ export { extractLocalProperties, getEffectiveDefinition, hasLocalProperties, inferSyntaxFromValue, normalizePropertyDefinition, parsePropertyToken };
237
223
  //# sourceMappingURL=index.js.map