@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,153 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '@/lib/utils'
5
+
6
+ interface InputOTPProps {
7
+ length?: number
8
+ value?: string
9
+ onChange?: (value: string) => void
10
+ disabled?: boolean
11
+ className?: string
12
+ pattern?: RegExp
13
+ }
14
+
15
+ function InputOTP({
16
+ length = 6,
17
+ value = '',
18
+ onChange,
19
+ disabled,
20
+ className,
21
+ pattern = /^[0-9]*$/,
22
+ }: InputOTPProps) {
23
+ const inputRefs = React.useRef<(HTMLInputElement | null)[]>([])
24
+
25
+ const chars = Array.from({ length }, (_, i) => value[i] ?? '')
26
+
27
+ const handleChange = (idx: number, char: string) => {
28
+ if (char && !pattern.test(char)) return
29
+ const next = chars.map((c, i) => (i === idx ? char.slice(-1) : c)).join('')
30
+ onChange?.(next)
31
+ if (char && idx < length - 1) {
32
+ inputRefs.current[idx + 1]?.focus()
33
+ }
34
+ }
35
+
36
+ const handleKeyDown = (idx: number, e: React.KeyboardEvent<HTMLInputElement>) => {
37
+ if (e.key === 'Backspace' && !chars[idx] && idx > 0) {
38
+ inputRefs.current[idx - 1]?.focus()
39
+ const next = chars.map((c, i) => (i === idx - 1 ? '' : c)).join('')
40
+ onChange?.(next)
41
+ }
42
+ if (e.key === 'ArrowLeft' && idx > 0) inputRefs.current[idx - 1]?.focus()
43
+ if (e.key === 'ArrowRight' && idx < length - 1) inputRefs.current[idx + 1]?.focus()
44
+ }
45
+
46
+ const handlePaste = (e: React.ClipboardEvent) => {
47
+ e.preventDefault()
48
+ const pasted = e.clipboardData.getData('text').slice(0, length)
49
+ if (!pattern.test(pasted)) return
50
+ onChange?.(pasted.padEnd(0, ''))
51
+ const focusIdx = Math.min(pasted.length, length - 1)
52
+ inputRefs.current[focusIdx]?.focus()
53
+ }
54
+
55
+ return (
56
+ <div
57
+ data-slot="input-otp"
58
+ className={cn('flex items-center gap-0', className)}
59
+ >
60
+ {chars.map((char, idx) => (
61
+ <React.Fragment key={idx}>
62
+ {idx > 0 && idx % 3 === 0 && (
63
+ <div
64
+ data-slot="input-otp-separator"
65
+ className="flex w-4 items-center justify-center text-xs text-muted-foreground select-none"
66
+ aria-hidden
67
+ >
68
+
69
+ </div>
70
+ )}
71
+ <input
72
+ ref={(el) => { inputRefs.current[idx] = el }}
73
+ type="text"
74
+ inputMode="numeric"
75
+ maxLength={1}
76
+ value={char}
77
+ disabled={disabled}
78
+ onChange={(e) => handleChange(idx, e.target.value)}
79
+ onKeyDown={(e) => handleKeyDown(idx, e)}
80
+ onPaste={handlePaste}
81
+ onFocus={(e) => e.target.select()}
82
+ className={cn(
83
+ 'flex h-10 w-10 items-center justify-center rounded-[var(--radius)] border-[length:var(--border-width)] border-foreground bg-transparent text-center text-sm font-medium caret-transparent outline-none',
84
+ 'first-of-type:rounded-[var(--radius)]',
85
+ // No stacking: right border only on all-but-last boxes when not separated
86
+ idx > 0 && idx % 3 !== 0 && '-ml-[length:var(--border-width)]',
87
+ 'focus:z-10 focus:border-foreground focus:ring-1 focus:ring-foreground/30',
88
+ 'disabled:cursor-not-allowed disabled:opacity-50',
89
+ )}
90
+ aria-label={`Digit ${idx + 1}`}
91
+ />
92
+ </React.Fragment>
93
+ ))}
94
+ </div>
95
+ )
96
+ }
97
+
98
+ function InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>) {
99
+ return (
100
+ <div
101
+ data-slot="input-otp-group"
102
+ className={cn('flex items-center', className)}
103
+ {...props}
104
+ />
105
+ )
106
+ }
107
+
108
+ function InputOTPSlot({
109
+ index,
110
+ active,
111
+ char,
112
+ className,
113
+ }: {
114
+ index: number
115
+ active?: boolean
116
+ char?: string
117
+ className?: string
118
+ }) {
119
+ return (
120
+ <div
121
+ data-slot="input-otp-slot"
122
+ data-active={active}
123
+ className={cn(
124
+ 'relative flex h-10 w-10 items-center justify-center rounded-[var(--radius)] border-[length:var(--border-width)] border-foreground text-sm font-medium',
125
+ index !== 0 && '-ml-[length:var(--border-width)]',
126
+ active && 'z-10 ring-1 ring-foreground',
127
+ className,
128
+ )}
129
+ >
130
+ {char}
131
+ {active && (
132
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
133
+ <div className="animate-caret-blink h-4 w-px bg-foreground duration-1000" />
134
+ </div>
135
+ )}
136
+ </div>
137
+ )
138
+ }
139
+
140
+ function InputOTPSeparator({ className, ...props }: React.ComponentProps<'div'>) {
141
+ return (
142
+ <div
143
+ data-slot="input-otp-separator"
144
+ role="separator"
145
+ className={cn('flex w-4 items-center justify-center text-xs text-muted-foreground', className)}
146
+ {...props}
147
+ >
148
+
149
+ </div>
150
+ )
151
+ }
152
+
153
+ export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
@@ -0,0 +1,20 @@
1
+ import { Input as InputPrimitive } from '@base-ui/react/input'
2
+ import type * as React from 'react'
3
+
4
+ import { cn } from '@/lib/utils'
5
+
6
+ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
7
+ return (
8
+ <InputPrimitive
9
+ type={type}
10
+ data-slot="input"
11
+ className={cn(
12
+ 'h-8 w-full min-w-0 rounded-[var(--radius)] border-[length:var(--border-width)] border-input bg-transparent px-2.5 py-1 text-xs file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-xs file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive md:text-xs dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50',
13
+ className,
14
+ )}
15
+ {...props}
16
+ />
17
+ )
18
+ }
19
+
20
+ export { Input }
@@ -0,0 +1,17 @@
1
+ import * as React from 'react'
2
+ import { cn } from '@/lib/utils'
3
+
4
+ function Label({ className, ...props }: React.ComponentProps<'label'>) {
5
+ return (
6
+ <label
7
+ data-slot="label"
8
+ className={cn(
9
+ 'text-xs font-medium uppercase tracking-wider leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
10
+ className,
11
+ )}
12
+ {...props}
13
+ />
14
+ )
15
+ }
16
+
17
+ export { Label }
@@ -0,0 +1,252 @@
1
+ // ─── Layout Primitives ────────────────────────────────────────────────────────
2
+ //
3
+ // These components enforce the dockets spacing and border system:
4
+ //
5
+ // BORDERS NEVER STACK RULES:
6
+ // • Stack/Row gaps use margin (not padding) so items never double-border
7
+ // • BentoGrid: container holds border-t + border-l.
8
+ // BentoCells add border-b + border-r → each edge drawn exactly once.
9
+ // • Use `border-[length:var(--border-width)]` throughout, never hardcoded 1px.
10
+ //
11
+ // SPACING:
12
+ // All padding/gap values pull from Tailwind's scale (1=4px, 2=8px, …).
13
+ // Macro layout values come from --space-layout-sm/md/lg defined in dockets.css.
14
+
15
+ import * as React from 'react'
16
+ import { cn } from '@/lib/utils'
17
+
18
+ // ─── Container ────────────────────────────────────────────────────────────────
19
+
20
+ interface ContainerProps extends React.ComponentProps<'div'> {
21
+ size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
22
+ }
23
+
24
+ const containerSizes = {
25
+ sm: 'max-w-2xl',
26
+ md: 'max-w-4xl',
27
+ lg: 'max-w-6xl',
28
+ xl: 'max-w-7xl',
29
+ full: 'max-w-none',
30
+ }
31
+
32
+ function Container({ className, size = 'lg', ...props }: ContainerProps) {
33
+ return (
34
+ <div
35
+ data-slot="container"
36
+ className={cn('mx-auto w-full px-4 sm:px-6', containerSizes[size], className)}
37
+ {...props}
38
+ />
39
+ )
40
+ }
41
+
42
+ // ─── Section ──────────────────────────────────────────────────────────────────
43
+
44
+ interface SectionProps extends React.ComponentProps<'section'> {
45
+ spacing?: 'sm' | 'md' | 'lg'
46
+ }
47
+
48
+ const sectionSpacing = {
49
+ sm: 'py-[var(--space-layout-sm)]',
50
+ md: 'py-[var(--space-layout-md)]',
51
+ lg: 'py-[var(--space-layout-lg)]',
52
+ }
53
+
54
+ function Section({ className, spacing = 'md', ...props }: SectionProps) {
55
+ return (
56
+ <section
57
+ data-slot="section"
58
+ className={cn('w-full', sectionSpacing[spacing], className)}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ // ─── Stack (vertical flex) ────────────────────────────────────────────────────
65
+
66
+ interface StackProps extends React.ComponentProps<'div'> {
67
+ gap?: 1 | 2 | 3 | 4 | 5 | 6 | 8
68
+ align?: 'start' | 'center' | 'end' | 'stretch'
69
+ }
70
+
71
+ const stackGaps: Record<number, string> = {
72
+ 1: 'gap-1', 2: 'gap-2', 3: 'gap-3', 4: 'gap-4', 5: 'gap-5', 6: 'gap-6', 8: 'gap-8',
73
+ }
74
+
75
+ const alignItems: Record<string, string> = {
76
+ start: 'items-start', center: 'items-center', end: 'items-end', stretch: 'items-stretch',
77
+ }
78
+
79
+ function Stack({ className, gap = 4, align = 'stretch', ...props }: StackProps) {
80
+ return (
81
+ <div
82
+ data-slot="stack"
83
+ className={cn('flex flex-col', stackGaps[gap], alignItems[align], className)}
84
+ {...props}
85
+ />
86
+ )
87
+ }
88
+
89
+ // ─── Row (horizontal flex) ────────────────────────────────────────────────────
90
+
91
+ interface RowProps extends React.ComponentProps<'div'> {
92
+ gap?: 1 | 2 | 3 | 4 | 5 | 6 | 8
93
+ align?: 'start' | 'center' | 'end' | 'baseline' | 'stretch'
94
+ justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'
95
+ wrap?: boolean
96
+ }
97
+
98
+ const justifyContent: Record<string, string> = {
99
+ start: 'justify-start', center: 'justify-center', end: 'justify-end',
100
+ between: 'justify-between', around: 'justify-around', evenly: 'justify-evenly',
101
+ }
102
+
103
+ function Row({ className, gap = 4, align = 'center', justify = 'start', wrap = false, ...props }: RowProps) {
104
+ return (
105
+ <div
106
+ data-slot="row"
107
+ className={cn(
108
+ 'flex flex-row',
109
+ stackGaps[gap],
110
+ alignItems[align],
111
+ justifyContent[justify],
112
+ wrap && 'flex-wrap',
113
+ className,
114
+ )}
115
+ {...props}
116
+ />
117
+ )
118
+ }
119
+
120
+ // ─── Spacer ───────────────────────────────────────────────────────────────────
121
+
122
+ function Spacer({ className, ...props }: React.ComponentProps<'div'>) {
123
+ return (
124
+ <div
125
+ data-slot="spacer"
126
+ className={cn('flex-1', className)}
127
+ aria-hidden
128
+ {...props}
129
+ />
130
+ )
131
+ }
132
+
133
+ // ─── Divider ──────────────────────────────────────────────────────────────────
134
+
135
+ interface DividerProps extends React.ComponentProps<'hr'> {
136
+ orientation?: 'horizontal' | 'vertical'
137
+ }
138
+
139
+ function Divider({ className, orientation = 'horizontal', ...props }: DividerProps) {
140
+ return (
141
+ <hr
142
+ data-slot="divider"
143
+ data-orientation={orientation}
144
+ className={cn(
145
+ 'border-0 bg-border',
146
+ orientation === 'horizontal'
147
+ ? 'h-[length:var(--border-width)] w-full'
148
+ : 'h-full w-[length:var(--border-width)] self-stretch',
149
+ className,
150
+ )}
151
+ {...props}
152
+ />
153
+ )
154
+ }
155
+
156
+ // ─── Grid ─────────────────────────────────────────────────────────────────────
157
+
158
+ interface GridProps extends React.ComponentProps<'div'> {
159
+ cols?: 1 | 2 | 3 | 4 | 6 | 12
160
+ gap?: 1 | 2 | 3 | 4 | 5 | 6 | 8
161
+ }
162
+
163
+ const gridCols: Record<number, string> = {
164
+ 1: 'grid-cols-1', 2: 'grid-cols-2', 3: 'grid-cols-3',
165
+ 4: 'grid-cols-4', 6: 'grid-cols-6', 12: 'grid-cols-12',
166
+ }
167
+
168
+ function Grid({ className, cols = 2, gap = 4, ...props }: GridProps) {
169
+ return (
170
+ <div
171
+ data-slot="grid"
172
+ className={cn('grid', gridCols[cols], stackGaps[gap], className)}
173
+ {...props}
174
+ />
175
+ )
176
+ }
177
+
178
+ // ─── BentoGrid ────────────────────────────────────────────────────────────────
179
+ //
180
+ // Newspaper-grid border pattern:
181
+ // Container: border-t + border-l (outer frame top and left)
182
+ // Cells: border-b + border-r (completes each cell, never double)
183
+ //
184
+ // Result: every cell boundary is a single border line.
185
+
186
+ interface BentoGridProps extends React.ComponentProps<'div'> {
187
+ cols?: 1 | 2 | 3 | 4
188
+ }
189
+
190
+ const bentoCols: Record<number, string> = {
191
+ 1: 'grid-cols-1', 2: 'grid-cols-2', 3: 'grid-cols-3', 4: 'grid-cols-4',
192
+ }
193
+
194
+ function BentoGrid({ className, cols = 3, ...props }: BentoGridProps) {
195
+ return (
196
+ <div
197
+ data-slot="bento-grid"
198
+ className={cn(
199
+ 'grid',
200
+ bentoCols[cols],
201
+ // Container provides top + left edge; cells fill right + bottom
202
+ 'border-t-[length:var(--border-width)] border-l-[length:var(--border-width)] border-foreground',
203
+ className,
204
+ )}
205
+ {...props}
206
+ />
207
+ )
208
+ }
209
+
210
+ // ─── BentoCell ────────────────────────────────────────────────────────────────
211
+
212
+ interface BentoCellProps extends React.ComponentProps<'div'> {
213
+ span?: 1 | 2 | 3 | 4
214
+ rowSpan?: 1 | 2 | 3
215
+ }
216
+
217
+ const colSpans: Record<number, string> = {
218
+ 1: 'col-span-1', 2: 'col-span-2', 3: 'col-span-3', 4: 'col-span-4',
219
+ }
220
+
221
+ const rowSpans: Record<number, string> = {
222
+ 1: 'row-span-1', 2: 'row-span-2', 3: 'row-span-3',
223
+ }
224
+
225
+ function BentoCell({ className, span = 1, rowSpan = 1, ...props }: BentoCellProps) {
226
+ return (
227
+ <div
228
+ data-slot="bento-cell"
229
+ className={cn(
230
+ colSpans[span],
231
+ rowSpans[rowSpan],
232
+ // Cell provides bottom + right edge (newspaper rule — completes grid lines)
233
+ 'border-b-[length:var(--border-width)] border-r-[length:var(--border-width)] border-foreground',
234
+ 'p-4',
235
+ className,
236
+ )}
237
+ {...props}
238
+ />
239
+ )
240
+ }
241
+
242
+ export {
243
+ Container,
244
+ Section,
245
+ Stack,
246
+ Row,
247
+ Spacer,
248
+ Divider,
249
+ Grid,
250
+ BentoGrid,
251
+ BentoCell,
252
+ }
@@ -0,0 +1,50 @@
1
+ import * as React from 'react'
2
+ import { cn, findNextFocusable } from '@/lib/utils'
3
+
4
+ interface ListItemProps extends React.HTMLAttributes<HTMLLIElement> {}
5
+
6
+ const ListItem = React.forwardRef<HTMLLIElement, ListItemProps>(
7
+ ({ children, className, ...props }, ref) => {
8
+ const itemRef = React.useRef<HTMLLIElement>(null)
9
+ const combinedRef = ref || itemRef
10
+
11
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLLIElement>) => {
12
+ switch (event.key) {
13
+ case 'Enter':
14
+ event.preventDefault()
15
+ ;(combinedRef as React.RefObject<HTMLLIElement>).current?.click()
16
+ break
17
+ case 'ArrowUp':
18
+ case 'ArrowLeft': {
19
+ event.preventDefault()
20
+ const previousFocusable = findNextFocusable(document.activeElement, 'previous')
21
+ previousFocusable?.focus()
22
+ break
23
+ }
24
+ case 'ArrowDown':
25
+ case 'ArrowRight': {
26
+ event.preventDefault()
27
+ const nextFocusable = findNextFocusable(document.activeElement, 'next')
28
+ nextFocusable?.focus()
29
+ break
30
+ }
31
+ }
32
+ }
33
+
34
+ return (
35
+ <li
36
+ ref={combinedRef}
37
+ data-slot="list-item"
38
+ className={cn('pl-[1ch]', className)}
39
+ tabIndex={0}
40
+ onKeyDown={handleKeyDown}
41
+ {...props}
42
+ >
43
+ {children}
44
+ </li>
45
+ )
46
+ },
47
+ )
48
+ ListItem.displayName = 'ListItem'
49
+
50
+ export { ListItem }