@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,86 @@
1
+ import * as React from 'react'
2
+ import {
3
+ Pagination,
4
+ PaginationContent,
5
+ PaginationItem,
6
+ PaginationLink,
7
+ PaginationPrevious,
8
+ PaginationNext,
9
+ PaginationEllipsis,
10
+ } from '@/components/ui/pagination'
11
+
12
+ export interface SimplePaginationProps {
13
+ page: number
14
+ totalPages: number
15
+ onPageChange?: (page: number) => void
16
+ className?: string
17
+ }
18
+
19
+ function SimplePagination({ page, totalPages, onPageChange, className }: SimplePaginationProps) {
20
+ const pages = Array.from({ length: totalPages }, (_, i) => i + 1)
21
+ const showEllipsisStart = page > 4
22
+ const showEllipsisEnd = page < totalPages - 3
23
+
24
+ const visiblePages = showEllipsisStart || showEllipsisEnd
25
+ ? [
26
+ 1,
27
+ ...(showEllipsisStart ? [] : [2, 3]),
28
+ ...(showEllipsisStart ? ['ellipsis-start'] : []),
29
+ ...(page > 2 && page < totalPages - 1 ? [page - 1, page, page + 1] : []),
30
+ ...(showEllipsisEnd ? ['ellipsis-end'] : []),
31
+ ...(showEllipsisEnd ? [] : [totalPages - 2, totalPages - 1]),
32
+ totalPages,
33
+ ].filter((v, i, arr) => arr.indexOf(v) === i)
34
+ : pages
35
+
36
+ return (
37
+ <Pagination className={className}>
38
+ <PaginationContent>
39
+ <PaginationItem>
40
+ <PaginationPrevious
41
+ href="#"
42
+ onClick={(e) => { e.preventDefault(); page > 1 && onPageChange?.(page - 1) }}
43
+ aria-disabled={page <= 1}
44
+ disabled={page <= 1}
45
+ />
46
+ </PaginationItem>
47
+ {visiblePages.map((p, i) =>
48
+ typeof p === 'string' ? (
49
+ <PaginationItem key={p}>
50
+ <PaginationEllipsis />
51
+ </PaginationItem>
52
+ ) : (
53
+ <PaginationItem key={p}>
54
+ <PaginationLink
55
+ href="#"
56
+ isActive={p === page}
57
+ onClick={(e) => { e.preventDefault(); onPageChange?.(p) }}
58
+ >
59
+ {p}
60
+ </PaginationLink>
61
+ </PaginationItem>
62
+ ),
63
+ )}
64
+ <PaginationItem>
65
+ <PaginationNext
66
+ href="#"
67
+ onClick={(e) => { e.preventDefault(); page < totalPages && onPageChange?.(page + 1) }}
68
+ aria-disabled={page >= totalPages}
69
+ disabled={page >= totalPages}
70
+ />
71
+ </PaginationItem>
72
+ </PaginationContent>
73
+ </Pagination>
74
+ )
75
+ }
76
+
77
+ export {
78
+ SimplePagination as Pagination,
79
+ PaginationContent,
80
+ PaginationItem,
81
+ PaginationLink,
82
+ PaginationPrevious,
83
+ PaginationNext,
84
+ PaginationEllipsis,
85
+ }
86
+ export { Pagination as PaginationRoot } from '@/components/ui/pagination'
@@ -0,0 +1,8 @@
1
+ export {
2
+ Popover,
3
+ PopoverTrigger,
4
+ PopoverPortal,
5
+ PopoverClose,
6
+ PopoverContent,
7
+ PopoverArrow,
8
+ } from '@/components/ui/popover'
@@ -0,0 +1,71 @@
1
+ import { ReceiptDivider, SectionHeader } from './contact-footer'
2
+
3
+ export interface PricingProduct {
4
+ id: string
5
+ orderNum: string
6
+ title: string
7
+ subtitle?: string
8
+ items: { name: string; value: string }[]
9
+ total: string
10
+ cta: string
11
+ link: string
12
+ learnMoreLink?: string
13
+ }
14
+
15
+ interface PricingReceiptProps {
16
+ product: PricingProduct
17
+ }
18
+
19
+ export function PricingReceipt({ product }: PricingReceiptProps) {
20
+ return (
21
+ <div className="bg-[var(--receipt-bg)] border w-full min-w-[288px] max-w-[336px] flex-1 p-6 text-sm leading-tight">
22
+ {/* Header */}
23
+ <div className="text-center h-12 mb-3">
24
+ <div className="text-base font-bold mb-1"># {product.orderNum}</div>
25
+ <div className="text-xs uppercase">{product.title}</div>
26
+ <div className="text-[10px] text-[var(--muted-color)] mt-0.5 h-4">
27
+ {product.subtitle || '\u00A0'}
28
+ </div>
29
+ </div>
30
+
31
+ <ReceiptDivider />
32
+
33
+ <SectionHeader>INCLUDES</SectionHeader>
34
+
35
+ {/* Items */}
36
+ <div className="h-24 mb-4">
37
+ {product.items.map((item) => (
38
+ <div key={item.name} className="flex justify-between items-start mb-px text-xs">
39
+ <span className="flex-1 uppercase break-words">{item.name}</span>
40
+ <span className="min-w-[72px] text-right shrink-0">{item.value}</span>
41
+ </div>
42
+ ))}
43
+ </div>
44
+
45
+ <ReceiptDivider />
46
+
47
+ {/* Total */}
48
+ <div className="h-6 mb-4">
49
+ <div className="flex justify-between text-xs mb-0.5">
50
+ <span>TOTAL:</span>
51
+ <span>{product.total}</span>
52
+ </div>
53
+ </div>
54
+
55
+ {/* CTA */}
56
+ <a
57
+ href={product.link}
58
+ className="inline-flex items-center justify-center gap-2 px-6 py-3 text-[11px] uppercase tracking-wide border bg-[var(--border-color)] text-[var(--receipt-bg)] no-underline cursor-pointer w-full text-center"
59
+ >
60
+ {product.cta}
61
+ </a>
62
+
63
+ {/* Footer */}
64
+ <div className="text-center h-6 pt-4 text-[11px]">
65
+ <a href={product.learnMoreLink || product.link} className="text-[11px] no-underline lowercase">
66
+ learn more
67
+ </a>
68
+ </div>
69
+ </div>
70
+ )
71
+ }
@@ -0,0 +1,60 @@
1
+ import { useState } from 'react'
2
+ import { type PricingProduct, PricingReceipt } from './pricing-receipt'
3
+ import { Button } from '@/components/button'
4
+
5
+ interface PricingTabsProps {
6
+ catchUpProducts: PricingProduct[]
7
+ keepUpProducts: PricingProduct[]
8
+ catchUpDescription?: string
9
+ keepUpDescription?: string
10
+ footer?: React.ReactNode
11
+ }
12
+
13
+ export function PricingTabs({
14
+ catchUpProducts,
15
+ keepUpProducts,
16
+ catchUpDescription = 'One-time projects · Get set up and running',
17
+ keepUpDescription = 'Monthly plans · Ongoing maintenance and support',
18
+ footer,
19
+ }: PricingTabsProps) {
20
+ const [activeTab, setActiveTab] = useState<'catchup' | 'keepup'>('catchup')
21
+
22
+ return (
23
+ <>
24
+ {/* Tabs */}
25
+ <div className="flex justify-center mb-3 w-full max-w-[336px] mx-auto border border-solid border-foreground">
26
+ <Button
27
+ variant={activeTab === 'catchup' ? 'default' : 'secondary'}
28
+ className="flex-1 rounded-[var(--radius)]"
29
+ onClick={() => setActiveTab('catchup')}
30
+ >
31
+ Catch Up
32
+ </Button>
33
+ <Button
34
+ variant={activeTab === 'keepup' ? 'default' : 'secondary'}
35
+ className={`flex-1 rounded-[var(--radius)] border-l border-solid border-foreground`}
36
+ onClick={() => setActiveTab('keepup')}
37
+ >
38
+ Keep Up
39
+ </Button>
40
+ </div>
41
+
42
+ {/* Tab Description */}
43
+ <div className="text-center mb-8">
44
+ <p className="text-[var(--muted-color)] text-[11px]">
45
+ {activeTab === 'catchup' ? catchUpDescription : keepUpDescription}
46
+ </p>
47
+ </div>
48
+
49
+ {/* Receipts */}
50
+ <div className="flex flex-wrap gap-12 justify-center items-start">
51
+ {(activeTab === 'catchup' ? catchUpProducts : keepUpProducts).map((product) => (
52
+ <PricingReceipt key={product.id} product={product} />
53
+ ))}
54
+ </div>
55
+
56
+ {/* Footer */}
57
+ {footer && <div className="text-center mt-8">{footer}</div>}
58
+ </>
59
+ )
60
+ }
@@ -0,0 +1,29 @@
1
+ import * as React from 'react'
2
+ import { Progress } from '@/components/ui/progress'
3
+ import { cn } from '@/lib/utils'
4
+
5
+ export interface LabelledProgressProps {
6
+ value?: number
7
+ max?: number
8
+ label?: string
9
+ showValue?: boolean
10
+ className?: string
11
+ }
12
+
13
+ function LabelledProgress({ value, max = 100, label, showValue = false, className }: LabelledProgressProps) {
14
+ const pct = value != null ? Math.round((value / max) * 100) : undefined
15
+ return (
16
+ <div className={cn('flex flex-col gap-1', className)}>
17
+ {(label || showValue) && (
18
+ <div className="flex items-center justify-between text-xs font-medium uppercase tracking-wider">
19
+ {label && <span>{label}</span>}
20
+ {showValue && <span className="text-muted-foreground">{pct != null ? `${pct}%` : '–'}</span>}
21
+ </div>
22
+ )}
23
+ <Progress value={value} max={max} />
24
+ </div>
25
+ )
26
+ }
27
+
28
+ export { LabelledProgress as Progress }
29
+ export { Progress as ProgressRoot } from '@/components/ui/progress'
@@ -0,0 +1,58 @@
1
+ import * as React from 'react'
2
+ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
3
+ import { Label } from '@/components/ui/label'
4
+ import { cn } from '@/lib/utils'
5
+
6
+ export interface RadioOption {
7
+ value: string
8
+ label: string
9
+ description?: string
10
+ disabled?: boolean
11
+ }
12
+
13
+ export interface RadioGroupFieldProps {
14
+ options: RadioOption[]
15
+ value?: string
16
+ defaultValue?: string
17
+ onValueChange?: (value: string) => void
18
+ className?: string
19
+ orientation?: 'horizontal' | 'vertical'
20
+ }
21
+
22
+ function RadioGroupField({
23
+ options,
24
+ value,
25
+ defaultValue,
26
+ onValueChange,
27
+ className,
28
+ orientation = 'vertical',
29
+ }: RadioGroupFieldProps) {
30
+ return (
31
+ <RadioGroup
32
+ value={value}
33
+ defaultValue={defaultValue}
34
+ onValueChange={onValueChange}
35
+ className={cn(
36
+ orientation === 'horizontal' ? 'flex-row flex-wrap' : 'flex-col',
37
+ className,
38
+ )}
39
+ >
40
+ {options.map((opt) => {
41
+ const id = `radio-${opt.value}`
42
+ return (
43
+ <div key={opt.value} className="flex items-start gap-2">
44
+ <RadioGroupItem value={opt.value} id={id} disabled={opt.disabled} className="mt-0.5" />
45
+ <div className="grid gap-0.5">
46
+ <Label htmlFor={id}>{opt.label}</Label>
47
+ {opt.description && (
48
+ <p className="text-xs/relaxed text-muted-foreground">{opt.description}</p>
49
+ )}
50
+ </div>
51
+ </div>
52
+ )
53
+ })}
54
+ </RadioGroup>
55
+ )
56
+ }
57
+
58
+ export { RadioGroupField, RadioGroup, RadioGroupItem }
@@ -0,0 +1 @@
1
+ export { ReceiptCard } from '@/components/ui/receipt-card'
@@ -0,0 +1,269 @@
1
+ import { cn } from '@/lib/utils'
2
+
3
+ // ─── DIVIDER ──────────────────────────────────────────────────────────────────
4
+
5
+ type DividerVariant = 'dots' | 'dashes' | 'equals'
6
+
7
+ export function Divider({
8
+ variant = 'dots',
9
+ className,
10
+ }: {
11
+ variant?: DividerVariant
12
+ className?: string
13
+ }) {
14
+ const chars: Record<DividerVariant, string> = {
15
+ dots: '.'.repeat(200),
16
+ dashes: '-'.repeat(200),
17
+ equals: '='.repeat(200),
18
+ }
19
+ return (
20
+ <div className={cn('text-xs my-2 h-[1em] w-full relative overflow-hidden', className)}>
21
+ <span className="absolute inset-x-0 whitespace-nowrap tracking-[-1px] text-[var(--border-color)] select-none">
22
+ {chars[variant]}
23
+ </span>
24
+ </div>
25
+ )
26
+ }
27
+
28
+ // ─── SECTION LABEL ────────────────────────────────────────────────────────────
29
+
30
+ export function SectionLabel({
31
+ children,
32
+ variant = 'default',
33
+ className,
34
+ }: {
35
+ children: React.ReactNode
36
+ variant?: 'default' | 'bordered'
37
+ className?: string
38
+ }) {
39
+ return (
40
+ <div
41
+ className={cn(
42
+ 'text-xs font-bold uppercase py-[3px] px-2 border-t border-b',
43
+ variant === 'default'
44
+ ? 'bg-[var(--border-color)] text-[var(--receipt-bg)]'
45
+ : 'bg-transparent text-[var(--text-color)]',
46
+ className,
47
+ )}
48
+ >
49
+ {children}
50
+ </div>
51
+ )
52
+ }
53
+
54
+ // ─── ROW ──────────────────────────────────────────────────────────────────────
55
+
56
+ export function Row({
57
+ label,
58
+ value,
59
+ fill = false,
60
+ bold = false,
61
+ className,
62
+ }: {
63
+ label: string
64
+ value: string
65
+ fill?: boolean
66
+ bold?: boolean
67
+ className?: string
68
+ }) {
69
+ return (
70
+ <div
71
+ className={cn(
72
+ 'flex items-end text-xs gap-0',
73
+ bold && 'font-bold',
74
+ className,
75
+ )}
76
+ >
77
+ <span className="uppercase shrink-0">{label}</span>
78
+ {fill ? (
79
+ <>
80
+ <span
81
+ aria-hidden
82
+ className="flex-1 min-w-[2ch] overflow-hidden tracking-[-1px] whitespace-nowrap opacity-20 text-[var(--border-color)] select-none"
83
+ >
84
+ {'.'.repeat(200)}
85
+ </span>
86
+ <span className="shrink-0">{value}</span>
87
+ </>
88
+ ) : (
89
+ <span className="flex-1 text-right">{value}</span>
90
+ )}
91
+ </div>
92
+ )
93
+ }
94
+
95
+ // ─── DATA TABLE ───────────────────────────────────────────────────────────────
96
+ //
97
+ // Column widths are in `ch` units (character widths), so columns snap to the
98
+ // monospace grid. Omit `width` on a column to have it fill remaining space.
99
+ //
100
+ // Example:
101
+ // columns={[{ label: 'service', width: 24 }, { label: 'total', align: 'right' }]}
102
+
103
+ export interface ColDef {
104
+ label: string
105
+ width?: number // ch units
106
+ align?: 'left' | 'right'
107
+ }
108
+
109
+ export function DataTable({
110
+ columns,
111
+ rows,
112
+ className,
113
+ }: {
114
+ columns: ColDef[]
115
+ rows: (string | React.ReactNode)[][]
116
+ className?: string
117
+ }) {
118
+ return (
119
+ <div className={cn('text-xs', className)}>
120
+ {/* Header row */}
121
+ <div className="flex border-b border-[var(--border-color)] pb-px mb-px">
122
+ {columns.map((col, i) => (
123
+ <span
124
+ key={i}
125
+ className={cn(
126
+ 'uppercase shrink-0 text-[var(--muted-color)]',
127
+ !col.width && 'flex-1',
128
+ col.align === 'right' && 'text-right',
129
+ )}
130
+ style={col.width ? { width: `${col.width}ch` } : undefined}
131
+ >
132
+ {col.label}
133
+ </span>
134
+ ))}
135
+ </div>
136
+ {/* Data rows */}
137
+ {rows.map((row, ri) => (
138
+ <div
139
+ key={ri}
140
+ className="flex border-b border-[var(--border-subtle)] py-px last:border-b-0"
141
+ >
142
+ {row.map((cell, ci) => {
143
+ const col = columns[ci]
144
+ return (
145
+ <span
146
+ key={ci}
147
+ className={cn(
148
+ 'shrink-0',
149
+ !col?.width && 'flex-1',
150
+ col?.align === 'right' && 'text-right',
151
+ )}
152
+ style={col?.width ? { width: `${col.width}ch` } : undefined}
153
+ >
154
+ {cell}
155
+ </span>
156
+ )
157
+ })}
158
+ </div>
159
+ ))}
160
+ </div>
161
+ )
162
+ }
163
+
164
+ // ─── GLYPH ────────────────────────────────────────────────────────────────────
165
+ //
166
+ // Fixed-size square containing a centred character. Size must be a multiple of
167
+ // 8 or 16. `--radius-full` is 0 in this theme so the inner circle is set via
168
+ // inline style (borderRadius: '50%') rather than a Tailwind class.
169
+ //
170
+ // Variants:
171
+ // default bordered square, normal bg
172
+ // filled inverted (black bg, light text)
173
+ // circle light bg + filled circle behind character (white text)
174
+ // circle-inverted black bg + light circle behind character (dark text)
175
+
176
+ export type GlyphSize = 16 | 24 | 32 | 48 | 64 | 96
177
+ export type GlyphVariant = 'default' | 'filled' | 'circle' | 'circle-inverted'
178
+
179
+ const GLYPH_FONT: Record<GlyphSize, string> = {
180
+ 16: 'text-[9px]',
181
+ 24: 'text-[11px]',
182
+ 32: 'text-xs',
183
+ 48: 'text-base',
184
+ 64: 'text-xl',
185
+ 96: 'text-3xl',
186
+ }
187
+
188
+ export function Glyph({
189
+ children,
190
+ size = 48,
191
+ variant = 'default',
192
+ className,
193
+ }: {
194
+ children: React.ReactNode
195
+ size?: GlyphSize
196
+ variant?: GlyphVariant
197
+ className?: string
198
+ }) {
199
+ const circleDiameter = Math.round(size * 0.68)
200
+
201
+ return (
202
+ <div
203
+ className={cn(
204
+ 'relative inline-flex items-center justify-center shrink-0 border font-bold select-none',
205
+ GLYPH_FONT[size],
206
+ variant === 'default' && 'bg-[var(--receipt-bg)] text-[var(--text-color)]',
207
+ variant === 'filled' && 'bg-[var(--border-color)] text-[var(--receipt-bg)]',
208
+ variant === 'circle' && 'bg-[var(--receipt-bg)]',
209
+ variant === 'circle-inverted' && 'bg-[var(--border-color)]',
210
+ className,
211
+ )}
212
+ style={{ width: size, height: size }}
213
+ >
214
+ {(variant === 'circle' || variant === 'circle-inverted') && (
215
+ <div
216
+ className={cn(
217
+ 'absolute',
218
+ variant === 'circle' && 'bg-[var(--border-color)]',
219
+ variant === 'circle-inverted' && 'bg-[var(--receipt-bg)]',
220
+ )}
221
+ style={{ width: circleDiameter, height: circleDiameter, borderRadius: '50%' }}
222
+ />
223
+ )}
224
+ <span
225
+ className={cn(
226
+ 'relative',
227
+ variant === 'circle' && 'text-[var(--receipt-bg)]',
228
+ variant === 'circle-inverted' && 'text-[var(--text-color)]',
229
+ (variant === 'default' || variant === 'filled') && 'text-inherit',
230
+ )}
231
+ >
232
+ {children}
233
+ </span>
234
+ </div>
235
+ )
236
+ }
237
+
238
+ // ─── LEDGER ───────────────────────────────────────────────────────────────────
239
+ //
240
+ // A self-contained block: optional section label, key/value rows, divider, total.
241
+
242
+ export function Ledger({
243
+ title,
244
+ rows,
245
+ total,
246
+ className,
247
+ }: {
248
+ title?: string
249
+ rows: { label: string; value: string; fill?: boolean }[]
250
+ total?: { label: string; value: string }
251
+ className?: string
252
+ }) {
253
+ return (
254
+ <div className={cn('text-xs', className)}>
255
+ {title && <SectionLabel>{title}</SectionLabel>}
256
+ <div className="py-2 space-y-px">
257
+ {rows.map((row, i) => (
258
+ <Row key={i} label={row.label} value={row.value} fill={row.fill} />
259
+ ))}
260
+ </div>
261
+ {total && (
262
+ <>
263
+ <Divider variant="equals" />
264
+ <Row label={total.label} value={total.value} bold />
265
+ </>
266
+ )}
267
+ </div>
268
+ )
269
+ }
@@ -0,0 +1 @@
1
+ export { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable'
@@ -0,0 +1 @@
1
+ export { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
@@ -0,0 +1,110 @@
1
+ import type * as React from 'react'
2
+
3
+ import {
4
+ SelectContent,
5
+ SelectGroup,
6
+ SelectItem,
7
+ SelectLabel,
8
+ Select as SelectPrimitive,
9
+ SelectTrigger,
10
+ SelectValue,
11
+ } from '@/components/ui/select'
12
+ import { cn } from '@/lib/utils'
13
+
14
+ export interface SelectOptionData {
15
+ value: string
16
+ label: React.ReactNode
17
+ disabled?: boolean
18
+ }
19
+
20
+ export interface SelectGroupData {
21
+ label?: string
22
+ options: SelectOptionData[]
23
+ }
24
+
25
+ export interface SelectProps {
26
+ options?: SelectOptionData[]
27
+ groups?: SelectGroupData[]
28
+ placeholder?: string
29
+ label?: string
30
+ error?: string
31
+ disabled?: boolean
32
+ required?: boolean
33
+ className?: string
34
+ triggerClassName?: string
35
+ value?: string
36
+ defaultValue?: string
37
+ onValueChange?: (value: string) => void
38
+ children?: React.ReactNode
39
+ }
40
+
41
+ function Select({
42
+ options,
43
+ groups,
44
+ placeholder = 'Select...',
45
+ label,
46
+ error,
47
+ disabled = false,
48
+ required = false,
49
+ className,
50
+ triggerClassName,
51
+ value,
52
+ defaultValue,
53
+ onValueChange,
54
+ children,
55
+ }: SelectProps) {
56
+ const hasError = Boolean(error)
57
+
58
+ const rootProps: Record<string, unknown> = { disabled }
59
+ if (value !== undefined) rootProps.value = value
60
+ if (defaultValue !== undefined) rootProps.defaultValue = defaultValue
61
+ if (onValueChange !== undefined) rootProps.onValueChange = onValueChange
62
+
63
+ const content = options ? (
64
+ <SelectContent>
65
+ {options.map((opt) => (
66
+ <SelectItem key={opt.value} value={opt.value} disabled={opt.disabled}>
67
+ {opt.label}
68
+ </SelectItem>
69
+ ))}
70
+ </SelectContent>
71
+ ) : groups ? (
72
+ <SelectContent>
73
+ {groups.map((group, i) => (
74
+ <SelectGroup key={`group-${i}`}>
75
+ {group.label && <SelectLabel>{group.label}</SelectLabel>}
76
+ {group.options.map((opt) => (
77
+ <SelectItem key={opt.value} value={opt.value} disabled={opt.disabled}>
78
+ {opt.label}
79
+ </SelectItem>
80
+ ))}
81
+ </SelectGroup>
82
+ ))}
83
+ </SelectContent>
84
+ ) : null
85
+
86
+ return (
87
+ <div className={cn('flex flex-col gap-1', className)} data-invalid={hasError || undefined}>
88
+ {label && (
89
+ <span className="text-xs font-medium uppercase tracking-wider">
90
+ {label}
91
+ {required && <span className="text-destructive ml-0.5">*</span>}
92
+ </span>
93
+ )}
94
+ <SelectPrimitive {...rootProps}>
95
+ <SelectTrigger
96
+ className={triggerClassName}
97
+ aria-invalid={hasError}
98
+ aria-label={label || undefined}
99
+ >
100
+ <SelectValue placeholder={placeholder} />
101
+ </SelectTrigger>
102
+ {content}
103
+ {children}
104
+ </SelectPrimitive>
105
+ {error && <p className="text-[10px] text-destructive">{error}</p>}
106
+ </div>
107
+ )
108
+ }
109
+
110
+ export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue }