@weatherboard/gyde-design 0.4.0 → 0.4.2
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/README.md +69 -1
- package/agentdocs.mjs +38 -1
- package/catalogue.mjs +33 -2
- package/cli.mjs +188 -19
- package/emit.mjs +89 -27
- package/index.mjs +2 -1
- package/markup.mjs +2 -2
- package/normalise.mjs +115 -0
- package/package.json +3 -2
- package/props.mjs +8 -5
- package/ratchet.mjs +191 -31
- package/ruleindex.mjs +138 -0
- package/rules.mjs +17 -0
- package/scope.mjs +195 -0
- package/selfgate.mjs +162 -0
- package/theme-choice.mjs +111 -0
- package/tokens.mjs +2 -0
- package/wiring.mjs +59 -4
- package/workflow.mjs +1 -1
package/emit.mjs
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
38
|
import { generateCss, SEED, varName, CSS_HEADER } from "./tokens.mjs";
|
|
39
|
+
import { emitThemeChoice } from "./theme-choice.mjs";
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* The version of what this emits.
|
|
@@ -60,7 +61,7 @@ import { generateCss, SEED, varName, CSS_HEADER } from "./tokens.mjs";
|
|
|
60
61
|
* `emit.test.mjs` enforces it: a fresh scaffold must pass the gate Gyde would
|
|
61
62
|
* run against it.
|
|
62
63
|
*/
|
|
63
|
-
export const SCAFFOLD_VERSION = "0.3.
|
|
64
|
+
export const SCAFFOLD_VERSION = "0.3.2";
|
|
64
65
|
|
|
65
66
|
const GENERATED = (what) => `/* GENERATED BY GYDE — then yours.
|
|
66
67
|
*
|
|
@@ -199,8 +200,8 @@ export function generateCss(dict) {
|
|
|
199
200
|
" }", "}", "",
|
|
200
201
|
"/* An explicit choice, on any element — which is what makes both themes",
|
|
201
202
|
" renderable on one page, and what a portalled popup looks up. */",
|
|
202
|
-
'[data-theme="dark"] {', decls("dark"), "}", "",
|
|
203
|
-
'[data-theme="light"] {', decls("light"), "}", "",
|
|
203
|
+
'[data-theme="dark"] {', ' color-scheme: dark;', decls("dark"), "}", "",
|
|
204
|
+
'[data-theme="light"] {', ' color-scheme: light;', decls("light"), "}", "",
|
|
204
205
|
].join("\\n");
|
|
205
206
|
}
|
|
206
207
|
`;
|
|
@@ -385,11 +386,25 @@ const defaultsModule = `${GENERATED("absence of a \"use client\" directive in th
|
|
|
385
386
|
* both graphs and its values survive on the server.
|
|
386
387
|
*/
|
|
387
388
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
389
|
+
/**
|
|
390
|
+
* G-127. EVERY prop a component declares appears here, except its children.
|
|
391
|
+
*
|
|
392
|
+
* The optional-props ban (G-68) removed the other way of saying "this one has
|
|
393
|
+
* an obvious answer". A destructure default — \`type = "button"\` — cannot say it
|
|
394
|
+
* any more, because the declaration it excused is gone. So the obvious answer
|
|
395
|
+
* moves here, where a caller opts into it explicitly by spreading, and the
|
|
396
|
+
* component itself makes no decision on anybody's behalf.
|
|
397
|
+
*
|
|
398
|
+
* The consequence to keep true: \`<Button {...BUTTON_DEFAULTS} onClick={...}>text</Button>\`
|
|
399
|
+
* must type-check. If a prop is added to a component and not added here, that
|
|
400
|
+
* sentence stops being true, and the only thing that will tell you is the
|
|
401
|
+
* scaffold's own gate.
|
|
402
|
+
*/
|
|
403
|
+
export const BUTTON_DEFAULTS = { tone: "neutral", emphasis: "filled", size: "md", type: "button", disabled: false, describedBy: null } as const;
|
|
404
|
+
export const TEXT_DEFAULTS = { role: "body", as: null } as const;
|
|
405
|
+
export const CARD_DEFAULTS = { tone: "default", inset: "md", as: "div" } as const;
|
|
406
|
+
export const CHECKBOX_DEFAULTS = { disabled: false, indeterminate: false, name: null } as const;
|
|
407
|
+
export const SELECT_DEFAULTS = { disabled: false, name: null } as const;
|
|
393
408
|
`;
|
|
394
409
|
|
|
395
410
|
/* -------------------------------------------------------------- components */
|
|
@@ -403,6 +418,22 @@ export const SELECT_DEFAULTS = { disabled: false } as const;
|
|
|
403
418
|
*/
|
|
404
419
|
export const SEED_COMPONENTS = ["Button", "Text", "Card", "Checkbox", "Select"];
|
|
405
420
|
|
|
421
|
+
/**
|
|
422
|
+
* G-127. Which defaults constant belongs to which component.
|
|
423
|
+
*
|
|
424
|
+
* Derived rather than hard-coded looks tidier — `Button` -> `BUTTON_DEFAULTS`
|
|
425
|
+
* is a `toUpperCase()` away — and it would emit a barrel export for a constant
|
|
426
|
+
* `defaults.ts` does not declare the moment somebody emits a set that is not
|
|
427
|
+
* the seed five. A map is the thing that can be wrong out loud.
|
|
428
|
+
*/
|
|
429
|
+
export const DEFAULTS_EXPORTS = {
|
|
430
|
+
Button: "BUTTON_DEFAULTS",
|
|
431
|
+
Text: "TEXT_DEFAULTS",
|
|
432
|
+
Card: "CARD_DEFAULTS",
|
|
433
|
+
Checkbox: "CHECKBOX_DEFAULTS",
|
|
434
|
+
Select: "SELECT_DEFAULTS",
|
|
435
|
+
};
|
|
436
|
+
|
|
406
437
|
function componentSources(primitive) {
|
|
407
438
|
return {
|
|
408
439
|
"Button.tsx": `"use client";
|
|
@@ -434,13 +465,22 @@ export type ButtonProps = {
|
|
|
434
465
|
* deliberate.
|
|
435
466
|
*/
|
|
436
467
|
onClick: (() => void) | null;
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
468
|
+
/**
|
|
469
|
+
* Required, with no default in the destructure. \`type = "button"\` looked like
|
|
470
|
+
* the same thing and is not: it put the answer where no caller could see it.
|
|
471
|
+
* BUTTON_DEFAULTS carries it instead.
|
|
472
|
+
*/
|
|
473
|
+
type: "button" | "submit";
|
|
474
|
+
disabled: boolean;
|
|
475
|
+
/**
|
|
476
|
+
* A disabled control with no explanation is the most common accessibility
|
|
477
|
+
* defect there is. Required so the absence is a decision, nullable because
|
|
478
|
+
* "there is nothing to point at" is a real answer to it.
|
|
479
|
+
*/
|
|
480
|
+
describedBy: string | null;
|
|
441
481
|
};
|
|
442
482
|
|
|
443
|
-
export function Button({ children, tone, emphasis, size, onClick, type
|
|
483
|
+
export function Button({ children, tone, emphasis, size, onClick, type, disabled, describedBy }: ButtonProps) {
|
|
444
484
|
return (
|
|
445
485
|
<Base
|
|
446
486
|
// focusableWhenDisabled: a disabled button that leaves the tab order is a
|
|
@@ -448,7 +488,7 @@ export function Button({ children, tone, emphasis, size, onClick, type = "button
|
|
|
448
488
|
focusableWhenDisabled
|
|
449
489
|
type={type}
|
|
450
490
|
disabled={disabled}
|
|
451
|
-
aria-describedby={describedBy}
|
|
491
|
+
aria-describedby={describedBy ?? undefined}
|
|
452
492
|
onClick={onClick ?? undefined}
|
|
453
493
|
data-ds="button"
|
|
454
494
|
data-tone={tone}
|
|
@@ -478,7 +518,11 @@ export type TextRole = "micro" | "small" | "body" | "lead" | "title" | "display"
|
|
|
478
518
|
export type TextProps = {
|
|
479
519
|
children: ReactNode;
|
|
480
520
|
role: TextRole;
|
|
481
|
-
|
|
521
|
+
/**
|
|
522
|
+
* Nullable rather than optional: null means "take the element this role
|
|
523
|
+
* implies", which is a choice, and DEFAULT_ELEMENT below is where it is read.
|
|
524
|
+
*/
|
|
525
|
+
as: "p" | "span" | "code" | null;
|
|
482
526
|
};
|
|
483
527
|
|
|
484
528
|
const DEFAULT_ELEMENT: Record<TextRole, "p" | "span"> = {
|
|
@@ -530,10 +574,15 @@ export type CardProps = {
|
|
|
530
574
|
children: ReactNode;
|
|
531
575
|
tone: CardTone;
|
|
532
576
|
inset: CardInset;
|
|
533
|
-
|
|
577
|
+
/**
|
|
578
|
+
* Not nullable. Unlike Text there is no role to derive an element from, so
|
|
579
|
+
* "no answer" would have to mean \`div\` silently — which is the omission the
|
|
580
|
+
* ban is about. CARD_DEFAULTS says \`div\` out loud.
|
|
581
|
+
*/
|
|
582
|
+
as: "div" | "section" | "article" | "li";
|
|
534
583
|
};
|
|
535
584
|
|
|
536
|
-
export function Card({ children, tone, inset, as: Tag
|
|
585
|
+
export function Card({ children, tone, inset, as: Tag }: CardProps) {
|
|
537
586
|
return <Tag data-ds="card" data-tone={tone} data-inset={inset}>{children}</Tag>;
|
|
538
587
|
}
|
|
539
588
|
`,
|
|
@@ -560,17 +609,18 @@ export type CheckboxProps = {
|
|
|
560
609
|
onChange: (checked: boolean) => void;
|
|
561
610
|
label: string;
|
|
562
611
|
id: string;
|
|
563
|
-
name
|
|
564
|
-
|
|
565
|
-
|
|
612
|
+
/** Nullable: a checkbox outside a form has no name, and that is an answer. */
|
|
613
|
+
name: string | null;
|
|
614
|
+
disabled: boolean;
|
|
615
|
+
indeterminate: boolean;
|
|
566
616
|
};
|
|
567
617
|
|
|
568
|
-
export function Checkbox({ checked, onChange, label, id, name, disabled
|
|
618
|
+
export function Checkbox({ checked, onChange, label, id, name, disabled, indeterminate }: CheckboxProps) {
|
|
569
619
|
return (
|
|
570
620
|
<span data-ds="checkbox">
|
|
571
621
|
<Base.Root
|
|
572
622
|
id={id}
|
|
573
|
-
name={name}
|
|
623
|
+
name={name ?? undefined}
|
|
574
624
|
checked={checked}
|
|
575
625
|
indeterminate={indeterminate}
|
|
576
626
|
disabled={disabled}
|
|
@@ -610,18 +660,19 @@ export type SelectProps = {
|
|
|
610
660
|
onChange: (value: string) => void;
|
|
611
661
|
options: readonly SelectOption[];
|
|
612
662
|
label: string;
|
|
613
|
-
disabled
|
|
614
|
-
name
|
|
663
|
+
disabled: boolean;
|
|
664
|
+
/** Nullable: a select outside a form has no name, and that is an answer. */
|
|
665
|
+
name: string | null;
|
|
615
666
|
};
|
|
616
667
|
|
|
617
|
-
export function Select({ value, onChange, options, label, disabled
|
|
668
|
+
export function Select({ value, onChange, options, label, disabled, name }: SelectProps) {
|
|
618
669
|
const trigger = useRef<HTMLDivElement>(null);
|
|
619
670
|
|
|
620
671
|
return (
|
|
621
672
|
<div ref={trigger} data-ds="select">
|
|
622
673
|
<Base.Root
|
|
623
674
|
value={value}
|
|
624
|
-
name={name}
|
|
675
|
+
name={name ?? undefined}
|
|
625
676
|
disabled={disabled}
|
|
626
677
|
// The primitive can clear to null; this control has no clear affordance,
|
|
627
678
|
// so narrowing here keeps the callback honest about what can happen.
|
|
@@ -770,6 +821,7 @@ function stylesheet(dict) {
|
|
|
770
821
|
export function emitSystem({ dict = SEED, primitive = "@base-ui/react", path = "packages/design-system", components = SEED_COMPONENTS, tokensPackage = "@scaffold/design-tokens", systemPackage = "@scaffold/design-system" } = {}) {
|
|
771
822
|
const sources = componentSources(primitive);
|
|
772
823
|
const out = {};
|
|
824
|
+
Object.assign(out, emitThemeChoice({ path, storageKey: `${systemPackage}:theme` }));
|
|
773
825
|
|
|
774
826
|
for (const name of components) {
|
|
775
827
|
const src = sources[`${name}.tsx`];
|
|
@@ -789,7 +841,7 @@ export function emitSystem({ dict = SEED, primitive = "@base-ui/react", path = "
|
|
|
789
841
|
type: "module",
|
|
790
842
|
main: "./src/index.ts",
|
|
791
843
|
types: "./src/index.ts",
|
|
792
|
-
exports: { ".": "./src/index.ts", "./styles.css": "./src/styles.css" },
|
|
844
|
+
exports: { ".": "./src/index.ts", "./styles.css": "./src/styles.css", "./theme-choice": "./src/theme-choice.js", "./theme-bootstrap.js": "./src/theme-bootstrap.js" },
|
|
793
845
|
// The primitive is a real dependency of THIS package and of nothing else —
|
|
794
846
|
// the boundary rule forbids an app from even declaring it.
|
|
795
847
|
dependencies: { [primitive]: "^1.0.0", [tokensPackage]: "workspace:*" },
|
|
@@ -800,6 +852,16 @@ export function emitSystem({ dict = SEED, primitive = "@base-ui/react", path = "
|
|
|
800
852
|
out[`${path}/src/index.ts`] =
|
|
801
853
|
`${GENERATED("completeness of this barrel — set.test.ts fails if a component is missing")}
|
|
802
854
|
${components.map((n) => `export { ${n} } from "./${n}";\nexport type { ${n}Props } from "./${n}";`).join("\n")}
|
|
855
|
+
/**
|
|
856
|
+
* G-127. The defaults are part of the public API, not an internal detail.
|
|
857
|
+
*
|
|
858
|
+
* Every prop is required (G-68), so \`{...BUTTON_DEFAULTS}\` is how a caller says
|
|
859
|
+
* "the ordinary answers, please" without restating six of them. Leaving these
|
|
860
|
+
* out of the barrel left the only ergonomic path behind a deep import into
|
|
861
|
+
* \`src/\`, which the boundary rules forbid — so the ban had no usable escape
|
|
862
|
+
* and every call site paid for it.
|
|
863
|
+
*/
|
|
864
|
+
export { ${components.filter((n) => DEFAULTS_EXPORTS[n]).map((n) => DEFAULTS_EXPORTS[n]).join(", ")} } from "./defaults";
|
|
803
865
|
export { themeVars, themeVar } from "./theme";
|
|
804
866
|
export type { SurfaceTheme, ThemeSlot } from "./theme";
|
|
805
867
|
`;
|
package/index.mjs
CHANGED
|
@@ -21,7 +21,7 @@ export { SEED, themed, validate, generateCss, generateConstants, generateDtcg, g
|
|
|
21
21
|
export { seedBoundaries, seedDependencyBoundaries, validateBoundaries, proveBoundaries, proveDependencyBoundaries, loadBoundaries, loadDependencyBoundaries, isImportBreach, checkBoundaries, checkDependencyBoundaries, findVersionDrift } from "./boundaries.mjs";
|
|
22
22
|
export { record, compare, gate, formatGate, tally, LEDGER_NOTE } from "./ratchet.mjs";
|
|
23
23
|
|
|
24
|
-
export { emitTokens, emitSystem, emitConfig, SEED_COMPONENTS, SCAFFOLD_VERSION, NOT_UPGRADEABLE } from "./emit.mjs";
|
|
24
|
+
export { emitTokens, emitSystem, emitConfig, SEED_COMPONENTS, DEFAULTS_EXPORTS, SCAFFOLD_VERSION, NOT_UPGRADEABLE } from "./emit.mjs";
|
|
25
25
|
export { emitCatalogue, CATALOGUE_ENTRIES } from "./catalogue.mjs";
|
|
26
26
|
export { emitAgentDocs, agentDoc, agentInstructionsFragment } from "./agentdocs.mjs";
|
|
27
27
|
|
|
@@ -37,4 +37,5 @@ export { reconcile, formatAdoption, LAYERS as ADOPTION_LAYERS } from "./adoption
|
|
|
37
37
|
export { classifyMarkup, markupAdoption, formatMarkup, KIND as MARKUP_KIND } from "./markup.mjs";
|
|
38
38
|
export { orphanedParts, formatOrphanedParts, RULE as COMPOUND_RULE } from "./compound.mjs";
|
|
39
39
|
export { checkDocDrift, importedFrom, formatDocDrift, namedInProse, isContractDoc, isFixtureDeclaration, stalePaths, rootDirectories, formatStalePaths, RULE as DOC_DRIFT_RULE, PROSE_RULE, PATH_RULE } from "./docdrift.mjs";
|
|
40
|
+
export { rules, rulesJson } from "./ruleindex.mjs";
|
|
40
41
|
export { isClientModule, parseImports, parseReexports, valueUses, checkClientBoundary, formatClientBoundary } from "./clientboundary.mjs";
|
package/markup.mjs
CHANGED
|
@@ -117,7 +117,7 @@ export function classifyMarkup(text, { systemNames = null } = {}) {
|
|
|
117
117
|
counts,
|
|
118
118
|
// The population where "should this have been a component?" is a real
|
|
119
119
|
// question. Never the total element count.
|
|
120
|
-
adoption: { used: counts.system, of, percent: of ? Math.
|
|
120
|
+
adoption: { used: counts.system, of, percent: of ? Math.floor((counts.system / of) * 100) : null },
|
|
121
121
|
bespokeElements,
|
|
122
122
|
};
|
|
123
123
|
}
|
|
@@ -149,7 +149,7 @@ export function markupAdoption(files, { systemNames = null } = {}) {
|
|
|
149
149
|
return {
|
|
150
150
|
unknown: false,
|
|
151
151
|
counts: totals,
|
|
152
|
-
adoption: { used: totals.system, of, percent: of ? Math.
|
|
152
|
+
adoption: { used: totals.system, of, percent: of ? Math.floor((totals.system / of) * 100) : null },
|
|
153
153
|
perFile: perFile.sort((a, b) => (a.percent ?? 101) - (b.percent ?? 101)),
|
|
154
154
|
filesWithMarkup: perFile.length,
|
|
155
155
|
};
|
package/normalise.mjs
CHANGED
|
@@ -342,6 +342,111 @@ export function normaliseUtilities(classString, { spacingStep = TAILWIND_SPACING
|
|
|
342
342
|
return out;
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Does this quoted run read as a class list, or as an English sentence?
|
|
347
|
+
*
|
|
348
|
+
* G-110. The unattached-string matcher below exists for `cva("… rounded-md
|
|
349
|
+
* px-4 …")`, which carries no `className`. Its gate was a word test — the run
|
|
350
|
+
* had to mention `shadow`, `border`, `flex` and so on — and the word `shadow`
|
|
351
|
+
* is *also* a complete Tailwind utility. So
|
|
352
|
+
*
|
|
353
|
+
* includes(f, "gl.shadowMap.enabled", "parks renderer shadow map must be enabled")
|
|
354
|
+
*
|
|
355
|
+
* parsed as a class list containing one class, and a Node assertion harness
|
|
356
|
+
* with no markup in it at all scored four `arbitrary-shadow` violations. A
|
|
357
|
+
* false positive of that shape cannot be fixed — nobody can tokenise an English
|
|
358
|
+
* sentence — so the only available response is a ledger entry, and the ledger is
|
|
359
|
+
* supposed to only ever get shorter.
|
|
360
|
+
*
|
|
361
|
+
* The distinguishing property is not vocabulary, it is SHAPE. Utility classes
|
|
362
|
+
* are overwhelmingly compound — `rounded-md`, `px-4`, `hover:bg-primary`,
|
|
363
|
+
* `w-[7px]` — while English words are bare. So: at least two compound tokens,
|
|
364
|
+
* and compound tokens in the majority.
|
|
365
|
+
*
|
|
366
|
+
* A ratio rather than "every token", deliberately. Real class lists are full of
|
|
367
|
+
* bare utilities (`flex`, `grid`, `border`, `truncate`, `italic`) and that set
|
|
368
|
+
* is open-ended; requiring all of them to be recognised is the widening that
|
|
369
|
+
* ends in a rule which matches nothing. This test is checked in both directions
|
|
370
|
+
* by fixtures: prose must not parse, and `cva("inline-flex … shadow-sm")` must.
|
|
371
|
+
*/
|
|
372
|
+
const COMPOUND_CLASS = /^-?[a-z0-9]+[a-z0-9./[\]-]*(?:[-:/[][^\s]*)$/i;
|
|
373
|
+
|
|
374
|
+
export function looksLikeClassList(run) {
|
|
375
|
+
const tokens = String(run).trim().split(/\s+/).filter(Boolean);
|
|
376
|
+
if (tokens.length < 3) return false;
|
|
377
|
+
const compound = tokens.filter((t) => /[-:/[]/.test(t) && COMPOUND_CLASS.test(t)).length;
|
|
378
|
+
return compound >= 2 && compound * 2 > tokens.length;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Is this `prop: value` inside a string literal a declaration, or a sentence
|
|
383
|
+
* with a colon in it?
|
|
384
|
+
*
|
|
385
|
+
* G-110, the same defect reached by the other matcher. The style-object pass
|
|
386
|
+
* reads `prop: value` out of raw line text, so
|
|
387
|
+
*
|
|
388
|
+
* assert(ok, "expected background: #ffffff to be applied")
|
|
389
|
+
*
|
|
390
|
+
* produced an `untokenised-colour` finding against a message.
|
|
391
|
+
*
|
|
392
|
+
* Refusing string literals wholesale would be wrong in the expensive direction,
|
|
393
|
+
* and it was measured before it was reasoned about: a consumer repository builds
|
|
394
|
+
* markup as strings —
|
|
395
|
+
*
|
|
396
|
+
* html += '<div style="display:flex;gap:8px;margin-bottom:20px">'
|
|
397
|
+
*
|
|
398
|
+
* — and blanket-skipping literals silently dropped **seven real findings** in
|
|
399
|
+
* four files. A styled-components template is the same shape. So the test is
|
|
400
|
+
* per-declaration and structural: the `;{}`-delimited SEGMENT the match sits in
|
|
401
|
+
* must be exactly `ident: value`, with nothing either side of it. `gap:8px` is;
|
|
402
|
+
* `expected background: #ffffff to be applied` is not, because of the words in
|
|
403
|
+
* front of the colon.
|
|
404
|
+
*
|
|
405
|
+
* The residual is narrow and worth naming: a sentence that BEGINS with a real
|
|
406
|
+
* CSS property name and a plausible value — `"color: red is expected"` — still
|
|
407
|
+
* parses. It takes a segment boundary to hide behind, and no such string exists
|
|
408
|
+
* in either repository measured.
|
|
409
|
+
*/
|
|
410
|
+
const DECL_SEGMENT = /^\s*-{0,2}[A-Za-z][\w$-]*\s*:\s*[^:;{}]+$/;
|
|
411
|
+
// Nested quotes are a boundary too: the whole reason these literals contain
|
|
412
|
+
// declarations is `style="…"` inside built markup, and without it the first
|
|
413
|
+
// declaration in every inline style block keeps the `<div style="` prefix and
|
|
414
|
+
// fails the test. That was measured — it cost `background:#4a3000` on the first
|
|
415
|
+
// attempt at this fix.
|
|
416
|
+
const SEGMENT_BOUNDARY = /[;{}"'`]/;
|
|
417
|
+
|
|
418
|
+
/** The `;{}"'`-delimited segment of `text` containing offset `at`. */
|
|
419
|
+
export function segmentAround(text, at) {
|
|
420
|
+
let start = at;
|
|
421
|
+
while (start > 0 && !SEGMENT_BOUNDARY.test(text[start - 1])) start--;
|
|
422
|
+
let end = at;
|
|
423
|
+
while (end < text.length && !SEGMENT_BOUNDARY.test(text[end])) end++;
|
|
424
|
+
return text.slice(start, end);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Single-line string literal spans on a line, as [start, end) over the QUOTED text. */
|
|
428
|
+
function stringSpans(line) {
|
|
429
|
+
const spans = [];
|
|
430
|
+
let i = 0;
|
|
431
|
+
while (i < line.length) {
|
|
432
|
+
const ch = line[i];
|
|
433
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
434
|
+
let j = i + 1;
|
|
435
|
+
while (j < line.length) {
|
|
436
|
+
if (line[j] === "\\") { j += 2; continue; }
|
|
437
|
+
if (line[j] === ch) break;
|
|
438
|
+
j++;
|
|
439
|
+
}
|
|
440
|
+
if (j >= line.length) return spans; // unterminated: a multi-line template
|
|
441
|
+
spans.push({ start: i, end: j + 1, content: line.slice(i + 1, j) });
|
|
442
|
+
i = j + 1;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
i++;
|
|
446
|
+
}
|
|
447
|
+
return spans;
|
|
448
|
+
}
|
|
449
|
+
|
|
345
450
|
function utilityProperty(prefix) {
|
|
346
451
|
if (prefix === "rounded") return PROP.RADIUS;
|
|
347
452
|
if (prefix === "text") return PROP.TYPE;
|
|
@@ -395,6 +500,9 @@ export function normaliseSource(text, { filename = "", rootFontSize = 16, spacin
|
|
|
395
500
|
// the same string seen twice.
|
|
396
501
|
for (const m of line.matchAll(/["'`]([a-z0-9-]+(?:\s+[a-z0-9:./[\]-]+){2,})["'`]/gi)) {
|
|
397
502
|
if (overlaps(m.index, m.index + m[0].length)) continue;
|
|
503
|
+
// G-110. Shape first: a word test admits any sentence containing the word
|
|
504
|
+
// `shadow`, and `shadow` is itself a complete utility.
|
|
505
|
+
if (!looksLikeClassList(m[1])) continue;
|
|
398
506
|
if (/\b(rounded|text-|bg-|p-|px-|py-|m-|gap-|border|shadow|flex|grid|h-|w-)/.test(m[1])) {
|
|
399
507
|
claim(m.index, m.index + m[0].length);
|
|
400
508
|
at(normaliseUtilities(m[1], { spacingStep }));
|
|
@@ -402,7 +510,14 @@ export function normaliseSource(text, { filename = "", rootFontSize = 16, spacin
|
|
|
402
510
|
}
|
|
403
511
|
|
|
404
512
|
// Style objects: `borderRadius: 8`, `padding: "16px"`, `color: colors.ink`.
|
|
513
|
+
//
|
|
514
|
+
// G-110. A `prop: value` pair INSIDE a string literal is only styling if
|
|
515
|
+
// the whole literal is declaration shaped — `styled.div\`padding: 12px\`` is,
|
|
516
|
+
// `"expected background: #ffffff to be applied"` is not.
|
|
517
|
+
const literals = stringSpans(line);
|
|
405
518
|
for (const m of line.matchAll(/([A-Za-z][\w$]*)\s*:\s*("[^"]*"|'[^']*'|`[^`]*`|[\w$.()#%-]+)/g)) {
|
|
519
|
+
const inside = literals.find((s) => m.index > s.start && m.index < s.end - 1);
|
|
520
|
+
if (inside && !DECL_SEGMENT.test(segmentAround(inside.content, m.index - inside.start - 1))) continue;
|
|
406
521
|
const value = m[2].replace(/^["'`]|["'`]$/g, "");
|
|
407
522
|
at(normaliseDeclaration(m[1], value, { source: "object", rootFontSize }));
|
|
408
523
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weatherboard/gyde-design",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Scaffolds a design system into a product repository, then keeps auditing it.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/
|
|
10
|
+
"url": "git+https://github.com/Weatherboard-Studio/gyde.git",
|
|
11
11
|
"directory": "packages/design"
|
|
12
12
|
},
|
|
13
13
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"./workspace.mjs": "./workspace.mjs",
|
|
19
19
|
"./normalise.mjs": "./normalise.mjs",
|
|
20
20
|
"./rules.mjs": "./rules.mjs",
|
|
21
|
+
"./ruleindex.mjs": "./ruleindex.mjs",
|
|
21
22
|
"./scan.mjs": "./scan.mjs",
|
|
22
23
|
"./tokens.mjs": "./tokens.mjs",
|
|
23
24
|
"./boundaries.mjs": "./boundaries.mjs",
|
package/props.mjs
CHANGED
|
@@ -29,11 +29,14 @@
|
|
|
29
29
|
* repository starts. That is CHARTER §4 exactly: debt is recorded, never
|
|
30
30
|
* amnestied, and the list may only get shorter.
|
|
31
31
|
*
|
|
32
|
-
* The mechanism that lets it arrive at all is
|
|
33
|
-
* ledger
|
|
34
|
-
* (ratchet.mjs, `rulesKnownTo`).
|
|
35
|
-
*
|
|
36
|
-
*
|
|
32
|
+
* The mechanism that lets it arrive at all is `gate --record-new-rules`: a rule
|
|
33
|
+
* the ledger does not name fails, and the failure carries the command that
|
|
34
|
+
* records its findings as existing debt (ratchet.mjs, `rulesKnownTo`, `adopt`).
|
|
35
|
+
*
|
|
36
|
+
* G-68 made that recording automatic and this rule is the reason we know it was
|
|
37
|
+
* wrong — it shipped after one consumer's baseline and was absorbed on every
|
|
38
|
+
* run: 173 findings across 80 files, gate green, ledger silently growing. G-108
|
|
39
|
+
* kept the escape hatch and removed the silence.
|
|
37
40
|
*
|
|
38
41
|
* WHAT IT DELIBERATELY DOES NOT DO.
|
|
39
42
|
*
|