@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.
- package/LICENSE +21 -0
- package/dist/chunk-243QYRUF.js +2145 -0
- package/dist/chunk-243QYRUF.js.map +1 -0
- package/dist/chunk-3LLRNCPX.js +111 -0
- package/dist/chunk-3LLRNCPX.js.map +1 -0
- package/dist/chunk-EIYNNS77.js +48 -0
- package/dist/chunk-EIYNNS77.js.map +1 -0
- package/dist/chunk-JNBFJ34I.js +326 -0
- package/dist/chunk-JNBFJ34I.js.map +1 -0
- package/dist/chunk-R3X7UBHU.js +228 -0
- package/dist/chunk-R3X7UBHU.js.map +1 -0
- package/dist/chunk-SH4KPIYH.js +393 -0
- package/dist/chunk-SH4KPIYH.js.map +1 -0
- package/dist/chunk-V4VQB57N.js +56 -0
- package/dist/chunk-V4VQB57N.js.map +1 -0
- package/dist/chunk-X34IA4LR.js +319 -0
- package/dist/chunk-X34IA4LR.js.map +1 -0
- package/dist/chunk-YF65VYRY.js +493 -0
- package/dist/chunk-YF65VYRY.js.map +1 -0
- package/dist/codes/index.d.ts +40 -0
- package/dist/codes/index.js +23 -0
- package/dist/codes/index.js.map +1 -0
- package/dist/compiled-types/index.d.ts +5 -0
- package/dist/compiled-types/index.js +1 -0
- package/dist/compiled-types/index.js.map +1 -0
- package/dist/generate/index.d.ts +47 -0
- package/dist/generate/index.js +11 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/governance-BOa3KyiJ.d.ts +3279 -0
- package/dist/graph/index.d.ts +66 -0
- package/dist/graph/index.js +17 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.d.ts +11388 -0
- package/dist/index.js +8711 -0
- package/dist/index.js.map +1 -0
- package/dist/package-identity.d.ts +42 -0
- package/dist/package-identity.js +23 -0
- package/dist/package-identity.js.map +1 -0
- package/dist/preview/index.d.ts +484 -0
- package/dist/preview/index.js +1129 -0
- package/dist/preview/index.js.map +1 -0
- package/dist/react-types.d.ts +22 -0
- package/dist/react-types.js +1 -0
- package/dist/react-types.js.map +1 -0
- package/dist/registry.d.ts +2367 -0
- package/dist/registry.js +62 -0
- package/dist/registry.js.map +1 -0
- package/dist/schemas/index.d.ts +2584 -0
- package/dist/schemas/index.js +55 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/severity.d.ts +18 -0
- package/dist/severity.js +27 -0
- package/dist/severity.js.map +1 -0
- package/dist/test-utils.d.ts +14 -0
- package/dist/test-utils.js +63 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/topology/index.d.ts +48 -0
- package/dist/topology/index.js +7 -0
- package/dist/topology/index.js.map +1 -0
- package/dist/types-xJ2xyp_G.d.ts +119 -0
- package/package.json +120 -0
- package/src/__tests__/contract-parser.test.ts +614 -0
- package/src/__tests__/dtcg-generators.test.ts +439 -0
- package/src/__tests__/dtcg-parser.test.ts +884 -0
- package/src/__tests__/preview-runtime.test.tsx +111 -0
- package/src/__tests__/schema.test.ts +31 -0
- package/src/__tests__/severity.test.ts +34 -0
- package/src/__tests__/style-comparison.test.ts +662 -0
- package/src/__tests__/tokens-compat.test.ts +51 -0
- package/src/agent-format.test.ts +105 -0
- package/src/agent-format.ts +141 -0
- package/src/bundle.ts +149 -0
- package/src/codes/__tests__/codes.test.ts +73 -0
- package/src/codes/codes.ts +463 -0
- package/src/codes/index.ts +7 -0
- package/src/codes/source-finding-bridge.ts +113 -0
- package/src/compiled-types/index.ts +293 -0
- package/src/component-contract.ts +231 -0
- package/src/component-discovery.ts +80 -0
- package/src/composition.test.ts +262 -0
- package/src/composition.ts +318 -0
- package/src/config-paths.test.ts +78 -0
- package/src/config-paths.ts +47 -0
- package/src/config.ts +18 -0
- package/src/conform-prove.test.ts +274 -0
- package/src/conform-prove.ts +326 -0
- package/src/conform.ts +160 -0
- package/src/constants.ts +114 -0
- package/src/context.ts +2 -0
- package/src/contract/fixture.ts +110 -0
- package/src/contract/hash.test.ts +86 -0
- package/src/contract/hash.ts +195 -0
- package/src/contract/index.ts +43 -0
- package/src/contract/preimage.test.ts +254 -0
- package/src/contract/preimage.ts +365 -0
- package/src/contract/stamp.test.ts +100 -0
- package/src/contract/stamp.ts +105 -0
- package/src/contract-parser.ts +141 -0
- package/src/defineFragment.test.ts +257 -0
- package/src/defineFragment.ts +293 -0
- package/src/dtcg-generators.ts +324 -0
- package/src/dtcg-parser.ts +1 -0
- package/src/dtcg.ts +237 -0
- package/src/facts/__snapshots__/facts.test.ts.snap +124 -0
- package/src/facts/builders.ts +835 -0
- package/src/facts/compile.ts +422 -0
- package/src/facts/fact-index.ts +394 -0
- package/src/facts/facts.test.ts +494 -0
- package/src/facts/ids.ts +125 -0
- package/src/facts/index.ts +119 -0
- package/src/facts/types.ts +614 -0
- package/src/figma.ts +263 -0
- package/src/fragment-types.ts +214 -0
- package/src/generate/generate.test.ts +594 -0
- package/src/generate/index.ts +438 -0
- package/src/governance-integrity.test.ts +186 -0
- package/src/governance-integrity.ts +282 -0
- package/src/governance.test.ts +300 -0
- package/src/governance.ts +605 -0
- package/src/governance.type-test.ts +75 -0
- package/src/graph/engine.ts +446 -0
- package/src/graph/graph.test.ts +746 -0
- package/src/graph/index.ts +32 -0
- package/src/graph/serialization.ts +55 -0
- package/src/graph/types.ts +178 -0
- package/src/index.ts +891 -0
- package/src/local-canonical.ts +102 -0
- package/src/mcp-snapshot.ts +222 -0
- package/src/package-identity-match.ts +33 -0
- package/src/package-identity.generated.ts +233 -0
- package/src/package-identity.test.ts +99 -0
- package/src/package-identity.ts +157 -0
- package/src/performance-presets.ts +142 -0
- package/src/preview/comparison-artifact.test.ts +228 -0
- package/src/preview/comparison-artifact.ts +181 -0
- package/src/preview/comparison-context.ts +93 -0
- package/src/preview/comparison-evidence.ts +112 -0
- package/src/preview/comparison-format.ts +21 -0
- package/src/preview/comparison-presentation.ts +187 -0
- package/src/preview/comparison-references.test.ts +430 -0
- package/src/preview/comparison-references.ts +72 -0
- package/src/preview/comparison-score.ts +59 -0
- package/src/preview/comparison-styles.ts +197 -0
- package/src/preview/comparison.test.ts +559 -0
- package/src/preview/comparison.ts +488 -0
- package/src/preview/index.ts +10 -0
- package/src/preview/types.ts +309 -0
- package/src/preview/validation.test.ts +290 -0
- package/src/preview/validation.ts +356 -0
- package/src/preview-runtime.tsx +144 -0
- package/src/raw-html-canonical.test.ts +41 -0
- package/src/raw-html-canonical.ts +179 -0
- package/src/react-types.ts +21 -0
- package/src/registry-install-plan.ts +344 -0
- package/src/registry.test.ts +245 -0
- package/src/registry.ts +369 -0
- package/src/rules/a11y-required-accessible-name.ts +93 -0
- package/src/rules/color-math.test.ts +72 -0
- package/src/rules/color-math.ts +192 -0
- package/src/rules/components-forbidden-prop-value.ts +108 -0
- package/src/rules/components-prefer-library.ts +943 -0
- package/src/rules/components-unknown-prop.ts +94 -0
- package/src/rules/composition-pattern-schema.ts +125 -0
- package/src/rules/composition-pattern.test.ts +320 -0
- package/src/rules/composition-pattern.ts +271 -0
- package/src/rules/emit-gate.test.ts +42 -0
- package/src/rules/emit-gate.ts +60 -0
- package/src/rules/finding.ts +54 -0
- package/src/rules/index.ts +235 -0
- package/src/rules/jsx-preferred-component.ts +68 -0
- package/src/rules/jsx-preferred-import-path.ts +67 -0
- package/src/rules/presets.ts +57 -0
- package/src/rules/props-invalid-value.ts +115 -0
- package/src/rules/rules.test.ts +3636 -0
- package/src/rules/spacing-resolution.ts +184 -0
- package/src/rules/styles-no-raw-color.ts +329 -0
- package/src/rules/styles-no-raw-dimensions.ts +357 -0
- package/src/rules/styles-no-raw-spacing.ts +236 -0
- package/src/rules/styles-no-raw-typography.ts +237 -0
- package/src/rules/tailwind-arbitrary-color.test.ts +128 -0
- package/src/rules/tailwind-arbitrary-color.ts +93 -0
- package/src/rules/tailwind-arbitrary-spacing.test.ts +157 -0
- package/src/rules/tailwind-arbitrary-spacing.ts +120 -0
- package/src/rules/tailwind-forbidden-palette.test.ts +116 -0
- package/src/rules/tailwind-forbidden-palette.ts +107 -0
- package/src/rules/tailwind-off-scale-spacing-token.test.ts +100 -0
- package/src/rules/tailwind-off-scale-spacing-token.ts +108 -0
- package/src/rules/tailwind-raw-color-via-token.test.ts +108 -0
- package/src/rules/tailwind-raw-color-via-token.ts +82 -0
- package/src/rules/tailwind-rule-utils.ts +106 -0
- package/src/rules/tailwind-unknown-class.test.ts +157 -0
- package/src/rules/tailwind-unknown-class.ts +121 -0
- package/src/rules/taxonomy.test.ts +238 -0
- package/src/rules/theme-no-theme-coupled-literal.ts +132 -0
- package/src/rules/tiers.ts +111 -0
- package/src/rules/tokens-css-vars-must-be-defined.test.ts +244 -0
- package/src/rules/tokens-css-vars-must-be-defined.ts +156 -0
- package/src/rules/tokens-require-dual-fallback.ts +78 -0
- package/src/rules/types.ts +27 -0
- package/src/rules/utils.test.ts +110 -0
- package/src/rules/utils.ts +599 -0
- package/src/schema.ts +427 -0
- package/src/schemas/index.ts +275 -0
- package/src/schemas/schemas.test.ts +124 -0
- package/src/severity.ts +54 -0
- package/src/storyAdapter.test.ts +571 -0
- package/src/storyAdapter.ts +761 -0
- package/src/storyFilters.test.ts +350 -0
- package/src/storyFilters.ts +253 -0
- package/src/storybook-csf.ts +11 -0
- package/src/style-comparison.ts +423 -0
- package/src/test-utils.ts +68 -0
- package/src/token-parser.ts +2 -0
- package/src/token-types.ts +287 -0
- package/src/tokens/categories.ts +131 -0
- package/src/tokens/color.ts +193 -0
- package/src/tokens/design-token-parser.test.ts +159 -0
- package/src/tokens/design-token-parser.ts +402 -0
- package/src/tokens/dtcg.ts +428 -0
- package/src/tokens/index.ts +130 -0
- package/src/tokens/lookup.ts +19 -0
- package/src/tokens/scss.ts +171 -0
- package/src/tokens/tailwind-v4.ts +187 -0
- package/src/tokens/types.ts +43 -0
- package/src/topology/index.ts +9 -0
- package/src/topology/resolve-area.test.ts +174 -0
- package/src/topology/resolve-area.ts +215 -0
- package/src/types.ts +1057 -0
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule helpers — small internal queries shared by Phase 5 rules.
|
|
3
|
+
*
|
|
4
|
+
* These live next to the rules instead of on `FactIndex` until they prove
|
|
5
|
+
* useful for two or more rules. The plan is to lift them onto the index only
|
|
6
|
+
* when a third caller appears.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
Fact,
|
|
11
|
+
FactId,
|
|
12
|
+
FactIndex,
|
|
13
|
+
StyleDeclarationFact,
|
|
14
|
+
TokenDefinitionFact,
|
|
15
|
+
UsageComponentFact,
|
|
16
|
+
UsageInlineStyleFact,
|
|
17
|
+
UsageNodeFact,
|
|
18
|
+
UsagePropResolvedFact,
|
|
19
|
+
UsageTextChildFact,
|
|
20
|
+
} from "../facts/index.js";
|
|
21
|
+
|
|
22
|
+
export interface LengthScale {
|
|
23
|
+
unit: "px" | "rem";
|
|
24
|
+
rootFontSizePx?: number;
|
|
25
|
+
emBasePx?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Group `usage_prop_resolved` facts by their parent `usage_node` id. */
|
|
29
|
+
export function indexPropsByNodeId(ix: FactIndex): Map<FactId, UsagePropResolvedFact[]> {
|
|
30
|
+
const out = new Map<FactId, UsagePropResolvedFact[]>();
|
|
31
|
+
for (const fact of ix.byKind("usage_prop_resolved")) {
|
|
32
|
+
let bucket = out.get(fact.nodeId);
|
|
33
|
+
if (!bucket) {
|
|
34
|
+
bucket = [];
|
|
35
|
+
out.set(fact.nodeId, bucket);
|
|
36
|
+
}
|
|
37
|
+
bucket.push(fact);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Group `usage_text_child` facts by their parent `usage_node` id. */
|
|
43
|
+
export function indexTextChildrenByNodeId(ix: FactIndex): Map<FactId, UsageTextChildFact[]> {
|
|
44
|
+
const out = new Map<FactId, UsageTextChildFact[]>();
|
|
45
|
+
for (const fact of ix.byKind("usage_text_child")) {
|
|
46
|
+
let bucket = out.get(fact.nodeId);
|
|
47
|
+
if (!bucket) {
|
|
48
|
+
bucket = [];
|
|
49
|
+
out.set(fact.nodeId, bucket);
|
|
50
|
+
}
|
|
51
|
+
bucket.push(fact);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Group `usage_inline_style` facts by their parent `usage_node` id. */
|
|
57
|
+
export function indexInlineStylesByNodeId(ix: FactIndex): Map<FactId, UsageInlineStyleFact[]> {
|
|
58
|
+
const out = new Map<FactId, UsageInlineStyleFact[]>();
|
|
59
|
+
for (const fact of ix.byKind("usage_inline_style")) {
|
|
60
|
+
let bucket = out.get(fact.nodeId);
|
|
61
|
+
if (!bucket) {
|
|
62
|
+
bucket = [];
|
|
63
|
+
out.set(fact.nodeId, bucket);
|
|
64
|
+
}
|
|
65
|
+
bucket.push(fact);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Map `usage_component` records by their `nodeId` for fast component lookup. */
|
|
71
|
+
export function indexComponentByNodeId(ix: FactIndex): Map<FactId, UsageComponentFact> {
|
|
72
|
+
const out = new Map<FactId, UsageComponentFact>();
|
|
73
|
+
for (const fact of ix.byKind("usage_component")) {
|
|
74
|
+
out.set(fact.nodeId, fact);
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A region: a container node paired with every `usage_node` nested beneath it
|
|
81
|
+
* within the same root tree. Membership is decided purely by `nodePath` — no
|
|
82
|
+
* component identity, no framework knowledge — so the same grouping works for
|
|
83
|
+
* any adapter that emits conformant paths.
|
|
84
|
+
*/
|
|
85
|
+
export interface Region {
|
|
86
|
+
/** The container node whose `nodePath` is a strict prefix of every member. */
|
|
87
|
+
container: UsageNodeFact;
|
|
88
|
+
/** Every node strictly nested beneath the container, at any depth. */
|
|
89
|
+
members: UsageNodeFact[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Group `usage_node` facts into regions by shared `nodePath` prefix.
|
|
94
|
+
*
|
|
95
|
+
* Every node is treated as a potential container; a node B is a member of node
|
|
96
|
+
* A's region iff A's path segments are a *strict* prefix of B's and both share
|
|
97
|
+
* the same `(file, rootIndex)`. The consuming rule decides which containers
|
|
98
|
+
* matter via its region selector — this helper does not interpret what a
|
|
99
|
+
* container "is."
|
|
100
|
+
*
|
|
101
|
+
* `nodePath` segments are opaque. The path is split into a leading `rootIndex`
|
|
102
|
+
* token (everything before the first `:`) and `/`-joined segments; only whole
|
|
103
|
+
* segments are compared. The `expr:` / `attr:` / `jsx:` tokens a JSX adapter
|
|
104
|
+
* encodes inside a segment are never parsed — that is what keeps this helper
|
|
105
|
+
* framework-blind (per the usage-node-fact contract). Non-conformant paths are
|
|
106
|
+
* skipped rather than mis-grouped.
|
|
107
|
+
*
|
|
108
|
+
* Cost is O(nodes²) within one `(file, rootIndex)` bucket (each path is parsed
|
|
109
|
+
* once up front); root trees are small in practice, so this stays well inside
|
|
110
|
+
* the per-file scan budget.
|
|
111
|
+
*/
|
|
112
|
+
export function regionsByPrefix(ix: FactIndex): Region[] {
|
|
113
|
+
// Bucket by (file, rootIndex) so a prefix comparison never crosses root
|
|
114
|
+
// trees — separate roots in one file restart segment numbering, so their
|
|
115
|
+
// paths can collide segment-for-segment without actually being nested.
|
|
116
|
+
const buckets = new Map<string, Array<{ fact: UsageNodeFact; segments: string[] }>>();
|
|
117
|
+
for (const fact of ix.byKind("usage_node")) {
|
|
118
|
+
const parsed = parseNodePath(fact.nodePath);
|
|
119
|
+
if (!parsed) continue;
|
|
120
|
+
// rootIndex is colon-free (it is the token before nodePath's first ":"), so
|
|
121
|
+
// putting it first lets that ":" cleanly delimit it from the free-form file.
|
|
122
|
+
const key = `${parsed.rootIndex}:${fact.file}`;
|
|
123
|
+
let bucket = buckets.get(key);
|
|
124
|
+
if (!bucket) {
|
|
125
|
+
bucket = [];
|
|
126
|
+
buckets.set(key, bucket);
|
|
127
|
+
}
|
|
128
|
+
bucket.push({ fact, segments: parsed.segments });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const regions: Region[] = [];
|
|
132
|
+
for (const bucket of buckets.values()) {
|
|
133
|
+
for (const candidate of bucket) {
|
|
134
|
+
const members: UsageNodeFact[] = [];
|
|
135
|
+
for (const other of bucket) {
|
|
136
|
+
if (other !== candidate && isStrictSegmentPrefix(candidate.segments, other.segments)) {
|
|
137
|
+
members.push(other.fact);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
regions.push({ container: candidate.fact, members });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return regions;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Split a `nodePath` into its leading `rootIndex` token and whole segments.
|
|
148
|
+
* Returns `null` for a path with no `rootIndex:` prefix (non-conformant — e.g.
|
|
149
|
+
* a future non-React adapter that hasn't adopted the contract); the caller
|
|
150
|
+
* skips those rather than risk mis-grouping. Segments are never split below the
|
|
151
|
+
* `/` boundary, keeping `expr:` / `attr:` / `jsx:` tokens opaque.
|
|
152
|
+
*/
|
|
153
|
+
function parseNodePath(nodePath: string): { rootIndex: string; segments: string[] } | null {
|
|
154
|
+
const colon = nodePath.indexOf(":");
|
|
155
|
+
if (colon === -1) return null;
|
|
156
|
+
const rest = nodePath.slice(colon + 1);
|
|
157
|
+
return {
|
|
158
|
+
rootIndex: nodePath.slice(0, colon),
|
|
159
|
+
segments: rest === "" ? [] : rest.split("/"),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Whether `prefix` is a strict (shorter) whole-segment prefix of `full`. */
|
|
164
|
+
function isStrictSegmentPrefix(prefix: readonly string[], full: readonly string[]): boolean {
|
|
165
|
+
if (prefix.length >= full.length) return false;
|
|
166
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
167
|
+
if (prefix[i] !== full[i]) return false;
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Read a `usage_node` fact through the index and narrow the kind. */
|
|
173
|
+
export function readUsageNode(ix: FactIndex, id: FactId): UsageNodeFact | undefined {
|
|
174
|
+
const fact: Fact | undefined = ix.get(id);
|
|
175
|
+
return fact && fact.kind === "usage_node" ? fact : undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Read a `style_declaration` fact through the index and narrow the kind. */
|
|
179
|
+
export function readStyleDeclaration(ix: FactIndex, id: FactId): StyleDeclarationFact | undefined {
|
|
180
|
+
const fact: Fact | undefined = ix.get(id);
|
|
181
|
+
return fact && fact.kind === "style_declaration" ? fact : undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The set of CSS custom-property names a token vocabulary defines, normalized to
|
|
186
|
+
* `--`-prefixed form. Shared by the token-vocabulary rules
|
|
187
|
+
* (`require-dual-fallback`, `css-vars-must-be-defined`).
|
|
188
|
+
*/
|
|
189
|
+
export function cssVariableNames(tokens: readonly TokenDefinitionFact[]): Set<string> {
|
|
190
|
+
const names = new Set<string>();
|
|
191
|
+
for (const token of tokens) {
|
|
192
|
+
names.add(token.name.startsWith("--") ? token.name : `--${token.name}`);
|
|
193
|
+
}
|
|
194
|
+
return names;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The prefix families of a token vocabulary — the namespace each token name
|
|
199
|
+
* declares, up to and including the first hyphen after the leading `--`
|
|
200
|
+
* (`--fui-color-accent` → `--fui-`). A single-segment token (`--accent`, no
|
|
201
|
+
* namespace) contributes no family. Used to decide whether an unknown `var(--x)`
|
|
202
|
+
* is "shaped like a contract token" (off-contract drift) or a genuinely foreign
|
|
203
|
+
* custom property (`--swiper-*`, `--radix-*`, layout/animation vars — ignored).
|
|
204
|
+
*/
|
|
205
|
+
export function contractPrefixFamilies(names: ReadonlySet<string>): Set<string> {
|
|
206
|
+
const families = new Set<string>();
|
|
207
|
+
for (const name of names) {
|
|
208
|
+
const hyphen = name.indexOf("-", 2);
|
|
209
|
+
if (hyphen === -1) continue;
|
|
210
|
+
families.add(name.slice(0, hyphen + 1));
|
|
211
|
+
}
|
|
212
|
+
return families;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Universal JSX props that pass `components/unknown-prop` regardless of the
|
|
217
|
+
* component's prop schema. These are React/DOM concerns, not design system
|
|
218
|
+
* concerns, and asking authors to declare them in `*.fragment.ts` would noise
|
|
219
|
+
* up every component.
|
|
220
|
+
*/
|
|
221
|
+
const UNIVERSAL_JSX_PROPS = new Set([
|
|
222
|
+
"key",
|
|
223
|
+
"ref",
|
|
224
|
+
"children",
|
|
225
|
+
"className",
|
|
226
|
+
"style",
|
|
227
|
+
"id",
|
|
228
|
+
"role",
|
|
229
|
+
"tabIndex",
|
|
230
|
+
"title",
|
|
231
|
+
"slot",
|
|
232
|
+
"lang",
|
|
233
|
+
"dir",
|
|
234
|
+
"hidden",
|
|
235
|
+
"draggable",
|
|
236
|
+
"spellCheck",
|
|
237
|
+
"translate",
|
|
238
|
+
"contentEditable",
|
|
239
|
+
"suppressContentEditableWarning",
|
|
240
|
+
"suppressHydrationWarning",
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
export function isUniversalJsxProp(prop: string): boolean {
|
|
244
|
+
if (UNIVERSAL_JSX_PROPS.has(prop)) return true;
|
|
245
|
+
if (prop.startsWith("aria-")) return true;
|
|
246
|
+
if (prop.startsWith("data-")) return true;
|
|
247
|
+
if (prop.startsWith("on") && prop.length > 2 && prop[2] === prop[2].toUpperCase()) {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Parse a length-ish CSS value (e.g., `13px`, `13`, `1.5rem`, `0`) into a
|
|
255
|
+
* numeric value and unit. Returns `null` for values that include CSS function
|
|
256
|
+
* calls, multi-value shorthands, or anything else that can't be cleanly
|
|
257
|
+
* compared against a numeric scale.
|
|
258
|
+
*/
|
|
259
|
+
export function parseLengthValue(
|
|
260
|
+
raw: string
|
|
261
|
+
): { value: number; unit: "px" | "rem" | "em" | null } | null {
|
|
262
|
+
const trimmed = raw.trim();
|
|
263
|
+
if (!trimmed) return null;
|
|
264
|
+
const match = /^(-?\d+(?:\.\d+)?)(px|rem|em)?$/i.exec(trimmed);
|
|
265
|
+
if (!match) return null;
|
|
266
|
+
const value = Number.parseFloat(match[1]);
|
|
267
|
+
if (!Number.isFinite(value)) return null;
|
|
268
|
+
const rawUnit = match[2]?.toLowerCase();
|
|
269
|
+
const unit = rawUnit === "px" || rawUnit === "rem" || rawUnit === "em" ? rawUnit : null;
|
|
270
|
+
return { value, unit };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export type ParsedLengthValue = NonNullable<ReturnType<typeof parseLengthValue>>;
|
|
274
|
+
|
|
275
|
+
export function matchesScale(value: number, allowed: readonly number[]): boolean {
|
|
276
|
+
const magnitude = Math.abs(value);
|
|
277
|
+
if (magnitude === 0) return allowed.includes(0);
|
|
278
|
+
return allowed.includes(magnitude);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function normalizeLengthForScale(
|
|
282
|
+
parsed: ParsedLengthValue,
|
|
283
|
+
scale: LengthScale
|
|
284
|
+
): {
|
|
285
|
+
value: number;
|
|
286
|
+
deterministicFix: boolean;
|
|
287
|
+
assumedRootFontSizePx?: number;
|
|
288
|
+
assumedEmBasePx?: number;
|
|
289
|
+
} | null {
|
|
290
|
+
const unit = parsed.unit ?? scale.unit;
|
|
291
|
+
if (unit === scale.unit) {
|
|
292
|
+
return { value: parsed.value, deterministicFix: true };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (scale.unit === "px" && unit === "rem") {
|
|
296
|
+
const explicit = scale.rootFontSizePx !== undefined;
|
|
297
|
+
const rootFontSizePx = scale.rootFontSizePx ?? 16;
|
|
298
|
+
return {
|
|
299
|
+
value: parsed.value * rootFontSizePx,
|
|
300
|
+
deterministicFix: explicit,
|
|
301
|
+
assumedRootFontSizePx: explicit ? undefined : rootFontSizePx,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (scale.unit === "px" && unit === "em") {
|
|
306
|
+
const explicit = scale.emBasePx !== undefined;
|
|
307
|
+
const emBasePx = scale.emBasePx ?? scale.rootFontSizePx ?? 16;
|
|
308
|
+
return {
|
|
309
|
+
value: parsed.value * emBasePx,
|
|
310
|
+
deterministicFix: explicit,
|
|
311
|
+
assumedEmBasePx: explicit ? undefined : emBasePx,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (scale.unit === "rem" && unit === "px") {
|
|
316
|
+
const explicit = scale.rootFontSizePx !== undefined;
|
|
317
|
+
if (!explicit) return null;
|
|
318
|
+
return {
|
|
319
|
+
value: parsed.value / scale.rootFontSizePx!,
|
|
320
|
+
deterministicFix: true,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function nearestScaleValue(value: number, allowed: readonly number[]): number | null {
|
|
328
|
+
if (allowed.length === 0) return null;
|
|
329
|
+
let best = allowed[0];
|
|
330
|
+
let bestDelta = Math.abs(value - best);
|
|
331
|
+
for (let i = 1; i < allowed.length; i++) {
|
|
332
|
+
const candidate = allowed[i];
|
|
333
|
+
const delta = Math.abs(value - candidate);
|
|
334
|
+
if (delta < bestDelta || (delta === bestDelta && candidate < best)) {
|
|
335
|
+
best = candidate;
|
|
336
|
+
bestDelta = delta;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return best;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function nearestSignedScaleValue(value: number, allowed: readonly number[]): number | null {
|
|
343
|
+
const nearest = nearestScaleValue(Math.abs(value), allowed);
|
|
344
|
+
if (nearest === null) return null;
|
|
345
|
+
if (nearest === 0) return 0;
|
|
346
|
+
return value < 0 ? -nearest : nearest;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Detect whether a CSS value contains a raw color literal — hex, rgb/rgba,
|
|
351
|
+
* hsl/hsla, or a named color. Returns the color literal so the rule can quote
|
|
352
|
+
* it in the message and as a fingerprint input. Returns `null` for token
|
|
353
|
+
* references (`var(--...)`) and for values that don't look like a color.
|
|
354
|
+
*/
|
|
355
|
+
export function detectRawColor(raw: string): string | null {
|
|
356
|
+
const trimmed = raw.trim();
|
|
357
|
+
if (!trimmed) return null;
|
|
358
|
+
|
|
359
|
+
// A hex/color literal sitting inside a `var(--token, <fallback>)` expression
|
|
360
|
+
// is the *fallback*, not a raw color — flagging it both produces false
|
|
361
|
+
// positives on shorthands (`border: 1px solid var(--x, #hex)`) and fights the
|
|
362
|
+
// dual-fallback rule, which mandates exactly that literal. Strip every var()
|
|
363
|
+
// expression (innermost-first, so nested fallbacks are handled) before
|
|
364
|
+
// scanning, so only a literal *outside* any var() is treated as raw.
|
|
365
|
+
const scanned = stripVarExpressions(trimmed);
|
|
366
|
+
if (!scanned.trim()) return null;
|
|
367
|
+
|
|
368
|
+
const hex = /#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})\b/i.exec(scanned);
|
|
369
|
+
if (hex) return hex[0];
|
|
370
|
+
|
|
371
|
+
const fn = /(rgb|rgba|hsl|hsla)\s*\([^)]*\)/i.exec(scanned);
|
|
372
|
+
if (fn) return fn[0];
|
|
373
|
+
|
|
374
|
+
for (const part of scanned.split(/\s+/)) {
|
|
375
|
+
const lower = part.toLowerCase();
|
|
376
|
+
if (/^[a-z]+$/i.test(lower) && CSS_NAMED_COLORS.has(lower)) {
|
|
377
|
+
return lower;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Remove every `var(--token, …)` expression from a CSS value, innermost-first,
|
|
386
|
+
* so a literal inside a var() fallback is not mistaken for a raw color. A bare
|
|
387
|
+
* `var(--token)` (no fallback) also disappears, preserving the previous
|
|
388
|
+
* behavior of exempting whole-value token references.
|
|
389
|
+
*/
|
|
390
|
+
function stripVarExpressions(value: string): string {
|
|
391
|
+
let out = value;
|
|
392
|
+
const innermost = /var\([^()]*\)/gi;
|
|
393
|
+
// Iterate so nested fallbacks (`var(--a, var(--b, #fff))`) collapse fully.
|
|
394
|
+
for (let i = 0; i < 10 && innermost.test(out); i += 1) {
|
|
395
|
+
out = out.replace(innermost, " ");
|
|
396
|
+
innermost.lastIndex = 0;
|
|
397
|
+
}
|
|
398
|
+
return out;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const CSS_NAMED_COLORS = new Set([
|
|
402
|
+
"aliceblue",
|
|
403
|
+
"antiquewhite",
|
|
404
|
+
"aqua",
|
|
405
|
+
"aquamarine",
|
|
406
|
+
"azure",
|
|
407
|
+
"beige",
|
|
408
|
+
"bisque",
|
|
409
|
+
"black",
|
|
410
|
+
"blanchedalmond",
|
|
411
|
+
"blue",
|
|
412
|
+
"blueviolet",
|
|
413
|
+
"brown",
|
|
414
|
+
"burlywood",
|
|
415
|
+
"cadetblue",
|
|
416
|
+
"chartreuse",
|
|
417
|
+
"chocolate",
|
|
418
|
+
"coral",
|
|
419
|
+
"cornflowerblue",
|
|
420
|
+
"cornsilk",
|
|
421
|
+
"crimson",
|
|
422
|
+
"cyan",
|
|
423
|
+
"darkblue",
|
|
424
|
+
"darkcyan",
|
|
425
|
+
"darkgoldenrod",
|
|
426
|
+
"darkgray",
|
|
427
|
+
"darkgreen",
|
|
428
|
+
"darkgrey",
|
|
429
|
+
"darkkhaki",
|
|
430
|
+
"darkmagenta",
|
|
431
|
+
"darkolivegreen",
|
|
432
|
+
"darkorange",
|
|
433
|
+
"darkorchid",
|
|
434
|
+
"darkred",
|
|
435
|
+
"darksalmon",
|
|
436
|
+
"darkseagreen",
|
|
437
|
+
"darkslateblue",
|
|
438
|
+
"darkslategray",
|
|
439
|
+
"darkslategrey",
|
|
440
|
+
"darkturquoise",
|
|
441
|
+
"darkviolet",
|
|
442
|
+
"deeppink",
|
|
443
|
+
"deepskyblue",
|
|
444
|
+
"dimgray",
|
|
445
|
+
"dimgrey",
|
|
446
|
+
"dodgerblue",
|
|
447
|
+
"firebrick",
|
|
448
|
+
"floralwhite",
|
|
449
|
+
"forestgreen",
|
|
450
|
+
"fuchsia",
|
|
451
|
+
"gainsboro",
|
|
452
|
+
"ghostwhite",
|
|
453
|
+
"gold",
|
|
454
|
+
"goldenrod",
|
|
455
|
+
"gray",
|
|
456
|
+
"green",
|
|
457
|
+
"greenyellow",
|
|
458
|
+
"grey",
|
|
459
|
+
"honeydew",
|
|
460
|
+
"hotpink",
|
|
461
|
+
"indianred",
|
|
462
|
+
"indigo",
|
|
463
|
+
"ivory",
|
|
464
|
+
"khaki",
|
|
465
|
+
"lavender",
|
|
466
|
+
"lavenderblush",
|
|
467
|
+
"lawngreen",
|
|
468
|
+
"lemonchiffon",
|
|
469
|
+
"lightblue",
|
|
470
|
+
"lightcoral",
|
|
471
|
+
"lightcyan",
|
|
472
|
+
"lightgoldenrodyellow",
|
|
473
|
+
"lightgray",
|
|
474
|
+
"lightgreen",
|
|
475
|
+
"lightgrey",
|
|
476
|
+
"lightpink",
|
|
477
|
+
"lightsalmon",
|
|
478
|
+
"lightseagreen",
|
|
479
|
+
"lightskyblue",
|
|
480
|
+
"lightslategray",
|
|
481
|
+
"lightslategrey",
|
|
482
|
+
"lightsteelblue",
|
|
483
|
+
"lightyellow",
|
|
484
|
+
"lime",
|
|
485
|
+
"limegreen",
|
|
486
|
+
"linen",
|
|
487
|
+
"magenta",
|
|
488
|
+
"maroon",
|
|
489
|
+
"mediumaquamarine",
|
|
490
|
+
"mediumblue",
|
|
491
|
+
"mediumorchid",
|
|
492
|
+
"mediumpurple",
|
|
493
|
+
"mediumseagreen",
|
|
494
|
+
"mediumslateblue",
|
|
495
|
+
"mediumspringgreen",
|
|
496
|
+
"mediumturquoise",
|
|
497
|
+
"mediumvioletred",
|
|
498
|
+
"midnightblue",
|
|
499
|
+
"mintcream",
|
|
500
|
+
"mistyrose",
|
|
501
|
+
"moccasin",
|
|
502
|
+
"navajowhite",
|
|
503
|
+
"navy",
|
|
504
|
+
"oldlace",
|
|
505
|
+
"olive",
|
|
506
|
+
"olivedrab",
|
|
507
|
+
"orange",
|
|
508
|
+
"orangered",
|
|
509
|
+
"orchid",
|
|
510
|
+
"palegoldenrod",
|
|
511
|
+
"palegreen",
|
|
512
|
+
"paleturquoise",
|
|
513
|
+
"palevioletred",
|
|
514
|
+
"papayawhip",
|
|
515
|
+
"peachpuff",
|
|
516
|
+
"peru",
|
|
517
|
+
"pink",
|
|
518
|
+
"plum",
|
|
519
|
+
"powderblue",
|
|
520
|
+
"purple",
|
|
521
|
+
"rebeccapurple",
|
|
522
|
+
"red",
|
|
523
|
+
"rosybrown",
|
|
524
|
+
"royalblue",
|
|
525
|
+
"saddlebrown",
|
|
526
|
+
"salmon",
|
|
527
|
+
"sandybrown",
|
|
528
|
+
"seagreen",
|
|
529
|
+
"seashell",
|
|
530
|
+
"sienna",
|
|
531
|
+
"silver",
|
|
532
|
+
"skyblue",
|
|
533
|
+
"slateblue",
|
|
534
|
+
"slategray",
|
|
535
|
+
"slategrey",
|
|
536
|
+
"snow",
|
|
537
|
+
"springgreen",
|
|
538
|
+
"steelblue",
|
|
539
|
+
"tan",
|
|
540
|
+
"teal",
|
|
541
|
+
"thistle",
|
|
542
|
+
"tomato",
|
|
543
|
+
"turquoise",
|
|
544
|
+
"violet",
|
|
545
|
+
"wheat",
|
|
546
|
+
"white",
|
|
547
|
+
"whitesmoke",
|
|
548
|
+
"yellow",
|
|
549
|
+
"yellowgreen",
|
|
550
|
+
]);
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Default exemptions for raw color rule. Combines policy-supplied `except`
|
|
554
|
+
* with universally meaningful keywords that aren't true colors.
|
|
555
|
+
*/
|
|
556
|
+
export const COLOR_KEYWORD_EXEMPTIONS = new Set([
|
|
557
|
+
"transparent",
|
|
558
|
+
"currentcolor",
|
|
559
|
+
"inherit",
|
|
560
|
+
"initial",
|
|
561
|
+
"unset",
|
|
562
|
+
]);
|
|
563
|
+
|
|
564
|
+
export function isExemptColor(value: string, except: readonly string[]): boolean {
|
|
565
|
+
const lower = value.trim().toLowerCase();
|
|
566
|
+
if (COLOR_KEYWORD_EXEMPTIONS.has(lower)) return true;
|
|
567
|
+
for (const ex of except) {
|
|
568
|
+
if (ex.toLowerCase() === lower) return true;
|
|
569
|
+
}
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Render a token name as the reference an author would write: SCSS `$vars`
|
|
575
|
+
* pass through, dotted/bare names become a `var(--…)` custom property. Shared by
|
|
576
|
+
* the color and spacing rules so a "use the token" fix is expressed the same way
|
|
577
|
+
* everywhere.
|
|
578
|
+
*/
|
|
579
|
+
export function tokenReference(name: string): string {
|
|
580
|
+
if (name.startsWith("$")) return name;
|
|
581
|
+
const cssVarName = name.startsWith("--") ? name : `--${name.replace(/\./g, "-")}`;
|
|
582
|
+
return `var(${cssVarName})`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Whether a token reference can be *applied* automatically (written verbatim
|
|
587
|
+
* and still compile). A `css-var` / `scss-var` token resolves on its own; a
|
|
588
|
+
* `scss-map` member (synthetic `$colors-primary` minted from a Sass map) or a
|
|
589
|
+
* `dtcg` JSON token (no CSS custom property emitted by default) does not, so
|
|
590
|
+
* those fixes are surfaced as suggestions rather than deterministic rewrites.
|
|
591
|
+
* Tokens without a known format (legacy/hand-built facts) are treated as safe.
|
|
592
|
+
*/
|
|
593
|
+
export function isApplicableTokenReference(
|
|
594
|
+
referenceFormat: TokenDefinitionFact["referenceFormat"]
|
|
595
|
+
): boolean {
|
|
596
|
+
return (
|
|
597
|
+
referenceFormat === undefined || referenceFormat === "css-var" || referenceFormat === "scss-var"
|
|
598
|
+
);
|
|
599
|
+
}
|