@ctrl-ui/react 0.9.6 → 0.9.8

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.
Files changed (119) hide show
  1. package/README.md +38 -2
  2. package/dist/activity.d.ts +3 -1
  3. package/dist/activity.js +2 -1
  4. package/dist/blocks/agent-run-sidebar.js +11 -10
  5. package/dist/blocks/agent-run.d.ts +3 -1
  6. package/dist/blocks/agent-run.js +21 -13
  7. package/dist/blocks/coding-agent.js +50 -69
  8. package/dist/blocks/design-canvas-inspector.js +1 -1
  9. package/dist/blocks/design-canvas.js +41 -34
  10. package/dist/blocks/discussion.js +1 -1
  11. package/dist/blocks/file-explorer.js +66 -47
  12. package/dist/blocks/settings.js +37 -41
  13. package/dist/blocks/team-chat.js +2 -1
  14. package/dist/blocks/theme-toggle.d.ts +2 -1
  15. package/dist/blocks/theme-toggle.js +13 -4
  16. package/dist/hooks/use-mobile.d.ts +1 -1
  17. package/dist/hooks/use-mobile.js +1 -1
  18. package/dist/knob-contracts/badge-knobs.d.ts +1 -1
  19. package/dist/knob-contracts/badge-knobs.js +3 -1
  20. package/dist/knob-contracts/button-knobs.d.ts +1 -1
  21. package/dist/knob-contracts/button-knobs.js +1 -0
  22. package/dist/knob-contracts/calendar-knobs.d.ts +1 -1
  23. package/dist/knob-contracts/calendar-knobs.js +9 -1
  24. package/dist/knob-contracts/card-knobs.d.ts +1 -1
  25. package/dist/knob-contracts/card-knobs.js +5 -1
  26. package/dist/knob-contracts/choice-knobs.d.ts +1 -1
  27. package/dist/knob-contracts/choice-knobs.js +1 -0
  28. package/dist/knob-contracts/collapsible-knobs.d.ts +1 -1
  29. package/dist/knob-contracts/collapsible-knobs.js +5 -1
  30. package/dist/knob-contracts/emoji-picker-knobs.d.ts +1 -1
  31. package/dist/knob-contracts/emoji-picker-knobs.js +3 -1
  32. package/dist/knob-contracts/field-knobs.d.ts +1 -1
  33. package/dist/knob-contracts/field-knobs.js +17 -1
  34. package/dist/knob-contracts/item-knobs.d.ts +1 -1
  35. package/dist/knob-contracts/item-knobs.js +5 -1
  36. package/dist/knob-contracts/kbd-knobs.d.ts +1 -1
  37. package/dist/knob-contracts/kbd-knobs.js +4 -1
  38. package/dist/knob-contracts/page-layout-knobs.d.ts +1 -1
  39. package/dist/knob-contracts/page-layout-knobs.js +1 -0
  40. package/dist/knob-contracts/pagination-knobs.d.ts +1 -1
  41. package/dist/knob-contracts/pagination-knobs.js +2 -1
  42. package/dist/knob-contracts/popup-knobs.d.ts +1 -1
  43. package/dist/knob-contracts/popup-knobs.js +9 -0
  44. package/dist/knob-contracts/range-knobs.d.ts +1 -1
  45. package/dist/knob-contracts/range-knobs.js +2 -0
  46. package/dist/knob-contracts/sidebar-knobs.d.ts +1 -1
  47. package/dist/knob-contracts/sidebar-knobs.js +2 -0
  48. package/dist/knob-contracts/tabs-knobs.d.ts +1 -1
  49. package/dist/knob-contracts/tabs-knobs.js +3 -0
  50. package/dist/knob-contracts/timeline-knobs.d.ts +1 -1
  51. package/dist/knob-contracts/timeline-knobs.js +4 -1
  52. package/dist/knob-contracts/tree-knobs.d.ts +1 -1
  53. package/dist/knob-contracts/tree-knobs.js +3 -1
  54. package/dist/skin.d.ts +2 -0
  55. package/dist/styles/index.css +2 -3
  56. package/dist/styles/recipes/activity.css +4 -0
  57. package/dist/styles/recipes/avatar.css +1 -1
  58. package/dist/styles/recipes/badge.css +17 -0
  59. package/dist/styles/recipes/button.css +8 -1
  60. package/dist/styles/recipes/calendar.css +82 -6
  61. package/dist/styles/recipes/card.css +40 -5
  62. package/dist/styles/recipes/choice.css +9 -2
  63. package/dist/styles/recipes/collapsible.css +8 -1
  64. package/dist/styles/recipes/color-picker-values.css +4 -0
  65. package/dist/styles/recipes/emoji-picker.css +29 -8
  66. package/dist/styles/recipes/field-parts.css +80 -5
  67. package/dist/styles/recipes/field.css +113 -40
  68. package/dist/styles/recipes/item.css +33 -5
  69. package/dist/styles/recipes/kbd.css +26 -6
  70. package/dist/styles/recipes/morphing-panel.css +1 -1
  71. package/dist/styles/recipes/page-layout.css +18 -2
  72. package/dist/styles/recipes/pagination.css +8 -0
  73. package/dist/styles/recipes/popup-controls.css +9 -0
  74. package/dist/styles/recipes/popup-drawer.css +1 -0
  75. package/dist/styles/recipes/popup-lists.css +55 -6
  76. package/dist/styles/recipes/popup-motion.css +7 -1
  77. package/dist/styles/recipes/popup.css +10 -0
  78. package/dist/styles/recipes/range.css +16 -2
  79. package/dist/styles/recipes/sidebar-mobile.css +1 -1
  80. package/dist/styles/recipes/sidebar-parts.css +17 -1
  81. package/dist/styles/recipes/sidebar.css +11 -0
  82. package/dist/styles/recipes/tabs.css +41 -8
  83. package/dist/styles/recipes/timeline.css +25 -2
  84. package/dist/styles/recipes/tree.css +33 -0
  85. package/dist/styles/theme.css +11 -1
  86. package/dist/ui/alert-dialog.js +1 -1
  87. package/dist/ui/badge.js +1 -1
  88. package/dist/ui/calendar.js +9 -4
  89. package/dist/ui/card.d.ts +3 -1
  90. package/dist/ui/card.js +2 -1
  91. package/dist/ui/dialog.js +1 -1
  92. package/dist/ui/dropdown-menu.d.ts +1 -1
  93. package/dist/ui/dropdown-menu.js +23 -6
  94. package/dist/ui/dropzone.js +10 -11
  95. package/dist/ui/field.d.ts +6 -2
  96. package/dist/ui/field.js +4 -2
  97. package/dist/ui/morphing-panel.d.ts +5 -1
  98. package/dist/ui/morphing-panel.js +4 -2
  99. package/dist/ui/select.js +2 -2
  100. package/dist/ui/sidebar-mobile.js +1 -1
  101. package/dist/ui/sidebar-provider.d.ts +3 -1
  102. package/dist/ui/sidebar-provider.js +3 -2
  103. package/dist/ui/sidebar-resize-rail.js +1 -1
  104. package/dist/ui/sidebar.d.ts +6 -5
  105. package/dist/ui/sidebar.js +10 -9
  106. package/dist/ui/tabs.d.ts +1 -1
  107. package/dist/ui/tabs.js +2 -13
  108. package/dist/ui/timeline.js +1 -1
  109. package/dist/ui/typography.d.ts +11 -5
  110. package/dist/ui/typography.js +18 -6
  111. package/lint/biome-preset.json +3 -0
  112. package/lint/class-policy.mjs +17 -0
  113. package/lint/eslint-plugin.mjs +87 -0
  114. package/lint/grit-rules.mjs +40 -0
  115. package/lint/no-component-style-classes.grit +16 -0
  116. package/lint/no-raw-type-classes.grit +2 -3
  117. package/lint/write-grit-rules.mjs +19 -0
  118. package/package.json +11 -2
  119. package/dist/styles/type-scale.css +0 -72
@@ -4,6 +4,7 @@ import { controlEffectsAttribute } from "../skin.js";
4
4
  import { useSkin } from "../skin-provider.js";
5
5
  import { popupItemFocusProps } from "../lib/popup-item-focus.js";
6
6
  import { popupItemStructureClasses } from "../surface-variants.js";
7
+ import { isValidElement } from "react";
7
8
  import { jsx, jsxs } from "react/jsx-runtime";
8
9
  import { mergeProps } from "@base-ui/react/merge-props";
9
10
  import { Menu } from "@base-ui/react/menu";
@@ -15,21 +16,37 @@ function DropdownMenu({ children, ...props }) {
15
16
  children
16
17
  });
17
18
  }
18
- function DropdownMenuTrigger({ size = "sm", iconOnly = false, variant = "surface", className, children, disabled, ...props }) {
19
+ function explicitAppearance({ size, variant, iconOnly }) {
20
+ const attributes = {};
21
+ if (size) attributes["data-size"] = size;
22
+ if (variant) attributes["data-variant"] = variant;
23
+ if (iconOnly) attributes["data-icon-only"] = "true";
24
+ return attributes;
25
+ }
26
+ function DropdownMenuTrigger({ size, iconOnly, variant, className, ...props }) {
27
+ if (props.render !== void 0 && !(isValidElement(props.render) && typeof props.render.type === "string")) return /* @__PURE__ */ jsx(Menu.Trigger, {
28
+ "data-control-ui": "dropdown-menu",
29
+ "data-slot": "trigger",
30
+ ...explicitAppearance({
31
+ size,
32
+ variant,
33
+ iconOnly
34
+ }),
35
+ className,
36
+ ...props
37
+ });
19
38
  return /* @__PURE__ */ jsx(Menu.Trigger, {
20
39
  "data-control-ui": "dropdown-menu",
21
40
  "data-control-family": "button",
22
41
  "data-slot": "trigger",
23
42
  "data-control": "true",
24
- "data-size": size,
43
+ "data-size": size ?? "sm",
25
44
  "data-icon-only": iconOnly ? "true" : void 0,
26
- "data-variant": variant,
45
+ "data-variant": variant ?? "surface",
27
46
  "data-tone": "neutral",
28
47
  "data-shape": "default",
29
48
  className: cn("relative isolate inline-flex shrink-0 items-center justify-center overflow-visible", className),
30
- disabled,
31
- ...props,
32
- children
49
+ ...props
33
50
  });
34
51
  }
35
52
  function DropdownMenuContent({ className, children, side, sideOffset = 6, align = "start", alignOffset, collisionBoundary, collisionPadding, collisionAvoidance, ...props }) {
@@ -1,12 +1,12 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.js";
3
3
  import { Button } from "./button.js";
4
+ import { Item, ItemActions, ItemContent, ItemDescription, ItemMedia, ItemTitle } from "./item.js";
4
5
  import { DropzoneErrorCode, formatDropzoneFileSize } from "../lib/dropzone-validation.js";
5
6
  import { useDropzone } from "../hooks/use-dropzone.js";
6
- import { Item, ItemActions, ItemContent, ItemDescription, ItemMedia, ItemTitle } from "./item.js";
7
7
  import { createContext, useContext, useEffect, useState } from "react";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
- import { CircleAlert, CloudUpload, FileIcon, LoaderCircle, Search, XIcon } from "lucide-react";
9
+ import { CircleAlertIcon, CloudUpload, FileIcon, LoaderCircle, Search, XIcon } from "lucide-react";
10
10
  //#region src/ui/dropzone.tsx
11
11
  const DropzoneContext = createContext(null);
12
12
  const DropzoneFocusContext = createContext(null);
@@ -300,7 +300,7 @@ function DropzoneFeedback({ state }) {
300
300
  icon = /* @__PURE__ */ jsx(CloudUpload, { "aria-hidden": "true" });
301
301
  message = "Release to add files.";
302
302
  } else if (state === "reject") {
303
- icon = /* @__PURE__ */ jsx(CircleAlert, { "aria-hidden": "true" });
303
+ icon = /* @__PURE__ */ jsx(CircleAlertIcon, { "aria-hidden": "true" });
304
304
  message = "Some files are not accepted.";
305
305
  } else if (state === "unknown") {
306
306
  icon = /* @__PURE__ */ jsx(Search, { "aria-hidden": "true" });
@@ -341,19 +341,18 @@ function DefaultFile({ file }) {
341
341
  ]
342
342
  });
343
343
  }
344
+ const rejectedItemStyle = {
345
+ "--cui-item-media-foreground": "var(--destructive-text)",
346
+ "--cui-item-description-foreground": "var(--destructive-text)"
347
+ };
344
348
  function DefaultRejection({ rejection }) {
345
349
  return /* @__PURE__ */ jsxs(Item, {
346
350
  variant: "muted",
347
- children: [/* @__PURE__ */ jsx(ItemMedia, {
348
- "data-dropzone-invalid": "",
349
- children: /* @__PURE__ */ jsx(CircleAlert, { "aria-hidden": "true" })
350
- }), /* @__PURE__ */ jsxs(ItemContent, { children: [/* @__PURE__ */ jsx(ItemTitle, {
351
+ style: rejectedItemStyle,
352
+ children: [/* @__PURE__ */ jsx(ItemMedia, { children: /* @__PURE__ */ jsx(CircleAlertIcon, { "aria-hidden": "true" }) }), /* @__PURE__ */ jsxs(ItemContent, { children: [/* @__PURE__ */ jsx(ItemTitle, {
351
353
  className: "wrap-anywhere",
352
354
  children: rejection.file.name
353
- }), rejection.errors.map((error) => /* @__PURE__ */ jsx(ItemDescription, {
354
- "data-dropzone-invalid": "",
355
- children: error.message
356
- }, `${error.code}-${error.message}`))] })]
355
+ }), rejection.errors.map((error) => /* @__PURE__ */ jsx(ItemDescription, { children: error.message }, `${error.code}-${error.message}`))] })]
357
356
  });
358
357
  }
359
358
  function getDropzoneStatusMessage(context) {
@@ -31,7 +31,9 @@ export type FieldErrorProps = Omit<ComponentProps<"div">, "style"> & {
31
31
  match?: FieldErrorMatch;
32
32
  style?: CSSProperties & FieldKnobStyle;
33
33
  };
34
+ export type FieldGroupVariant = "default" | "card";
34
35
  export type FieldGroupProps = ComponentProps<"div"> & {
36
+ variant?: FieldGroupVariant;
35
37
  style?: CSSProperties & FieldKnobStyle;
36
38
  };
37
39
  export type FieldSeparatorProps = Omit<ComponentProps<"div">, "style"> & {
@@ -44,7 +46,9 @@ export type FieldItemProps = ComponentProps<"div"> & {
44
46
  export type FieldSetProps = Omit<ComponentProps<"fieldset">, "style"> & {
45
47
  style?: CSSProperties & FieldKnobStyle;
46
48
  };
49
+ export type FieldLegendVariant = "default" | "section";
47
50
  export type FieldLegendProps = Omit<ComponentProps<"div">, "style"> & {
51
+ variant?: FieldLegendVariant;
48
52
  style?: CSSProperties & FieldKnobStyle;
49
53
  };
50
54
  export declare function Field({ className, orientation, ...props }: FieldProps): import("react").JSX.Element;
@@ -55,9 +59,9 @@ type RefinedFieldControlProps = FieldControlProps & Pick<ComponentProps<typeof F
55
59
  export declare function FieldControl({ className, render, ...props }: RefinedFieldControlProps): import("react").JSX.Element;
56
60
  export declare function FieldDescription({ className, ...props }: FieldDescriptionProps): import("react").JSX.Element;
57
61
  export declare function FieldError({ className, ...props }: FieldErrorProps): import("react").JSX.Element;
58
- export declare function FieldGroup({ className, ...props }: FieldGroupProps): import("react").JSX.Element;
62
+ export declare function FieldGroup({ className, variant, ...props }: FieldGroupProps): import("react").JSX.Element;
59
63
  export declare function FieldSeparator({ children, className, ...props }: FieldSeparatorProps): import("react").JSX.Element;
60
64
  export declare function FieldItem({ className, ...props }: FieldItemProps): import("react").JSX.Element;
61
65
  export declare function FieldSet({ className, ...props }: FieldSetProps): import("react").JSX.Element;
62
- export declare function FieldLegend({ className, ...props }: FieldLegendProps): import("react").JSX.Element;
66
+ export declare function FieldLegend({ className, variant, ...props }: FieldLegendProps): import("react").JSX.Element;
63
67
  //#endregion
package/dist/ui/field.js CHANGED
@@ -103,12 +103,13 @@ function FieldError({ className, ...props }) {
103
103
  })
104
104
  });
105
105
  }
106
- function FieldGroup({ className, ...props }) {
106
+ function FieldGroup({ className, variant = "default", ...props }) {
107
107
  return /* @__PURE__ */ jsx("div", {
108
108
  "data-control-ui": "field",
109
109
  "data-control-family": "field",
110
110
  "data-field-kind": "field",
111
111
  "data-slot": "group",
112
+ "data-variant": variant,
112
113
  className: cn("group/field-group @container/field-group flex flex-col", className),
113
114
  ...props
114
115
  });
@@ -152,12 +153,13 @@ function FieldSet({ className, ...props }) {
152
153
  ...props
153
154
  });
154
155
  }
155
- function FieldLegend({ className, ...props }) {
156
+ function FieldLegend({ className, variant = "default", ...props }) {
156
157
  return /* @__PURE__ */ jsx(Fieldset.Legend, {
157
158
  "data-control-ui": "field",
158
159
  "data-control-family": "field",
159
160
  "data-field-kind": "field",
160
161
  "data-slot": "legend",
162
+ "data-variant": variant,
161
163
  className,
162
164
  ...props
163
165
  });
@@ -35,6 +35,10 @@ export declare function MorphingPanelTrigger({ render, className, children, ...p
35
35
  export declare function MorphingPanelContent({ autoFocus, ...props }: MorphingPanelContentProps): import("react").JSX.Element;
36
36
  export declare function MorphingPanelClose({ onClick, ...props }: MorphingPanelCloseProps): import("react").JSX.Element;
37
37
  export declare function MorphingPanelHeader({ className, ...props }: MorphingPanelPartProps): import("react").JSX.Element;
38
- export declare function MorphingPanelBody({ className, ...props }: MorphingPanelPartProps): import("react").JSX.Element;
38
+ export declare const morphingPanelBodyPaddings: readonly ["default", "none"];
39
+ export type MorphingPanelBodyPadding = (typeof morphingPanelBodyPaddings)[number];
40
+ export declare function MorphingPanelBody({ className, padding, ...props }: MorphingPanelPartProps & {
41
+ padding?: MorphingPanelBodyPadding;
42
+ }): import("react").JSX.Element;
39
43
  export declare function MorphingPanelFooter({ className, ...props }: MorphingPanelPartProps): import("react").JSX.Element;
40
44
  //#endregion
@@ -182,12 +182,14 @@ function MorphingPanelHeader({ className, ...props }) {
182
182
  className: cn("flex shrink-0 items-center", className)
183
183
  });
184
184
  }
185
- function MorphingPanelBody({ className, ...props }) {
185
+ const morphingPanelBodyPaddings = ["default", "none"];
186
+ function MorphingPanelBody({ className, padding = "default", ...props }) {
186
187
  return /* @__PURE__ */ jsx("div", {
187
188
  ...props,
188
189
  "data-control-ui": "morphing-panel",
189
190
  "data-control-family": "morphing-panel",
190
191
  "data-slot": "body",
192
+ "data-padding": padding,
191
193
  className: cn("min-h-0 min-w-0 flex-1 overflow-y-auto overscroll-contain", className)
192
194
  });
193
195
  }
@@ -201,4 +203,4 @@ function MorphingPanelFooter({ className, ...props }) {
201
203
  });
202
204
  }
203
205
  //#endregion
204
- export { MorphingPanel, MorphingPanelBody, MorphingPanelClose, MorphingPanelContent, MorphingPanelFooter, MorphingPanelHeader, MorphingPanelPositioner, MorphingPanelTrigger };
206
+ export { MorphingPanel, MorphingPanelBody, MorphingPanelClose, MorphingPanelContent, MorphingPanelFooter, MorphingPanelHeader, MorphingPanelPositioner, MorphingPanelTrigger, morphingPanelBodyPaddings };
package/dist/ui/select.js CHANGED
@@ -81,7 +81,7 @@ function SelectValue({ children, ...props }) {
81
81
  children
82
82
  });
83
83
  }
84
- function SelectContent({ className, children, alignItemWithTrigger = false, ...props }) {
84
+ function SelectContent({ className, children, alignItemWithTrigger, ...props }) {
85
85
  const skin = useSkin();
86
86
  return /* @__PURE__ */ jsx(Select$1.Portal, { children: /* @__PURE__ */ jsx(Select$1.Positioner, {
87
87
  "data-skin": skin.id,
@@ -89,7 +89,7 @@ function SelectContent({ className, children, alignItemWithTrigger = false, ...p
89
89
  side: "bottom",
90
90
  align: "start",
91
91
  sideOffset: 6,
92
- alignItemWithTrigger,
92
+ alignItemWithTrigger: alignItemWithTrigger ?? skin.selectAlignItemWithTrigger ?? false,
93
93
  className: "z-(--z-popup)",
94
94
  children: /* @__PURE__ */ jsx(Select$1.Popup, {
95
95
  "data-control-ui": "select",
@@ -14,7 +14,7 @@ function SidebarMobileNav({ variant = "bar", "aria-label": label = "Mobile navig
14
14
  "data-slot": "mobile-nav-container",
15
15
  "data-layout": layout,
16
16
  "data-variant": variant,
17
- className: "shrink-0 lg:hidden",
17
+ className: "shrink-0 sidebar:hidden",
18
18
  style,
19
19
  children: /* @__PURE__ */ jsx("nav", {
20
20
  ...props,
@@ -6,6 +6,7 @@ export type SidebarStyle = CSSProperties & SidebarKnobStyle & {
6
6
  "--sidebar-width-icon"?: string;
7
7
  };
8
8
  export type SidebarLayoutMode = "viewport" | "contained";
9
+ export type SidebarWrapperSurface = "transparent" | "background";
9
10
  type SidebarContextProps = {
10
11
  state: "expanded" | "collapsed";
11
12
  open: boolean;
@@ -55,9 +56,10 @@ export type SidebarProviderProps = Omit<ComponentProps<"div">, "style"> & {
55
56
  persistOpen?: boolean;
56
57
  keyboardShortcut?: string | null;
57
58
  layout?: SidebarLayoutMode;
59
+ surface?: SidebarWrapperSurface;
58
60
  skipLinkLabel?: string;
59
61
  contentId?: string;
60
62
  style?: SidebarStyle;
61
63
  };
62
- export declare function SidebarProvider({ defaultOpen, open: openProp, onOpenChange, defaultWidth, minWidth, maxWidth, width: widthProp, onWidthChange, persistOpen, keyboardShortcut, layout, skipLinkLabel, contentId: contentIdProp, ref, className, style, children, ...props }: SidebarProviderProps): import("react").JSX.Element;
64
+ export declare function SidebarProvider({ defaultOpen, open: openProp, onOpenChange, defaultWidth, minWidth, maxWidth, width: widthProp, onWidthChange, persistOpen, keyboardShortcut, layout, surface, skipLinkLabel, contentId: contentIdProp, ref, className, style, children, ...props }: SidebarProviderProps): import("react").JSX.Element;
63
65
  //#endregion
@@ -9,7 +9,7 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
9
9
  //#region src/ui/sidebar-provider.tsx
10
10
  const SIDEBAR_COOKIE_MAX_AGE = 604800;
11
11
  const SIDEBAR_KEYBOARD_SHORTCUT = "b";
12
- const SIDEBAR_MOBILE_BREAKPOINT = "--breakpoint-lg";
12
+ const SIDEBAR_MOBILE_BREAKPOINT = "--breakpoint-sidebar";
13
13
  const SidebarElementsContext = createContext(null);
14
14
  const SidebarSurfaceContext = createContext(null);
15
15
  function useSidebarElements() {
@@ -35,7 +35,7 @@ function useSidebar() {
35
35
  if (!context) throw new Error("useSidebar must be used within a SidebarProvider.");
36
36
  return context;
37
37
  }
38
- function SidebarProvider({ defaultOpen = true, open: openProp, onOpenChange, defaultWidth, minWidth = 224, maxWidth = 420, width: widthProp, onWidthChange, persistOpen = true, keyboardShortcut = SIDEBAR_KEYBOARD_SHORTCUT, layout = "viewport", skipLinkLabel = "Skip to content", contentId: contentIdProp, ref, className, style, children, ...props }) {
38
+ function SidebarProvider({ defaultOpen = true, open: openProp, onOpenChange, defaultWidth, minWidth = 224, maxWidth = 420, width: widthProp, onWidthChange, persistOpen = true, keyboardShortcut = SIDEBAR_KEYBOARD_SHORTCUT, layout = "viewport", surface = "transparent", skipLinkLabel = "Skip to content", contentId: contentIdProp, ref, className, style, children, ...props }) {
39
39
  const isMobile = useIsMobile(SIDEBAR_MOBILE_BREAKPOINT);
40
40
  const [openMobile, setOpenMobile] = useState(false);
41
41
  if (!isMobile && openMobile) setOpenMobile(false);
@@ -135,6 +135,7 @@ function SidebarProvider({ defaultOpen = true, open: openProp, onOpenChange, def
135
135
  "data-control-family": "sidebar",
136
136
  "data-slot": "wrapper",
137
137
  "data-layout": layout,
138
+ "data-surface": surface,
138
139
  style: wrapperStyle,
139
140
  className: cn("group/sidebar-wrapper relative flex w-full", layout === "contained" ? "h-full min-h-0 overflow-hidden" : "min-h-svh", className),
140
141
  children: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("a", {
@@ -183,7 +183,7 @@ function SidebarResizeRail({ className, ref, onPointerDown, onClick, onKeyDown,
183
183
  onPointerDown: handlePointerDown,
184
184
  onClick: handleClick,
185
185
  onKeyDown: handleKeyDown,
186
- className: cn("absolute inset-y-0 z-20 hidden cursor-col-resize touch-pan-y group-data-[side=left]:-end-1 group-data-[side=right]:-start-1 group-data-[side=left]:group-data-[collapsible=offcanvas]:-end-2 group-data-[side=right]:group-data-[collapsible=offcanvas]:-start-2 lg:block", className),
186
+ className: cn("absolute inset-y-0 z-20 hidden cursor-col-resize touch-pan-y group-data-[side=left]:-end-1 group-data-[side=right]:-start-1 group-data-[side=left]:group-data-[collapsible=offcanvas]:-end-2 group-data-[side=right]:group-data-[collapsible=offcanvas]:-start-2 sidebar:block", className),
187
187
  children: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
188
188
  ref: widthMeasureRef,
189
189
  "aria-hidden": true,
@@ -8,11 +8,11 @@ import { SidebarMobileNav, SidebarMobileNavItem, SidebarMobileNavProps, SidebarM
8
8
  import { SidebarLayoutMode, SidebarProvider, SidebarProviderProps, SidebarStyle, useSidebar } from "./sidebar-provider.js";
9
9
  import { CSSProperties, ComponentProps } from "react";
10
10
  //#region src/ui/sidebar.d.ts
11
- export type SidebarRailProps = (Omit<ComponentProps<"button">, "style"> & {
12
- resizable?: false;
11
+ export type SidebarRailProps = (SidebarResizeRailProps & {
12
+ resizable?: true;
13
+ }) | (Omit<ComponentProps<"button">, "style"> & {
14
+ resizable: false;
13
15
  style?: CSSProperties & SidebarKnobStyle;
14
- }) | (SidebarResizeRailProps & {
15
- resizable: true;
16
16
  });
17
17
  export type SidebarInsetProps = Omit<ComponentProps<"main">, "style" | "id"> & {
18
18
  render?: RenderProp<ComponentProps<"main">>;
@@ -33,7 +33,8 @@ export declare function SidebarTrigger({ className, onClick, size, label, ref, .
33
33
  }): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
34
34
  export declare function SidebarRail(props: SidebarRailProps): any;
35
35
  export declare function SidebarInset({ className, render, ref, ...props }: SidebarInsetProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
36
- export declare function SidebarHeader({ className, ...props }: ComponentProps<"div"> & {
36
+ export declare function SidebarHeader({ className, divider, ...props }: ComponentProps<"div"> & {
37
+ divider?: boolean;
37
38
  style?: CSSProperties & SidebarKnobStyle;
38
39
  }): import("react").JSX.Element;
39
40
  export declare function SidebarFooter({ className, ...props }: ComponentProps<"div"> & {
@@ -53,7 +53,7 @@ function SidebarSurface({ variant, mobileVariant = "sheet", label = "Navigation"
53
53
  "data-control-family": "sidebar",
54
54
  "data-slot": "container",
55
55
  tabIndex: -1,
56
- className: cn(layout === "contained" ? "absolute h-full" : "fixed h-svh", "inset-y-0 z-10 hidden w-(--sidebar-width) lg:flex", side === "left" ? "start-0" : "end-0", padded ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)", className),
56
+ className: cn(layout === "contained" ? "absolute h-full" : "fixed h-svh", "inset-y-0 z-10 hidden w-(--sidebar-width) sidebar:flex", side === "left" ? "start-0" : "end-0", padded ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)", className),
57
57
  children: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
58
58
  "data-control-ui": "sidebar",
59
59
  "data-control-family": "sidebar",
@@ -94,7 +94,7 @@ function SidebarSurface({ variant, mobileVariant = "sheet", label = "Navigation"
94
94
  })]
95
95
  });
96
96
  return /* @__PURE__ */ jsxs("div", {
97
- className: cn("group peer hidden lg:block", side === "right" && "order-last"),
97
+ className: cn("group peer hidden sidebar:block", side === "right" && "order-last"),
98
98
  "data-control-ui": "sidebar",
99
99
  "data-control-family": "sidebar",
100
100
  "data-slot": "root",
@@ -232,12 +232,12 @@ function SidebarRail(props) {
232
232
  const { isMobile } = useSidebar();
233
233
  const { railContainer } = useSidebarSurface();
234
234
  if (isMobile || !railContainer) return null;
235
- if (props.resizable) {
236
- const { resizable, ...resizeProps } = props;
237
- return createPortal(/* @__PURE__ */ jsx(SidebarResizeRail, { ...resizeProps }), railContainer);
235
+ if (props.resizable === false) {
236
+ const { resizable, ...toggleProps } = props;
237
+ return createPortal(/* @__PURE__ */ jsx(SidebarToggleRail, { ...toggleProps }), railContainer);
238
238
  }
239
- const { resizable, ...toggleProps } = props;
240
- return createPortal(/* @__PURE__ */ jsx(SidebarToggleRail, { ...toggleProps }), railContainer);
239
+ const { resizable, ...resizeProps } = props;
240
+ return createPortal(/* @__PURE__ */ jsx(SidebarResizeRail, { ...resizeProps }), railContainer);
241
241
  }
242
242
  function SidebarToggleRail({ className, ref, onClick, ...props }) {
243
243
  const { toggleSidebar, open } = useSidebar();
@@ -259,7 +259,7 @@ function SidebarToggleRail({ className, ref, onClick, ...props }) {
259
259
  if (!event.defaultPrevented) toggleSidebar();
260
260
  },
261
261
  title: props.title ?? "Toggle sidebar",
262
- className: cn("absolute inset-y-0 z-20 hidden -translate-x-1/2 rtl:translate-x-1/2 group-data-[side=left]:-end-4 group-data-[side=right]:start-0 lg:flex", "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize", "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", "group-data-[collapsible=offcanvas]:translate-x-0", className)
262
+ className: cn("absolute inset-y-0 z-20 hidden -translate-x-1/2 rtl:translate-x-1/2 group-data-[side=left]:-end-4 group-data-[side=right]:start-0 sidebar:flex", "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize", "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", "group-data-[collapsible=offcanvas]:translate-x-0", className)
263
263
  }
264
264
  });
265
265
  }
@@ -281,11 +281,12 @@ function SidebarInset({ className, render, ref, ...props }) {
281
281
  }
282
282
  });
283
283
  }
284
- function SidebarHeader({ className, ...props }) {
284
+ function SidebarHeader({ className, divider = false, ...props }) {
285
285
  return /* @__PURE__ */ jsx("div", {
286
286
  "data-control-ui": "sidebar",
287
287
  "data-control-family": "sidebar",
288
288
  "data-slot": "header",
289
+ "data-divider": divider ? "" : void 0,
289
290
  className: cn("flex flex-col", className),
290
291
  ...props
291
292
  });
package/dist/ui/tabs.d.ts CHANGED
@@ -37,7 +37,7 @@ export type TabsSurfaceProps = Omit<ComponentProps<"div">, "style"> & {
37
37
  };
38
38
  export declare function TabsSurface({ className, ...props }: TabsSurfaceProps): import("react").JSX.Element;
39
39
  export declare function Tabs<TValue extends string = string>({ className, onValueChange, children, ...props }: TabsProps<TValue>): import("react").JSX.Element;
40
- export declare function TabsList({ size, variant, className, children, style, ...props }: TabsListProps): import("react").JSX.Element;
40
+ export declare function TabsList({ size, variant, className, children, ...props }: TabsListProps): import("react").JSX.Element;
41
41
  export declare function TabsTab({ className, ...props }: TabsTabProps): import("react").JSX.Element;
42
42
  export declare function TabsPanel({ className, value, ref, ...props }: TabsPanelProps): import("react").JSX.Element;
43
43
  //#endregion
package/dist/ui/tabs.js CHANGED
@@ -109,12 +109,6 @@ function Tabs({ className, onValueChange, children, ...props }) {
109
109
  })
110
110
  });
111
111
  }
112
- const controlHeights = {
113
- xs: "var(--control-h-xs)",
114
- sm: "var(--control-h-sm)",
115
- md: "var(--control-h-md)",
116
- lg: "var(--control-h-lg)"
117
- };
118
112
  function countTabs(children) {
119
113
  return Children.toArray(children).reduce((count, child) => {
120
114
  if (!isValidElement(child)) return count;
@@ -123,12 +117,8 @@ function countTabs(children) {
123
117
  return count;
124
118
  }, 0);
125
119
  }
126
- function TabsList({ size = "sm", variant = "default", className, children, style, ...props }) {
120
+ function TabsList({ size = "sm", variant = "default", className, children, ...props }) {
127
121
  const isSingle = countTabs(children) === 1;
128
- const controlStyle = {
129
- "--_tabs-trigger-h": controlHeights[size],
130
- ...style
131
- };
132
122
  return /* @__PURE__ */ jsxs(Tabs$1.List, {
133
123
  "data-control-ui": "tabs",
134
124
  "data-control-family": "tabs",
@@ -137,13 +127,12 @@ function TabsList({ size = "sm", variant = "default", className, children, style
137
127
  "data-variant": variant,
138
128
  "data-single": isSingle ? "true" : void 0,
139
129
  className: cn(variant === "browser" && "gap-0 p-0 px-(--_tabs-browser-inset) pt-(--cui-tabs-list-padding)", className),
140
- style: controlStyle,
141
130
  ...props,
142
131
  children: [children, isSingle ? null : /* @__PURE__ */ jsx(Tabs$1.Indicator, {
143
132
  "data-control-ui": "tabs",
144
133
  "data-control-family": "tabs",
145
134
  "data-slot": "indicator",
146
- className: variant === "browser" ? "top-auto bottom-0 left-(--_tabs-browser-indicator-x) z-0 h-(--_tabs-trigger-h) w-(--_tabs-browser-indicator-width) transform-none" : void 0
135
+ className: variant === "browser" ? "top-auto bottom-0 left-(--_tabs-browser-indicator-x) z-0 h-(--cui-tabs-trigger-height) w-(--_tabs-browser-indicator-width) transform-none" : void 0
147
136
  })]
148
137
  });
149
138
  }
@@ -41,7 +41,7 @@ function TimelineSeparator({ className, ...props }) {
41
41
  "data-control-family": "timeline",
42
42
  "data-slot": "separator",
43
43
  ...props,
44
- className: cn("absolute top-5 bottom-0 start-[calc(0.5rem-0.5px)] col-start-1 w-px group-last/timeline-item:hidden", className)
44
+ className: cn("absolute top-5 bottom-0 col-start-1 group-last/timeline-item:hidden", className)
45
45
  });
46
46
  }
47
47
  function TimelineContent({ className, ...props }) {
@@ -30,21 +30,27 @@ export type TypeSize = keyof typeof sizeClassNames;
30
30
  export type TypeTone = keyof typeof toneClassNames;
31
31
  export type TextWeight = keyof typeof weightClassNames;
32
32
  export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
33
- export type HeadingProps = ComponentProps<"h1"> & {
33
+ type TypeFace = {
34
+ /** Sets the text in the monospace face, for code, paths, and identifiers. */
35
+ mono?: boolean;
36
+ /** Uses tabular figures so changing numbers keep their width. */
37
+ numeric?: boolean;
38
+ };
39
+ export type HeadingProps = ComponentProps<"h1"> & TypeFace & {
34
40
  level: HeadingLevel;
35
41
  size?: TypeSize;
36
42
  weight?: TextWeight;
37
43
  tone?: TypeTone;
38
44
  };
39
- export declare function headingClassName({ level, size, weight, tone, className }: HeadingProps): string;
40
- export declare function Heading({ level, size, weight, tone, className, ...props }: HeadingProps): import("react").JSX.Element;
45
+ export declare function headingClassName({ level, size, weight, tone, mono, numeric, className }: HeadingProps): string;
46
+ export declare function Heading({ level, size, weight, tone, mono, numeric, className, ...props }: HeadingProps): import("react").JSX.Element;
41
47
  export type TextTag = "p" | "span" | "div" | "small" | "strong" | "em" | "code" | "li" | "dt" | "dd" | "figcaption" | "legend" | "blockquote";
42
- export type TextProps = HTMLAttributes<HTMLElement> & {
48
+ export type TextProps = HTMLAttributes<HTMLElement> & TypeFace & {
43
49
  ref?: Ref<HTMLElement>;
44
50
  as?: TextTag;
45
51
  size?: TypeSize;
46
52
  weight?: TextWeight;
47
53
  tone?: TypeTone;
48
54
  };
49
- export declare function Text({ as: Tag, size, weight, tone, className, ref, ...props }: TextProps): import("react").JSX.Element;
55
+ export declare function Text({ as: Tag, size, weight, tone, mono, numeric, className, ref, ...props }: TextProps): import("react").JSX.Element;
50
56
  //#endregion
@@ -35,10 +35,17 @@ const headingSizeByLevel = {
35
35
  5: "heading-4",
36
36
  6: "heading-4"
37
37
  };
38
- function headingClassName({ level, size = headingSizeByLevel[level], weight, tone = "default", className }) {
39
- return cn(sizeClassNames[size], weight ? weightClassNames[weight] : void 0, toneClassNames[tone], className);
38
+ function faceClassNames({ mono, numeric }) {
39
+ return cn(mono && "font-mono", numeric && "tabular-nums");
40
40
  }
41
- function Heading({ level, size, weight, tone, className, ...props }) {
41
+ function headingClassName({ level, size = headingSizeByLevel[level], weight, tone = "default", mono, numeric, className }) {
42
+ return cn(sizeClassNames[size], weight ? weightClassNames[weight] : void 0, toneClassNames[tone], faceClassNames({
43
+ mono,
44
+ numeric
45
+ }), className);
46
+ }
47
+ const programmaticFocusTarget = "[&[tabindex='-1']]:outline-none";
48
+ function Heading({ level, size, weight, tone, mono, numeric, className, ...props }) {
42
49
  const Tag = `h${level}`;
43
50
  return /* @__PURE__ */ jsx(Tag, {
44
51
  "data-control-ui": "typography",
@@ -49,7 +56,9 @@ function Heading({ level, size, weight, tone, className, ...props }) {
49
56
  size,
50
57
  weight,
51
58
  tone,
52
- className
59
+ mono,
60
+ numeric,
61
+ className: cn(programmaticFocusTarget, className)
53
62
  }),
54
63
  ...props
55
64
  });
@@ -58,13 +67,16 @@ function attachRef(ref, node) {
58
67
  if (typeof ref === "function") return ref(node);
59
68
  if (ref) ref.current = node;
60
69
  }
61
- function Text({ as: Tag = "span", size = "body", weight, tone = "default", className, ref, ...props }) {
70
+ function Text({ as: Tag = "span", size = "body", weight, tone = "default", mono, numeric, className, ref, ...props }) {
62
71
  return /* @__PURE__ */ jsx(Tag, {
63
72
  ref: ref ? (node) => attachRef(ref, node) : void 0,
64
73
  "data-control-ui": "typography",
65
74
  "data-control-family": "typography",
66
75
  "data-slot": "text",
67
- className: cn(sizeClassNames[size], weight ? weightClassNames[weight] : void 0, toneClassNames[tone], className),
76
+ className: cn(sizeClassNames[size], weight ? weightClassNames[weight] : void 0, toneClassNames[tone], faceClassNames({
77
+ mono,
78
+ numeric
79
+ }), className),
68
80
  ...props
69
81
  });
70
82
  }
@@ -0,0 +1,3 @@
1
+ {
2
+ "plugins": ["./node_modules/@ctrl-ui/react/lint/no-component-style-classes.grit"]
3
+ }
@@ -0,0 +1,17 @@
1
+ const controlUiImportPath = `(?:@ctrl-ui/react|[^'"]*components/control-ui)/`;
2
+
3
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4
+
5
+ export function controlUiImportPattern(importSources = []) {
6
+ return `(?:${[controlUiImportPath, ...importSources.map(escapeRegExp)].join("|")})`;
7
+ }
8
+
9
+ export const componentStyleClass = String.raw`[^\s"'\x60]!(?:[\s"'\x60]|$)|(?:^|[\s"'\x60])![\w\[-]|(?:^|[^\w-])(?:p[xytrblse]?-|bg-[\w\[]|text-(?:foreground|muted|primary|secondary|accent|destructive|success|warning|info|card|popover|sidebar|background|canvas|current|inherit|transparent|white|black|[a-z]+-\d{2,3}|\[(?:#|oklch|rgb|hsl|lab|lch|color|var))|font-|leading-|tracking-|border|divide-|rounded|shadow|ring\b|ring-|outline|opacity-|backdrop-|fill-|stroke-|decoration-|underline|line-through|italic|uppercase|lowercase|capitalize|tabular-nums|(?:hover|focus|focus-visible|focus-within|active|disabled|open|checked|dark|placeholder|aria-[^:\s]*|data-[^:\s]*|group-[^:\s]*|peer-[^:\s]*|has-[^:\s]*|\*|\[&[^\s]*?\]):)`;
10
+
11
+ 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[^\]]*\]`;
12
+
13
+ export const componentStyleMessage =
14
+ "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).";
15
+
16
+ export const rawTypeMessage =
17
+ "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.";
@@ -0,0 +1,87 @@
1
+ import { componentStyleClass, componentStyleMessage, controlUiImportPattern, rawTypeClass, rawTypeMessage } from "./class-policy.mjs";
2
+
3
+ const componentStyle = new RegExp(componentStyleClass);
4
+ const rawType = new RegExp(rawTypeClass);
5
+
6
+ function stringsIn(node) {
7
+ if (Array.isArray(node)) return node.flatMap(stringsIn);
8
+ if (!node || typeof node !== "object") return [];
9
+ if (node.type === "Literal") return typeof node.value === "string" ? [node.value] : [];
10
+ if (node.type === "TemplateElement") return [node.value.cooked ?? node.value.raw];
11
+ return Object.entries(node).flatMap(([key, child]) => (key === "parent" ? [] : stringsIn(child)));
12
+ }
13
+
14
+ function classNameMatches(attribute, pattern) {
15
+ if (attribute.name.name !== "className" || !attribute.value) return false;
16
+ return stringsIn(attribute.value).some((text) => pattern.test(text));
17
+ }
18
+
19
+ function elementRootName(name) {
20
+ if (name.type === "JSXIdentifier") return name.name;
21
+ if (name.type === "JSXMemberExpression") return elementRootName(name.object);
22
+ return undefined;
23
+ }
24
+
25
+ function controlUiLocalNames(program, controlUiImport) {
26
+ const names = new Set();
27
+ for (const statement of program.body) {
28
+ if (statement.type !== "ImportDeclaration" || statement.importKind === "type") continue;
29
+ if (!controlUiImport.test(statement.source.value)) continue;
30
+ for (const specifier of statement.specifiers) names.add(specifier.local.name);
31
+ }
32
+ return names;
33
+ }
34
+
35
+ const noComponentStyleClasses = {
36
+ meta: {
37
+ type: "problem",
38
+ docs: { description: "Keep className on Control UI components to layout; the component owns its look." },
39
+ messages: { componentStyle: componentStyleMessage },
40
+ schema: [
41
+ {
42
+ type: "object",
43
+ properties: { importSources: { type: "array", items: { type: "string" } } },
44
+ additionalProperties: false,
45
+ },
46
+ ],
47
+ },
48
+ create(context) {
49
+ const controlUiImport = new RegExp(`^${controlUiImportPattern(context.options[0]?.importSources)}`);
50
+ let controlUiNames = new Set();
51
+ return {
52
+ Program(program) {
53
+ controlUiNames = controlUiLocalNames(program, controlUiImport);
54
+ },
55
+ JSXAttribute(attribute) {
56
+ if (!controlUiNames.has(elementRootName(attribute.parent.name))) return;
57
+ if (classNameMatches(attribute, componentStyle)) context.report({ node: attribute, messageId: "componentStyle" });
58
+ },
59
+ };
60
+ },
61
+ };
62
+
63
+ const noRawTypeClasses = {
64
+ meta: {
65
+ type: "problem",
66
+ docs: { description: "Size text through the Control UI type scale instead of text-* classes." },
67
+ messages: { rawType: rawTypeMessage },
68
+ schema: [],
69
+ },
70
+ create(context) {
71
+ return {
72
+ JSXAttribute(attribute) {
73
+ if (classNameMatches(attribute, rawType)) context.report({ node: attribute, messageId: "rawType" });
74
+ },
75
+ };
76
+ },
77
+ };
78
+
79
+ const plugin = {
80
+ meta: { name: "control-ui" },
81
+ rules: {
82
+ "no-component-style-classes": noComponentStyleClasses,
83
+ "no-raw-type-classes": noRawTypeClasses,
84
+ },
85
+ };
86
+
87
+ export default plugin;