@ctrl-ui/react 0.9.4 → 0.9.5

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
@@ -26,6 +26,33 @@ import { Button } from "@ctrl-ui/react/ui/button";
26
26
 
27
27
  Import paths mirror the registry tree. To ship less CSS, import `styles/theme.css` and only the recipes and effects your components use.
28
28
 
29
+ ## Text and class names
30
+
31
+ Render text with `Text` and `Heading`. Size, tone, and weight are props; `className` carries layout only:
32
+
33
+ ```tsx
34
+ import { Text } from "@ctrl-ui/react/ui/typography";
35
+
36
+ <Text size="caption" tone="muted">Saved</Text>
37
+ ```
38
+
39
+ Merge class names with the package's `cn`. A bare `twMerge` reads `text-micro` as a text color and silently drops it next to `text-muted-foreground`. Add your own type rungs on top of `withControlUiMerge` instead of starting a second merger:
40
+
41
+ ```ts
42
+ import { withControlUiMerge } from "@ctrl-ui/react/lib/cn";
43
+ import { extendTailwindMerge } from "tailwind-merge";
44
+
45
+ const twMerge = extendTailwindMerge({ extend: { classGroups: { "font-size": [{ text: ["hero"] }] } } }, withControlUiMerge);
46
+ ```
47
+
48
+ Controls size their own text: pick the control's `size`, or set its `--cui-*-font-size` knob in your theme, rather than wrapping the label in a `text-*` span.
49
+
50
+ With Biome, the shipped GritQL plugin warns on type rungs in `className`:
51
+
52
+ ```json
53
+ { "plugins": ["./node_modules/@ctrl-ui/react/lint/no-raw-type-classes.grit"] }
54
+ ```
55
+
29
56
  ## Customize
30
57
 
31
58
  Your application owns its theme. Override the library's neutral defaults in your CSS:
@@ -182,10 +182,7 @@ function CodingAgentSidebar({ appName, brand, sidebarTop, navigation, projects,
182
182
  }, project.id))
183
183
  })]
184
184
  })] }),
185
- sidebarFooter ? /* @__PURE__ */ jsx(SidebarFooter, {
186
- className: "border-t border-sidebar-border",
187
- children: sidebarFooter
188
- }) : null,
185
+ sidebarFooter ? /* @__PURE__ */ jsx(SidebarFooter, { children: sidebarFooter }) : null,
189
186
  /* @__PURE__ */ jsx(SidebarRail, {})
190
187
  ]
191
188
  });
@@ -147,7 +147,7 @@ function FileExplorerSidebar({ locations, activeLocationId, sidebarTop, sidebarF
147
147
  collapsible: "offcanvas",
148
148
  children: [
149
149
  sidebarTop ? /* @__PURE__ */ jsx(SidebarHeader, {
150
- className: "min-h-11 justify-center border-b border-sidebar-border px-3",
150
+ className: "min-h-11 justify-center px-3",
151
151
  children: sidebarTop
152
152
  }) : null,
153
153
  /* @__PURE__ */ jsx(SidebarContent, { children: groups.map((group) => /* @__PURE__ */ jsxs(SidebarGroup, { children: [group.label ? /* @__PURE__ */ jsx(SidebarGroupLabel, { children: group.label }) : null, /* @__PURE__ */ jsx(SidebarMenu, { children: group.items.map((location) => /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsxs(SidebarMenuButton, {
@@ -159,24 +159,22 @@ function FileExplorerSidebar({ locations, activeLocationId, sidebarTop, sidebarF
159
159
  },
160
160
  children: [location.icon ?? /* @__PURE__ */ jsx(FolderIcon, {}), /* @__PURE__ */ jsx("span", { children: location.label })]
161
161
  }) }, location.id)) })] }, group.label || "locations")) }),
162
- sidebarFooter ? /* @__PURE__ */ jsx(SidebarFooter, {
163
- className: "border-t border-sidebar-border",
164
- children: sidebarFooter
165
- }) : null,
162
+ sidebarFooter ? /* @__PURE__ */ jsx(SidebarFooter, { children: sidebarFooter }) : null,
166
163
  /* @__PURE__ */ jsx(SidebarRail, {})
167
164
  ]
168
165
  });
169
166
  }
170
167
  function FileExplorerHeader({ location, headingLevel, breadcrumbs, query, searchPlaceholder, searchLabel, clearSearchLabel, backLabel, moreActionsLabel, canGoBack, headerActions, headerMenuItems, onBack, onBreadcrumbSelect, onQueryChange }) {
171
168
  return /* @__PURE__ */ jsxs("header", {
172
- className: "shrink-0 border-b border-border/70 bg-card/35",
169
+ className: "shrink-0 border-b border-border/70",
173
170
  children: [/* @__PURE__ */ jsxs("div", {
174
171
  className: "flex h-12 items-center gap-2 px-3",
175
172
  children: [
176
- /* @__PURE__ */ jsx(SidebarTrigger, { size: "xs" }),
173
+ /* @__PURE__ */ jsx(SidebarTrigger, { size: "sm" }),
177
174
  /* @__PURE__ */ jsx(Button, {
178
175
  variant: "ghost",
179
- size: "xs",
176
+ size: "sm",
177
+ iconOnly: true,
180
178
  className: "hidden @xl/explorer:inline-flex",
181
179
  disabled: !canGoBack,
182
180
  "aria-label": backLabel,
@@ -200,7 +198,7 @@ function FileExplorerHeader({ location, headingLevel, breadcrumbs, query, search
200
198
  }) : null,
201
199
  headerMenuItems ? /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
202
200
  variant: "ghost",
203
- size: "xs",
201
+ size: "sm",
204
202
  iconOnly: true,
205
203
  "aria-label": moreActionsLabel,
206
204
  className: headerActions ? "@2xl/explorer:hidden" : void 0,
@@ -214,7 +212,7 @@ function FileExplorerHeader({ location, headingLevel, breadcrumbs, query, search
214
212
  })] }) : null,
215
213
  searchPlaceholder ? /* @__PURE__ */ jsxs(InputGroup, {
216
214
  size: "sm",
217
- className: "flex-[0_1_9rem] bg-background/60 @xl/explorer:flex-[0_0_12rem]",
215
+ className: "flex-[0_1_9rem] @xl/explorer:flex-[0_0_12rem]",
218
216
  children: [
219
217
  /* @__PURE__ */ jsx(InputGroupAddon, {
220
218
  className: "ps-2",
@@ -408,7 +406,7 @@ function FileExplorerColumns({ columns, selectedEntry, selectedPath, headingLeve
408
406
  })
409
407
  }) }) : null })] })]
410
408
  }), /* @__PURE__ */ jsx("div", {
411
- className: "min-h-0 flex-1 bg-card",
409
+ className: "min-h-0 flex-1",
412
410
  children: activeColumn ? renderColumn(activeColumn, activeIndex, "narrow") : null
413
411
  })] }) : null
414
412
  }), /* @__PURE__ */ jsx("div", {
@@ -416,7 +414,7 @@ function FileExplorerColumns({ columns, selectedEntry, selectedPath, headingLeve
416
414
  className: "hidden h-full min-h-0 @xl/explorer:block",
417
415
  children: wideDisplayed ? /* @__PURE__ */ jsxs(ResizablePanelGroup, {
418
416
  orientation: "horizontal",
419
- className: "rounded-none border-0 bg-card shadow-none",
417
+ variant: "nested",
420
418
  children: [columns.map((column, columnIndex) => /* @__PURE__ */ jsx(FileExplorerPanel, {
421
419
  defaultSize: columnSize,
422
420
  minSize: "16%",
@@ -531,7 +529,7 @@ function FileExplorerColumnView({ column, optionRef, onSelect, onOpen, onOptionK
531
529
  }
532
530
  function FileExplorerPreview({ entry, headingLevel }) {
533
531
  if (!entry) return /* @__PURE__ */ jsx(Empty, {
534
- className: "h-full rounded-none bg-background/45",
532
+ className: "h-full rounded-none",
535
533
  children: /* @__PURE__ */ jsxs(EmptyHeader, { children: [
536
534
  /* @__PURE__ */ jsx(EmptyMedia, { children: /* @__PURE__ */ jsx(FileIcon, {}) }),
537
535
  /* @__PURE__ */ jsx(EmptyTitle, { children: "Select an item" }),
@@ -539,7 +537,7 @@ function FileExplorerPreview({ entry, headingLevel }) {
539
537
  ] })
540
538
  });
541
539
  return /* @__PURE__ */ jsx(ScrollArea, {
542
- className: "h-full bg-background/45",
540
+ className: "h-full",
543
541
  lockAxis: "x",
544
542
  children: /* @__PURE__ */ jsx(FileExplorerPreviewContent, {
545
543
  entry,
@@ -681,7 +679,7 @@ function FileExplorerStatusBar({ breadcrumbs, selectedEntry }) {
681
679
  const itemCount = selectedEntry?.children?.length ?? 0;
682
680
  const itemLabel = itemCount === 1 ? "1 item" : `${itemCount} items`;
683
681
  return /* @__PURE__ */ jsxs("footer", {
684
- className: "flex h-8 shrink-0 items-center gap-3 border-t border-border/70 bg-card/45 px-3 text-micro text-muted-foreground",
682
+ className: "flex h-8 shrink-0 items-center gap-3 border-t border-border/70 px-3 text-micro text-muted-foreground",
685
683
  children: [/* @__PURE__ */ jsx("span", {
686
684
  className: "min-w-0 flex-1 truncate",
687
685
  title: path,
@@ -695,7 +693,7 @@ function FileExplorerStatusBar({ breadcrumbs, selectedEntry }) {
695
693
  function FileExplorerAddressBar({ breadcrumbs, onSelect }) {
696
694
  return /* @__PURE__ */ jsx(Breadcrumb, {
697
695
  "aria-label": "Current folder",
698
- className: "min-w-0 overflow-hidden rounded-[var(--radius-control)] border bg-background/55 px-2 py-1 shadow-inner",
696
+ className: "min-w-0 overflow-hidden px-1",
699
697
  children: /* @__PURE__ */ jsx(BreadcrumbList, {
700
698
  className: "min-w-0 flex-nowrap overflow-hidden",
701
699
  children: breadcrumbs.map((breadcrumb, index) => {
package/dist/lib/cn.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { ClassValue } from "clsx";
2
+ import { Config } from "tailwind-merge";
2
3
  //#region src/lib/cn.d.ts
4
+ export declare function withControlUiMerge(config: Config<string, string>): Config<string, string>;
3
5
  export declare function cn(...inputs: ClassValue[]): string;
4
6
  //#endregion
package/dist/lib/cn.js CHANGED
@@ -1,30 +1,35 @@
1
1
  import { clsx } from "clsx";
2
- import { extendTailwindMerge } from "tailwind-merge";
2
+ import { extendTailwindMerge, mergeConfigs } from "tailwind-merge";
3
3
  //#region src/lib/cn.ts
4
- const twMerge = extendTailwindMerge({
5
- extend: { classGroups: {
6
- "font-size": [{ text: [
7
- "micro",
8
- "caption",
9
- "label",
10
- "body",
11
- "body-lg",
12
- "heading-1",
13
- "heading-2",
14
- "heading-3",
15
- "heading-4",
16
- "display"
17
- ] }],
18
- shadow: [{ shadow: [
19
- "pop",
20
- "soft",
21
- "modal"
22
- ] }]
23
- } },
24
- override: { conflictingClassGroups: { "font-size": [] } }
25
- });
4
+ const textScale = [
5
+ "micro",
6
+ "caption",
7
+ "label",
8
+ "body",
9
+ "body-lg",
10
+ "heading-1",
11
+ "heading-2",
12
+ "heading-3",
13
+ "heading-4",
14
+ "display"
15
+ ];
16
+ const shadowScale = [
17
+ "pop",
18
+ "soft",
19
+ "modal"
20
+ ];
21
+ function withControlUiMerge(config) {
22
+ return mergeConfigs(config, {
23
+ extend: { classGroups: {
24
+ "font-size": [{ text: textScale }],
25
+ shadow: [{ shadow: shadowScale }]
26
+ } },
27
+ override: { conflictingClassGroups: { "font-size": [] } }
28
+ });
29
+ }
30
+ const twMerge = extendTailwindMerge(withControlUiMerge);
26
31
  function cn(...inputs) {
27
32
  return twMerge(clsx(inputs));
28
33
  }
29
34
  //#endregion
30
- export { cn };
35
+ export { cn, withControlUiMerge };
@@ -63,4 +63,16 @@
63
63
  cursor: not-allowed;
64
64
  opacity: var(--disabled-opacity);
65
65
  }
66
+
67
+ @media (forced-colors: active) {
68
+ :where([data-control-family="button"][data-control="true"]:not(:focus-visible, :has(:focus-visible))) {
69
+ outline: 1px solid currentColor;
70
+ outline-offset: -1px;
71
+ }
72
+
73
+ :where([data-control-family="button"][data-control="true"][data-active="true"]:not(:focus-visible, :has(:focus-visible))) {
74
+ outline: var(--focus-ring-width) solid Highlight;
75
+ outline-offset: calc(var(--focus-ring-width) * -1);
76
+ }
77
+ }
66
78
  }
@@ -164,7 +164,7 @@
164
164
  --cui-button-shadow: inset 0 0 0 var(--control-rim-width) var(--control-rim), var(--shadow-sm);
165
165
  --cui-button-active-background: oklch(from var(--primary) l c h / 0.08);
166
166
  --cui-button-active-foreground: var(--primary-text);
167
- --cui-button-active-shadow: inset 0 0 0 1px oklch(from var(--primary) l c h / 0.6), var(--shadow-sm);
167
+ --cui-button-active-shadow: inset 0 0 0 var(--control-rim-width) oklch(from var(--primary) l c h / 0.6), var(--shadow-sm);
168
168
  --cui-button-active-hover-background: oklch(from var(--primary) l c h / 0.1);
169
169
  }
170
170
 
@@ -25,6 +25,13 @@
25
25
  transition-timing-function: var(--ease-standard);
26
26
  }
27
27
 
28
+ @media (forced-colors: active) {
29
+ :where([data-control-family="chat-composer-attachment"][data-slot="root"][data-variant]:not(:focus-visible, :has(:focus-visible))) {
30
+ outline: 1px solid CanvasText;
31
+ outline-offset: -1px;
32
+ }
33
+ }
34
+
28
35
  :where([data-control-family="chat-composer-attachment"][data-slot="list"]) {
29
36
  gap: calc(var(--spacing) * 2);
30
37
  padding: var(--cui-chat-composer-attachment-list-padding);
@@ -5,6 +5,13 @@
5
5
  box-shadow: var(--cui-color-picker-trigger-shadow);
6
6
  }
7
7
 
8
+ @media (forced-colors: active) {
9
+ :where([data-control-family="color-picker"][data-slot="trigger-color"]) {
10
+ outline: 1px solid CanvasText;
11
+ outline-offset: -1px;
12
+ }
13
+ }
14
+
8
15
  :where([data-control-family="color-picker"][data-slot="trigger"]) {
9
16
  border-radius: var(--cui-color-picker-trigger-radius);
10
17
  transition-property: transform, translate, scale, rotate;
@@ -25,6 +25,13 @@
25
25
  backdrop-filter: var(--cui-field-backdrop-filter);
26
26
  }
27
27
 
28
+ @media (forced-colors: active) {
29
+ :where([data-control-family="field"][data-control="true"]:not(:focus-visible, :has(:focus-visible))) {
30
+ outline: 1px solid CanvasText;
31
+ outline-offset: -1px;
32
+ }
33
+ }
34
+
28
35
  :where(
29
36
  [data-control-family="field"][data-control="true"]:not([data-control-family="field"][data-field-kind="autocomplete"]):not(
30
37
  [data-control-family="field"][data-field-kind="combobox"]
@@ -52,6 +52,13 @@
52
52
  }
53
53
  }
54
54
 
55
+ @media (forced-colors: active) {
56
+ :where([data-control-family="inline-attachment"][data-slot="root"]:not(:focus-visible)) {
57
+ outline: 1px solid CanvasText;
58
+ outline-offset: -1px;
59
+ }
60
+ }
61
+
55
62
  :where([data-control-family="inline-attachment"][data-slot="image"]) {
56
63
  border-radius: inherit;
57
64
  }
@@ -28,6 +28,13 @@
28
28
  --cui-kbd-shadow: 0 0 transparent;
29
29
  }
30
30
 
31
+ @media (forced-colors: active) {
32
+ :where([data-control-family="kbd"][data-slot="root"]:not([data-variant="ghost"])) {
33
+ outline: 1px solid CanvasText;
34
+ outline-offset: -1px;
35
+ }
36
+ }
37
+
31
38
  :where([data-control-family="kbd"][data-slot="group"]) {
32
39
  gap: calc(var(--spacing) * 1);
33
40
  }
@@ -17,7 +17,7 @@
17
17
  border-radius: var(--cui-pagination-link-radius);
18
18
  background: var(--cui-pagination-link-background);
19
19
  box-shadow:
20
- inset 0 0 0 1px var(--_pagination-link-border-color),
20
+ inset 0 0 0 var(--control-rim-width) var(--_pagination-link-border-color),
21
21
  var(--cui-pagination-link-shadow);
22
22
  color: var(--cui-pagination-link-foreground);
23
23
  font-variant-numeric: tabular-nums;
@@ -39,6 +39,13 @@
39
39
  opacity: var(--disabled-opacity);
40
40
  }
41
41
 
42
+ @media (forced-colors: active) {
43
+ :where([data-control-family="pagination"][data-slot="link"][data-active="true"]:not(:focus-visible)) {
44
+ outline: var(--focus-ring-width) solid Highlight;
45
+ outline-offset: calc(var(--focus-ring-width) * -1);
46
+ }
47
+ }
48
+
42
49
  :where([data-control-family="pagination"][data-slot="link"]:has(> svg:first-child)) {
43
50
  padding-inline-start: calc(var(--spacing) * 2);
44
51
  }
@@ -39,14 +39,12 @@
39
39
  :where(
40
40
  [data-control-family="scroll-area"][data-slot="root"][data-scrollbar-gutter="stable"]
41
41
  > [data-slot="scrollbar"][data-orientation="vertical"]:not([data-has-overflow-y])
42
- > [data-slot="thumb"]
43
42
  ),
44
43
  :where(
45
44
  [data-control-family="scroll-area"][data-slot="root"][data-scrollbar-gutter="stable"]
46
45
  > [data-slot="scrollbar"][data-orientation="horizontal"]:not([data-has-overflow-x])
47
- > [data-slot="thumb"]
48
46
  ) {
49
- display: none;
47
+ visibility: hidden;
50
48
  }
51
49
 
52
50
  @supports (corner-shape: squircle) {
@@ -114,6 +114,18 @@
114
114
  opacity: var(--disabled-opacity);
115
115
  }
116
116
 
117
+ @media (forced-colors: active) {
118
+ :where([data-control-family="tabs"][data-slot="list"]:not([data-variant="browser"], [data-single="true"])) {
119
+ outline: 1px solid CanvasText;
120
+ outline-offset: -1px;
121
+ }
122
+
123
+ :where([data-control-family="tabs"][data-slot="list"]:not([data-variant="browser"]) [data-slot="indicator"]) {
124
+ outline: var(--focus-ring-width) solid Highlight;
125
+ outline-offset: calc(var(--focus-ring-width) * -1);
126
+ }
127
+ }
128
+
117
129
  :where([data-control-family="tabs"][data-slot="list"][data-variant="browser"]) {
118
130
  --_tabs-browser-corner-double: calc(var(--_tabs-trigger-radius-fit) + var(--_tabs-trigger-radius-fit));
119
131
  --_tabs-browser-inset: calc(var(--_tabs-trigger-radius-fit) + var(--cui-tabs-surface-radius));
@@ -65,7 +65,8 @@
65
65
 
66
66
  /*
67
67
  * History: fixed 3 competing type systems (Tailwind defaults + named rungs + raw text-[Npx]) into one rung/role; 14px = body.
68
- * Usage: apply the rung directly to the semantic element (no <Text> component), e.g. text-heading-1 on <h1>, text-body on <p>.
68
+ * Usage: React text goes through <Text>/<Heading> (ui/typography): size, tone and weight are props, className stays layout-only.
69
+ * A bare rung is for markup React doesn't render as components (MDX, markdown, email HTML), e.g. text-heading-1 on <h1>.
69
70
  * Heading LEVEL is the document outline; the RUNG is the look. They are decoupled on purpose: an <h3> may wear text-heading-2.
70
71
  * Control chrome text is sized by its recipe (`--cui-button-font-size`, `--cui-field-font-size`), not by these utilities.
71
72
  */
@@ -309,7 +309,7 @@ function SidebarContent({ className, children, ...props }) {
309
309
  className: cn("min-h-0 flex-1 group-data-[collapsible=icon]:overflow-hidden", className),
310
310
  lockAxis: "x",
311
311
  scrollbarGutter: iconOnly ? "auto" : void 0,
312
- viewportClassName: "overscroll-y-contain",
312
+ viewportClassName: "data-has-overflow-y:overscroll-y-contain",
313
313
  ...props,
314
314
  children: /* @__PURE__ */ jsx("div", {
315
315
  "data-control-ui": "sidebar",
@@ -17,6 +17,8 @@ declare const toneClassNames: {
17
17
  readonly foreground: "text-foreground";
18
18
  readonly muted: "text-muted-foreground";
19
19
  readonly primary: "text-primary-text";
20
+ readonly success: "text-success-text";
21
+ readonly warning: "text-warning-text";
20
22
  readonly destructive: "text-destructive-text";
21
23
  };
22
24
  declare const weightClassNames: {
@@ -18,6 +18,8 @@ const toneClassNames = {
18
18
  foreground: "text-foreground",
19
19
  muted: "text-muted-foreground",
20
20
  primary: "text-primary-text",
21
+ success: "text-success-text",
22
+ warning: "text-warning-text",
21
23
  destructive: "text-destructive-text"
22
24
  };
23
25
  const weightClassNames = {
@@ -0,0 +1,7 @@
1
+ language js(typescript, jsx)
2
+
3
+ JsxAttribute(name=$name, initializer=$value) as $attr where {
4
+ $name <: `className`,
5
+ $value <: contains r".*\btext-(?:micro|caption|label|body|body-lg|heading-[1-4]|display)\b.*",
6
+ register_diagnostic(span=$attr, message="Type rung in className: render text through <Text>/<Heading>, or size the control with its size prop or --cui-*-font-size knob.", severity="warn")
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctrl-ui/react",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
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",
@@ -32,7 +32,8 @@
32
32
  "**/*.css"
33
33
  ],
34
34
  "files": [
35
- "dist"
35
+ "dist",
36
+ "lint/*.grit"
36
37
  ],
37
38
  "publishConfig": {
38
39
  "access": "public"