@exxatdesignux/ui 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +72 -0
- package/src/components/ui/avatar.tsx +384 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/banner.tsx +364 -0
- package/src/components/ui/breadcrumb.tsx +120 -0
- package/src/components/ui/button.tsx +66 -0
- package/src/components/ui/calendar.tsx +220 -0
- package/src/components/ui/card.tsx +136 -0
- package/src/components/ui/chart.tsx +378 -0
- package/src/components/ui/checkbox.tsx +160 -0
- package/src/components/ui/coach-mark.tsx +361 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/command.tsx +232 -0
- package/src/components/ui/date-picker-field.tsx +186 -0
- package/src/components/ui/dialog.tsx +171 -0
- package/src/components/ui/drag-handle-grip.tsx +10 -0
- package/src/components/ui/drawer.tsx +134 -0
- package/src/components/ui/dropdown-menu.tsx +422 -0
- package/src/components/ui/field.tsx +238 -0
- package/src/components/ui/form.tsx +137 -0
- package/src/components/ui/input-group.tsx +156 -0
- package/src/components/ui/input-mask.tsx +135 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/kbd.tsx +55 -0
- package/src/components/ui/label.tsx +25 -0
- package/src/components/ui/payment-card-fields.tsx +65 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/radio-group.tsx +217 -0
- package/src/components/ui/select.tsx +191 -0
- package/src/components/ui/selection-tile-grid.tsx +246 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +147 -0
- package/src/components/ui/sidebar.tsx +716 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +39 -0
- package/src/components/ui/status-badge.tsx +109 -0
- package/src/components/ui/table.tsx +117 -0
- package/src/components/ui/tabs.tsx +90 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tip.tsx +21 -0
- package/src/components/ui/toggle-group.tsx +89 -0
- package/src/components/ui/toggle-switch.tsx +31 -0
- package/src/components/ui/toggle.tsx +48 -0
- package/src/components/ui/tooltip.tsx +59 -0
- package/src/components/ui/view-segmented-control.tsx +160 -0
- package/src/globals.css +1795 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/use-app-theme.ts +172 -0
- package/src/hooks/use-coach-mark.ts +342 -0
- package/src/hooks/use-mobile.ts +31 -0
- package/src/hooks/use-mod-key-label.ts +29 -0
- package/src/index.ts +55 -0
- package/src/lib/compose-refs.ts +15 -0
- package/src/lib/date-filter.ts +67 -0
- package/src/lib/utils.ts +6 -0
- package/src/theme/apply-windows-contrast-theme.ts +29 -0
- package/src/theme/windows-contrast-theme.json +147 -0
- package/src/theme.css +1130 -0
- package/src/types/react-payment-inputs.d.ts +20 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CoachMark — contextual onboarding / feature-discovery popover
|
|
5
|
+
*
|
|
6
|
+
* Targets elements by CSS selector, scrolls them into view, and positions
|
|
7
|
+
* the popover relative to the target using Radix's virtual anchor.
|
|
8
|
+
*
|
|
9
|
+
* Variants:
|
|
10
|
+
* • single — standalone tip anchored to a target element
|
|
11
|
+
* • flow — multi-step walkthrough with prev/next and step indicator
|
|
12
|
+
* • image — includes a hero image above the content
|
|
13
|
+
* • no-image — text-only (title + description)
|
|
14
|
+
*
|
|
15
|
+
* Brand-colored background with spotlight overlay on the target element.
|
|
16
|
+
*
|
|
17
|
+
* WCAG 2.1 AA:
|
|
18
|
+
* • Focus trapped inside while open
|
|
19
|
+
* • Escape dismisses
|
|
20
|
+
* • aria-labelledby / aria-describedby wired automatically
|
|
21
|
+
* • Step indicator announced via aria-live
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as React from "react"
|
|
25
|
+
import { createPortal } from "react-dom"
|
|
26
|
+
import { Popover as PopoverPrimitive } from "radix-ui"
|
|
27
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
28
|
+
import { Button } from "./button"
|
|
29
|
+
import { cn } from "../../lib/utils"
|
|
30
|
+
import type { CoachMarkState } from "../../hooks/use-coach-mark"
|
|
31
|
+
|
|
32
|
+
/* ── Variant styles ─────────────────────────────────────────────────────── */
|
|
33
|
+
|
|
34
|
+
const coachMarkVariants = cva(
|
|
35
|
+
"z-[60] flex flex-col overflow-hidden rounded-xl bg-brand-deep text-white shadow-xl outline-none hc:!bg-background hc:!text-foreground hc:!border-2 hc:!border-foreground hc:!shadow-none",
|
|
36
|
+
{
|
|
37
|
+
variants: {
|
|
38
|
+
size: {
|
|
39
|
+
default: "w-[320px]",
|
|
40
|
+
sm: "w-[260px]",
|
|
41
|
+
lg: "w-[400px]",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
defaultVariants: {
|
|
45
|
+
size: "default",
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
/* ── Sub-components ─────────────────────────────────────────────────────── */
|
|
51
|
+
|
|
52
|
+
function CoachMarkImage({
|
|
53
|
+
src,
|
|
54
|
+
alt,
|
|
55
|
+
}: {
|
|
56
|
+
src: string
|
|
57
|
+
alt: string
|
|
58
|
+
}) {
|
|
59
|
+
return (
|
|
60
|
+
<div className="relative w-full overflow-hidden">
|
|
61
|
+
<img
|
|
62
|
+
src={src}
|
|
63
|
+
alt={alt}
|
|
64
|
+
className="h-[160px] w-full object-cover"
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function CoachMarkStepIndicator({
|
|
71
|
+
current,
|
|
72
|
+
total,
|
|
73
|
+
}: {
|
|
74
|
+
current: number
|
|
75
|
+
total: number
|
|
76
|
+
}) {
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
className="flex items-center gap-1"
|
|
80
|
+
role="status"
|
|
81
|
+
aria-live="polite"
|
|
82
|
+
aria-label={`Step ${current + 1} of ${total}`}
|
|
83
|
+
>
|
|
84
|
+
{Array.from({ length: total }, (_, i) => (
|
|
85
|
+
<span
|
|
86
|
+
key={i}
|
|
87
|
+
className={cn(
|
|
88
|
+
"h-1.5 rounded-full transition-all duration-200",
|
|
89
|
+
i === current
|
|
90
|
+
? "w-4 bg-white hc:bg-foreground"
|
|
91
|
+
: "w-1.5 bg-white/30 hc:bg-foreground/40"
|
|
92
|
+
)}
|
|
93
|
+
aria-hidden="true"
|
|
94
|
+
/>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ── Spotlight overlay — highlights the target element ──────────────────── */
|
|
101
|
+
|
|
102
|
+
function SpotlightOverlay({
|
|
103
|
+
rect,
|
|
104
|
+
maskId,
|
|
105
|
+
}: {
|
|
106
|
+
rect: { x: number; y: number; width: number; height: number }
|
|
107
|
+
/** Unique per coach instance — multiple flows on one page must not duplicate SVG mask ids. */
|
|
108
|
+
maskId: string
|
|
109
|
+
}) {
|
|
110
|
+
const padding = 6
|
|
111
|
+
const borderRadius = 8
|
|
112
|
+
const x = rect.x - padding
|
|
113
|
+
const y = rect.y - padding
|
|
114
|
+
const w = rect.width + padding * 2
|
|
115
|
+
const h = rect.height + padding * 2
|
|
116
|
+
const maskUrl = `url(#${maskId})`
|
|
117
|
+
|
|
118
|
+
return createPortal(
|
|
119
|
+
<div
|
|
120
|
+
className="fixed inset-0 z-[55]"
|
|
121
|
+
aria-hidden="true"
|
|
122
|
+
>
|
|
123
|
+
{/* Semi-transparent overlay with a cutout for the target */}
|
|
124
|
+
<svg className="absolute inset-0 w-full h-full">
|
|
125
|
+
<defs>
|
|
126
|
+
<mask id={maskId}>
|
|
127
|
+
<rect width="100%" height="100%" fill="white" />
|
|
128
|
+
<rect
|
|
129
|
+
x={x}
|
|
130
|
+
y={y}
|
|
131
|
+
width={w}
|
|
132
|
+
height={h}
|
|
133
|
+
rx={borderRadius}
|
|
134
|
+
ry={borderRadius}
|
|
135
|
+
fill="black"
|
|
136
|
+
/>
|
|
137
|
+
</mask>
|
|
138
|
+
</defs>
|
|
139
|
+
<rect
|
|
140
|
+
width="100%"
|
|
141
|
+
height="100%"
|
|
142
|
+
fill="rgba(0,0,0,0.5)"
|
|
143
|
+
mask={maskUrl}
|
|
144
|
+
/>
|
|
145
|
+
</svg>
|
|
146
|
+
|
|
147
|
+
{/* Highlight ring around the target */}
|
|
148
|
+
<div
|
|
149
|
+
className="absolute rounded-lg ring-2 ring-brand hc:ring-foreground shadow-[0_0_0_4px_rgba(0,0,0,0.3)] hc:shadow-none"
|
|
150
|
+
style={{
|
|
151
|
+
left: x,
|
|
152
|
+
top: y,
|
|
153
|
+
width: w,
|
|
154
|
+
height: h,
|
|
155
|
+
borderRadius,
|
|
156
|
+
}}
|
|
157
|
+
/>
|
|
158
|
+
</div>,
|
|
159
|
+
document.body
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* ── Main component ─────────────────────────────────────────────────────── */
|
|
164
|
+
|
|
165
|
+
export interface CoachMarkProps
|
|
166
|
+
extends VariantProps<typeof coachMarkVariants> {
|
|
167
|
+
/** State from useCoachMark hook */
|
|
168
|
+
state: CoachMarkState
|
|
169
|
+
/** Default popover placement side (step-level side takes priority) */
|
|
170
|
+
side?: "top" | "bottom" | "left" | "right"
|
|
171
|
+
/** Default popover alignment (step-level align takes priority) */
|
|
172
|
+
align?: "start" | "center" | "end"
|
|
173
|
+
/** Offset from anchor element in px */
|
|
174
|
+
sideOffset?: number
|
|
175
|
+
/** Label for the primary (next/done) button — defaults to "Next" / "Got it" */
|
|
176
|
+
nextLabel?: string
|
|
177
|
+
/** Label for the skip button — defaults to "Skip" */
|
|
178
|
+
skipLabel?: string
|
|
179
|
+
/** Extra className for the content container */
|
|
180
|
+
className?: string
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function CoachMark({
|
|
184
|
+
state,
|
|
185
|
+
side = "bottom",
|
|
186
|
+
align = "center",
|
|
187
|
+
sideOffset = 12,
|
|
188
|
+
nextLabel,
|
|
189
|
+
skipLabel = "Skip",
|
|
190
|
+
size,
|
|
191
|
+
className,
|
|
192
|
+
}: CoachMarkProps) {
|
|
193
|
+
const spotlightMaskId = React.useId().replace(/:/g, "")
|
|
194
|
+
const {
|
|
195
|
+
isOpen,
|
|
196
|
+
step,
|
|
197
|
+
currentStep,
|
|
198
|
+
totalSteps,
|
|
199
|
+
isFlow,
|
|
200
|
+
isFirst,
|
|
201
|
+
isLast,
|
|
202
|
+
next,
|
|
203
|
+
prev,
|
|
204
|
+
skip,
|
|
205
|
+
anchorRect,
|
|
206
|
+
} = state
|
|
207
|
+
|
|
208
|
+
if (!isOpen || !step) return null
|
|
209
|
+
if (!anchorRect) return null
|
|
210
|
+
|
|
211
|
+
const titleId = `coach-mark-title-${step.id}`
|
|
212
|
+
const descId = `coach-mark-desc-${step.id}`
|
|
213
|
+
const hasImage = Boolean(step.image)
|
|
214
|
+
const primaryLabel = nextLabel ?? (isLast ? "Got it" : "Next")
|
|
215
|
+
const resolvedSide = step.side ?? side
|
|
216
|
+
const resolvedAlign = step.align ?? align
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<>
|
|
220
|
+
{/* Spotlight overlay */}
|
|
221
|
+
<SpotlightOverlay rect={anchorRect} maskId={`coach-spotlight-${spotlightMaskId}`} />
|
|
222
|
+
|
|
223
|
+
{/* Popover with virtual anchor */}
|
|
224
|
+
<PopoverPrimitive.Root open>
|
|
225
|
+
<PopoverPrimitive.Anchor
|
|
226
|
+
virtualRef={{
|
|
227
|
+
current: {
|
|
228
|
+
getBoundingClientRect: () => ({
|
|
229
|
+
x: anchorRect.x,
|
|
230
|
+
y: anchorRect.y,
|
|
231
|
+
top: anchorRect.y,
|
|
232
|
+
left: anchorRect.x,
|
|
233
|
+
bottom: anchorRect.y + anchorRect.height,
|
|
234
|
+
right: anchorRect.x + anchorRect.width,
|
|
235
|
+
width: anchorRect.width,
|
|
236
|
+
height: anchorRect.height,
|
|
237
|
+
toJSON: () => {},
|
|
238
|
+
}),
|
|
239
|
+
},
|
|
240
|
+
}}
|
|
241
|
+
/>
|
|
242
|
+
|
|
243
|
+
<PopoverPrimitive.Portal>
|
|
244
|
+
<PopoverPrimitive.Content
|
|
245
|
+
data-slot="coach-mark"
|
|
246
|
+
side={resolvedSide}
|
|
247
|
+
align={resolvedAlign}
|
|
248
|
+
sideOffset={sideOffset}
|
|
249
|
+
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
250
|
+
onInteractOutside={(e) => e.preventDefault()}
|
|
251
|
+
onEscapeKeyDown={() => skip()}
|
|
252
|
+
aria-labelledby={titleId}
|
|
253
|
+
aria-describedby={descId}
|
|
254
|
+
className={cn(
|
|
255
|
+
coachMarkVariants({ size }),
|
|
256
|
+
/* animations */
|
|
257
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
258
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
259
|
+
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
260
|
+
"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
|
|
261
|
+
"data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
|
|
262
|
+
className
|
|
263
|
+
)}
|
|
264
|
+
>
|
|
265
|
+
{/* Image (optional) */}
|
|
266
|
+
{hasImage && (
|
|
267
|
+
<CoachMarkImage
|
|
268
|
+
src={step.image!}
|
|
269
|
+
alt={step.imageAlt ?? step.title}
|
|
270
|
+
/>
|
|
271
|
+
)}
|
|
272
|
+
|
|
273
|
+
{/* Body */}
|
|
274
|
+
<div className="flex flex-col gap-3 p-4">
|
|
275
|
+
{/* Title + close */}
|
|
276
|
+
<div className="flex items-start gap-2">
|
|
277
|
+
<div className="flex-1 min-w-0">
|
|
278
|
+
<h3
|
|
279
|
+
id={titleId}
|
|
280
|
+
className="text-sm font-semibold text-white leading-snug"
|
|
281
|
+
>
|
|
282
|
+
{step.title}
|
|
283
|
+
</h3>
|
|
284
|
+
</div>
|
|
285
|
+
<button
|
|
286
|
+
type="button"
|
|
287
|
+
onClick={skip}
|
|
288
|
+
className="shrink-0 -mt-0.5 -mr-1 flex h-6 w-6 items-center justify-center rounded-md text-white/60 hover:bg-white/15 hover:text-white transition-colors focus-visible:outline-2 focus-visible:outline-white hc:!text-foreground hc:hover:!bg-foreground/10"
|
|
289
|
+
aria-label="Dismiss"
|
|
290
|
+
>
|
|
291
|
+
<i className="fa-light fa-xmark text-xs" aria-hidden="true" />
|
|
292
|
+
</button>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
{/* Description */}
|
|
296
|
+
<p
|
|
297
|
+
id={descId}
|
|
298
|
+
className="text-sm text-white/80 leading-relaxed"
|
|
299
|
+
>
|
|
300
|
+
{step.description}
|
|
301
|
+
</p>
|
|
302
|
+
|
|
303
|
+
{/* Footer: step indicator + actions */}
|
|
304
|
+
<div className="flex items-center justify-between gap-3 pt-1">
|
|
305
|
+
{/* Left: step dots (flow only) */}
|
|
306
|
+
<div className="flex-1">
|
|
307
|
+
{isFlow && (
|
|
308
|
+
<CoachMarkStepIndicator
|
|
309
|
+
current={currentStep}
|
|
310
|
+
total={totalSteps}
|
|
311
|
+
/>
|
|
312
|
+
)}
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
{/* Right: buttons */}
|
|
316
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
317
|
+
{isFlow && !isLast && (
|
|
318
|
+
<button
|
|
319
|
+
type="button"
|
|
320
|
+
onClick={skip}
|
|
321
|
+
className="h-8 px-3 text-xs font-medium text-white/60 hover:text-white transition-colors rounded-md focus-visible:outline-2 focus-visible:outline-white hc:!text-foreground hc:hover:!text-foreground/80"
|
|
322
|
+
>
|
|
323
|
+
{skipLabel}
|
|
324
|
+
</button>
|
|
325
|
+
)}
|
|
326
|
+
{isFlow && !isFirst && (
|
|
327
|
+
<button
|
|
328
|
+
type="button"
|
|
329
|
+
onClick={prev}
|
|
330
|
+
className="inline-flex items-center gap-1.5 h-8 px-3 text-xs font-medium text-white bg-white/15 hover:bg-white/25 rounded-md border border-white/20 transition-colors focus-visible:outline-2 focus-visible:outline-white hc:!bg-background hc:!text-foreground hc:!border-foreground"
|
|
331
|
+
>
|
|
332
|
+
<i className="fa-light fa-arrow-left text-xs" aria-hidden="true" />
|
|
333
|
+
Back
|
|
334
|
+
</button>
|
|
335
|
+
)}
|
|
336
|
+
<button
|
|
337
|
+
type="button"
|
|
338
|
+
onClick={next}
|
|
339
|
+
className="inline-flex items-center gap-1.5 h-8 px-3 text-xs font-medium text-brand-deep bg-white hover:bg-white/90 rounded-md transition-colors focus-visible:outline-2 focus-visible:outline-white hc:!bg-foreground hc:!text-background hc:border hc:border-foreground"
|
|
340
|
+
>
|
|
341
|
+
{primaryLabel}
|
|
342
|
+
{isFlow && !isLast && (
|
|
343
|
+
<i className="fa-light fa-arrow-right text-xs" aria-hidden="true" />
|
|
344
|
+
)}
|
|
345
|
+
</button>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
{/* Arrow */}
|
|
351
|
+
<PopoverPrimitive.Arrow
|
|
352
|
+
className="fill-brand-deep drop-shadow-sm hc:fill-background"
|
|
353
|
+
width={12}
|
|
354
|
+
height={6}
|
|
355
|
+
/>
|
|
356
|
+
</PopoverPrimitive.Content>
|
|
357
|
+
</PopoverPrimitive.Portal>
|
|
358
|
+
</PopoverPrimitive.Root>
|
|
359
|
+
</>
|
|
360
|
+
)
|
|
361
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
|
|
4
|
+
|
|
5
|
+
function Collapsible({
|
|
6
|
+
...props
|
|
7
|
+
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
|
8
|
+
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function CollapsibleTrigger({
|
|
12
|
+
...props
|
|
13
|
+
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
|
14
|
+
return (
|
|
15
|
+
<CollapsiblePrimitive.CollapsibleTrigger
|
|
16
|
+
data-slot="collapsible-trigger"
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function CollapsibleContent({
|
|
23
|
+
...props
|
|
24
|
+
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
|
25
|
+
return (
|
|
26
|
+
<CollapsiblePrimitive.CollapsibleContent
|
|
27
|
+
data-slot="collapsible-content"
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Command as CommandPrimitive } from "cmdk"
|
|
5
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|
6
|
+
|
|
7
|
+
import { cn } from "../../lib/utils"
|
|
8
|
+
import {
|
|
9
|
+
InputGroup,
|
|
10
|
+
InputGroupAddon,
|
|
11
|
+
} from "./input-group"
|
|
12
|
+
import { SearchIcon, CheckIcon } from "lucide-react"
|
|
13
|
+
|
|
14
|
+
function Command({
|
|
15
|
+
className,
|
|
16
|
+
...props
|
|
17
|
+
}: React.ComponentProps<typeof CommandPrimitive>) {
|
|
18
|
+
return (
|
|
19
|
+
<CommandPrimitive
|
|
20
|
+
data-slot="command"
|
|
21
|
+
className={cn(
|
|
22
|
+
"flex size-full flex-col overflow-hidden rounded-xl! bg-popover p-1 text-popover-foreground",
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function CommandDialog({
|
|
31
|
+
title = "Command Palette",
|
|
32
|
+
description = "Search for a command to run...",
|
|
33
|
+
children,
|
|
34
|
+
className,
|
|
35
|
+
overlayClassName,
|
|
36
|
+
showCloseButton: _showCloseButton = false,
|
|
37
|
+
...props
|
|
38
|
+
}: React.ComponentProps<typeof DialogPrimitive.Root> & {
|
|
39
|
+
title?: string
|
|
40
|
+
description?: string
|
|
41
|
+
className?: string
|
|
42
|
+
/** Backdrop — default is invisible (no dim/blur) but still captures outside clicks for modal behavior */
|
|
43
|
+
overlayClassName?: string
|
|
44
|
+
showCloseButton?: boolean
|
|
45
|
+
}) {
|
|
46
|
+
return (
|
|
47
|
+
<DialogPrimitive.Root {...props}>
|
|
48
|
+
<DialogPrimitive.Portal>
|
|
49
|
+
<DialogPrimitive.Overlay
|
|
50
|
+
className={cn(
|
|
51
|
+
"fixed inset-0 z-50 bg-transparent data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
|
52
|
+
overlayClassName,
|
|
53
|
+
)}
|
|
54
|
+
/>
|
|
55
|
+
<DialogPrimitive.Content
|
|
56
|
+
data-slot="dialog-content"
|
|
57
|
+
className={cn(
|
|
58
|
+
"fixed top-[max(1rem,8vh)] start-1/2 z-50 w-full max-w-[min(42rem,calc(100%-2rem))] -translate-x-1/2 rtl:translate-x-1/2 overflow-hidden rounded-xl bg-popover p-0 text-popover-foreground ring-1 ring-foreground/10 shadow-lg sm:max-w-3xl md:max-w-4xl data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
59
|
+
className,
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
<DialogPrimitive.Title className="sr-only">{title}</DialogPrimitive.Title>
|
|
63
|
+
<DialogPrimitive.Description className="sr-only">{description}</DialogPrimitive.Description>
|
|
64
|
+
{children}
|
|
65
|
+
</DialogPrimitive.Content>
|
|
66
|
+
</DialogPrimitive.Portal>
|
|
67
|
+
</DialogPrimitive.Root>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const CommandInput = React.forwardRef<
|
|
72
|
+
HTMLInputElement,
|
|
73
|
+
React.ComponentProps<typeof CommandPrimitive.Input> & {
|
|
74
|
+
/**
|
|
75
|
+
* `palette` — flat header row (icon + field) for ⌘K dialogs; matches blocks.so command-menu-02.
|
|
76
|
+
* `default` — pill `InputGroup` for embedded / library previews.
|
|
77
|
+
*/
|
|
78
|
+
variant?: "default" | "palette"
|
|
79
|
+
}
|
|
80
|
+
>(function CommandInput({ className, variant = "default", ...props }, ref) {
|
|
81
|
+
if (variant === "palette") {
|
|
82
|
+
return (
|
|
83
|
+
<div data-slot="command-input-wrapper" className="min-w-0 flex-1">
|
|
84
|
+
<div className="flex min-h-10 w-full items-center gap-2">
|
|
85
|
+
<SearchIcon className="size-4 shrink-0 opacity-50" aria-hidden />
|
|
86
|
+
<CommandPrimitive.Input
|
|
87
|
+
ref={ref}
|
|
88
|
+
data-slot="command-input"
|
|
89
|
+
className={cn(
|
|
90
|
+
// cmdk keeps DOM focus here; rows use accent fill (not rings) so this can match `Input` focus without doubling ring-on-ring with items.
|
|
91
|
+
"flex h-10 w-full min-w-0 flex-1 border-0 bg-transparent p-0 text-[15px] leading-normal text-foreground shadow-none outline-none ring-0 placeholder:text-muted-foreground focus:outline-none focus-visible:border-transparent focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50",
|
|
92
|
+
className
|
|
93
|
+
)}
|
|
94
|
+
{...props}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div data-slot="command-input-wrapper" className="p-1 pb-0">
|
|
103
|
+
<InputGroup className="h-8! rounded-lg! border-input/30 bg-input/30 shadow-none! *:data-[slot=input-group-addon]:ps-2!">
|
|
104
|
+
<CommandPrimitive.Input
|
|
105
|
+
ref={ref}
|
|
106
|
+
data-slot="command-input"
|
|
107
|
+
className={cn(
|
|
108
|
+
"w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
|
109
|
+
className
|
|
110
|
+
)}
|
|
111
|
+
{...props}
|
|
112
|
+
/>
|
|
113
|
+
<InputGroupAddon>
|
|
114
|
+
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
|
115
|
+
</InputGroupAddon>
|
|
116
|
+
</InputGroup>
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
CommandInput.displayName = "CommandInput"
|
|
121
|
+
|
|
122
|
+
function CommandList({
|
|
123
|
+
className,
|
|
124
|
+
...props
|
|
125
|
+
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
|
126
|
+
return (
|
|
127
|
+
<CommandPrimitive.List
|
|
128
|
+
data-slot="command-list"
|
|
129
|
+
className={cn(
|
|
130
|
+
"no-scrollbar max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto outline-none",
|
|
131
|
+
className
|
|
132
|
+
)}
|
|
133
|
+
{...props}
|
|
134
|
+
/>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function CommandEmpty({
|
|
139
|
+
className,
|
|
140
|
+
...props
|
|
141
|
+
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
|
142
|
+
return (
|
|
143
|
+
<CommandPrimitive.Empty
|
|
144
|
+
data-slot="command-empty"
|
|
145
|
+
className={cn("py-6 text-center text-sm", className)}
|
|
146
|
+
{...props}
|
|
147
|
+
/>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function CommandGroup({
|
|
152
|
+
className,
|
|
153
|
+
...props
|
|
154
|
+
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
|
155
|
+
return (
|
|
156
|
+
<CommandPrimitive.Group
|
|
157
|
+
data-slot="command-group"
|
|
158
|
+
className={cn(
|
|
159
|
+
"overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground",
|
|
160
|
+
className
|
|
161
|
+
)}
|
|
162
|
+
{...props}
|
|
163
|
+
/>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function CommandSeparator({
|
|
168
|
+
className,
|
|
169
|
+
...props
|
|
170
|
+
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
|
171
|
+
return (
|
|
172
|
+
<CommandPrimitive.Separator
|
|
173
|
+
data-slot="command-separator"
|
|
174
|
+
className={cn("-mx-1 h-px bg-border", className)}
|
|
175
|
+
{...props}
|
|
176
|
+
/>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function CommandItem({
|
|
181
|
+
className,
|
|
182
|
+
children,
|
|
183
|
+
...props
|
|
184
|
+
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
|
185
|
+
return (
|
|
186
|
+
<CommandPrimitive.Item
|
|
187
|
+
data-slot="command-item"
|
|
188
|
+
className={cn(
|
|
189
|
+
// Match popover lists (e.g. DropdownMenuItem): accent surface for hover + cmdk selection — same ring language as sidebar rows (ring-ring) is avoided here so input + row don’t read as double focus.
|
|
190
|
+
// NOTE: cmdk sets `data-selected="false"` on every item and `="true"` only on the highlighted one.
|
|
191
|
+
// Tailwind v4's bare `data-selected:` variant matches attribute PRESENCE, so it applies to every
|
|
192
|
+
// item — in HC mode, accent resolves to brand purple and every row looks selected. Match `=true`
|
|
193
|
+
// explicitly. Also dial HC selection to a border+ring affordance so the palette stays readable
|
|
194
|
+
// when only one row is truly active.
|
|
195
|
+
"group/command-item relative flex cursor-default items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-hidden ring-ring transition-colors select-none in-data-[slot=dialog-content]:rounded-lg! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:bg-accent hover:text-accent-foreground data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[selected=true]:*:[svg]:text-accent-foreground hc:data-[selected=true]:ring-2 hc:data-[selected=true]:ring-ring hc:data-[selected=true]:ring-inset forced-colors:data-[selected=true]:bg-[Highlight] forced-colors:data-[selected=true]:text-[HighlightText]",
|
|
196
|
+
className
|
|
197
|
+
)}
|
|
198
|
+
{...props}
|
|
199
|
+
>
|
|
200
|
+
{children}
|
|
201
|
+
<CheckIcon className="ms-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
|
|
202
|
+
</CommandPrimitive.Item>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function CommandShortcut({
|
|
207
|
+
className,
|
|
208
|
+
...props
|
|
209
|
+
}: React.ComponentProps<"span">) {
|
|
210
|
+
return (
|
|
211
|
+
<span
|
|
212
|
+
data-slot="command-shortcut"
|
|
213
|
+
className={cn(
|
|
214
|
+
"ms-auto text-xs tracking-widest text-muted-foreground group-data-[selected=true]/command-item:text-accent-foreground",
|
|
215
|
+
className
|
|
216
|
+
)}
|
|
217
|
+
{...props}
|
|
218
|
+
/>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export {
|
|
223
|
+
Command,
|
|
224
|
+
CommandDialog,
|
|
225
|
+
CommandInput,
|
|
226
|
+
CommandList,
|
|
227
|
+
CommandEmpty,
|
|
228
|
+
CommandGroup,
|
|
229
|
+
CommandItem,
|
|
230
|
+
CommandShortcut,
|
|
231
|
+
CommandSeparator,
|
|
232
|
+
}
|