@reqvire-org/ads 0.3.1 → 0.3.3
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/boundaries.mjs +21 -23
- package/dist/check.mjs +45 -15
- package/dist/cli.mjs +1 -0
- package/dist/contracts/foundation.json +211 -0
- package/dist/foundation.mjs +114 -0
- package/dist/mcp/build-status.mjs +22 -9
- package/dist/mcp/mutations.mjs +1 -1
- package/dist/mcp/preview-watch.mjs +35 -12
- package/dist/mcp/prompts.mjs +1 -1
- package/dist/mcp/server.mjs +6 -6
- package/dist/oxlint.mjs +1 -1
- package/dist/scenarios-compiler.mjs +86 -43
- package/dist/skills/ads-assimilate/SKILL.md +43 -2
- package/dist/skills/ads-assimilate/references/cli.md +3 -2
- package/dist/skills/ads-assimilate/references/native-implementation.md +16 -0
- package/dist/skills/ads-break-ui/SKILL.md +43 -2
- package/dist/skills/ads-designer/SKILL.md +43 -2
- 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 +43 -2
- package/dist/skills/ads-import/references/cli.md +3 -2
- package/dist/skills/ads-motion/SKILL.md +43 -2
- package/dist/style-boundaries.mjs +59 -0
- package/dist/units.mjs +37 -1
- package/docs/ADS.md +34 -1
- package/docs/ARCHITECTURE.md +38 -6
- package/docs/CHANGELOG.md +28 -0
- package/docs/GUIDE.md +18 -1
- package/docs/MCP.md +48 -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/boundaries.mjs
CHANGED
|
@@ -90,31 +90,29 @@ function isWrite(node) {
|
|
|
90
90
|
|| ts.isDeleteExpression(parent);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
/** Exact writes and known reads share one interpretation with exception audits.
|
|
94
|
+
* undefined = unrelated, false = read, null = unknown/bulk, string = exact write. */
|
|
95
|
+
export function styleAccessProperty(node) {
|
|
96
|
+
if (!isMember(node) || memberName(node) !== "style") return undefined;
|
|
97
|
+
const access = unwrapped(node), parent = access.parent;
|
|
98
|
+
if (!isMember(parent) || parent.expression !== access) return null;
|
|
99
|
+
const property = cssName(memberName(parent)), target = unwrapped(parent);
|
|
100
|
+
if (ts.isCallExpression(target.parent) && target.parent.expression === target) {
|
|
101
|
+
if (["set-property", "remove-property"].includes(property)) {
|
|
102
|
+
const name = target.parent.arguments[0];
|
|
103
|
+
return name && ts.isStringLiteralLike(name) ? name.text : null;
|
|
104
|
+
}
|
|
105
|
+
return ["get-property-value", "get-property-priority", "item"].includes(property) ? false : null;
|
|
106
|
+
}
|
|
107
|
+
if (isWrite(target)) return property ?? null;
|
|
108
|
+
return property && !["set-property", "remove-property"].includes(property) ? false : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Aliases, computed keys, cssText and bulk writes cannot widen an exception.
|
|
95
112
|
export function styleWriteViolation(node, filename, { root, config }) {
|
|
96
113
|
const allowed = config.boundaries.styleWrites.find((boundary) => path.resolve(root, boundary.file) === filename)?.properties ?? [];
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const parent = access.parent;
|
|
100
|
-
let property;
|
|
101
|
-
if (isMember(parent) && parent.expression === access) {
|
|
102
|
-
property = cssName(memberName(parent));
|
|
103
|
-
const target = unwrapped(parent);
|
|
104
|
-
if (ts.isCallExpression(target.parent) && target.parent.expression === target) {
|
|
105
|
-
if (["set-property", "remove-property"].includes(property)) {
|
|
106
|
-
const name = target.parent.arguments[0];
|
|
107
|
-
property = name && ts.isStringLiteralLike(name) ? name.text : null;
|
|
108
|
-
} else if (["get-property-value", "get-property-priority", "item"].includes(property)) return null;
|
|
109
|
-
else property = null;
|
|
110
|
-
} else if (!isWrite(target)) {
|
|
111
|
-
// Plain property reads are allowed. Passing a method reference or a
|
|
112
|
-
// computed member elsewhere is not a known property read.
|
|
113
|
-
if (property && !["set-property", "remove-property"].includes(property)) return null;
|
|
114
|
-
property = null;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
if (property && property !== "css-text" && allowed.includes(property)) return null;
|
|
114
|
+
const property = styleAccessProperty(node);
|
|
115
|
+
if (property !== undefined && property !== false && !(property && property !== "css-text" && allowed.includes(property))) {
|
|
118
116
|
return "Imperative styles require a file boundary and an explicit property. Bulk writes, computed property names, and style-object escapes are forbidden.";
|
|
119
117
|
}
|
|
120
118
|
if (ts.isCallExpression(node) && isMember(node.expression) && ["setAttribute", "setAttributeNS"].includes(memberName(node.expression))) {
|
package/dist/check.mjs
CHANGED
|
@@ -14,6 +14,11 @@ 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 { inspectStyleBoundaries, pointerCaptureAdvisories } from "./style-boundaries.mjs";
|
|
18
|
+
import { foundationDeclaration, foundationSelector } from "./foundation.mjs";
|
|
19
|
+
|
|
20
|
+
const scenarioLayoutGuidance = "Put demo-only sizing and framing in a public review/layout pattern. Keep required behavior in the runtime owner; add component props for deliberate product API needs.";
|
|
21
|
+
const inlineStyleGuidance = `Own visual styles in component/pattern Linaria. ${scenarioLayoutGuidance} Reserve inlineStyleFiles for documented measured geometry, not demo layout.`;
|
|
17
22
|
|
|
18
23
|
const showcaseControls = new Set(["a", "button", "input", "select", "textarea", "option", "optgroup", "table", "thead", "tbody", "tfoot", "tr", "td", "th", "dialog", "details", "summary", "progress", "meter"]);
|
|
19
24
|
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"]);
|
|
@@ -23,7 +28,7 @@ const widgetRoles = new Set(["button", "link", "checkbox", "radio", "switch", "c
|
|
|
23
28
|
export function checkProject(directory, { trackingRoot } = {}) {
|
|
24
29
|
const options = { ...loadConfig(directory), ...(trackingRoot ? { trackingRoot: path.resolve(trackingRoot) } : {}) };
|
|
25
30
|
const { root, config, dsRoot, sourceRoot } = options;
|
|
26
|
-
const diagnostics = [];
|
|
31
|
+
const diagnostics = [], advisories = [];
|
|
27
32
|
for (const [field, file] of [["dsDir", dsRoot], ["publicEntry", path.join(dsRoot, config.publicEntry)], ["stylesEntry", path.join(dsRoot, config.stylesEntry)]]) {
|
|
28
33
|
if (!existsSync(file) || (field === "dsDir" ? !statSync(file).isDirectory() : !statSync(file).isFile())) diagnostics.push({ rule: "project-structure", file: path.relative(root, file), line: 1, column: 1, message: `Missing or invalid ${field}. Bootstrap an empty ADS or configure its actual path.` });
|
|
29
34
|
}
|
|
@@ -35,13 +40,15 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
35
40
|
const resolveImport = importResolver(options);
|
|
36
41
|
const rootHtml = path.join(root, "index.html");
|
|
37
42
|
const files = [...new Set([...sourceFiles(sourceRoot), ...sourceFiles(dsRoot), ...(existsSync(rootHtml) ? [rootHtml] : [])])];
|
|
38
|
-
const
|
|
43
|
+
const boundaryFiles = [...config.inlineStyleFiles, ...config.boundaries.styleWrites.map(entry => entry.file)].map(file => path.resolve(root, file)).filter(file => existsSync(file) && statSync(file).isFile());
|
|
44
|
+
const scriptFiles = [...new Set([...files, ...boundaryFiles])].filter((file) => /\.[cm]?[jt]sx?$/.test(file));
|
|
39
45
|
const compiler = projectCompilerOptions(options);
|
|
40
46
|
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 });
|
|
41
47
|
const checker = program.getTypeChecker();
|
|
42
48
|
function report(file, rule, message, line = 1, column = 1) {
|
|
43
49
|
diagnostics.push({ file: path.relative(root, file).replaceAll("\\", "/"), rule, message, line, column });
|
|
44
50
|
}
|
|
51
|
+
inspectStyleBoundaries(options, program, report);
|
|
45
52
|
inspectShowcasePages(options, program, resolveImport, report);
|
|
46
53
|
inspectShowcaseCoverage(options, program, report);
|
|
47
54
|
inspectScenarioCoverage(options, program, report);
|
|
@@ -54,13 +61,14 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
54
61
|
const actual = imports.map(node => foundationImportTarget(node, foundationEntry));
|
|
55
62
|
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
63
|
} catch (error) { report(foundationEntry, "css-syntax", error.reason ?? error.message, error.line ?? 1); }
|
|
57
|
-
function checkCss(file, text, offset = { line: 1, column: 1 }) {
|
|
64
|
+
function checkCss(file, text, offset = { line: 1, column: 1 }, foundation = false) {
|
|
58
65
|
let css;
|
|
59
66
|
try { css = postcss.parse(text, { from: file }); }
|
|
60
67
|
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 (isDevelopmentFile(file, options) && css.nodes.some((node) => node.type !== "comment" && !(node.type === "atrule" && node.name === "import"))) report(file, "showcase-style",
|
|
68
|
+
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
|
+
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);
|
|
63
70
|
css.walkAtRules("import", (rule) => {
|
|
71
|
+
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
72
|
const first = valueParser(rule.params).nodes.find((node) => node.type !== "space" && node.type !== "comment");
|
|
65
73
|
const specifier = first?.type === "string" ? first.value : first?.type === "function" && first.value === "url" ? first.nodes[0]?.value : null;
|
|
66
74
|
if (!specifier || /^(?:[a-z]+:|\/\/)/i.test(specifier)) return;
|
|
@@ -69,19 +77,40 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
69
77
|
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
78
|
});
|
|
71
79
|
css.walkRules((rule) => {
|
|
80
|
+
if (foundation) {
|
|
81
|
+
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);
|
|
82
|
+
const violation = foundationSelector(rule.selector, { definitions: rule.nodes.some(node => node.type === "decl" && node.prop.startsWith("--")) });
|
|
83
|
+
if (violation) report(file, "foundation-selector", violation, rule.source.start.line, rule.source.start.column);
|
|
84
|
+
}
|
|
72
85
|
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
86
|
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
87
|
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
88
|
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
89
|
});
|
|
90
|
+
if (foundation) css.walkAtRules(rule => {
|
|
91
|
+
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);
|
|
92
|
+
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);
|
|
93
|
+
});
|
|
77
94
|
css.walkDecls((decl) => {
|
|
95
|
+
if (foundation && decl.prop.startsWith("--")) {
|
|
96
|
+
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);
|
|
97
|
+
return; // Definition names, values and aliases are validated by inspectTokens.
|
|
98
|
+
}
|
|
99
|
+
const fontDescriptor = foundation && path.basename(file) === "fonts.css" && decl.parent.type === "atrule" && decl.parent.name === "font-face";
|
|
100
|
+
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);
|
|
101
|
+
if (foundation && path.basename(file) === "base.css" && decl.parent.type === "rule") {
|
|
102
|
+
const violation = foundationDeclaration(decl.parent, decl);
|
|
103
|
+
if (violation) report(file, "foundation-role", violation, decl.source.start.line, decl.source.start.column);
|
|
104
|
+
}
|
|
78
105
|
const variable = checkComponentVariable(decl.prop, file, options);
|
|
79
106
|
if (variable) report(file, variable.rule, variable.message, offset.line + decl.source.start.line - 1, decl.source.start.column);
|
|
80
107
|
for (const ref of references(decl.value)) {
|
|
81
108
|
const local = /^(?:--ds-|--ux-|--showcase-)/.test(ref.name);
|
|
109
|
+
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
110
|
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
111
|
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
112
|
}
|
|
113
|
+
if (fontDescriptor) return; // Font metadata defines source assets, not token-backed UI styles.
|
|
85
114
|
const violations = new Set();
|
|
86
115
|
valueParser(decl.value).walk((node) => {
|
|
87
116
|
if (node.type === "function" && node.value === "url") return false;
|
|
@@ -91,12 +120,12 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
91
120
|
if (node.type === "word" && /^ease(?:-in|-out|-in-out)?$/.test(node.value)) violations.add("Use a motion token for easing.");
|
|
92
121
|
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
122
|
});
|
|
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.");
|
|
123
|
+
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
124
|
for (const message of violations) report(file, "style-token", message, offset.line + decl.source.start.line - 1, decl.source.start.column);
|
|
96
125
|
});
|
|
97
126
|
}
|
|
98
127
|
for (const file of files) {
|
|
99
|
-
if (file.endsWith(".css") && isInside(path.join(dsRoot, config.tokenDir), file) && !isDevelopmentFile(file, options)) continue;
|
|
128
|
+
if (file.endsWith(".css") && isInside(path.join(dsRoot, config.tokenDir), file) && !isDevelopmentFile(file, options)) { checkCss(file, readFileSync(file, "utf8"), undefined, true); continue; }
|
|
100
129
|
if (file.endsWith(".html")) { inspectHtml(readFileSync(file, "utf8"), (rule, message, line) => report(file, rule, message, line)); continue; }
|
|
101
130
|
if (file.endsWith(".css")) {
|
|
102
131
|
const text = readFileSync(file, "utf8");
|
|
@@ -105,6 +134,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
105
134
|
}
|
|
106
135
|
const source = program.getSourceFile(file);
|
|
107
136
|
if (!source) continue;
|
|
137
|
+
advisories.push(...pointerCaptureAdvisories(source).map(entry => ({ file: path.relative(root, file).replaceAll("\\", "/"), ...entry })));
|
|
108
138
|
for (const diagnostic of program.getSyntacticDiagnostics(source)) {
|
|
109
139
|
const position = source.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
|
|
110
140
|
report(file, "source-syntax", ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"), position.line + 1, position.character + 1);
|
|
@@ -163,14 +193,14 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
163
193
|
for (const property of properties) {
|
|
164
194
|
if (!ts.isPropertyAssignment(property) || !(ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name))) continue;
|
|
165
195
|
if (tag && ts.isStringLiteralLike(tag) && svgTags.has(tag.text) && importedUiApi(node.expression, checker)?.module === "preact") checkSvgAttribute(property, property.name.text);
|
|
166
|
-
if (property.name.text === "style" && (!inlineAllowed || showcase)) nodeReport(property, "no-inline-style",
|
|
196
|
+
if (property.name.text === "style" && (!inlineAllowed || showcase)) nodeReport(property, "no-inline-style", showcase ? `Scenarios define no inline styles. ${scenarioLayoutGuidance}` : inlineStyleGuidance);
|
|
167
197
|
if (property.name.text === "style") inspectProperty(property.initializer);
|
|
168
198
|
if (property.name.text === "className") inspectClass(property.initializer);
|
|
169
|
-
if (showcase && property.name.text === "className") nodeReport(property, "showcase-class",
|
|
199
|
+
if (showcase && property.name.text === "className") nodeReport(property, "showcase-class", `Scenarios define no custom class names. ${scenarioLayoutGuidance}`);
|
|
170
200
|
}
|
|
171
201
|
if (showcase && tag && ts.isStringLiteralLike(tag)) {
|
|
172
202
|
const role = properties.find(property => ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)) && property.name.text === "role")?.initializer;
|
|
173
|
-
if (showcaseControls.has(tag.text) || role && ts.isStringLiteralLike(role) && widgetRoles.has(role.text)) nodeReport(node, "showcase-control",
|
|
203
|
+
if (showcaseControls.has(tag.text) || role && ts.isStringLiteralLike(role) && widgetRoles.has(role.text)) nodeReport(node, "showcase-control", `Scenario controls must render public ADS components for actual interaction. ${scenarioLayoutGuidance}`);
|
|
174
204
|
}
|
|
175
205
|
}
|
|
176
206
|
if (isInside(dsRoot, file) && (ts.isIdentifier(node) && ["fetch", "XMLHttpRequest", "WebSocket", "EventSource"].includes(node.text) && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node))) {
|
|
@@ -181,9 +211,9 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
181
211
|
if (showcase && (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) && ts.isIdentifier(node.tagName) && /^[a-z]/.test(node.tagName.text)) {
|
|
182
212
|
const role = node.attributes.properties.find((attr) => ts.isJsxAttribute(attr) && attr.name.text === "role")?.initializer;
|
|
183
213
|
const roleValue = role && ts.isStringLiteralLike(role) ? role.text : role && ts.isJsxExpression(role) && role.expression && ts.isStringLiteralLike(role.expression) ? role.expression.text : null;
|
|
184
|
-
if (showcaseControls.has(node.tagName.text) || widgetRoles.has(roleValue)) nodeReport(node, "showcase-control",
|
|
214
|
+
if (showcaseControls.has(node.tagName.text) || widgetRoles.has(roleValue)) nodeReport(node, "showcase-control", `Scenario controls must render public ADS components for actual interaction. ${scenarioLayoutGuidance}`);
|
|
185
215
|
}
|
|
186
|
-
if (showcase && ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "className") nodeReport(node, "showcase-class",
|
|
216
|
+
if (showcase && ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "className") nodeReport(node, "showcase-class", `Scenarios define no custom class names. ${scenarioLayoutGuidance}`);
|
|
187
217
|
let specifier;
|
|
188
218
|
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) specifier = node.moduleSpecifier.text;
|
|
189
219
|
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments[0] && ts.isStringLiteral(node.arguments[0])) specifier = node.arguments[0].text;
|
|
@@ -191,13 +221,13 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
191
221
|
if (specifier && resolveImport(specifier, file).some((target) => (specifier.startsWith(".") || existsSync(target)) && target.endsWith(".css") && isInside(root, target) && !isInside(dsRoot, target) && target !== path.join(sourceRoot, "app-mount.css") && !isInside(path.join(root, "node_modules"), target))) nodeReport(node, "style-ownership", "Imported project CSS belongs to ADS components, patterns, or the foundation. Only structural app-mount.css is allowed outside the ADS.");
|
|
192
222
|
if (specifier && appImportViolation(specifier, file, options, resolveImport)) nodeReport(node, "no-app-import", "ADS implementations and showcase fixtures must not import app code. Use public ADS APIs with local fixture data; keep application integration harnesses in application tests.");
|
|
193
223
|
if (specifier && showcaseImportViolation(specifier, file, options, resolveImport)) nodeReport(node, "showcase-import", "Showcase code and styles are development fixtures; runtime code must not import them.");
|
|
194
|
-
if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "style" && (!inlineAllowed || showcase)) nodeReport(node, "no-inline-style",
|
|
224
|
+
if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "style" && (!inlineAllowed || showcase)) nodeReport(node, "no-inline-style", showcase ? `Scenarios define no inline styles. ${scenarioLayoutGuidance}` : inlineStyleGuidance);
|
|
195
225
|
if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "style" && node.initializer) {
|
|
196
226
|
inspectProperty(node.initializer);
|
|
197
227
|
}
|
|
198
228
|
const mutation = styleWriteViolation(node, file, options);
|
|
199
229
|
if (mutation) nodeReport(node, "style-mutation", mutation);
|
|
200
|
-
if (!inlineAllowed && isUiCssProperties(node, checker)) nodeReport(node, "css-properties", "
|
|
230
|
+
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 public review pattern.");
|
|
201
231
|
if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "className" && node.initializer) {
|
|
202
232
|
inspectClass(node.initializer);
|
|
203
233
|
}
|
|
@@ -211,5 +241,5 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
211
241
|
}
|
|
212
242
|
visit(source);
|
|
213
243
|
}
|
|
214
|
-
return { ok: diagnostics.length === 0, diagnostics };
|
|
244
|
+
return { ok: diagnostics.length === 0, diagnostics, ...(advisories.length ? { advisories } : {}) };
|
|
215
245
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -96,6 +96,7 @@ async function main(args) {
|
|
|
96
96
|
if (!json && result.diagnostics?.length) {
|
|
97
97
|
for (const diagnostic of result.diagnostics) console.error(`${diagnostic.file}:${diagnostic.line}:${diagnostic.column} [${diagnostic.rule}] ${diagnostic.message}`);
|
|
98
98
|
}
|
|
99
|
+
if (!json) for (const advisory of result.advisories ?? []) console.error(`${advisory.file}:${advisory.line}:${advisory.column} [advisory:${advisory.rule}] ${advisory.message}`);
|
|
99
100
|
if (result.ok === false || result.diagnostics?.length) process.exitCode = 1;
|
|
100
101
|
}
|
|
101
102
|
|
|
@@ -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
|
+
}
|