@reqvire-org/ads 0.3.0 → 0.3.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.
Files changed (56) hide show
  1. package/README.md +1 -1
  2. package/dist/bootstrap-ds/ADS.md +47 -7
  3. package/dist/bootstrap-ds/README.md +12 -10
  4. package/dist/bootstrap-ds/design-system/index.ts +1 -1
  5. package/dist/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +4 -0
  6. package/dist/bootstrap-ds/design-system/patterns/token-preview/TokenPreview.tsx +31 -0
  7. package/dist/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +8 -0
  8. package/dist/bootstrap-ds/design-system/tokens/Foundation.fixture.ts +44 -43
  9. package/dist/bootstrap-ds/design-system/tokens/Foundation.scenario.tsx +3 -13
  10. package/dist/bootstrap-ds/design-system/tokens/base.css +4 -1
  11. package/dist/bootstrap-ds/design-system/tokens/colors.css +2 -2
  12. package/dist/bootstrap-ds/ds.config.json +10 -0
  13. package/dist/check.mjs +29 -13
  14. package/dist/contracts/foundation.json +211 -0
  15. package/dist/foundation.mjs +114 -0
  16. package/dist/init.mjs +22 -5
  17. package/dist/mcp/apps.mjs +2 -2
  18. package/dist/mcp/build-status.mjs +22 -9
  19. package/dist/mcp/context.mjs +1 -1
  20. package/dist/mcp/preview-watch.mjs +35 -12
  21. package/dist/mcp/preview-widget.bundle.js +40 -38
  22. package/dist/mcp/project-widget.bundle.js +42 -38
  23. package/dist/mcp/prompts.mjs +1 -1
  24. package/dist/mcp/server.mjs +5 -5
  25. package/dist/preview-resources.mjs +1 -1
  26. package/dist/scenarios-compiler.mjs +86 -43
  27. package/dist/scenarios.mjs +16 -5
  28. package/dist/skills/ads-assimilate/SKILL.md +47 -12
  29. package/dist/skills/ads-assimilate/references/cli.md +3 -2
  30. package/dist/skills/ads-break-ui/SKILL.md +47 -12
  31. package/dist/skills/ads-break-ui/references/verification.md +4 -0
  32. package/dist/skills/ads-designer/SKILL.md +49 -14
  33. package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +47 -7
  34. package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +12 -10
  35. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/index.ts +1 -1
  36. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +4 -0
  37. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/TokenPreview.tsx +31 -0
  38. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +8 -0
  39. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/Foundation.fixture.ts +44 -43
  40. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/Foundation.scenario.tsx +3 -13
  41. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/base.css +4 -1
  42. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/colors.css +2 -2
  43. package/dist/skills/ads-designer/assets/bootstrap-ds/ds.config.json +10 -0
  44. package/dist/skills/ads-designer/references/cli.md +3 -2
  45. package/dist/skills/ads-import/SKILL.md +47 -12
  46. package/dist/skills/ads-import/references/cli.md +3 -2
  47. package/dist/skills/ads-motion/SKILL.md +47 -12
  48. package/dist/token-policy.mjs +12 -0
  49. package/dist/token-samples.mjs +8 -0
  50. package/dist/units.mjs +34 -10
  51. package/docs/ADS.md +52 -9
  52. package/docs/ARCHITECTURE.md +44 -3
  53. package/docs/CHANGELOG.md +32 -1
  54. package/docs/GUIDE.md +17 -6
  55. package/docs/MCP.md +62 -9
  56. package/package.json +3 -2
package/README.md CHANGED
@@ -272,7 +272,7 @@ The [dummy project](tests/fixtures/guard-project/README.md) tests the generic gu
272
272
  There are two different starting points in this checkout:
273
273
 
274
274
  - [bootstrap-ds/](bootstrap-ds/README.md) is an empty React application starter:
275
- token defaults, build setup, empty component/pattern folders, and a project preview compiler adapter. It is bundled with `ads-designer` so an agent can copy it when starting
275
+ token defaults, build setup, empty product component folders, a project-owned token review pattern, and a project preview compiler adapter. It is bundled with `ads-designer` so an agent can copy it when starting
276
276
  from zero. `ads init` uses its empty foundation files when a project has no ADS.
277
277
  - [design-system/](design-system/README.md) is this repository's working example,
278
278
  with real components, patterns, and scenarios inspected through the built-in showcase. It demonstrates the architecture
@@ -40,6 +40,35 @@ Token names and prefixes are fixed by ADS architecture; foreign brand prefixes
40
40
  are rejected. Shared assignments belong in token files. Components own
41
41
  `--ds-*`, patterns `--ux-*`, and the showcase `--showcase-*`; token files cannot
42
42
  assign those local variables or override component state.
43
+
44
+ Foundation roles are enforced by `foundation-role`, `foundation-selector` and
45
+ `foundation-import`, alongside the regular token checks. Shared definitions
46
+ belong on the document root or explicit root themes; ordinary declarations belong
47
+ in `base.css`, and font-face descriptors in optional `fonts.css`. The entry imports
48
+ canonical sheets; those sheets cannot import another styling layer. Component
49
+ classes, IDs, data/ARIA hooks, contextual selectors, keyframes and scoped UI rules
50
+ are rejected in the foundation, including nested functional selectors. Raw visual
51
+ values and undefined token references are checked in native defaults too; literal
52
+ font-face metadata keeps its separate asset role.
53
+
54
+ `base.css` has three closed roles:
55
+
56
+ - Document defaults: root/body typography, canvas colors and theme behavior,
57
+ global text selection and document scrollbars.
58
+ - Native resets: permitted normalization properties and values, such as zero
59
+ spacing, inherited control typography, list resets and responsive replaced media.
60
+ - Global accessibility: universal `:focus-visible` outline rules, and reduced-motion
61
+ overrides inside `@media (prefers-reduced-motion: reduce)`.
62
+
63
+ The `foundation-role` check validates every selector branch, property and value.
64
+ Native selectors are not a bypass: token-backed button backgrounds, flex/grid
65
+ layout, component-specific focus rules and hover/disabled skins are rejected.
66
+ Shared token definitions belong in value sheets, not `base.css`. No project token
67
+ names are embedded in this policy. A zero-specificity `:where()` wrapper does not
68
+ change ownership. Put component design and animation in Linaria owners, and
69
+ scenario layout variations in typed public review-pattern props. Browser review
70
+ still checks the effects and accessibility of permitted defaults.
71
+
43
72
  Own primitive markup, focus, hover, selected, disabled, loading, and motion in
44
73
  the primitive. Use typed props for variations. Keep reusable structure and skin
45
74
  in colocated Linaria blocks using `css` and `cx` from `@linaria/atomic`.
@@ -114,13 +143,24 @@ design-system/
114
143
  Flat owners may use `Button.scenario.tsx` and `Button.fixture.ts` instead.
115
144
  `Scenario.tsx` exports a default independently mountable view and literal metadata
116
145
  `export const scenario = { title: "Button states", covers: ["Button"] };`.
117
- Token entries under the configured token directory also declare `tokens`:
118
- `export const scenario = { title: "Typography", covers: ["TokenSamples"], tokens: fixture.tokens };`.
119
- Metadata may read literal const fixture fields; it cannot run expressions,
120
- functions, wildcard lists or token discovery. The same fixture list must be
121
- consumed by the default entry. `covers: []` is valid for a minimal token value
122
- inspection entry without public UI; styled samples use a real public review
123
- pattern. Names and sample text belong to the fixture, values to foundation CSS.
146
+ Token entries under the configured token directory declare
147
+ `export const scenario = { title: fixture.title, covers: ["TokenSamples"], samples: fixture.samples };`.
148
+ `TokenSamples` is any project-owned public review pattern, not a required name.
149
+ Its colocated fixture declares literal `samples: [{ token, property, probe }]`.
150
+ Each token must exist and have sample coverage; duplicate bindings, missing probes,
151
+ empty covers and value-only inspectors fail. The default entry passes the declared
152
+ samples to a rendered public pattern's `samples` prop. No token-name/property mapping
153
+ or sample values are embedded in the tool. Display values come from foundation CSS;
154
+ probes are isolated verification inputs owned by the fixture.
155
+
156
+ The renderer marks the actual styled element with `data-ads-token` and
157
+ `data-ads-property`. In a browser, the App validates the declared property/value and
158
+ probe using `CSS.supports`, checks the sample is rendered, then temporarily changes
159
+ that token to its fixture probe. Its computed sample property must respond. Values
160
+ are restored before yielding to painting; hardcoded lookalikes fail. Source gates
161
+ prove the contract structure, not successful rendering. The browser check covers
162
+ the mounted scenario and theme; visual quality and other states still need review.
163
+
124
164
  It imports real public UI through `@ds` and its local fixture. The adapter supplies
125
165
  initial state, callbacks and simulated services. The default entry needs no
126
166
  mandatory caller inputs or prior page initialization. Related parts may share a
@@ -1,8 +1,8 @@
1
1
  # Empty native design system
2
2
 
3
- This private React/TypeScript workspace contains semantic CSS tokens, an empty
4
- public barrel, Linaria extraction and a project preview compiler adapter. It has
5
- no components, patterns, icons, fonts or example screens. The minimal showcase
3
+ This private React/TypeScript workspace contains semantic CSS tokens, a project-owned
4
+ token review pattern, a public barrel, Linaria extraction and a preview compiler
5
+ adapter. It has no product components, icons, fonts or example screens. The minimal showcase
6
6
  shell and MCP client come from ADS tooling.
7
7
 
8
8
  Read the local [contract](ADS.md) and the
@@ -13,7 +13,7 @@ Read the local [contract](ADS.md) and the
13
13
  Copy the contents of `bootstrap-ds/`, including dotfiles, into a new project.
14
14
  The installed `ads-designer` skill contains the same files in
15
15
  `assets/bootstrap-ds/`. Set the project name in `package.json`. Use Node 22.12 or
16
- newer and ADS 0.3 or later for the shared-resource preview compiler.
16
+ newer and ADS 0.3.1 or later for the shared-resource preview compiler.
17
17
 
18
18
  ```sh
19
19
  npx @reqvire-org/ads init --framework react
@@ -25,7 +25,7 @@ npm run dev:showcase
25
25
  For unreleased tooling, run init using the real local package:
26
26
 
27
27
  ```sh
28
- ads_tool_package="/absolute/path/to/reqvire-org-ads-0.3.0.tgz"
28
+ ads_tool_package="/absolute/path/to/reqvire-org-ads-0.3.1.tgz"
29
29
  npx --yes --package "$ads_tool_package" ads init --framework react --tool-package "$ads_tool_package"
30
30
  npm install
31
31
  npm run build
@@ -44,7 +44,7 @@ native Preact app without a saved ADS framework, use `ads init --framework preac
44
44
  Init records that choice and generates native hook imports without migrating the
45
45
  app or rewriting its compiler. Subsequent init runs reuse the saved choice.
46
46
  Init adds missing foundation files and connects architectural checks; it does
47
- not configure Linaria or generate component source. Generated `.ds/` files are
47
+ not configure Linaria or generate product component source. Generated `.ds/` files are
48
48
  ignored; original artwork and provenance belong in `assets/brand/`.
49
49
 
50
50
  ## Inspect and add UI
@@ -56,10 +56,12 @@ Its MCP client discovers entries, invokes preview tools and follows notification
56
56
  There are no required TokensPage/ComponentsPage/PatternsPage/CompositionPage
57
57
  files, and no separate project browser app to implement or rebuild.
58
58
 
59
- The existing `tokens/Foundation.scenario.tsx` and colocated fixture inspect all
60
- starter token values. Add/remove fixture names alongside token definitions;
61
- `ads check` rejects missing coverage and invented names. Styled token samples
62
- belong to your public review patterns and mount through the same scenario pipeline.
59
+ The existing `tokens/Foundation.scenario.tsx` and colocated fixture visually demonstrate all
60
+ starter token values. The fixture owns `{ token, property, probe }` bindings;
61
+ the public `TokenPreview` pattern owns their presentation and applies the real CSS
62
+ variables. Add/remove samples alongside token definitions. `ads check` rejects
63
+ missing coverage and invented names; the App verifies the declared CSS bindings
64
+ in the browser. Extend or replace the project-owned pattern as your tokens evolve.
63
65
 
64
66
  Create a component under `design-system/components/`, export it through `@ds`,
65
67
  and colocate `Scenario.tsx` plus `fixture.ts` (or `<Owner>.scenario.tsx` and
@@ -2,4 +2,4 @@
2
2
  import "./styles.css";
3
3
 
4
4
  // Add public component and pattern exports here as they are implemented.
5
- export {};
5
+ export { TokenPreview } from "./patterns/token-preview/TokenPreview";
@@ -0,0 +1,4 @@
1
+ import { TokenPreview } from "@ds";
2
+ import { fixture } from "./fixture";
3
+ export const scenario = { title: fixture.title, covers: ["TokenPreview"] };
4
+ export default function Scenario() { return <TokenPreview {...fixture} />; }
@@ -0,0 +1,31 @@
1
+ import { useLayoutEffect, useRef } from "react";
2
+ import { css, cx } from "@linaria/atomic";
3
+
4
+ export interface TokenSample { token: `--${string}`; property: string; }
5
+ export interface TokenPreviewProps { title: string; samples: readonly TokenSample[]; text: string; }
6
+ const sampleStyle = css`
7
+ display: inline-block;
8
+ position: relative;
9
+ white-space: pre-line;
10
+ &[data-ads-property="animation-duration"] { animation-duration: var(--ux-token-preview-value); }
11
+ &[data-ads-property="animation-timing-function"] { animation-timing-function: var(--ux-token-preview-value); }
12
+ &[data-ads-property="background-color"] { background-color: var(--ux-token-preview-value); }
13
+ &[data-ads-property="border-radius"] { border-radius: var(--ux-token-preview-value); }
14
+ &[data-ads-property="box-shadow"] { box-shadow: var(--ux-token-preview-value); }
15
+ &[data-ads-property="color"] { color: var(--ux-token-preview-value); }
16
+ &[data-ads-property="font-family"] { font-family: var(--ux-token-preview-value); }
17
+ &[data-ads-property="font-size"] { font-size: var(--ux-token-preview-value); }
18
+ &[data-ads-property="font-weight"] { font-weight: var(--ux-token-preview-value); }
19
+ &[data-ads-property="line-height"] { line-height: var(--ux-token-preview-value); }
20
+ &[data-ads-property="width"] { width: var(--ux-token-preview-value); }
21
+ &[data-ads-property="z-index"] { z-index: var(--ux-token-preview-value); }
22
+ `;
23
+ function Sample({ sample, text }: { sample: TokenSample; text: string }) {
24
+ const ref = useRef<HTMLSpanElement>(null);
25
+ useLayoutEffect(() => { ref.current?.style.setProperty("--ux-token-preview-value", `var(${sample.token})`); }, [sample.token]);
26
+ return <span ref={ref} className={cx("ux-token-preview__sample", sampleStyle)} data-ads-token={sample.token} data-ads-property={sample.property}>{text}</span>;
27
+ }
28
+ /** Project-owned review UI. Values always come from the real foundation CSS. */
29
+ export function TokenPreview({ title, samples, text }: TokenPreviewProps) {
30
+ return <section className="ux-token-preview"><h1>{title}</h1>{samples.map(sample => <div key={`${sample.token}:${sample.property}`} className="ux-token-preview__item"><code className="ux-token-preview__name">{sample.token}</code><Sample sample={sample} text={text}/></div>)}</section>;
31
+ }
@@ -0,0 +1,8 @@
1
+ export const fixture = {
2
+ "title": "Token preview",
3
+ "text": "Aa",
4
+ samples: [
5
+ {"token": "--accent", "property": "background-color", "probe": "rgb(31, 97, 163)"},
6
+ {"token": "--space-4", "property": "width", "probe": "13.375px"},
7
+ ]
8
+ } as const;
@@ -1,47 +1,48 @@
1
1
  export const fixture = {
2
2
  "title": "Foundation",
3
- "tokens": [
4
- "--accent",
5
- "--accent-fg",
6
- "--accent-hover",
7
- "--bg-canvas",
8
- "--bg-hover",
9
- "--bg-surface",
10
- "--border-default",
11
- "--border-focus",
12
- "--border-w",
13
- "--dur-fast",
14
- "--dur-normal",
15
- "--ease-standard",
16
- "--focus-offset",
17
- "--focus-w",
18
- "--font-mono",
19
- "--font-sans",
20
- "--leading-normal",
21
- "--leading-tight",
22
- "--radius-lg",
23
- "--radius-md",
24
- "--radius-sm",
25
- "--shadow-raised",
26
- "--shadow-xs",
27
- "--space-0",
28
- "--space-12",
29
- "--space-16",
30
- "--space-2",
31
- "--space-4",
32
- "--space-6",
33
- "--space-8",
34
- "--text-base",
35
- "--text-body",
36
- "--text-inverse",
37
- "--text-lg",
38
- "--text-muted",
39
- "--text-sm",
40
- "--text-strong",
41
- "--text-xl",
42
- "--weight-medium",
43
- "--weight-normal",
44
- "--weight-semibold",
45
- "--z-overlay"
3
+ "text": "Aa",
4
+ samples: [
5
+ {"token": "--accent", "property": "background-color", "probe": "rgb(31, 97, 163)"},
6
+ {"token": "--accent-fg", "property": "background-color", "probe": "rgb(31, 97, 163)"},
7
+ {"token": "--accent-hover", "property": "background-color", "probe": "rgb(31, 97, 163)"},
8
+ {"token": "--bg-canvas", "property": "background-color", "probe": "rgb(31, 97, 163)"},
9
+ {"token": "--bg-hover", "property": "background-color", "probe": "rgb(31, 97, 163)"},
10
+ {"token": "--bg-surface", "property": "background-color", "probe": "rgb(31, 97, 163)"},
11
+ {"token": "--border-default", "property": "background-color", "probe": "rgb(31, 97, 163)"},
12
+ {"token": "--border-focus", "property": "background-color", "probe": "rgb(31, 97, 163)"},
13
+ {"token": "--border-w", "property": "width", "probe": "13.375px"},
14
+ {"token": "--dur-fast", "property": "animation-duration", "probe": "0.375s"},
15
+ {"token": "--dur-normal", "property": "animation-duration", "probe": "0.375s"},
16
+ {"token": "--ease-standard", "property": "animation-timing-function", "probe": "cubic-bezier(.13,.27,.61,.83)"},
17
+ {"token": "--focus-offset", "property": "width", "probe": "13.375px"},
18
+ {"token": "--focus-w", "property": "width", "probe": "13.375px"},
19
+ {"token": "--font-mono", "property": "font-family", "probe": "monospace, serif"},
20
+ {"token": "--font-sans", "property": "font-family", "probe": "monospace, serif"},
21
+ {"token": "--leading-normal", "property": "line-height", "probe": "2.375"},
22
+ {"token": "--leading-tight", "property": "line-height", "probe": "2.375"},
23
+ {"token": "--radius-lg", "property": "border-radius", "probe": "13.375px"},
24
+ {"token": "--radius-md", "property": "border-radius", "probe": "13.375px"},
25
+ {"token": "--radius-sm", "property": "border-radius", "probe": "13.375px"},
26
+ {"token": "--shadow-raised", "property": "box-shadow", "probe": "1px 3px 5px rgb(31, 97, 163)"},
27
+ {"token": "--shadow-xs", "property": "box-shadow", "probe": "1px 3px 5px rgb(31, 97, 163)"},
28
+ {"token": "--space-0", "property": "width", "probe": "13.375px"},
29
+ {"token": "--space-12", "property": "width", "probe": "13.375px"},
30
+ {"token": "--space-16", "property": "width", "probe": "13.375px"},
31
+ {"token": "--space-2", "property": "width", "probe": "13.375px"},
32
+ {"token": "--space-4", "property": "width", "probe": "13.375px"},
33
+ {"token": "--space-6", "property": "width", "probe": "13.375px"},
34
+ {"token": "--space-8", "property": "width", "probe": "13.375px"},
35
+ {"token": "--text-base", "property": "font-size", "probe": "41.375px"},
36
+ {"token": "--text-body", "property": "color", "probe": "rgb(31, 97, 163)"},
37
+ {"token": "--text-inverse", "property": "color", "probe": "rgb(31, 97, 163)"},
38
+ {"token": "--text-lg", "property": "font-size", "probe": "41.375px"},
39
+ {"token": "--text-muted", "property": "color", "probe": "rgb(31, 97, 163)"},
40
+ {"token": "--text-sm", "property": "font-size", "probe": "41.375px"},
41
+ {"token": "--text-strong", "property": "color", "probe": "rgb(31, 97, 163)"},
42
+ {"token": "--text-xl", "property": "font-size", "probe": "41.375px"},
43
+ {"token": "--weight-medium", "property": "font-weight", "probe": "327"},
44
+ {"token": "--weight-normal", "property": "font-weight", "probe": "327"},
45
+ {"token": "--weight-semibold", "property": "font-weight", "probe": "327"},
46
+ {"token": "--z-overlay", "property": "z-index", "probe": "367"},
46
47
  ]
47
48
  } as const;
@@ -1,14 +1,4 @@
1
- import { useEffect, useState } from "react";
1
+ import { TokenPreview } from "@ds";
2
2
  import { fixture } from "./Foundation.fixture";
3
- export const scenario = { title: fixture.title, covers: [], tokens: fixture.tokens };
4
- export default function Scenario() {
5
- const [values, setValues] = useState<Record<string, string>>({});
6
- useEffect(() => {
7
- const read = () => { const style = getComputedStyle(document.documentElement); setValues(Object.fromEntries(fixture.tokens.map(name => [name, style.getPropertyValue(name).trim()]))); };
8
- read();
9
- const observer = new MutationObserver(read);
10
- observer.observe(document.documentElement, {attributes: true, attributeFilter: ["data-theme"]});
11
- return () => observer.disconnect();
12
- }, []);
13
- return <section><h1>{fixture.title}</h1><dl>{fixture.tokens.map(name => <div key={name}><dt><code>{name}</code></dt><dd>{values[name]}</dd></div>)}</dl></section>;
14
- }
3
+ export const scenario = { title: fixture.title, covers: ["TokenPreview"], samples: fixture.samples };
4
+ export default function Scenario() { return <TokenPreview {...fixture} />; }
@@ -1,3 +1,6 @@
1
+ :root { color-scheme: light; }
2
+ :root[data-theme="dark"] { color-scheme: dark; }
3
+
1
4
  *,
2
5
  *::before,
3
6
  *::after {
@@ -5,7 +8,7 @@
5
8
  }
6
9
 
7
10
  body {
8
- margin: var(--space-0);
11
+ margin: 0;
9
12
  background: var(--bg-canvas);
10
13
  color: var(--text-body);
11
14
  font-family: var(--font-sans);
@@ -1,5 +1,5 @@
1
1
  :root {
2
- color-scheme: light;
2
+
3
3
  --bg-canvas: #faf9f6;
4
4
  --bg-surface: #fff;
5
5
  --bg-hover: #f1efea;
@@ -15,7 +15,7 @@
15
15
  }
16
16
 
17
17
  :root[data-theme="dark"] {
18
- color-scheme: dark;
18
+
19
19
  --bg-canvas: #17191e;
20
20
  --bg-surface: #20232a;
21
21
  --bg-hover: #2c3039;
@@ -9,5 +9,15 @@
9
9
  "--watch"
10
10
  ]
11
11
  }
12
+ },
13
+ "boundaries": {
14
+ "styleWrites": [
15
+ {
16
+ "file": "design-system/patterns/token-preview/TokenPreview.tsx",
17
+ "properties": [
18
+ "--ux-token-preview-value"
19
+ ]
20
+ }
21
+ ]
12
22
  }
13
23
  }
package/dist/check.mjs CHANGED
@@ -5,7 +5,7 @@ import postcss from "postcss";
5
5
  import valueParser from "postcss-value-parser";
6
6
  import { importViolation, isInside, loadConfig, sourceFiles, isDevelopmentFile, isTestFile } from "./config.mjs";
7
7
  import { inspectTokens, references } from "./tokens.mjs";
8
- import { foundationFiles, isSharedToken } from "./token-policy.mjs";
8
+ import { foundationFiles, foundationImportTarget, isSharedToken } from "./token-policy.mjs";
9
9
  import { inspectAssets } from "./assets.mjs";
10
10
  import { appImportViolation, showcaseImportViolation, checkComponentVariable, importResolver, projectCompilerOptions, isUiCssProperties, styleWriteViolation } from "./boundaries.mjs";
11
11
  import { inspectPublicProps } from "./public-props.mjs";
@@ -14,6 +14,7 @@ import { inspectShowcasePages } from "./showcase.mjs";
14
14
  import { inspectShowcaseCoverage } from "./showcase-coverage.mjs";
15
15
  import { inspectScenarioCoverage } from "./scenarios.mjs";
16
16
  import { uiOperation, importedUiApi } from "./ui-framework.mjs";
17
+ import { foundationDeclaration, foundationSelector } from "./foundation.mjs";
17
18
 
18
19
  const showcaseControls = new Set(["a", "button", "input", "select", "textarea", "option", "optgroup", "table", "thead", "tbody", "tfoot", "tr", "td", "th", "dialog", "details", "summary", "progress", "meter"]);
19
20
  const svgTags = new Set(["svg", "g", "path", "circle", "rect", "ellipse", "line", "polyline", "polygon", "text", "tspan", "use", "defs", "clipPath", "mask", "filter", "linearGradient", "radialGradient", "stop", "marker", "pattern"]);
@@ -51,23 +52,17 @@ export function checkProject(directory, { trackingRoot } = {}) {
51
52
  const css = postcss.parse(readFileSync(foundationEntry, "utf8"), { from: foundationEntry });
52
53
  const imports = css.nodes.filter((node) => node.type !== "comment");
53
54
  const expected = foundationFiles.filter((name) => name !== "fonts.css" || existsSync(path.join(dsRoot, config.tokenDir, name))).map((name) => path.join(dsRoot, config.tokenDir, name));
54
- const actual = imports.map((node) => {
55
- if (node.type !== "atrule" || node.name !== "import" || node.nodes) return null;
56
- const parts = valueParser(node.params).nodes.filter((part) => part.type !== "space" && part.type !== "comment");
57
- if (parts.length !== 1) return null;
58
- const part = parts[0];
59
- const value = part.type === "string" ? part.value : part.type === "function" && part.value === "url" && part.nodes.length === 1 ? part.nodes[0].value : null;
60
- return value ? path.resolve(path.dirname(foundationEntry), value) : null;
61
- });
55
+ const actual = imports.map(node => foundationImportTarget(node, foundationEntry));
62
56
  if (actual.length !== expected.length || actual.some((file, index) => file !== expected[index] || !existsSync(file))) report(foundationEntry, "styles-entry", "Foundation CSS must import only fonts (when present), colors, typography, spacing, elevation, and base, in that order.");
63
57
  } catch (error) { report(foundationEntry, "css-syntax", error.reason ?? error.message, error.line ?? 1); }
64
- function checkCss(file, text, offset = { line: 1, column: 1 }) {
58
+ function checkCss(file, text, offset = { line: 1, column: 1 }, foundation = false) {
65
59
  let css;
66
60
  try { css = postcss.parse(text, { from: file }); }
67
61
  catch (error) { report(file, "css-syntax", error.reason ?? error.message, offset.line + (error.line ?? 1) - 1); return; }
68
- if (isDevelopmentFile(file, options) || !isInside(path.join(dsRoot, "components"), file) && !isInside(path.join(dsRoot, "patterns"), file) && file !== foundationEntry) report(file, "style-ownership", "Visual CSS belongs to the local ADS's runtime components or patterns.");
62
+ if (!foundation && (isDevelopmentFile(file, options) || !isInside(path.join(dsRoot, "components"), file) && !isInside(path.join(dsRoot, "patterns"), file) && file !== foundationEntry)) report(file, "style-ownership", "Visual CSS belongs to the local ADS's runtime components or patterns.");
69
63
  if (isDevelopmentFile(file, options) && css.nodes.some((node) => node.type !== "comment" && !(node.type === "atrule" && node.name === "import"))) report(file, "showcase-style", "Scenarios and fixtures define no styles. Put the frame/layout in a public ADS pattern and product UI in ADS components or patterns.", offset.line);
70
64
  css.walkAtRules("import", (rule) => {
65
+ if (foundation) report(file, "foundation-import", "Import foundation sheets through the configured styles entry. Token sheets cannot import an unchecked styling layer.", rule.source.start.line);
71
66
  const first = valueParser(rule.params).nodes.find((node) => node.type !== "space" && node.type !== "comment");
72
67
  const specifier = first?.type === "string" ? first.value : first?.type === "function" && first.value === "url" ? first.nodes[0]?.value : null;
73
68
  if (!specifier || /^(?:[a-z]+:|\/\/)/i.test(specifier)) return;
@@ -76,19 +71,40 @@ export function checkProject(directory, { trackingRoot } = {}) {
76
71
  if (showcaseImportViolation(specifier, file, options, resolveImport)) report(file, "showcase-import", "Showcase code and styles are development fixtures; runtime code must not import them.", line);
77
72
  });
78
73
  css.walkRules((rule) => {
74
+ if (foundation) {
75
+ if (rule.parent.type === "rule") report(file, "foundation-role", "Foundation rules cannot nest owner/context selectors. Use root theme conditions or Linaria owners.", rule.source.start.line);
76
+ const violation = foundationSelector(rule.selector, { definitions: rule.nodes.some(node => node.type === "decl" && node.prop.startsWith("--")) });
77
+ if (violation) report(file, "foundation-selector", violation, rule.source.start.line, rule.source.start.column);
78
+ }
79
79
  if (isInside(path.join(dsRoot, "showcase"), file) && /:(?:hover|active|focus(?:-visible|-within)?|checked|disabled|enabled)\b|\[(?:aria-(?:selected|pressed|checked|disabled|expanded)|data-state)\b/.test(rule.selector)) report(file, "showcase-state", "Interactive state styling belongs to the real ADS component or pattern, not showcase or mock styles.", offset.line + rule.source.start.line - 1);
80
80
  if (/\.ads-[\w-]+/.test(rule.selector) && !isInside(path.join(dsRoot, "components"), file)) report(file, "style-ownership", "ADS component internals are owned by components, not app or showcase CSS.", offset.line + rule.source.start.line - 1);
81
81
  if (/\.ux-[\w-]+/.test(rule.selector) && !isInside(path.join(dsRoot, "patterns"), file)) report(file, "style-ownership", "Pattern internals are owned by patterns, not app or showcase CSS.", offset.line + rule.source.start.line - 1);
82
82
  if (/\.showcase-[\w-]+/.test(rule.selector) && !isInside(path.join(dsRoot, "showcase"), file)) report(file, "style-ownership", "Showcase internals are owned by showcase scaffolding.", offset.line + rule.source.start.line - 1);
83
83
  });
84
+ if (foundation) css.walkAtRules(rule => {
85
+ if (/(?:^|-)keyframes$|^(?:scope|custom-selector|starting-style)$/i.test(rule.name)) report(file, "foundation-role", "Animations and scoped UI styles belong in their component/pattern's Linaria source, not the token foundation.", rule.source.start.line);
86
+ if (rule.name === "font-face" && path.basename(file) !== "fonts.css") report(file, "foundation-role", "Font faces belong in the canonical fonts.css foundation sheet.", rule.source.start.line);
87
+ });
84
88
  css.walkDecls((decl) => {
89
+ if (foundation && decl.prop.startsWith("--")) {
90
+ if (path.basename(file) === "base.css") report(file, "foundation-role", "Shared token definitions belong in value sheets, not base.css.", decl.source.start.line, decl.source.start.column);
91
+ return; // Definition names, values and aliases are validated by inspectTokens.
92
+ }
93
+ const fontDescriptor = foundation && path.basename(file) === "fonts.css" && decl.parent.type === "atrule" && decl.parent.name === "font-face";
94
+ if (foundation && !fontDescriptor && (path.basename(file) !== "base.css" || decl.parent.type !== "rule")) report(file, "foundation-role", "Token sheets contain shared definitions; native defaults belong in base.css, font descriptors in fonts.css, and UI styling in Linaria owners.", decl.source.start.line, decl.source.start.column);
95
+ if (foundation && path.basename(file) === "base.css" && decl.parent.type === "rule") {
96
+ const violation = foundationDeclaration(decl.parent, decl);
97
+ if (violation) report(file, "foundation-role", violation, decl.source.start.line, decl.source.start.column);
98
+ }
85
99
  const variable = checkComponentVariable(decl.prop, file, options);
86
100
  if (variable) report(file, variable.rule, variable.message, offset.line + decl.source.start.line - 1, decl.source.start.column);
87
101
  for (const ref of references(decl.value)) {
88
102
  const local = /^(?:--ds-|--ux-|--showcase-)/.test(ref.name);
103
+ if (foundation && local) report(file, "foundation-role", "Foundation defaults consume shared tokens, not component, pattern or scenario-local variables.", offset.line + decl.source.start.line - 1, decl.source.start.column);
89
104
  if (!local && !isSharedToken(ref.name)) report(file, "token-name", `${ref.name} does not follow ADS's fixed token naming contract.`, offset.line + decl.source.start.line - 1, decl.source.start.column);
90
105
  else if (!local && !declaredTokens.has(ref.name) && !ref.fallback) report(file, "token-reference", `Undefined shared token ${ref.name}.`, offset.line + decl.source.start.line - 1, decl.source.start.column);
91
106
  }
107
+ if (fontDescriptor) return; // Font metadata defines source assets, not token-backed UI styles.
92
108
  const violations = new Set();
93
109
  valueParser(decl.value).walk((node) => {
94
110
  if (node.type === "function" && node.value === "url") return false;
@@ -98,12 +114,12 @@ export function checkProject(directory, { trackingRoot } = {}) {
98
114
  if (node.type === "word" && /^ease(?:-in|-out|-in-out)?$/.test(node.value)) violations.add("Use a motion token for easing.");
99
115
  if (node.type === "word" && /^(?:color|background-color|(?:border|outline|caret|text-decoration|column-rule)(?:-[a-z]+)?-color|fill|stroke)$/.test(decl.prop) && /^[a-z]+$/i.test(node.value) && !/^(?:inherit|initial|unset|revert|currentcolor|transparent|none)$/i.test(node.value)) violations.add("Use a semantic color token.");
100
116
  });
101
- if (/^(?:font-family|font-weight|z-index)$/.test(decl.prop) && !/^(?:var\(|inherit$|auto$)/.test(decl.value)) violations.add("Use a semantic typography or layering token.");
117
+ if (/^(?:font-family|font-weight|z-index)$/.test(decl.prop) && !/^(?:var\(|inherit$|initial$|unset$|revert(?:-layer)?$|auto$)/.test(decl.value) && !(foundation && decl.prop === "font-weight" && /^(?:bolder|lighter)$/.test(decl.value))) violations.add("Use a semantic typography or layering token.");
102
118
  for (const message of violations) report(file, "style-token", message, offset.line + decl.source.start.line - 1, decl.source.start.column);
103
119
  });
104
120
  }
105
121
  for (const file of files) {
106
- if (file.endsWith(".css") && isInside(path.join(dsRoot, config.tokenDir), file) && !isDevelopmentFile(file, options)) continue;
122
+ if (file.endsWith(".css") && isInside(path.join(dsRoot, config.tokenDir), file) && !isDevelopmentFile(file, options)) { checkCss(file, readFileSync(file, "utf8"), undefined, true); continue; }
107
123
  if (file.endsWith(".html")) { inspectHtml(readFileSync(file, "utf8"), (rule, message, line) => report(file, rule, message, line)); continue; }
108
124
  if (file.endsWith(".css")) {
109
125
  const text = readFileSync(file, "utf8");