@vvedantb/ui 0.17.1 → 0.18.1

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 +251 -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/speech-input.tsx +5 -3
  33. package/src/ai-elements/test-results.tsx +510 -0
  34. package/src/ai-elements/trace.tsx +243 -0
  35. package/src/ai-elements/use-streaming-disclosure.ts +50 -0
  36. package/src/ai-elements/web-preview.tsx +349 -0
  37. package/src/composer/input-area/ComposerSlashMenu.tsx +1 -1
  38. package/src/index.ts +66 -12
  39. package/src/kibo/avatar-stack.tsx +40 -0
  40. package/src/kibo/contribution-graph.tsx +539 -0
  41. package/src/kibo/cursor.tsx +72 -0
  42. package/src/kibo/gantt-features.tsx +439 -0
  43. package/src/kibo/gantt-header.tsx +181 -0
  44. package/src/kibo/gantt-provider.tsx +657 -0
  45. package/src/kibo/gantt-sidebar.tsx +157 -0
  46. package/src/kibo/gantt-timeline.tsx +154 -0
  47. package/src/kibo/index.ts +151 -0
  48. package/src/kibo/kanban.tsx +338 -0
  49. package/src/kibo/kanbanDropTarget.ts +25 -0
  50. package/src/kibo/list.tsx +162 -0
  51. package/src/kibo/theme-switcher.tsx +109 -0
  52. package/src/kibo/video-player.tsx +237 -0
  53. package/src/markdown/Markdown.tsx +112 -0
  54. package/src/markdown/MarkdownBlockquote.tsx +45 -0
  55. package/src/markdown/MarkdownCodeBlock.tsx +197 -0
  56. package/src/markdown/MarkdownInlineCode.tsx +40 -0
  57. package/src/markdown/MarkdownLink.tsx +92 -0
  58. package/src/markdown/codeFence.ts +57 -0
  59. package/src/markdown/codeHighlight.ts +138 -0
  60. package/src/markdown/incrementalBlocks.ts +182 -0
  61. package/src/markdown/index.ts +3 -0
  62. package/src/markdown/markdownClipboard.ts +178 -0
  63. package/src/markdown/mermaid.ts +155 -0
  64. package/src/markdown/rehypeGithubAlerts.ts +80 -0
  65. package/src/motion/presets.ts +24 -0
  66. package/src/ui/__snapshots__/alt-wrappers.test.tsx.snap +19 -0
  67. package/src/ui/__snapshots__/sonner-dom.test.tsx.snap +5 -0
  68. package/src/ui/_menu-parts.tsx +29 -0
  69. package/src/ui/accordion.alt.tsx +46 -53
  70. package/src/ui/accordion.tsx +59 -10
  71. package/src/ui/avatar.tsx +56 -0
  72. package/src/ui/badge.tsx +45 -21
  73. package/src/ui/border-beam.tsx +97 -0
  74. package/src/ui/button-group.tsx +50 -21
  75. package/src/ui/button.tsx +67 -45
  76. package/src/ui/calendar.tsx +48 -5
  77. package/src/ui/card.tsx +77 -8
  78. package/src/ui/carousel.tsx +60 -0
  79. package/src/ui/centered-spinner.tsx +30 -0
  80. package/src/ui/checkbox.tsx +34 -16
  81. package/src/ui/clear-input.alt.tsx +33 -75
  82. package/src/ui/clear-input.tsx +140 -15
  83. package/src/ui/collapsible.tsx +40 -2
  84. package/src/ui/command.tsx +143 -70
  85. package/src/ui/context-menu.tsx +159 -93
  86. package/src/ui/crossfade-icon.tsx +123 -0
  87. package/src/ui/dialog.tsx +154 -70
  88. package/src/ui/dropdown-menu.tsx +177 -117
  89. package/src/ui/hover-card.tsx +59 -17
  90. package/src/ui/input-group.alt.tsx +83 -83
  91. package/src/ui/input-group.tsx +129 -58
  92. package/src/ui/input.tsx +13 -4
  93. package/src/ui/label.tsx +11 -8
  94. package/src/ui/list-row.tsx +232 -0
  95. package/src/ui/loading-state.tsx +215 -0
  96. package/src/ui/popover.tsx +56 -23
  97. package/src/ui/progress.tsx +28 -16
  98. package/src/ui/refresh-spin-icon.tsx +41 -0
  99. package/src/ui/search-input.tsx +66 -15
  100. package/src/ui/select.tsx +171 -100
  101. package/src/ui/separator.tsx +23 -14
  102. package/src/ui/slider.tsx +21 -7
  103. package/src/ui/sonner.tsx +68 -30
  104. package/src/ui/spinner.tsx +229 -11
  105. package/src/ui/status-dot.tsx +84 -0
  106. package/src/ui/surface.tsx +49 -0
  107. package/src/ui/switch.tsx +29 -15
  108. package/src/ui/table.tsx +38 -16
  109. package/src/ui/tabs.tsx +123 -35
  110. package/src/ui/tabsSliding.ts +33 -1
  111. package/src/ui/textarea.tsx +13 -4
  112. package/src/ui/tooltip.tsx +50 -14
  113. package/src/ui/ui-config.tsx +149 -0
  114. package/src/utils/cat-color.ts +123 -0
  115. package/src/utils/dayjs.ts +6 -0
  116. package/src/utils/gesture.ts +59 -0
  117. package/src/utils/runtimeVisibility.ts +167 -0
  118. package/src/utils/sharedClock.ts +96 -0
  119. package/src/utils/surface-radius.ts +16 -0
  120. package/src/utils/useDragSensors.ts +58 -0
  121. package/tokens/examples/tokens-moved/Fixture.tsx +283 -0
  122. package/tokens/examples/tokens-moved/README.md +20 -0
  123. package/tokens/examples/tokens-moved/index.html +12 -0
  124. package/tokens/examples/tokens-moved/main.tsx +6 -0
  125. package/tokens/examples/tokens-moved/package.json +14 -0
  126. package/tokens/examples/tokens-moved/styles.css +12 -0
  127. package/tokens/examples/tokens-moved/tsconfig.json +14 -0
  128. package/tokens/examples/tokens-moved/vite.config.ts +11 -0
  129. package/tokens/src/css/border-beam.css +116 -0
  130. package/tokens/src/css/markdown.css +318 -0
@@ -6,6 +6,12 @@ import useEmblaCarousel, {
6
6
  import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
7
7
  import { cn } from "../utils/cn";
8
8
  import { Button } from "./button";
9
+ import { useCn, useUiConfig } from "./ui-config";
10
+
11
+ /** `UiConfig.Carousel`: `classes` replace the built-in `CarouselDots` part classes. */
12
+ export interface CarouselConfig {
13
+ classes?: { dots?: string; dot?: string; dotActive?: string; dotInactive?: string };
14
+ }
9
15
  type CarouselApi = UseEmblaCarouselType[1];
10
16
  type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
11
17
  type CarouselOptions = UseCarouselParameters[0];
@@ -23,6 +29,11 @@ type CarouselContextProps = {
23
29
  scrollNext: () => void;
24
30
  canScrollPrev: boolean;
25
31
  canScrollNext: boolean;
32
+ /** Index of the selected snap. */
33
+ selectedIndex: number;
34
+ /** Embla's snap positions, one per page. */
35
+ scrollSnaps: number[];
36
+ scrollTo: (index: number) => void;
26
37
  } & CarouselProps;
27
38
  const CarouselContext = React.createContext<CarouselContextProps | null>(null);
28
39
  function useCarousel() {
@@ -50,10 +61,18 @@ function Carousel({
50
61
  );
51
62
  const [canScrollPrev, setCanScrollPrev] = React.useState(false);
52
63
  const [canScrollNext, setCanScrollNext] = React.useState(false);
64
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
65
+ const [scrollSnaps, setScrollSnaps] = React.useState<number[]>([]);
53
66
  const onSelect = (api: CarouselApi) => {
54
67
  if (!api) return;
55
68
  setCanScrollPrev(api.canScrollPrev());
56
69
  setCanScrollNext(api.canScrollNext());
70
+ setSelectedIndex(api.selectedScrollSnap());
71
+ // keep the old array when nothing moved, so this can run on every render
72
+ const next = api.scrollSnapList();
73
+ setScrollSnaps((prev) =>
74
+ prev.length === next.length && prev.every((snap, i) => snap === next[i]) ? prev : next,
75
+ );
57
76
  };
58
77
  const scrollPrev = () => {
59
78
  api?.scrollPrev();
@@ -61,6 +80,9 @@ function Carousel({
61
80
  const scrollNext = () => {
62
81
  api?.scrollNext();
63
82
  };
83
+ const scrollTo = (index: number) => {
84
+ api?.scrollTo(index);
85
+ };
64
86
  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
65
87
  if (event.key === "ArrowLeft") {
66
88
  event.preventDefault();
@@ -95,6 +117,9 @@ function Carousel({
95
117
  scrollNext,
96
118
  canScrollPrev,
97
119
  canScrollNext,
120
+ selectedIndex,
121
+ scrollSnaps,
122
+ scrollTo,
98
123
  }}
99
124
  >
100
125
  <div
@@ -201,6 +226,39 @@ function CarouselNext({
201
226
  </Button>
202
227
  );
203
228
  }
229
+ /** One dot per snap; the selected one is wider. Renders nothing for a single page. */
230
+ function CarouselDots({ className }: { className?: string }) {
231
+ const { scrollSnaps, selectedIndex, scrollTo } = useCarousel();
232
+ const classes = useUiConfig("Carousel")?.classes;
233
+ const merge = useCn();
234
+
235
+ if (scrollSnaps.length <= 1) return null;
236
+
237
+ return (
238
+ <div
239
+ data-slot="carousel-dots"
240
+ className={merge(classes?.dots ?? "flex justify-center gap-1.5 pt-2", className)}
241
+ >
242
+ {scrollSnaps.map((_, index) => (
243
+ <button
244
+ key={index}
245
+ type="button"
246
+ className={merge(
247
+ classes?.dot ??
248
+ "h-1.5 cursor-pointer rounded-full transition-[background-color,transform] duration-150 ease-out active:scale-90",
249
+ index === selectedIndex
250
+ ? (classes?.dotActive ?? "w-4 bg-primary")
251
+ : (classes?.dotInactive ??
252
+ "w-1.5 bg-muted-foreground/30 hover:bg-muted-foreground/50"),
253
+ )}
254
+ onClick={() => scrollTo(index)}
255
+ aria-label={`Go to slide ${index + 1}`}
256
+ />
257
+ ))}
258
+ </div>
259
+ );
260
+ }
261
+
204
262
  export {
205
263
  type CarouselApi,
206
264
  Carousel,
@@ -208,4 +266,6 @@ export {
208
266
  CarouselItem,
209
267
  CarouselPrevious,
210
268
  CarouselNext,
269
+ CarouselDots,
270
+ useCarousel,
211
271
  };
@@ -0,0 +1,30 @@
1
+ import { Spinner } from "./spinner";
2
+ import { useCn } from "./ui-config";
3
+
4
+ /**
5
+ * A loading state for a pane, list or page: {@link Spinner} (with the app's
6
+ * `UiConfig.Spinner` mark, if any), centred in whatever box it is given.
7
+ * `label` names the spinner for assistive tech.
8
+ *
9
+ * A spinner says "wait" as well as a skeleton does, and never has to be
10
+ * re-shaped when the content changes.
11
+ */
12
+ export function CenteredSpinner({
13
+ label,
14
+ className,
15
+ ...props
16
+ }: React.ComponentProps<"div"> & { label: string }) {
17
+ const merge = useCn();
18
+ return (
19
+ <div
20
+ aria-busy="true"
21
+ className={merge(
22
+ "flex min-h-0 w-full flex-1 items-center justify-center p-6",
23
+ className,
24
+ )}
25
+ {...props}
26
+ >
27
+ <Spinner aria-label={label} />
28
+ </div>
29
+ );
30
+ }
@@ -2,26 +2,44 @@ import * as React from "react";
2
2
  import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
3
3
  import { IconCheck } from "@tabler/icons-react";
4
4
  import { cn } from "../utils/cn";
5
+ import { useCn, useUiConfig } from "./ui-config";
6
+
7
+ /** `UiConfig.Checkbox`: `classes` replace built-in part classes; `icon` is the default for the prop. */
8
+ export interface CheckboxConfig {
9
+ classes?: { root?: string; indicator?: string };
10
+ icon?: React.ReactNode;
11
+ }
12
+
13
+ type CheckboxProps = React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> & {
14
+ /** Shown in the indicator while checked. Default a 14px check icon. */
15
+ icon?: React.ReactNode;
16
+ };
5
17
 
6
18
  const Checkbox = React.forwardRef<
7
19
  React.ComponentRef<typeof CheckboxPrimitive.Root>,
8
- React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
9
- >(({ className, ...props }, ref) => (
10
- <CheckboxPrimitive.Root
11
- ref={ref}
12
- className={cn(
13
- "peer relative h-4 w-4 shrink-0 rounded-[4px] border border-border bg-field-background transition-[background-color,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground before:absolute before:inset-[-12px] before:content-['']",
14
- className,
15
- )}
16
- {...props}
17
- >
18
- <CheckboxPrimitive.Indicator
19
- className={cn("flex items-center justify-center text-current")}
20
+ CheckboxProps
21
+ >(({ className, icon, ...props }, ref) => {
22
+ const config = useUiConfig("Checkbox");
23
+ const merge = useCn();
24
+ return (
25
+ <CheckboxPrimitive.Root
26
+ ref={ref}
27
+ className={merge(
28
+ config?.classes?.root ??
29
+ "peer relative h-4 w-4 shrink-0 rounded-[4px] border border-border bg-field-background transition-[background-color,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground before:absolute before:inset-[-12px] before:content-['']",
30
+ className,
31
+ )}
32
+ {...props}
20
33
  >
21
- <IconCheck className="h-3.5 w-3.5" />
22
- </CheckboxPrimitive.Indicator>
23
- </CheckboxPrimitive.Root>
24
- ));
34
+ <CheckboxPrimitive.Indicator
35
+ className={config?.classes?.indicator ?? cn("flex items-center justify-center text-current")}
36
+ >
37
+ {icon ?? config?.icon ?? <IconCheck className="h-3.5 w-3.5" />}
38
+ </CheckboxPrimitive.Indicator>
39
+ </CheckboxPrimitive.Root>
40
+ );
41
+ });
25
42
  Checkbox.displayName = CheckboxPrimitive.Root.displayName;
26
43
 
27
44
  export { Checkbox };
45
+ export type { CheckboxProps };
@@ -1,78 +1,36 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
- import { IconX } from "@tabler/icons-react";
4
- import { cn } from "../utils/cn";
5
3
  import { fieldFocusClass } from "./_field-classes";
6
- export interface ClearInputProps
7
- extends Omit<React.ComponentProps<"input">, "value" | "onChange"> {
8
- value: string;
9
- onChange: (value: string) => void;
10
- wrapperClassName?: string;
11
- /** Icon or adornment positioned in the field's left gutter. */
12
- leading?: React.ReactNode;
13
- clearLabel?: string;
14
- }
15
- const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
16
- (
17
- {
18
- value,
19
- onChange,
20
- wrapperClassName,
21
- leading,
22
- clearLabel = "Clear",
23
- className,
24
- placeholder,
25
- ...props
26
- },
27
- ref,
28
- ) => {
29
- const inputRef = React.useRef<HTMLInputElement | null>(null);
30
- const mergedRef = (node: HTMLInputElement | null) => {
31
- inputRef.current = node;
32
- if (typeof ref === "function") {
33
- ref(node);
34
- } else if (ref) {
35
- ref.current = node;
36
- }
37
- };
38
- const hasValue = value.length > 0;
39
- return (
40
- <div className={cn("relative", wrapperClassName)}>
41
- {leading}
42
- <input
43
- ref={mergedRef}
44
- type="text"
45
- value={value}
46
- onChange={(event) => {
47
- onChange(event.target.value);
48
- }}
49
- placeholder={placeholder}
50
- className={cn(
51
- "flex h-10 w-full rounded-field border border-border bg-field-background px-3.5 py-2 text-sm transition-[border-color,background-color] duration-200 ease-smooth placeholder:text-field-placeholder disabled:cursor-not-allowed disabled:opacity-50",
52
- fieldFocusClass,
53
- "pr-9",
54
- className,
55
- )}
56
- {...props}
57
- />
58
- <button
59
- type="button"
60
- tabIndex={hasValue ? 0 : -1}
61
- className={cn(
62
- "absolute right-2 top-1/2 inline-flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-[opacity,background-color,color,transform] duration-150 ease-out active:duration-100 active:ease-out active:scale-[0.97] hover:bg-surface-tertiary hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring before:absolute before:inset-[-8px] before:content-['']",
63
- hasValue ? "opacity-100" : "pointer-events-none opacity-0",
64
- )}
65
- aria-hidden={!hasValue}
66
- aria-label={clearLabel}
67
- onClick={() => {
68
- onChange("");
69
- inputRef.current?.focus({ preventScroll: true });
70
- }}
71
- >
72
- <IconX size={14} stroke={1.75} />
73
- </button>
74
- </div>
75
- );
76
- },
77
- );
78
- export { ClearInput };
4
+ import { ClearInput, type ClearInputProps } from "./clear-input";
5
+
6
+ const altClasses = {
7
+ wrapper: "relative",
8
+ input: `flex h-10 w-full rounded-field border border-border bg-field-background px-3.5 py-2 text-sm transition-[border-color,background-color] duration-200 ease-smooth placeholder:text-field-placeholder disabled:cursor-not-allowed disabled:opacity-50 ${fieldFocusClass}`,
9
+ button:
10
+ "absolute right-2 top-1/2 inline-flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-[opacity,background-color,color,transform] duration-150 ease-out active:duration-100 active:ease-out active:scale-[0.97] hover:bg-surface-tertiary hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring before:absolute before:inset-[-8px] before:content-['']",
11
+ };
12
+
13
+ /**
14
+ * The `Alt` look on `ClearInput`'s `animation="none"` path: the clear button
15
+ * stays mounted (`clearButton="always"`), per-instance `classes` replace the
16
+ * main classes (and any `UiConfig.ClearInput` entry), and an explicit
17
+ * `placeholder={undefined}` keeps the input free of the main `placeholder=""`.
18
+ *
19
+ * @deprecated Use `ClearInput`. Kept for existing consumers; removed in a later minor after they move.
20
+ */
21
+ const ClearInputAlt = React.forwardRef<
22
+ HTMLInputElement,
23
+ Omit<ClearInputProps, "animation" | "clearButton" | "classes">
24
+ >(({ placeholder, ...props }, ref) => (
25
+ <ClearInput
26
+ ref={ref}
27
+ animation="none"
28
+ clearButton="always"
29
+ classes={altClasses}
30
+ placeholder={placeholder}
31
+ {...props}
32
+ />
33
+ ));
34
+ ClearInputAlt.displayName = "ClearInputAlt";
35
+
36
+ export { ClearInputAlt };
@@ -2,9 +2,23 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import { IconX } from "@tabler/icons-react";
5
- import { cn } from "../utils/cn";
5
+ import { assignRef } from "../utils/ref";
6
6
  import { fieldFocusClass } from "./_field-classes";
7
7
  import { runClearInputDissolve } from "./clearInputDissolve";
8
+ import { useCn, useUiConfig } from "./ui-config";
9
+
10
+ type ClearInputAnimation = "dissolve" | "none";
11
+
12
+ /**
13
+ * `UiConfig.ClearInput`: `animation` is the default for the prop. `classes`
14
+ * replace the built-in input and button classes, and the wrapper's `relative`
15
+ * in the `none` layout (the `dissolve` wrapper keeps its `t-clear` hooks and
16
+ * adds `classes.wrapper`). The `classes` prop wins over it key by key.
17
+ */
18
+ export interface ClearInputConfig {
19
+ animation?: ClearInputAnimation;
20
+ classes?: { wrapper?: string; input?: string; button?: string };
21
+ }
8
22
 
9
23
  export interface ClearInputProps extends Omit<
10
24
  React.ComponentProps<"input">,
@@ -16,9 +30,123 @@ export interface ClearInputProps extends Omit<
16
30
  /** Icon or adornment positioned in the field's left gutter. */
17
31
  leading?: React.ReactNode;
18
32
  clearLabel?: string;
33
+ /**
34
+ * `dissolve` (default): the text dissolves before the value clears (needs
35
+ * `clear-input.css`). `none`: clear at once, focus the input, and render
36
+ * only the wrapper, input and clear button.
37
+ */
38
+ animation?: ClearInputAnimation;
39
+ /**
40
+ * With `animation="none"` only. `"filled"` (default): the clear button
41
+ * mounts while there is a value. `"always"`: it stays mounted so the field
42
+ * never reflows; while empty it is hidden (`pointer-events-none opacity-0`,
43
+ * `tabIndex=-1`, `aria-hidden`), while filled it shows (`opacity-100`,
44
+ * `tabIndex=0`), and the input always reserves `pr-9`.
45
+ */
46
+ clearButton?: "filled" | "always";
47
+ /** Replaces the wrapper, input and button classes for this instance (each key wins over `UiConfig.ClearInput.classes`). */
48
+ classes?: ClearInputConfig["classes"];
19
49
  }
20
50
 
51
+ const inputClasses =
52
+ "flex h-10 w-full 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 placeholder:text-field-placeholder disabled:cursor-not-allowed disabled:opacity-50";
53
+
54
+ const buttonClasses =
55
+ "absolute right-2 top-1/2 z-4 inline-flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-[background-color,color,transform] duration-100 ease-out active:scale-[0.92] hover:bg-surface-tertiary/50 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring";
56
+
21
57
  const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
58
+ ({ animation, clearButton, classes, ...props }, ref) => {
59
+ const config = useUiConfig("ClearInput");
60
+ const merged = { ...config?.classes, ...classes };
61
+ if ((animation ?? config?.animation) === "none") {
62
+ return <PlainClearInput ref={ref} classes={merged} clearButton={clearButton} {...props} />;
63
+ }
64
+ return <DissolveClearInput ref={ref} classes={merged} {...props} />;
65
+ },
66
+ );
67
+ ClearInput.displayName = "ClearInput";
68
+
69
+ type ClearInputPartsProps = Omit<ClearInputProps, "animation" | "clearButton">;
70
+
71
+ const PlainClearInput = React.forwardRef<
72
+ HTMLInputElement,
73
+ ClearInputPartsProps & Pick<ClearInputProps, "clearButton">
74
+ >(
75
+ (
76
+ {
77
+ value,
78
+ onChange,
79
+ wrapperClassName,
80
+ leading,
81
+ clearLabel = "Clear",
82
+ className,
83
+ classes,
84
+ clearButton = "filled",
85
+ ...props
86
+ },
87
+ ref,
88
+ ) => {
89
+ const merge = useCn();
90
+ const filled = value.length > 0;
91
+ const always = clearButton === "always";
92
+ const inputRef = React.useRef<HTMLInputElement | null>(null);
93
+
94
+ const preventFocusSteal = (event: React.SyntheticEvent) => {
95
+ if (document.activeElement === inputRef.current) {
96
+ event.preventDefault();
97
+ }
98
+ };
99
+
100
+ return (
101
+ <div className={merge(classes?.wrapper ?? "relative", wrapperClassName)}>
102
+ {leading}
103
+ <input
104
+ ref={(node) => {
105
+ inputRef.current = node;
106
+ assignRef(ref, node);
107
+ }}
108
+ type="text"
109
+ value={value}
110
+ onChange={(event) => {
111
+ onChange(event.target.value);
112
+ }}
113
+ // A caller `placeholder` in `props` replaces this default in place;
114
+ // an explicit `undefined` drops the attribute.
115
+ placeholder=""
116
+ className={merge(
117
+ classes?.input ?? [inputClasses, fieldFocusClass],
118
+ (always || filled) && "pr-9",
119
+ className,
120
+ )}
121
+ {...props}
122
+ />
123
+ {always || filled ? (
124
+ <button
125
+ type="button"
126
+ {...(always ? { tabIndex: filled ? 0 : -1 } : null)}
127
+ className={
128
+ always
129
+ ? merge(classes?.button ?? buttonClasses, filled ? "opacity-100" : "pointer-events-none opacity-0")
130
+ : (classes?.button ?? buttonClasses)
131
+ }
132
+ {...(always ? { "aria-hidden": !filled } : null)}
133
+ aria-label={clearLabel}
134
+ onPointerDown={preventFocusSteal}
135
+ onMouseDown={preventFocusSteal}
136
+ onClick={() => {
137
+ onChange("");
138
+ inputRef.current?.focus();
139
+ }}
140
+ >
141
+ <IconX size={14} stroke={1.75} />
142
+ </button>
143
+ ) : null}
144
+ </div>
145
+ );
146
+ },
147
+ );
148
+
149
+ const DissolveClearInput = React.forwardRef<HTMLInputElement, ClearInputPartsProps>(
22
150
  (
23
151
  {
24
152
  value,
@@ -28,10 +156,12 @@ const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
28
156
  clearLabel = "Clear",
29
157
  className,
30
158
  placeholder = "",
159
+ classes,
31
160
  ...props
32
161
  },
33
162
  ref,
34
163
  ) => {
164
+ const merge = useCn();
35
165
  const wrapRef = React.useRef<HTMLDivElement>(null);
36
166
  const inputRef = React.useRef<HTMLInputElement | null>(null);
37
167
  const mirrorRef = React.useRef<HTMLDivElement>(null);
@@ -42,11 +172,7 @@ const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
42
172
 
43
173
  function mergedRef(node: HTMLInputElement | null) {
44
174
  inputRef.current = node;
45
- if (typeof ref === "function") {
46
- ref(node);
47
- } else if (ref) {
48
- ref.current = node;
49
- }
175
+ assignRef(ref, node);
50
176
  }
51
177
 
52
178
  const hasValue = value.length > 0 || clearing;
@@ -104,15 +230,16 @@ const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
104
230
  );
105
231
  };
106
232
 
107
- const fieldClassName = cn(hasValue && "pr-9", className);
233
+ const fieldClassName = merge(hasValue && "pr-9", className);
108
234
 
109
235
  return (
110
236
  <div
111
237
  ref={wrapRef}
112
- className={cn(
238
+ className={merge(
113
239
  "t-clear",
114
240
  hasValue && "has-value",
115
241
  clearing && "is-clearing",
242
+ classes?.wrapper,
116
243
  wrapperClassName,
117
244
  )}
118
245
  >
@@ -125,9 +252,8 @@ const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
125
252
  onChange(event.target.value);
126
253
  }}
127
254
  placeholder={placeholder}
128
- className={cn(
129
- "flex h-10 w-full 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 placeholder:text-field-placeholder disabled:cursor-not-allowed disabled:opacity-50",
130
- fieldFocusClass,
255
+ className={merge(
256
+ classes?.input ?? [inputClasses, fieldFocusClass],
131
257
  fieldClassName,
132
258
  )}
133
259
  {...props}
@@ -135,14 +261,14 @@ const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
135
261
  <div className="t-clear-clip" aria-hidden="true">
136
262
  <div
137
263
  ref={mirrorRef}
138
- className={cn(
264
+ className={merge(
139
265
  "t-clear-mirror border-0 bg-transparent text-foreground shadow-none",
140
266
  fieldClassName,
141
267
  )}
142
268
  />
143
269
  <div
144
270
  ref={placeholderRef}
145
- className={cn(
271
+ className={merge(
146
272
  "t-clear-placeholder border-0 bg-transparent text-field-placeholder shadow-none",
147
273
  fieldClassName,
148
274
  )}
@@ -154,7 +280,7 @@ const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
154
280
  {value.length > 0 && !clearing ? (
155
281
  <button
156
282
  type="button"
157
- className="t-clear-btn absolute right-2 top-1/2 z-4 inline-flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-[background-color,color,transform] duration-100 ease-out active:scale-[0.92] hover:bg-surface-tertiary/50 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring"
283
+ className={classes?.button ? `t-clear-btn ${classes.button}` : `t-clear-btn ${buttonClasses}`}
158
284
  aria-label={clearLabel}
159
285
  onPointerDown={preventFocusSteal}
160
286
  onMouseDown={preventFocusSteal}
@@ -167,6 +293,5 @@ const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
167
293
  );
168
294
  },
169
295
  );
170
- ClearInput.displayName = "ClearInput";
171
296
 
172
297
  export { ClearInput };
@@ -1,7 +1,45 @@
1
+ import type * as React from "react";
1
2
  import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
3
+ import { useCn, useUiConfig } from "./ui-config";
4
+
5
+ /**
6
+ * `UiConfig.Collapsible`: classes for the trigger and the content (for
7
+ * example an app's open/close animation class). The parts have no built-in
8
+ * classes, so without an entry `className` passes through untouched.
9
+ */
10
+ export interface CollapsibleConfig {
11
+ classes?: { trigger?: string; content?: string };
12
+ }
2
13
 
3
14
  const Collapsible = CollapsiblePrimitive.Root;
4
- const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
5
- const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
15
+
16
+ function CollapsibleTrigger({
17
+ className,
18
+ ...props
19
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
20
+ const trigger = useUiConfig("Collapsible")?.classes?.trigger;
21
+ const cn = useCn();
22
+ return (
23
+ <CollapsiblePrimitive.CollapsibleTrigger
24
+ className={trigger === undefined ? className : cn(trigger, className)}
25
+ {...props}
26
+ />
27
+ );
28
+ }
29
+
30
+ export type CollapsibleContentProps = React.ComponentProps<
31
+ typeof CollapsiblePrimitive.CollapsibleContent
32
+ >;
33
+
34
+ function CollapsibleContent({ className, ...props }: CollapsibleContentProps) {
35
+ const content = useUiConfig("Collapsible")?.classes?.content;
36
+ const cn = useCn();
37
+ return (
38
+ <CollapsiblePrimitive.CollapsibleContent
39
+ className={content === undefined ? className : cn(content, className)}
40
+ {...props}
41
+ />
42
+ );
43
+ }
6
44
 
7
45
  export { Collapsible, CollapsibleTrigger, CollapsibleContent };