@leitware/dockets 0.1.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 (135) hide show
  1. package/dist/cli.d.ts +3 -0
  2. package/dist/cli.d.ts.map +1 -0
  3. package/dist/cli.js +18 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/commands/add.d.ts +3 -0
  6. package/dist/commands/add.d.ts.map +1 -0
  7. package/dist/commands/add.js +86 -0
  8. package/dist/commands/add.js.map +1 -0
  9. package/dist/commands/list.d.ts +3 -0
  10. package/dist/commands/list.d.ts.map +1 -0
  11. package/dist/commands/list.js +36 -0
  12. package/dist/commands/list.js.map +1 -0
  13. package/dist/registry.d.ts +18 -0
  14. package/dist/registry.d.ts.map +1 -0
  15. package/dist/registry.js +712 -0
  16. package/dist/registry.js.map +1 -0
  17. package/package.json +40 -0
  18. package/templates/accordion.tsx +77 -0
  19. package/templates/alert-dialog.tsx +66 -0
  20. package/templates/alert.tsx +41 -0
  21. package/templates/aspect-ratio.tsx +15 -0
  22. package/templates/avatar.tsx +27 -0
  23. package/templates/badge.tsx +1 -0
  24. package/templates/block-loader.tsx +1 -0
  25. package/templates/breadcrumb.tsx +31 -0
  26. package/templates/button.tsx +1 -0
  27. package/templates/calendar.tsx +45 -0
  28. package/templates/card.tsx +35 -0
  29. package/templates/carousel.tsx +39 -0
  30. package/templates/checkbox.tsx +50 -0
  31. package/templates/code-block.tsx +1 -0
  32. package/templates/collapsible.tsx +35 -0
  33. package/templates/combobox.tsx +154 -0
  34. package/templates/command.tsx +50 -0
  35. package/templates/contact-footer.tsx +193 -0
  36. package/templates/context-menu.tsx +16 -0
  37. package/templates/dialog.tsx +67 -0
  38. package/templates/drawer.tsx +12 -0
  39. package/templates/dropdown-menu.tsx +95 -0
  40. package/templates/form-input.tsx +64 -0
  41. package/templates/form.tsx +10 -0
  42. package/templates/hover-card.tsx +5 -0
  43. package/templates/input-otp.tsx +6 -0
  44. package/templates/label.tsx +1 -0
  45. package/templates/layout-primitives.tsx +11 -0
  46. package/templates/layouts.tsx +346 -0
  47. package/templates/lib/utils.ts +49 -0
  48. package/templates/list-item.tsx +1 -0
  49. package/templates/list-items.tsx +41 -0
  50. package/templates/list.tsx +89 -0
  51. package/templates/logo.tsx +12 -0
  52. package/templates/marketing-footer.tsx +33 -0
  53. package/templates/marketing-header.tsx +46 -0
  54. package/templates/menubar.tsx +16 -0
  55. package/templates/navigation-menu.tsx +11 -0
  56. package/templates/pagination.tsx +86 -0
  57. package/templates/popover.tsx +8 -0
  58. package/templates/pricing-receipt.tsx +71 -0
  59. package/templates/pricing-tabs.tsx +60 -0
  60. package/templates/progress.tsx +29 -0
  61. package/templates/radio-group.tsx +58 -0
  62. package/templates/receipt-card.tsx +1 -0
  63. package/templates/receipt.tsx +269 -0
  64. package/templates/resizable.tsx +1 -0
  65. package/templates/scroll-area.tsx +1 -0
  66. package/templates/select.tsx +110 -0
  67. package/templates/separator.tsx +1 -0
  68. package/templates/sheet.tsx +12 -0
  69. package/templates/sidebar.tsx +15 -0
  70. package/templates/simple-footer.tsx +43 -0
  71. package/templates/simple-header.tsx +77 -0
  72. package/templates/skeleton.tsx +33 -0
  73. package/templates/slider.tsx +55 -0
  74. package/templates/styles/dockets.css +104 -0
  75. package/templates/switch.tsx +49 -0
  76. package/templates/table.tsx +73 -0
  77. package/templates/tabs.tsx +61 -0
  78. package/templates/theme-toggle.tsx +46 -0
  79. package/templates/toast.tsx +1 -0
  80. package/templates/toggle-group.tsx +1 -0
  81. package/templates/toggle.tsx +1 -0
  82. package/templates/tooltip.tsx +31 -0
  83. package/templates/tree-view.tsx +1 -0
  84. package/templates/ui/accordion.tsx +73 -0
  85. package/templates/ui/alert-dialog.tsx +128 -0
  86. package/templates/ui/alert.tsx +56 -0
  87. package/templates/ui/aspect-ratio.tsx +19 -0
  88. package/templates/ui/avatar.tsx +74 -0
  89. package/templates/ui/badge.tsx +48 -0
  90. package/templates/ui/block-loader.tsx +40 -0
  91. package/templates/ui/button.tsx +77 -0
  92. package/templates/ui/calendar.tsx +160 -0
  93. package/templates/ui/card.tsx +73 -0
  94. package/templates/ui/carousel.tsx +149 -0
  95. package/templates/ui/checkbox.tsx +33 -0
  96. package/templates/ui/code-block.tsx +36 -0
  97. package/templates/ui/collapsible.tsx +48 -0
  98. package/templates/ui/combobox.tsx +295 -0
  99. package/templates/ui/command.tsx +148 -0
  100. package/templates/ui/context-menu.tsx +212 -0
  101. package/templates/ui/dialog.tsx +138 -0
  102. package/templates/ui/drawer.tsx +134 -0
  103. package/templates/ui/dropdown-menu.tsx +254 -0
  104. package/templates/ui/form.tsx +122 -0
  105. package/templates/ui/hover-card.tsx +44 -0
  106. package/templates/ui/input-group.tsx +148 -0
  107. package/templates/ui/input-otp.tsx +153 -0
  108. package/templates/ui/input.tsx +20 -0
  109. package/templates/ui/label.tsx +17 -0
  110. package/templates/ui/layout.tsx +252 -0
  111. package/templates/ui/list-item.tsx +50 -0
  112. package/templates/ui/menubar.tsx +225 -0
  113. package/templates/ui/navigation-menu.tsx +117 -0
  114. package/templates/ui/pagination.tsx +110 -0
  115. package/templates/ui/popover.tsx +77 -0
  116. package/templates/ui/progress.tsx +37 -0
  117. package/templates/ui/radio-group.tsx +41 -0
  118. package/templates/ui/receipt-card.tsx +70 -0
  119. package/templates/ui/resizable.tsx +140 -0
  120. package/templates/ui/scroll-area.tsx +64 -0
  121. package/templates/ui/select.tsx +186 -0
  122. package/templates/ui/separator.tsx +21 -0
  123. package/templates/ui/sheet.tsx +134 -0
  124. package/templates/ui/sidebar.tsx +222 -0
  125. package/templates/ui/skeleton.tsx +35 -0
  126. package/templates/ui/slider.tsx +60 -0
  127. package/templates/ui/switch.tsx +33 -0
  128. package/templates/ui/table.tsx +114 -0
  129. package/templates/ui/tabs.tsx +79 -0
  130. package/templates/ui/textarea.tsx +18 -0
  131. package/templates/ui/toast.tsx +139 -0
  132. package/templates/ui/toggle-group.tsx +68 -0
  133. package/templates/ui/toggle.tsx +47 -0
  134. package/templates/ui/tooltip.tsx +53 -0
  135. package/templates/ui/tree-view.tsx +76 -0
@@ -0,0 +1,154 @@
1
+ import type * as React from 'react'
2
+ import { useRef, useState, useMemo } from 'react'
3
+ import { XIcon } from 'lucide-react'
4
+
5
+ import {
6
+ Combobox as ComboboxPrimitive,
7
+ ComboboxChipsInput,
8
+ ComboboxContent,
9
+ ComboboxEmpty,
10
+ ComboboxItem,
11
+ ComboboxList,
12
+ } from '@/components/ui/combobox'
13
+ import { cn } from '@/lib/utils'
14
+
15
+ export interface ComboboxOption {
16
+ value: string
17
+ label: string
18
+ icon?: React.ReactNode
19
+ description?: string
20
+ }
21
+
22
+ export interface ComboboxProps {
23
+ options?: ComboboxOption[]
24
+ value?: string[]
25
+ onValueChange?: (ids: string[]) => void
26
+ placeholder?: string
27
+ emptyMessage?: string
28
+ label?: string
29
+ disabled?: boolean
30
+ className?: string
31
+ children?: React.ReactNode
32
+ }
33
+
34
+ function Combobox({
35
+ options,
36
+ value = [],
37
+ onValueChange,
38
+ placeholder = 'Search...',
39
+ emptyMessage = 'No results found.',
40
+ label,
41
+ disabled = false,
42
+ className,
43
+ children,
44
+ }: ComboboxProps) {
45
+ const [inputValue, setInputValue] = useState('')
46
+ const anchorRef = useRef<HTMLDivElement>(null)
47
+
48
+ const optionsByValue = useMemo(() => {
49
+ const map = new Map<string, ComboboxOption>()
50
+ for (const opt of options ?? []) {
51
+ map.set(opt.value, opt)
52
+ }
53
+ return map
54
+ }, [options])
55
+
56
+ const filtered = useMemo(() => {
57
+ if (!options) return []
58
+ if (!inputValue) return options
59
+ const lower = inputValue.toLowerCase()
60
+ return options.filter(
61
+ (opt) =>
62
+ opt.label.toLowerCase().includes(lower) ||
63
+ opt.description?.toLowerCase().includes(lower),
64
+ )
65
+ }, [options, inputValue])
66
+
67
+ if (children) {
68
+ return (
69
+ <ComboboxPrimitive multiple disabled={disabled}>
70
+ {children}
71
+ </ComboboxPrimitive>
72
+ )
73
+ }
74
+
75
+ const removeValue = (v: string) => {
76
+ onValueChange?.(value.filter((x) => x !== v))
77
+ }
78
+
79
+ return (
80
+ <div className={cn('flex flex-col gap-1', className)}>
81
+ {label && (
82
+ <span className="text-xs font-medium uppercase tracking-wider">
83
+ {label}
84
+ </span>
85
+ )}
86
+ <ComboboxPrimitive
87
+ multiple
88
+ value={value}
89
+ onValueChange={(val) => onValueChange?.(val as string[])}
90
+ disabled={disabled}
91
+ >
92
+ {/* Selected chips + inline search input */}
93
+ <div ref={anchorRef} className="flex min-h-8 flex-wrap items-center gap-1 border border-input bg-transparent px-2.5 py-1 text-xs">
94
+ {value.map((v) => {
95
+ const opt = optionsByValue.get(v)
96
+ return (
97
+ <span
98
+ key={v}
99
+ className="flex items-center gap-1 bg-muted px-1.5 py-0.5 text-xs font-medium whitespace-nowrap"
100
+ >
101
+ {opt?.icon && <span className="shrink-0">{opt.icon}</span>}
102
+ {opt?.label ?? v}
103
+ <button
104
+ type="button"
105
+ onClick={(e) => {
106
+ e.stopPropagation()
107
+ removeValue(v)
108
+ }}
109
+ className="opacity-50 hover:opacity-100 cursor-pointer bg-transparent border-none p-0"
110
+ aria-label={`Remove ${opt?.label ?? v}`}
111
+ >
112
+ <XIcon className="w-3 h-3" />
113
+ </button>
114
+ </span>
115
+ )
116
+ })}
117
+ <ComboboxChipsInput
118
+ placeholder={value.length === 0 ? placeholder : ''}
119
+ value={inputValue}
120
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => setInputValue(e.target.value)}
121
+ />
122
+ </div>
123
+ <ComboboxContent anchor={anchorRef}>
124
+ <ComboboxList>
125
+ {filtered.map((opt) => (
126
+ <ComboboxItem key={opt.value} value={opt.value}>
127
+ {opt.icon && (
128
+ <span className="shrink-0">{opt.icon}</span>
129
+ )}
130
+ <span className="flex-1 min-w-0">
131
+ <span className="text-xs">{opt.label}</span>
132
+ {opt.description && (
133
+ <span className="block text-[10px] text-muted-foreground">
134
+ {opt.description}
135
+ </span>
136
+ )}
137
+ </span>
138
+ </ComboboxItem>
139
+ ))}
140
+ </ComboboxList>
141
+ <ComboboxEmpty>{emptyMessage}</ComboboxEmpty>
142
+ </ComboboxContent>
143
+ </ComboboxPrimitive>
144
+ </div>
145
+ )
146
+ }
147
+
148
+ export {
149
+ Combobox,
150
+ ComboboxContent,
151
+ ComboboxEmpty,
152
+ ComboboxItem,
153
+ ComboboxList,
154
+ }
@@ -0,0 +1,50 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import {
5
+ Command,
6
+ CommandInput,
7
+ CommandList,
8
+ CommandEmpty,
9
+ CommandGroup,
10
+ CommandItem,
11
+ CommandSeparator,
12
+ CommandShortcut,
13
+ } from '@/components/ui/command'
14
+ import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
15
+ import { cn } from '@/lib/utils'
16
+
17
+ function CommandDialog({
18
+ open,
19
+ onOpenChange,
20
+ children,
21
+ }: {
22
+ open?: boolean
23
+ onOpenChange?: (open: boolean) => void
24
+ children?: React.ReactNode
25
+ }) {
26
+ return (
27
+ <DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
28
+ <DialogPrimitive.Portal>
29
+ <DialogPrimitive.Backdrop className="fixed inset-0 z-50 bg-black/10 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0" />
30
+ <DialogPrimitive.Popup className="fixed top-[20%] left-1/2 z-50 w-full max-w-lg -translate-x-1/2 rounded-[var(--radius)] border-[length:var(--border-width)] border-foreground bg-popover text-popover-foreground shadow-none outline-none 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">
31
+ <Command className="border-0">
32
+ {children}
33
+ </Command>
34
+ </DialogPrimitive.Popup>
35
+ </DialogPrimitive.Portal>
36
+ </DialogPrimitive.Root>
37
+ )
38
+ }
39
+
40
+ export {
41
+ Command,
42
+ CommandDialog,
43
+ CommandInput,
44
+ CommandList,
45
+ CommandEmpty,
46
+ CommandGroup,
47
+ CommandItem,
48
+ CommandSeparator,
49
+ CommandShortcut,
50
+ }
@@ -0,0 +1,193 @@
1
+ import { useMutation } from 'convex/react'
2
+ import { Check } from 'lucide-react'
3
+ import { BlockLoader } from '@/components/block-loader'
4
+ import { useState } from 'react'
5
+ import { FormInput } from '@/components/form-input'
6
+ import { Button } from '@/components/button'
7
+ import { CAL_LINK, COMPANY_EMAIL, COMPANY_PHONE } from '@/lib/constants'
8
+ import { api } from '../../convex/_generated/api'
9
+
10
+ interface ContactFooterProps {
11
+ /**
12
+ * Override the Cal.com booking link
13
+ * @default CAL_LINK
14
+ */
15
+ calLink?: string
16
+ }
17
+
18
+ export function ReceiptDivider() {
19
+ return (
20
+ <div className="text-xs my-3 tracking-[-1px] h-[1em] relative text-center overflow-hidden">
21
+ <span className="absolute left-1/2 top-0 -translate-x-1/2 whitespace-nowrap tracking-[-1px]">
22
+ ..................................................
23
+ </span>
24
+ </div>
25
+ )
26
+ }
27
+
28
+ export function SectionHeader({ children }: { children: React.ReactNode }) {
29
+ return (
30
+ <div className="text-[11px] font-bold mb-2 text-center py-[3px] border-t border-b bg-[var(--border-color)] text-[var(--receipt-bg)]">
31
+ {children}
32
+ </div>
33
+ )
34
+ }
35
+
36
+ export function ContactFooter({ calLink = CAL_LINK }: ContactFooterProps) {
37
+ return (
38
+ <footer className="mt-12 max-w-[1152px] mx-auto px-6">
39
+ <div className="bg-[var(--receipt-bg)] border border-b-0">
40
+ <div className="max-w-[576px] mx-auto px-6 pt-6 pb-12">
41
+ <div className="text-center h-12 mb-3">
42
+ <div className="text-base font-bold mb-1"># CONTACT</div>
43
+ <div className="text-xs uppercase">request follow up</div>
44
+ <div className="text-[10px] text-[var(--muted-color)] mt-0.5 h-4">
45
+ or{' '}
46
+ <a href={calLink} className="underline">
47
+ book a call instead
48
+ </a>
49
+ </div>
50
+ </div>
51
+
52
+ <ReceiptDivider />
53
+
54
+ <SectionHeader>YOUR DETAILS</SectionHeader>
55
+
56
+ <FollowUpForm calLink={calLink} />
57
+
58
+ <div className="mt-6">
59
+ <ReceiptDivider />
60
+ </div>
61
+
62
+ <div className="text-center pt-4 text-[11px]">
63
+ <div className="mb-4">Thank You</div>
64
+ <div className="text-[var(--muted-color)] text-[10px] uppercase mb-1">
65
+ © {new Date().getFullYear()} LEITWARE
66
+ </div>
67
+ <div className="text-[var(--muted-color)] text-[10px] mb-1">
68
+ <a href={`mailto:${COMPANY_EMAIL}`} className="no-underline hover:underline">
69
+ {COMPANY_EMAIL}
70
+ </a>
71
+ </div>
72
+ <div className="text-[var(--muted-color)] text-[10px] mb-2">
73
+ <a href={`tel:${COMPANY_PHONE}`} className="no-underline hover:underline">
74
+ +44 778 004 2494
75
+ </a>
76
+ </div>
77
+ <div className="flex gap-4 justify-center">
78
+ <a
79
+ href="/terms-and-conditions"
80
+ className="text-[var(--muted-color)] text-[10px] uppercase no-underline hover:underline"
81
+ >
82
+ Terms &amp; Conditions
83
+ </a>
84
+ <a
85
+ href="/privacy"
86
+ className="text-[var(--muted-color)] text-[10px] uppercase no-underline hover:underline"
87
+ >
88
+ Privacy Policy
89
+ </a>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </footer>
95
+ )
96
+ }
97
+
98
+ function FollowUpForm({ calLink = CAL_LINK }: { calLink?: string }) {
99
+ const submitFollowUp = useMutation(api.myFunctions.submitFollowUp)
100
+ const [isSubmitting, setIsSubmitting] = useState(false)
101
+ const [isSubmitted, setIsSubmitted] = useState(false)
102
+ const [error, setError] = useState<string | null>(null)
103
+
104
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
105
+ e.preventDefault()
106
+ setIsSubmitting(true)
107
+ setError(null)
108
+
109
+ const formData = new FormData(e.currentTarget)
110
+
111
+ try {
112
+ await submitFollowUp({
113
+ email: formData.get('email') as string,
114
+ name: formData.get('name') as string,
115
+ companyName: formData.get('companyName') as string,
116
+ website: (formData.get('website') as string) || undefined,
117
+ useCases: formData.get('useCases') as string,
118
+ otherDetails: (formData.get('otherDetails') as string) || undefined,
119
+ })
120
+ setIsSubmitted(true)
121
+ } catch {
122
+ setError('Something went wrong. Please try again.')
123
+ } finally {
124
+ setIsSubmitting(false)
125
+ }
126
+ }
127
+
128
+ if (isSubmitted) {
129
+ return (
130
+ <div className="text-center py-8">
131
+ <Check className="w-6 h-6 mx-auto mb-3" />
132
+ <div className="font-bold uppercase text-xs">Received!</div>
133
+ <div className="text-[var(--muted-color)] text-[11px] mt-1">We'll be in touch soon.</div>
134
+ </div>
135
+ )
136
+ }
137
+
138
+ return (
139
+ <form onSubmit={handleSubmit}>
140
+ <div className="grid grid-cols-2 gap-3 mb-3">
141
+ <FormInput label="Name" required name="name" autoComplete="name" />
142
+ <FormInput label="Email" required name="email" type="email" autoComplete="email" />
143
+ </div>
144
+ <div className="grid grid-cols-2 gap-3 mb-3">
145
+ <FormInput label="Company" required name="companyName" autoComplete="organization" />
146
+ <FormInput label="Website" name="website" type="url" placeholder="https://..." autoComplete="url" />
147
+ </div>
148
+ <div className="mb-3">
149
+ <label htmlFor="useCases" className="text-xs font-medium uppercase tracking-wider">
150
+ What do you want to automate?<span className="text-destructive ml-0.5">*</span>
151
+ </label>
152
+ <textarea
153
+ id="useCases"
154
+ name="useCases"
155
+ required
156
+ rows={3}
157
+ className="w-full p-2 text-xs border bg-[var(--receipt-bg)] text-[var(--text-color)] resize-none mt-1"
158
+ />
159
+ </div>
160
+ <div className="mb-4">
161
+ <label htmlFor="otherDetails" className="text-xs font-medium uppercase tracking-wider">
162
+ Other Details
163
+ </label>
164
+ <textarea
165
+ id="otherDetails"
166
+ name="otherDetails"
167
+ rows={2}
168
+ className="w-full p-2 text-xs border bg-[var(--receipt-bg)] text-[var(--text-color)] resize-none mt-1"
169
+ />
170
+ </div>
171
+ {error && <p className="text-[11px] text-destructive mb-3">{error}</p>}
172
+ <Button type="submit" disabled={isSubmitting} className="w-full">
173
+ {isSubmitting ? (
174
+ <>
175
+ <BlockLoader />
176
+ Processing...
177
+ </>
178
+ ) : (
179
+ 'Request Follow Up'
180
+ )}
181
+ </Button>
182
+ <div className="text-center mt-3">
183
+ <Button
184
+ render={<a href={calLink} />}
185
+ variant="link"
186
+ className="text-[10px] text-muted-foreground"
187
+ >
188
+ or book a call instead →
189
+ </Button>
190
+ </div>
191
+ </form>
192
+ )
193
+ }
@@ -0,0 +1,16 @@
1
+ export {
2
+ ContextMenu,
3
+ ContextMenuTrigger,
4
+ ContextMenuPortal,
5
+ ContextMenuContent,
6
+ ContextMenuItem,
7
+ ContextMenuCheckboxItem,
8
+ ContextMenuRadioGroup,
9
+ ContextMenuRadioItem,
10
+ ContextMenuLabel,
11
+ ContextMenuSeparator,
12
+ ContextMenuShortcut,
13
+ ContextMenuSub,
14
+ ContextMenuSubTrigger,
15
+ ContextMenuSubContent,
16
+ } from '@/components/ui/context-menu'
@@ -0,0 +1,67 @@
1
+ import type { Dialog as DialogPrimitiveBase } from '@base-ui/react/dialog'
2
+ import type * as React from 'react'
3
+
4
+ import {
5
+ DialogClose,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogFooter,
9
+ DialogHeader,
10
+ Dialog as DialogPrimitive,
11
+ DialogTitle,
12
+ DialogTrigger,
13
+ } from '@/components/ui/dialog'
14
+
15
+ export interface DialogProps extends DialogPrimitiveBase.Root.Props {
16
+ trigger?: React.ReactNode
17
+ title?: React.ReactNode
18
+ description?: React.ReactNode
19
+ footer?: React.ReactNode
20
+ showCloseButton?: boolean
21
+ contentClassName?: string
22
+ children?: React.ReactNode
23
+ }
24
+
25
+ function Dialog({
26
+ trigger,
27
+ title,
28
+ description,
29
+ footer,
30
+ showCloseButton = true,
31
+ contentClassName,
32
+ children,
33
+ ...dialogProps
34
+ }: DialogProps) {
35
+ // Opinionated API: trigger + title + description + content + footer
36
+ if (trigger || title) {
37
+ return (
38
+ <DialogPrimitive {...dialogProps}>
39
+ {trigger && <DialogTrigger>{trigger}</DialogTrigger>}
40
+ <DialogContent showCloseButton={showCloseButton} className={contentClassName}>
41
+ {(title || description) && (
42
+ <DialogHeader>
43
+ {title && <DialogTitle>{title}</DialogTitle>}
44
+ {description && <DialogDescription>{description}</DialogDescription>}
45
+ </DialogHeader>
46
+ )}
47
+ {children}
48
+ {footer && <DialogFooter>{footer}</DialogFooter>}
49
+ </DialogContent>
50
+ </DialogPrimitive>
51
+ )
52
+ }
53
+
54
+ // Fallback: children-based composition
55
+ return <DialogPrimitive {...dialogProps}>{children}</DialogPrimitive>
56
+ }
57
+
58
+ export {
59
+ Dialog,
60
+ DialogClose,
61
+ DialogContent,
62
+ DialogDescription,
63
+ DialogFooter,
64
+ DialogHeader,
65
+ DialogTitle,
66
+ DialogTrigger,
67
+ }
@@ -0,0 +1,12 @@
1
+ export {
2
+ Drawer,
3
+ DrawerTrigger,
4
+ DrawerClose,
5
+ DrawerPortal,
6
+ DrawerOverlay,
7
+ DrawerContent,
8
+ DrawerHeader,
9
+ DrawerFooter,
10
+ DrawerTitle,
11
+ DrawerDescription,
12
+ } from '@/components/ui/drawer'
@@ -0,0 +1,95 @@
1
+ import type { Menu as MenuPrimitiveBase } from '@base-ui/react/menu'
2
+ import type * as React from 'react'
3
+
4
+ import {
5
+ DropdownMenuContent,
6
+ DropdownMenuItem,
7
+ DropdownMenu as DropdownMenuPrimitive,
8
+ DropdownMenuSeparator,
9
+ DropdownMenuTrigger,
10
+ } from '@/components/ui/dropdown-menu'
11
+
12
+ export interface DropdownMenuItemData {
13
+ label: React.ReactNode
14
+ onClick?: () => void
15
+ icon?: React.ReactNode
16
+ variant?: 'default' | 'destructive'
17
+ disabled?: boolean
18
+ separator?: false
19
+ }
20
+
21
+ export interface DropdownMenuSeparatorData {
22
+ separator: true
23
+ }
24
+
25
+ export type DropdownMenuEntry = DropdownMenuItemData | DropdownMenuSeparatorData
26
+
27
+ export interface DropdownMenuProps {
28
+ trigger: React.ReactNode
29
+ items?: DropdownMenuEntry[]
30
+ align?: 'start' | 'center' | 'end'
31
+ side?: 'top' | 'bottom' | 'left' | 'right'
32
+ contentClassName?: string
33
+ children?: React.ReactNode
34
+ open?: boolean
35
+ onOpenChange?: (open: boolean) => void
36
+ }
37
+
38
+ function DropdownMenu({
39
+ trigger,
40
+ items,
41
+ align = 'start',
42
+ side = 'bottom',
43
+ contentClassName,
44
+ children,
45
+ open,
46
+ onOpenChange,
47
+ }: DropdownMenuProps) {
48
+ const rootProps: MenuPrimitiveBase.Root.Props = {}
49
+ if (open !== undefined) rootProps.open = open
50
+ if (onOpenChange !== undefined) rootProps.onOpenChange = onOpenChange
51
+
52
+ if (items) {
53
+ return (
54
+ <DropdownMenuPrimitive {...rootProps}>
55
+ <DropdownMenuTrigger>{trigger}</DropdownMenuTrigger>
56
+ <DropdownMenuContent align={align} side={side} className={contentClassName}>
57
+ {items.map((entry, i) => {
58
+ if (entry.separator) {
59
+ return <DropdownMenuSeparator key={`sep-${i}`} />
60
+ }
61
+ return (
62
+ <DropdownMenuItem
63
+ key={`item-${i}`}
64
+ onSelect={entry.onClick}
65
+ variant={entry.variant}
66
+ disabled={entry.disabled}
67
+ >
68
+ {entry.icon}
69
+ {entry.label}
70
+ </DropdownMenuItem>
71
+ )
72
+ })}
73
+ </DropdownMenuContent>
74
+ </DropdownMenuPrimitive>
75
+ )
76
+ }
77
+
78
+ return (
79
+ <DropdownMenuPrimitive {...rootProps}>
80
+ <DropdownMenuTrigger>{trigger}</DropdownMenuTrigger>
81
+ <DropdownMenuContent align={align} side={side} className={contentClassName}>
82
+ {children}
83
+ </DropdownMenuContent>
84
+ </DropdownMenuPrimitive>
85
+ )
86
+ }
87
+
88
+ // Re-export sub-components for advanced use
89
+ export {
90
+ DropdownMenu,
91
+ DropdownMenuContent,
92
+ DropdownMenuItem,
93
+ DropdownMenuSeparator,
94
+ DropdownMenuTrigger,
95
+ }
@@ -0,0 +1,64 @@
1
+ import * as React from 'react'
2
+
3
+ import { Input } from '@/components/ui/input'
4
+ import { cn } from '@/lib/utils'
5
+
6
+ export interface FormInputProps extends Omit<React.ComponentProps<'input'>, 'size'> {
7
+ label?: string
8
+ description?: string
9
+ error?: string
10
+ disabled?: boolean
11
+ required?: boolean
12
+ className?: string
13
+ }
14
+
15
+ function FormInput({
16
+ label,
17
+ description,
18
+ error,
19
+ disabled = false,
20
+ required = false,
21
+ className,
22
+ id,
23
+ ...inputProps
24
+ }: FormInputProps) {
25
+ const generatedId = React.useId()
26
+ const inputId = id || generatedId
27
+ const hasError = Boolean(error)
28
+
29
+ return (
30
+ <div className={cn('flex flex-col gap-1', className)} data-invalid={hasError || undefined}>
31
+ {label && (
32
+ <label htmlFor={inputId} className="text-xs font-medium uppercase tracking-wider">
33
+ {label}
34
+ {required && <span className="text-destructive ml-0.5">*</span>}
35
+ </label>
36
+ )}
37
+ <Input
38
+ {...inputProps}
39
+ id={inputId}
40
+ disabled={disabled}
41
+ required={required}
42
+ aria-invalid={hasError}
43
+ aria-required={required}
44
+ aria-describedby={
45
+ [description ? `${inputId}-desc` : null, error ? `${inputId}-error` : null]
46
+ .filter(Boolean)
47
+ .join(' ') || undefined
48
+ }
49
+ />
50
+ {description && (
51
+ <p id={`${inputId}-desc`} className="text-[10px] text-muted-foreground">
52
+ {description}
53
+ </p>
54
+ )}
55
+ {error && (
56
+ <p id={`${inputId}-error`} className="text-[10px] text-destructive">
57
+ {error}
58
+ </p>
59
+ )}
60
+ </div>
61
+ )
62
+ }
63
+
64
+ export { FormInput }
@@ -0,0 +1,10 @@
1
+ export {
2
+ Form,
3
+ FormField,
4
+ FormLabel,
5
+ FormControl,
6
+ FormDescription,
7
+ FormMessage,
8
+ FormItem,
9
+ useFormField,
10
+ } from '@/components/ui/form'
@@ -0,0 +1,5 @@
1
+ export {
2
+ HoverCard,
3
+ HoverCardTrigger,
4
+ HoverCardContent,
5
+ } from '@/components/ui/hover-card'
@@ -0,0 +1,6 @@
1
+ export {
2
+ InputOTP,
3
+ InputOTPGroup,
4
+ InputOTPSlot,
5
+ InputOTPSeparator,
6
+ } from '@/components/ui/input-otp'
@@ -0,0 +1 @@
1
+ export { Label } from '@/components/ui/label'
@@ -0,0 +1,11 @@
1
+ export {
2
+ Container,
3
+ Section,
4
+ Stack,
5
+ Row,
6
+ Spacer,
7
+ Divider,
8
+ Grid,
9
+ BentoGrid,
10
+ BentoCell,
11
+ } from '@/components/ui/layout'