@ctrl-ui/react 0.9.3 → 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:
@@ -1,4 +1,6 @@
1
1
  import { ChatLayout } from "../chat-layout.js";
2
+ import { SidebarProviderProps } from "../ui/sidebar-provider.js";
3
+ import "../ui/sidebar.js";
2
4
  import { ComponentProps, ReactNode } from "react";
3
5
  //#region src/blocks/coding-agent.d.ts
4
6
  export type CodingAgentNavigationItem = {
@@ -19,7 +21,7 @@ export type CodingAgentProject = {
19
21
  tasks: readonly CodingAgentTask[];
20
22
  open?: boolean;
21
23
  };
22
- export type CodingAgentBlockProps = Omit<ComponentProps<"div">, "children"> & {
24
+ export type CodingAgentBlockProps = Omit<ComponentProps<"div">, "children"> & Pick<SidebarProviderProps, "persistOpen" | "keyboardShortcut"> & {
23
25
  appName?: string;
24
26
  brand?: ReactNode;
25
27
  sidebarTop?: ReactNode;
@@ -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
  });
@@ -1,7 +1,9 @@
1
+ import { SidebarProviderProps } from "../ui/sidebar-provider.js";
2
+ import "../ui/sidebar.js";
1
3
  import { FileExplorerBreadcrumb, FileExplorerColumn, FileExplorerEntry, FileExplorerEntryDetail, FileExplorerLocation, FileExplorerResolution, FileExplorerSearchResult } from "./file-explorer-data.js";
2
4
  import { ComponentProps, ReactNode } from "react";
3
5
  //#region src/blocks/file-explorer.d.ts
4
- export type FileExplorerBlockProps = Omit<ComponentProps<"div">, "children" | "defaultValue" | "onChange"> & {
6
+ export type FileExplorerBlockProps = Omit<ComponentProps<"div">, "children" | "defaultValue" | "onChange"> & Pick<SidebarProviderProps, "persistOpen" | "keyboardShortcut"> & {
5
7
  locations: readonly FileExplorerLocation[];
6
8
  activeLocationId?: string;
7
9
  defaultActiveLocationId?: string;
@@ -27,6 +29,6 @@ export type FileExplorerBlockProps = Omit<ComponentProps<"div">, "children" | "d
27
29
  detailsLabel?: string;
28
30
  layout?: "viewport" | "contained";
29
31
  };
30
- export declare function FileExplorerBlock({ locations, activeLocationId, defaultActiveLocationId, selectedPath, defaultSelectedPath, searchValue, defaultSearchValue, onActiveLocationChange, onSelectedPathChange, onSearchValueChange, onOpenEntry, sidebarTop, sidebarFooter, headerActions, headerMenuItems, searchPlaceholder, searchLabel, clearSearchLabel, searchResultsLabel, searchStatusLabel, backLabel, moreActionsLabel, detailsLabel, layout, className, style, ...props }: FileExplorerBlockProps): import("react").JSX.Element;
32
+ export declare function FileExplorerBlock({ locations, activeLocationId, defaultActiveLocationId, selectedPath, defaultSelectedPath, searchValue, defaultSearchValue, onActiveLocationChange, onSelectedPathChange, onSearchValueChange, onOpenEntry, sidebarTop, sidebarFooter, headerActions, headerMenuItems, searchPlaceholder, searchLabel, clearSearchLabel, searchResultsLabel, searchStatusLabel, backLabel, moreActionsLabel, detailsLabel, layout, persistOpen, keyboardShortcut, className, style, ...props }: FileExplorerBlockProps): import("react").JSX.Element;
31
33
  //#endregion
32
34
  export type { FileExplorerBreadcrumb, FileExplorerColumn, FileExplorerEntry, FileExplorerEntryDetail, FileExplorerLocation, FileExplorerResolution, FileExplorerSearchResult };
@@ -23,7 +23,7 @@ function defaultSearchStatusLabel(count, query) {
23
23
  if (count === 0) return `No results for “${query}”`;
24
24
  return count === 1 ? "1 result" : `${count} results`;
25
25
  }
26
- function FileExplorerBlock({ locations, activeLocationId, defaultActiveLocationId, selectedPath, defaultSelectedPath = [], searchValue, defaultSearchValue = "", onActiveLocationChange, onSelectedPathChange, onSearchValueChange, onOpenEntry, sidebarTop, sidebarFooter, headerActions, headerMenuItems, searchPlaceholder = "Search files", searchLabel = "Search files", clearSearchLabel = "Clear search", searchResultsLabel = "Search results", searchStatusLabel = defaultSearchStatusLabel, backLabel = "Go back", moreActionsLabel = "More actions", detailsLabel = "Details", layout = "viewport", className, style, ...props }) {
26
+ function FileExplorerBlock({ locations, activeLocationId, defaultActiveLocationId, selectedPath, defaultSelectedPath = [], searchValue, defaultSearchValue = "", onActiveLocationChange, onSelectedPathChange, onSearchValueChange, onOpenEntry, sidebarTop, sidebarFooter, headerActions, headerMenuItems, searchPlaceholder = "Search files", searchLabel = "Search files", clearSearchLabel = "Clear search", searchResultsLabel = "Search results", searchStatusLabel = defaultSearchStatusLabel, backLabel = "Go back", moreActionsLabel = "More actions", detailsLabel = "Details", layout = "viewport", persistOpen, keyboardShortcut, className, style, ...props }) {
27
27
  const [internalLocationId, setInternalLocationId] = useState(defaultActiveLocationId ?? locations[0]?.id);
28
28
  const [internalSelectedPath, setInternalSelectedPath] = useState(defaultSelectedPath);
29
29
  const [internalSearchValue, setInternalSearchValue] = useState(defaultSearchValue);
@@ -68,6 +68,8 @@ function FileExplorerBlock({ locations, activeLocationId, defaultActiveLocationI
68
68
  };
69
69
  return /* @__PURE__ */ jsxs(SidebarProvider, {
70
70
  layout,
71
+ persistOpen,
72
+ keyboardShortcut,
71
73
  className: cn("bg-background text-foreground", !contained && "h-svh overflow-hidden", className),
72
74
  style: providerStyle,
73
75
  ...props,
@@ -145,7 +147,7 @@ function FileExplorerSidebar({ locations, activeLocationId, sidebarTop, sidebarF
145
147
  collapsible: "offcanvas",
146
148
  children: [
147
149
  sidebarTop ? /* @__PURE__ */ jsx(SidebarHeader, {
148
- className: "min-h-11 justify-center border-b border-sidebar-border px-3",
150
+ className: "min-h-11 justify-center px-3",
149
151
  children: sidebarTop
150
152
  }) : null,
151
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, {
@@ -157,24 +159,22 @@ function FileExplorerSidebar({ locations, activeLocationId, sidebarTop, sidebarF
157
159
  },
158
160
  children: [location.icon ?? /* @__PURE__ */ jsx(FolderIcon, {}), /* @__PURE__ */ jsx("span", { children: location.label })]
159
161
  }) }, location.id)) })] }, group.label || "locations")) }),
160
- sidebarFooter ? /* @__PURE__ */ jsx(SidebarFooter, {
161
- className: "border-t border-sidebar-border",
162
- children: sidebarFooter
163
- }) : null,
162
+ sidebarFooter ? /* @__PURE__ */ jsx(SidebarFooter, { children: sidebarFooter }) : null,
164
163
  /* @__PURE__ */ jsx(SidebarRail, {})
165
164
  ]
166
165
  });
167
166
  }
168
167
  function FileExplorerHeader({ location, headingLevel, breadcrumbs, query, searchPlaceholder, searchLabel, clearSearchLabel, backLabel, moreActionsLabel, canGoBack, headerActions, headerMenuItems, onBack, onBreadcrumbSelect, onQueryChange }) {
169
168
  return /* @__PURE__ */ jsxs("header", {
170
- className: "shrink-0 border-b border-border/70 bg-card/35",
169
+ className: "shrink-0 border-b border-border/70",
171
170
  children: [/* @__PURE__ */ jsxs("div", {
172
171
  className: "flex h-12 items-center gap-2 px-3",
173
172
  children: [
174
- /* @__PURE__ */ jsx(SidebarTrigger, { size: "xs" }),
173
+ /* @__PURE__ */ jsx(SidebarTrigger, { size: "sm" }),
175
174
  /* @__PURE__ */ jsx(Button, {
176
175
  variant: "ghost",
177
- size: "xs",
176
+ size: "sm",
177
+ iconOnly: true,
178
178
  className: "hidden @xl/explorer:inline-flex",
179
179
  disabled: !canGoBack,
180
180
  "aria-label": backLabel,
@@ -198,7 +198,7 @@ function FileExplorerHeader({ location, headingLevel, breadcrumbs, query, search
198
198
  }) : null,
199
199
  headerMenuItems ? /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
200
200
  variant: "ghost",
201
- size: "xs",
201
+ size: "sm",
202
202
  iconOnly: true,
203
203
  "aria-label": moreActionsLabel,
204
204
  className: headerActions ? "@2xl/explorer:hidden" : void 0,
@@ -212,7 +212,7 @@ function FileExplorerHeader({ location, headingLevel, breadcrumbs, query, search
212
212
  })] }) : null,
213
213
  searchPlaceholder ? /* @__PURE__ */ jsxs(InputGroup, {
214
214
  size: "sm",
215
- 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]",
216
216
  children: [
217
217
  /* @__PURE__ */ jsx(InputGroupAddon, {
218
218
  className: "ps-2",
@@ -406,7 +406,7 @@ function FileExplorerColumns({ columns, selectedEntry, selectedPath, headingLeve
406
406
  })
407
407
  }) }) : null })] })]
408
408
  }), /* @__PURE__ */ jsx("div", {
409
- className: "min-h-0 flex-1 bg-card",
409
+ className: "min-h-0 flex-1",
410
410
  children: activeColumn ? renderColumn(activeColumn, activeIndex, "narrow") : null
411
411
  })] }) : null
412
412
  }), /* @__PURE__ */ jsx("div", {
@@ -414,7 +414,7 @@ function FileExplorerColumns({ columns, selectedEntry, selectedPath, headingLeve
414
414
  className: "hidden h-full min-h-0 @xl/explorer:block",
415
415
  children: wideDisplayed ? /* @__PURE__ */ jsxs(ResizablePanelGroup, {
416
416
  orientation: "horizontal",
417
- className: "rounded-none border-0 bg-card shadow-none",
417
+ variant: "nested",
418
418
  children: [columns.map((column, columnIndex) => /* @__PURE__ */ jsx(FileExplorerPanel, {
419
419
  defaultSize: columnSize,
420
420
  minSize: "16%",
@@ -529,7 +529,7 @@ function FileExplorerColumnView({ column, optionRef, onSelect, onOpen, onOptionK
529
529
  }
530
530
  function FileExplorerPreview({ entry, headingLevel }) {
531
531
  if (!entry) return /* @__PURE__ */ jsx(Empty, {
532
- className: "h-full rounded-none bg-background/45",
532
+ className: "h-full rounded-none",
533
533
  children: /* @__PURE__ */ jsxs(EmptyHeader, { children: [
534
534
  /* @__PURE__ */ jsx(EmptyMedia, { children: /* @__PURE__ */ jsx(FileIcon, {}) }),
535
535
  /* @__PURE__ */ jsx(EmptyTitle, { children: "Select an item" }),
@@ -537,7 +537,7 @@ function FileExplorerPreview({ entry, headingLevel }) {
537
537
  ] })
538
538
  });
539
539
  return /* @__PURE__ */ jsx(ScrollArea, {
540
- className: "h-full bg-background/45",
540
+ className: "h-full",
541
541
  lockAxis: "x",
542
542
  children: /* @__PURE__ */ jsx(FileExplorerPreviewContent, {
543
543
  entry,
@@ -679,7 +679,7 @@ function FileExplorerStatusBar({ breadcrumbs, selectedEntry }) {
679
679
  const itemCount = selectedEntry?.children?.length ?? 0;
680
680
  const itemLabel = itemCount === 1 ? "1 item" : `${itemCount} items`;
681
681
  return /* @__PURE__ */ jsxs("footer", {
682
- 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",
683
683
  children: [/* @__PURE__ */ jsx("span", {
684
684
  className: "min-w-0 flex-1 truncate",
685
685
  title: path,
@@ -693,7 +693,7 @@ function FileExplorerStatusBar({ breadcrumbs, selectedEntry }) {
693
693
  function FileExplorerAddressBar({ breadcrumbs, onSelect }) {
694
694
  return /* @__PURE__ */ jsx(Breadcrumb, {
695
695
  "aria-label": "Current folder",
696
- 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",
697
697
  children: /* @__PURE__ */ jsx(BreadcrumbList, {
698
698
  className: "min-w-0 flex-nowrap overflow-hidden",
699
699
  children: breadcrumbs.map((breadcrumb, index) => {
@@ -1,8 +1,10 @@
1
1
  import { Button } from "../ui/button.js";
2
+ import { SidebarProviderProps } from "../ui/sidebar-provider.js";
3
+ import "../ui/sidebar.js";
2
4
  import { SettingDefinition, SettingsControlProps, SettingsPageDefinition, SettingsSectionDefinition } from "./settings-data.js";
3
5
  import { ComponentProps, ReactNode } from "react";
4
6
  //#region src/blocks/settings.d.ts
5
- export type SettingsBlockProps = Omit<ComponentProps<"div">, "children"> & {
7
+ export type SettingsBlockProps = Omit<ComponentProps<"div">, "children"> & Pick<SidebarProviderProps, "persistOpen" | "keyboardShortcut"> & {
6
8
  pages: readonly SettingsPageDefinition[];
7
9
  activePageId?: string;
8
10
  defaultActivePageId?: string;
@@ -1,4 +1,4 @@
1
1
  //#region src/knob-contracts/range-knobs.d.ts
2
- export declare const rangeKnobs: readonly ["--cui-range-level-warning-background", "--cui-range-level-clip-background", "--cui-range-level-size", "--cui-range-level-scale-foreground", "--cui-range-indeterminate-size", "--cui-range-track-radius", "--cui-range-track-background", "--cui-range-indicator-radius", "--cui-range-indicator-background", "--cui-range-thumb-radius", "--cui-range-thumb-background", "--cui-range-thumb-border-color", "--cui-range-thumb-shadow"];
2
+ export declare const rangeKnobs: readonly ["--cui-range-level-warning-background", "--cui-range-level-clip-background", "--cui-range-level-muted-background", "--cui-range-level-size", "--cui-range-level-scale-foreground", "--cui-range-indeterminate-size", "--cui-range-track-radius", "--cui-range-track-background", "--cui-range-indicator-radius", "--cui-range-indicator-background", "--cui-range-thumb-radius", "--cui-range-thumb-background", "--cui-range-thumb-border-color", "--cui-range-thumb-shadow"];
3
3
  export type RangeKnobStyle = Partial<Record<(typeof rangeKnobs)[number], string>>;
4
4
  //#endregion
@@ -2,6 +2,7 @@
2
2
  const rangeKnobs = [
3
3
  "--cui-range-level-warning-background",
4
4
  "--cui-range-level-clip-background",
5
+ "--cui-range-level-muted-background",
5
6
  "--cui-range-level-size",
6
7
  "--cui-range-level-scale-foreground",
7
8
  "--cui-range-indeterminate-size",
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
  }
@@ -2,6 +2,7 @@
2
2
  :where([data-control-family="range"][data-range-kind="level-meter"][data-slot="root"]) {
3
3
  --cui-range-level-warning-background: var(--warning);
4
4
  --cui-range-level-clip-background: var(--destructive);
5
+ --cui-range-level-muted-background: var(--muted-foreground);
5
6
  --cui-range-level-size: calc(var(--spacing) * 2);
6
7
  --cui-range-level-scale-foreground: var(--muted-foreground);
7
8
  --_range-level-peak-size: 2px;
@@ -96,6 +97,13 @@
96
97
  );
97
98
  }
98
99
 
100
+ :where(
101
+ [data-range-kind="level-meter"][data-slot="root"][data-muted]
102
+ [data-control-family="range"][data-range-kind="level-meter"]:is([data-slot="bar"], [data-slot="peak"])
103
+ ) {
104
+ background: var(--cui-range-level-muted-background);
105
+ }
106
+
99
107
  :where([data-control-family="range"][data-range-kind="level-meter"][data-slot="bar"]) {
100
108
  clip-path: inset(0 calc((1 - var(--_range-level, 0)) * 100%) 0 0 round var(--cui-range-indicator-radius));
101
109
  }
@@ -145,6 +153,12 @@
145
153
  initial-value: transparent;
146
154
  }
147
155
 
156
+ @property --cui-range-level-muted-background {
157
+ syntax: "<color>";
158
+ inherits: true;
159
+ initial-value: transparent;
160
+ }
161
+
148
162
  @property --cui-range-level-size {
149
163
  syntax: "<length-percentage>";
150
164
  inherits: true;
@@ -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
  */
@@ -12,6 +12,7 @@ export type LevelMeterProps = Omit<ComponentProps<"div">, "style"> & {
12
12
  maxDb?: number;
13
13
  warningDb?: number;
14
14
  clipDb?: number;
15
+ muted?: boolean;
15
16
  orientation?: LevelMeterOrientation;
16
17
  style?: CSSProperties & RangeKnobStyle;
17
18
  };
@@ -28,7 +29,7 @@ export type LevelMeterScaleProps = Omit<ComponentProps<"div">, "style" | "childr
28
29
  ticks?: readonly number[];
29
30
  style?: CSSProperties & RangeKnobStyle;
30
31
  };
31
- export declare function LevelMeter({ value, source, minDb, maxDb, warningDb, clipDb, orientation, className, style, children, ref, ...props }: LevelMeterProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
32
+ export declare function LevelMeter({ value, source, minDb, maxDb, warningDb, clipDb, muted, orientation, className, style, children, ref, ...props }: LevelMeterProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
32
33
  export declare function LevelMeterTrack({ className, ...props }: LevelMeterTrackProps): import("react").JSX.Element;
33
34
  export declare function LevelMeterBar({ className, ...props }: LevelMeterBarProps): import("react").JSX.Element;
34
35
  export declare function LevelMeterPeak({ className, ...props }: LevelMeterPeakProps): import("react").JSX.Element;
@@ -77,7 +77,7 @@ function createLevelMeterPainter() {
77
77
  }
78
78
  };
79
79
  }
80
- function LevelMeter({ value, source, minDb = -60, maxDb = 0, warningDb = -18, clipDb = -6, orientation = "horizontal", className, style, children, ref, ...props }) {
80
+ function LevelMeter({ value, source, minDb = -60, maxDb = 0, warningDb = -18, clipDb = -6, muted = false, orientation = "horizontal", className, style, children, ref, ...props }) {
81
81
  const [painter] = useState(createLevelMeterPainter);
82
82
  const rootRef = useRef(null);
83
83
  const labelled = Boolean(props["aria-label"] || props["aria-labelledby"]);
@@ -130,6 +130,7 @@ function LevelMeter({ value, source, minDb = -60, maxDb = 0, warningDb = -18, cl
130
130
  "data-range-kind": "level-meter",
131
131
  "data-slot": "root",
132
132
  "data-orientation": orientation,
133
+ "data-muted": muted || void 0,
133
134
  className: cn("grid", className),
134
135
  style: zoneStyle,
135
136
  children: /* @__PURE__ */ jsx(LevelMeterScaleContext, {
@@ -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",
package/dist/ui/toggle.js CHANGED
@@ -76,7 +76,7 @@ function ToggleGroup({ className, orientation = "horizontal", indicator, childre
76
76
  "data-orientation": orientation,
77
77
  "data-track": resolvedIndicator,
78
78
  orientation,
79
- className: cn(buttonTrackStructureClasses, "inline-flex items-center data-[track=hover]:gap-0", orientation === "vertical" && "flex-col", className),
79
+ className: cn(buttonTrackStructureClasses, "inline-flex items-center", orientation === "vertical" && "flex-col", className),
80
80
  ...props,
81
81
  children: [children, tracksHover ? /* @__PURE__ */ jsx(TrackHighlight, { className: "z-0" }) : null]
82
82
  })
@@ -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.3",
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"