@vvedantb/ui 0.17.0 → 0.18.0

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 (130) hide show
  1. package/README.md +253 -3
  2. package/package.json +13 -5
  3. package/shell/src/SidebarResize.tsx +1 -4
  4. package/src/ai-elements/__snapshots__/queue.test.tsx.snap +11 -0
  5. package/src/ai-elements/activity-question-utils.ts +26 -0
  6. package/src/ai-elements/activity-question.tsx +159 -0
  7. package/src/ai-elements/activity-shared.tsx +459 -0
  8. package/src/ai-elements/activity-step-detail.tsx +115 -0
  9. package/src/ai-elements/activity-step-label.ts +981 -0
  10. package/src/ai-elements/activity-tasks-utils.ts +199 -0
  11. package/src/ai-elements/activity-tasks.tsx +770 -0
  12. package/src/ai-elements/artifact.tsx +162 -52
  13. package/src/ai-elements/attachments.tsx +62 -32
  14. package/src/ai-elements/code-block.tsx +76 -22
  15. package/src/ai-elements/context.tsx +370 -0
  16. package/src/ai-elements/conversation.tsx +186 -62
  17. package/src/ai-elements/file-chip.tsx +53 -0
  18. package/src/ai-elements/message.tsx +161 -50
  19. package/src/ai-elements/model-picker-content.tsx +505 -0
  20. package/src/ai-elements/model-picker-types.ts +56 -0
  21. package/src/ai-elements/model-picker.tsx +301 -0
  22. package/src/ai-elements/model-selector.tsx +102 -21
  23. package/src/ai-elements/plan.tsx +130 -0
  24. package/src/ai-elements/prompt-input-settings.tsx +144 -0
  25. package/src/ai-elements/prompt-input-speech.tsx +189 -0
  26. package/src/ai-elements/prompt-input-traits.tsx +281 -0
  27. package/src/ai-elements/prompt-input.tsx +320 -134
  28. package/src/ai-elements/provider-icon.tsx +133 -0
  29. package/src/ai-elements/queue.tsx +293 -9
  30. package/src/ai-elements/shimmer.tsx +72 -1
  31. package/src/ai-elements/simple-model-ladder.tsx +108 -0
  32. package/src/ai-elements/test-results.tsx +510 -0
  33. package/src/ai-elements/trace.tsx +243 -0
  34. package/src/ai-elements/use-streaming-disclosure.ts +50 -0
  35. package/src/ai-elements/web-preview.tsx +349 -0
  36. package/src/composer/input-area/ComposerSlashMenu.tsx +1 -1
  37. package/src/index.ts +66 -12
  38. package/src/kibo/avatar-stack.tsx +40 -0
  39. package/src/kibo/contribution-graph.tsx +539 -0
  40. package/src/kibo/cursor.tsx +72 -0
  41. package/src/kibo/gantt-features.tsx +439 -0
  42. package/src/kibo/gantt-header.tsx +181 -0
  43. package/src/kibo/gantt-provider.tsx +657 -0
  44. package/src/kibo/gantt-sidebar.tsx +157 -0
  45. package/src/kibo/gantt-timeline.tsx +154 -0
  46. package/src/kibo/index.ts +151 -0
  47. package/src/kibo/kanban.tsx +338 -0
  48. package/src/kibo/kanbanDropTarget.ts +25 -0
  49. package/src/kibo/list.tsx +162 -0
  50. package/src/kibo/theme-switcher.tsx +109 -0
  51. package/src/kibo/video-player.tsx +237 -0
  52. package/src/markdown/Markdown.tsx +112 -0
  53. package/src/markdown/MarkdownBlockquote.tsx +45 -0
  54. package/src/markdown/MarkdownCodeBlock.tsx +197 -0
  55. package/src/markdown/MarkdownInlineCode.tsx +40 -0
  56. package/src/markdown/MarkdownLink.tsx +92 -0
  57. package/src/markdown/codeFence.ts +57 -0
  58. package/src/markdown/codeHighlight.ts +138 -0
  59. package/src/markdown/incrementalBlocks.ts +182 -0
  60. package/src/markdown/index.ts +3 -0
  61. package/src/markdown/markdownClipboard.ts +178 -0
  62. package/src/markdown/mermaid.ts +155 -0
  63. package/src/markdown/rehypeGithubAlerts.ts +80 -0
  64. package/src/motion/presets.ts +24 -0
  65. package/src/ui/__snapshots__/alt-wrappers.test.tsx.snap +19 -0
  66. package/src/ui/__snapshots__/sonner-dom.test.tsx.snap +5 -0
  67. package/src/ui/_menu-parts.tsx +29 -0
  68. package/src/ui/accordion.alt.tsx +46 -53
  69. package/src/ui/accordion.tsx +59 -10
  70. package/src/ui/avatar.tsx +56 -0
  71. package/src/ui/badge.tsx +45 -21
  72. package/src/ui/border-beam.tsx +97 -0
  73. package/src/ui/button-group.tsx +50 -21
  74. package/src/ui/button.tsx +67 -45
  75. package/src/ui/calendar.tsx +48 -5
  76. package/src/ui/card.tsx +77 -8
  77. package/src/ui/carousel.tsx +60 -0
  78. package/src/ui/centered-spinner.tsx +30 -0
  79. package/src/ui/checkbox.tsx +34 -16
  80. package/src/ui/clear-input.alt.tsx +33 -75
  81. package/src/ui/clear-input.tsx +140 -15
  82. package/src/ui/collapsible.tsx +40 -2
  83. package/src/ui/command.tsx +143 -70
  84. package/src/ui/context-menu.tsx +159 -93
  85. package/src/ui/crossfade-icon.tsx +123 -0
  86. package/src/ui/dialog.tsx +154 -70
  87. package/src/ui/dropdown-menu.tsx +177 -117
  88. package/src/ui/hover-card.tsx +59 -17
  89. package/src/ui/input-group.alt.tsx +83 -83
  90. package/src/ui/input-group.tsx +129 -58
  91. package/src/ui/input.tsx +13 -4
  92. package/src/ui/label.tsx +11 -8
  93. package/src/ui/list-row.tsx +232 -0
  94. package/src/ui/loading-state.tsx +215 -0
  95. package/src/ui/popover.tsx +56 -23
  96. package/src/ui/progress.tsx +28 -16
  97. package/src/ui/refresh-spin-icon.tsx +41 -0
  98. package/src/ui/search-input.tsx +66 -15
  99. package/src/ui/select.tsx +171 -100
  100. package/src/ui/separator.tsx +23 -14
  101. package/src/ui/slider.tsx +21 -7
  102. package/src/ui/sonner.tsx +68 -30
  103. package/src/ui/spinner.tsx +229 -11
  104. package/src/ui/status-dot.tsx +84 -0
  105. package/src/ui/surface.tsx +49 -0
  106. package/src/ui/switch.tsx +29 -15
  107. package/src/ui/table.tsx +38 -16
  108. package/src/ui/tabs.tsx +123 -35
  109. package/src/ui/tabsSliding.ts +33 -1
  110. package/src/ui/textarea.tsx +13 -4
  111. package/src/ui/tooltip.tsx +50 -14
  112. package/src/ui/ui-config.tsx +149 -0
  113. package/src/utils/cat-color.ts +123 -0
  114. package/src/utils/dayjs.ts +6 -0
  115. package/src/utils/gesture.ts +59 -0
  116. package/src/utils/runtimeVisibility.ts +167 -0
  117. package/src/utils/sharedClock.ts +96 -0
  118. package/src/utils/surface-radius.ts +16 -0
  119. package/src/utils/useDragSensors.ts +58 -0
  120. package/tokens/examples/tokens-moved/Fixture.tsx +283 -0
  121. package/tokens/examples/tokens-moved/README.md +20 -0
  122. package/tokens/examples/tokens-moved/index.html +12 -0
  123. package/tokens/examples/tokens-moved/main.tsx +6 -0
  124. package/tokens/examples/tokens-moved/package.json +14 -0
  125. package/tokens/examples/tokens-moved/styles.css +12 -0
  126. package/tokens/examples/tokens-moved/tsconfig.json +14 -0
  127. package/tokens/examples/tokens-moved/vite.config.ts +11 -0
  128. package/tokens/src/css/border-beam.css +116 -0
  129. package/tokens/src/css/compat-shadcn-rgb.css +6 -0
  130. package/tokens/src/css/markdown.css +318 -0
@@ -5,10 +5,23 @@ import { cva, type VariantProps } from "class-variance-authority";
5
5
  import { cn } from "../utils/cn";
6
6
  import { fieldFocusWithinClass } from "./_field-classes";
7
7
  import { Button, type ButtonProps } from "./button";
8
- import { Input } from "./input";
9
- import { Textarea } from "./textarea";
8
+ import {
9
+ InputGroupAddon,
10
+ InputGroupInput,
11
+ InputGroupText,
12
+ InputGroupTextarea,
13
+ } from "./input-group";
10
14
 
11
- const InputGroup = React.forwardRef<
15
+ // The `Alt` look on the main parts: per-instance `classes` replace the main
16
+ // classes (and any `UiConfig.InputGroup` entry), and `role` / `data-slot`
17
+ // set to `undefined` drop the main attributes, so the markup is the
18
+ // pre-wrapper one. Two parts cannot go through the main ones without changing
19
+ // their markup: the root puts `data-input-group` (the addon's focus hook)
20
+ // before `class`, and the button has no default `type`; they stay a plain
21
+ // `<div>` and a `Button`.
22
+
23
+ /** @deprecated Use `InputGroup`. Kept for existing consumers; removed in a later minor after they move. */
24
+ const InputGroupAlt = React.forwardRef<
12
25
  HTMLDivElement,
13
26
  React.HTMLAttributes<HTMLDivElement> & { error?: boolean }
14
27
  >(({ className, error, ...props }, ref) => (
@@ -28,10 +41,11 @@ const InputGroup = React.forwardRef<
28
41
  {...props}
29
42
  />
30
43
  ));
31
- InputGroup.displayName = "InputGroup";
44
+ InputGroupAlt.displayName = "InputGroupAlt";
32
45
 
33
- const inputGroupAddonVariants = cva("flex items-center", {
34
- variants: {
46
+ const addonClasses = {
47
+ addon: {
48
+ base: "flex items-center",
35
49
  align: {
36
50
  "inline-start": "flex-row ps-3",
37
51
  "inline-end": "flex-row pe-3",
@@ -39,35 +53,38 @@ const inputGroupAddonVariants = cva("flex items-center", {
39
53
  "block-end": "w-full flex-row px-3 py-2",
40
54
  },
41
55
  },
42
- defaultVariants: {
43
- align: "inline-start",
44
- },
45
- });
56
+ };
46
57
 
47
- const InputGroupAddon = React.forwardRef<
48
- HTMLDivElement,
49
- React.HTMLAttributes<HTMLDivElement> &
50
- VariantProps<typeof inputGroupAddonVariants>
51
- >(({ className, align, onClick, ...props }, ref) => (
52
- <div
53
- ref={ref}
54
- data-align={align}
55
- className={cn(inputGroupAddonVariants({ align }), className)}
56
- onClick={(e) => {
57
- if (onClick) {
58
- onClick(e);
59
- return;
60
- }
61
- const group = (e.currentTarget as HTMLElement).closest(
62
- "[data-input-group]",
63
- );
64
- const input = group?.querySelector("textarea, input");
65
- if (input instanceof HTMLElement) input.focus();
66
- }}
67
- {...props}
68
- />
69
- ));
70
- InputGroupAddon.displayName = "InputGroupAddon";
58
+ /** `align={null}` (allowed by the old variant props) adds no align classes. */
59
+ const addonClassesNoAlign = { addon: { base: "flex items-center", align: { "inline-start": "" } } };
60
+
61
+ /** Focuses the group's first textarea or input (the click target of an addon). */
62
+ function focusGroupField(event: React.MouseEvent<HTMLDivElement>) {
63
+ const group = event.currentTarget.closest("[data-input-group]");
64
+ const field = group?.querySelector("textarea, input");
65
+ if (field instanceof HTMLElement) field.focus();
66
+ }
67
+
68
+ /** @deprecated Use `InputGroupAddon`. Kept for existing consumers; removed in a later minor after they move. */
69
+ function InputGroupAddonAlt({
70
+ align,
71
+ onClick,
72
+ ...props
73
+ }: React.ComponentProps<"div"> & {
74
+ align?: "inline-start" | "inline-end" | "block-start" | "block-end" | null;
75
+ }) {
76
+ return (
77
+ <InputGroupAddon
78
+ role={undefined}
79
+ data-slot={undefined}
80
+ align={align}
81
+ data-align={align ?? undefined}
82
+ classes={align === null ? addonClassesNoAlign : addonClasses}
83
+ onClick={onClick ?? focusGroupField}
84
+ {...props}
85
+ />
86
+ );
87
+ }
71
88
 
72
89
  const inputGroupButtonVariants = cva("shrink-0 shadow-none", {
73
90
  variants: {
@@ -83,7 +100,8 @@ const inputGroupButtonVariants = cva("shrink-0 shadow-none", {
83
100
  },
84
101
  });
85
102
 
86
- const InputGroupButton = React.forwardRef<
103
+ /** @deprecated Use `InputGroupButton`. Kept for existing consumers; removed in a later minor after they move. */
104
+ const InputGroupButtonAlt = React.forwardRef<
87
105
  HTMLButtonElement,
88
106
  ButtonProps & VariantProps<typeof inputGroupButtonVariants>
89
107
  >(({ className, size, variant = "ghost", ...props }, ref) => (
@@ -94,58 +112,40 @@ const InputGroupButton = React.forwardRef<
94
112
  {...props}
95
113
  />
96
114
  ));
97
- InputGroupButton.displayName = "InputGroupButton";
115
+ InputGroupButtonAlt.displayName = "InputGroupButtonAlt";
98
116
 
99
- function InputGroupText({
100
- className,
101
- ...props
102
- }: React.HTMLAttributes<HTMLSpanElement>) {
103
- return (
104
- <span
105
- className={cn(
106
- "flex items-center text-sm text-muted-foreground/95",
107
- className,
108
- )}
109
- {...props}
110
- />
111
- );
117
+ const textClasses = { text: "flex items-center text-sm text-muted-foreground/95" };
118
+
119
+ /** @deprecated Use `InputGroupText`. Kept for existing consumers; removed in a later minor after they move. */
120
+ function InputGroupTextAlt(props: React.HTMLAttributes<HTMLSpanElement>) {
121
+ return <InputGroupText classes={textClasses} {...props} />;
112
122
  }
113
123
 
114
- const InputGroupInput = React.forwardRef<
115
- HTMLInputElement,
116
- React.ComponentProps<typeof Input>
117
- >(({ className, ...props }, ref) => (
118
- <Input
119
- ref={ref}
120
- className={cn(
121
- "h-full flex-1 rounded-none border-0 bg-transparent px-3 py-2 shadow-none focus-visible:border-0 focus-visible:ring-0",
122
- className,
123
- )}
124
- {...props}
125
- />
126
- ));
127
- InputGroupInput.displayName = "InputGroupInput";
124
+ const inputClasses = {
125
+ input:
126
+ "h-full flex-1 rounded-none border-0 bg-transparent px-3 py-2 shadow-none focus-visible:border-0 focus-visible:ring-0",
127
+ };
128
128
 
129
- const InputGroupTextarea = React.forwardRef<
130
- HTMLTextAreaElement,
131
- React.ComponentProps<typeof Textarea>
132
- >(({ className, ...props }, ref) => (
133
- <Textarea
134
- ref={ref}
135
- className={cn(
136
- "flex-1 resize-none rounded-none border-0 bg-transparent px-3 py-2.5 shadow-none focus-visible:border-0 focus-visible:ring-0 field-sizing-content",
137
- className,
138
- )}
139
- {...props}
140
- />
141
- ));
142
- InputGroupTextarea.displayName = "InputGroupTextarea";
129
+ /** @deprecated Use `InputGroupInput`. Kept for existing consumers; removed in a later minor after they move. */
130
+ function InputGroupInputAlt(props: React.ComponentProps<"input">) {
131
+ return <InputGroupInput data-slot={undefined} classes={inputClasses} {...props} />;
132
+ }
133
+
134
+ const textareaClasses = {
135
+ textarea:
136
+ "flex-1 resize-none rounded-none border-0 bg-transparent px-3 py-2.5 shadow-none focus-visible:border-0 focus-visible:ring-0 field-sizing-content",
137
+ };
138
+
139
+ /** @deprecated Use `InputGroupTextarea`. Kept for existing consumers; removed in a later minor after they move. */
140
+ function InputGroupTextareaAlt(props: React.ComponentProps<"textarea">) {
141
+ return <InputGroupTextarea data-slot={undefined} classes={textareaClasses} {...props} />;
142
+ }
143
143
 
144
144
  export {
145
- InputGroup,
146
- InputGroupAddon,
147
- InputGroupButton,
148
- InputGroupText,
149
- InputGroupInput,
150
- InputGroupTextarea,
145
+ InputGroupAlt,
146
+ InputGroupAddonAlt,
147
+ InputGroupButtonAlt,
148
+ InputGroupTextAlt,
149
+ InputGroupInputAlt,
150
+ InputGroupTextareaAlt,
151
151
  };
@@ -3,64 +3,105 @@
3
3
  import * as React from "react";
4
4
  import { cva, type VariantProps } from "class-variance-authority";
5
5
 
6
- import { cn } from "../utils/cn";
7
6
  import { Button } from "./button";
8
7
  import { Input } from "./input";
9
8
  import { Textarea } from "./textarea";
9
+ import { useCn, useUiConfig } from "./ui-config";
10
+
11
+ type InputGroupAlign = "inline-start" | "inline-end" | "block-start" | "block-end";
12
+ type InputGroupButtonSize = "xs" | "sm" | "icon-xs" | "icon-sm";
13
+
14
+ type InputGroupClasses = {
15
+ root?: string;
16
+ addon?: { base?: string; align?: Partial<Record<InputGroupAlign, string>> };
17
+ button?: { base?: string; size?: Partial<Record<InputGroupButtonSize, string>> };
18
+ text?: string;
19
+ input?: string;
20
+ textarea?: string;
21
+ };
22
+
23
+ /**
24
+ * `UiConfig.InputGroup`: each given class string replaces the built-in one.
25
+ * `InputGroupAddon`, `InputGroupText`, `InputGroupInput` and
26
+ * `InputGroupTextarea` also take a `classes` prop with their own key, which
27
+ * wins over the config for that instance.
28
+ */
29
+ export interface InputGroupConfig {
30
+ classes?: InputGroupClasses;
31
+ }
32
+
33
+ const inputGroupRootClasses = [
34
+ "group/input-group relative flex w-full flex-wrap items-center rounded-field border border-border bg-field-background text-sm transition-[border-color,box-shadow] duration-200 ease-smooth outline-hidden",
35
+ "h-10 min-w-0 has-[>textarea]:h-auto",
36
+ "[&>input]:has-[>[data-align=inline-start]]:pl-2",
37
+ "[&>input]:has-[>[data-align=inline-end]]:pr-2",
38
+ "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col [&>input]:has-[>[data-align=block-start]]:pb-3",
39
+ "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col [&>input]:has-[>[data-align=block-end]]:pt-3",
40
+ "has-[[data-slot=input-group-control]:focus-visible]:border-focus-border has-[[data-slot=input-group-control]:focus-visible]:ring-1 has-[[data-slot=input-group-control]:focus-visible]:ring-focus-ring",
41
+ "has-[[data-slot][aria-invalid=true]]:border-danger-border has-[[data-slot][aria-invalid=true]]:ring-danger-ring",
42
+ ];
10
43
 
11
44
  function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
45
+ const root = useUiConfig("InputGroup")?.classes?.root;
46
+ const merge = useCn();
12
47
  return (
13
48
  <div
14
49
  data-slot="input-group"
15
50
  role="group"
16
- className={cn(
17
- "group/input-group relative flex w-full flex-wrap items-center rounded-field border border-border bg-field-background text-sm transition-[border-color,box-shadow] duration-200 ease-smooth outline-hidden",
18
- "h-10 min-w-0 has-[>textarea]:h-auto",
19
- "[&>input]:has-[>[data-align=inline-start]]:pl-2",
20
- "[&>input]:has-[>[data-align=inline-end]]:pr-2",
21
- "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col [&>input]:has-[>[data-align=block-start]]:pb-3",
22
- "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col [&>input]:has-[>[data-align=block-end]]:pt-3",
23
- "has-[[data-slot=input-group-control]:focus-visible]:border-focus-border has-[[data-slot=input-group-control]:focus-visible]:ring-1 has-[[data-slot=input-group-control]:focus-visible]:ring-focus-ring",
24
- "has-[[data-slot][aria-invalid=true]]:border-danger-border has-[[data-slot][aria-invalid=true]]:ring-danger-ring",
25
- className,
26
- )}
51
+ className={merge(root ?? inputGroupRootClasses, className)}
27
52
  {...props}
28
53
  />
29
54
  );
30
55
  }
31
56
 
32
- const inputGroupAddonVariants = cva(
33
- "text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-md group-data-[disabled=true]/input-group:opacity-50",
34
- {
35
- variants: {
36
- align: {
37
- "inline-start":
38
- "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
39
- "inline-end":
40
- "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
41
- "block-start":
42
- "order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
43
- "block-end":
44
- "order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
45
- },
46
- },
47
- defaultVariants: {
48
- align: "inline-start",
49
- },
57
+ const inputGroupAddonBaseClasses =
58
+ "text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-md group-data-[disabled=true]/input-group:opacity-50";
59
+
60
+ const inputGroupAddonAlignClasses = {
61
+ "inline-start":
62
+ "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
63
+ "inline-end":
64
+ "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
65
+ "block-start":
66
+ "order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
67
+ "block-end":
68
+ "order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
69
+ };
70
+
71
+ const inputGroupAddonVariants = cva(inputGroupAddonBaseClasses, {
72
+ variants: {
73
+ align: inputGroupAddonAlignClasses,
74
+ },
75
+ defaultVariants: {
76
+ align: "inline-start",
50
77
  },
51
- );
78
+ });
52
79
 
53
80
  function InputGroupAddon({
54
81
  className,
55
82
  align = "inline-start",
83
+ classes: instance,
56
84
  ...props
57
- }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
85
+ }: React.ComponentProps<"div"> &
86
+ VariantProps<typeof inputGroupAddonVariants> & { classes?: Pick<InputGroupClasses, "addon"> }) {
87
+ const config = useUiConfig("InputGroup")?.classes;
88
+ const classes = instance?.addon ?? config?.addon;
89
+ const merge = useCn();
90
+ const a = align ?? "inline-start";
58
91
  return (
59
92
  <div
60
93
  role="group"
61
94
  data-slot="input-group-addon"
62
95
  data-align={align}
63
- className={cn(inputGroupAddonVariants({ align }), className)}
96
+ className={
97
+ classes
98
+ ? merge(
99
+ classes.base ?? inputGroupAddonBaseClasses,
100
+ classes.align?.[a] ?? inputGroupAddonAlignClasses[a],
101
+ className,
102
+ )
103
+ : merge(inputGroupAddonVariants({ align }), className)
104
+ }
64
105
  onClick={(e) => {
65
106
  if ((e.target as HTMLElement).closest("button")) {
66
107
  return;
@@ -72,22 +113,23 @@ function InputGroupAddon({
72
113
  );
73
114
  }
74
115
 
75
- const inputGroupButtonVariants = cva(
76
- "text-sm shadow-none flex gap-2 items-center",
77
- {
78
- variants: {
79
- size: {
80
- xs: "h-7 gap-1 rounded-lg px-2 text-xs [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
81
- sm: "h-8 rounded-lg px-2.5 gap-1.5 text-xs has-[>svg]:px-2.5",
82
- "icon-xs": "size-7 rounded-lg p-0 has-[>svg]:p-0",
83
- "icon-sm": "size-8 rounded-lg p-0 has-[>svg]:p-0",
84
- },
85
- },
86
- defaultVariants: {
87
- size: "xs",
88
- },
116
+ const inputGroupButtonBaseClasses = "text-sm shadow-none flex gap-2 items-center";
117
+
118
+ const inputGroupButtonSizeClasses = {
119
+ xs: "h-7 gap-1 rounded-lg px-2 text-xs [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
120
+ sm: "h-8 rounded-lg px-2.5 gap-1.5 text-xs has-[>svg]:px-2.5",
121
+ "icon-xs": "size-7 rounded-lg p-0 has-[>svg]:p-0",
122
+ "icon-sm": "size-8 rounded-lg p-0 has-[>svg]:p-0",
123
+ };
124
+
125
+ const inputGroupButtonVariants = cva(inputGroupButtonBaseClasses, {
126
+ variants: {
127
+ size: inputGroupButtonSizeClasses,
128
+ },
129
+ defaultVariants: {
130
+ size: "xs",
89
131
  },
90
- );
132
+ });
91
133
 
92
134
  function InputGroupButton({
93
135
  className,
@@ -97,22 +139,41 @@ function InputGroupButton({
97
139
  ...props
98
140
  }: Omit<React.ComponentProps<typeof Button>, "size"> &
99
141
  VariantProps<typeof inputGroupButtonVariants>) {
142
+ const classes = useUiConfig("InputGroup")?.classes?.button;
143
+ const merge = useCn();
144
+ const s = size ?? "xs";
100
145
  return (
101
146
  <Button
102
147
  type={type}
103
148
  data-size={size}
104
149
  variant={variant}
105
- className={cn(inputGroupButtonVariants({ size }), className)}
150
+ className={
151
+ classes
152
+ ? merge(
153
+ classes.base ?? inputGroupButtonBaseClasses,
154
+ classes.size?.[s] ?? inputGroupButtonSizeClasses[s],
155
+ className,
156
+ )
157
+ : merge(inputGroupButtonVariants({ size }), className)
158
+ }
106
159
  {...props}
107
160
  />
108
161
  );
109
162
  }
110
163
 
111
- function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
164
+ function InputGroupText({
165
+ className,
166
+ classes,
167
+ ...props
168
+ }: React.ComponentProps<"span"> & { classes?: Pick<InputGroupClasses, "text"> }) {
169
+ const config = useUiConfig("InputGroup")?.classes;
170
+ const base = classes?.text ?? config?.text;
171
+ const merge = useCn();
112
172
  return (
113
173
  <span
114
- className={cn(
115
- "text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
174
+ className={merge(
175
+ base ??
176
+ "text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
116
177
  className,
117
178
  )}
118
179
  {...props}
@@ -122,13 +183,18 @@ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
122
183
 
123
184
  function InputGroupInput({
124
185
  className,
186
+ classes,
125
187
  ...props
126
- }: React.ComponentProps<"input">) {
188
+ }: React.ComponentProps<"input"> & { classes?: Pick<InputGroupClasses, "input"> }) {
189
+ const config = useUiConfig("InputGroup")?.classes;
190
+ const base = classes?.input ?? config?.input;
191
+ const merge = useCn();
127
192
  return (
128
193
  <Input
129
194
  data-slot="input-group-control"
130
- className={cn(
131
- "flex-1 rounded-none border-0 bg-transparent shadow-none focus:outline-hidden focus-visible:ring-0",
195
+ className={merge(
196
+ base ??
197
+ "flex-1 rounded-none border-0 bg-transparent shadow-none focus:outline-hidden focus-visible:ring-0",
132
198
  className,
133
199
  )}
134
200
  {...props}
@@ -138,13 +204,18 @@ function InputGroupInput({
138
204
 
139
205
  function InputGroupTextarea({
140
206
  className,
207
+ classes,
141
208
  ...props
142
- }: React.ComponentProps<"textarea">) {
209
+ }: React.ComponentProps<"textarea"> & { classes?: Pick<InputGroupClasses, "textarea"> }) {
210
+ const config = useUiConfig("InputGroup")?.classes;
211
+ const base = classes?.textarea ?? config?.textarea;
212
+ const merge = useCn();
143
213
  return (
144
214
  <Textarea
145
215
  data-slot="input-group-control"
146
- className={cn(
147
- "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus:outline-hidden focus-visible:ring-0",
216
+ className={merge(
217
+ base ??
218
+ "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus:outline-hidden focus-visible:ring-0",
148
219
  className,
149
220
  )}
150
221
  {...props}
package/src/ui/input.tsx CHANGED
@@ -1,15 +1,24 @@
1
1
  import * as React from "react";
2
- import { cn } from "../utils/cn";
3
2
  import { fieldFocusClass } from "./_field-classes";
3
+ import { useCn, useUiConfig } from "./ui-config";
4
+
5
+ /** `UiConfig.Input`: `classes.base` replaces the built-in field classes. */
6
+ export interface InputConfig {
7
+ classes?: { base?: string };
8
+ }
4
9
 
5
10
  const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
6
11
  ({ className, type, ...props }, ref) => {
12
+ const base = useUiConfig("Input")?.classes?.base;
13
+ const merge = useCn();
7
14
  return (
8
15
  <input
9
16
  type={type}
10
- className={cn(
11
- "flex h-10 w-full max-sm:min-w-0 rounded-field border border-border bg-field-background px-3.5 py-2 text-base sm:text-sm transition-[border-color,box-shadow,background-color] duration-200 ease-smooth file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-field-placeholder disabled:cursor-not-allowed disabled:opacity-50",
12
- fieldFocusClass,
17
+ className={merge(
18
+ base ?? [
19
+ "flex h-10 w-full max-sm:min-w-0 rounded-field border border-border bg-field-background px-3.5 py-2 text-base sm:text-sm transition-[border-color,box-shadow,background-color] duration-200 ease-smooth file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-field-placeholder disabled:cursor-not-allowed disabled:opacity-50",
20
+ fieldFocusClass,
21
+ ],
13
22
  className,
14
23
  )}
15
24
  ref={ref}
package/src/ui/label.tsx CHANGED
@@ -1,6 +1,11 @@
1
1
  import * as React from "react";
2
2
  import * as LabelPrimitive from "@radix-ui/react-label";
3
- import { cn } from "../utils/cn";
3
+ import { useCn, useUiConfig } from "./ui-config";
4
+
5
+ export interface LabelConfig {
6
+ /** Replaces the built-in classes of a part. */
7
+ classes?: { root?: string };
8
+ }
4
9
 
5
10
  const labelClass =
6
11
  "text-sm font-medium leading-none tracking-normal text-foreground/90 peer-disabled:cursor-not-allowed peer-disabled:opacity-70";
@@ -8,13 +13,11 @@ const labelClass =
8
13
  const Label = React.forwardRef<
9
14
  React.ComponentRef<typeof LabelPrimitive.Root>,
10
15
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
11
- >(({ className, ...props }, ref) => (
12
- <LabelPrimitive.Root
13
- ref={ref}
14
- className={cn(labelClass, className)}
15
- {...props}
16
- />
17
- ));
16
+ >(({ className, ...props }, ref) => {
17
+ const root = useUiConfig("Label")?.classes?.root;
18
+ const merge = useCn();
19
+ return <LabelPrimitive.Root ref={ref} className={merge(root ?? labelClass, className)} {...props} />;
20
+ });
18
21
  Label.displayName = LabelPrimitive.Root.displayName;
19
22
 
20
23
  export { Label };