@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,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,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 }
|