@reqvire-org/ads 0.3.1 → 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.
- package/dist/bootstrap-ds/ADS.md +29 -0
- package/dist/bootstrap-ds/design-system/tokens/base.css +4 -1
- package/dist/bootstrap-ds/design-system/tokens/colors.css +2 -2
- package/dist/check.mjs +27 -4
- package/dist/contracts/foundation.json +211 -0
- package/dist/foundation.mjs +114 -0
- package/dist/mcp/build-status.mjs +22 -9
- package/dist/mcp/preview-watch.mjs +35 -12
- package/dist/scenarios-compiler.mjs +86 -43
- package/dist/skills/ads-assimilate/SKILL.md +19 -0
- package/dist/skills/ads-assimilate/references/cli.md +3 -2
- package/dist/skills/ads-break-ui/SKILL.md +19 -0
- package/dist/skills/ads-designer/SKILL.md +19 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +29 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/base.css +4 -1
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/colors.css +2 -2
- package/dist/skills/ads-designer/references/cli.md +3 -2
- package/dist/skills/ads-import/SKILL.md +19 -0
- package/dist/skills/ads-import/references/cli.md +3 -2
- package/dist/skills/ads-motion/SKILL.md +19 -0
- package/docs/ADS.md +29 -0
- package/docs/ARCHITECTURE.md +32 -3
- package/docs/CHANGELOG.md +16 -0
- package/docs/GUIDE.md +9 -1
- package/docs/MCP.md +18 -7
- package/package.json +2 -1
package/dist/bootstrap-ds/ADS.md
CHANGED
|
@@ -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`.
|
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
18
|
+
|
|
19
19
|
--bg-canvas: #17191e;
|
|
20
20
|
--bg-surface: #20232a;
|
|
21
21
|
--bg-hover: #2c3039;
|
package/dist/check.mjs
CHANGED
|
@@ -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"]);
|
|
@@ -54,13 +55,14 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
54
55
|
const actual = imports.map(node => foundationImportTarget(node, foundationEntry));
|
|
55
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.");
|
|
56
57
|
} catch (error) { report(foundationEntry, "css-syntax", error.reason ?? error.message, error.line ?? 1); }
|
|
57
|
-
function checkCss(file, text, offset = { line: 1, column: 1 }) {
|
|
58
|
+
function checkCss(file, text, offset = { line: 1, column: 1 }, foundation = false) {
|
|
58
59
|
let css;
|
|
59
60
|
try { css = postcss.parse(text, { from: file }); }
|
|
60
61
|
catch (error) { report(file, "css-syntax", error.reason ?? error.message, offset.line + (error.line ?? 1) - 1); return; }
|
|
61
|
-
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.");
|
|
62
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);
|
|
63
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);
|
|
64
66
|
const first = valueParser(rule.params).nodes.find((node) => node.type !== "space" && node.type !== "comment");
|
|
65
67
|
const specifier = first?.type === "string" ? first.value : first?.type === "function" && first.value === "url" ? first.nodes[0]?.value : null;
|
|
66
68
|
if (!specifier || /^(?:[a-z]+:|\/\/)/i.test(specifier)) return;
|
|
@@ -69,19 +71,40 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
69
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);
|
|
70
72
|
});
|
|
71
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
|
+
}
|
|
72
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);
|
|
73
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);
|
|
74
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);
|
|
75
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);
|
|
76
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
|
+
});
|
|
77
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
|
+
}
|
|
78
99
|
const variable = checkComponentVariable(decl.prop, file, options);
|
|
79
100
|
if (variable) report(file, variable.rule, variable.message, offset.line + decl.source.start.line - 1, decl.source.start.column);
|
|
80
101
|
for (const ref of references(decl.value)) {
|
|
81
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);
|
|
82
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);
|
|
83
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);
|
|
84
106
|
}
|
|
107
|
+
if (fontDescriptor) return; // Font metadata defines source assets, not token-backed UI styles.
|
|
85
108
|
const violations = new Set();
|
|
86
109
|
valueParser(decl.value).walk((node) => {
|
|
87
110
|
if (node.type === "function" && node.value === "url") return false;
|
|
@@ -91,12 +114,12 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
91
114
|
if (node.type === "word" && /^ease(?:-in|-out|-in-out)?$/.test(node.value)) violations.add("Use a motion token for easing.");
|
|
92
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.");
|
|
93
116
|
});
|
|
94
|
-
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.");
|
|
95
118
|
for (const message of violations) report(file, "style-token", message, offset.line + decl.source.start.line - 1, decl.source.start.column);
|
|
96
119
|
});
|
|
97
120
|
}
|
|
98
121
|
for (const file of files) {
|
|
99
|
-
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; }
|
|
100
123
|
if (file.endsWith(".html")) { inspectHtml(readFileSync(file, "utf8"), (rule, message, line) => report(file, rule, message, line)); continue; }
|
|
101
124
|
if (file.endsWith(".css")) {
|
|
102
125
|
const text = readFileSync(file, "utf8");
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
{
|
|
2
|
+
"document": [
|
|
3
|
+
"color",
|
|
4
|
+
"background",
|
|
5
|
+
"background-color",
|
|
6
|
+
"font",
|
|
7
|
+
"font-family",
|
|
8
|
+
"font-size",
|
|
9
|
+
"font-weight",
|
|
10
|
+
"font-style",
|
|
11
|
+
"font-stretch",
|
|
12
|
+
"line-height",
|
|
13
|
+
"letter-spacing",
|
|
14
|
+
"word-spacing",
|
|
15
|
+
"text-rendering",
|
|
16
|
+
"font-synthesis",
|
|
17
|
+
"font-kerning",
|
|
18
|
+
"font-feature-settings",
|
|
19
|
+
"font-variation-settings",
|
|
20
|
+
"font-optical-sizing",
|
|
21
|
+
"-webkit-font-smoothing",
|
|
22
|
+
"-moz-osx-font-smoothing",
|
|
23
|
+
"-webkit-text-size-adjust",
|
|
24
|
+
"text-size-adjust",
|
|
25
|
+
"color-scheme",
|
|
26
|
+
"scrollbar-color",
|
|
27
|
+
"scrollbar-width"
|
|
28
|
+
],
|
|
29
|
+
"reset": {
|
|
30
|
+
"box-sizing": [
|
|
31
|
+
"border-box",
|
|
32
|
+
"content-box"
|
|
33
|
+
],
|
|
34
|
+
"font": [
|
|
35
|
+
"inherit"
|
|
36
|
+
],
|
|
37
|
+
"font-family": [
|
|
38
|
+
"inherit"
|
|
39
|
+
],
|
|
40
|
+
"font-size": [
|
|
41
|
+
"inherit",
|
|
42
|
+
"100%"
|
|
43
|
+
],
|
|
44
|
+
"font-weight": [
|
|
45
|
+
"inherit",
|
|
46
|
+
"bolder",
|
|
47
|
+
"lighter"
|
|
48
|
+
],
|
|
49
|
+
"font-style": [
|
|
50
|
+
"inherit",
|
|
51
|
+
"normal"
|
|
52
|
+
],
|
|
53
|
+
"line-height": [
|
|
54
|
+
"inherit",
|
|
55
|
+
"normal"
|
|
56
|
+
],
|
|
57
|
+
"letter-spacing": [
|
|
58
|
+
"inherit",
|
|
59
|
+
"normal"
|
|
60
|
+
],
|
|
61
|
+
"word-spacing": [
|
|
62
|
+
"inherit",
|
|
63
|
+
"normal"
|
|
64
|
+
],
|
|
65
|
+
"text-align": [
|
|
66
|
+
"inherit",
|
|
67
|
+
"start"
|
|
68
|
+
],
|
|
69
|
+
"text-transform": [
|
|
70
|
+
"inherit",
|
|
71
|
+
"none"
|
|
72
|
+
],
|
|
73
|
+
"text-decoration": [
|
|
74
|
+
"inherit",
|
|
75
|
+
"none"
|
|
76
|
+
],
|
|
77
|
+
"text-indent": [
|
|
78
|
+
"0"
|
|
79
|
+
],
|
|
80
|
+
"color": [
|
|
81
|
+
"inherit",
|
|
82
|
+
"currentcolor"
|
|
83
|
+
],
|
|
84
|
+
"background": [
|
|
85
|
+
"transparent",
|
|
86
|
+
"none"
|
|
87
|
+
],
|
|
88
|
+
"background-color": [
|
|
89
|
+
"transparent"
|
|
90
|
+
],
|
|
91
|
+
"border": [
|
|
92
|
+
"0",
|
|
93
|
+
"none"
|
|
94
|
+
],
|
|
95
|
+
"border-width": [
|
|
96
|
+
"0"
|
|
97
|
+
],
|
|
98
|
+
"border-style": [
|
|
99
|
+
"none"
|
|
100
|
+
],
|
|
101
|
+
"border-color": [
|
|
102
|
+
"inherit",
|
|
103
|
+
"currentcolor",
|
|
104
|
+
"transparent"
|
|
105
|
+
],
|
|
106
|
+
"border-radius": [
|
|
107
|
+
"0"
|
|
108
|
+
],
|
|
109
|
+
"border-collapse": [
|
|
110
|
+
"collapse",
|
|
111
|
+
"separate"
|
|
112
|
+
],
|
|
113
|
+
"border-spacing": [
|
|
114
|
+
"0"
|
|
115
|
+
],
|
|
116
|
+
"list-style": [
|
|
117
|
+
"none"
|
|
118
|
+
],
|
|
119
|
+
"list-style-type": [
|
|
120
|
+
"none"
|
|
121
|
+
],
|
|
122
|
+
"list-style-image": [
|
|
123
|
+
"none"
|
|
124
|
+
],
|
|
125
|
+
"vertical-align": [
|
|
126
|
+
"baseline",
|
|
127
|
+
"middle"
|
|
128
|
+
],
|
|
129
|
+
"box-shadow": [
|
|
130
|
+
"none"
|
|
131
|
+
],
|
|
132
|
+
"text-shadow": [
|
|
133
|
+
"none"
|
|
134
|
+
],
|
|
135
|
+
"appearance": [
|
|
136
|
+
"none",
|
|
137
|
+
"auto"
|
|
138
|
+
],
|
|
139
|
+
"-webkit-appearance": [
|
|
140
|
+
"none",
|
|
141
|
+
"auto"
|
|
142
|
+
],
|
|
143
|
+
"overflow": [
|
|
144
|
+
"visible"
|
|
145
|
+
],
|
|
146
|
+
"cursor": [
|
|
147
|
+
"inherit",
|
|
148
|
+
"default"
|
|
149
|
+
],
|
|
150
|
+
"-webkit-tap-highlight-color": [
|
|
151
|
+
"transparent"
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
"replacedElements": [
|
|
155
|
+
"img",
|
|
156
|
+
"svg",
|
|
157
|
+
"video",
|
|
158
|
+
"canvas",
|
|
159
|
+
"audio",
|
|
160
|
+
"iframe",
|
|
161
|
+
"embed",
|
|
162
|
+
"object"
|
|
163
|
+
],
|
|
164
|
+
"replacedReset": {
|
|
165
|
+
"display": [
|
|
166
|
+
"block",
|
|
167
|
+
"inline-block"
|
|
168
|
+
],
|
|
169
|
+
"max-width": [
|
|
170
|
+
"100%"
|
|
171
|
+
],
|
|
172
|
+
"width": [
|
|
173
|
+
"auto"
|
|
174
|
+
],
|
|
175
|
+
"height": [
|
|
176
|
+
"auto"
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
"focus": [
|
|
180
|
+
"outline",
|
|
181
|
+
"outline-width",
|
|
182
|
+
"outline-style",
|
|
183
|
+
"outline-color",
|
|
184
|
+
"outline-offset"
|
|
185
|
+
],
|
|
186
|
+
"reducedMotion": [
|
|
187
|
+
"animation-duration",
|
|
188
|
+
"animation-delay",
|
|
189
|
+
"animation-iteration-count",
|
|
190
|
+
"transition-duration",
|
|
191
|
+
"transition-delay",
|
|
192
|
+
"scroll-behavior"
|
|
193
|
+
],
|
|
194
|
+
"selection": [
|
|
195
|
+
"color",
|
|
196
|
+
"background",
|
|
197
|
+
"background-color"
|
|
198
|
+
],
|
|
199
|
+
"scrollbar": [
|
|
200
|
+
"width",
|
|
201
|
+
"height",
|
|
202
|
+
"background",
|
|
203
|
+
"background-color",
|
|
204
|
+
"background-clip",
|
|
205
|
+
"border",
|
|
206
|
+
"border-width",
|
|
207
|
+
"border-style",
|
|
208
|
+
"border-color",
|
|
209
|
+
"border-radius"
|
|
210
|
+
]
|
|
211
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import selectorParser from "postcss-selector-parser";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
// CSS-language policy, not a project token catalogue or renderer classification.
|
|
5
|
+
const policy = JSON.parse(readFileSync(new URL("./contracts/foundation.json", import.meta.url), "utf8"));
|
|
6
|
+
const cssWide = /^(?:inherit|initial|unset|revert|revert-layer)$/i;
|
|
7
|
+
|
|
8
|
+
function rootAnchor(nodes) {
|
|
9
|
+
return nodes.some(node => node.type === "tag" && !node.namespace && node.value.toLowerCase() === "html"
|
|
10
|
+
|| node.type === "pseudo" && node.value.toLowerCase() === ":root"
|
|
11
|
+
|| node.type === "pseudo" && /^:(?:is|where)$/i.test(node.value) && node.nodes.length && node.nodes.every(selector => !selector.nodes.some(node => node.type === "combinator") && rootAnchor(selector.nodes)));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function rootCompound(node) {
|
|
15
|
+
for (let current = node; current.parent; current = current.parent) {
|
|
16
|
+
const parent = current.parent;
|
|
17
|
+
if (parent.type !== "selector") continue;
|
|
18
|
+
const index = parent.nodes.indexOf(current);
|
|
19
|
+
let start = index, end = index + 1;
|
|
20
|
+
while (start && parent.nodes[start - 1].type !== "combinator") --start;
|
|
21
|
+
while (end < parent.nodes.length && parent.nodes[end].type !== "combinator") ++end;
|
|
22
|
+
// A negative/relational argument cannot establish root context, but an
|
|
23
|
+
// explicit root outside it still can: :root:not([data-theme=light]).
|
|
24
|
+
const argument = parent.parent?.type === "pseudo" && !/^:(?:is|where)$/i.test(parent.parent.value);
|
|
25
|
+
if (!argument && rootAnchor(parent.nodes.slice(start, end))) return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Global defaults may address native elements and explicit document themes, not UI owners. */
|
|
31
|
+
export function foundationSelector(selector, { definitions = false } = {}) {
|
|
32
|
+
try {
|
|
33
|
+
let violation = false;
|
|
34
|
+
selectorParser(root => {
|
|
35
|
+
if (definitions && root.nodes.some(selector => selector.nodes.some(node => node.type === "combinator") || !rootAnchor(selector.nodes))) violation = true;
|
|
36
|
+
root.walk(node => {
|
|
37
|
+
if (["class", "id"].includes(node.type) && !rootCompound(node)) violation = true;
|
|
38
|
+
if (node.type === "tag" && node.value.includes("-")) violation = true;
|
|
39
|
+
if (node.type === "attribute" && /^(?:class|id|style|data-.+|aria-.+)$/i.test(node.attribute) && !rootCompound(node)) violation = true;
|
|
40
|
+
if (node.type === "pseudo" && node.value.toLowerCase() === ":has") violation = true;
|
|
41
|
+
if (node.type === "combinator") {
|
|
42
|
+
const before = node.parent.nodes.slice(0, node.parent.nodes.indexOf(node));
|
|
43
|
+
if (before.some(node => node.type === "combinator") || !rootAnchor(before)) violation = true;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}).processSync(selector);
|
|
47
|
+
return violation ? "Foundation defaults address native elements or explicit document-root themes. Component/pattern hooks, contextual layout and relational state selectors belong to their Linaria owner." : null;
|
|
48
|
+
} catch (error) { return `Invalid foundation selector: ${error.message}`; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function subjects(nodes) {
|
|
52
|
+
let variants = [[]];
|
|
53
|
+
for (const node of nodes) {
|
|
54
|
+
if (node.type === "pseudo" && /^:(?:where|is)$/i.test(node.value)) {
|
|
55
|
+
variants = variants.flatMap(prefix => node.nodes.flatMap(selector => subjects(selector.nodes).map(subject => [...prefix, ...subject])));
|
|
56
|
+
} else variants.forEach(subject => subject.push(node));
|
|
57
|
+
}
|
|
58
|
+
return variants;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function reducedMotion(rule) {
|
|
62
|
+
for (let parent = rule.parent; parent; parent = parent.parent) {
|
|
63
|
+
if (parent.type === "atrule" && parent.name.toLowerCase() === "media"
|
|
64
|
+
&& /^\(\s*prefers-reduced-motion\s*:\s*reduce\s*\)$/i.test(parent.params.trim())) return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function reset(property, value, nodes) {
|
|
70
|
+
const words = value.toLowerCase().split(/\s+/);
|
|
71
|
+
if (/^(?:margin|padding)(?:-(?:top|right|bottom|left|block|inline)(?:-(?:start|end))?)?$/.test(property)) return words.every(word => word === "0") || cssWide.test(value);
|
|
72
|
+
const tag = nodes.find(node => node.type === "tag")?.value.toLowerCase();
|
|
73
|
+
if (property === "font-weight" && /^(?:bolder|lighter)$/.test(value)) return ["b", "strong"].includes(tag);
|
|
74
|
+
if (policy.reset[property]) return policy.reset[property].includes(value.toLowerCase()) || cssWide.test(value);
|
|
75
|
+
if (policy.replacedElements.includes(tag) && policy.replacedReset[property]) return policy.replacedReset[property].includes(value.toLowerCase()) || cssWide.test(value);
|
|
76
|
+
return tag === "textarea" && property === "resize" && /^(?:vertical|inherit|initial|unset|revert(?:-layer)?)$/.test(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function roleAllows(rule, nodes, property, value) {
|
|
80
|
+
const pseudos = nodes.filter(node => node.type === "pseudo");
|
|
81
|
+
const tag = nodes.find(node => node.type === "tag")?.value.toLowerCase();
|
|
82
|
+
const universal = nodes.every(node => node.type === "universal" || node.type === "pseudo" || node.type === "comment");
|
|
83
|
+
if (universal && pseudos.some(node => node.value === ":focus-visible") && pseudos.every(node => node.value === ":focus-visible")) return policy.focus.includes(property) && !/^(?:none|0)$/.test(value);
|
|
84
|
+
if (universal && pseudos.some(node => node.value === "::selection") && pseudos.every(node => node.value === "::selection")) return policy.selection.includes(property);
|
|
85
|
+
if (universal && pseudos.every(node => ["::before", "::after"].includes(node.value)) && reducedMotion(rule)) {
|
|
86
|
+
if (policy.reducedMotion.includes(property)) return property !== "scroll-behavior" || value === "auto";
|
|
87
|
+
if (["animation", "transition"].includes(property) && value === "none") return true;
|
|
88
|
+
}
|
|
89
|
+
const document = (rootAnchor(nodes) || tag === "body") && pseudos.every(node => [":root", ":not"].includes(node.value));
|
|
90
|
+
if (document && policy.document.includes(property)) return true;
|
|
91
|
+
const documentScrollbar = (rootAnchor(nodes) || tag === "body") && pseudos.some(node => /^::-webkit-scrollbar(?:-thumb|-track)?$/.test(node.value))
|
|
92
|
+
&& pseudos.every(node => node.value === ":root" || /^::-webkit-scrollbar(?:-thumb|-track)?$/.test(node.value) || node.value === ":hover");
|
|
93
|
+
if (documentScrollbar) return policy.scrollbar.includes(property);
|
|
94
|
+
if (pseudos.some(node => node.value === "::placeholder") && pseudos.every(node => node.value === "::placeholder")) return property === "color" || property === "opacity" && value === "1";
|
|
95
|
+
if (pseudos.some(node => ![":root", ":not", "::before", "::after"].includes(node.value))) return false;
|
|
96
|
+
if (pseudos.some(node => node.value === ":not") && !rootAnchor(nodes)) return false;
|
|
97
|
+
return reset(property, value, nodes);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Closed foundation roles apply even when a native selector uses valid tokens. */
|
|
101
|
+
export function foundationDeclaration(rule, declaration) {
|
|
102
|
+
try {
|
|
103
|
+
const selectors = selectorParser().astSync(rule.selector);
|
|
104
|
+
const property = declaration.prop.toLowerCase(), value = declaration.value.trim();
|
|
105
|
+
for (const selector of selectors.nodes) {
|
|
106
|
+
const lastCombinator = selector.nodes.findLastIndex(node => node.type === "combinator");
|
|
107
|
+
for (let nodes of subjects(selector.nodes.slice(lastCombinator + 1))) {
|
|
108
|
+
nodes = nodes.slice(nodes.findLastIndex(node => node.type === "combinator") + 1);
|
|
109
|
+
if (!roleAllows(rule, nodes, property, value)) return "base.css permits document-root/body typography and canvas defaults, limited native normalization, global focus indicators and reduced-motion rules. Put control skins, layout and state styling in their Linaria owner.";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
} catch (error) { return `Invalid foundation declaration context: ${error.message}`; }
|
|
114
|
+
}
|
|
@@ -8,9 +8,9 @@ import { readPreviewManifest } from "../preview-resources.mjs";
|
|
|
8
8
|
export const BUILD_URI = "ads://project/build";
|
|
9
9
|
export const buildStatusFile = (root) => path.join(root, ".ds/build-status.json");
|
|
10
10
|
const maxBytes = 256 * 1024;
|
|
11
|
-
const diagnostic = z.object({ message: z.string().max(4000), file: z.string().max(1000).optional(), line: z.number().int().positive().optional(), column: z.number().int().positive().optional(), plugin: z.string().max(200).optional() }).strict();
|
|
11
|
+
const diagnostic = z.object({ message: z.string().max(4000), file: z.string().max(1000).optional(), line: z.number().int().positive().optional(), column: z.number().int().positive().optional(), plugin: z.string().max(200).optional(), code: z.string().max(100).optional() }).strict();
|
|
12
12
|
const preview = z.object({ id: z.string().max(256), state: z.enum(["idle", "building", "ready", "error"]), revision: z.number().int().nonnegative(), lastSuccessfulRevision: z.number().int().positive().nullable(), artifactDigest: z.string().regex(/^sha256:[a-f0-9]{64}$/).nullable(), diagnostics: z.array(diagnostic).max(20), diagnosticSet: z.number().int().nonnegative().optional() }).strict();
|
|
13
|
-
const snapshot = z.object({ schemaVersion: z.literal(1), session: z.string().regex(/^[\w-]{1,100}$/), sequence: z.number().int().nonnegative(), updatedAt: z.string().datetime(), previews: z.array(preview), diagnosticSets: z.array(z.array(diagnostic).max(20)).optional() }).strict().refine(value => value.previews.every(entry => entry.diagnosticSet === undefined || value.diagnosticSets?.[entry.diagnosticSet]), "Invalid shared diagnostic reference.");
|
|
13
|
+
const snapshot = z.object({ schemaVersion: z.literal(1), session: z.string().regex(/^[\w-]{1,100}$/), sequence: z.number().int().nonnegative(), updatedAt: z.string().datetime(), previews: z.array(preview), diagnostics: z.array(diagnostic).max(20).optional(), diagnosticSets: z.array(z.array(diagnostic).max(20)).optional() }).strict().refine(value => value.previews.every(entry => entry.diagnosticSet === undefined || value.diagnosticSets?.[entry.diagnosticSet]), "Invalid shared diagnostic reference.");
|
|
14
14
|
export const artifactDigest = (file) => {
|
|
15
15
|
if (realpathSync(file) !== file) throw new Error("Preview output cannot resolve through a symlink.");
|
|
16
16
|
if (statSync(file).size > 2 * 1024 * 1024) throw new Error("Preview exceeds 2 MiB.");
|
|
@@ -26,7 +26,7 @@ function normalizeDiagnostic(root, error) {
|
|
|
26
26
|
return { message: String(error?.message ?? error).slice(0, 4000),
|
|
27
27
|
...(file ? { file: (path.isAbsolute(file) ? path.relative(root, file) : file).replaceAll("\\", "/").slice(0, 1000) } : {}),
|
|
28
28
|
...(Number.isInteger(line) && line > 0 ? { line } : {}), ...(Number.isInteger(column) && column > 0 ? { column } : {}),
|
|
29
|
-
...(typeof error?.plugin === "string" ? { plugin: error.plugin.slice(0, 200) } : {}) };
|
|
29
|
+
...(typeof error?.plugin === "string" ? { plugin: error.plugin.slice(0, 200) } : {}), ...(typeof error?.code === "string" ? { code: error.code.slice(0, 100) } : {}) };
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/** One publisher per project compiler; no dependency on a UI framework. */
|
|
@@ -36,8 +36,9 @@ export function createBuildReporter(directory, { apps, session = process.env.ADS
|
|
|
36
36
|
if (!isInside(root, target)) throw new Error("Build status must stay inside the project.");
|
|
37
37
|
const entries = new Map(apps.map((app) => [app.id, { id: app.id, state: "idle", revision: 0, lastSuccessfulRevision: null, artifactDigest: null, diagnostics: [] }]));
|
|
38
38
|
let sequence = 0;
|
|
39
|
+
let diagnostics = [];
|
|
39
40
|
function save() {
|
|
40
|
-
const value = { schemaVersion: 1, session, sequence: ++sequence, updatedAt: new Date().toISOString(), previews: [...entries.values()] };
|
|
41
|
+
const value = { schemaVersion: 1, session, sequence: ++sequence, updatedAt: new Date().toISOString(), previews: [...entries.values()], ...(diagnostics.length ? { diagnostics } : {}) };
|
|
41
42
|
let text = JSON.stringify(snapshot.parse(value));
|
|
42
43
|
// A shared graph failure applies to every scenario. Store repeated details
|
|
43
44
|
// once only when necessary; ordinary reports retain inline diagnostics.
|
|
@@ -66,6 +67,17 @@ export function createBuildReporter(directory, { apps, session = process.env.ADS
|
|
|
66
67
|
save();
|
|
67
68
|
return {
|
|
68
69
|
session,
|
|
70
|
+
failure(error) {
|
|
71
|
+
diagnostics = (Array.isArray(error) ? error : [error]).slice(0, 20).map(error => normalizeDiagnostic(root, error));
|
|
72
|
+
for (const value of entries.values()) { value.state = "error"; ++value.revision; value.diagnostics = []; }
|
|
73
|
+
save();
|
|
74
|
+
},
|
|
75
|
+
clearFailure() {
|
|
76
|
+
if (!diagnostics.length) return;
|
|
77
|
+
diagnostics = [];
|
|
78
|
+
for (const value of entries.values()) if (value.state === "error" && !value.diagnostics.length) value.state = "idle";
|
|
79
|
+
save();
|
|
80
|
+
},
|
|
69
81
|
setApps(next) {
|
|
70
82
|
const ids = new Set(next.map(app => app.id));
|
|
71
83
|
for (const id of entries.keys()) if (!ids.has(id)) entries.delete(id);
|
|
@@ -104,11 +116,11 @@ export function createBuildReporter(directory, { apps, session = process.env.ADS
|
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
/** Read durable build events; a running process alone never means a fresh build. */
|
|
107
|
-
export function inspectBuildStatus(root, config, { session, processState } = {}) {
|
|
119
|
+
export function inspectBuildStatus(root, config, { session, processState, file = buildStatusFile(root) } = {}) {
|
|
108
120
|
let report;
|
|
109
121
|
let reportError;
|
|
110
|
-
const file = buildStatusFile(root);
|
|
111
122
|
try {
|
|
123
|
+
if (!isInside(root, path.resolve(file))) throw new Error("Build status must stay inside the project.");
|
|
112
124
|
if (existsSync(file)) {
|
|
113
125
|
if (realpathSync(file) !== file || statSync(file).size > maxBytes) throw new Error("Build status must be a real project file under 256 KiB.");
|
|
114
126
|
report = snapshot.parse(JSON.parse(readFileSync(file, "utf8")));
|
|
@@ -125,6 +137,7 @@ export function inspectBuildStatus(root, config, { session, processState } = {})
|
|
|
125
137
|
const value = report?.previews.find((entry) => entry.id === app.id);
|
|
126
138
|
let state = value?.state ?? "unreported";
|
|
127
139
|
let diagnostics = value?.diagnosticSet !== undefined ? report.diagnosticSets[value.diagnosticSet] : value?.diagnostics ?? [];
|
|
140
|
+
if (report?.diagnostics?.length) { state = "error"; diagnostics = [...report.diagnostics, ...diagnostics].slice(0, 20); }
|
|
128
141
|
const available = existsSync(path.resolve(root, app.file));
|
|
129
142
|
if (reportError) { state = "error"; diagnostics = [{ message: reportError }]; }
|
|
130
143
|
if (graphError && app.id.includes("/")) { state = "error"; diagnostics = [{ message: graphError }]; }
|
|
@@ -139,8 +152,8 @@ export function inspectBuildStatus(root, config, { session, processState } = {})
|
|
|
139
152
|
return { id: app.id, state, session: report?.session ?? null, revision: value?.revision ?? null, lastSuccessfulRevision: value?.lastSuccessfulRevision ?? null,
|
|
140
153
|
current, stale: state === "unreported" ? null : !current, available, diagnostics };
|
|
141
154
|
});
|
|
142
|
-
const state = previews.some((entry) => entry.state === "error") ? "error" : previews.some((entry) => entry.state === "building") ? "building" : previews.some((entry) => entry.state === "unreported") ? "unreported" : previews.some(entry => entry.current) ? "ready" : previews.length ? "idle" : "disabled";
|
|
143
|
-
return { state, reported: Boolean(report), session: report?.session ?? null, sequence: report?.sequence ?? null, updatedAt: report?.updatedAt ?? null, previews, resourceUri: BUILD_URI };
|
|
155
|
+
const state = reportError || report?.diagnostics?.length || previews.some((entry) => entry.state === "error") ? "error" : previews.some((entry) => entry.state === "building") ? "building" : previews.some((entry) => entry.state === "unreported") ? "unreported" : previews.some(entry => entry.current) ? "ready" : previews.length ? "idle" : "disabled";
|
|
156
|
+
return { state, reported: Boolean(report), session: report?.session ?? null, sequence: report?.sequence ?? null, updatedAt: report?.updatedAt ?? null, previews, ...(report?.diagnostics?.length ? { diagnostics: report.diagnostics } : {}), resourceUri: BUILD_URI };
|
|
144
157
|
}
|
|
145
158
|
|
|
146
159
|
export class PreviewBuildError extends Error {
|
|
@@ -154,7 +167,7 @@ export function summarizeBuild(build) {
|
|
|
154
167
|
return { ...value, diagnostics, diagnosticCount: value.diagnostics.length,
|
|
155
168
|
diagnosticsTruncated: value.diagnostics.length > 3 || value.diagnostics.some((entry) => entry.message.length > 1000) };
|
|
156
169
|
}
|
|
157
|
-
return build.previews ? { ...build, previews: build.previews.map(bounded) } : { ...bounded(build), resourceUri: BUILD_URI };
|
|
170
|
+
return build.previews ? { ...build, ...(build.diagnostics ? bounded(build) : {}), previews: build.previews.map(bounded) } : { ...bounded(build), resourceUri: BUILD_URI };
|
|
158
171
|
}
|
|
159
172
|
|
|
160
173
|
export function buildFailure(build) {
|