@instruments/materia-ui 0.4.0 → 0.4.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/README.md +2 -4
- package/agent-docs.md +9 -9
- package/dist/components/accordion/accordion-root.d.ts +1 -0
- package/dist/components/accordion/accordion-root.js +1 -0
- package/dist/components/alert/alert-root.d.ts +1 -0
- package/dist/components/alert/alert-root.js +1 -0
- package/dist/components/alert-dialog/alert-dialog-root.d.ts +1 -0
- package/dist/components/alert-dialog/alert-dialog-root.js +1 -0
- package/dist/components/avatar/avatar-root.d.ts +1 -0
- package/dist/components/avatar/avatar-root.js +1 -0
- package/dist/components/badge/badge-root.d.ts +1 -0
- package/dist/components/badge/badge-root.js +1 -0
- package/dist/components/calendar/calendar-root.js +1 -0
- package/dist/components/carousel/carousel-root.d.ts +1 -0
- package/dist/components/carousel/carousel-root.js +1 -0
- package/dist/components/chart/chart-root.d.ts +1 -0
- package/dist/components/chart/chart-root.js +1 -0
- package/dist/components/collapsible/collapsible-root.d.ts +1 -0
- package/dist/components/collapsible/collapsible-root.js +1 -0
- package/dist/components/combobox/combobox-root.d.ts +1 -0
- package/dist/components/combobox/combobox-root.js +1 -0
- package/dist/components/command/command-dialog.d.ts +5 -1
- package/dist/components/command/command-dialog.js +1 -1
- package/dist/components/command/command-input.js +5 -2
- package/dist/components/command/command-root.d.ts +1 -0
- package/dist/components/command/command-root.js +1 -0
- package/dist/components/dialog/dialog-content.d.ts +2 -0
- package/dist/components/dialog/dialog-content.js +3 -2
- package/dist/components/dialog/dialog-footer.d.ts +2 -0
- package/dist/components/dialog/dialog-footer.js +8 -8
- package/dist/components/dialog/dialog-root.d.ts +1 -0
- package/dist/components/dialog/dialog-root.js +1 -0
- package/dist/components/dialog/dialog-title.d.ts +2 -0
- package/dist/components/dialog/dialog-title.js +3 -1
- package/dist/components/drawer/drawer-root.d.ts +1 -0
- package/dist/components/drawer/drawer-root.js +1 -0
- package/dist/components/heading/heading-root.d.ts +1 -0
- package/dist/components/heading/heading-root.js +1 -0
- package/dist/components/input/input-root.d.ts +1 -0
- package/dist/components/input/input-root.js +1 -0
- package/dist/components/kbd/kbd-root.d.ts +1 -0
- package/dist/components/kbd/kbd-root.js +1 -0
- package/dist/components/logo/logo-mark.d.ts +1 -1
- package/dist/components/logo/logo-mark.js +1 -1
- package/dist/components/logotype/logotype-paths.d.ts +1 -1
- package/dist/components/logotype/logotype-paths.js +1 -1
- package/dist/components/month-year-picker/month-year-picker-root.d.ts +1 -0
- package/dist/components/month-year-picker/month-year-picker-root.js +1 -0
- package/dist/components/native-select/native-select-root.d.ts +1 -0
- package/dist/components/native-select/native-select-root.js +1 -0
- package/dist/components/pill/pill-root.d.ts +1 -0
- package/dist/components/pill/pill-root.js +1 -0
- package/dist/components/popover/popover-root.d.ts +1 -0
- package/dist/components/popover/popover-root.js +1 -0
- package/dist/components/quantity-control/quantity-control-root.d.ts +1 -0
- package/dist/components/quantity-control/quantity-control-root.js +1 -0
- package/dist/components/segmented-control/segmented-control-root.js +6 -3
- package/dist/components/segmented-control/segmented-control-variants.js +4 -1
- package/dist/components/select/select-root.d.ts +1 -0
- package/dist/components/select/select-root.js +1 -0
- package/dist/components/separator/separator-root.d.ts +1 -0
- package/dist/components/separator/separator-root.js +1 -0
- package/dist/components/sheet/sheet-root.d.ts +1 -0
- package/dist/components/sheet/sheet-root.js +1 -0
- package/dist/components/spinner/spinner-root.d.ts +1 -0
- package/dist/components/spinner/spinner-root.js +1 -0
- package/dist/components/stepper/stepper-root.d.ts +1 -0
- package/dist/components/stepper/stepper-root.js +1 -0
- package/dist/components/success-tick-ripple/success-tick-ripple-root.d.ts +1 -0
- package/dist/components/success-tick-ripple/success-tick-ripple-root.js +1 -0
- package/dist/components/swatch-selector/swatch-selector-item.js +4 -1
- package/dist/components/switch/switch-root.d.ts +1 -0
- package/dist/components/switch/switch-root.js +1 -0
- package/dist/components/tag/tag-root.d.ts +1 -0
- package/dist/components/tag/tag-root.js +1 -0
- package/dist/components/textarea/textarea-root.d.ts +1 -0
- package/dist/components/textarea/textarea-root.js +1 -0
- package/dist/components/time-slot-picker/time-slot-picker-root.d.ts +1 -0
- package/dist/components/time-slot-picker/time-slot-picker-root.js +1 -0
- package/dist/components/toast/toast-root.d.ts +1 -0
- package/dist/components/toast/toast-root.js +1 -0
- package/dist/components/tooltip/tooltip-root.d.ts +1 -0
- package/dist/components/tooltip/tooltip-root.js +1 -0
- package/dist/compositions/empty-state/empty-state-root.d.ts +1 -0
- package/dist/compositions/empty-state/empty-state-root.js +1 -0
- package/dist/compositions/file-upload/file-upload-dropzone.d.ts +1 -0
- package/dist/compositions/file-upload/file-upload-dropzone.js +1 -0
- package/dist/data/decision-trees.d.ts +1 -1
- package/dist/data/decision-trees.js +1 -1
- package/dist/data/docs-manifest.d.ts +2 -0
- package/dist/data/docs-manifest.js +235 -68
- package/dist/styles/index.css +0 -1
- package/dist/tokens/segmented.d.ts +8 -0
- package/dist/tokens/segmented.js +26 -0
- package/dist/utils/cn-theme.js +1 -1
- package/lint/README.md +6 -2
- package/lint/doctrine-rules.mjs +4 -4
- package/lint/oxlint/index.d.mts +13 -5
- package/lint/oxlint/index.mjs +26 -0
- package/lint/oxlint/no-naked-element.mjs +123 -65
- package/lint/oxlint/no-render-layout-as.mjs +61 -56
- package/lint/scan-classes.d.mts +58 -5
- package/lint/scan-classes.mjs +274 -38
- package/lint/tokens.d.mts +16 -8
- package/lint/tokens.mjs +13 -3
- package/lint/type-case.d.mts +16 -8
- package/lint/type-case.mjs +17 -0
- package/package.json +27 -14
- package/lint/oxlint/fixtures/allowed.tsx +0 -12
- package/lint/oxlint/fixtures/fixture.config.mjs +0 -29
- package/lint/oxlint/fixtures/invalid.tsx +0 -24
- package/lint/oxlint/fixtures/layout-only.tsx +0 -26
- package/lint/oxlint/fixtures/render-as-invalid.tsx +0 -11
- package/lint/oxlint/fixtures/render-as-valid.tsx +0 -14
- package/lint/oxlint/fixtures/valid.tsx +0 -18
- package/lint/oxlint/no-naked-element.test.ts +0 -155
- package/lint/oxlint/no-render-layout-as.test.ts +0 -81
package/dist/styles/index.css
CHANGED
|
@@ -20,6 +20,14 @@ import type { ComponentSize } from "./size.js";
|
|
|
20
20
|
export declare const SEGMENTED_SHELL_BASE = "rounded-full bg-surface-inset text-muted-foreground";
|
|
21
21
|
/** Shared shell padding scale by size. */
|
|
22
22
|
export declare const SEGMENTED_SHELL_PADDING: Record<ComponentSize, string>;
|
|
23
|
+
/**
|
|
24
|
+
* The horizontal half of `SEGMENTED_SHELL_PADDING`. SegmentedControl puts this
|
|
25
|
+
* on the shell and the vertical half inside its scroll area, so each item's
|
|
26
|
+
* hit area can fill the shell's full height.
|
|
27
|
+
*/
|
|
28
|
+
export declare const SEGMENTED_SHELL_PADDING_X: Record<ComponentSize, string>;
|
|
29
|
+
/** The vertical half of `SEGMENTED_SHELL_PADDING`. */
|
|
30
|
+
export declare const SEGMENTED_SHELL_PADDING_Y: Record<ComponentSize, string>;
|
|
23
31
|
/**
|
|
24
32
|
* Shared trigger/item text-size scale by size. `SegmentedControl` items,
|
|
25
33
|
* `TabsTrigger` and `TabNavigationLink` all read it, so they render identical
|
package/dist/tokens/segmented.js
CHANGED
|
@@ -28,6 +28,32 @@ export const SEGMENTED_SHELL_PADDING = {
|
|
|
28
28
|
xl: "p-1",
|
|
29
29
|
xs: "p-0.5",
|
|
30
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* The horizontal half of `SEGMENTED_SHELL_PADDING`. SegmentedControl puts this
|
|
33
|
+
* on the shell and the vertical half inside its scroll area, so each item's
|
|
34
|
+
* hit area can fill the shell's full height.
|
|
35
|
+
*/
|
|
36
|
+
export const SEGMENTED_SHELL_PADDING_X = {
|
|
37
|
+
"2xl": "px-1.5",
|
|
38
|
+
"2xs": "px-0.5",
|
|
39
|
+
"3xl": "px-1.5",
|
|
40
|
+
base: "px-0.5",
|
|
41
|
+
lg: "px-1",
|
|
42
|
+
sm: "px-0.5",
|
|
43
|
+
xl: "px-1",
|
|
44
|
+
xs: "px-0.5",
|
|
45
|
+
};
|
|
46
|
+
/** The vertical half of `SEGMENTED_SHELL_PADDING`. */
|
|
47
|
+
export const SEGMENTED_SHELL_PADDING_Y = {
|
|
48
|
+
"2xl": "py-1.5",
|
|
49
|
+
"2xs": "py-0.5",
|
|
50
|
+
"3xl": "py-1.5",
|
|
51
|
+
base: "py-0.5",
|
|
52
|
+
lg: "py-1",
|
|
53
|
+
sm: "py-0.5",
|
|
54
|
+
xl: "py-1",
|
|
55
|
+
xs: "py-0.5",
|
|
56
|
+
};
|
|
31
57
|
/**
|
|
32
58
|
* Shared trigger/item text-size scale by size. `SegmentedControl` items,
|
|
33
59
|
* `TabsTrigger` and `TabNavigationLink` all read it, so they render identical
|
package/dist/utils/cn-theme.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// GENERATED by scripts/generate-cn-theme.
|
|
1
|
+
// GENERATED by scripts/generate-cn-theme.ts - do not edit.
|
|
2
2
|
// Regenerate with `pnpm generate:cn-theme` after changing index.css; the
|
|
3
3
|
// cn test fails when a token or utility there no longer merges.
|
|
4
4
|
/** The house tokens and utilities `cn` resolves conflicts between. */
|
package/lint/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Guardrails that keep app code inside the design system. The four rules below are
|
|
|
11
11
|
|
|
12
12
|
The import restrictions are Oxlint config fragments because every unit in this repo lints with `@howells/lint` (oxlint). The hex and arbitrary-value rules scan class strings, which no import-level linter can see, so they run as a small Node script.
|
|
13
13
|
|
|
14
|
-
The two fragments live at the repo root rather than in this package: they are repo-local policy, extended by each unit's own `oxlint.config.ts`, and each carries the `@doctrine` sentence and the published ban message that `lint/doctrine-rules.mjs` and `scripts/generate-docs-manifest.
|
|
14
|
+
The two fragments live at the repo root rather than in this package: they are repo-local policy, extended by each unit's own `oxlint.config.ts`, and each carries the `@doctrine` sentence and the published ban message that `lint/doctrine-rules.mjs` and `scripts/generate-docs-manifest.ts` hydrate the doctrine page from.
|
|
15
15
|
|
|
16
16
|
## Rollout - enable per rule, not all-or-nothing
|
|
17
17
|
|
|
@@ -112,5 +112,9 @@ Run the scanner as part of your lint script. It is exposed as the `materia-ui-li
|
|
|
112
112
|
| `--all` | enable both rules |
|
|
113
113
|
| `--no-hex` | disable the hex rule |
|
|
114
114
|
| `--exclude <substr>` | skip any file whose path contains `<substr>` (repeatable) |
|
|
115
|
+
| `--json` | print one JSON document, `{ violations, rules, files }`, instead of text lines |
|
|
116
|
+
| `--help` | print usage and exit |
|
|
115
117
|
|
|
116
|
-
The scanner scopes detection to class strings only: `class`/`className` attribute values and the argument lists of `cn` / `clsx` / `cva` / `tv` / `twMerge` / `twJoin` / `classNames`. Inline `style={{ … }}` colours and ordinary colour constants are not flagged. It
|
|
118
|
+
The scanner scopes detection to class strings only: `class`/`className` attribute values and the argument lists of `cn` / `clsx` / `cva` / `tv` / `twMerge` / `twJoin` / `classNames`. Inline `style={{ … }}` colours and ordinary colour constants are not flagged. It prints `file:line:col rule value message` for each violation.
|
|
119
|
+
|
|
120
|
+
Exit codes: `0` clean, `1` violations found, `2` usage error (unknown flag, no paths), `3` a path could not be read.
|
package/lint/doctrine-rules.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* layer matrix, one api-drift gate) with NO single source file. This module is
|
|
7
7
|
* the one place that names them as a coherent set. It carries only the stable
|
|
8
8
|
* editorial framing (id / name / one-line statement / which gates enforce it);
|
|
9
|
-
* `scripts/generate-docs-manifest.
|
|
9
|
+
* `scripts/generate-docs-manifest.ts` HYDRATES each rule at build time with the
|
|
10
10
|
* live data it must never restate by hand:
|
|
11
11
|
*
|
|
12
12
|
* - `restrictedImports` - the import bans read straight out of the repo's
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* `{ group, message }` (see `IMPORT_BAN_SOURCES`). A rule opts in via
|
|
15
15
|
* `banMatch`, a substring matched against those live messages.
|
|
16
16
|
* - `matrix` / `layers` - the layer dependency matrix imported from
|
|
17
|
-
* scripts/layer-matrix.
|
|
17
|
+
* scripts/layer-matrix.ts's exported `LAYERS` / `ALLOWED`.
|
|
18
18
|
* - per-gate `doctrine` prose - the single `@doctrine` sentinel line in each
|
|
19
19
|
* gate script's header, extracted by a strict tag-line parse.
|
|
20
20
|
*
|
|
@@ -92,7 +92,7 @@ export const DOCTRINE_RULES = [
|
|
|
92
92
|
kind: "layer-matrix",
|
|
93
93
|
statement:
|
|
94
94
|
"The six src layers form a strict one-directional dependency matrix (data ← tokens ← utils ← hooks ← components ← compositions); tokens additionally take no external packages.",
|
|
95
|
-
gates: ["packages/ui/scripts/layer-matrix.
|
|
95
|
+
gates: ["packages/ui/scripts/layer-matrix.ts"],
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
id: "no-naked-element",
|
|
@@ -106,6 +106,6 @@ export const DOCTRINE_RULES = [
|
|
|
106
106
|
name: "API surface drift gate",
|
|
107
107
|
statement:
|
|
108
108
|
"The full public export surface is captured as resolved-type hashes in api-report.md; any change to a public signature flips a hash and fails the gate until the report is regenerated and reviewed.",
|
|
109
|
-
gates: ["packages/ui/scripts/generate-api-report.
|
|
109
|
+
gates: ["packages/ui/scripts/generate-api-report.ts"],
|
|
110
110
|
},
|
|
111
111
|
];
|
package/lint/oxlint/index.d.mts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
// GENERATED by scripts/generate-lint-types.ts from the JSDoc in lint/oxlint/index.mjs - do not edit.
|
|
2
|
+
// Regenerate with `pnpm --filter @instruments/materia-ui generate:lint-types`.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One rule of the plugin: its metadata and the visitor factory oxlint calls.
|
|
6
|
+
*/
|
|
2
7
|
export interface MateriaRule {
|
|
3
8
|
readonly meta: {
|
|
4
9
|
readonly type: "problem";
|
|
5
|
-
readonly docs: {
|
|
10
|
+
readonly docs: {
|
|
11
|
+
readonly description: string;
|
|
12
|
+
};
|
|
6
13
|
readonly messages: Readonly<Record<string, string>>;
|
|
7
14
|
readonly schema: readonly unknown[];
|
|
8
15
|
};
|
|
9
|
-
readonly create: (context:
|
|
16
|
+
readonly create: (context: never) => object;
|
|
10
17
|
}
|
|
11
18
|
|
|
12
19
|
/**
|
|
@@ -14,11 +21,12 @@ export interface MateriaRule {
|
|
|
14
21
|
* rules as `materia-ui/no-naked-element` and `materia-ui/no-render-layout-as`.
|
|
15
22
|
*/
|
|
16
23
|
declare const plugin: {
|
|
17
|
-
readonly meta: {
|
|
24
|
+
readonly meta: {
|
|
25
|
+
readonly name: "materia-ui";
|
|
26
|
+
};
|
|
18
27
|
readonly rules: {
|
|
19
28
|
readonly "no-naked-element": MateriaRule;
|
|
20
29
|
readonly "no-render-layout-as": MateriaRule;
|
|
21
30
|
};
|
|
22
31
|
};
|
|
23
|
-
|
|
24
32
|
export default plugin;
|
package/lint/oxlint/index.mjs
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import { rule as noNakedElement } from "./no-naked-element.mjs";
|
|
2
2
|
import { rule as noRenderLayoutAs } from "./no-render-layout-as.mjs";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* One rule of the plugin: its metadata and the visitor factory oxlint calls.
|
|
6
|
+
*
|
|
7
|
+
* @typedef {{
|
|
8
|
+
* readonly meta: {
|
|
9
|
+
* readonly type: "problem";
|
|
10
|
+
* readonly docs: { readonly description: string };
|
|
11
|
+
* readonly messages: Readonly<Record<string, string>>;
|
|
12
|
+
* readonly schema: readonly unknown[];
|
|
13
|
+
* };
|
|
14
|
+
* readonly create: (context: never) => object;
|
|
15
|
+
* }} MateriaRule
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The `materia-ui` oxlint plugin. Load it through `jsPlugins` and enable its
|
|
20
|
+
* rules as `materia-ui/no-naked-element` and `materia-ui/no-render-layout-as`.
|
|
21
|
+
*
|
|
22
|
+
* @type {{
|
|
23
|
+
* readonly meta: { readonly name: "materia-ui" };
|
|
24
|
+
* readonly rules: {
|
|
25
|
+
* readonly "no-naked-element": MateriaRule;
|
|
26
|
+
* readonly "no-render-layout-as": MateriaRule;
|
|
27
|
+
* };
|
|
28
|
+
* }}
|
|
29
|
+
*/
|
|
4
30
|
const plugin = {
|
|
5
31
|
meta: {
|
|
6
32
|
name: "materia-ui",
|
|
@@ -10,6 +10,20 @@
|
|
|
10
10
|
//
|
|
11
11
|
// @doctrine Application markup renders only design-system components - every raw lowercase HTML host element is lint-banned, leaving the closed polymorphic `as` vocabulary and a tokens-policed className as the only routes to the metal.
|
|
12
12
|
|
|
13
|
+
/** @typedef {Parameters<import("oxlint/plugins-dev").RuleTester["run"]>[1]} Rule */
|
|
14
|
+
/** @typedef {NonNullable<Rule["create"]>} Create */
|
|
15
|
+
/** @typedef {Parameters<Create>[0]} Context */
|
|
16
|
+
/** @typedef {ReturnType<Create>} Visitor */
|
|
17
|
+
/** @typedef {Parameters<NonNullable<Visitor["JSXOpeningElement"]>>[0]} OpeningElement */
|
|
18
|
+
/** @typedef {Extract<OpeningElement["attributes"][number], { type: "JSXAttribute" }>} Attribute */
|
|
19
|
+
/** @typedef {NonNullable<Attribute["value"]>} AttributeValue */
|
|
20
|
+
/** @typedef {Extract<AttributeValue, { type: "JSXExpressionContainer" }>["expression"]} Expression */
|
|
21
|
+
/** @typedef {Extract<Expression, { type: "CallExpression" }>["arguments"][number]} Argument */
|
|
22
|
+
/** @typedef {Extract<Expression, { type: "TemplateLiteral" }>} TemplateLiteral */
|
|
23
|
+
/** @typedef {Extract<Expression, { type: "Literal"; value: string }>} StringLiteral */
|
|
24
|
+
/** @typedef {Context["options"][number]} OptionValue */
|
|
25
|
+
/** @typedef {Extract<OptionValue, Record<string, OptionValue>>} OptionObject */
|
|
26
|
+
|
|
13
27
|
/**
|
|
14
28
|
* Map of raw HTML tag → the design-system replacement guidance surfaced in the
|
|
15
29
|
* diagnostic. Every member of the tokens `LAYOUT_ELEMENTS` / `TEXT_ELEMENTS`
|
|
@@ -67,14 +81,16 @@ const ESCAPE_HATCH =
|
|
|
67
81
|
* Build the diagnostic message for a banned raw tag.
|
|
68
82
|
*
|
|
69
83
|
* @param {string} tag lowercase HTML tag name
|
|
70
|
-
* @returns {string}
|
|
84
|
+
* @returns {string} The message naming the sanctioned replacement.
|
|
71
85
|
*/
|
|
72
|
-
export
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
export const messageFor = (tag) => {
|
|
87
|
+
const replacement = REPLACEMENTS[tag];
|
|
88
|
+
const guidance =
|
|
89
|
+
replacement === undefined
|
|
90
|
+
? "Use a @instruments/materia-ui component instead."
|
|
91
|
+
: `Use ${replacement} instead.`;
|
|
76
92
|
return `Raw <${tag}> is banned in the LLM-safe design system. ${guidance} ${ESCAPE_HATCH}`;
|
|
77
|
-
}
|
|
93
|
+
};
|
|
78
94
|
|
|
79
95
|
/**
|
|
80
96
|
* Hosts a layout primitive can emit through `as` - the tokens `LAYOUT_ELEMENTS`
|
|
@@ -110,26 +126,27 @@ const LAYOUT_UTILITY_SET = new Set(LAYOUT_UTILITIES);
|
|
|
110
126
|
*
|
|
111
127
|
* @param {string} tag lowercase HTML tag name
|
|
112
128
|
* @param {string} utility the layout utility found in its classes
|
|
113
|
-
* @returns {string}
|
|
129
|
+
* @returns {string} The message naming the layout primitive to use.
|
|
114
130
|
*/
|
|
115
|
-
export
|
|
131
|
+
export const layoutMessageFor = (tag, utility) => {
|
|
116
132
|
const primitive = utility.endsWith("grid")
|
|
117
133
|
? "Grid | SimpleGrid"
|
|
118
134
|
: "Flex | Stack | Group | Center";
|
|
119
135
|
const as = tag === "div" ? "" : ` with as="${tag}"`;
|
|
120
136
|
return `Raw <${tag}> with \`${utility}\` is a hand-rolled layout. Use ${primitive}${as} instead. ${ESCAPE_HATCH}`;
|
|
121
|
-
}
|
|
137
|
+
};
|
|
122
138
|
|
|
123
139
|
/**
|
|
124
140
|
* Whole class tokens of one template literal, dropping any token glued to an
|
|
125
141
|
* interpolation (`size-${n}`), since only its static half is known.
|
|
126
142
|
*
|
|
127
|
-
* @param {
|
|
128
|
-
* @returns {string[]}
|
|
143
|
+
* @param {TemplateLiteral} node The template literal.
|
|
144
|
+
* @returns {string[]} The class tokens whose full text is known.
|
|
129
145
|
*/
|
|
130
|
-
|
|
146
|
+
const templateTokens = (node) => {
|
|
147
|
+
/** @type {string[]} */
|
|
131
148
|
const tokens = [];
|
|
132
|
-
node.quasis.
|
|
149
|
+
for (const [index, quasi] of node.quasis.entries()) {
|
|
133
150
|
const text = quasi.value.cooked ?? quasi.value.raw;
|
|
134
151
|
const parts = text.split(/\s+/u);
|
|
135
152
|
if (index > 0 && !/^\s/u.test(text)) {
|
|
@@ -139,9 +156,16 @@ function templateTokens(node) {
|
|
|
139
156
|
parts.pop();
|
|
140
157
|
}
|
|
141
158
|
tokens.push(...parts);
|
|
142
|
-
}
|
|
159
|
+
}
|
|
143
160
|
return tokens;
|
|
144
|
-
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {Expression | Argument | AttributeValue} node A node that may be a literal.
|
|
165
|
+
* @returns {node is StringLiteral} Whether it is a string literal.
|
|
166
|
+
*/
|
|
167
|
+
const isStringLiteral = (node) =>
|
|
168
|
+
node.type === "Literal" && node.value?.constructor === String;
|
|
145
169
|
|
|
146
170
|
/**
|
|
147
171
|
* Class tokens an expression always contributes: string and template literals,
|
|
@@ -149,15 +173,15 @@ function templateTokens(node) {
|
|
|
149
173
|
* Conditional branches are skipped - a host that is only sometimes a flex
|
|
150
174
|
* container is not a fixed layout.
|
|
151
175
|
*
|
|
152
|
-
* @param {
|
|
153
|
-
* @param {boolean} [inCall]
|
|
154
|
-
* @returns {string[]}
|
|
176
|
+
* @param {Expression | Argument | AttributeValue | null | undefined} node The class expression.
|
|
177
|
+
* @param {boolean} [inCall] Whether `node` is already an argument of a helper call.
|
|
178
|
+
* @returns {string[]} The class tokens that always apply.
|
|
155
179
|
*/
|
|
156
|
-
|
|
180
|
+
const unconditionalTokens = (node, inCall = false) => {
|
|
157
181
|
if (!node) {
|
|
158
182
|
return [];
|
|
159
183
|
}
|
|
160
|
-
if (node
|
|
184
|
+
if (isStringLiteral(node)) {
|
|
161
185
|
return node.value.split(/\s+/u);
|
|
162
186
|
}
|
|
163
187
|
if (node.type === "TemplateLiteral") {
|
|
@@ -167,31 +191,63 @@ function unconditionalTokens(node, inCall = false) {
|
|
|
167
191
|
return node.arguments.flatMap((arg) => unconditionalTokens(arg, true));
|
|
168
192
|
}
|
|
169
193
|
return [];
|
|
170
|
-
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @param {OpeningElement["attributes"][number]} attribute A JSX attribute.
|
|
198
|
+
* @returns {attribute is Attribute} Whether it is `className`.
|
|
199
|
+
*/
|
|
200
|
+
const isClassName = (attribute) =>
|
|
201
|
+
attribute.type === "JSXAttribute" &&
|
|
202
|
+
attribute.name.type === "JSXIdentifier" &&
|
|
203
|
+
attribute.name.name === "className";
|
|
171
204
|
|
|
172
205
|
/**
|
|
173
206
|
* The first layout utility among a JSX element's unconditional classes.
|
|
174
207
|
*
|
|
175
|
-
* @param {
|
|
176
|
-
* @returns {string | undefined}
|
|
208
|
+
* @param {OpeningElement} openingElement The element to inspect.
|
|
209
|
+
* @returns {string | undefined} The utility, if there is one.
|
|
177
210
|
*/
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
211
|
+
const layoutUtilityOf = (openingElement) => {
|
|
212
|
+
const value = openingElement.attributes.find(isClassName)?.value;
|
|
213
|
+
const expression =
|
|
214
|
+
value?.type === "JSXExpressionContainer" ? value.expression : value;
|
|
215
|
+
return unconditionalTokens(expression).find((token) =>
|
|
216
|
+
LAYOUT_UTILITY_SET.has(token)
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @param {OptionValue | undefined} value A rule option.
|
|
222
|
+
* @returns {value is OptionObject} Whether it is an options object.
|
|
223
|
+
*/
|
|
224
|
+
const isOptionObject = (value) =>
|
|
225
|
+
value instanceof Object && !Array.isArray(value);
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @param {OptionValue | undefined} value An entry of the `allow` option.
|
|
229
|
+
* @returns {value is string} Whether it is a tag name.
|
|
230
|
+
*/
|
|
231
|
+
const isTagName = (value) => value?.constructor === String;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The rule's options as configured: `allow` names exempt tags and `layoutOnly`
|
|
235
|
+
* narrows the rule to hand-rolled layout.
|
|
236
|
+
*
|
|
237
|
+
* @param {Context} context The rule context.
|
|
238
|
+
* @returns {{ allow: Set<string>, layoutOnly: boolean }} The options with defaults applied.
|
|
239
|
+
*/
|
|
240
|
+
const readOptions = (context) => {
|
|
241
|
+
const [option] = context.options;
|
|
242
|
+
if (!isOptionObject(option)) {
|
|
243
|
+
return { allow: new Set(), layoutOnly: false };
|
|
192
244
|
}
|
|
193
|
-
|
|
194
|
-
|
|
245
|
+
const { allow, layoutOnly } = option;
|
|
246
|
+
return {
|
|
247
|
+
allow: new Set(Array.isArray(allow) ? allow.filter(isTagName) : []),
|
|
248
|
+
layoutOnly: layoutOnly === true,
|
|
249
|
+
};
|
|
250
|
+
};
|
|
195
251
|
|
|
196
252
|
/**
|
|
197
253
|
* ESLint-style rule factory. Visits every JSX opening element and reports raw
|
|
@@ -199,17 +255,17 @@ function layoutUtilityOf(openingElement) {
|
|
|
199
255
|
* `options[0].layoutOnly`, only layout hosts whose unconditional classes carry
|
|
200
256
|
* a flex or grid display utility report.
|
|
201
257
|
*
|
|
202
|
-
* @param {
|
|
258
|
+
* @param {Context} context The rule context.
|
|
259
|
+
* @returns {Visitor} The visitor oxlint runs over the file.
|
|
203
260
|
*/
|
|
204
|
-
export
|
|
205
|
-
const allow =
|
|
206
|
-
const layoutOnly = context.options?.[0]?.layoutOnly === true;
|
|
261
|
+
export const create = (context) => {
|
|
262
|
+
const { allow, layoutOnly } = readOptions(context);
|
|
207
263
|
|
|
208
264
|
return {
|
|
209
265
|
JSXOpeningElement(node) {
|
|
210
266
|
const { name } = node;
|
|
211
267
|
// JSXMemberExpression (Foo.Bar) and JSXNamespacedName are never bare hosts.
|
|
212
|
-
if (name
|
|
268
|
+
if (name.type !== "JSXIdentifier") {
|
|
213
269
|
return;
|
|
214
270
|
}
|
|
215
271
|
const tag = name.name;
|
|
@@ -233,27 +289,29 @@ export function create(context) {
|
|
|
233
289
|
}
|
|
234
290
|
},
|
|
235
291
|
};
|
|
236
|
-
}
|
|
292
|
+
};
|
|
237
293
|
|
|
238
294
|
/** Rule metadata + factory, ready to slot into the plugin's `rules` map. */
|
|
239
|
-
export const rule =
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
schema: [
|
|
248
|
-
{
|
|
249
|
-
type: "object",
|
|
250
|
-
properties: {
|
|
251
|
-
allow: { type: "array", items: { type: "string" } },
|
|
252
|
-
layoutOnly: { type: "boolean" },
|
|
253
|
-
},
|
|
254
|
-
additionalProperties: false,
|
|
295
|
+
export const rule =
|
|
296
|
+
/** @satisfies {Rule} */
|
|
297
|
+
({
|
|
298
|
+
meta: {
|
|
299
|
+
type: "problem",
|
|
300
|
+
docs: {
|
|
301
|
+
description:
|
|
302
|
+
"Disallow raw lowercase HTML host elements - use the design-system component vocabulary.",
|
|
255
303
|
},
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
304
|
+
messages: {},
|
|
305
|
+
schema: [
|
|
306
|
+
{
|
|
307
|
+
type: "object",
|
|
308
|
+
properties: {
|
|
309
|
+
allow: { type: "array", items: { type: "string" } },
|
|
310
|
+
layoutOnly: { type: "boolean" },
|
|
311
|
+
},
|
|
312
|
+
additionalProperties: false,
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
create,
|
|
317
|
+
});
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
//
|
|
7
7
|
// @doctrine Use `as` to change a layout primitive's element; use `render` only to swap in a component, never `render={<Box as=.../>}`.
|
|
8
8
|
|
|
9
|
+
/** @typedef {Parameters<import("oxlint/plugins-dev").RuleTester["run"]>[1]} Rule */
|
|
10
|
+
/** @typedef {NonNullable<Rule["create"]>} Create */
|
|
11
|
+
/** @typedef {Parameters<Create>[0]} Context */
|
|
12
|
+
/** @typedef {ReturnType<Create>} Visitor */
|
|
13
|
+
|
|
9
14
|
/**
|
|
10
15
|
* Layout primitives that share the closed `as` / `render` contract. A
|
|
11
16
|
* `render={<Name as=... />}` on any prop is the mistake, so the set is every
|
|
@@ -34,73 +39,73 @@ export const LAYOUT_PRIMITIVES = new Set([
|
|
|
34
39
|
* Build the diagnostic message for a banned `render={<Primitive as=... />}`.
|
|
35
40
|
*
|
|
36
41
|
* @param {string} primitive component name used inside `render`
|
|
37
|
-
* @returns {string}
|
|
42
|
+
* @returns {string} The message telling the author to pass `as` to the outer element.
|
|
38
43
|
*/
|
|
39
|
-
export
|
|
40
|
-
|
|
41
|
-
}
|
|
44
|
+
export const messageFor = (primitive) =>
|
|
45
|
+
`render={<${primitive} as=... />} nests a layout primitive only to set a tag. Pass \`as\` directly on the outer element; use \`render\` only to swap in a component (e.g. a router Link).`;
|
|
42
46
|
|
|
43
47
|
/**
|
|
44
48
|
* ESLint-style rule factory. Visits every layout primitive whose `render` prop
|
|
45
49
|
* is another layout-primitive element carrying an `as` attribute.
|
|
46
50
|
*
|
|
47
|
-
* @param {
|
|
51
|
+
* @param {Context} context The rule context.
|
|
52
|
+
* @returns {Visitor} The visitor oxlint runs over the file.
|
|
48
53
|
*/
|
|
49
|
-
export
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
export const create = (context) => ({
|
|
55
|
+
JSXOpeningElement(opening) {
|
|
56
|
+
// Only a layout primitive as the OUTER element: it has `as`, so nesting is
|
|
57
|
+
// needless. A trigger that has no `as` (TooltipTrigger, say) legitimately
|
|
58
|
+
// takes `render={<Box as="span" />}` to get a tag.
|
|
59
|
+
if (
|
|
60
|
+
opening.name.type !== "JSXIdentifier" ||
|
|
61
|
+
!LAYOUT_PRIMITIVES.has(opening.name.name)
|
|
62
|
+
) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const attribute of opening.attributes) {
|
|
55
66
|
if (
|
|
56
|
-
|
|
57
|
-
|
|
67
|
+
attribute.type !== "JSXAttribute" ||
|
|
68
|
+
attribute.name.type !== "JSXIdentifier" ||
|
|
69
|
+
attribute.name.name !== "render"
|
|
58
70
|
) {
|
|
59
|
-
|
|
71
|
+
continue;
|
|
60
72
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
attribute.type !== "JSXAttribute" ||
|
|
64
|
-
attribute.name?.type !== "JSXIdentifier" ||
|
|
65
|
-
attribute.name.name !== "render"
|
|
66
|
-
) {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
const element = attribute.value?.expression;
|
|
70
|
-
if (element?.type !== "JSXElement") {
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
const {name} = element.openingElement;
|
|
74
|
-
if (
|
|
75
|
-
name?.type !== "JSXIdentifier" ||
|
|
76
|
-
!LAYOUT_PRIMITIVES.has(name.name)
|
|
77
|
-
) {
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
const hasAs = element.openingElement.attributes.some(
|
|
81
|
-
(inner) =>
|
|
82
|
-
inner.type === "JSXAttribute" &&
|
|
83
|
-
inner.name?.type === "JSXIdentifier" &&
|
|
84
|
-
inner.name.name === "as"
|
|
85
|
-
);
|
|
86
|
-
if (hasAs) {
|
|
87
|
-
context.report({ node: element, message: messageFor(name.name) });
|
|
88
|
-
}
|
|
73
|
+
if (attribute.value?.type !== "JSXExpressionContainer") {
|
|
74
|
+
continue;
|
|
89
75
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
76
|
+
const element = attribute.value.expression;
|
|
77
|
+
if (element.type !== "JSXElement") {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const { name } = element.openingElement;
|
|
81
|
+
if (name.type !== "JSXIdentifier" || !LAYOUT_PRIMITIVES.has(name.name)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const hasAs = element.openingElement.attributes.some(
|
|
85
|
+
(inner) =>
|
|
86
|
+
inner.type === "JSXAttribute" &&
|
|
87
|
+
inner.name.type === "JSXIdentifier" &&
|
|
88
|
+
inner.name.name === "as"
|
|
89
|
+
);
|
|
90
|
+
if (hasAs) {
|
|
91
|
+
context.report({ node: element, message: messageFor(name.name) });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
});
|
|
93
96
|
|
|
94
97
|
/** Rule metadata + factory, ready to slot into the plugin's `rules` map. */
|
|
95
|
-
export const rule =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
export const rule =
|
|
99
|
+
/** @satisfies {Rule} */
|
|
100
|
+
({
|
|
101
|
+
meta: {
|
|
102
|
+
type: "problem",
|
|
103
|
+
docs: {
|
|
104
|
+
description:
|
|
105
|
+
"Disallow render={<Box as=... />}: pass `as` to the outer layout primitive instead.",
|
|
106
|
+
},
|
|
107
|
+
messages: {},
|
|
108
|
+
schema: [],
|
|
101
109
|
},
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
},
|
|
105
|
-
create,
|
|
106
|
-
};
|
|
110
|
+
create,
|
|
111
|
+
});
|