@assure-one/design-system 1.37.0 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -16
- package/codemods/README.md +40 -0
- package/codemods/lib/registry.mjs +1 -0
- package/codemods/transforms/cm-18-data-table-parts.mjs +234 -0
- package/dist/css/components.css +1 -1
- package/dist/css/legacy-aliases.css +8 -0
- package/dist/css/tokens.css +8 -0
- package/dist/{design-system-provider-C8UlFe52.d.ts → design-system-provider-B5MbvbRc.d.ts} +61 -1
- package/dist/index.d.ts +1358 -206
- package/dist/index.js +2201 -607
- package/dist/index.js.map +1 -1
- package/dist/next/index.d.ts +1 -1
- package/dist/styles.css +1 -1
- package/docs/components.md +36 -8
- package/docs/components.registry.json +2399 -463
- package/docs/for-ai-agents.md +2 -0
- package/eslint-config/index.mjs +210 -0
- package/eslint-config/stylelint-rules.mjs +54 -0
- package/eslint-config/stylelint.mjs +73 -0
- package/package.json +8 -1
package/docs/for-ai-agents.md
CHANGED
|
@@ -55,6 +55,8 @@ If a UI element exists in the design system, use it. Do not write `<button>`, `<
|
|
|
55
55
|
| `flex gap-*` wrappers | `Stack`, `Inline`; `Surface` / `Card` for boxes |
|
|
56
56
|
| `title="…"`, `window.confirm()`, `alert()` | `Tooltip`, `ConfirmActionButton` / `AlertDialog`, `useToast` |
|
|
57
57
|
|
|
58
|
+
Two shareable lint configs report the first three rows for you: `@assure-one/design-system/eslint-config` and `@assure-one/design-system/stylelint-config` (warn-level, experimental — see the README section "Lint configs for consumers").
|
|
59
|
+
|
|
58
60
|
Before writing any element, look it up in the ["Pick a component"](./components.md#pick-a-component) table. The only raw elements that are fine are structural ones the design system does not model (`<main>`, `<article>`, `<ul>` for plain lists, `<img>` for content images, `<form>` itself).
|
|
59
61
|
|
|
60
62
|
## 3. Forms: the `Field` recipe
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@assure-one/design-system/eslint-config` (W7-18).
|
|
3
|
+
*
|
|
4
|
+
* The consumer-facing half of the in-repo ratchets: the four findings the
|
|
5
|
+
* design system ratchets on its own source (`scripts/ratchet/*` — raw platform
|
|
6
|
+
* controls, direct Radix imports, deep imports past the public entries,
|
|
7
|
+
* `--ds-*` redefinition outside a preset) reported in a product's own lint run.
|
|
8
|
+
*
|
|
9
|
+
* Every rule is **warn** by default, deliberately: a product adopts this on a
|
|
10
|
+
* codebase that already has hundreds of findings, records the count, and
|
|
11
|
+
* ratchets it down (W7-19). Raise the ones you have cleaned to "error" in your
|
|
12
|
+
* own config.
|
|
13
|
+
*
|
|
14
|
+
* Flat config only:
|
|
15
|
+
*
|
|
16
|
+
* import ds from "@assure-one/design-system/eslint-config";
|
|
17
|
+
* export default [...ds.configs.recommended];
|
|
18
|
+
*
|
|
19
|
+
* Or wire the plugin yourself: `plugins: { ds }` plus the rules you want.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const ENTRY_SUBPATHS = new Set([
|
|
23
|
+
"tokens",
|
|
24
|
+
"icons",
|
|
25
|
+
"next",
|
|
26
|
+
"testing",
|
|
27
|
+
"styles.css",
|
|
28
|
+
"package.json",
|
|
29
|
+
"eslint-config",
|
|
30
|
+
"stylelint-config",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/** The DS component(s) that own each raw element (scripts/ratchet/lib/raw-controls-owners.mjs). */
|
|
34
|
+
const RAW_CONTROLS = {
|
|
35
|
+
button: "Button, IconButton, LinkButton, SubmitButton or Pressable",
|
|
36
|
+
input: "Input, NumberInput, SearchInput, Checkbox, RadioGroup, Switch, Slider, DatePicker…",
|
|
37
|
+
select: "Select, Combobox or MultiSelect",
|
|
38
|
+
textarea: "Textarea",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const DS_PROPERTY = /^--ds-/;
|
|
42
|
+
const DEFAULT_PRESET_PATH = "(^|/)presets/";
|
|
43
|
+
|
|
44
|
+
const isStringLiteral = (node) => node?.type === "Literal" && typeof node.value === "string";
|
|
45
|
+
|
|
46
|
+
/** The `--ds-*` name a property key or JSX attribute declares, or undefined. */
|
|
47
|
+
function dsPropertyName(key, computed) {
|
|
48
|
+
if (computed) return undefined;
|
|
49
|
+
const name = isStringLiteral(key) ? key.value : key?.type === "Identifier" ? key.name : undefined;
|
|
50
|
+
return name && DS_PROPERTY.test(name) ? name : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Import-like source string of a node, or undefined. */
|
|
54
|
+
function importSource(node) {
|
|
55
|
+
if (node.type === "ImportDeclaration" || node.type === "ExportNamedDeclaration") {
|
|
56
|
+
return isStringLiteral(node.source) ? node.source.value : undefined;
|
|
57
|
+
}
|
|
58
|
+
if (node.type === "ExportAllDeclaration") {
|
|
59
|
+
return isStringLiteral(node.source) ? node.source.value : undefined;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const importVisitors = (check) => ({
|
|
65
|
+
ImportDeclaration: (node) => check(node, importSource(node)),
|
|
66
|
+
ExportNamedDeclaration: (node) => check(node, importSource(node)),
|
|
67
|
+
ExportAllDeclaration: (node) => check(node, importSource(node)),
|
|
68
|
+
ImportExpression: (node) =>
|
|
69
|
+
check(node, isStringLiteral(node.source) ? node.source.value : undefined),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/** @type {import("eslint").Rule.RuleModule} */
|
|
73
|
+
const noRawControls = {
|
|
74
|
+
meta: {
|
|
75
|
+
type: "problem",
|
|
76
|
+
docs: {
|
|
77
|
+
description:
|
|
78
|
+
"Use the design system's control instead of a raw <button>, <input>, <select> or <textarea>.",
|
|
79
|
+
},
|
|
80
|
+
schema: [],
|
|
81
|
+
messages: {
|
|
82
|
+
raw: "<{{ element }}> is owned by the design system. Use {{ use }} (docs/components.md → “Never hand-roll”).",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
create(context) {
|
|
86
|
+
return {
|
|
87
|
+
JSXOpeningElement(node) {
|
|
88
|
+
if (node.name?.type !== "JSXIdentifier") return;
|
|
89
|
+
const element = node.name.name;
|
|
90
|
+
const use = RAW_CONTROLS[element];
|
|
91
|
+
if (!use) return;
|
|
92
|
+
context.report({ node, messageId: "raw", data: { element, use } });
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** @type {import("eslint").Rule.RuleModule} */
|
|
99
|
+
const noRadixImports = {
|
|
100
|
+
meta: {
|
|
101
|
+
type: "problem",
|
|
102
|
+
docs: {
|
|
103
|
+
description: "Import the design system's primitive instead of the Radix package it wraps.",
|
|
104
|
+
},
|
|
105
|
+
schema: [],
|
|
106
|
+
messages: {
|
|
107
|
+
radix:
|
|
108
|
+
'"{{ source }}" is wrapped by the design system. Import the primitive from "@assure-one/design-system" so behaviour, tokens and the Radix version stay in one place.',
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
create(context) {
|
|
112
|
+
return importVisitors((node, source) => {
|
|
113
|
+
if (!source?.startsWith("@radix-ui/")) return;
|
|
114
|
+
context.report({ node, messageId: "radix", data: { source } });
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** @type {import("eslint").Rule.RuleModule} */
|
|
120
|
+
const noDeepImports = {
|
|
121
|
+
meta: {
|
|
122
|
+
type: "problem",
|
|
123
|
+
docs: {
|
|
124
|
+
description:
|
|
125
|
+
"Import from a published design-system entry, never from dist/ or src/ (ADR-011).",
|
|
126
|
+
},
|
|
127
|
+
schema: [],
|
|
128
|
+
messages: {
|
|
129
|
+
deep: '"{{ source }}" is not a published entry. Import from "@assure-one/design-system" or one of {{ entries }} — anything else may change without a major release (ADR-011).',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
create(context) {
|
|
133
|
+
const entries = [...ENTRY_SUBPATHS].map((s) => `/${s}`).join(", ");
|
|
134
|
+
return importVisitors((node, source) => {
|
|
135
|
+
if (!source?.startsWith("@assure-one/design-system/")) return;
|
|
136
|
+
const subpath = source.slice("@assure-one/design-system/".length);
|
|
137
|
+
if (ENTRY_SUBPATHS.has(subpath) || subpath.startsWith("css/")) return;
|
|
138
|
+
context.report({ node, messageId: "deep", data: { source, entries } });
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** @type {import("eslint").Rule.RuleModule} */
|
|
144
|
+
const noDsTokenRedefinition = {
|
|
145
|
+
meta: {
|
|
146
|
+
type: "problem",
|
|
147
|
+
docs: {
|
|
148
|
+
description:
|
|
149
|
+
"Set a `--ds-*` custom property only in a theme preset, never inline in a component.",
|
|
150
|
+
},
|
|
151
|
+
schema: [
|
|
152
|
+
{
|
|
153
|
+
type: "object",
|
|
154
|
+
properties: { presetPath: { type: "string" } },
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
messages: {
|
|
159
|
+
redefined:
|
|
160
|
+
"{{ name }} is a design-system token. Override it in a theme preset (a file under presets/, or ThemeScope/BrandScope), not inline: an inline value escapes the theme and the token contract.",
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
create(context) {
|
|
164
|
+
const presetPath = new RegExp(context.options?.[0]?.presetPath ?? DEFAULT_PRESET_PATH);
|
|
165
|
+
const filename = (context.filename ?? context.getFilename?.() ?? "").replace(/\\/g, "/");
|
|
166
|
+
if (presetPath.test(filename)) return {};
|
|
167
|
+
const report = (node, name) => context.report({ node, messageId: "redefined", data: { name } });
|
|
168
|
+
return {
|
|
169
|
+
Property(node) {
|
|
170
|
+
const name = dsPropertyName(node.key, node.computed);
|
|
171
|
+
if (name) report(node, name);
|
|
172
|
+
},
|
|
173
|
+
JSXAttribute(node) {
|
|
174
|
+
if (node.name?.type !== "JSXNamespacedName") return;
|
|
175
|
+
const name = `${node.name.namespace?.name}:${node.name.name?.name}`;
|
|
176
|
+
if (DS_PROPERTY.test(name)) report(node, name);
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/** The `ds` plugin: the rules alone, for a config that wires them itself. */
|
|
183
|
+
export const plugin = {
|
|
184
|
+
meta: { name: "@assure-one/design-system/eslint-config" },
|
|
185
|
+
rules: {
|
|
186
|
+
"no-raw-controls": noRawControls,
|
|
187
|
+
"no-radix-imports": noRadixImports,
|
|
188
|
+
"no-deep-imports": noDeepImports,
|
|
189
|
+
"no-ds-token-redefinition": noDsTokenRedefinition,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/** Warn-level rule table (W7-18): every rule this config ships. */
|
|
194
|
+
export const rules = Object.fromEntries(
|
|
195
|
+
Object.keys(plugin.rules).map((name) => [`ds/${name}`, "warn"]),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
/** Flat config: `export default [...ds.configs.recommended]`. */
|
|
199
|
+
export const configs = {
|
|
200
|
+
recommended: [
|
|
201
|
+
{
|
|
202
|
+
name: "@assure-one/design-system/recommended",
|
|
203
|
+
files: ["**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}"],
|
|
204
|
+
plugins: { ds: plugin },
|
|
205
|
+
rules,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export default { plugin, rules, configs };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two stylelint checks of W7-18, as plain PostCSS finders.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of any `stylelint` import so they can be exercised directly (and
|
|
5
|
+
* reused by the repo's own CSS tooling): `stylelint.mjs` wraps them into the
|
|
6
|
+
* published plugin. They mirror the `css-hygiene` ratchet family's concerns
|
|
7
|
+
* (`scripts/ratchet/lib/css-hygiene-analyse.mjs`) from the consumer side.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** A declaration whose property is a design-system token. */
|
|
11
|
+
const DS_PROPERTY = /^--ds-/;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Selectors that reach design-system DOM: a `ds:`-prefixed utility class
|
|
15
|
+
* (written `.ds\:flex` in CSS), the `ds-` component classes, and the
|
|
16
|
+
* `data-slot` / `data-ds-*` attributes components expose (ADR-008).
|
|
17
|
+
*/
|
|
18
|
+
const DS_SELECTOR = /\.ds\\?:|\.ds-|\[data-slot|\[data-ds-/;
|
|
19
|
+
|
|
20
|
+
/** The nearest enclosing rule's selector, or undefined at the top level. */
|
|
21
|
+
function enclosingSelector(node) {
|
|
22
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
23
|
+
if (parent.type === "rule") return parent.selector;
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Every `--ds-*` declaration in a stylesheet: `[{ prop, decl }]`. */
|
|
29
|
+
export function dsTokenDeclarations(root) {
|
|
30
|
+
const found = [];
|
|
31
|
+
root.walkDecls((decl) => {
|
|
32
|
+
if (DS_PROPERTY.test(decl.prop)) found.push({ prop: decl.prop, decl });
|
|
33
|
+
});
|
|
34
|
+
return found;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Every `!important` declaration under a design-system selector: `[{ prop, selector, decl }]`. */
|
|
38
|
+
export function importantOnDsSelectors(root) {
|
|
39
|
+
const found = [];
|
|
40
|
+
root.walkDecls((decl) => {
|
|
41
|
+
if (!decl.important) return;
|
|
42
|
+
const selector = enclosingSelector(decl);
|
|
43
|
+
if (!selector || !DS_SELECTOR.test(selector)) return;
|
|
44
|
+
found.push({ prop: decl.prop, selector, decl });
|
|
45
|
+
});
|
|
46
|
+
return found;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const messages = {
|
|
50
|
+
dsToken: (prop) =>
|
|
51
|
+
`${prop} is a design-system token. Set it in a theme preset (the block \`pnpm derive-consumer-preset\` writes, or ThemeScope/BrandScope), not in app CSS — an override here escapes the theme and the token contract.`,
|
|
52
|
+
important: (prop, selector) =>
|
|
53
|
+
`Avoid \`!important\` on a design-system selector (\`${selector}\` → ${prop}). Pass \`className\`/\`classNames\` instead: consumer classes already win by cascade (ADR-004).`,
|
|
54
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@assure-one/design-system/stylelint-config` (W7-18).
|
|
3
|
+
*
|
|
4
|
+
* The CSS half of the consumer lint surface: the two things a product's own
|
|
5
|
+
* stylesheet does to the design system that the `css-hygiene` ratchet catches
|
|
6
|
+
* inside this repository — redefining a `--ds-*` token outside a theme preset,
|
|
7
|
+
* and `!important` on a design-system selector.
|
|
8
|
+
*
|
|
9
|
+
* Both are **warnings** (the `severity: "warning"` below), for the same reason
|
|
10
|
+
* the ESLint rules are: a product adopts this on CSS that already has
|
|
11
|
+
* findings, records the count, and ratchets it down (W7-19).
|
|
12
|
+
*
|
|
13
|
+
* // stylelint.config.mjs
|
|
14
|
+
* import ds from "@assure-one/design-system/stylelint-config";
|
|
15
|
+
* export default { extends: [], ...ds };
|
|
16
|
+
*
|
|
17
|
+
* `stylelint` itself is the consumer's (an optional peer dependency here).
|
|
18
|
+
* Preset files are exempt: override `ignoreFiles`/`overrides` to move them.
|
|
19
|
+
*/
|
|
20
|
+
import stylelint from "stylelint";
|
|
21
|
+
|
|
22
|
+
import { dsTokenDeclarations, importantOnDsSelectors, messages } from "./stylelint-rules.mjs";
|
|
23
|
+
|
|
24
|
+
const NAMESPACE = "assure-ds";
|
|
25
|
+
|
|
26
|
+
const plugin = (ruleName, find, message) =>
|
|
27
|
+
stylelint.createPlugin(`${NAMESPACE}/${ruleName}`, (primary, secondary, context) => {
|
|
28
|
+
const name = `${NAMESPACE}/${ruleName}`;
|
|
29
|
+
return (root, result) => {
|
|
30
|
+
if (!primary) return;
|
|
31
|
+
for (const found of find(root)) {
|
|
32
|
+
stylelint.utils.report({
|
|
33
|
+
result,
|
|
34
|
+
ruleName: name,
|
|
35
|
+
node: found.decl,
|
|
36
|
+
word: found.prop,
|
|
37
|
+
message: message(found),
|
|
38
|
+
severity: secondary?.severity ?? context?.severity ?? "warning",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/** The two plugin rules, for a config that wires them itself. */
|
|
45
|
+
export const plugins = [
|
|
46
|
+
plugin("no-ds-token-redefinition", dsTokenDeclarations, ({ prop }) => messages.dsToken(prop)),
|
|
47
|
+
plugin("no-important-on-ds-selectors", importantOnDsSelectors, ({ prop, selector }) =>
|
|
48
|
+
messages.important(prop, selector),
|
|
49
|
+
),
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/** Warn-level rule table (W7-18). */
|
|
53
|
+
export const rules = {
|
|
54
|
+
[`${NAMESPACE}/no-ds-token-redefinition`]: [true, { severity: "warning" }],
|
|
55
|
+
[`${NAMESPACE}/no-important-on-ds-selectors`]: [true, { severity: "warning" }],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The shareable config. Theme presets are where `--ds-*` belongs, so any file
|
|
60
|
+
* under a `presets/` directory (or named `*.preset.css`) is exempt.
|
|
61
|
+
*/
|
|
62
|
+
export const config = {
|
|
63
|
+
plugins,
|
|
64
|
+
rules,
|
|
65
|
+
overrides: [
|
|
66
|
+
{
|
|
67
|
+
files: ["**/presets/**/*.css", "**/*.preset.css"],
|
|
68
|
+
rules: { [`${NAMESPACE}/no-ds-token-redefinition`]: null },
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default config;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assure-one/design-system",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.38.0",
|
|
4
4
|
"description": "Assure One design system — tokens, primitives, and patterns for the Assure Suite.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -45,12 +45,15 @@
|
|
|
45
45
|
"default": "./dist/testing/index.cjs"
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
|
+
"./eslint-config": "./eslint-config/index.mjs",
|
|
49
|
+
"./stylelint-config": "./eslint-config/stylelint.mjs",
|
|
48
50
|
"./package.json": "./package.json"
|
|
49
51
|
},
|
|
50
52
|
"files": [
|
|
51
53
|
"dist",
|
|
52
54
|
"codemods",
|
|
53
55
|
"!codemods/__test__",
|
|
56
|
+
"eslint-config",
|
|
54
57
|
"docs/components.md",
|
|
55
58
|
"docs/components.registry.json",
|
|
56
59
|
"docs/for-ai-agents.md",
|
|
@@ -65,12 +68,16 @@
|
|
|
65
68
|
},
|
|
66
69
|
"peerDependencies": {
|
|
67
70
|
"next": ">=15",
|
|
71
|
+
"stylelint": ">=16",
|
|
68
72
|
"react": ">=19",
|
|
69
73
|
"react-dom": ">=19"
|
|
70
74
|
},
|
|
71
75
|
"peerDependenciesMeta": {
|
|
72
76
|
"next": {
|
|
73
77
|
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"stylelint": {
|
|
80
|
+
"optional": true
|
|
74
81
|
}
|
|
75
82
|
},
|
|
76
83
|
"dependencies": {
|