@ctrl-ui/react 0.9.13 → 0.9.15
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 +19 -8
- package/dist/blocks/design-canvas.js +16 -10
- package/dist/control-variants.d.ts +1 -1
- package/dist/control-variants.js +2 -1
- package/dist/knob-contracts/button-knobs.d.ts +1 -1
- package/dist/knob-contracts/button-knobs.js +11 -10
- package/dist/knob-contracts/reveal-knobs.d.ts +4 -0
- package/dist/knob-contracts/reveal-knobs.js +4 -0
- package/dist/styles/index.css +1 -0
- package/dist/styles/recipes/button-states.css +50 -0
- package/dist/styles/recipes/button.css +19 -52
- package/dist/styles/recipes/infinite-canvas.css +8 -0
- package/dist/styles/recipes/reveal.css +26 -0
- package/dist/ui/reveal.d.ts +15 -0
- package/dist/ui/reveal.js +29 -0
- package/lint/biome-preset.json +8 -1
- package/lint/class-policy.mjs +50 -1
- package/lint/eslint-plugin.mjs +98 -49
- package/lint/grit-rules.mjs +131 -42
- package/lint/no-component-part-css.grit +16 -0
- package/lint/no-component-style-classes.grit +66 -36
- package/lint/no-component-style-props.grit +51 -0
- package/lint/no-raw-type-classes.grit +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -49,12 +49,14 @@ Controls size their own text: pick the control's `size`, or set its `--cui-*-fon
|
|
|
49
49
|
|
|
50
50
|
## Lint rules
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
These rules keep call sites honest. They come from one policy (`lint/class-policy.mjs`) and ship for Biome, ESLint, and oxlint, except the stylesheet rule, which is Biome only:
|
|
53
53
|
|
|
54
|
-
- `no-component-style-classes` errors when `className` on a component imported from `@ctrl-ui/react` (or a `components/control-ui` directory) sets what the component owns: colors, type, padding, borders, radius, shadows, state styles such as `hover:` and `data-[…]:`, or `!important`. Every `text-*` class counts except alignment and wrapping (`text-center`, `text-pretty`, `text-ellipsis`…). The rule also reads string constants the `className` references in the same file, and the `className` of the element passed to the component's `render` prop. Layout classes (`flex`, `gap-*`, widths, heights, margins, placement) stay allowed. Restyle through the component's props or its `--cui-*` knobs instead.
|
|
54
|
+
- `no-component-style-classes` errors when `className` on a component imported from `@ctrl-ui/react` (or a `components/control-ui` directory) sets what the component owns: colors, type, padding, borders, radius, shadows, filters, state styles such as `hover:` and `data-[…]:`, arbitrary properties that paint (`[background:…]`), or `!important`. Every `text-*` class counts except alignment and wrapping (`text-center`, `text-pretty`, `text-ellipsis`…). The rule also reads string constants the `className` references in the same file, and the `className` of the element passed to the component's `render` prop. On any other element, it errors on classes that reach into a component's parts, such as `[&_[data-slot=trigger]]:` or `*:data-[slot=icon]:`. Layout classes (`flex`, `gap-*`, widths, heights, margins, placement) stay allowed. Restyle through the component's props or its `--cui-*` knobs instead.
|
|
55
|
+
- `no-component-style-props` applies the same boundary to `style`: `--cui-*` knobs and layout keys pass, while paint keys error (`color`, `background`, `font*`, `padding*`, `border*`, `boxShadow`, `opacity`, `filter`…). It also checks same-file constant objects and the element passed to `render`. When the paint is data, such as a user-picked fill, put it on a plain element of your own and let the component inherit it.
|
|
56
|
+
- `no-component-part-css` errors when a stylesheet paints a component part: a rule whose subject is `[data-slot]`, `[data-control-family]`, or another `[data-control…]` attribute, including nested `&` rules and `@apply`. Setting the part's `--cui-*` knobs and layout there stays allowed. The preset skips installed Control UI source under `components/control-ui`.
|
|
55
57
|
- `no-raw-type-classes` errors on text sizes in any `className`: the type rungs, Tailwind's `text-xs`…`text-9xl`, and arbitrary `text-[13px]`. It reaches beyond Control UI, so adopt it folder by folder while you migrate.
|
|
56
58
|
|
|
57
|
-
The Biome rules need Biome 2.
|
|
59
|
+
The Biome rules need Biome 2.5.0 or later: the preset scopes the stylesheet rule with plugin `includes`.
|
|
58
60
|
|
|
59
61
|
Biome, extending the shipped preset so rules added later arrive with the package update:
|
|
60
62
|
|
|
@@ -70,22 +72,31 @@ ESLint flat config:
|
|
|
70
72
|
```js
|
|
71
73
|
import controlUi from "@ctrl-ui/react/eslint-plugin";
|
|
72
74
|
|
|
73
|
-
export default [
|
|
75
|
+
export default [
|
|
76
|
+
{
|
|
77
|
+
plugins: { "control-ui": controlUi },
|
|
78
|
+
rules: { "control-ui/no-component-style-classes": "error", "control-ui/no-component-style-props": "error" },
|
|
79
|
+
},
|
|
80
|
+
];
|
|
74
81
|
```
|
|
75
82
|
|
|
76
83
|
oxlint, through its JS plugin support:
|
|
77
84
|
|
|
78
85
|
```json
|
|
79
|
-
{
|
|
86
|
+
{
|
|
87
|
+
"jsPlugins": ["@ctrl-ui/react/eslint-plugin"],
|
|
88
|
+
"rules": { "control-ui/no-component-style-classes": "error", "control-ui/no-component-style-props": "error" }
|
|
89
|
+
}
|
|
80
90
|
```
|
|
81
91
|
|
|
82
|
-
When your app imports components through its own wrapper package or folder, declare that path so the
|
|
92
|
+
When your app imports components through its own wrapper package or folder, declare that path so the rules follow those imports too:
|
|
83
93
|
|
|
84
94
|
```js
|
|
85
|
-
|
|
95
|
+
const options = { importSources: ["@acme/ui/components/"] };
|
|
96
|
+
rules: { "control-ui/no-component-style-classes": ["error", options], "control-ui/no-component-style-props": ["error", options] }
|
|
86
97
|
```
|
|
87
98
|
|
|
88
|
-
Biome's Grit plugins take no options, so the package ships `control-ui-grit` to write the rules with your paths. Run it on install so a package update regenerates them, gitignore `.control-ui`, and list
|
|
99
|
+
Biome's Grit plugins take no options, so the package ships `control-ui-grit` to write the rules with your paths. Run it on install so a package update regenerates them, gitignore `.control-ui`, and list the generated `no-component-style-classes.grit`, `no-component-style-props.grit`, and `no-component-part-css.grit` from `./.control-ui/` under `plugins` in place of the preset:
|
|
89
100
|
|
|
90
101
|
```json
|
|
91
102
|
{ "scripts": { "postinstall": "control-ui-grit --import-source=@acme/ui/components/ --out=.control-ui" } }
|
|
@@ -412,8 +412,7 @@ function DesignCanvasLayerNode({ layer, selected, onSelect, onKeyDown, onLayerCh
|
|
|
412
412
|
style: {
|
|
413
413
|
width: layer.width,
|
|
414
414
|
height: layer.height,
|
|
415
|
-
rotate: `${layer.rotation}deg
|
|
416
|
-
opacity: layer.opacity / 100
|
|
415
|
+
rotate: `${layer.rotation}deg`
|
|
417
416
|
},
|
|
418
417
|
onPointerDown: onSelect,
|
|
419
418
|
onKeyDown,
|
|
@@ -429,18 +428,25 @@ function DesignCanvasLayerNode({ layer, selected, onSelect, onKeyDown, onLayerCh
|
|
|
429
428
|
className: "absolute start-0 bottom-full mb-1 whitespace-nowrap",
|
|
430
429
|
children: layer.name
|
|
431
430
|
}) : null,
|
|
432
|
-
layer.kind === "text" ? /* @__PURE__ */ jsx(
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
children:
|
|
431
|
+
layer.kind === "text" ? /* @__PURE__ */ jsx("div", {
|
|
432
|
+
className: "size-full",
|
|
433
|
+
style: {
|
|
434
|
+
color: fillColor,
|
|
435
|
+
opacity: layer.opacity / 100
|
|
436
|
+
},
|
|
437
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
438
|
+
as: "p",
|
|
439
|
+
size: "heading-2",
|
|
440
|
+
weight: "normal",
|
|
441
|
+
className: "size-full overflow-hidden whitespace-nowrap",
|
|
442
|
+
children: layer.text
|
|
443
|
+
})
|
|
439
444
|
}) : /* @__PURE__ */ jsx("div", {
|
|
440
445
|
className: "size-full",
|
|
441
446
|
style: {
|
|
442
447
|
background: fillColor,
|
|
443
|
-
borderRadius: layer.kind === "ellipse" ? "50%" : layer.cornerRadius
|
|
448
|
+
borderRadius: layer.kind === "ellipse" ? "50%" : layer.cornerRadius,
|
|
449
|
+
opacity: layer.opacity / 100
|
|
444
450
|
}
|
|
445
451
|
}),
|
|
446
452
|
selected ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region src/control-variants.d.ts
|
|
2
2
|
export type ControlSize = "xs" | "sm" | "md" | "lg";
|
|
3
|
-
export declare const buttonVariants: readonly ["solid", "surface", "ghost", "quiet", "dashed", "link"];
|
|
3
|
+
export declare const buttonVariants: readonly ["solid", "surface", "ghost", "quiet", "dashed", "link", "scrim"];
|
|
4
4
|
export type ButtonVariant = (typeof buttonVariants)[number];
|
|
5
5
|
export declare const controlTones: readonly ["neutral", "primary", "danger"];
|
|
6
6
|
export type ControlTone = (typeof controlTones)[number];
|
package/dist/control-variants.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
//#region src/knob-contracts/button-knobs.d.ts
|
|
2
|
-
export declare const buttonKnobs: readonly ["--cui-button-press-background", "--cui-button-press-scale", "--cui-button-hover-border-color", "--cui-button-open-border-color", "--cui-button-open-background", "--cui-button-open-foreground", "--cui-button-open-shadow", "--cui-button-
|
|
2
|
+
export declare const buttonKnobs: readonly ["--cui-button-press-background", "--cui-button-press-scale", "--cui-button-hover-border-color", "--cui-button-open-border-color", "--cui-button-open-background", "--cui-button-open-foreground", "--cui-button-open-shadow", "--cui-button-hover-background-image", "--cui-button-hover-background", "--cui-button-hover-foreground", "--cui-button-active-background", "--cui-button-active-foreground", "--cui-button-active-hover-background", "--cui-button-hover-shadow", "--cui-button-press-shadow", "--cui-button-active-shadow", "--cui-button-radius", "--cui-button-gap", "--cui-button-icon", "--cui-button-height", "--cui-button-padding-inline", "--cui-button-padding-block", "--cui-button-icon-padding-inline", "--cui-button-font-size", "--cui-button-font-weight", "--cui-button-background", "--cui-button-background-image", "--cui-button-foreground", "--cui-button-shadow", "--cui-button-border-width", "--cui-button-border-style", "--cui-button-border-color", "--cui-button-backdrop-filter"];
|
|
3
3
|
export type ButtonKnobStyle = Partial<Record<(typeof buttonKnobs)[number], string>>;
|
|
4
4
|
//#endregion
|
|
@@ -7,6 +7,15 @@ const buttonKnobs = [
|
|
|
7
7
|
"--cui-button-open-background",
|
|
8
8
|
"--cui-button-open-foreground",
|
|
9
9
|
"--cui-button-open-shadow",
|
|
10
|
+
"--cui-button-hover-background-image",
|
|
11
|
+
"--cui-button-hover-background",
|
|
12
|
+
"--cui-button-hover-foreground",
|
|
13
|
+
"--cui-button-active-background",
|
|
14
|
+
"--cui-button-active-foreground",
|
|
15
|
+
"--cui-button-active-hover-background",
|
|
16
|
+
"--cui-button-hover-shadow",
|
|
17
|
+
"--cui-button-press-shadow",
|
|
18
|
+
"--cui-button-active-shadow",
|
|
10
19
|
"--cui-button-radius",
|
|
11
20
|
"--cui-button-gap",
|
|
12
21
|
"--cui-button-icon",
|
|
@@ -18,20 +27,12 @@ const buttonKnobs = [
|
|
|
18
27
|
"--cui-button-font-weight",
|
|
19
28
|
"--cui-button-background",
|
|
20
29
|
"--cui-button-background-image",
|
|
21
|
-
"--cui-button-hover-background-image",
|
|
22
30
|
"--cui-button-foreground",
|
|
23
|
-
"--cui-button-hover-background",
|
|
24
|
-
"--cui-button-hover-foreground",
|
|
25
|
-
"--cui-button-active-background",
|
|
26
|
-
"--cui-button-active-foreground",
|
|
27
|
-
"--cui-button-active-hover-background",
|
|
28
31
|
"--cui-button-shadow",
|
|
29
|
-
"--cui-button-hover-shadow",
|
|
30
|
-
"--cui-button-press-shadow",
|
|
31
|
-
"--cui-button-active-shadow",
|
|
32
32
|
"--cui-button-border-width",
|
|
33
33
|
"--cui-button-border-style",
|
|
34
|
-
"--cui-button-border-color"
|
|
34
|
+
"--cui-button-border-color",
|
|
35
|
+
"--cui-button-backdrop-filter"
|
|
35
36
|
];
|
|
36
37
|
//#endregion
|
|
37
38
|
export { buttonKnobs };
|
package/dist/styles/index.css
CHANGED
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
@import "./recipes/pagination.css";
|
|
85
85
|
@import "./recipes/phone-input.css";
|
|
86
86
|
@import "./recipes/resize-handle.css";
|
|
87
|
+
@import "./recipes/reveal.css";
|
|
87
88
|
@import "./recipes/rich-tooltip.css";
|
|
88
89
|
@import "./recipes/skeleton.css";
|
|
89
90
|
@import "./recipes/stepper.css";
|
|
@@ -125,3 +125,53 @@
|
|
|
125
125
|
syntax: "*";
|
|
126
126
|
inherits: true;
|
|
127
127
|
}
|
|
128
|
+
|
|
129
|
+
@property --cui-button-hover-background-image {
|
|
130
|
+
syntax: "*";
|
|
131
|
+
inherits: true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@property --cui-button-hover-background {
|
|
135
|
+
syntax: "<color>";
|
|
136
|
+
inherits: true;
|
|
137
|
+
initial-value: transparent;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@property --cui-button-hover-foreground {
|
|
141
|
+
syntax: "<color>";
|
|
142
|
+
inherits: true;
|
|
143
|
+
initial-value: transparent;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@property --cui-button-active-background {
|
|
147
|
+
syntax: "<color>";
|
|
148
|
+
inherits: true;
|
|
149
|
+
initial-value: transparent;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@property --cui-button-active-foreground {
|
|
153
|
+
syntax: "<color>";
|
|
154
|
+
inherits: true;
|
|
155
|
+
initial-value: transparent;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@property --cui-button-active-hover-background {
|
|
159
|
+
syntax: "<color>";
|
|
160
|
+
inherits: true;
|
|
161
|
+
initial-value: transparent;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@property --cui-button-hover-shadow {
|
|
165
|
+
syntax: "*";
|
|
166
|
+
inherits: true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@property --cui-button-press-shadow {
|
|
170
|
+
syntax: "*";
|
|
171
|
+
inherits: true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@property --cui-button-active-shadow {
|
|
175
|
+
syntax: "*";
|
|
176
|
+
inherits: true;
|
|
177
|
+
}
|
|
@@ -56,10 +56,12 @@
|
|
|
56
56
|
--cui-button-border-color: var(--control-rim);
|
|
57
57
|
--cui-button-hover-border-color: var(--cui-button-border-color);
|
|
58
58
|
--cui-button-open-border-color: var(--cui-button-hover-border-color);
|
|
59
|
+
--cui-button-backdrop-filter: none;
|
|
59
60
|
border: var(--cui-button-border-width) var(--cui-button-border-style) var(--cui-button-border-color);
|
|
60
61
|
border-radius: var(--cui-button-radius);
|
|
61
62
|
background-color: var(--cui-button-background);
|
|
62
63
|
background-image: var(--cui-button-background-image);
|
|
64
|
+
backdrop-filter: var(--cui-button-backdrop-filter);
|
|
63
65
|
box-shadow: var(--cui-button-shadow);
|
|
64
66
|
color: var(--cui-button-foreground);
|
|
65
67
|
cursor: pointer;
|
|
@@ -193,6 +195,16 @@
|
|
|
193
195
|
--cui-button-hover-background: transparent;
|
|
194
196
|
}
|
|
195
197
|
|
|
198
|
+
:where([data-control-family="button"][data-variant="scrim"]) {
|
|
199
|
+
--cui-button-background: oklch(0 0 0 / 0.6);
|
|
200
|
+
--cui-button-foreground: oklch(1 0 0);
|
|
201
|
+
--cui-button-hover-background: oklch(0 0 0 / 0.8);
|
|
202
|
+
--cui-button-hover-foreground: var(--cui-button-foreground);
|
|
203
|
+
--cui-button-active-background: var(--cui-button-foreground);
|
|
204
|
+
--cui-button-active-foreground: oklch(0 0 0);
|
|
205
|
+
--cui-button-backdrop-filter: blur(8px);
|
|
206
|
+
}
|
|
207
|
+
|
|
196
208
|
:where([data-control-family="button"]:is([data-variant="surface"], [data-variant="ghost"], [data-variant="quiet"])) {
|
|
197
209
|
--cui-button-open-background: oklch(from var(--foreground) l c h / 0.07);
|
|
198
210
|
}
|
|
@@ -210,7 +222,7 @@
|
|
|
210
222
|
}
|
|
211
223
|
}
|
|
212
224
|
|
|
213
|
-
:where([data-control-family="button"][data-tone="primary"]:not([data-variant="solid"])) {
|
|
225
|
+
:where([data-control-family="button"][data-tone="primary"]:not([data-variant="solid"], [data-variant="scrim"])) {
|
|
214
226
|
--cui-button-foreground: var(--primary-text);
|
|
215
227
|
--cui-button-hover-foreground: var(--primary-text);
|
|
216
228
|
--cui-button-active-foreground: var(--primary-text);
|
|
@@ -226,7 +238,7 @@
|
|
|
226
238
|
--cui-button-active-hover-background: var(--cui-button-hover-background);
|
|
227
239
|
}
|
|
228
240
|
|
|
229
|
-
:where([data-control-family="button"][data-tone="danger"]:not([data-variant="solid"])) {
|
|
241
|
+
:where([data-control-family="button"][data-tone="danger"]:not([data-variant="solid"], [data-variant="scrim"])) {
|
|
230
242
|
--cui-button-foreground: var(--destructive-text);
|
|
231
243
|
--cui-button-hover-foreground: var(--destructive-text);
|
|
232
244
|
--cui-button-active-foreground: var(--destructive-text);
|
|
@@ -316,67 +328,17 @@
|
|
|
316
328
|
inherits: true;
|
|
317
329
|
}
|
|
318
330
|
|
|
319
|
-
@property --cui-button-hover-background-image {
|
|
320
|
-
syntax: "*";
|
|
321
|
-
inherits: true;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
331
|
@property --cui-button-foreground {
|
|
325
332
|
syntax: "<color>";
|
|
326
333
|
inherits: true;
|
|
327
334
|
initial-value: transparent;
|
|
328
335
|
}
|
|
329
336
|
|
|
330
|
-
@property --cui-button-hover-background {
|
|
331
|
-
syntax: "<color>";
|
|
332
|
-
inherits: true;
|
|
333
|
-
initial-value: transparent;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
@property --cui-button-hover-foreground {
|
|
337
|
-
syntax: "<color>";
|
|
338
|
-
inherits: true;
|
|
339
|
-
initial-value: transparent;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
@property --cui-button-active-background {
|
|
343
|
-
syntax: "<color>";
|
|
344
|
-
inherits: true;
|
|
345
|
-
initial-value: transparent;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
@property --cui-button-active-foreground {
|
|
349
|
-
syntax: "<color>";
|
|
350
|
-
inherits: true;
|
|
351
|
-
initial-value: transparent;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
@property --cui-button-active-hover-background {
|
|
355
|
-
syntax: "<color>";
|
|
356
|
-
inherits: true;
|
|
357
|
-
initial-value: transparent;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
337
|
@property --cui-button-shadow {
|
|
361
338
|
syntax: "*";
|
|
362
339
|
inherits: true;
|
|
363
340
|
}
|
|
364
341
|
|
|
365
|
-
@property --cui-button-hover-shadow {
|
|
366
|
-
syntax: "*";
|
|
367
|
-
inherits: true;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
@property --cui-button-press-shadow {
|
|
371
|
-
syntax: "*";
|
|
372
|
-
inherits: true;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
@property --cui-button-active-shadow {
|
|
376
|
-
syntax: "*";
|
|
377
|
-
inherits: true;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
342
|
@property --cui-button-border-width {
|
|
381
343
|
syntax: "<length>";
|
|
382
344
|
inherits: true;
|
|
@@ -393,3 +355,8 @@
|
|
|
393
355
|
inherits: true;
|
|
394
356
|
initial-value: transparent;
|
|
395
357
|
}
|
|
358
|
+
|
|
359
|
+
@property --cui-button-backdrop-filter {
|
|
360
|
+
syntax: "*";
|
|
361
|
+
inherits: true;
|
|
362
|
+
}
|
|
@@ -26,6 +26,14 @@
|
|
|
26
26
|
--focus-ring-offset: calc(var(--focus-ring-width) * -1);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
:where([data-control-family="infinite-canvas"][data-slot="root"][data-pan-ready]:not([data-panning])) {
|
|
30
|
+
cursor: grab;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
:where([data-control-family="infinite-canvas"][data-slot="root"][data-panning]) {
|
|
34
|
+
cursor: grabbing;
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
:where([data-control-family="infinite-canvas"][data-slot="root"][data-easing]) {
|
|
30
38
|
transition-property: background-position, background-size;
|
|
31
39
|
transition-duration: var(--duration-base);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
:where([data-control-family="reveal"][data-slot="scope"]) {
|
|
3
|
+
--cui-reveal-hidden-opacity: 0;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
:where([data-control-family="reveal"][data-slot="content"]) {
|
|
7
|
+
transition-property: opacity;
|
|
8
|
+
transition-duration: var(--duration-fast);
|
|
9
|
+
transition-timing-function: var(--ease-standard);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@media (hover: hover) and (pointer: fine) {
|
|
13
|
+
:where(
|
|
14
|
+
[data-control-family="reveal"][data-slot="scope"]:not(:hover, :focus-within, :has([data-popup-open]))
|
|
15
|
+
[data-control-family="reveal"][data-slot="content"]
|
|
16
|
+
) {
|
|
17
|
+
opacity: var(--cui-reveal-hidden-opacity);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@property --cui-reveal-hidden-opacity {
|
|
23
|
+
syntax: "<number>";
|
|
24
|
+
inherits: true;
|
|
25
|
+
initial-value: 1;
|
|
26
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { RenderProp } from "../control-props.js";
|
|
2
|
+
import { RevealKnobStyle } from "../knob-contracts/reveal-knobs.js";
|
|
3
|
+
import { CSSProperties, ComponentProps } from "react";
|
|
4
|
+
//#region src/ui/reveal.d.ts
|
|
5
|
+
export type RevealScopeProps = Omit<ComponentProps<"div">, "style"> & {
|
|
6
|
+
render?: RenderProp<ComponentProps<"div">>;
|
|
7
|
+
style?: CSSProperties & RevealKnobStyle;
|
|
8
|
+
};
|
|
9
|
+
export type RevealProps = Omit<ComponentProps<"span">, "style"> & {
|
|
10
|
+
render?: RenderProp<ComponentProps<"span">>;
|
|
11
|
+
style?: CSSProperties & RevealKnobStyle;
|
|
12
|
+
};
|
|
13
|
+
export declare function RevealScope({ render, ...props }: RevealScopeProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
14
|
+
export declare function Reveal({ render, ...props }: RevealProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
15
|
+
//#endregion
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
+
//#region src/ui/reveal.tsx
|
|
4
|
+
function RevealScope({ render, ...props }) {
|
|
5
|
+
return useRender({
|
|
6
|
+
defaultTagName: "div",
|
|
7
|
+
render,
|
|
8
|
+
props: {
|
|
9
|
+
...props,
|
|
10
|
+
"data-control-ui": "reveal",
|
|
11
|
+
"data-control-family": "reveal",
|
|
12
|
+
"data-slot": "scope"
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function Reveal({ render, ...props }) {
|
|
17
|
+
return useRender({
|
|
18
|
+
defaultTagName: "span",
|
|
19
|
+
render,
|
|
20
|
+
props: {
|
|
21
|
+
...props,
|
|
22
|
+
"data-control-ui": "reveal",
|
|
23
|
+
"data-control-family": "reveal",
|
|
24
|
+
"data-slot": "content"
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { Reveal, RevealScope };
|
package/lint/biome-preset.json
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"plugins": [
|
|
2
|
+
"plugins": [
|
|
3
|
+
"./node_modules/@ctrl-ui/react/lint/no-component-style-classes.grit",
|
|
4
|
+
"./node_modules/@ctrl-ui/react/lint/no-component-style-props.grit",
|
|
5
|
+
{
|
|
6
|
+
"path": "./node_modules/@ctrl-ui/react/lint/no-component-part-css.grit",
|
|
7
|
+
"includes": ["**/*.css", "!**/components/control-ui/**"]
|
|
8
|
+
}
|
|
9
|
+
]
|
|
3
10
|
}
|
package/lint/class-policy.mjs
CHANGED
|
@@ -36,7 +36,47 @@ const textLayoutSuffixes = [
|
|
|
36
36
|
"clip",
|
|
37
37
|
];
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
const paintProperties = [
|
|
40
|
+
"padding",
|
|
41
|
+
"background",
|
|
42
|
+
"color",
|
|
43
|
+
"font",
|
|
44
|
+
"line-height",
|
|
45
|
+
"letter-spacing",
|
|
46
|
+
"border",
|
|
47
|
+
"outline",
|
|
48
|
+
"box-shadow",
|
|
49
|
+
"text-shadow",
|
|
50
|
+
"text-decoration",
|
|
51
|
+
"text-underline",
|
|
52
|
+
"text-transform",
|
|
53
|
+
"opacity",
|
|
54
|
+
"filter",
|
|
55
|
+
"backdrop-filter",
|
|
56
|
+
"fill",
|
|
57
|
+
"stroke",
|
|
58
|
+
"caret-color",
|
|
59
|
+
"accent-color",
|
|
60
|
+
"mask",
|
|
61
|
+
"-webkit-mask",
|
|
62
|
+
"-webkit-font-smoothing",
|
|
63
|
+
"-moz-osx-font-smoothing",
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const camelCase = (property) =>
|
|
67
|
+
property.replace(/^-(\w)/, (_, first) => first.toUpperCase()).replace(/-(\w)/g, (_, letter) => letter.toUpperCase());
|
|
68
|
+
|
|
69
|
+
export const paintProperty = String.raw`(?:${paintProperties.join("|")})(?:-[\w-]*)?`;
|
|
70
|
+
|
|
71
|
+
export const componentStyleKey = String.raw`["']?(?:(?:${paintProperties.map(camelCase).join("|")})(?:[A-Z]\w*)?|${paintProperty})["']?`;
|
|
72
|
+
|
|
73
|
+
const partAttribute = String.raw`data-(?:control|slot)\b`;
|
|
74
|
+
|
|
75
|
+
export const componentPartClass = String.raw`\[&[^${classDelimiter}]*?${partAttribute}|(?:^|[${classDelimiter}:])\*{1,2}:data-\[(?:control|slot)\b`;
|
|
76
|
+
|
|
77
|
+
export const componentPartSelector = String.raw`(?s).*\[${partAttribute}[^\s>+~]*\s*`;
|
|
78
|
+
|
|
79
|
+
export const componentStyleClass = String.raw`[^${classDelimiter}]!${classEnd}|(?:^|[${classDelimiter}])![\w\[-]|(?:^|[^\w-])(?:p[xytrblse]?-|bg-[\w\[]|text-${classSuffixOtherThan(textLayoutSuffixes)}|font-|leading-|tracking-|border|divide-|rounded|shadow|inset-shadow|drop-shadow|ring\b|ring-|inset-ring|outline|opacity-|backdrop-|fill-|stroke-|caret-|accent-|scheme-|mask-|decoration-|underline|no-underline|overline|line-through|italic|not-italic|uppercase|lowercase|capitalize|normal-case|antialiased|subpixel-antialiased|ordinal|slashed-zero|(?:normal|lining|oldstyle|proportional|tabular)-nums|(?:diagonal|stacked)-fractions|blur(?:-|${classEnd})|brightness-|contrast-[\d\[]|grayscale|hue-rotate-|invert|saturate-|sepia|(?:hover|focus|focus-visible|focus-within|active|disabled|enabled|open|checked|indeterminate|visited|target|invalid|valid|user-invalid|user-valid|required|optional|read-only|autofill|placeholder-shown|in-range|out-of-range|inert|starting|dark|placeholder|selection|marker|file|first-letter|first-line|backdrop|details-content|aria-[^:\s]*|data-[^:\s]*|group-[^:\s]*|peer-[^:\s]*|has-[^:\s]*|\*|\[&[^\s]*?\]):)|(?:^|[^\w-])\[${paintProperty}:`;
|
|
40
80
|
|
|
41
81
|
export const rawTypeClass = String.raw`(?:^|[^\w-])text-(?:micro|caption|label|body|body-lg|heading-[1-4]|display|xs|sm|base|lg|xl|[2-9]xl)\b|(?:^|[^\w-])text-\[\d[^\]]*\]`;
|
|
42
82
|
|
|
@@ -45,3 +85,12 @@ export const componentStyleMessage =
|
|
|
45
85
|
|
|
46
86
|
export const rawTypeMessage =
|
|
47
87
|
"Text size in className: render text through <Text>/<Heading> on the type scale, or size the control with its size prop or --cui-*-font-size knob.";
|
|
88
|
+
|
|
89
|
+
export const componentPartMessage =
|
|
90
|
+
"className styles a Control UI component's parts from outside through a data-slot or data-control selector. The component owns its parts: use its props, compose its parts, or set a --cui-* knob on it.";
|
|
91
|
+
|
|
92
|
+
export const componentStylePropMessage =
|
|
93
|
+
"Control UI component painted through style: color, type, padding, borders, radius, shadows and opacity belong to the component. Set a --cui-* knob in style or use its props; keep other style keys for layout.";
|
|
94
|
+
|
|
95
|
+
export const componentPartCssMessage =
|
|
96
|
+
"Stylesheet paints a Control UI part: color, type, padding, borders, radius, shadows and opacity belong to the component recipe. Set the part's --cui-* knobs here instead, or use the component's props.";
|
package/lint/eslint-plugin.mjs
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
componentPartClass,
|
|
3
|
+
componentPartMessage,
|
|
4
|
+
componentStyleClass,
|
|
5
|
+
componentStyleKey,
|
|
6
|
+
componentStyleMessage,
|
|
7
|
+
componentStylePropMessage,
|
|
8
|
+
controlUiImportPattern,
|
|
9
|
+
rawTypeClass,
|
|
10
|
+
rawTypeMessage,
|
|
11
|
+
} from "./class-policy.mjs";
|
|
2
12
|
|
|
3
13
|
const componentStyle = new RegExp(componentStyleClass);
|
|
14
|
+
const componentPart = new RegExp(componentPartClass);
|
|
15
|
+
const componentStyleProperty = new RegExp(`^(?:${componentStyleKey})$`);
|
|
4
16
|
const rawType = new RegExp(rawTypeClass);
|
|
5
17
|
|
|
6
18
|
function stringsIn(node) {
|
|
@@ -20,19 +32,30 @@ function referencedNames(node) {
|
|
|
20
32
|
return Object.entries(node).flatMap(([key, child]) => (key === "parent" ? [] : referencedNames(child)));
|
|
21
33
|
}
|
|
22
34
|
|
|
35
|
+
function propertyKeysIn(node) {
|
|
36
|
+
if (Array.isArray(node)) return node.flatMap(propertyKeysIn);
|
|
37
|
+
if (!node || typeof node !== "object") return [];
|
|
38
|
+
const key = node.type === "Property" && !node.computed ? [node.key.name ?? node.key.value] : [];
|
|
39
|
+
return [...key, ...Object.entries(node).flatMap(([name, child]) => (name === "parent" ? [] : propertyKeysIn(child)))];
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
const classLiteralTypes = new Set(["Literal", "TemplateLiteral", "ObjectExpression"]);
|
|
43
|
+
const styleLiteralTypes = new Set(["ObjectExpression"]);
|
|
24
44
|
|
|
25
|
-
function
|
|
45
|
+
function literalOf(init, types) {
|
|
26
46
|
const value = init?.type === "TSAsExpression" || init?.type === "TSSatisfiesExpression" ? init.expression : init;
|
|
27
|
-
return
|
|
47
|
+
return types.has(value?.type) ? value : undefined;
|
|
28
48
|
}
|
|
29
49
|
|
|
30
|
-
function
|
|
31
|
-
|
|
50
|
+
function insideRenderProp(node) {
|
|
51
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
52
|
+
if (parent.type === "JSXAttribute" && parent.name.name === "render") return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
32
55
|
}
|
|
33
56
|
|
|
34
57
|
function classNameMatches(attribute, pattern) {
|
|
35
|
-
return
|
|
58
|
+
return attribute.name.name === "className" && Boolean(attribute.value) && stringsIn(attribute.value).some((text) => pattern.test(text));
|
|
36
59
|
}
|
|
37
60
|
|
|
38
61
|
function elementRootName(name) {
|
|
@@ -59,50 +82,75 @@ function controlUiLocalNames(program, controlUiImport) {
|
|
|
59
82
|
return names;
|
|
60
83
|
}
|
|
61
84
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if ([...stringsIn(attribute.value), ...constants].some((text) => componentStyle.test(text))) {
|
|
99
|
-
context.report({ node: attribute, messageId: "componentStyle" });
|
|
85
|
+
function componentAttributeRule({ attribute, description, message, literalTypes, valuesIn, pattern, part }) {
|
|
86
|
+
return {
|
|
87
|
+
meta: {
|
|
88
|
+
type: "problem",
|
|
89
|
+
docs: { description },
|
|
90
|
+
messages: { component: message, ...(part && { part: part.message }) },
|
|
91
|
+
schema: [
|
|
92
|
+
{
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: { importSources: { type: "array", items: { type: "string" } } },
|
|
95
|
+
additionalProperties: false,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
create(context) {
|
|
100
|
+
const controlUiImport = new RegExp(`^${controlUiImportPattern(context.options[0]?.importSources)}`);
|
|
101
|
+
let controlUiNames = new Set();
|
|
102
|
+
const isControlUi = (element) => element !== undefined && controlUiNames.has(elementRootName(element.name));
|
|
103
|
+
const constants = new Map();
|
|
104
|
+
const checks = [];
|
|
105
|
+
return {
|
|
106
|
+
Program(program) {
|
|
107
|
+
controlUiNames = controlUiLocalNames(program, controlUiImport);
|
|
108
|
+
},
|
|
109
|
+
VariableDeclarator(declarator) {
|
|
110
|
+
const literal = literalOf(declarator.init, literalTypes);
|
|
111
|
+
if (declarator.parent.kind !== "const" || declarator.id.type !== "Identifier" || !literal) return;
|
|
112
|
+
constants.set(declarator.id.name, [...(constants.get(declarator.id.name) ?? []), ...valuesIn(literal)]);
|
|
113
|
+
},
|
|
114
|
+
JSXAttribute(node) {
|
|
115
|
+
if (node.name.name !== attribute || !node.value) return;
|
|
116
|
+
const element = node.parent;
|
|
117
|
+
if (isControlUi(element) || isControlUi(renderPropOwner(element.parent))) {
|
|
118
|
+
checks.push({ node, messageId: "component", pattern });
|
|
119
|
+
} else if (part && !insideRenderProp(node)) {
|
|
120
|
+
checks.push({ node, messageId: "part", pattern: part.pattern });
|
|
100
121
|
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
122
|
+
},
|
|
123
|
+
"Program:exit"() {
|
|
124
|
+
for (const check of checks) {
|
|
125
|
+
const referenced = referencedNames(check.node.value).flatMap((name) => constants.get(name) ?? []);
|
|
126
|
+
if ([...valuesIn(check.node.value), ...referenced].some((value) => check.pattern.test(value))) {
|
|
127
|
+
context.report({ node: check.node, messageId: check.messageId });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const noComponentStyleClasses = componentAttributeRule({
|
|
137
|
+
attribute: "className",
|
|
138
|
+
description: "Keep className on Control UI components to layout, and off their parts; the component owns its look.",
|
|
139
|
+
message: componentStyleMessage,
|
|
140
|
+
literalTypes: classLiteralTypes,
|
|
141
|
+
valuesIn: stringsIn,
|
|
142
|
+
pattern: componentStyle,
|
|
143
|
+
part: { message: componentPartMessage, pattern: componentPart },
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const noComponentStyleProps = componentAttributeRule({
|
|
147
|
+
attribute: "style",
|
|
148
|
+
description: "Keep style on Control UI components to --cui-* knobs and layout; the component owns its look.",
|
|
149
|
+
message: componentStylePropMessage,
|
|
150
|
+
literalTypes: styleLiteralTypes,
|
|
151
|
+
valuesIn: propertyKeysIn,
|
|
152
|
+
pattern: componentStyleProperty,
|
|
153
|
+
});
|
|
106
154
|
|
|
107
155
|
const noRawTypeClasses = {
|
|
108
156
|
meta: {
|
|
@@ -124,6 +172,7 @@ const plugin = {
|
|
|
124
172
|
meta: { name: "control-ui" },
|
|
125
173
|
rules: {
|
|
126
174
|
"no-component-style-classes": noComponentStyleClasses,
|
|
175
|
+
"no-component-style-props": noComponentStyleProps,
|
|
127
176
|
"no-raw-type-classes": noRawTypeClasses,
|
|
128
177
|
},
|
|
129
178
|
};
|
package/lint/grit-rules.mjs
CHANGED
|
@@ -1,30 +1,66 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
componentPartClass,
|
|
3
|
+
componentPartCssMessage,
|
|
4
|
+
componentPartMessage,
|
|
5
|
+
componentPartSelector,
|
|
6
|
+
componentStyleClass,
|
|
7
|
+
componentStyleKey,
|
|
8
|
+
componentStyleMessage,
|
|
9
|
+
componentStylePropMessage,
|
|
10
|
+
controlUiImportPattern,
|
|
11
|
+
paintProperty,
|
|
12
|
+
rawTypeClass,
|
|
13
|
+
rawTypeMessage,
|
|
14
|
+
} from "./class-policy.mjs";
|
|
2
15
|
|
|
3
16
|
function gritRegex(pattern) {
|
|
4
17
|
return `r"(?s).*(?:${pattern.replaceAll('"', '\\"')}).*"`;
|
|
5
18
|
}
|
|
6
19
|
|
|
20
|
+
function gritExactRegex(pattern) {
|
|
21
|
+
return `r"${pattern.replaceAll('"', '\\"')}"`;
|
|
22
|
+
}
|
|
23
|
+
|
|
7
24
|
function gritString(text) {
|
|
8
25
|
return JSON.stringify(text);
|
|
9
26
|
}
|
|
10
27
|
|
|
11
|
-
const stringTexts =
|
|
28
|
+
const stringTexts = (name) =>
|
|
29
|
+
`or { JsxString(), JsStringLiteralExpression(), JsTemplateChunkElement(), JsLiteralMemberName() as $${name}Member where { $${name}Member <: r"(?s)[\\"'].*" } }`;
|
|
12
30
|
const classLiterals = "or { JsStringLiteralExpression(), JsTemplateExpression(), JsObjectExpression() }";
|
|
13
31
|
const renderedTag = "or { JsxTagExpression(tag=$tag), JsParenthesizedExpression(expression=JsxTagExpression(tag=$tag)) }";
|
|
32
|
+
const componentName = `r"([A-Z]\\w*)(?:\\.[\\w.]+)?"`;
|
|
14
33
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
$value <: or { ${classLiterals}, TsAsExpression(expression=${classLiterals}), TsSatisfiesExpression(expression=${classLiterals}) },
|
|
24
|
-
$value <: contains ${stringTexts} as $constText where { $constText <: ${styled} }
|
|
34
|
+
function constantMatches(reference, literals, offends) {
|
|
35
|
+
return `contains JsReferenceIdentifier() as $${reference} where {
|
|
36
|
+
$${reference} <: r"(.+)"($${reference}Name),
|
|
37
|
+
$module <: contains JsVariableDeclaration(declarators=$${reference}Declarators) as $${reference}Declaration where {
|
|
38
|
+
$${reference}Declaration <: r"(?s)const\\s.*",
|
|
39
|
+
$${reference}Declarators <: some JsVariableDeclarator(id=$${reference}Name, initializer=JsInitializerClause(expression=$${reference}Value)),
|
|
40
|
+
$${reference}Value <: or { ${literals}, TsAsExpression(expression=${literals}), TsSatisfiesExpression(expression=${literals}) },
|
|
41
|
+
$${reference}Value <: ${offends}
|
|
25
42
|
}
|
|
26
|
-
}
|
|
43
|
+
}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function classNameMatches(attribute, pattern) {
|
|
47
|
+
const offending = (prefix) => `contains ${stringTexts(prefix)} as $${prefix}Text where { $${prefix}Text <: ${gritRegex(pattern)} }`;
|
|
48
|
+
const name = attribute.slice(1);
|
|
49
|
+
return `${attribute} <: or {
|
|
50
|
+
${offending(name)},
|
|
51
|
+
${constantMatches(`${name}Constant`, classLiterals, offending(`${name}Constant`))}
|
|
27
52
|
}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function styleMatches(attribute) {
|
|
56
|
+
const offending = (prefix) =>
|
|
57
|
+
`contains or { JsPropertyObjectMember(name=$${prefix}Key), JsShorthandPropertyObjectMember(name=$${prefix}Key) } where { $${prefix}Key <: ${gritExactRegex(componentStyleKey)} }`;
|
|
58
|
+
const name = attribute.slice(1);
|
|
59
|
+
return `${attribute} <: or {
|
|
60
|
+
${offending(name)},
|
|
61
|
+
${constantMatches(`${name}Constant`, "JsObjectExpression()", offending(`${name}Constant`))}
|
|
62
|
+
}`;
|
|
63
|
+
}
|
|
28
64
|
|
|
29
65
|
function importedFromControlUi(component, importSources, suffix = "") {
|
|
30
66
|
return `$module <: contains JsImport() as $import${suffix} where {
|
|
@@ -33,47 +69,100 @@ function importedFromControlUi(component, importSources, suffix = "") {
|
|
|
33
69
|
}`;
|
|
34
70
|
}
|
|
35
71
|
|
|
72
|
+
function componentAttributePattern({ attribute, offends, message, importSources }) {
|
|
73
|
+
return `or {
|
|
74
|
+
or {
|
|
75
|
+
JsxSelfClosingElement(name=$name, attributes=$attributes),
|
|
76
|
+
JsxOpeningElement(name=$name, attributes=$attributes)
|
|
77
|
+
} where {
|
|
78
|
+
$attributes <: some JsxAttribute(name=\`${attribute}\`) as $attr
|
|
79
|
+
},
|
|
80
|
+
JsxAttribute(name=\`render\`, initializer=JsxAttributeInitializerClause(value=JsxExpressionAttributeValue(expression=$rendered))) as $render where {
|
|
81
|
+
$rendered <: or {
|
|
82
|
+
JsxTagExpression(tag=$tag),
|
|
83
|
+
JsArrowFunctionExpression(body=or {
|
|
84
|
+
${renderedTag},
|
|
85
|
+
JsFunctionBody(statements=some JsReturnStatement(argument=${renderedTag}))
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
$tag <: or {
|
|
89
|
+
JsxSelfClosingElement(name=$renderedName, attributes=$renderedAttributes),
|
|
90
|
+
JsxElement(opening_element=JsxOpeningElement(name=$renderedName, attributes=$renderedAttributes))
|
|
91
|
+
},
|
|
92
|
+
$renderedAttributes <: some JsxAttribute(name=\`${attribute}\`) as $attr,
|
|
93
|
+
$render <: within or { JsxSelfClosingElement(name=$name), JsxOpeningElement(name=$name) },
|
|
94
|
+
$render <: within JsModule() as $module,
|
|
95
|
+
$renderedName <: r"([^.]+).*"($renderedComponent),
|
|
96
|
+
not ${importedFromControlUi("$renderedComponent", importSources, "Rendered")}
|
|
97
|
+
}
|
|
98
|
+
} where {
|
|
99
|
+
$name <: ${componentName}($component),
|
|
100
|
+
$attr <: within JsModule() as $module,
|
|
101
|
+
${offends("$attr")},
|
|
102
|
+
${importedFromControlUi("$component", importSources)},
|
|
103
|
+
register_diagnostic(span=$attr, message=${gritString(message)}, severity="error")
|
|
104
|
+
}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function partClassPattern(importSources) {
|
|
108
|
+
return `or {
|
|
109
|
+
JsxSelfClosingElement(name=$plainName, attributes=$plainAttributes),
|
|
110
|
+
JsxOpeningElement(name=$plainName, attributes=$plainAttributes)
|
|
111
|
+
} where {
|
|
112
|
+
$plainAttributes <: some JsxAttribute(name=\`className\`) as $plainAttr,
|
|
113
|
+
not $plainAttr <: within JsxAttribute(name=\`render\`),
|
|
114
|
+
$plainAttr <: within JsModule() as $module,
|
|
115
|
+
${classNameMatches("$plainAttr", componentPartClass)},
|
|
116
|
+
not and {
|
|
117
|
+
$plainName <: ${componentName}($plainComponent),
|
|
118
|
+
${importedFromControlUi("$plainComponent", importSources, "Plain")}
|
|
119
|
+
},
|
|
120
|
+
register_diagnostic(span=$plainAttr, message=${gritString(componentPartMessage)}, severity="error")
|
|
121
|
+
}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const partSubject = gritExactRegex(componentPartSelector);
|
|
125
|
+
const enclosingRule = (selectors) => `or { CssQualifiedRule(prelude=${selectors}), CssNestedQualifiedRule(prelude=${selectors}) }`;
|
|
126
|
+
|
|
36
127
|
export function gritRules(importSources = []) {
|
|
37
128
|
return {
|
|
38
129
|
"no-component-style-classes.grit": `language js(typescript, jsx)
|
|
39
130
|
|
|
40
131
|
or {
|
|
132
|
+
${componentAttributePattern({
|
|
133
|
+
attribute: "className",
|
|
134
|
+
offends: (attribute) => classNameMatches(attribute, componentStyleClass),
|
|
135
|
+
message: componentStyleMessage,
|
|
136
|
+
importSources,
|
|
137
|
+
})},
|
|
138
|
+
${partClassPattern(importSources)}
|
|
139
|
+
}
|
|
140
|
+
`,
|
|
141
|
+
"no-component-style-props.grit": `language js(typescript, jsx)
|
|
142
|
+
|
|
143
|
+
${componentAttributePattern({ attribute: "style", offends: styleMatches, message: componentStylePropMessage, importSources })}
|
|
144
|
+
`,
|
|
145
|
+
"no-component-part-css.grit": `language css
|
|
146
|
+
|
|
147
|
+
or {
|
|
148
|
+
CssGenericProperty(name=$property) as $paint where { $property <: ${gritExactRegex(`(?i)${paintProperty}`)} },
|
|
149
|
+
CssAtRule() as $paint where { $paint <: r"(?s)@apply\\s.*", $paint <: ${gritRegex(componentStyleClass)} }
|
|
150
|
+
} where {
|
|
151
|
+
$paint <: within ${enclosingRule("$selectors")} as $rule,
|
|
41
152
|
or {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
153
|
+
$selectors <: some ${partSubject},
|
|
154
|
+
and {
|
|
155
|
+
$selectors <: some r"\\s*&[^\\s>+~]*\\s*",
|
|
156
|
+
$rule <: within ${enclosingRule("$outer")} where { $outer <: some ${partSubject} }
|
|
157
|
+
}
|
|
46
158
|
},
|
|
47
|
-
|
|
48
|
-
$rendered <: or {
|
|
49
|
-
JsxTagExpression(tag=$tag),
|
|
50
|
-
JsArrowFunctionExpression(body=or {
|
|
51
|
-
${renderedTag},
|
|
52
|
-
JsFunctionBody(statements=some JsReturnStatement(argument=${renderedTag}))
|
|
53
|
-
})
|
|
54
|
-
},
|
|
55
|
-
$tag <: or {
|
|
56
|
-
JsxSelfClosingElement(name=$renderedName, attributes=$renderedAttributes),
|
|
57
|
-
JsxElement(opening_element=JsxOpeningElement(name=$renderedName, attributes=$renderedAttributes))
|
|
58
|
-
},
|
|
59
|
-
$renderedAttributes <: some JsxAttribute(name=\`className\`) as $attr,
|
|
60
|
-
$render <: within or { JsxSelfClosingElement(name=$name), JsxOpeningElement(name=$name) },
|
|
61
|
-
$render <: within JsModule() as $module,
|
|
62
|
-
$renderedName <: r"([^.]+).*"($renderedComponent),
|
|
63
|
-
not ${importedFromControlUi("$renderedComponent", importSources, "Rendered")}
|
|
64
|
-
}
|
|
65
|
-
} where {
|
|
66
|
-
$name <: r"([A-Z]\\w*)(?:\\.[\\w.]+)?"($component),
|
|
67
|
-
$attr <: within JsModule() as $module,
|
|
68
|
-
${styledClassName},
|
|
69
|
-
${importedFromControlUi("$component", importSources)},
|
|
70
|
-
register_diagnostic(span=$attr, message=${gritString(componentStyleMessage)}, severity="error")
|
|
159
|
+
register_diagnostic(span=$paint, message=${gritString(componentPartCssMessage)}, severity="error")
|
|
71
160
|
}
|
|
72
161
|
`,
|
|
73
162
|
"no-raw-type-classes.grit": `language js(typescript, jsx)
|
|
74
163
|
|
|
75
164
|
JsxAttribute(name=\`className\`) as $attr where {
|
|
76
|
-
$attr <: contains ${stringTexts} as $text where { $text <: ${gritRegex(rawTypeClass)} },
|
|
165
|
+
$attr <: contains ${stringTexts("attr")} as $text where { $text <: ${gritRegex(rawTypeClass)} },
|
|
77
166
|
register_diagnostic(span=$attr, message=${gritString(rawTypeMessage)}, severity="error")
|
|
78
167
|
}
|
|
79
168
|
`,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
language css
|
|
2
|
+
|
|
3
|
+
or {
|
|
4
|
+
CssGenericProperty(name=$property) as $paint where { $property <: r"(?i)(?:padding|background|color|font|line-height|letter-spacing|border|outline|box-shadow|text-shadow|text-decoration|text-underline|text-transform|opacity|filter|backdrop-filter|fill|stroke|caret-color|accent-color|mask|-webkit-mask|-webkit-font-smoothing|-moz-osx-font-smoothing)(?:-[\w-]*)?" },
|
|
5
|
+
CssAtRule() as $paint where { $paint <: r"(?s)@apply\s.*", $paint <: r"(?s).*(?:[^\s\"'\x60]!(?:[\s\"'\x60]|$)|(?:^|[\s\"'\x60])![\w\[-]|(?:^|[^\w-])(?:p[xytrblse]?-|bg-[\w\[]|text-(?:(?:[\s\"'\x60]|$)|[^lcrjsewnbp\s\"'\x60]|l(?:(?:[\s\"'\x60]|$)|[^e\s\"'\x60]|e(?:(?:[\s\"'\x60]|$)|[^f\s\"'\x60]|f(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:[^\s\"'\x60]))))|c(?:(?:[\s\"'\x60]|$)|[^el\s\"'\x60]|e(?:(?:[\s\"'\x60]|$)|[^n\s\"'\x60]|n(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:(?:[\s\"'\x60]|$)|[^e\s\"'\x60]|e(?:(?:[\s\"'\x60]|$)|[^r\s\"'\x60]|r(?:[^\s\"'\x60])))))|l(?:(?:[\s\"'\x60]|$)|[^i\s\"'\x60]|i(?:(?:[\s\"'\x60]|$)|[^p\s\"'\x60]|p(?:[^\s\"'\x60]))))|r(?:(?:[\s\"'\x60]|$)|[^i\s\"'\x60]|i(?:(?:[\s\"'\x60]|$)|[^g\s\"'\x60]|g(?:(?:[\s\"'\x60]|$)|[^h\s\"'\x60]|h(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:[^\s\"'\x60])))))|j(?:(?:[\s\"'\x60]|$)|[^u\s\"'\x60]|u(?:(?:[\s\"'\x60]|$)|[^s\s\"'\x60]|s(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:(?:[\s\"'\x60]|$)|[^i\s\"'\x60]|i(?:(?:[\s\"'\x60]|$)|[^f\s\"'\x60]|f(?:(?:[\s\"'\x60]|$)|[^y\s\"'\x60]|y(?:[^\s\"'\x60])))))))|s(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:(?:[\s\"'\x60]|$)|[^a\s\"'\x60]|a(?:(?:[\s\"'\x60]|$)|[^r\s\"'\x60]|r(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:[^\s\"'\x60])))))|e(?:(?:[\s\"'\x60]|$)|[^nl\s\"'\x60]|n(?:(?:[\s\"'\x60]|$)|[^d\s\"'\x60]|d(?:[^\s\"'\x60]))|l(?:(?:[\s\"'\x60]|$)|[^l\s\"'\x60]|l(?:(?:[\s\"'\x60]|$)|[^i\s\"'\x60]|i(?:(?:[\s\"'\x60]|$)|[^p\s\"'\x60]|p(?:(?:[\s\"'\x60]|$)|[^s\s\"'\x60]|s(?:(?:[\s\"'\x60]|$)|[^i\s\"'\x60]|i(?:(?:[\s\"'\x60]|$)|[^s\s\"'\x60]|s(?:[^\s\"'\x60]))))))))|w(?:(?:[\s\"'\x60]|$)|[^r\s\"'\x60]|r(?:(?:[\s\"'\x60]|$)|[^a\s\"'\x60]|a(?:(?:[\s\"'\x60]|$)|[^p\s\"'\x60]|p(?:[^\s\"'\x60]))))|n(?:(?:[\s\"'\x60]|$)|[^o\s\"'\x60]|o(?:(?:[\s\"'\x60]|$)|[^w\s\"'\x60]|w(?:(?:[\s\"'\x60]|$)|[^r\s\"'\x60]|r(?:(?:[\s\"'\x60]|$)|[^a\s\"'\x60]|a(?:(?:[\s\"'\x60]|$)|[^p\s\"'\x60]|p(?:[^\s\"'\x60]))))))|b(?:(?:[\s\"'\x60]|$)|[^a\s\"'\x60]|a(?:(?:[\s\"'\x60]|$)|[^l\s\"'\x60]|l(?:(?:[\s\"'\x60]|$)|[^a\s\"'\x60]|a(?:(?:[\s\"'\x60]|$)|[^n\s\"'\x60]|n(?:(?:[\s\"'\x60]|$)|[^c\s\"'\x60]|c(?:(?:[\s\"'\x60]|$)|[^e\s\"'\x60]|e(?:[^\s\"'\x60])))))))|p(?:(?:[\s\"'\x60]|$)|[^r\s\"'\x60]|r(?:(?:[\s\"'\x60]|$)|[^e\s\"'\x60]|e(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:(?:[\s\"'\x60]|$)|[^y\s\"'\x60]|y(?:[^\s\"'\x60])))))))|font-|leading-|tracking-|border|divide-|rounded|shadow|inset-shadow|drop-shadow|ring\b|ring-|inset-ring|outline|opacity-|backdrop-|fill-|stroke-|caret-|accent-|scheme-|mask-|decoration-|underline|no-underline|overline|line-through|italic|not-italic|uppercase|lowercase|capitalize|normal-case|antialiased|subpixel-antialiased|ordinal|slashed-zero|(?:normal|lining|oldstyle|proportional|tabular)-nums|(?:diagonal|stacked)-fractions|blur(?:-|(?:[\s\"'\x60]|$))|brightness-|contrast-[\d\[]|grayscale|hue-rotate-|invert|saturate-|sepia|(?:hover|focus|focus-visible|focus-within|active|disabled|enabled|open|checked|indeterminate|visited|target|invalid|valid|user-invalid|user-valid|required|optional|read-only|autofill|placeholder-shown|in-range|out-of-range|inert|starting|dark|placeholder|selection|marker|file|first-letter|first-line|backdrop|details-content|aria-[^:\s]*|data-[^:\s]*|group-[^:\s]*|peer-[^:\s]*|has-[^:\s]*|\*|\[&[^\s]*?\]):)|(?:^|[^\w-])\[(?:padding|background|color|font|line-height|letter-spacing|border|outline|box-shadow|text-shadow|text-decoration|text-underline|text-transform|opacity|filter|backdrop-filter|fill|stroke|caret-color|accent-color|mask|-webkit-mask|-webkit-font-smoothing|-moz-osx-font-smoothing)(?:-[\w-]*)?:).*" }
|
|
6
|
+
} where {
|
|
7
|
+
$paint <: within or { CssQualifiedRule(prelude=$selectors), CssNestedQualifiedRule(prelude=$selectors) } as $rule,
|
|
8
|
+
or {
|
|
9
|
+
$selectors <: some r"(?s).*\[data-(?:control|slot)\b[^\s>+~]*\s*",
|
|
10
|
+
and {
|
|
11
|
+
$selectors <: some r"\s*&[^\s>+~]*\s*",
|
|
12
|
+
$rule <: within or { CssQualifiedRule(prelude=$outer), CssNestedQualifiedRule(prelude=$outer) } where { $outer <: some r"(?s).*\[data-(?:control|slot)\b[^\s>+~]*\s*" }
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
register_diagnostic(span=$paint, message="Stylesheet paints a Control UI part: color, type, padding, borders, radius, shadows and opacity belong to the component recipe. Set the part's --cui-* knobs here instead, or use the component's props.", severity="error")
|
|
16
|
+
}
|
|
@@ -2,50 +2,80 @@ language js(typescript, jsx)
|
|
|
2
2
|
|
|
3
3
|
or {
|
|
4
4
|
or {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
JsxAttribute(name=`render`, initializer=JsxAttributeInitializerClause(value=JsxExpressionAttributeValue(expression=$rendered))) as $render where {
|
|
11
|
-
$rendered <: or {
|
|
12
|
-
JsxTagExpression(tag=$tag),
|
|
13
|
-
JsArrowFunctionExpression(body=or {
|
|
14
|
-
or { JsxTagExpression(tag=$tag), JsParenthesizedExpression(expression=JsxTagExpression(tag=$tag)) },
|
|
15
|
-
JsFunctionBody(statements=some JsReturnStatement(argument=or { JsxTagExpression(tag=$tag), JsParenthesizedExpression(expression=JsxTagExpression(tag=$tag)) }))
|
|
16
|
-
})
|
|
5
|
+
or {
|
|
6
|
+
JsxSelfClosingElement(name=$name, attributes=$attributes),
|
|
7
|
+
JsxOpeningElement(name=$name, attributes=$attributes)
|
|
8
|
+
} where {
|
|
9
|
+
$attributes <: some JsxAttribute(name=`className`) as $attr
|
|
17
10
|
},
|
|
18
|
-
$
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
11
|
+
JsxAttribute(name=`render`, initializer=JsxAttributeInitializerClause(value=JsxExpressionAttributeValue(expression=$rendered))) as $render where {
|
|
12
|
+
$rendered <: or {
|
|
13
|
+
JsxTagExpression(tag=$tag),
|
|
14
|
+
JsArrowFunctionExpression(body=or {
|
|
15
|
+
or { JsxTagExpression(tag=$tag), JsParenthesizedExpression(expression=JsxTagExpression(tag=$tag)) },
|
|
16
|
+
JsFunctionBody(statements=some JsReturnStatement(argument=or { JsxTagExpression(tag=$tag), JsParenthesizedExpression(expression=JsxTagExpression(tag=$tag)) }))
|
|
17
|
+
})
|
|
18
|
+
},
|
|
19
|
+
$tag <: or {
|
|
20
|
+
JsxSelfClosingElement(name=$renderedName, attributes=$renderedAttributes),
|
|
21
|
+
JsxElement(opening_element=JsxOpeningElement(name=$renderedName, attributes=$renderedAttributes))
|
|
22
|
+
},
|
|
23
|
+
$renderedAttributes <: some JsxAttribute(name=`className`) as $attr,
|
|
24
|
+
$render <: within or { JsxSelfClosingElement(name=$name), JsxOpeningElement(name=$name) },
|
|
25
|
+
$render <: within JsModule() as $module,
|
|
26
|
+
$renderedName <: r"([^.]+).*"($renderedComponent),
|
|
27
|
+
not $module <: contains JsImport() as $importRendered where {
|
|
27
28
|
$importRendered <: r"(?s).*(?:from\s+['\"](?:(?:@ctrl-ui/react|[^'\"]*components/control-ui)/)).*",
|
|
28
29
|
$importRendered <: contains JsIdentifierBinding() as $bindingRendered where { $bindingRendered <: $renderedComponent }
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
-
} where {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
contains or { JsxString(), JsStringLiteralExpression(), JsTemplateChunkElement(), JsLiteralMemberName() as $
|
|
36
|
-
contains JsReferenceIdentifier() as $
|
|
37
|
-
$
|
|
38
|
-
$module <: contains JsVariableDeclaration(declarators=$
|
|
39
|
-
$
|
|
40
|
-
$
|
|
41
|
-
$
|
|
42
|
-
$
|
|
31
|
+
}
|
|
32
|
+
} where {
|
|
33
|
+
$name <: r"([A-Z]\w*)(?:\.[\w.]+)?"($component),
|
|
34
|
+
$attr <: within JsModule() as $module,
|
|
35
|
+
$attr <: or {
|
|
36
|
+
contains or { JsxString(), JsStringLiteralExpression(), JsTemplateChunkElement(), JsLiteralMemberName() as $attrMember where { $attrMember <: r"(?s)[\"'].*" } } as $attrText where { $attrText <: r"(?s).*(?:[^\s\"'\x60]!(?:[\s\"'\x60]|$)|(?:^|[\s\"'\x60])![\w\[-]|(?:^|[^\w-])(?:p[xytrblse]?-|bg-[\w\[]|text-(?:(?:[\s\"'\x60]|$)|[^lcrjsewnbp\s\"'\x60]|l(?:(?:[\s\"'\x60]|$)|[^e\s\"'\x60]|e(?:(?:[\s\"'\x60]|$)|[^f\s\"'\x60]|f(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:[^\s\"'\x60]))))|c(?:(?:[\s\"'\x60]|$)|[^el\s\"'\x60]|e(?:(?:[\s\"'\x60]|$)|[^n\s\"'\x60]|n(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:(?:[\s\"'\x60]|$)|[^e\s\"'\x60]|e(?:(?:[\s\"'\x60]|$)|[^r\s\"'\x60]|r(?:[^\s\"'\x60])))))|l(?:(?:[\s\"'\x60]|$)|[^i\s\"'\x60]|i(?:(?:[\s\"'\x60]|$)|[^p\s\"'\x60]|p(?:[^\s\"'\x60]))))|r(?:(?:[\s\"'\x60]|$)|[^i\s\"'\x60]|i(?:(?:[\s\"'\x60]|$)|[^g\s\"'\x60]|g(?:(?:[\s\"'\x60]|$)|[^h\s\"'\x60]|h(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:[^\s\"'\x60])))))|j(?:(?:[\s\"'\x60]|$)|[^u\s\"'\x60]|u(?:(?:[\s\"'\x60]|$)|[^s\s\"'\x60]|s(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:(?:[\s\"'\x60]|$)|[^i\s\"'\x60]|i(?:(?:[\s\"'\x60]|$)|[^f\s\"'\x60]|f(?:(?:[\s\"'\x60]|$)|[^y\s\"'\x60]|y(?:[^\s\"'\x60])))))))|s(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:(?:[\s\"'\x60]|$)|[^a\s\"'\x60]|a(?:(?:[\s\"'\x60]|$)|[^r\s\"'\x60]|r(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:[^\s\"'\x60])))))|e(?:(?:[\s\"'\x60]|$)|[^nl\s\"'\x60]|n(?:(?:[\s\"'\x60]|$)|[^d\s\"'\x60]|d(?:[^\s\"'\x60]))|l(?:(?:[\s\"'\x60]|$)|[^l\s\"'\x60]|l(?:(?:[\s\"'\x60]|$)|[^i\s\"'\x60]|i(?:(?:[\s\"'\x60]|$)|[^p\s\"'\x60]|p(?:(?:[\s\"'\x60]|$)|[^s\s\"'\x60]|s(?:(?:[\s\"'\x60]|$)|[^i\s\"'\x60]|i(?:(?:[\s\"'\x60]|$)|[^s\s\"'\x60]|s(?:[^\s\"'\x60]))))))))|w(?:(?:[\s\"'\x60]|$)|[^r\s\"'\x60]|r(?:(?:[\s\"'\x60]|$)|[^a\s\"'\x60]|a(?:(?:[\s\"'\x60]|$)|[^p\s\"'\x60]|p(?:[^\s\"'\x60]))))|n(?:(?:[\s\"'\x60]|$)|[^o\s\"'\x60]|o(?:(?:[\s\"'\x60]|$)|[^w\s\"'\x60]|w(?:(?:[\s\"'\x60]|$)|[^r\s\"'\x60]|r(?:(?:[\s\"'\x60]|$)|[^a\s\"'\x60]|a(?:(?:[\s\"'\x60]|$)|[^p\s\"'\x60]|p(?:[^\s\"'\x60]))))))|b(?:(?:[\s\"'\x60]|$)|[^a\s\"'\x60]|a(?:(?:[\s\"'\x60]|$)|[^l\s\"'\x60]|l(?:(?:[\s\"'\x60]|$)|[^a\s\"'\x60]|a(?:(?:[\s\"'\x60]|$)|[^n\s\"'\x60]|n(?:(?:[\s\"'\x60]|$)|[^c\s\"'\x60]|c(?:(?:[\s\"'\x60]|$)|[^e\s\"'\x60]|e(?:[^\s\"'\x60])))))))|p(?:(?:[\s\"'\x60]|$)|[^r\s\"'\x60]|r(?:(?:[\s\"'\x60]|$)|[^e\s\"'\x60]|e(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:(?:[\s\"'\x60]|$)|[^y\s\"'\x60]|y(?:[^\s\"'\x60])))))))|font-|leading-|tracking-|border|divide-|rounded|shadow|inset-shadow|drop-shadow|ring\b|ring-|inset-ring|outline|opacity-|backdrop-|fill-|stroke-|caret-|accent-|scheme-|mask-|decoration-|underline|no-underline|overline|line-through|italic|not-italic|uppercase|lowercase|capitalize|normal-case|antialiased|subpixel-antialiased|ordinal|slashed-zero|(?:normal|lining|oldstyle|proportional|tabular)-nums|(?:diagonal|stacked)-fractions|blur(?:-|(?:[\s\"'\x60]|$))|brightness-|contrast-[\d\[]|grayscale|hue-rotate-|invert|saturate-|sepia|(?:hover|focus|focus-visible|focus-within|active|disabled|enabled|open|checked|indeterminate|visited|target|invalid|valid|user-invalid|user-valid|required|optional|read-only|autofill|placeholder-shown|in-range|out-of-range|inert|starting|dark|placeholder|selection|marker|file|first-letter|first-line|backdrop|details-content|aria-[^:\s]*|data-[^:\s]*|group-[^:\s]*|peer-[^:\s]*|has-[^:\s]*|\*|\[&[^\s]*?\]):)|(?:^|[^\w-])\[(?:padding|background|color|font|line-height|letter-spacing|border|outline|box-shadow|text-shadow|text-decoration|text-underline|text-transform|opacity|filter|backdrop-filter|fill|stroke|caret-color|accent-color|mask|-webkit-mask|-webkit-font-smoothing|-moz-osx-font-smoothing)(?:-[\w-]*)?:).*" },
|
|
37
|
+
contains JsReferenceIdentifier() as $attrConstant where {
|
|
38
|
+
$attrConstant <: r"(.+)"($attrConstantName),
|
|
39
|
+
$module <: contains JsVariableDeclaration(declarators=$attrConstantDeclarators) as $attrConstantDeclaration where {
|
|
40
|
+
$attrConstantDeclaration <: r"(?s)const\s.*",
|
|
41
|
+
$attrConstantDeclarators <: some JsVariableDeclarator(id=$attrConstantName, initializer=JsInitializerClause(expression=$attrConstantValue)),
|
|
42
|
+
$attrConstantValue <: or { or { JsStringLiteralExpression(), JsTemplateExpression(), JsObjectExpression() }, TsAsExpression(expression=or { JsStringLiteralExpression(), JsTemplateExpression(), JsObjectExpression() }), TsSatisfiesExpression(expression=or { JsStringLiteralExpression(), JsTemplateExpression(), JsObjectExpression() }) },
|
|
43
|
+
$attrConstantValue <: contains or { JsxString(), JsStringLiteralExpression(), JsTemplateChunkElement(), JsLiteralMemberName() as $attrConstantMember where { $attrConstantMember <: r"(?s)[\"'].*" } } as $attrConstantText where { $attrConstantText <: r"(?s).*(?:[^\s\"'\x60]!(?:[\s\"'\x60]|$)|(?:^|[\s\"'\x60])![\w\[-]|(?:^|[^\w-])(?:p[xytrblse]?-|bg-[\w\[]|text-(?:(?:[\s\"'\x60]|$)|[^lcrjsewnbp\s\"'\x60]|l(?:(?:[\s\"'\x60]|$)|[^e\s\"'\x60]|e(?:(?:[\s\"'\x60]|$)|[^f\s\"'\x60]|f(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:[^\s\"'\x60]))))|c(?:(?:[\s\"'\x60]|$)|[^el\s\"'\x60]|e(?:(?:[\s\"'\x60]|$)|[^n\s\"'\x60]|n(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:(?:[\s\"'\x60]|$)|[^e\s\"'\x60]|e(?:(?:[\s\"'\x60]|$)|[^r\s\"'\x60]|r(?:[^\s\"'\x60])))))|l(?:(?:[\s\"'\x60]|$)|[^i\s\"'\x60]|i(?:(?:[\s\"'\x60]|$)|[^p\s\"'\x60]|p(?:[^\s\"'\x60]))))|r(?:(?:[\s\"'\x60]|$)|[^i\s\"'\x60]|i(?:(?:[\s\"'\x60]|$)|[^g\s\"'\x60]|g(?:(?:[\s\"'\x60]|$)|[^h\s\"'\x60]|h(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:[^\s\"'\x60])))))|j(?:(?:[\s\"'\x60]|$)|[^u\s\"'\x60]|u(?:(?:[\s\"'\x60]|$)|[^s\s\"'\x60]|s(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:(?:[\s\"'\x60]|$)|[^i\s\"'\x60]|i(?:(?:[\s\"'\x60]|$)|[^f\s\"'\x60]|f(?:(?:[\s\"'\x60]|$)|[^y\s\"'\x60]|y(?:[^\s\"'\x60])))))))|s(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:(?:[\s\"'\x60]|$)|[^a\s\"'\x60]|a(?:(?:[\s\"'\x60]|$)|[^r\s\"'\x60]|r(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:[^\s\"'\x60])))))|e(?:(?:[\s\"'\x60]|$)|[^nl\s\"'\x60]|n(?:(?:[\s\"'\x60]|$)|[^d\s\"'\x60]|d(?:[^\s\"'\x60]))|l(?:(?:[\s\"'\x60]|$)|[^l\s\"'\x60]|l(?:(?:[\s\"'\x60]|$)|[^i\s\"'\x60]|i(?:(?:[\s\"'\x60]|$)|[^p\s\"'\x60]|p(?:(?:[\s\"'\x60]|$)|[^s\s\"'\x60]|s(?:(?:[\s\"'\x60]|$)|[^i\s\"'\x60]|i(?:(?:[\s\"'\x60]|$)|[^s\s\"'\x60]|s(?:[^\s\"'\x60]))))))))|w(?:(?:[\s\"'\x60]|$)|[^r\s\"'\x60]|r(?:(?:[\s\"'\x60]|$)|[^a\s\"'\x60]|a(?:(?:[\s\"'\x60]|$)|[^p\s\"'\x60]|p(?:[^\s\"'\x60]))))|n(?:(?:[\s\"'\x60]|$)|[^o\s\"'\x60]|o(?:(?:[\s\"'\x60]|$)|[^w\s\"'\x60]|w(?:(?:[\s\"'\x60]|$)|[^r\s\"'\x60]|r(?:(?:[\s\"'\x60]|$)|[^a\s\"'\x60]|a(?:(?:[\s\"'\x60]|$)|[^p\s\"'\x60]|p(?:[^\s\"'\x60]))))))|b(?:(?:[\s\"'\x60]|$)|[^a\s\"'\x60]|a(?:(?:[\s\"'\x60]|$)|[^l\s\"'\x60]|l(?:(?:[\s\"'\x60]|$)|[^a\s\"'\x60]|a(?:(?:[\s\"'\x60]|$)|[^n\s\"'\x60]|n(?:(?:[\s\"'\x60]|$)|[^c\s\"'\x60]|c(?:(?:[\s\"'\x60]|$)|[^e\s\"'\x60]|e(?:[^\s\"'\x60])))))))|p(?:(?:[\s\"'\x60]|$)|[^r\s\"'\x60]|r(?:(?:[\s\"'\x60]|$)|[^e\s\"'\x60]|e(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:(?:[\s\"'\x60]|$)|[^t\s\"'\x60]|t(?:(?:[\s\"'\x60]|$)|[^y\s\"'\x60]|y(?:[^\s\"'\x60])))))))|font-|leading-|tracking-|border|divide-|rounded|shadow|inset-shadow|drop-shadow|ring\b|ring-|inset-ring|outline|opacity-|backdrop-|fill-|stroke-|caret-|accent-|scheme-|mask-|decoration-|underline|no-underline|overline|line-through|italic|not-italic|uppercase|lowercase|capitalize|normal-case|antialiased|subpixel-antialiased|ordinal|slashed-zero|(?:normal|lining|oldstyle|proportional|tabular)-nums|(?:diagonal|stacked)-fractions|blur(?:-|(?:[\s\"'\x60]|$))|brightness-|contrast-[\d\[]|grayscale|hue-rotate-|invert|saturate-|sepia|(?:hover|focus|focus-visible|focus-within|active|disabled|enabled|open|checked|indeterminate|visited|target|invalid|valid|user-invalid|user-valid|required|optional|read-only|autofill|placeholder-shown|in-range|out-of-range|inert|starting|dark|placeholder|selection|marker|file|first-letter|first-line|backdrop|details-content|aria-[^:\s]*|data-[^:\s]*|group-[^:\s]*|peer-[^:\s]*|has-[^:\s]*|\*|\[&[^\s]*?\]):)|(?:^|[^\w-])\[(?:padding|background|color|font|line-height|letter-spacing|border|outline|box-shadow|text-shadow|text-decoration|text-underline|text-transform|opacity|filter|backdrop-filter|fill|stroke|caret-color|accent-color|mask|-webkit-mask|-webkit-font-smoothing|-moz-osx-font-smoothing)(?:-[\w-]*)?:).*" }
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
},
|
|
46
|
-
|
|
47
|
+
$module <: contains JsImport() as $import where {
|
|
47
48
|
$import <: r"(?s).*(?:from\s+['\"](?:(?:@ctrl-ui/react|[^'\"]*components/control-ui)/)).*",
|
|
48
49
|
$import <: contains JsIdentifierBinding() as $binding where { $binding <: $component }
|
|
49
50
|
},
|
|
50
|
-
|
|
51
|
+
register_diagnostic(span=$attr, message="Control UI component styled through className: color, type, padding, borders, radius, shadows, state styles and !important belong to the component. Use its props or a --cui-* knob; keep className for layout (flex, gap, width, margin, placement).", severity="error")
|
|
52
|
+
},
|
|
53
|
+
or {
|
|
54
|
+
JsxSelfClosingElement(name=$plainName, attributes=$plainAttributes),
|
|
55
|
+
JsxOpeningElement(name=$plainName, attributes=$plainAttributes)
|
|
56
|
+
} where {
|
|
57
|
+
$plainAttributes <: some JsxAttribute(name=`className`) as $plainAttr,
|
|
58
|
+
not $plainAttr <: within JsxAttribute(name=`render`),
|
|
59
|
+
$plainAttr <: within JsModule() as $module,
|
|
60
|
+
$plainAttr <: or {
|
|
61
|
+
contains or { JsxString(), JsStringLiteralExpression(), JsTemplateChunkElement(), JsLiteralMemberName() as $plainAttrMember where { $plainAttrMember <: r"(?s)[\"'].*" } } as $plainAttrText where { $plainAttrText <: r"(?s).*(?:\[&[^\s\"'\x60]*?data-(?:control|slot)\b|(?:^|[\s\"'\x60:])\*{1,2}:data-\[(?:control|slot)\b).*" },
|
|
62
|
+
contains JsReferenceIdentifier() as $plainAttrConstant where {
|
|
63
|
+
$plainAttrConstant <: r"(.+)"($plainAttrConstantName),
|
|
64
|
+
$module <: contains JsVariableDeclaration(declarators=$plainAttrConstantDeclarators) as $plainAttrConstantDeclaration where {
|
|
65
|
+
$plainAttrConstantDeclaration <: r"(?s)const\s.*",
|
|
66
|
+
$plainAttrConstantDeclarators <: some JsVariableDeclarator(id=$plainAttrConstantName, initializer=JsInitializerClause(expression=$plainAttrConstantValue)),
|
|
67
|
+
$plainAttrConstantValue <: or { or { JsStringLiteralExpression(), JsTemplateExpression(), JsObjectExpression() }, TsAsExpression(expression=or { JsStringLiteralExpression(), JsTemplateExpression(), JsObjectExpression() }), TsSatisfiesExpression(expression=or { JsStringLiteralExpression(), JsTemplateExpression(), JsObjectExpression() }) },
|
|
68
|
+
$plainAttrConstantValue <: contains or { JsxString(), JsStringLiteralExpression(), JsTemplateChunkElement(), JsLiteralMemberName() as $plainAttrConstantMember where { $plainAttrConstantMember <: r"(?s)[\"'].*" } } as $plainAttrConstantText where { $plainAttrConstantText <: r"(?s).*(?:\[&[^\s\"'\x60]*?data-(?:control|slot)\b|(?:^|[\s\"'\x60:])\*{1,2}:data-\[(?:control|slot)\b).*" }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
not and {
|
|
73
|
+
$plainName <: r"([A-Z]\w*)(?:\.[\w.]+)?"($plainComponent),
|
|
74
|
+
$module <: contains JsImport() as $importPlain where {
|
|
75
|
+
$importPlain <: r"(?s).*(?:from\s+['\"](?:(?:@ctrl-ui/react|[^'\"]*components/control-ui)/)).*",
|
|
76
|
+
$importPlain <: contains JsIdentifierBinding() as $bindingPlain where { $bindingPlain <: $plainComponent }
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
register_diagnostic(span=$plainAttr, message="className styles a Control UI component's parts from outside through a data-slot or data-control selector. The component owns its parts: use its props, compose its parts, or set a --cui-* knob on it.", severity="error")
|
|
80
|
+
}
|
|
51
81
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
language js(typescript, jsx)
|
|
2
|
+
|
|
3
|
+
or {
|
|
4
|
+
or {
|
|
5
|
+
JsxSelfClosingElement(name=$name, attributes=$attributes),
|
|
6
|
+
JsxOpeningElement(name=$name, attributes=$attributes)
|
|
7
|
+
} where {
|
|
8
|
+
$attributes <: some JsxAttribute(name=`style`) as $attr
|
|
9
|
+
},
|
|
10
|
+
JsxAttribute(name=`render`, initializer=JsxAttributeInitializerClause(value=JsxExpressionAttributeValue(expression=$rendered))) as $render where {
|
|
11
|
+
$rendered <: or {
|
|
12
|
+
JsxTagExpression(tag=$tag),
|
|
13
|
+
JsArrowFunctionExpression(body=or {
|
|
14
|
+
or { JsxTagExpression(tag=$tag), JsParenthesizedExpression(expression=JsxTagExpression(tag=$tag)) },
|
|
15
|
+
JsFunctionBody(statements=some JsReturnStatement(argument=or { JsxTagExpression(tag=$tag), JsParenthesizedExpression(expression=JsxTagExpression(tag=$tag)) }))
|
|
16
|
+
})
|
|
17
|
+
},
|
|
18
|
+
$tag <: or {
|
|
19
|
+
JsxSelfClosingElement(name=$renderedName, attributes=$renderedAttributes),
|
|
20
|
+
JsxElement(opening_element=JsxOpeningElement(name=$renderedName, attributes=$renderedAttributes))
|
|
21
|
+
},
|
|
22
|
+
$renderedAttributes <: some JsxAttribute(name=`style`) as $attr,
|
|
23
|
+
$render <: within or { JsxSelfClosingElement(name=$name), JsxOpeningElement(name=$name) },
|
|
24
|
+
$render <: within JsModule() as $module,
|
|
25
|
+
$renderedName <: r"([^.]+).*"($renderedComponent),
|
|
26
|
+
not $module <: contains JsImport() as $importRendered where {
|
|
27
|
+
$importRendered <: r"(?s).*(?:from\s+['\"](?:(?:@ctrl-ui/react|[^'\"]*components/control-ui)/)).*",
|
|
28
|
+
$importRendered <: contains JsIdentifierBinding() as $bindingRendered where { $bindingRendered <: $renderedComponent }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} where {
|
|
32
|
+
$name <: r"([A-Z]\w*)(?:\.[\w.]+)?"($component),
|
|
33
|
+
$attr <: within JsModule() as $module,
|
|
34
|
+
$attr <: or {
|
|
35
|
+
contains or { JsPropertyObjectMember(name=$attrKey), JsShorthandPropertyObjectMember(name=$attrKey) } where { $attrKey <: r"[\"']?(?:(?:padding|background|color|font|lineHeight|letterSpacing|border|outline|boxShadow|textShadow|textDecoration|textUnderline|textTransform|opacity|filter|backdropFilter|fill|stroke|caretColor|accentColor|mask|WebkitMask|WebkitFontSmoothing|MozOsxFontSmoothing)(?:[A-Z]\w*)?|(?:padding|background|color|font|line-height|letter-spacing|border|outline|box-shadow|text-shadow|text-decoration|text-underline|text-transform|opacity|filter|backdrop-filter|fill|stroke|caret-color|accent-color|mask|-webkit-mask|-webkit-font-smoothing|-moz-osx-font-smoothing)(?:-[\w-]*)?)[\"']?" },
|
|
36
|
+
contains JsReferenceIdentifier() as $attrConstant where {
|
|
37
|
+
$attrConstant <: r"(.+)"($attrConstantName),
|
|
38
|
+
$module <: contains JsVariableDeclaration(declarators=$attrConstantDeclarators) as $attrConstantDeclaration where {
|
|
39
|
+
$attrConstantDeclaration <: r"(?s)const\s.*",
|
|
40
|
+
$attrConstantDeclarators <: some JsVariableDeclarator(id=$attrConstantName, initializer=JsInitializerClause(expression=$attrConstantValue)),
|
|
41
|
+
$attrConstantValue <: or { JsObjectExpression(), TsAsExpression(expression=JsObjectExpression()), TsSatisfiesExpression(expression=JsObjectExpression()) },
|
|
42
|
+
$attrConstantValue <: contains or { JsPropertyObjectMember(name=$attrConstantKey), JsShorthandPropertyObjectMember(name=$attrConstantKey) } where { $attrConstantKey <: r"[\"']?(?:(?:padding|background|color|font|lineHeight|letterSpacing|border|outline|boxShadow|textShadow|textDecoration|textUnderline|textTransform|opacity|filter|backdropFilter|fill|stroke|caretColor|accentColor|mask|WebkitMask|WebkitFontSmoothing|MozOsxFontSmoothing)(?:[A-Z]\w*)?|(?:padding|background|color|font|line-height|letter-spacing|border|outline|box-shadow|text-shadow|text-decoration|text-underline|text-transform|opacity|filter|backdrop-filter|fill|stroke|caret-color|accent-color|mask|-webkit-mask|-webkit-font-smoothing|-moz-osx-font-smoothing)(?:-[\w-]*)?)[\"']?" }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
$module <: contains JsImport() as $import where {
|
|
47
|
+
$import <: r"(?s).*(?:from\s+['\"](?:(?:@ctrl-ui/react|[^'\"]*components/control-ui)/)).*",
|
|
48
|
+
$import <: contains JsIdentifierBinding() as $binding where { $binding <: $component }
|
|
49
|
+
},
|
|
50
|
+
register_diagnostic(span=$attr, message="Control UI component painted through style: color, type, padding, borders, radius, shadows and opacity belong to the component. Set a --cui-* knob in style or use its props; keep other style keys for layout.", severity="error")
|
|
51
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
language js(typescript, jsx)
|
|
2
2
|
|
|
3
3
|
JsxAttribute(name=`className`) as $attr where {
|
|
4
|
-
$attr <: contains or { JsxString(), JsStringLiteralExpression(), JsTemplateChunkElement(), JsLiteralMemberName() as $
|
|
4
|
+
$attr <: contains or { JsxString(), JsStringLiteralExpression(), JsTemplateChunkElement(), JsLiteralMemberName() as $attrMember where { $attrMember <: r"(?s)[\"'].*" } } as $text where { $text <: r"(?s).*(?:(?:^|[^\w-])text-(?:micro|caption|label|body|body-lg|heading-[1-4]|display|xs|sm|base|lg|xl|[2-9]xl)\b|(?:^|[^\w-])text-\[\d[^\]]*\]).*" },
|
|
5
5
|
register_diagnostic(span=$attr, message="Text size in className: render text through <Text>/<Heading> on the type scale, or size the control with its size prop or --cui-*-font-size knob.", severity="error")
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ctrl-ui/react",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.15",
|
|
4
4
|
"description": "100+ React components for Tailwind CSS and shadcn/ui: pickers, trees, editors, canvases, app templates, and AI chat surfaces, with neutral defaults and consumer-owned themes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"@base-ui/react": "^1.9.0",
|
|
64
|
-
"@biomejs/biome": ">=2.
|
|
64
|
+
"@biomejs/biome": ">=2.5.0",
|
|
65
65
|
"@tanstack/charts": "^1.1.0",
|
|
66
66
|
"@tanstack/react-virtual": "^3.14.13",
|
|
67
67
|
"@tiptap/core": "3.31.4",
|