@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 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
- Two rules keep call sites honest, shipped for Biome, ESLint, and oxlint from one policy (`lint/class-policy.mjs`):
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.4.11 or later; older versions fail with "Failed to compile the Grit plugin".
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 [{ plugins: { "control-ui": controlUi }, rules: { "control-ui/no-component-style-classes": "error" } }];
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
- { "jsPlugins": ["@ctrl-ui/react/eslint-plugin"], "rules": { "control-ui/no-component-style-classes": "error" } }
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 rule follows those imports too:
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
- rules: { "control-ui/no-component-style-classes": ["error", { importSources: ["@acme/ui/components/"] }] }
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 `./.control-ui/no-component-style-classes.grit` under `plugins` in place of the preset:
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(Text, {
433
- as: "p",
434
- size: "heading-2",
435
- weight: "normal",
436
- className: "size-full overflow-hidden whitespace-nowrap",
437
- style: { color: fillColor },
438
- children: layer.text
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];
@@ -5,7 +5,8 @@ const buttonVariants = [
5
5
  "ghost",
6
6
  "quiet",
7
7
  "dashed",
8
- "link"
8
+ "link",
9
+ "scrim"
9
10
  ];
10
11
  const controlTones = [
11
12
  "neutral",
@@ -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-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-hover-background-image", "--cui-button-foreground", "--cui-button-hover-background", "--cui-button-hover-foreground", "--cui-button-active-background", "--cui-button-active-foreground", "--cui-button-active-hover-background", "--cui-button-shadow", "--cui-button-hover-shadow", "--cui-button-press-shadow", "--cui-button-active-shadow", "--cui-button-border-width", "--cui-button-border-style", "--cui-button-border-color"];
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 };
@@ -0,0 +1,4 @@
1
+ //#region src/knob-contracts/reveal-knobs.d.ts
2
+ export declare const revealKnobs: readonly ["--cui-reveal-hidden-opacity"];
3
+ export type RevealKnobStyle = Partial<Record<(typeof revealKnobs)[number], string>>;
4
+ //#endregion
@@ -0,0 +1,4 @@
1
+ //#region src/knob-contracts/reveal-knobs.ts
2
+ const revealKnobs = ["--cui-reveal-hidden-opacity"];
3
+ //#endregion
4
+ export { revealKnobs };
@@ -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 };
@@ -1,3 +1,10 @@
1
1
  {
2
- "plugins": ["./node_modules/@ctrl-ui/react/lint/no-component-style-classes.grit"]
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
  }
@@ -36,7 +36,47 @@ const textLayoutSuffixes = [
36
36
  "clip",
37
37
  ];
38
38
 
39
- 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|(?: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]*?\]):)`;
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.";
@@ -1,6 +1,18 @@
1
- import { componentStyleClass, componentStyleMessage, controlUiImportPattern, rawTypeClass, rawTypeMessage } from "./class-policy.mjs";
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 classLiteral(init) {
45
+ function literalOf(init, types) {
26
46
  const value = init?.type === "TSAsExpression" || init?.type === "TSSatisfiesExpression" ? init.expression : init;
27
- return classLiteralTypes.has(value?.type) ? value : undefined;
47
+ return types.has(value?.type) ? value : undefined;
28
48
  }
29
49
 
30
- function isClassName(attribute) {
31
- return attribute.name.name === "className" && Boolean(attribute.value);
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 isClassName(attribute) && stringsIn(attribute.value).some((text) => pattern.test(text));
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
- const noComponentStyleClasses = {
63
- meta: {
64
- type: "problem",
65
- docs: { description: "Keep className on Control UI components to layout; the component owns its look." },
66
- messages: { componentStyle: componentStyleMessage },
67
- schema: [
68
- {
69
- type: "object",
70
- properties: { importSources: { type: "array", items: { type: "string" } } },
71
- additionalProperties: false,
72
- },
73
- ],
74
- },
75
- create(context) {
76
- const controlUiImport = new RegExp(`^${controlUiImportPattern(context.options[0]?.importSources)}`);
77
- let controlUiNames = new Set();
78
- const isControlUi = (element) => element !== undefined && controlUiNames.has(elementRootName(element.name));
79
- const constantClasses = new Map();
80
- const componentClassNames = [];
81
- return {
82
- Program(program) {
83
- controlUiNames = controlUiLocalNames(program, controlUiImport);
84
- },
85
- VariableDeclarator(declarator) {
86
- const literal = classLiteral(declarator.init);
87
- if (declarator.parent.kind !== "const" || declarator.id.type !== "Identifier" || !literal) return;
88
- constantClasses.set(declarator.id.name, [...(constantClasses.get(declarator.id.name) ?? []), ...stringsIn(literal)]);
89
- },
90
- JSXAttribute(attribute) {
91
- if (!isClassName(attribute)) return;
92
- const element = attribute.parent;
93
- if (isControlUi(element) || isControlUi(renderPropOwner(element.parent))) componentClassNames.push(attribute);
94
- },
95
- "Program:exit"() {
96
- for (const attribute of componentClassNames) {
97
- const constants = referencedNames(attribute.value).flatMap((name) => constantClasses.get(name) ?? []);
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
  };
@@ -1,30 +1,66 @@
1
- import { componentStyleClass, componentStyleMessage, controlUiImportPattern, rawTypeClass, rawTypeMessage } from "./class-policy.mjs";
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 = `or { JsxString(), JsStringLiteralExpression(), JsTemplateChunkElement(), JsLiteralMemberName() as $memberName where { $memberName <: r"(?s)[\\"'].*" } }`;
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
- const styled = gritRegex(componentStyleClass);
16
- const styledClassName = `$attr <: or {
17
- contains ${stringTexts} as $text where { $text <: ${styled} },
18
- contains JsReferenceIdentifier() as $reference where {
19
- $reference <: r"(.+)"($referenced),
20
- $module <: contains JsVariableDeclaration(declarators=$declarators) as $declaration where {
21
- $declaration <: r"(?s)const\\s.*",
22
- $declarators <: some JsVariableDeclarator(id=$referenced, initializer=JsInitializerClause(expression=$value)),
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
- JsxSelfClosingElement(name=$name, attributes=$attributes),
43
- JsxOpeningElement(name=$name, attributes=$attributes)
44
- } where {
45
- $attributes <: some JsxAttribute(name=\`className\`) as $attr
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
- JsxAttribute(name=\`render\`, initializer=JsxAttributeInitializerClause(value=JsxExpressionAttributeValue(expression=$rendered))) as $render where {
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
- JsxSelfClosingElement(name=$name, attributes=$attributes),
6
- JsxOpeningElement(name=$name, attributes=$attributes)
7
- } where {
8
- $attributes <: some JsxAttribute(name=`className`) 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
- })
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
- $tag <: or {
19
- JsxSelfClosingElement(name=$renderedName, attributes=$renderedAttributes),
20
- JsxElement(opening_element=JsxOpeningElement(name=$renderedName, attributes=$renderedAttributes))
21
- },
22
- $renderedAttributes <: some JsxAttribute(name=`className`) 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 {
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
- $name <: r"([A-Z]\w*)(?:\.[\w.]+)?"($component),
33
- $attr <: within JsModule() as $module,
34
- $attr <: or {
35
- contains or { JsxString(), JsStringLiteralExpression(), JsTemplateChunkElement(), JsLiteralMemberName() as $memberName where { $memberName <: r"(?s)[\"'].*" } } as $text where { $text <: 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|(?: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]*?\]):)).*" },
36
- contains JsReferenceIdentifier() as $reference where {
37
- $reference <: r"(.+)"($referenced),
38
- $module <: contains JsVariableDeclaration(declarators=$declarators) as $declaration where {
39
- $declaration <: r"(?s)const\s.*",
40
- $declarators <: some JsVariableDeclarator(id=$referenced, initializer=JsInitializerClause(expression=$value)),
41
- $value <: or { or { JsStringLiteralExpression(), JsTemplateExpression(), JsObjectExpression() }, TsAsExpression(expression=or { JsStringLiteralExpression(), JsTemplateExpression(), JsObjectExpression() }), TsSatisfiesExpression(expression=or { JsStringLiteralExpression(), JsTemplateExpression(), JsObjectExpression() }) },
42
- $value <: contains or { JsxString(), JsStringLiteralExpression(), JsTemplateChunkElement(), JsLiteralMemberName() as $memberName where { $memberName <: r"(?s)[\"'].*" } } as $constText where { $constText <: 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|(?: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]*?\]):)).*" }
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
- $module <: contains JsImport() as $import where {
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
- 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")
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 $memberName where { $memberName <: 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[^\]]*\]).*" },
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.13",
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.4.11",
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",