@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.
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +18 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +86 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +36 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/registry.d.ts +18 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +712 -0
- package/dist/registry.js.map +1 -0
- package/package.json +40 -0
- package/templates/accordion.tsx +77 -0
- package/templates/alert-dialog.tsx +66 -0
- package/templates/alert.tsx +41 -0
- package/templates/aspect-ratio.tsx +15 -0
- package/templates/avatar.tsx +27 -0
- package/templates/badge.tsx +1 -0
- package/templates/block-loader.tsx +1 -0
- package/templates/breadcrumb.tsx +31 -0
- package/templates/button.tsx +1 -0
- package/templates/calendar.tsx +45 -0
- package/templates/card.tsx +35 -0
- package/templates/carousel.tsx +39 -0
- package/templates/checkbox.tsx +50 -0
- package/templates/code-block.tsx +1 -0
- package/templates/collapsible.tsx +35 -0
- package/templates/combobox.tsx +154 -0
- package/templates/command.tsx +50 -0
- package/templates/contact-footer.tsx +193 -0
- package/templates/context-menu.tsx +16 -0
- package/templates/dialog.tsx +67 -0
- package/templates/drawer.tsx +12 -0
- package/templates/dropdown-menu.tsx +95 -0
- package/templates/form-input.tsx +64 -0
- package/templates/form.tsx +10 -0
- package/templates/hover-card.tsx +5 -0
- package/templates/input-otp.tsx +6 -0
- package/templates/label.tsx +1 -0
- package/templates/layout-primitives.tsx +11 -0
- package/templates/layouts.tsx +346 -0
- package/templates/lib/utils.ts +49 -0
- package/templates/list-item.tsx +1 -0
- package/templates/list-items.tsx +41 -0
- package/templates/list.tsx +89 -0
- package/templates/logo.tsx +12 -0
- package/templates/marketing-footer.tsx +33 -0
- package/templates/marketing-header.tsx +46 -0
- package/templates/menubar.tsx +16 -0
- package/templates/navigation-menu.tsx +11 -0
- package/templates/pagination.tsx +86 -0
- package/templates/popover.tsx +8 -0
- package/templates/pricing-receipt.tsx +71 -0
- package/templates/pricing-tabs.tsx +60 -0
- package/templates/progress.tsx +29 -0
- package/templates/radio-group.tsx +58 -0
- package/templates/receipt-card.tsx +1 -0
- package/templates/receipt.tsx +269 -0
- package/templates/resizable.tsx +1 -0
- package/templates/scroll-area.tsx +1 -0
- package/templates/select.tsx +110 -0
- package/templates/separator.tsx +1 -0
- package/templates/sheet.tsx +12 -0
- package/templates/sidebar.tsx +15 -0
- package/templates/simple-footer.tsx +43 -0
- package/templates/simple-header.tsx +77 -0
- package/templates/skeleton.tsx +33 -0
- package/templates/slider.tsx +55 -0
- package/templates/styles/dockets.css +104 -0
- package/templates/switch.tsx +49 -0
- package/templates/table.tsx +73 -0
- package/templates/tabs.tsx +61 -0
- package/templates/theme-toggle.tsx +46 -0
- package/templates/toast.tsx +1 -0
- package/templates/toggle-group.tsx +1 -0
- package/templates/toggle.tsx +1 -0
- package/templates/tooltip.tsx +31 -0
- package/templates/tree-view.tsx +1 -0
- package/templates/ui/accordion.tsx +73 -0
- package/templates/ui/alert-dialog.tsx +128 -0
- package/templates/ui/alert.tsx +56 -0
- package/templates/ui/aspect-ratio.tsx +19 -0
- package/templates/ui/avatar.tsx +74 -0
- package/templates/ui/badge.tsx +48 -0
- package/templates/ui/block-loader.tsx +40 -0
- package/templates/ui/button.tsx +77 -0
- package/templates/ui/calendar.tsx +160 -0
- package/templates/ui/card.tsx +73 -0
- package/templates/ui/carousel.tsx +149 -0
- package/templates/ui/checkbox.tsx +33 -0
- package/templates/ui/code-block.tsx +36 -0
- package/templates/ui/collapsible.tsx +48 -0
- package/templates/ui/combobox.tsx +295 -0
- package/templates/ui/command.tsx +148 -0
- package/templates/ui/context-menu.tsx +212 -0
- package/templates/ui/dialog.tsx +138 -0
- package/templates/ui/drawer.tsx +134 -0
- package/templates/ui/dropdown-menu.tsx +254 -0
- package/templates/ui/form.tsx +122 -0
- package/templates/ui/hover-card.tsx +44 -0
- package/templates/ui/input-group.tsx +148 -0
- package/templates/ui/input-otp.tsx +153 -0
- package/templates/ui/input.tsx +20 -0
- package/templates/ui/label.tsx +17 -0
- package/templates/ui/layout.tsx +252 -0
- package/templates/ui/list-item.tsx +50 -0
- package/templates/ui/menubar.tsx +225 -0
- package/templates/ui/navigation-menu.tsx +117 -0
- package/templates/ui/pagination.tsx +110 -0
- package/templates/ui/popover.tsx +77 -0
- package/templates/ui/progress.tsx +37 -0
- package/templates/ui/radio-group.tsx +41 -0
- package/templates/ui/receipt-card.tsx +70 -0
- package/templates/ui/resizable.tsx +140 -0
- package/templates/ui/scroll-area.tsx +64 -0
- package/templates/ui/select.tsx +186 -0
- package/templates/ui/separator.tsx +21 -0
- package/templates/ui/sheet.tsx +134 -0
- package/templates/ui/sidebar.tsx +222 -0
- package/templates/ui/skeleton.tsx +35 -0
- package/templates/ui/slider.tsx +60 -0
- package/templates/ui/switch.tsx +33 -0
- package/templates/ui/table.tsx +114 -0
- package/templates/ui/tabs.tsx +79 -0
- package/templates/ui/textarea.tsx +18 -0
- package/templates/ui/toast.tsx +139 -0
- package/templates/ui/toggle-group.tsx +68 -0
- package/templates/ui/toggle.tsx +47 -0
- package/templates/ui/tooltip.tsx +53 -0
- 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 }
|