@usefragments/core 1.0.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 (228) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-243QYRUF.js +2145 -0
  3. package/dist/chunk-243QYRUF.js.map +1 -0
  4. package/dist/chunk-3LLRNCPX.js +111 -0
  5. package/dist/chunk-3LLRNCPX.js.map +1 -0
  6. package/dist/chunk-EIYNNS77.js +48 -0
  7. package/dist/chunk-EIYNNS77.js.map +1 -0
  8. package/dist/chunk-JNBFJ34I.js +326 -0
  9. package/dist/chunk-JNBFJ34I.js.map +1 -0
  10. package/dist/chunk-R3X7UBHU.js +228 -0
  11. package/dist/chunk-R3X7UBHU.js.map +1 -0
  12. package/dist/chunk-SH4KPIYH.js +393 -0
  13. package/dist/chunk-SH4KPIYH.js.map +1 -0
  14. package/dist/chunk-V4VQB57N.js +56 -0
  15. package/dist/chunk-V4VQB57N.js.map +1 -0
  16. package/dist/chunk-X34IA4LR.js +319 -0
  17. package/dist/chunk-X34IA4LR.js.map +1 -0
  18. package/dist/chunk-YF65VYRY.js +493 -0
  19. package/dist/chunk-YF65VYRY.js.map +1 -0
  20. package/dist/codes/index.d.ts +40 -0
  21. package/dist/codes/index.js +23 -0
  22. package/dist/codes/index.js.map +1 -0
  23. package/dist/compiled-types/index.d.ts +5 -0
  24. package/dist/compiled-types/index.js +1 -0
  25. package/dist/compiled-types/index.js.map +1 -0
  26. package/dist/generate/index.d.ts +47 -0
  27. package/dist/generate/index.js +11 -0
  28. package/dist/generate/index.js.map +1 -0
  29. package/dist/governance-BOa3KyiJ.d.ts +3279 -0
  30. package/dist/graph/index.d.ts +66 -0
  31. package/dist/graph/index.js +17 -0
  32. package/dist/graph/index.js.map +1 -0
  33. package/dist/index.d.ts +11388 -0
  34. package/dist/index.js +8711 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/package-identity.d.ts +42 -0
  37. package/dist/package-identity.js +23 -0
  38. package/dist/package-identity.js.map +1 -0
  39. package/dist/preview/index.d.ts +484 -0
  40. package/dist/preview/index.js +1129 -0
  41. package/dist/preview/index.js.map +1 -0
  42. package/dist/react-types.d.ts +22 -0
  43. package/dist/react-types.js +1 -0
  44. package/dist/react-types.js.map +1 -0
  45. package/dist/registry.d.ts +2367 -0
  46. package/dist/registry.js +62 -0
  47. package/dist/registry.js.map +1 -0
  48. package/dist/schemas/index.d.ts +2584 -0
  49. package/dist/schemas/index.js +55 -0
  50. package/dist/schemas/index.js.map +1 -0
  51. package/dist/severity.d.ts +18 -0
  52. package/dist/severity.js +27 -0
  53. package/dist/severity.js.map +1 -0
  54. package/dist/test-utils.d.ts +14 -0
  55. package/dist/test-utils.js +63 -0
  56. package/dist/test-utils.js.map +1 -0
  57. package/dist/topology/index.d.ts +48 -0
  58. package/dist/topology/index.js +7 -0
  59. package/dist/topology/index.js.map +1 -0
  60. package/dist/types-xJ2xyp_G.d.ts +119 -0
  61. package/package.json +120 -0
  62. package/src/__tests__/contract-parser.test.ts +614 -0
  63. package/src/__tests__/dtcg-generators.test.ts +439 -0
  64. package/src/__tests__/dtcg-parser.test.ts +884 -0
  65. package/src/__tests__/preview-runtime.test.tsx +111 -0
  66. package/src/__tests__/schema.test.ts +31 -0
  67. package/src/__tests__/severity.test.ts +34 -0
  68. package/src/__tests__/style-comparison.test.ts +662 -0
  69. package/src/__tests__/tokens-compat.test.ts +51 -0
  70. package/src/agent-format.test.ts +105 -0
  71. package/src/agent-format.ts +141 -0
  72. package/src/bundle.ts +149 -0
  73. package/src/codes/__tests__/codes.test.ts +73 -0
  74. package/src/codes/codes.ts +463 -0
  75. package/src/codes/index.ts +7 -0
  76. package/src/codes/source-finding-bridge.ts +113 -0
  77. package/src/compiled-types/index.ts +293 -0
  78. package/src/component-contract.ts +231 -0
  79. package/src/component-discovery.ts +80 -0
  80. package/src/composition.test.ts +262 -0
  81. package/src/composition.ts +318 -0
  82. package/src/config-paths.test.ts +78 -0
  83. package/src/config-paths.ts +47 -0
  84. package/src/config.ts +18 -0
  85. package/src/conform-prove.test.ts +274 -0
  86. package/src/conform-prove.ts +326 -0
  87. package/src/conform.ts +160 -0
  88. package/src/constants.ts +114 -0
  89. package/src/context.ts +2 -0
  90. package/src/contract/fixture.ts +110 -0
  91. package/src/contract/hash.test.ts +86 -0
  92. package/src/contract/hash.ts +195 -0
  93. package/src/contract/index.ts +43 -0
  94. package/src/contract/preimage.test.ts +254 -0
  95. package/src/contract/preimage.ts +365 -0
  96. package/src/contract/stamp.test.ts +100 -0
  97. package/src/contract/stamp.ts +105 -0
  98. package/src/contract-parser.ts +141 -0
  99. package/src/defineFragment.test.ts +257 -0
  100. package/src/defineFragment.ts +293 -0
  101. package/src/dtcg-generators.ts +324 -0
  102. package/src/dtcg-parser.ts +1 -0
  103. package/src/dtcg.ts +237 -0
  104. package/src/facts/__snapshots__/facts.test.ts.snap +124 -0
  105. package/src/facts/builders.ts +835 -0
  106. package/src/facts/compile.ts +422 -0
  107. package/src/facts/fact-index.ts +394 -0
  108. package/src/facts/facts.test.ts +494 -0
  109. package/src/facts/ids.ts +125 -0
  110. package/src/facts/index.ts +119 -0
  111. package/src/facts/types.ts +614 -0
  112. package/src/figma.ts +263 -0
  113. package/src/fragment-types.ts +214 -0
  114. package/src/generate/generate.test.ts +594 -0
  115. package/src/generate/index.ts +438 -0
  116. package/src/governance-integrity.test.ts +186 -0
  117. package/src/governance-integrity.ts +282 -0
  118. package/src/governance.test.ts +300 -0
  119. package/src/governance.ts +605 -0
  120. package/src/governance.type-test.ts +75 -0
  121. package/src/graph/engine.ts +446 -0
  122. package/src/graph/graph.test.ts +746 -0
  123. package/src/graph/index.ts +32 -0
  124. package/src/graph/serialization.ts +55 -0
  125. package/src/graph/types.ts +178 -0
  126. package/src/index.ts +891 -0
  127. package/src/local-canonical.ts +102 -0
  128. package/src/mcp-snapshot.ts +222 -0
  129. package/src/package-identity-match.ts +33 -0
  130. package/src/package-identity.generated.ts +233 -0
  131. package/src/package-identity.test.ts +99 -0
  132. package/src/package-identity.ts +157 -0
  133. package/src/performance-presets.ts +142 -0
  134. package/src/preview/comparison-artifact.test.ts +228 -0
  135. package/src/preview/comparison-artifact.ts +181 -0
  136. package/src/preview/comparison-context.ts +93 -0
  137. package/src/preview/comparison-evidence.ts +112 -0
  138. package/src/preview/comparison-format.ts +21 -0
  139. package/src/preview/comparison-presentation.ts +187 -0
  140. package/src/preview/comparison-references.test.ts +430 -0
  141. package/src/preview/comparison-references.ts +72 -0
  142. package/src/preview/comparison-score.ts +59 -0
  143. package/src/preview/comparison-styles.ts +197 -0
  144. package/src/preview/comparison.test.ts +559 -0
  145. package/src/preview/comparison.ts +488 -0
  146. package/src/preview/index.ts +10 -0
  147. package/src/preview/types.ts +309 -0
  148. package/src/preview/validation.test.ts +290 -0
  149. package/src/preview/validation.ts +356 -0
  150. package/src/preview-runtime.tsx +144 -0
  151. package/src/raw-html-canonical.test.ts +41 -0
  152. package/src/raw-html-canonical.ts +179 -0
  153. package/src/react-types.ts +21 -0
  154. package/src/registry-install-plan.ts +344 -0
  155. package/src/registry.test.ts +245 -0
  156. package/src/registry.ts +369 -0
  157. package/src/rules/a11y-required-accessible-name.ts +93 -0
  158. package/src/rules/color-math.test.ts +72 -0
  159. package/src/rules/color-math.ts +192 -0
  160. package/src/rules/components-forbidden-prop-value.ts +108 -0
  161. package/src/rules/components-prefer-library.ts +943 -0
  162. package/src/rules/components-unknown-prop.ts +94 -0
  163. package/src/rules/composition-pattern-schema.ts +125 -0
  164. package/src/rules/composition-pattern.test.ts +320 -0
  165. package/src/rules/composition-pattern.ts +271 -0
  166. package/src/rules/emit-gate.test.ts +42 -0
  167. package/src/rules/emit-gate.ts +60 -0
  168. package/src/rules/finding.ts +54 -0
  169. package/src/rules/index.ts +235 -0
  170. package/src/rules/jsx-preferred-component.ts +68 -0
  171. package/src/rules/jsx-preferred-import-path.ts +67 -0
  172. package/src/rules/presets.ts +57 -0
  173. package/src/rules/props-invalid-value.ts +115 -0
  174. package/src/rules/rules.test.ts +3636 -0
  175. package/src/rules/spacing-resolution.ts +184 -0
  176. package/src/rules/styles-no-raw-color.ts +329 -0
  177. package/src/rules/styles-no-raw-dimensions.ts +357 -0
  178. package/src/rules/styles-no-raw-spacing.ts +236 -0
  179. package/src/rules/styles-no-raw-typography.ts +237 -0
  180. package/src/rules/tailwind-arbitrary-color.test.ts +128 -0
  181. package/src/rules/tailwind-arbitrary-color.ts +93 -0
  182. package/src/rules/tailwind-arbitrary-spacing.test.ts +157 -0
  183. package/src/rules/tailwind-arbitrary-spacing.ts +120 -0
  184. package/src/rules/tailwind-forbidden-palette.test.ts +116 -0
  185. package/src/rules/tailwind-forbidden-palette.ts +107 -0
  186. package/src/rules/tailwind-off-scale-spacing-token.test.ts +100 -0
  187. package/src/rules/tailwind-off-scale-spacing-token.ts +108 -0
  188. package/src/rules/tailwind-raw-color-via-token.test.ts +108 -0
  189. package/src/rules/tailwind-raw-color-via-token.ts +82 -0
  190. package/src/rules/tailwind-rule-utils.ts +106 -0
  191. package/src/rules/tailwind-unknown-class.test.ts +157 -0
  192. package/src/rules/tailwind-unknown-class.ts +121 -0
  193. package/src/rules/taxonomy.test.ts +238 -0
  194. package/src/rules/theme-no-theme-coupled-literal.ts +132 -0
  195. package/src/rules/tiers.ts +111 -0
  196. package/src/rules/tokens-css-vars-must-be-defined.test.ts +244 -0
  197. package/src/rules/tokens-css-vars-must-be-defined.ts +156 -0
  198. package/src/rules/tokens-require-dual-fallback.ts +78 -0
  199. package/src/rules/types.ts +27 -0
  200. package/src/rules/utils.test.ts +110 -0
  201. package/src/rules/utils.ts +599 -0
  202. package/src/schema.ts +427 -0
  203. package/src/schemas/index.ts +275 -0
  204. package/src/schemas/schemas.test.ts +124 -0
  205. package/src/severity.ts +54 -0
  206. package/src/storyAdapter.test.ts +571 -0
  207. package/src/storyAdapter.ts +761 -0
  208. package/src/storyFilters.test.ts +350 -0
  209. package/src/storyFilters.ts +253 -0
  210. package/src/storybook-csf.ts +11 -0
  211. package/src/style-comparison.ts +423 -0
  212. package/src/test-utils.ts +68 -0
  213. package/src/token-parser.ts +2 -0
  214. package/src/token-types.ts +287 -0
  215. package/src/tokens/categories.ts +131 -0
  216. package/src/tokens/color.ts +193 -0
  217. package/src/tokens/design-token-parser.test.ts +159 -0
  218. package/src/tokens/design-token-parser.ts +402 -0
  219. package/src/tokens/dtcg.ts +428 -0
  220. package/src/tokens/index.ts +130 -0
  221. package/src/tokens/lookup.ts +19 -0
  222. package/src/tokens/scss.ts +171 -0
  223. package/src/tokens/tailwind-v4.ts +187 -0
  224. package/src/tokens/types.ts +43 -0
  225. package/src/topology/index.ts +9 -0
  226. package/src/topology/resolve-area.test.ts +174 -0
  227. package/src/topology/resolve-area.ts +215 -0
  228. package/src/types.ts +1057 -0
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { colorDistance, parseOpaqueColor } from "./color-math.js";
4
+
5
+ describe("parseOpaqueColor", () => {
6
+ it("parses hex in 3, 6, and opaque 4/8 digit forms", () => {
7
+ expect(parseOpaqueColor("#25f")).toEqual({ r: 0x22, g: 0x55, b: 0xff });
8
+ expect(parseOpaqueColor("#2563eb")).toEqual({ r: 0x25, g: 0x63, b: 0xeb });
9
+ expect(parseOpaqueColor("#2563ebff")).toEqual({ r: 0x25, g: 0x63, b: 0xeb });
10
+ expect(parseOpaqueColor("#25ff")).toEqual({ r: 0x22, g: 0x55, b: 0xff });
11
+ });
12
+
13
+ it("rejects translucent values in every notation", () => {
14
+ expect(parseOpaqueColor("#2563eb80")).toBeNull();
15
+ expect(parseOpaqueColor("rgba(37, 99, 235, 0.5)")).toBeNull();
16
+ expect(parseOpaqueColor("rgb(37 99 235 / 0.5)")).toBeNull();
17
+ expect(parseOpaqueColor("hsl(217 91% 60% / 50%)")).toBeNull();
18
+ expect(parseOpaqueColor("oklch(0.55 0.24 263 / 0.8)")).toBeNull();
19
+ });
20
+
21
+ it("parses rgb()/rgba() in comma and space syntax, with percentages", () => {
22
+ expect(parseOpaqueColor("rgb(37, 99, 235)")).toEqual({ r: 37, g: 99, b: 235 });
23
+ expect(parseOpaqueColor("rgba(37, 99, 235, 1)")).toEqual({ r: 37, g: 99, b: 235 });
24
+ expect(parseOpaqueColor("rgb(37 99 235)")).toEqual({ r: 37, g: 99, b: 235 });
25
+ expect(parseOpaqueColor("rgb(37 99 235 / 100%)")).toEqual({ r: 37, g: 99, b: 235 });
26
+ expect(parseOpaqueColor("rgb(100% 0% 50%)")).toEqual({ r: 255, g: 0, b: 128 });
27
+ });
28
+
29
+ it("parses hsl()/hsla() to the same color as its hex equivalent", () => {
30
+ // Tailwind blue-500: #3b82f6 ≈ hsl(217, 91%, 60%)
31
+ const fromHsl = parseOpaqueColor("hsl(217, 91%, 60%)");
32
+ const fromHex = parseOpaqueColor("#3b82f6");
33
+ expect(fromHsl).not.toBeNull();
34
+ expect(colorDistance(fromHsl!, fromHex!)).toBeLessThan(10);
35
+ expect(parseOpaqueColor("hsla(217, 91%, 60%, 1)")).toEqual(fromHsl);
36
+ expect(parseOpaqueColor("hsl(217deg 91% 60%)")).toEqual(fromHsl);
37
+ });
38
+
39
+ it("parses oklch() to the same color as its hex equivalent", () => {
40
+ // #2563eb = oklch(0.54615 0.215208 262.881) (exact sRGB→OKLCH conversion)
41
+ const fromOklch = parseOpaqueColor("oklch(0.54615 0.215208 262.881)");
42
+ const fromHex = parseOpaqueColor("#2563eb");
43
+ expect(fromOklch).not.toBeNull();
44
+ expect(colorDistance(fromOklch!, fromHex!)).toBeLessThan(5);
45
+ // Percentage lightness form.
46
+ expect(parseOpaqueColor("oklch(54.615% 0.215208 262.881)")).toEqual(fromOklch);
47
+ // Published reference: sRGB red is oklch(0.62796 0.25768 29.234).
48
+ const red = parseOpaqueColor("oklch(0.62796 0.25768 29.234)");
49
+ expect(colorDistance(red!, parseOpaqueColor("#ff0000")!)).toBeLessThan(5);
50
+ });
51
+
52
+ it("returns null for unsupported notations", () => {
53
+ expect(parseOpaqueColor("rebeccapurple")).toBeNull();
54
+ expect(parseOpaqueColor("lab(52% 40 59)")).toBeNull();
55
+ expect(parseOpaqueColor("color(display-p3 1 0 0)")).toBeNull();
56
+ expect(parseOpaqueColor("var(--fui-color-accent)")).toBeNull();
57
+ });
58
+ });
59
+
60
+ describe("colorDistance", () => {
61
+ it("is zero for identical colors and small for same-family shades", () => {
62
+ const accent = parseOpaqueColor("#7c3aed")!;
63
+ expect(colorDistance(accent, accent)).toBe(0);
64
+ expect(colorDistance(accent, parseOpaqueColor("#7c3acc")!)).toBeLessThan(60);
65
+ });
66
+
67
+ it("is large across hue families", () => {
68
+ const blue = parseOpaqueColor("#2563eb")!;
69
+ const red = parseOpaqueColor("#dc2626")!;
70
+ expect(colorDistance(blue, red)).toBeGreaterThan(200);
71
+ });
72
+ });
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Color parsing + perceptual distance for nearest-token suggestions.
3
+ *
4
+ * Every supported notation normalizes to 8-bit sRGB before comparison, so a
5
+ * raw `#7c3acc` can match a token authored as `oklch(0.54 0.2 295)` and vice
6
+ * versa. Only fully-opaque colors parse — a nearest suggestion that changes
7
+ * opacity is worse than no suggestion. Unsupported notations (named colors,
8
+ * lab/lch, color()) return null and simply get no nearest hint.
9
+ */
10
+
11
+ export interface Rgb {
12
+ r: number;
13
+ g: number;
14
+ b: number;
15
+ }
16
+
17
+ /** Parse a fully-opaque CSS color literal (hex, rgb/rgba, hsl/hsla, oklch) to sRGB. */
18
+ export function parseOpaqueColor(value: string): Rgb | null {
19
+ const lower = value.trim().toLowerCase();
20
+ return (
21
+ parseHex(lower) ?? parseRgbFunction(lower) ?? parseHslFunction(lower) ?? parseOklch(lower)
22
+ );
23
+ }
24
+
25
+ /**
26
+ * Redmean — a cheap perceptually-weighted RGB distance (range 0–~765).
27
+ * Good enough to separate "same color family, different shade" from
28
+ * "different hue" without a full CIE pipeline.
29
+ */
30
+ export function colorDistance(a: Rgb, b: Rgb): number {
31
+ const rMean = (a.r + b.r) / 2;
32
+ const dr = a.r - b.r;
33
+ const dg = a.g - b.g;
34
+ const db = a.b - b.b;
35
+ return Math.sqrt(
36
+ (2 + rMean / 256) * dr * dr + 4 * dg * dg + (2 + (255 - rMean) / 256) * db * db
37
+ );
38
+ }
39
+
40
+ function parseHex(lower: string): Rgb | null {
41
+ const hex = /^#([0-9a-f]{3,8})$/.exec(lower)?.[1];
42
+ if (!hex) return null;
43
+ if (hex.length === 3 || hex.length === 4) {
44
+ if (hex.length === 4 && hex[3] !== "f") return null;
45
+ return {
46
+ r: parseInt(hex[0] + hex[0], 16),
47
+ g: parseInt(hex[1] + hex[1], 16),
48
+ b: parseInt(hex[2] + hex[2], 16),
49
+ };
50
+ }
51
+ if (hex.length === 6 || hex.length === 8) {
52
+ if (hex.length === 8 && hex.slice(6) !== "ff") return null;
53
+ return {
54
+ r: parseInt(hex.slice(0, 2), 16),
55
+ g: parseInt(hex.slice(2, 4), 16),
56
+ b: parseInt(hex.slice(4, 6), 16),
57
+ };
58
+ }
59
+ return null;
60
+ }
61
+
62
+ /** A number with an optional `%` or `deg` suffix, used by every function notation. */
63
+ const COMPONENT = String.raw`[+-]?(?:\d+\.?\d*|\.\d+)(?:%|deg)?`;
64
+
65
+ /**
66
+ * Split a CSS color function's arguments, accepting both legacy comma syntax
67
+ * (`rgb(1, 2, 3)`, `rgba(1, 2, 3, 1)`) and modern space syntax with an
68
+ * optional `/ alpha` (`rgb(1 2 3 / 1)`). Returns the channel components plus
69
+ * the alpha component (null when omitted).
70
+ */
71
+ function splitColorArgs(
72
+ lower: string,
73
+ fn: string
74
+ ): { components: string[]; alpha: string | null } | null {
75
+ const match = new RegExp(`^${fn}a?\\(([^)]+)\\)$`).exec(lower);
76
+ if (!match) return null;
77
+ const body = match[1].trim();
78
+
79
+ let channelPart = body;
80
+ let alpha: string | null = null;
81
+ if (body.includes("/")) {
82
+ const [channels, alphaPart, ...rest] = body.split("/");
83
+ if (rest.length > 0) return null;
84
+ channelPart = channels.trim();
85
+ alpha = alphaPart.trim();
86
+ }
87
+
88
+ const components = channelPart.includes(",")
89
+ ? channelPart.split(",").map((part) => part.trim())
90
+ : channelPart.split(/\s+/);
91
+ if (alpha === null && components.length === 4) {
92
+ alpha = components.pop() ?? null;
93
+ }
94
+ if (components.length !== 3) return null;
95
+ if (!components.every((part) => new RegExp(`^${COMPONENT}$`).test(part))) return null;
96
+ if (alpha !== null && !new RegExp(`^${COMPONENT}$`).test(alpha)) return null;
97
+ return { components, alpha };
98
+ }
99
+
100
+ /** True when an alpha component is omitted or fully opaque (`1`, `1.0`, `100%`). */
101
+ function isOpaqueAlpha(alpha: string | null): boolean {
102
+ if (alpha === null) return true;
103
+ const value = alpha.endsWith("%") ? Number(alpha.slice(0, -1)) / 100 : Number(alpha);
104
+ return value === 1;
105
+ }
106
+
107
+ function numberOrPercent(part: string, percentScale: number): number {
108
+ return part.endsWith("%") ? (Number(part.slice(0, -1)) / 100) * percentScale : Number(part);
109
+ }
110
+
111
+ function clampByte(value: number): number {
112
+ return Math.min(255, Math.max(0, Math.round(value)));
113
+ }
114
+
115
+ function parseRgbFunction(lower: string): Rgb | null {
116
+ const args = splitColorArgs(lower, "rgb");
117
+ if (!args || !isOpaqueAlpha(args.alpha)) return null;
118
+ const [r, g, b] = args.components.map((part) => numberOrPercent(part, 255));
119
+ if ([r, g, b].some((channel) => Number.isNaN(channel) || channel < 0 || channel > 255)) {
120
+ return null;
121
+ }
122
+ return { r: Math.round(r), g: Math.round(g), b: Math.round(b) };
123
+ }
124
+
125
+ function parseHslFunction(lower: string): Rgb | null {
126
+ const args = splitColorArgs(lower, "hsl");
127
+ if (!args || !isOpaqueAlpha(args.alpha)) return null;
128
+ const h = Number(args.components[0].replace(/deg$/, ""));
129
+ const s = numberOrPercent(args.components[1], 1);
130
+ const l = numberOrPercent(args.components[2], 1);
131
+ if ([h, s, l].some(Number.isNaN) || s < 0 || s > 1 || l < 0 || l > 1) return null;
132
+
133
+ const hue = ((h % 360) + 360) % 360;
134
+ const c = (1 - Math.abs(2 * l - 1)) * s;
135
+ const x = c * (1 - Math.abs(((hue / 60) % 2) - 1));
136
+ const m = l - c / 2;
137
+ const sector = Math.floor(hue / 60);
138
+ const [r1, g1, b1] = [
139
+ [c, x, 0],
140
+ [x, c, 0],
141
+ [0, c, x],
142
+ [0, x, c],
143
+ [x, 0, c],
144
+ [c, 0, x],
145
+ ][sector] ?? [0, 0, 0];
146
+ return {
147
+ r: clampByte((r1 + m) * 255),
148
+ g: clampByte((g1 + m) * 255),
149
+ b: clampByte((b1 + m) * 255),
150
+ };
151
+ }
152
+
153
+ /**
154
+ * oklch(L C H [/ alpha]) → sRGB via OKLab. L accepts 0–1 or a percentage; C
155
+ * accepts a number or a percentage of 0.4 (per spec); H is degrees. Out-of-gamut
156
+ * results clamp to sRGB — fine for nearness, which only needs "is this the same
157
+ * color family", not colorimetric fidelity.
158
+ */
159
+ function parseOklch(lower: string): Rgb | null {
160
+ const args = splitColorArgs(lower, "oklch");
161
+ if (!args || !isOpaqueAlpha(args.alpha)) return null;
162
+ const l = numberOrPercent(args.components[0], 1);
163
+ const c = numberOrPercent(args.components[1], 0.4);
164
+ const h = Number(args.components[2].replace(/deg$/, ""));
165
+ if ([l, c, h].some(Number.isNaN) || l < 0 || l > 1 || c < 0) return null;
166
+
167
+ const hRad = (h * Math.PI) / 180;
168
+ const labA = c * Math.cos(hRad);
169
+ const labB = c * Math.sin(hRad);
170
+
171
+ const l_ = l + 0.3963377774 * labA + 0.2158037573 * labB;
172
+ const m_ = l - 0.1055613458 * labA - 0.0638541728 * labB;
173
+ const s_ = l - 0.0894841775 * labA - 1.291485548 * labB;
174
+ const lc = l_ * l_ * l_;
175
+ const mc = m_ * m_ * m_;
176
+ const sc = s_ * s_ * s_;
177
+
178
+ const linearR = 4.0767416621 * lc - 3.3077115913 * mc + 0.2309699292 * sc;
179
+ const linearG = -1.2684380046 * lc + 2.6097574011 * mc - 0.3413193965 * sc;
180
+ const linearB = -0.0041960863 * lc - 0.7034186147 * mc + 1.707614701 * sc;
181
+
182
+ return {
183
+ r: clampByte(srgbGamma(linearR) * 255),
184
+ g: clampByte(srgbGamma(linearG) * 255),
185
+ b: clampByte(srgbGamma(linearB) * 255),
186
+ };
187
+ }
188
+
189
+ function srgbGamma(linear: number): number {
190
+ const clamped = Math.min(1, Math.max(0, linear));
191
+ return clamped <= 0.0031308 ? 12.92 * clamped : 1.055 * Math.pow(clamped, 1 / 2.4) - 0.055;
192
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * `components/forbidden-prop-value` — fires when a JSX prop is assigned a
3
+ * value that the component governance forbids. Path-scoped policies only
4
+ * fire when the file matches the policy's path glob; unscoped policies fire
5
+ * everywhere.
6
+ *
7
+ * Only static prop values are checked. Dynamic props (`variant={kind}`) and
8
+ * spreads (`{...props}`) cannot be evaluated at scan time.
9
+ */
10
+
11
+ import type { FactIndex } from "../facts/index.js";
12
+
13
+ import { makeFinding } from "./finding.js";
14
+ import type { Finding } from "./types.js";
15
+ import { indexComponentByNodeId, indexPropsByNodeId, readUsageNode } from "./utils.js";
16
+
17
+ export const RULE_ID = "components/forbidden-prop-value";
18
+ export const RULE_VERSION = "1";
19
+
20
+ export function ruleComponentsForbiddenPropValue(ix: FactIndex): Finding[] {
21
+ const findings: Finding[] = [];
22
+ const componentByNode = indexComponentByNodeId(ix);
23
+ const propsByNode = indexPropsByNodeId(ix);
24
+
25
+ for (const [nodeId, propUsages] of propsByNode) {
26
+ const usageComponent = componentByNode.get(nodeId);
27
+ if (!usageComponent) continue;
28
+ const node = readUsageNode(ix, nodeId);
29
+ if (!node) continue;
30
+
31
+ for (const propUsage of propUsages) {
32
+ if (propUsage.resolution !== "static") continue;
33
+ if (propUsage.value === undefined) continue;
34
+
35
+ const matched = ix.policy.forbiddenPropValues(usageComponent.componentId, {
36
+ prop: propUsage.prop,
37
+ value: propUsage.value,
38
+ path: node.file,
39
+ });
40
+ if (matched.length === 0) continue;
41
+
42
+ const policy = matched[0];
43
+ const replacement = policy.replaceWith;
44
+ const fix =
45
+ replacement !== undefined
46
+ ? {
47
+ kind: "replacePropValue" as const,
48
+ title: `Replace ${propUsage.prop} with ${formatValue(replacement)}`,
49
+ prop: propUsage.prop,
50
+ value: replacement,
51
+ deterministic: true,
52
+ }
53
+ : undefined;
54
+
55
+ findings.push(
56
+ makeFinding({
57
+ ruleId: RULE_ID,
58
+ ruleVersion: RULE_VERSION,
59
+ severity: policy.severity,
60
+ message: buildMessage(
61
+ node.element,
62
+ propUsage.prop,
63
+ propUsage.value,
64
+ policy.because,
65
+ replacement
66
+ ),
67
+ location: node.location,
68
+ evidence: ix.evidence([node.id, usageComponent.id, propUsage.id, policy.id]),
69
+ fingerprintIdentity: {
70
+ file: node.file,
71
+ componentId: usageComponent.componentId,
72
+ element: node.element,
73
+ nodePath: node.nodePath,
74
+ prop: propUsage.prop,
75
+ value: propUsage.value,
76
+ },
77
+ fix,
78
+ attributes: {
79
+ componentId: usageComponent.componentId,
80
+ prop: propUsage.prop,
81
+ rawValue: propUsage.value,
82
+ suggestedValue: replacement,
83
+ },
84
+ })
85
+ );
86
+ }
87
+ }
88
+
89
+ return findings;
90
+ }
91
+
92
+ function formatValue(value: unknown): string {
93
+ if (typeof value === "string") return `"${value}"`;
94
+ return String(value);
95
+ }
96
+
97
+ function buildMessage(
98
+ element: string,
99
+ prop: string,
100
+ value: unknown,
101
+ because: string,
102
+ replacement: unknown
103
+ ): string {
104
+ const head = `<${element}> ${prop}=${formatValue(value)} is forbidden.`;
105
+ const reason = because ? ` ${because.trim()}` : "";
106
+ const suggestion = replacement !== undefined ? ` Use ${formatValue(replacement)}.` : "";
107
+ return `${head}${reason}${suggestion}`.trim();
108
+ }