@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,346 @@
1
+ import { cn } from '@/lib/utils'
2
+
3
+ // ─── BORDER SYSTEM ────────────────────────────────────────────────────────────
4
+ //
5
+ // container: border border-[var(--border-color)] bg-[var(--border-color)] gap-px
6
+ // cells: bg-[var(--receipt-bg)]
7
+ //
8
+ // Gap IS the border. Never add `border` to cells (stacks).
9
+ // Nested sub-grids: bg-[var(--border-color)] gap-px grid — no `border` on sub-containers.
10
+
11
+ // ─── STAT CELL ────────────────────────────────────────────────────────────────
12
+ //
13
+ // Helper primitive used as a direct grid child in layout components.
14
+ // Participates in the gap border system — no border of its own.
15
+
16
+ export function StatCell({
17
+ label,
18
+ value,
19
+ large,
20
+ }: {
21
+ label: string
22
+ value: string
23
+ large?: boolean
24
+ }) {
25
+ return (
26
+ <div className="bg-[var(--receipt-bg)] p-4">
27
+ <div className="text-[10px] uppercase text-[var(--muted-color)] mb-1">{label}</div>
28
+ <div className={cn('font-bold', large ? 'text-xl' : 'text-xs')}>{value}</div>
29
+ </div>
30
+ )
31
+ }
32
+
33
+ // ─── BENTO SPLIT ──────────────────────────────────────────────────────────────
34
+ //
35
+ // icon (200px, row-span-2) | content + stats
36
+ // Mobile: single column stack. Desktop: 200px icon col | 1fr content col.
37
+ // Icon is first in DOM — spans 2 rows via md:row-span-2 so content + stats
38
+ // auto-place into column 2.
39
+
40
+ export function BentoSplit({
41
+ icon,
42
+ content,
43
+ stats,
44
+ className,
45
+ }: {
46
+ icon: React.ReactNode
47
+ content: React.ReactNode
48
+ stats: React.ReactNode
49
+ className?: string
50
+ }) {
51
+ return (
52
+ <div
53
+ className={cn(
54
+ 'grid grid-cols-1 md:grid-cols-[200px_1fr]',
55
+ 'border border-[var(--border-color)] bg-[var(--border-color)] gap-px',
56
+ className,
57
+ )}
58
+ >
59
+ {/* Icon — spans both rows on desktop */}
60
+ <div className="bg-[var(--receipt-bg)] flex items-center justify-center p-10 md:row-span-2">
61
+ {icon}
62
+ </div>
63
+ {/* Content — auto-places into col 2, row 1 */}
64
+ <div className="bg-[var(--receipt-bg)] p-4">{content}</div>
65
+ {/* Stats — auto-places into col 2, row 2 */}
66
+ <div>{stats}</div>
67
+ </div>
68
+ )
69
+ }
70
+
71
+ // ─── BENTO LEADER ─────────────────────────────────────────────────────────────
72
+ //
73
+ // Full-width header spanning all N columns, then N equal columns below.
74
+ // Desktop column count from lookup — static strings for Tailwind JIT scanner.
75
+
76
+ const LEADER_COLS: Record<2 | 3 | 4, string> = {
77
+ 2: 'md:grid-cols-2',
78
+ 3: 'md:grid-cols-3',
79
+ 4: 'md:grid-cols-4',
80
+ }
81
+
82
+ export function BentoLeader({
83
+ header,
84
+ columns,
85
+ className,
86
+ }: {
87
+ header: React.ReactNode
88
+ columns: React.ReactNode[]
89
+ className?: string
90
+ }) {
91
+ const n = Math.min(Math.max(columns.length, 2), 4) as 2 | 3 | 4
92
+ return (
93
+ <div
94
+ className={cn(
95
+ 'grid grid-cols-1',
96
+ LEADER_COLS[n],
97
+ 'border border-[var(--border-color)] bg-[var(--border-color)] gap-px',
98
+ className,
99
+ )}
100
+ >
101
+ {/* Header spans all columns */}
102
+ <div className="bg-[var(--receipt-bg)] md:[grid-column:1/-1]">{header}</div>
103
+ {columns.map((col, i) => (
104
+ <div key={i} className="bg-[var(--receipt-bg)]">
105
+ {col}
106
+ </div>
107
+ ))}
108
+ </div>
109
+ )
110
+ }
111
+
112
+ // ─── BENTO QUAD ───────────────────────────────────────────────────────────────
113
+ //
114
+ // 2×2 grid with 2fr | 1fr column ratio.
115
+ // Cell wrappers provide bg-[var(--receipt-bg)] only — slots control their own padding.
116
+
117
+ export function BentoQuad({
118
+ topLeft,
119
+ topRight,
120
+ bottomLeft,
121
+ bottomRight,
122
+ className,
123
+ }: {
124
+ topLeft: React.ReactNode
125
+ topRight: React.ReactNode
126
+ bottomLeft: React.ReactNode
127
+ bottomRight: React.ReactNode
128
+ className?: string
129
+ }) {
130
+ return (
131
+ <div
132
+ className={cn(
133
+ 'grid grid-cols-1 md:grid-cols-[2fr_1fr]',
134
+ 'border border-[var(--border-color)] bg-[var(--border-color)] gap-px',
135
+ className,
136
+ )}
137
+ >
138
+ <div className="bg-[var(--receipt-bg)]">{topLeft}</div>
139
+ <div className="bg-[var(--receipt-bg)]">{topRight}</div>
140
+ <div>{bottomLeft}</div>
141
+ <div className="bg-[var(--receipt-bg)]">{bottomRight}</div>
142
+ </div>
143
+ )
144
+ }
145
+
146
+ // ─── BENTO TRIPLE ─────────────────────────────────────────────────────────────
147
+ //
148
+ // 3-row layout: header (full width) | aside + body | footer (full width)
149
+ // Desktop: 1fr | 2fr columns. Header and footer span both columns.
150
+ // Mobile stack order: header → aside → body → footer (natural DOM order).
151
+
152
+ export function BentoTriple({
153
+ header,
154
+ aside,
155
+ body,
156
+ footer,
157
+ className,
158
+ }: {
159
+ header: React.ReactNode
160
+ aside: React.ReactNode
161
+ body: React.ReactNode
162
+ footer: React.ReactNode
163
+ className?: string
164
+ }) {
165
+ return (
166
+ <div
167
+ className={cn(
168
+ 'grid grid-cols-1 md:grid-cols-[1fr_2fr]',
169
+ 'border border-[var(--border-color)] bg-[var(--border-color)] gap-px',
170
+ className,
171
+ )}
172
+ >
173
+ {/* Header spans both columns */}
174
+ <div className="bg-[var(--receipt-bg)] md:[grid-column:1/-1]">{header}</div>
175
+ {/* Aside — col 1, row 2 */}
176
+ <div className="bg-[var(--receipt-bg)] flex items-center justify-center">{aside}</div>
177
+ {/* Body — col 2, row 2 */}
178
+ <div className="bg-[var(--receipt-bg)]">{body}</div>
179
+ {/* Footer spans both columns */}
180
+ <div className="bg-[var(--border-color)] md:[grid-column:1/-1]">{footer}</div>
181
+ </div>
182
+ )
183
+ }
184
+
185
+ // ─── HERO PRIMARY ─────────────────────────────────────────────────────────────
186
+ //
187
+ // Side panel (5fr, row-span-3) | 3-row content area (8fr).
188
+ // Side is first in DOM — md:row-span-3 lets header/body/footer auto-place into col 2.
189
+ // footer slot is opaque ReactNode (use StatCell sub-grid or anything).
190
+
191
+ export function HeroPrimary({
192
+ side,
193
+ header,
194
+ body,
195
+ footer,
196
+ className,
197
+ }: {
198
+ side: React.ReactNode
199
+ header: React.ReactNode
200
+ body: React.ReactNode
201
+ footer: React.ReactNode
202
+ className?: string
203
+ }) {
204
+ return (
205
+ <div
206
+ className={cn(
207
+ 'grid grid-cols-1 md:grid-cols-[5fr_8fr]',
208
+ 'border border-[var(--border-color)] bg-[var(--border-color)] gap-px',
209
+ className,
210
+ )}
211
+ >
212
+ {/* Side — spans 3 rows on desktop */}
213
+ <div className="bg-[var(--receipt-bg)] flex items-center justify-center p-16 md:row-span-3">
214
+ {side}
215
+ </div>
216
+ {/* Header — auto-places into col 2, row 1 */}
217
+ <div className="bg-[var(--receipt-bg)] p-5">{header}</div>
218
+ {/* Body — auto-places into col 2, row 2 */}
219
+ <div className="bg-[var(--receipt-bg)] p-5">{body}</div>
220
+ {/* Footer — auto-places into col 2, row 3 */}
221
+ <div>{footer}</div>
222
+ </div>
223
+ )
224
+ }
225
+
226
+ // ─── CELL GRID ────────────────────────────────────────────────────────────────
227
+ //
228
+ // Rule A: equal-column grid with gap-as-border.
229
+ // Mobile: single column. Desktop: N equal columns.
230
+ // Grid bg IS the border color — gap-px exposes it as 1px lines.
231
+ // Cells need bg-[var(--receipt-bg)] on themselves.
232
+
233
+ const CELL_GRID_COLS: Record<2 | 3 | 4 | 5, string> = {
234
+ 2: 'md:grid-cols-2',
235
+ 3: 'md:grid-cols-3',
236
+ 4: 'md:grid-cols-4',
237
+ 5: 'md:grid-cols-5',
238
+ }
239
+
240
+ export function CellGrid({
241
+ cols,
242
+ subtle,
243
+ className,
244
+ children,
245
+ }: {
246
+ cols: 2 | 3 | 4 | 5
247
+ subtle?: boolean
248
+ className?: string
249
+ children: React.ReactNode
250
+ }) {
251
+ return (
252
+ <div
253
+ className={cn(
254
+ 'grid grid-cols-1',
255
+ CELL_GRID_COLS[cols],
256
+ 'gap-px',
257
+ subtle
258
+ ? 'border border-[var(--border-subtle)] bg-[var(--border-subtle)]'
259
+ : 'border border-[var(--border-color)] bg-[var(--border-color)]',
260
+ className,
261
+ )}
262
+ >
263
+ {children}
264
+ </div>
265
+ )
266
+ }
267
+
268
+ // ─── CELL ROW ─────────────────────────────────────────────────────────────────
269
+ //
270
+ // Rule B: flex direction flip with gap-as-divider.
271
+ // Mobile: flex-col (stacked). Desktop: flex-row (side by side).
272
+ // Children control their own widths via md:w-N md:shrink-0.
273
+
274
+ export function CellRow({
275
+ className,
276
+ children,
277
+ }: {
278
+ className?: string
279
+ children: React.ReactNode
280
+ }) {
281
+ return (
282
+ <div
283
+ className={cn(
284
+ 'border border-[var(--border-color)]',
285
+ 'flex flex-col md:flex-row',
286
+ 'divide-y divide-[var(--border-color)] md:divide-y-0 md:divide-x divide-[var(--border-color)]',
287
+ className,
288
+ )}
289
+ >
290
+ {children}
291
+ </div>
292
+ )
293
+ }
294
+
295
+ // ─── HERO SECONDARY ───────────────────────────────────────────────────────────
296
+ //
297
+ // 3-row content (3fr) | side panel (1fr, explicit right column).
298
+ // statsRow: ReactNode[] — rendered in a nested sub-grid (each item a direct child).
299
+ // Side is last in DOM, explicitly placed at col 2 spanning all 3 rows.
300
+ // Mobile: natural DOM stack (side at bottom).
301
+
302
+ const STATS_COLS: Record<2 | 3 | 4, string> = {
303
+ 2: 'md:grid-cols-2',
304
+ 3: 'md:grid-cols-3',
305
+ 4: 'md:grid-cols-4',
306
+ }
307
+
308
+ export function HeroSecondary({
309
+ header,
310
+ statsRow,
311
+ content,
312
+ side,
313
+ className,
314
+ }: {
315
+ header: React.ReactNode
316
+ statsRow: React.ReactNode[]
317
+ content: React.ReactNode
318
+ side: React.ReactNode
319
+ className?: string
320
+ }) {
321
+ const n = Math.min(Math.max(statsRow.length, 2), 4) as 2 | 3 | 4
322
+ return (
323
+ <div
324
+ className={cn(
325
+ 'grid grid-cols-1 md:grid-cols-[3fr_1fr]',
326
+ 'border border-[var(--border-color)] bg-[var(--border-color)] gap-px',
327
+ className,
328
+ )}
329
+ >
330
+ {/* Header — col 1, row 1 */}
331
+ <div className="bg-[var(--receipt-bg)] p-4">{header}</div>
332
+ {/* Stats row — col 1, row 2: nested sub-grid */}
333
+ <div className={cn('bg-[var(--border-color)] gap-px grid grid-cols-1', STATS_COLS[n])}>
334
+ {statsRow.map((stat, i) => (
335
+ <div key={i}>{stat}</div>
336
+ ))}
337
+ </div>
338
+ {/* Content — col 1, row 3 */}
339
+ <div className="bg-[var(--receipt-bg)] p-4">{content}</div>
340
+ {/* Side — explicitly placed at col 2, rows 1–4 on desktop */}
341
+ <div className="bg-[var(--receipt-bg)] flex items-center justify-center p-12 md:[grid-column:2] md:[grid-row:1/4]">
342
+ {side}
343
+ </div>
344
+ </div>
345
+ )
346
+ }
@@ -0,0 +1,49 @@
1
+ import { type ClassValue, clsx } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
7
+
8
+ export function leftPad(input: string, length: number): string {
9
+ const zerosNeeded = length - input.length
10
+ if (zerosNeeded <= 0) {
11
+ return input
12
+ }
13
+ return '0'.repeat(zerosNeeded) + input
14
+ }
15
+
16
+ export function findNextFocusable(
17
+ element: Element | null,
18
+ direction: 'next' | 'previous' = 'next'
19
+ ): HTMLElement | null {
20
+ if (!element) return null
21
+
22
+ const focusableSelectors = [
23
+ 'a[href]',
24
+ 'button',
25
+ 'input',
26
+ 'select',
27
+ 'textarea',
28
+ '[tabindex]:not([tabindex="-1"])',
29
+ '[contenteditable="true"]',
30
+ ]
31
+
32
+ const focusableElements = Array.from(
33
+ document.querySelectorAll<HTMLElement>(focusableSelectors.join(', '))
34
+ )
35
+
36
+ const currentIndex = focusableElements.indexOf(element as HTMLElement)
37
+
38
+ if (currentIndex !== -1) {
39
+ const nextIndex =
40
+ direction === 'next'
41
+ ? (currentIndex + 1) % focusableElements.length
42
+ : (currentIndex - 1 + focusableElements.length) %
43
+ focusableElements.length
44
+
45
+ return focusableElements[nextIndex]
46
+ }
47
+
48
+ return null
49
+ }
@@ -0,0 +1 @@
1
+ export { ListItem } from '@/components/ui/list-item'
@@ -0,0 +1,41 @@
1
+ import { Check } from 'lucide-react'
2
+
3
+ interface ArrowListItemProps {
4
+ children: React.ReactNode
5
+ }
6
+
7
+ export function ArrowListItem({ children }: ArrowListItemProps) {
8
+ return (
9
+ <li className="flex items-start gap-3">
10
+ <span className="text-klein text-sm">→</span>
11
+ <span className="text-muted-foreground">{children}</span>
12
+ </li>
13
+ )
14
+ }
15
+
16
+ interface CheckListItemProps {
17
+ children: React.ReactNode
18
+ /**
19
+ * Whether to display with a border and padding (card style)
20
+ * @default false
21
+ */
22
+ bordered?: boolean
23
+ }
24
+
25
+ export function CheckListItem({ children, bordered = false }: CheckListItemProps) {
26
+ if (bordered) {
27
+ return (
28
+ <li className="flex items-start gap-3 border p-4">
29
+ <Check className="w-5 h-5 text-klein flex-shrink-0 mt-0.5" aria-hidden="true" />
30
+ <span className="text-sm">{children}</span>
31
+ </li>
32
+ )
33
+ }
34
+
35
+ return (
36
+ <li className="flex items-start gap-3">
37
+ <Check className="w-4 h-4 text-klein flex-shrink-0 mt-1" aria-hidden="true" />
38
+ <span className="text-muted-foreground text-sm">{children}</span>
39
+ </li>
40
+ )
41
+ }
@@ -0,0 +1,89 @@
1
+ import type * as React from 'react'
2
+ import { Check } from 'lucide-react'
3
+ import { cn } from '@/lib/utils'
4
+ import { ListItem } from '@/components/ui/list-item'
5
+
6
+ /* ─── Item data types ─── */
7
+
8
+ export interface ListItemData {
9
+ content: React.ReactNode
10
+ }
11
+
12
+ /* ─── Variant definitions ─── */
13
+
14
+ type ListVariant = 'arrow' | 'check' | 'check-bordered' | 'bullet'
15
+
16
+ /* ─── Props ─── */
17
+
18
+ export interface ListProps {
19
+ items?: ListItemData[]
20
+ variant?: ListVariant
21
+ className?: string
22
+ children?: React.ReactNode
23
+ }
24
+
25
+ /* ─── Item renderers ─── */
26
+
27
+ function ArrowItem({ content }: ListItemData) {
28
+ return (
29
+ <li className="flex items-start gap-3">
30
+ <span className="text-klein text-sm">→</span>
31
+ <span className="text-muted-foreground">{content}</span>
32
+ </li>
33
+ )
34
+ }
35
+
36
+ function CheckItem({ content }: ListItemData) {
37
+ return (
38
+ <li className="flex items-start gap-3">
39
+ <Check className="w-4 h-4 text-klein flex-shrink-0 mt-1" aria-hidden="true" />
40
+ <span className="text-muted-foreground text-sm">{content}</span>
41
+ </li>
42
+ )
43
+ }
44
+
45
+ function CheckBorderedItem({ content }: ListItemData) {
46
+ return (
47
+ <li className="flex items-start gap-3 border p-4">
48
+ <Check className="w-5 h-5 text-klein flex-shrink-0 mt-0.5" aria-hidden="true" />
49
+ <span className="text-sm">{content}</span>
50
+ </li>
51
+ )
52
+ }
53
+
54
+ function BulletItem({ content }: ListItemData) {
55
+ return <li>{content}</li>
56
+ }
57
+
58
+ const VARIANT_ITEM: Record<ListVariant, React.FC<ListItemData>> = {
59
+ arrow: ArrowItem,
60
+ check: CheckItem,
61
+ 'check-bordered': CheckBorderedItem,
62
+ bullet: BulletItem,
63
+ }
64
+
65
+ const VARIANT_UL_CLASS: Record<ListVariant, string> = {
66
+ arrow: 'space-y-4',
67
+ check: 'space-y-3',
68
+ 'check-bordered': 'grid md:grid-cols-2 gap-4',
69
+ bullet: 'list-disc list-inside space-y-1',
70
+ }
71
+
72
+ /* ─── Component ─── */
73
+
74
+ function List({ items, variant = 'bullet', className, children }: ListProps) {
75
+ if (items) {
76
+ const ItemComponent = VARIANT_ITEM[variant]
77
+ return (
78
+ <ul className={cn(VARIANT_UL_CLASS[variant], className)}>
79
+ {items.map((item, i) => (
80
+ <ItemComponent key={i} content={item.content} />
81
+ ))}
82
+ </ul>
83
+ )
84
+ }
85
+
86
+ return <ul className={cn(className)}>{children}</ul>
87
+ }
88
+
89
+ export { List, ListItem }
@@ -0,0 +1,12 @@
1
+ export function Logo({ className }: { className?: string }) {
2
+ return (
3
+ <svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
4
+ <rect x="0" y="0" width="3" height="24" />
5
+ <rect x="21" y="0" width="3" height="24" />
6
+ <rect x="0" y="0" width="24" height="3" />
7
+ <rect x="0" y="7" width="24" height="3" />
8
+ <rect x="0" y="14" width="24" height="3" />
9
+ <rect x="0" y="21" width="24" height="3" />
10
+ </svg>
11
+ )
12
+ }
@@ -0,0 +1,33 @@
1
+ import { CAL_LINK } from '@/lib/constants'
2
+
3
+ interface MarketingFooterProps {
4
+ /**
5
+ * The call-to-action link URL
6
+ * @default CAL_LINK
7
+ */
8
+ ctaLink?: string
9
+ /**
10
+ * The call-to-action text
11
+ * @default "Book a Call"
12
+ */
13
+ ctaText?: string
14
+ }
15
+
16
+ export function MarketingFooter({
17
+ ctaLink = CAL_LINK,
18
+ ctaText = 'Book a Call',
19
+ }: MarketingFooterProps) {
20
+ return (
21
+ <footer className="border-t border-[var(--border-subtle)] py-6">
22
+ <div className="wrap flex flex-col md:flex-row items-center justify-between gap-4">
23
+ <p className="text-xs text-muted-foreground">© {new Date().getFullYear()} LEITWARE</p>
24
+ <a
25
+ href={ctaLink}
26
+ className="text-xs text-muted-foreground hover:text-klein focus-ring"
27
+ >
28
+ {ctaText}
29
+ </a>
30
+ </div>
31
+ </footer>
32
+ )
33
+ }
@@ -0,0 +1,46 @@
1
+ import { Logo } from '@/components/logo'
2
+ import { ThemeToggle } from '@/components/theme-toggle'
3
+ import { Button } from '@/components/button'
4
+ import { CAL_LINK } from '@/lib/constants'
5
+
6
+ interface MarketingHeaderProps {
7
+ /**
8
+ * The call-to-action link URL
9
+ * @default CAL_LINK
10
+ */
11
+ ctaLink?: string
12
+ /**
13
+ * The call-to-action button text
14
+ * @default "Get Started"
15
+ */
16
+ ctaText?: string
17
+ }
18
+
19
+ export function MarketingHeader({
20
+ ctaLink = CAL_LINK,
21
+ ctaText = 'Get Started',
22
+ }: MarketingHeaderProps) {
23
+ return (
24
+ <nav
25
+ className="sticky top-0 z-50 bg-background/80 backdrop-blur-md border-b border-[var(--border-subtle)]"
26
+ aria-label="Main navigation"
27
+ >
28
+ <div className="wrap flex items-center justify-between h-16">
29
+ <a
30
+ href="/"
31
+ className="flex items-center gap-2 font-medium focus-ring"
32
+ aria-label="LEITWARE home"
33
+ >
34
+ <Logo className="w-7 h-7" />
35
+ <span>LEITWARE</span>
36
+ </a>
37
+ <div className="flex items-center gap-3">
38
+ <ThemeToggle />
39
+ <Button asChild size="sm">
40
+ <a href={ctaLink}>{ctaText}</a>
41
+ </Button>
42
+ </div>
43
+ </div>
44
+ </nav>
45
+ )
46
+ }
@@ -0,0 +1,16 @@
1
+ export {
2
+ Menubar,
3
+ MenubarMenu,
4
+ MenubarTrigger,
5
+ MenubarContent,
6
+ MenubarItem,
7
+ MenubarCheckboxItem,
8
+ MenubarRadioGroup,
9
+ MenubarRadioItem,
10
+ MenubarLabel,
11
+ MenubarSeparator,
12
+ MenubarShortcut,
13
+ MenubarSub,
14
+ MenubarSubTrigger,
15
+ MenubarSubContent,
16
+ } from '@/components/ui/menubar'
@@ -0,0 +1,11 @@
1
+ export {
2
+ NavigationMenu,
3
+ NavigationMenuList,
4
+ NavigationMenuItem,
5
+ NavigationMenuTrigger,
6
+ NavigationMenuContent,
7
+ NavigationMenuLink,
8
+ NavigationMenuViewport,
9
+ NavigationMenuIndicator,
10
+ navigationMenuTriggerStyle,
11
+ } from '@/components/ui/navigation-menu'