@reqvire-org/ads 0.3.18 → 0.3.20
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/dist/behaviors.mjs +5 -5
- package/dist/bootstrap-ds/ADS.md +2 -2
- package/dist/bootstrap-ds/README.md +1 -1
- package/dist/bootstrap-ds/design-system/index.ts +0 -1
- package/dist/bootstrap-ds/design-system/review/index.ts +1 -0
- package/dist/bootstrap-ds/design-system/tokens/Foundation.scenario.tsx +1 -1
- package/dist/bootstrap-ds/ds.config.json +1 -1
- package/dist/bootstrap-ds/tsconfig.json +18 -3
- package/dist/bootstrap-ds/vite.config.ts +1 -1
- package/dist/boundaries.mjs +4 -2
- package/dist/check.mjs +20 -13
- package/dist/config.mjs +13 -2
- package/dist/init.mjs +17 -14
- package/dist/layers.mjs +288 -0
- package/dist/lint-config.mjs +1 -1
- package/dist/mcp/context.mjs +2 -1
- package/dist/mcp/edit-preparation.mjs +3 -3
- package/dist/mcp/mutations.mjs +1 -1
- package/dist/mcp/prompts.mjs +1 -1
- package/dist/mcp/server.mjs +6 -2
- package/dist/mcp/widget.bundle.js +1 -1
- package/dist/oxlint.mjs +3 -1
- package/dist/preview-resources.mjs +11 -1
- package/dist/public-api.mjs +13 -6
- package/dist/scenario-trace.mjs +6 -6
- package/dist/scenarios-vite.mjs +2 -1
- package/dist/scenarios.mjs +10 -7
- package/dist/showcase/client.bundle.js +1 -1
- package/dist/showcase/sandbox.bundle.js +1 -1
- package/dist/skills/ads-break-ui/SKILL.md +21 -6
- package/dist/skills/ads-break-ui/references/guarded-editing.md +1 -1
- package/dist/skills/ads-break-ui/references/layers.md +123 -0
- package/dist/skills/ads-break-ui/references/review.md +96 -0
- package/dist/skills/ads-break-ui/references/scenarios.md +24 -3
- package/dist/skills/ads-designer/SKILL.md +22 -7
- package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +2 -2
- package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +1 -1
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/index.ts +0 -1
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/review/index.ts +1 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/Foundation.scenario.tsx +1 -1
- package/dist/skills/ads-designer/assets/bootstrap-ds/ds.config.json +1 -1
- package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.json +18 -3
- package/dist/skills/ads-designer/assets/bootstrap-ds/vite.config.ts +1 -1
- package/dist/skills/ads-designer/references/guarded-editing.md +1 -1
- package/dist/skills/ads-designer/references/layers.md +123 -0
- package/dist/skills/ads-designer/references/review.md +96 -0
- package/dist/skills/ads-designer/references/scenarios.md +24 -3
- package/dist/skills/ads-import/SKILL.md +21 -6
- package/dist/skills/ads-import/references/guarded-editing.md +1 -1
- package/dist/skills/ads-import/references/layers.md +123 -0
- package/dist/skills/ads-import/references/review.md +96 -0
- package/dist/skills/ads-import/references/scenarios.md +24 -3
- package/dist/skills/ads-migrate/SKILL.md +21 -6
- package/dist/skills/ads-migrate/references/guarded-editing.md +1 -1
- package/dist/skills/ads-migrate/references/layers.md +123 -0
- package/dist/skills/ads-migrate/references/review.md +96 -0
- package/dist/skills/ads-migrate/references/scenarios.md +24 -3
- package/dist/skills/ads-motion/SKILL.md +22 -7
- package/dist/skills/ads-motion/references/audit.md +1 -1
- package/dist/skills/ads-motion/references/guarded-editing.md +1 -1
- package/dist/skills/ads-motion/references/layers.md +123 -0
- package/dist/skills/ads-motion/references/motion-review.md +35 -0
- package/dist/skills/ads-motion/references/review.md +96 -35
- package/dist/skills/ads-motion/references/scenarios.md +24 -3
- package/dist/units.mjs +17 -14
- package/docs/ADS.md +32 -5
- package/docs/ARCHITECTURE.md +37 -4
- package/docs/BEHAVIORS.md +7 -0
- package/docs/CHANGELOG.md +23 -0
- package/docs/GUIDE.md +3 -2
- package/docs/MCP.md +27 -6
- package/package.json +1 -1
- package/dist/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
- package/dist/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
- /package/dist/bootstrap-ds/design-system/{patterns → review}/token-preview/TokenPreview.tsx +0 -0
- /package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/{patterns → review}/token-preview/TokenPreview.tsx +0 -0
package/dist/behaviors.mjs
CHANGED
|
@@ -3,10 +3,10 @@ import path from "node:path";
|
|
|
3
3
|
import { isBuiltin } from "node:module";
|
|
4
4
|
import ts from "typescript";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { isBehaviorFile, isDevelopmentFile, isInside, sourceFiles } from "./config.mjs";
|
|
6
|
+
import { isBehaviorFile, isDevelopmentFile, isInside, sourceFiles, isReviewFile } from "./config.mjs";
|
|
7
7
|
import { importResolver, projectCompilerOptions } from "./boundaries.mjs";
|
|
8
8
|
import { inspectUnits } from "./units.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import { scenarioApi } from "./public-api.mjs";
|
|
10
10
|
import { inspectScenarios } from "./scenarios.mjs";
|
|
11
11
|
import { uiOperation } from "./ui-framework.mjs";
|
|
12
12
|
|
|
@@ -31,7 +31,7 @@ export function inspectBehaviors(options, suppliedProgram, suppliedRegistry) {
|
|
|
31
31
|
if (!files.length) return { contracts: [], diagnostics: [] };
|
|
32
32
|
const program = suppliedProgram ?? ts.createProgram(sourceFiles(dsRoot).filter(file => /\.[cm]?[jt]sx?$/.test(file)), { ...projectCompilerOptions(options), allowJs: true });
|
|
33
33
|
const checker = program.getTypeChecker();
|
|
34
|
-
const apis = new Map(
|
|
34
|
+
const apis = new Map(scenarioApi(program, options).map(item => [item.name, item]));
|
|
35
35
|
const scenarios = (suppliedRegistry ?? inspectScenarios(options, program)).scenarios;
|
|
36
36
|
const diagnostics = [], contracts = [], owners = new Set();
|
|
37
37
|
const relative = file => path.relative(root, file).replaceAll("\\", "/");
|
|
@@ -45,7 +45,7 @@ export function inspectBehaviors(options, suppliedProgram, suppliedRegistry) {
|
|
|
45
45
|
value = parsed.data;
|
|
46
46
|
} catch (error) { report(error.message); continue; }
|
|
47
47
|
const unit = inventory.owner(inventory.relative(relative(file)));
|
|
48
|
-
if (!["components", "patterns", "controllers"].includes(unit?.split("/")[0])) { report("Behavior contracts belong to a component, pattern or
|
|
48
|
+
if (!["components", "patterns", "controllers", "review"].includes(unit?.split("/")[0])) { report("Behavior contracts belong to a component, pattern, controller or development review unit."); continue; }
|
|
49
49
|
if (owners.has(unit)) { report(`Only one behavior contract may describe ${unit}.`); continue; }
|
|
50
50
|
owners.add(unit);
|
|
51
51
|
function owned(target, field) {
|
|
@@ -61,7 +61,7 @@ export function inspectBehaviors(options, suppliedProgram, suppliedRegistry) {
|
|
|
61
61
|
if (!api || inventory.owner(inventory.relative(relative(api.file))) !== unit) report(`Subject ${name} must resolve to a public API owned by ${unit}: ${entry.api}`);
|
|
62
62
|
} else {
|
|
63
63
|
const source = owned(entry.source, `Subject ${name}`);
|
|
64
|
-
if (source && isDevelopmentFile(source, options)) report(`Subject ${name} must identify runtime implementation, not a fixture, scenario or check.`);
|
|
64
|
+
if (source && isDevelopmentFile(source, options) && !isReviewFile(source, options)) report(`Subject ${name} must identify runtime implementation, not a fixture, scenario or check.`);
|
|
65
65
|
if (source && entry.symbol) {
|
|
66
66
|
const ast = program.getSourceFile(source);
|
|
67
67
|
if (!ast || !checker.getSymbolsInScope(ast, ts.SymbolFlags.Value).some(symbol => symbol.name === entry.symbol && symbol.declarations?.some(declaration => declaration.getSourceFile() === ast))) report(`Subject ${name}: symbol ${entry.symbol} is not declared in ${entry.source}.`);
|
package/dist/bootstrap-ds/ADS.md
CHANGED
|
@@ -72,7 +72,7 @@ layout, component-specific focus rules and hover/disabled skins are rejected.
|
|
|
72
72
|
Shared token definitions belong in value sheets, not `base.css`. No project token
|
|
73
73
|
names are embedded in this policy. A zero-specificity `:where()` wrapper does not
|
|
74
74
|
change ownership. Put component design and animation in Linaria owners, and
|
|
75
|
-
scenario layout variations in typed
|
|
75
|
+
scenario layout variations in typed review-helper props. Browser review
|
|
76
76
|
still checks the effects and accessibility of permitted defaults.
|
|
77
77
|
|
|
78
78
|
Own primitive markup, focus, hover, selected, disabled, loading, and motion in
|
|
@@ -151,7 +151,7 @@ Flat owners may use `Button.scenario.tsx` and `Button.fixture.ts` instead.
|
|
|
151
151
|
`export const scenario = { title: "Button states", covers: ["Button"] };`.
|
|
152
152
|
Token entries under the configured token directory declare
|
|
153
153
|
`export const scenario = { title: fixture.title, covers: ["TokenSamples"], samples: fixture.samples };`.
|
|
154
|
-
`TokenSamples` is any project-owned
|
|
154
|
+
`TokenSamples` is any project-owned development-only review renderer, not a required name.
|
|
155
155
|
Its colocated fixture declares literal `samples: [{ token, property, probe }]`.
|
|
156
156
|
Each token must exist and have sample coverage; duplicate bindings, missing probes,
|
|
157
157
|
empty covers and value-only inspectors fail. The default entry passes the declared
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Empty native design system
|
|
2
2
|
|
|
3
3
|
This private React/TypeScript workspace contains semantic CSS tokens, a project-owned
|
|
4
|
-
token review
|
|
4
|
+
token review renderer, a public barrel, Linaria extraction and a preview compiler
|
|
5
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
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TokenPreview } from "./token-preview/TokenPreview";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TokenPreview } from "@ds";
|
|
1
|
+
import { TokenPreview } from "@ds/review";
|
|
2
2
|
import { fixture } from "./Foundation.fixture";
|
|
3
3
|
export const scenario = { title: fixture.title, covers: ["TokenPreview"], samples: fixture.samples };
|
|
4
4
|
export default function Scenario() { return <TokenPreview {...fixture} />; }
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2022",
|
|
4
|
-
"lib": [
|
|
4
|
+
"lib": [
|
|
5
|
+
"ES2022",
|
|
6
|
+
"DOM",
|
|
7
|
+
"DOM.Iterable"
|
|
8
|
+
],
|
|
5
9
|
"module": "ESNext",
|
|
6
10
|
"moduleResolution": "bundler",
|
|
7
11
|
"skipLibCheck": true,
|
|
@@ -14,7 +18,18 @@
|
|
|
14
18
|
"noUnusedParameters": true,
|
|
15
19
|
"noFallthroughCasesInSwitch": true,
|
|
16
20
|
"baseUrl": ".",
|
|
17
|
-
"paths": {
|
|
21
|
+
"paths": {
|
|
22
|
+
"@ds/review": [
|
|
23
|
+
"design-system/review/index.ts"
|
|
24
|
+
],
|
|
25
|
+
"@ds": [
|
|
26
|
+
"design-system/index.ts"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
18
29
|
},
|
|
19
|
-
"include": [
|
|
30
|
+
"include": [
|
|
31
|
+
"src",
|
|
32
|
+
"design-system",
|
|
33
|
+
"vite.config.ts"
|
|
34
|
+
]
|
|
20
35
|
}
|
|
@@ -7,7 +7,7 @@ import { linariaTagResolver } from "@reqvire-org/ads/linaria";
|
|
|
7
7
|
export default defineConfig({
|
|
8
8
|
plugins: [wyw({ tagResolver: linariaTagResolver }), react()],
|
|
9
9
|
resolve: {
|
|
10
|
-
alias: { "@ds": path.resolve(import.meta.dirname, "design-system/index.ts") },
|
|
10
|
+
alias: { "@ds/review": path.resolve(import.meta.dirname, "design-system/review/index.ts"), "@ds": path.resolve(import.meta.dirname, "design-system/index.ts") },
|
|
11
11
|
},
|
|
12
12
|
build: {
|
|
13
13
|
outDir: "dist",
|
package/dist/boundaries.mjs
CHANGED
|
@@ -5,14 +5,16 @@ import { isInside, isStateVariable, isTestFile, isDevelopmentFile } from "./conf
|
|
|
5
5
|
import { isSharedToken } from "./token-policy.mjs";
|
|
6
6
|
import { scopedTokenOwner } from "./scoped-tokens.mjs";
|
|
7
7
|
|
|
8
|
-
export function projectCompilerOptions({ root }) {
|
|
8
|
+
export function projectCompilerOptions({ root, config, dsRoot }) {
|
|
9
9
|
const file = ts.findConfigFile(root, ts.sys.fileExists, "tsconfig.json");
|
|
10
10
|
let compiler = {};
|
|
11
11
|
if (file && isInside(root, file)) {
|
|
12
12
|
const parsed = ts.readConfigFile(file, ts.sys.readFile);
|
|
13
13
|
if (!parsed.error) compiler = ts.parseJsonConfigFileContent(parsed.config, ts.sys, path.dirname(file)).options;
|
|
14
14
|
}
|
|
15
|
-
return compiler
|
|
15
|
+
return config && dsRoot ? { ...compiler, paths: { ...compiler.paths,
|
|
16
|
+
[config.publicAlias]: [path.join(dsRoot, config.publicEntry)],
|
|
17
|
+
[`${config.publicAlias}/review`]: [path.join(dsRoot, config.reviewEntry)] } } : compiler;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export function importResolver(options) {
|
package/dist/check.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import ts from "typescript";
|
|
4
4
|
import postcss from "postcss";
|
|
5
5
|
import valueParser from "postcss-value-parser";
|
|
6
|
-
import { importViolation, isInside, loadConfig, sourceFiles, isDevelopmentFile, isTestFile } from "./config.mjs";
|
|
6
|
+
import { importViolation, isInside, loadConfig, sourceFiles, isDevelopmentFile, isTestFile, isReviewFile } from "./config.mjs";
|
|
7
7
|
import { inspectTokens, references } from "./tokens.mjs";
|
|
8
8
|
import { foundationFiles, foundationImportTarget, isSharedToken } from "./token-policy.mjs";
|
|
9
9
|
import { inspectAssets } from "./assets.mjs";
|
|
@@ -25,8 +25,9 @@ import { createScenarioSlots } from "./scenario-slots.mjs";
|
|
|
25
25
|
import { createScenarioSelectors } from "./scenario-selectors.mjs";
|
|
26
26
|
import { architectureEvidence, failure } from "./evidence.mjs";
|
|
27
27
|
import { propertyRegistration } from "./property-registrations.mjs";
|
|
28
|
+
import { createLayerBoundaries, layerSourceFiles } from "./layers.mjs";
|
|
28
29
|
|
|
29
|
-
const scenarioLayoutGuidance = "Put demo-only sizing and framing in
|
|
30
|
+
const scenarioLayoutGuidance = "Put demo-only sizing and framing in the development-only review/ layer, imported through the separate review entry. Patterns are production UI. Keep required behavior in the runtime owner; add component props for deliberate product API needs.";
|
|
30
31
|
const inlineStyleGuidance = `Own visual styles in component/pattern Linaria. ${scenarioLayoutGuidance} Reserve inlineStyleFiles for documented measured geometry, not demo layout.`;
|
|
31
32
|
|
|
32
33
|
const showcaseControls = new Set(["a", "button", "input", "select", "textarea", "option", "optgroup", "table", "thead", "tbody", "tfoot", "tr", "td", "th", "dialog", "details", "summary", "progress", "meter"]);
|
|
@@ -52,16 +53,19 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
52
53
|
const rootHtml = path.join(root, "index.html");
|
|
53
54
|
const files = [...new Set([...sourceFiles(sourceRoot), ...sourceFiles(dsRoot), ...(existsSync(rootHtml) ? [rootHtml] : [])])];
|
|
54
55
|
const boundaryFiles = [...config.inlineStyleFiles, ...config.boundaries.styleWrites.map(entry => entry.file)].map(file => path.resolve(root, file)).filter(file => existsSync(file) && statSync(file).isFile());
|
|
55
|
-
const scriptFiles = [...new Set([...files, ...boundaryFiles])].filter((file) => /\.[cm]?[jt]sx?$/.test(file));
|
|
56
|
+
const scriptFiles = layerSourceFiles([...new Set([...files, ...boundaryFiles])].filter((file) => /\.[cm]?[jt]sx?$/.test(file)), options);
|
|
56
57
|
const compiler = projectCompilerOptions(options);
|
|
57
58
|
const program = ts.createProgram(scriptFiles, { ...compiler, paths: { ...compiler.paths, [config.publicAlias]: [path.join(dsRoot, config.publicEntry)] }, allowJs: true, noResolve: true, noLib: true, types: [], target: ts.ScriptTarget.Latest, module: ts.ModuleKind.ESNext, jsx: ts.JsxEmit.Preserve });
|
|
58
59
|
const checker = program.getTypeChecker();
|
|
59
60
|
const atomicCompositionReview = createAtomicCompositionReview(program, options);
|
|
60
61
|
const scenarioTrace = createScenarioTrace(program, options);
|
|
61
62
|
let scenarioSlot, scenarioSelector;
|
|
62
|
-
function report(file, rule, message, line = 1, column = 1) {
|
|
63
|
-
diagnostics.push({ file: path.relative(root, file).replaceAll("\\", "/"), rule, message, line, column });
|
|
63
|
+
function report(file, rule, message, line = 1, column = 1, detail = {}) {
|
|
64
|
+
diagnostics.push({ file: path.relative(root, file).replaceAll("\\", "/"), rule, message, line, column, ...detail });
|
|
64
65
|
}
|
|
66
|
+
const layers = createLayerBoundaries(program, options, report), layerStyles = [];
|
|
67
|
+
layers.inspectDependencies();
|
|
68
|
+
for (const file of layers.runtimeHelpers()) if (!files.includes(file)) files.push(file);
|
|
65
69
|
inspectStyleBoundaries(options, program, report);
|
|
66
70
|
inspectShowcasePages(options, program, resolveImport, report);
|
|
67
71
|
inspectShowcaseCoverage(options, program, report);
|
|
@@ -82,7 +86,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
82
86
|
let defined=false;css.walkDecls(registration.name,()=>{defined=true;});
|
|
83
87
|
if(!isSharedToken(registration.name)||!declaredTokens.has(registration.name)||!defined||["base.css","fonts.css"].includes(path.basename(file)))throw new Error("Shared @property registrations belong beside their real token definitions in foundation value sheets, not base.css or fonts.css.");
|
|
84
88
|
}else{
|
|
85
|
-
if(!owner||!/^(?:components|patterns)\//.test(owner))throw new Error("@property belongs to its
|
|
89
|
+
if(!owner||!/^(?:components|patterns|review)\//.test(owner))throw new Error("@property belongs to its component/pattern/review implementation or real token value sheet, never application, scenario, controller or shared reset styling.");
|
|
86
90
|
const violation=checkComponentVariable(registration.name,file,options);if(violation)throw new Error(violation.message);
|
|
87
91
|
}
|
|
88
92
|
for(const descriptor of rule.nodes??[])if(descriptor.type==="decl")for(const ref of references(descriptor.value)){
|
|
@@ -106,9 +110,11 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
106
110
|
let css;
|
|
107
111
|
try { css = postcss.parse(text, { from: file }); }
|
|
108
112
|
catch (error) { report(file, "css-syntax", error.reason ?? error.message, offset.line + (error.line ?? 1) - 1); return; }
|
|
113
|
+
layers.collectComponentStyles(file, css);
|
|
114
|
+
layerStyles.push({ file, css, offset });
|
|
109
115
|
checkRegistrations(file,css,offset);
|
|
110
|
-
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
|
|
111
|
-
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. ${scenarioLayoutGuidance}`, offset.line);
|
|
116
|
+
if (!foundation && !isReviewFile(file, options) && (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/patterns or development review implementations.");
|
|
117
|
+
if (isDevelopmentFile(file, options) && !isReviewFile(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. ${scenarioLayoutGuidance}`, offset.line);
|
|
112
118
|
css.walkAtRules("import", (rule) => {
|
|
113
119
|
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);
|
|
114
120
|
const first = valueParser(rule.params).nodes.find((node) => node.type !== "space" && node.type !== "comment");
|
|
@@ -126,7 +132,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
126
132
|
}
|
|
127
133
|
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);
|
|
128
134
|
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);
|
|
129
|
-
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
|
|
135
|
+
if (/\.ux-[\w-]+/.test(rule.selector) && !isInside(path.join(dsRoot, "patterns"), file) && !isReviewFile(file, options)) report(file, "style-ownership", "Pattern internals are owned by patterns, not app or scenario CSS.", offset.line + rule.source.start.line - 1);
|
|
130
136
|
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);
|
|
131
137
|
});
|
|
132
138
|
if (foundation) css.walkAtRules(rule => {
|
|
@@ -190,7 +196,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
190
196
|
report(file, "source-syntax", ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"), position.line + 1, position.character + 1);
|
|
191
197
|
}
|
|
192
198
|
const inlineAllowed = config.inlineStyleFiles.some((allowed) => path.resolve(root, allowed) === file);
|
|
193
|
-
const showcase = isDevelopmentFile(file, options) && !isTestFile(file);
|
|
199
|
+
const showcase = isDevelopmentFile(file, options) && !isTestFile(file) && !isReviewFile(file, options);
|
|
194
200
|
function nodeReport(node, rule, message, detail) {
|
|
195
201
|
const position = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
196
202
|
report(file, rule, message, position.line + 1, position.character + 1);
|
|
@@ -234,8 +240,8 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
234
240
|
function inspectClass(value) {
|
|
235
241
|
if (ts.isStringLiteralLike(value)) {
|
|
236
242
|
if (/(?:^|\s)ads-[\w-]+/.test(value.text) && !isInside(path.join(dsRoot, "components"), file)) nodeReport(value, "style-ownership", "Primitive ads-* classes must be emitted by components, not consumers.");
|
|
237
|
-
if (/(?:^|\s)ux-[\w-]+/.test(value.text) && !isInside(path.join(dsRoot, "patterns"), file)) nodeReport(value, "style-ownership", "Pattern ux-* classes must be emitted by patterns, not consumers.");
|
|
238
|
-
if (/(?:^|\s)showcase-[\w-]+/.test(value.text) && !isInside(path.join(dsRoot, "showcase"), file)) nodeReport(value, "style-ownership", "Showcase hooks must be emitted by
|
|
243
|
+
if (/(?:^|\s)ux-[\w-]+/.test(value.text) && !isInside(path.join(dsRoot, "patterns"), file) && !isReviewFile(file, options)) nodeReport(value, "style-ownership", "Pattern ux-* classes must be emitted by patterns, not consumers.");
|
|
244
|
+
if (/(?:^|\s)showcase-[\w-]+/.test(value.text) && !isInside(path.join(dsRoot, "showcase"), file) && !isReviewFile(file, options)) nodeReport(value, "style-ownership", "Showcase hooks must be emitted by development review code, not runtime implementations.");
|
|
239
245
|
}
|
|
240
246
|
ts.forEachChild(value, inspectClass);
|
|
241
247
|
}
|
|
@@ -295,7 +301,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
295
301
|
}
|
|
296
302
|
const mutation = styleWriteViolation(node, file, options);
|
|
297
303
|
if (mutation) nodeReport(node, "style-mutation", mutation);
|
|
298
|
-
if (!inlineAllowed && isUiCssProperties(node, checker)) nodeReport(node, "css-properties", "Keep CSSProperties out of the public API. Use deliberate component contracts or a documented measured-geometry renderer boundary; demo layout belongs in a
|
|
304
|
+
if (!inlineAllowed && isUiCssProperties(node, checker)) nodeReport(node, "css-properties", "Keep CSSProperties out of the public API. Use deliberate component contracts or a documented measured-geometry renderer boundary; demo layout belongs in a development-only review/ helper.");
|
|
299
305
|
if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "className" && node.initializer) {
|
|
300
306
|
inspectClass(node.initializer);
|
|
301
307
|
}
|
|
@@ -310,6 +316,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
310
316
|
visit(source);
|
|
311
317
|
}
|
|
312
318
|
// Bounded clients should see actionable warnings before source-analysis limits.
|
|
319
|
+
for (const { file, css, offset } of layerStyles) layers.inspectStyles(file, css, offset);
|
|
313
320
|
advisories.sort((first, second) => Number(first.severity === "info") - Number(second.severity === "info"));
|
|
314
321
|
return architectureEvidence({ ok: diagnostics.length === 0, diagnostics, ...(advisories.length ? { advisories } : {}) });
|
|
315
322
|
}
|
package/dist/config.mjs
CHANGED
|
@@ -11,6 +11,7 @@ export const defaultConfig = {
|
|
|
11
11
|
dsDir: "design-system",
|
|
12
12
|
publicAlias: "@ds",
|
|
13
13
|
publicEntry: "index.ts",
|
|
14
|
+
reviewEntry: "review/index.ts",
|
|
14
15
|
stylesEntry: "styles.css",
|
|
15
16
|
tokenDir: "tokens",
|
|
16
17
|
inlineStyleFiles: [],
|
|
@@ -54,7 +55,8 @@ export function loadConfig(directory = process.cwd()) {
|
|
|
54
55
|
if (!Array.isArray(config.inlineStyleFiles)) throw new Error("inlineStyleFiles must be an array of relative paths.");
|
|
55
56
|
const sourceRoot = projectPath(root, config.sourceDir, "sourceDir");
|
|
56
57
|
const dsRoot = projectPath(root, config.dsDir, "dsDir");
|
|
57
|
-
for (const field of ["publicEntry", "stylesEntry", "tokenDir"]) projectPath(dsRoot, config[field], field);
|
|
58
|
+
for (const field of ["publicEntry", "reviewEntry", "stylesEntry", "tokenDir"]) projectPath(dsRoot, config[field], field);
|
|
59
|
+
if (!isInside(path.join(dsRoot, "review"), path.resolve(dsRoot, config.reviewEntry)) || path.resolve(dsRoot, config.reviewEntry) === path.join(dsRoot, "review") || !/\.[cm]?[jt]sx?$/.test(config.reviewEntry)) throw new Error("reviewEntry must name a script entry inside the ADS review/ development layer.");
|
|
58
60
|
for (const file of config.inlineStyleFiles) projectPath(root, file, "inlineStyleFiles");
|
|
59
61
|
if (!Array.isArray(config.boundaries.componentVariables) || config.boundaries.componentVariables.some((name) => typeof name !== "string" || !/^--ds-[\w-]+$/.test(name) || isStateVariable(name))) throw new Error("boundaries.componentVariables must name non-state --ds-* customization variables.");
|
|
60
62
|
if (!Array.isArray(config.boundaries.styleWrites)) throw new Error("boundaries.styleWrites must be an array.");
|
|
@@ -165,11 +167,16 @@ export function isDevelopmentFile(file, { dsRoot }) {
|
|
|
165
167
|
if (!isInside(dsRoot, file)) return isTestFile(file);
|
|
166
168
|
const relative = path.relative(dsRoot, file).replaceAll("\\", "/");
|
|
167
169
|
return isTestFile(file) || isScenarioFile(file) || isFixtureFile(file) || isBehaviorFile(file) || isTokenManifestFile(file)
|
|
168
|
-
|| /^(?:showcase|composition)\//.test(relative)
|
|
170
|
+
|| /^(?:showcase|composition|review)\//.test(relative)
|
|
169
171
|
|| /(?:^|\/)(?:scenario|fixtures)(?:\/|$)/.test(relative)
|
|
170
172
|
|| /\.scenario(?:-adapter)?\.[cm]?[jt]sx?$/.test(relative);
|
|
171
173
|
}
|
|
172
174
|
|
|
175
|
+
/** Review implementations may own demo presentation; scenarios/fixtures may not. */
|
|
176
|
+
export function isReviewFile(file, { dsRoot }) {
|
|
177
|
+
return isInside(path.join(dsRoot, "review"), file) && !isTestFile(file) && !isScenarioFile(file) && !isFixtureFile(file) && !isBehaviorFile(file) && !isTokenManifestFile(file);
|
|
178
|
+
}
|
|
179
|
+
|
|
173
180
|
const ignored = new Set(["node_modules", "dist", "dist-showcase", "dist-kit", ".git", ".vite", ".ds", "coverage"]);
|
|
174
181
|
|
|
175
182
|
export function sourceFiles(root) {
|
|
@@ -188,6 +195,7 @@ export function importViolation(specifier, filename, options, resolve) {
|
|
|
188
195
|
const { root, dsRoot, config } = options;
|
|
189
196
|
// ADS implementation code needs its private imports; the showcase is a consumer.
|
|
190
197
|
if (isInside(dsRoot, filename) && !isDevelopmentFile(filename, options)) return false;
|
|
198
|
+
if (specifier === `${config.publicAlias}/review`) return !isDevelopmentFile(filename, options) && !isTestFile(filename);
|
|
191
199
|
if (specifier.startsWith(`${config.publicAlias}/`)) return true;
|
|
192
200
|
if (specifier === config.publicAlias) return false;
|
|
193
201
|
const imported = resolve ? resolve(specifier, filename)
|
|
@@ -195,6 +203,9 @@ export function importViolation(specifier, filename, options, resolve) {
|
|
|
195
203
|
: specifier.startsWith(`${config.dsDir}/`) ? [path.resolve(root, specifier)] : [];
|
|
196
204
|
return imported.some((file) => {
|
|
197
205
|
if (!isInside(dsRoot, file)) return false;
|
|
206
|
+
// Review implementations can reuse local foundation helpers. Resolved public
|
|
207
|
+
// UI/controller contracts are enforced by the layer inspector, as for patterns.
|
|
208
|
+
if (isReviewFile(filename, options)) return false;
|
|
198
209
|
// Owner-local white-box tests may exercise private helpers without making
|
|
199
210
|
// those helpers public API. Scenarios remain public-API consumers.
|
|
200
211
|
if (isTestFile(filename) && isInside(dsRoot, filename) && isInside(path.dirname(filename), file)) return false;
|
package/dist/init.mjs
CHANGED
|
@@ -153,15 +153,15 @@ export function initProject(directory = process.cwd(), { toolPackage, packageMan
|
|
|
153
153
|
});
|
|
154
154
|
if (!existingTokens.length) {
|
|
155
155
|
for (const name of ["Foundation.scenario.tsx", "Foundation.fixture.ts"]) {
|
|
156
|
-
const content = readFileSync(path.join(starter, "design-system/tokens", name), "utf8").replaceAll('"@ds"', JSON.stringify(config.publicAlias));
|
|
156
|
+
const content = readFileSync(path.join(starter, "design-system/tokens", name), "utf8").replaceAll('"@ds/review"', JSON.stringify(`${config.publicAlias}/review`)).replaceAll('"@ds"', JSON.stringify(config.publicAlias));
|
|
157
157
|
addMissing(path.join(tokenRoot, name), content);
|
|
158
158
|
}
|
|
159
|
-
for (const name of ["TokenPreview.tsx"
|
|
160
|
-
let content = readFileSync(path.join(starter, "design-system/
|
|
159
|
+
for (const name of ["TokenPreview.tsx"]) {
|
|
160
|
+
let content = readFileSync(path.join(starter, "design-system/review/token-preview", name), "utf8");
|
|
161
161
|
if (selectedFramework === "preact") content = content.replace('from "react"', 'from "preact/hooks"');
|
|
162
|
-
addMissing(path.join(dsRoot, "
|
|
162
|
+
addMissing(path.join(dsRoot, "review/token-preview", name), content.replaceAll('"@ds"', JSON.stringify(config.publicAlias)));
|
|
163
163
|
}
|
|
164
|
-
const reviewFile = path.relative(root, path.join(dsRoot, "
|
|
164
|
+
const reviewFile = path.relative(root, path.join(dsRoot, "review/token-preview/TokenPreview.tsx")).replaceAll("\\", "/");
|
|
165
165
|
if (!config.boundaries.styleWrites.some(item => item.file === reviewFile)) config.boundaries.styleWrites.push({ file: reviewFile, properties: ["--ux-token-preview-value"] });
|
|
166
166
|
}
|
|
167
167
|
tokenFiles.sort((a, b) => foundationFiles.indexOf(path.basename(a)) - foundationFiles.indexOf(path.basename(b)) || a.localeCompare(b));
|
|
@@ -172,16 +172,19 @@ export function initProject(directory = process.cwd(), { toolPackage, packageMan
|
|
|
172
172
|
};
|
|
173
173
|
addMissing(stylesFile, tokenFiles.map((file) => `@import ${JSON.stringify(relativeImport(stylesFile, file))};`).join("\n") + "\n");
|
|
174
174
|
const entryFile = path.join(dsRoot, config.publicEntry);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
current =
|
|
175
|
+
addMissing(entryFile, `// Export your project's public components and types here.\nimport ${JSON.stringify(relativeImport(entryFile, stylesFile))};\n`);
|
|
176
|
+
if (!existingTokens.length) {
|
|
177
|
+
const reviewEntry = path.join(dsRoot, config.reviewEntry);
|
|
178
|
+
const reviewExport = `export { TokenPreview } from ${JSON.stringify(relativeImport(reviewEntry, path.join(dsRoot, "review/token-preview/TokenPreview")))};\n`;
|
|
179
|
+
addMissing(reviewEntry, reviewExport);
|
|
180
|
+
if (existsSync(reviewEntry) && !readFileSync(reviewEntry, "utf8").includes("token-preview/TokenPreview")) throw new Error("The project owns an existing review entry. Add the new token renderer through that development API before initializing the foundation.");
|
|
181
|
+
if (existsSync(entryFile)) {
|
|
182
|
+
let current = readFileSync(entryFile, "utf8");
|
|
183
|
+
const ast = ts.createSourceFile(entryFile, current, ts.ScriptTarget.Latest, true);
|
|
184
|
+
for (const statement of [...ast.statements].reverse()) if (ts.isExportDeclaration(statement) && !statement.moduleSpecifier && statement.exportClause && ts.isNamedExports(statement.exportClause) && !statement.exportClause.elements.length) current = current.slice(0, statement.getStart(ast)) + current.slice(statement.end);
|
|
185
|
+
if (!ast.statements.some(statement => ts.isImportDeclaration(statement) || statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword))) current += `\nimport ${JSON.stringify(relativeImport(entryFile, stylesFile))};\n`;
|
|
186
|
+
if (current !== readFileSync(entryFile, "utf8")) updates.set(entryFile, current);
|
|
183
187
|
}
|
|
184
|
-
updates.set(entryFile, current + "\n" + reviewExport);
|
|
185
188
|
}
|
|
186
189
|
for (const section of ["components", "patterns", "controllers", "composition"]) addMissing(path.join(dsRoot, section, ".gitkeep"), "");
|
|
187
190
|
addMissing(path.join(dsRoot, "assets/brand/README.md"), readFileSync(path.join(starter, "design-system/assets/brand/README.md")));
|