@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
@@ -0,0 +1,123 @@
1
+ import type { ReactNode } from "react";
2
+ import { m, AnimatePresence, type Transition } from "motion/react";
3
+
4
+ const DEFAULT_TRANSITION: Transition = {
5
+ type: "spring",
6
+ duration: 0.3,
7
+ bounce: 0,
8
+ };
9
+
10
+ const SOFT_EASE: [number, number, number, number] = [0.22, 1, 0.36, 1];
11
+ const SOFT_TRANSITION: Transition = {
12
+ duration: 0.15,
13
+ ease: SOFT_EASE,
14
+ };
15
+
16
+ const DEFAULT_ICON_MOTION = {
17
+ initial: { scale: 0.25, opacity: 0, filter: "blur(4px)" },
18
+ animate: { scale: 1, opacity: 1, filter: "blur(0px)" },
19
+ exit: { scale: 0.25, opacity: 0, filter: "blur(4px)" },
20
+ transition: DEFAULT_TRANSITION,
21
+ };
22
+
23
+ /** Near-imperceptible swap for high-frequency toggles (150ms, no blur). */
24
+ const SOFT_ICON_MOTION = {
25
+ initial: { scale: 0.96, opacity: 0 },
26
+ animate: { scale: 1, opacity: 1 },
27
+ exit: { scale: 0.96, opacity: 0 },
28
+ transition: SOFT_TRANSITION,
29
+ };
30
+
31
+ interface CrossfadeIconProps {
32
+ show: boolean;
33
+ whenTrue: ReactNode;
34
+ whenFalse: ReactNode;
35
+ trueKey?: string;
36
+ falseKey?: string;
37
+ className?: string;
38
+ /** `soft` = opacity + scale(0.96→1), no blur — for tens/day toggles. */
39
+ variant?: "default" | "soft";
40
+ }
41
+
42
+ /** Cross-fades between two icons in the same slot. */
43
+ export function CrossfadeIcon({
44
+ show,
45
+ whenTrue,
46
+ whenFalse,
47
+ trueKey = "on",
48
+ falseKey = "off",
49
+ className = "relative flex size-5 items-center justify-center",
50
+ variant = "default",
51
+ }: CrossfadeIconProps) {
52
+ const motion = variant === "soft" ? SOFT_ICON_MOTION : DEFAULT_ICON_MOTION;
53
+
54
+ return (
55
+ <span className={className}>
56
+ <AnimatePresence initial={false}>
57
+ {show ? (
58
+ <m.span
59
+ key={trueKey}
60
+ className="absolute inset-0 flex items-center justify-center"
61
+ initial={motion.initial}
62
+ animate={motion.animate}
63
+ exit={motion.exit}
64
+ transition={motion.transition}
65
+ >
66
+ {whenTrue}
67
+ </m.span>
68
+ ) : (
69
+ <m.span
70
+ key={falseKey}
71
+ className="absolute inset-0 flex items-center justify-center"
72
+ initial={motion.initial}
73
+ animate={motion.animate}
74
+ exit={motion.exit}
75
+ transition={motion.transition}
76
+ >
77
+ {whenFalse}
78
+ </m.span>
79
+ )}
80
+ </AnimatePresence>
81
+ </span>
82
+ );
83
+ }
84
+
85
+ /**
86
+ * Cross-fades between any number of keyed icons in the same slot.
87
+ *
88
+ * `CrossfadeIcon` covers the two-state case, which is most of them. A control
89
+ * that cycles through three or more states (the composer's submit button:
90
+ * send, submitting, streaming, error) needs the key to come from the state
91
+ * rather than from a boolean.
92
+ */
93
+ export function CrossfadeIconSlot({
94
+ iconKey,
95
+ children,
96
+ className = "relative flex size-4 items-center justify-center",
97
+ variant = "soft",
98
+ }: {
99
+ /** Identity of the icon currently shown; a change triggers the swap. */
100
+ iconKey: string;
101
+ children: ReactNode;
102
+ className?: string;
103
+ variant?: "default" | "soft";
104
+ }) {
105
+ const motion = variant === "soft" ? SOFT_ICON_MOTION : DEFAULT_ICON_MOTION;
106
+
107
+ return (
108
+ <span className={className}>
109
+ <AnimatePresence initial={false}>
110
+ <m.span
111
+ key={iconKey}
112
+ className="absolute inset-0 flex items-center justify-center"
113
+ initial={motion.initial}
114
+ animate={motion.animate}
115
+ exit={motion.exit}
116
+ transition={motion.transition}
117
+ >
118
+ {children}
119
+ </m.span>
120
+ </AnimatePresence>
121
+ </span>
122
+ );
123
+ }
package/src/ui/dialog.tsx CHANGED
@@ -11,29 +11,69 @@ import {
11
11
  treeHasDialogDescription,
12
12
  useCompactOverlay,
13
13
  } from "./overlay-a11y";
14
+ import { useCn, useUiConfig } from "./ui-config";
15
+
16
+ /** App defaults for `Dialog*` (see `UiConfigProvider`). */
17
+ export interface DialogConfig {
18
+ /** Replaces the built-in classes of a part. */
19
+ classes?: {
20
+ trigger?: string;
21
+ overlay?: string;
22
+ /** Content classes shared by both layouts. */
23
+ content?: string;
24
+ /** Content layout classes for the centred dialog. */
25
+ centered?: string;
26
+ /** Content layout classes for the compact bottom sheet. */
27
+ compact?: string;
28
+ close?: string;
29
+ header?: string;
30
+ body?: string;
31
+ footer?: string;
32
+ title?: string;
33
+ description?: string;
34
+ };
35
+ /** Default for `DialogContent` `viewportAware`. */
36
+ viewportAware?: boolean;
37
+ }
14
38
 
15
39
  const Dialog = DialogPrimitive.Root;
16
- const DialogTrigger = DialogPrimitive.Trigger;
17
40
  const DialogPortal = DialogPrimitive.Portal;
18
41
  const DialogClose = DialogPrimitive.Close;
19
42
 
43
+ const DialogTrigger = React.forwardRef<
44
+ React.ComponentRef<typeof DialogPrimitive.Trigger>,
45
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>
46
+ >((props, ref) => {
47
+ const trigger = useUiConfig("Dialog")?.classes?.trigger;
48
+ const merge = useCn();
49
+ // Without config, pass props untouched: Radix's Slot turns an explicit
50
+ // `className={undefined}` into `class=""`.
51
+ if (trigger === undefined) return <DialogPrimitive.Trigger ref={ref} {...props} />;
52
+ const { className, ...rest } = props;
53
+ return <DialogPrimitive.Trigger ref={ref} className={merge(trigger, className)} {...rest} />;
54
+ });
55
+ DialogTrigger.displayName = DialogPrimitive.Trigger.displayName;
56
+
20
57
  const overlayMotionClass =
21
58
  "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 motion-reduce:animate-none motion-reduce:data-[state=open]:animate-none motion-reduce:data-[state=closed]:animate-none";
22
59
 
23
60
  const DialogOverlay = React.forwardRef<
24
61
  React.ComponentRef<typeof DialogPrimitive.Overlay>,
25
62
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
26
- >(({ className, ...props }, ref) => (
27
- <DialogPrimitive.Overlay
28
- ref={ref}
29
- className={cn(
30
- "fixed inset-0 z-50 bg-backdrop backdrop-blur-md",
31
- overlayMotionClass,
32
- className,
33
- )}
34
- {...props}
35
- />
36
- ));
63
+ >(({ className, ...props }, ref) => {
64
+ const overlay = useUiConfig("Dialog")?.classes?.overlay;
65
+ const merge = useCn();
66
+ return (
67
+ <DialogPrimitive.Overlay
68
+ ref={ref}
69
+ className={merge(
70
+ overlay ?? ["fixed inset-0 z-50 bg-backdrop backdrop-blur-md", overlayMotionClass],
71
+ className,
72
+ )}
73
+ {...props}
74
+ />
75
+ );
76
+ });
37
77
  DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
38
78
 
39
79
  const DEFAULT_ALERT_DESCRIPTION = "This action needs confirmation.";
@@ -42,6 +82,12 @@ const DialogContent = React.forwardRef<
42
82
  React.ComponentRef<typeof DialogPrimitive.Content>,
43
83
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
44
84
  hideCloseButton?: boolean;
85
+ /**
86
+ * Bottom sheet on compact viewports, visual-viewport sizing, keyboard-aware
87
+ * scrolling and the body pointer-events fix. Default `true`. `false` keeps a
88
+ * centred dialog at every size and leaves the viewport and body alone.
89
+ */
90
+ viewportAware?: boolean;
45
91
  }
46
92
  >(
47
93
  (
@@ -49,6 +95,7 @@ const DialogContent = React.forwardRef<
49
95
  className,
50
96
  children,
51
97
  hideCloseButton,
98
+ viewportAware: viewportAwareProp,
52
99
  role,
53
100
  onOpenAutoFocus,
54
101
  "aria-describedby": ariaDescribedBy,
@@ -56,7 +103,12 @@ const DialogContent = React.forwardRef<
56
103
  },
57
104
  ref,
58
105
  ) => {
59
- const compact = useCompactOverlay();
106
+ const config = useUiConfig("Dialog");
107
+ const classes = config?.classes;
108
+ const merge = useCn();
109
+ const viewportAware = viewportAwareProp ?? config?.viewportAware ?? true;
110
+ const compactViewport = useCompactOverlay();
111
+ const compact = viewportAware && compactViewport;
60
112
  const isAlert = role === "alertdialog";
61
113
  const contentNodeRef = React.useRef<HTMLDivElement | null>(null);
62
114
  const surfaceCleanupRef = React.useRef<(() => void) | null>(null);
@@ -88,6 +140,10 @@ const DialogContent = React.forwardRef<
88
140
  if (!compact) {
89
141
  surfaceCleanupRef.current = connectModalSurface(node);
90
142
  }
143
+ if (!viewportAware) {
144
+ assignRef(ref, node);
145
+ return;
146
+ }
91
147
  const viewport = connectOverlayViewport(node);
92
148
  const keyboard = connectKeyboardScroll(node);
93
149
  const pointerEvents = connectBodyPointerEventsFix();
@@ -100,7 +156,7 @@ const DialogContent = React.forwardRef<
100
156
 
101
157
  assignRef(ref, node);
102
158
  },
103
- [ref, compact],
159
+ [ref, compact, viewportAware],
104
160
  );
105
161
 
106
162
  React.useLayoutEffect(() => {
@@ -134,10 +190,11 @@ const DialogContent = React.forwardRef<
134
190
  : {})}
135
191
  onOpenAutoFocus={handleOpenAutoFocus}
136
192
  data-compact={compact ? "true" : undefined}
137
- className={cn(
138
- "glass-panel-strong z-50 flex w-full flex-col gap-4 p-6 text-overlay-foreground smooth-shadow-ring-xl",
193
+ className={merge(
194
+ classes?.content ??
195
+ "glass-panel-strong z-50 flex w-full flex-col gap-4 p-6 text-overlay-foreground smooth-shadow-ring-xl",
139
196
  compact
140
- ? cn(
197
+ ? classes?.compact ?? cn(
141
198
  "overflow-y-auto fixed inset-x-0 top-auto w-full max-w-none rounded-t-2xl rounded-b-none pt-3",
142
199
  "max-h-[var(--overlay-vv-height,100dvh)]",
143
200
  "bottom-[var(--overlay-vv-bottom,0px)]",
@@ -146,7 +203,8 @@ const DialogContent = React.forwardRef<
146
203
  "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:slide-in-from-bottom data-[state=closed]:slide-out-to-bottom",
147
204
  "motion-reduce:animate-none motion-reduce:data-[state=open]:animate-none motion-reduce:data-[state=closed]:animate-none",
148
205
  )
149
- : "t-modal fixed left-1/2 top-1/2 max-h-[90dvh] max-w-lg max-sm:w-[calc(100vw-2rem)] overflow-y-auto rounded-lg",
206
+ : classes?.centered ??
207
+ "t-modal fixed left-1/2 top-1/2 max-h-[90dvh] max-w-lg max-sm:w-[calc(100vw-2rem)] overflow-y-auto rounded-lg",
150
208
  className,
151
209
  )}
152
210
  {...props}
@@ -170,10 +228,13 @@ const DialogContent = React.forwardRef<
170
228
  {children}
171
229
  {!hideCloseButton && (
172
230
  <DialogPrimitive.Close
173
- className={cn(
174
- "absolute right-2 top-2 inline-flex h-11 w-11 items-center justify-center rounded-lg text-overlay-foreground/70 transition-[background-color,transform] hover:bg-surface-tertiary/50 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:pointer-events-none",
175
- "motion-reduce:transition-none",
176
- )}
231
+ className={
232
+ classes?.close ??
233
+ cn(
234
+ "absolute right-2 top-2 inline-flex h-11 w-11 items-center justify-center rounded-lg text-overlay-foreground/70 transition-[background-color,transform] hover:bg-surface-tertiary/50 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:pointer-events-none",
235
+ "motion-reduce:transition-none",
236
+ )
237
+ }
177
238
  >
178
239
  <IconX className="h-4 w-4" />
179
240
  <span className="sr-only">Close</span>
@@ -189,26 +250,34 @@ DialogContent.displayName = DialogPrimitive.Content.displayName;
189
250
  const DialogHeader = ({
190
251
  className,
191
252
  ...props
192
- }: React.HTMLAttributes<HTMLDivElement>) => (
193
- <div
194
- className={cn("flex flex-col gap-1.5 pr-10 text-left", className)}
195
- {...props}
196
- />
197
- );
253
+ }: React.HTMLAttributes<HTMLDivElement>) => {
254
+ const header = useUiConfig("Dialog")?.classes?.header;
255
+ const merge = useCn();
256
+ return (
257
+ <div
258
+ className={merge(header ?? "flex flex-col gap-1.5 pr-10 text-left", className)}
259
+ {...props}
260
+ />
261
+ );
262
+ };
198
263
  DialogHeader.displayName = "DialogHeader";
199
264
 
200
265
  const DialogBody = ({
201
266
  className,
202
267
  ...props
203
- }: React.HTMLAttributes<HTMLDivElement>) => (
204
- <div
205
- className={cn(
206
- "min-h-11 flex-1 overflow-y-auto overscroll-contain",
207
- className,
208
- )}
209
- {...props}
210
- />
211
- );
268
+ }: React.HTMLAttributes<HTMLDivElement>) => {
269
+ const body = useUiConfig("Dialog")?.classes?.body;
270
+ const merge = useCn();
271
+ return (
272
+ <div
273
+ className={merge(
274
+ body ?? "min-h-11 flex-1 overflow-y-auto overscroll-contain",
275
+ className,
276
+ )}
277
+ {...props}
278
+ />
279
+ );
280
+ };
212
281
  DialogBody.displayName = "DialogBody";
213
282
 
214
283
  // no fill of its own: the footer is not sticky, so it shows the content's
@@ -218,49 +287,64 @@ DialogBody.displayName = "DialogBody";
218
287
  const DialogFooter = ({
219
288
  className,
220
289
  ...props
221
- }: React.HTMLAttributes<HTMLDivElement>) => (
222
- <div
223
- data-slot="dialog-footer"
224
- className={cn(
225
- "mt-auto flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end",
226
- "pb-[max(0px,env(safe-area-inset-bottom,0px))]",
227
- "[&_button]:max-sm:min-h-11",
228
- className,
229
- )}
230
- {...props}
231
- />
232
- );
290
+ }: React.HTMLAttributes<HTMLDivElement>) => {
291
+ const footer = useUiConfig("Dialog")?.classes?.footer;
292
+ const merge = useCn();
293
+ return (
294
+ <div
295
+ data-slot="dialog-footer"
296
+ className={merge(
297
+ footer ?? [
298
+ "mt-auto flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end",
299
+ "pb-[max(0px,env(safe-area-inset-bottom,0px))]",
300
+ "[&_button]:max-sm:min-h-11",
301
+ ],
302
+ className,
303
+ )}
304
+ {...props}
305
+ />
306
+ );
307
+ };
233
308
  DialogFooter.displayName = "DialogFooter";
234
309
 
235
310
  const DialogTitle = React.forwardRef<
236
311
  React.ComponentRef<typeof DialogPrimitive.Title>,
237
312
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
238
- >(({ className, ...props }, ref) => (
239
- <DialogPrimitive.Title
240
- ref={ref}
241
- className={cn(
242
- "text-lg font-semibold tracking-normal text-foreground text-balance",
243
- className,
244
- )}
245
- {...props}
246
- />
247
- ));
313
+ >(({ className, ...props }, ref) => {
314
+ const title = useUiConfig("Dialog")?.classes?.title;
315
+ const merge = useCn();
316
+ return (
317
+ <DialogPrimitive.Title
318
+ ref={ref}
319
+ className={merge(
320
+ title ?? "text-lg font-semibold tracking-normal text-foreground text-balance",
321
+ className,
322
+ )}
323
+ {...props}
324
+ />
325
+ );
326
+ });
248
327
  DialogTitle.displayName = DialogPrimitive.Title.displayName;
249
328
 
250
329
  const DialogDescription = React.forwardRef<
251
330
  React.ComponentRef<typeof DialogPrimitive.Description>,
252
331
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
253
- >(({ className, ...props }, ref) => (
254
- <DialogPrimitive.Description
255
- ref={ref}
256
- className={cn(
257
- // `--muted` is ~3.5:1 on overlay; mix toward overlay-foreground for 4.5:1.
258
- "text-sm leading-relaxed text-[color-mix(in_oklch,var(--color-popover-foreground,var(--overlay-foreground))_72%,var(--color-popover,var(--overlay)))]",
259
- className,
260
- )}
261
- {...props}
262
- />
263
- ));
332
+ >(({ className, ...props }, ref) => {
333
+ const description = useUiConfig("Dialog")?.classes?.description;
334
+ const merge = useCn();
335
+ return (
336
+ <DialogPrimitive.Description
337
+ ref={ref}
338
+ className={merge(
339
+ description ??
340
+ // `--muted` is ~3.5:1 on overlay; mix toward overlay-foreground for 4.5:1.
341
+ "text-sm leading-relaxed text-[color-mix(in_oklch,var(--color-popover-foreground,var(--overlay-foreground))_72%,var(--color-popover,var(--overlay)))]",
342
+ className,
343
+ )}
344
+ {...props}
345
+ />
346
+ );
347
+ });
264
348
  DialogDescription.displayName = DialogPrimitive.Description.displayName;
265
349
 
266
350
  const DialogRawContent = DialogPrimitive.Content;