@usefragments/core 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/chunk-RANPUC6C.js +72 -0
  2. package/dist/chunk-RANPUC6C.js.map +1 -0
  3. package/dist/{chunk-WVFNDPM4.js → chunk-WNMWKUYG.js} +26 -14
  4. package/dist/chunk-WNMWKUYG.js.map +1 -0
  5. package/dist/codes/index.d.ts +1 -1
  6. package/dist/codes/index.js +1 -1
  7. package/dist/compiled-types/index.d.ts +1 -1
  8. package/dist/compiled-types/index.js +8 -0
  9. package/dist/generate/index.d.ts +1 -1
  10. package/dist/{governance-DxFipN5V.d.ts → governance-D9KtH-vg.d.ts} +9 -3
  11. package/dist/index.d.ts +165 -139
  12. package/dist/index.js +700 -201
  13. package/dist/index.js.map +1 -1
  14. package/dist/react-types.d.ts +1 -1
  15. package/dist/registry.d.ts +36 -36
  16. package/dist/schemas/index.d.ts +1 -1
  17. package/dist/test-utils.d.ts +1 -1
  18. package/package.json +2 -1
  19. package/src/agent-format.test.ts +13 -0
  20. package/src/agent-format.ts +9 -3
  21. package/src/codes/__tests__/codes.test.ts +0 -1
  22. package/src/codes/codes.ts +1 -2
  23. package/src/compiled-types/index.ts +81 -0
  24. package/src/compiled-types/parse.test.ts +47 -0
  25. package/src/facts/builders.ts +13 -0
  26. package/src/facts/compile.ts +13 -13
  27. package/src/facts/facts.test.ts +38 -1
  28. package/src/facts/index.ts +2 -0
  29. package/src/facts/types.ts +15 -0
  30. package/src/governance-integrity.test.ts +98 -1
  31. package/src/governance-integrity.ts +40 -20
  32. package/src/index.ts +8 -0
  33. package/src/rules/a11y-required-accessible-name.ts +175 -28
  34. package/src/rules/a11y-standard.ts +102 -0
  35. package/src/rules/a11y-utils.ts +7 -0
  36. package/src/rules/components-prefer-library.test.ts +75 -28
  37. package/src/rules/components-prefer-library.ts +35 -15
  38. package/src/rules/components-shadow-component.test.ts +21 -9
  39. package/src/rules/emit-gate.test.ts +74 -4
  40. package/src/rules/emit-gate.ts +24 -9
  41. package/src/rules/families.ts +1 -1
  42. package/src/rules/fix-availability.ts +1 -0
  43. package/src/rules/index.ts +12 -2
  44. package/src/rules/rules.test.ts +63 -7
  45. package/src/rules/tiers.ts +1 -0
  46. package/src/tokens/design-token-parser.test.ts +131 -0
  47. package/src/tokens/design-token-parser.ts +362 -49
  48. package/src/types.ts +2 -2
  49. package/dist/chunk-WVFNDPM4.js.map +0 -1
  50. package/dist/{index-DbkPE46t.d.ts → index-hZAlYCli.d.ts} +8 -8
@@ -11,6 +11,7 @@ import {
11
11
  makeClassNameDynamicFact,
12
12
  makeStyleDeclarationFact,
13
13
  makeTokenDefinitionFact,
14
+ makeUsageChildContentFact,
14
15
  makeUsageComponentFact,
15
16
  makeUsageImportFact,
16
17
  makeUsageInlineStyleFact,
@@ -18,6 +19,7 @@ import {
18
19
  makeUsagePropResolvedFact,
19
20
  makeUsageTextChildFact,
20
21
  projectSupersededImportPathPreferences,
22
+ RULES,
21
23
  ruleA11yRequiredAccessibleName,
22
24
  ruleComponentsForbiddenPropValue,
23
25
  ruleComponentsPreferLibrary,
@@ -34,6 +36,7 @@ import {
34
36
  runRules,
35
37
  } from "../index.js";
36
38
  import type { CanonicalBridgeV1, Fact, FactId } from "../index.js";
39
+ import { RULE_VERSION as A11Y_REQUIRED_ACCESSIBLE_NAME_VERSION } from "./a11y-required-accessible-name.js";
37
40
 
38
41
  type ButtonProps = {
39
42
  variant?: "primary" | "secondary" | "ghost" | "link";
@@ -138,6 +141,7 @@ interface ButtonUsageInput {
138
141
  value: string;
139
142
  }>;
140
143
  text?: string;
144
+ childContent?: "static-text" | "runtime" | "structural" | "empty";
141
145
  }
142
146
 
143
147
  function addButtonUsage(ix: FactIndex, input: ButtonUsageInput): { nodeId: FactId } {
@@ -176,6 +180,14 @@ function addButtonUsage(ix: FactIndex, input: ButtonUsageInput): { nodeId: FactI
176
180
  if (input.text !== undefined) {
177
181
  ix.add(makeUsageTextChildFact({ nodeId: node.id, text: input.text, index: 0 }));
178
182
  }
183
+ if (input.childContent !== undefined) {
184
+ ix.add(
185
+ makeUsageChildContentFact({
186
+ nodeId: node.id,
187
+ content: input.childContent,
188
+ })
189
+ );
190
+ }
179
191
  return { nodeId: node.id };
180
192
  }
181
193
 
@@ -1272,6 +1284,47 @@ describe("a11y/required-accessible-name", () => {
1272
1284
  }
1273
1285
  });
1274
1286
 
1287
+ it("does not fire for conservatively classified runtime child content", () => {
1288
+ const ix = emptyIndexWithGovernance();
1289
+ addButtonUsage(ix, {
1290
+ file: "apps/x.tsx",
1291
+ nodePath: "0",
1292
+ line: 1,
1293
+ column: 0,
1294
+ props: [{ prop: "children", resolution: "jsx" }],
1295
+ childContent: "runtime",
1296
+ });
1297
+
1298
+ expect(ruleA11yRequiredAccessibleName(ix)).toHaveLength(0);
1299
+ });
1300
+
1301
+ it.each(["structural", "empty"] as const)("still fires for %s child content", (childContent) => {
1302
+ const ix = emptyIndexWithGovernance();
1303
+ addButtonUsage(ix, {
1304
+ file: "apps/x.tsx",
1305
+ nodePath: "0",
1306
+ line: 1,
1307
+ column: 0,
1308
+ props: [{ prop: "children", resolution: "jsx" }],
1309
+ childContent,
1310
+ });
1311
+
1312
+ expect(ruleA11yRequiredAccessibleName(ix)).toHaveLength(1);
1313
+ });
1314
+
1315
+ it("does not infer a name from the public children prop-resolution fact", () => {
1316
+ const ix = emptyIndexWithGovernance();
1317
+ addButtonUsage(ix, {
1318
+ file: "apps/x.tsx",
1319
+ nodePath: "0",
1320
+ line: 1,
1321
+ column: 0,
1322
+ props: [{ prop: "children", resolution: "dynamic" }],
1323
+ });
1324
+
1325
+ expect(ruleA11yRequiredAccessibleName(ix)).toHaveLength(1);
1326
+ });
1327
+
1275
1328
  it("fires for an empty Button with no name source", () => {
1276
1329
  const ix = emptyIndexWithGovernance();
1277
1330
  addButtonUsage(ix, {
@@ -1298,6 +1351,11 @@ describe("a11y/required-accessible-name", () => {
1298
1351
 
1299
1352
  expect(ruleA11yRequiredAccessibleName(ix)).toHaveLength(0);
1300
1353
  });
1354
+
1355
+ it("keeps the registry version in lockstep with the emitted finding version", () => {
1356
+ const registered = RULES.find((rule) => rule.id === "a11y/required-accessible-name");
1357
+ expect(registered?.version).toBe(A11Y_REQUIRED_ACCESSIBLE_NAME_VERSION);
1358
+ });
1301
1359
  });
1302
1360
 
1303
1361
  // ---------------------------------------------------------------------------
@@ -3115,7 +3173,7 @@ describe("components/prefer-library", () => {
3115
3173
  expect(ruleComponentsPreferLibrary(ix)).toHaveLength(0);
3116
3174
  });
3117
3175
 
3118
- it("flags raw <button> → Button without advertising it as deterministic when Button is not imported", () => {
3176
+ it("keeps raw <button> → Button advisory without treating the directory as import-ready", () => {
3119
3177
  const ix = new FactIndex();
3120
3178
  ix.addMany(
3121
3179
  compileGlobalGovernanceFacts({
@@ -3151,14 +3209,12 @@ describe("components/prefer-library", () => {
3151
3209
  rawValue: "button",
3152
3210
  suggestedComponent: "Button",
3153
3211
  suggestedImportSourceKind: "directory",
3212
+ canonicalDirectory: "src/fragments/ui/components",
3213
+ importReady: false,
3214
+ advisory: true,
3154
3215
  },
3155
3216
  });
3156
- expect(findings[0].fix).toMatchObject({
3157
- kind: "replaceComponent",
3158
- from: "button",
3159
- to: "Button",
3160
- deterministic: false,
3161
- });
3217
+ expect(findings[0].fix).toBeUndefined();
3162
3218
  });
3163
3219
 
3164
3220
  it("flags a raw element by canonical vocab id when display name diverges (Modal → Dialog)", () => {
@@ -51,6 +51,7 @@ export const RULE_TIER: Readonly<Record<string, RuleTier>> = {
51
51
  "tokens/require-dual-fallback": "hygiene",
52
52
  "theme/no-theme-coupled-literal": "hygiene",
53
53
  "a11y/required-accessible-name": "hygiene",
54
+ "a11y/standard": "hygiene",
54
55
  // Composition constraints are authored contracts, but not part of the
55
56
  // two-choice headline contract; they self-activate when patterns are declared
56
57
  // (the compiler injects their configs), so off-by-default here is harmless.
@@ -175,4 +175,135 @@ describe("parseDesignTokenContent — CSS terminator robustness (#34/#35)", () =
175
175
  expect(res.tokens.find((token) => token.name === "--sp-1")?.category).toBe("spacing");
176
176
  expect(res.tokens.find((token) => token.name === "--fui-sp-2")?.category).toBe("spacing");
177
177
  });
178
+
179
+ it("preserves the same custom property across configured theme selectors", () => {
180
+ const content = [
181
+ ":root { --surface: #ffffff; --text: #111111; }",
182
+ '[data-theme="dark"] { --surface: #111111; --text: var(--surface); }',
183
+ ".high-contrast { --surface: #000000; }",
184
+ ].join("\n");
185
+ const res = parseDesignTokenContent(content, {
186
+ filePath: "tokens.css",
187
+ themeSelectors: {
188
+ ":root": "default",
189
+ '[data-theme="dark"]': "dark",
190
+ ".high-contrast": "high-contrast",
191
+ },
192
+ });
193
+
194
+ expect(
195
+ res.tokens
196
+ .filter((token) => token.name === "--surface")
197
+ .map((token) => [token.theme, token.resolvedValue, token.selector])
198
+ ).toEqual([
199
+ ["default", "#ffffff", ":root"],
200
+ ["dark", "#111111", '[data-theme="dark"]'],
201
+ ["high-contrast", "#000000", ".high-contrast"],
202
+ ]);
203
+ expect(
204
+ res.tokens.find((token) => token.name === "--text" && token.theme === "dark")?.resolvedValue
205
+ ).toBe("#111111");
206
+ });
207
+
208
+ it("preserves selector-specific values in compact single-line theme CSS", () => {
209
+ const content =
210
+ ':root{--surface:#ffffff;--text:#111111}[data-theme="dark"]{--surface:#111111;--text:var(--surface)}.high-contrast{--surface:#000000}';
211
+ const res = parseDesignTokenContent(content, {
212
+ filePath: "tokens.min.css",
213
+ themeSelectors: {
214
+ ":root": "default",
215
+ '[data-theme="dark"]': "dark",
216
+ ".high-contrast": "high-contrast",
217
+ },
218
+ });
219
+
220
+ expect(
221
+ res.tokens
222
+ .filter((token) => token.name === "--surface")
223
+ .map((token) => [token.theme, token.resolvedValue, token.selector])
224
+ ).toEqual([
225
+ ["default", "#ffffff", ":root"],
226
+ ["dark", "#111111", '[data-theme="dark"]'],
227
+ ["high-contrast", "#000000", ".high-contrast"],
228
+ ]);
229
+ expect(
230
+ res.tokens.find((token) => token.name === "--text" && token.theme === "dark")?.resolvedValue
231
+ ).toBe("#111111");
232
+ });
233
+
234
+ it("uses the nearest selector through compact at-rules and ignores structural text in comments", () => {
235
+ const content =
236
+ ':root{/* fake } [data-theme="dark"] { */--surface:#fff}@media(prefers-color-scheme:dark){[data-theme="dark"]{--surface:#111}}';
237
+ const res = parseDesignTokenContent(content, {
238
+ filePath: "tokens.min.css",
239
+ themeSelectors: {
240
+ ":root": "default",
241
+ '[data-theme="dark"]': "dark",
242
+ },
243
+ });
244
+
245
+ expect(
246
+ res.tokens
247
+ .filter((token) => token.name === "--surface")
248
+ .map((token) => [token.theme, token.resolvedValue, token.selector])
249
+ ).toEqual([
250
+ ["default", "#fff", ":root"],
251
+ ["dark", "#111", '[data-theme="dark"]'],
252
+ ]);
253
+ });
254
+
255
+ it("ignores declaration-shaped text in comments and strings while preserving quoted delimiters", () => {
256
+ const content =
257
+ ':root{/* --comment-token:red; } */--label:"literal; { value }";--image:url("data:image/svg+xml;utf8,<svg>{}</svg>");--raw-image:url(data:image/svg+xml;utf8,<svg>{}</svg>);--real:#fff;content:"--string-token:blue;"}';
258
+ const res = parseDesignTokenContent(content, { filePath: "tokens.min.css" });
259
+
260
+ expect(res.tokens.map((token) => [token.name, token.rawValue])).toEqual([
261
+ ["--label", '"literal; { value }"'],
262
+ ["--image", 'url("data:image/svg+xml;utf8,<svg>{}</svg>")'],
263
+ ["--raw-image", "url(data:image/svg+xml;utf8,<svg>{}</svg>)"],
264
+ ["--real", "#fff"],
265
+ ]);
266
+ });
267
+
268
+ it("keeps the nearest theme selector through multiple nested compact at-rules", () => {
269
+ const content =
270
+ '@layer theme{@supports(--feature-probe:#fff){:root{--surface:#fff}@media(prefers-color-scheme:dark){[data-theme="dark"]{--surface:#111;--text:var(--surface)}}}}';
271
+ const res = parseDesignTokenContent(content, {
272
+ filePath: "tokens.min.css",
273
+ themeSelectors: {
274
+ ":root": "default",
275
+ '[data-theme="dark"]': "dark",
276
+ },
277
+ });
278
+
279
+ expect(
280
+ res.tokens.map((token) => [token.name, token.theme, token.selector, token.resolvedValue])
281
+ ).toEqual([
282
+ ["--surface", "default", ":root", "#fff"],
283
+ ["--surface", "dark", '[data-theme="dark"]', "#111"],
284
+ ["--text", "dark", '[data-theme="dark"]', "#111"],
285
+ ]);
286
+ });
287
+
288
+ it("handles thousands of compact declarations without losing selector context", () => {
289
+ const declarationCount = 4_000;
290
+ const declarations = Array.from(
291
+ { length: declarationCount },
292
+ (_, index) => `--token-${index}:${index}px`
293
+ ).join(";");
294
+ const res = parseDesignTokenContent(
295
+ `@layer theme{@media(min-width:0){[data-theme="dense"]{${declarations}}}}`,
296
+ {
297
+ filePath: "tokens.min.css",
298
+ themeSelectors: { '[data-theme="dense"]': "dense" },
299
+ }
300
+ );
301
+
302
+ expect(res.tokens).toHaveLength(declarationCount);
303
+ expect(res.tokens.at(-1)).toMatchObject({
304
+ name: `--token-${declarationCount - 1}`,
305
+ theme: "dense",
306
+ selector: '[data-theme="dense"]',
307
+ });
308
+ });
178
309
  });