@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,154 @@
|
|
|
1
|
+
import type * as React from 'react'
|
|
2
|
+
import { useRef, useState, useMemo } from 'react'
|
|
3
|
+
import { XIcon } from 'lucide-react'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Combobox as ComboboxPrimitive,
|
|
7
|
+
ComboboxChipsInput,
|
|
8
|
+
ComboboxContent,
|
|
9
|
+
ComboboxEmpty,
|
|
10
|
+
ComboboxItem,
|
|
11
|
+
ComboboxList,
|
|
12
|
+
} from '@/components/ui/combobox'
|
|
13
|
+
import { cn } from '@/lib/utils'
|
|
14
|
+
|
|
15
|
+
export interface ComboboxOption {
|
|
16
|
+
value: string
|
|
17
|
+
label: string
|
|
18
|
+
icon?: React.ReactNode
|
|
19
|
+
description?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ComboboxProps {
|
|
23
|
+
options?: ComboboxOption[]
|
|
24
|
+
value?: string[]
|
|
25
|
+
onValueChange?: (ids: string[]) => void
|
|
26
|
+
placeholder?: string
|
|
27
|
+
emptyMessage?: string
|
|
28
|
+
label?: string
|
|
29
|
+
disabled?: boolean
|
|
30
|
+
className?: string
|
|
31
|
+
children?: React.ReactNode
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function Combobox({
|
|
35
|
+
options,
|
|
36
|
+
value = [],
|
|
37
|
+
onValueChange,
|
|
38
|
+
placeholder = 'Search...',
|
|
39
|
+
emptyMessage = 'No results found.',
|
|
40
|
+
label,
|
|
41
|
+
disabled = false,
|
|
42
|
+
className,
|
|
43
|
+
children,
|
|
44
|
+
}: ComboboxProps) {
|
|
45
|
+
const [inputValue, setInputValue] = useState('')
|
|
46
|
+
const anchorRef = useRef<HTMLDivElement>(null)
|
|
47
|
+
|
|
48
|
+
const optionsByValue = useMemo(() => {
|
|
49
|
+
const map = new Map<string, ComboboxOption>()
|
|
50
|
+
for (const opt of options ?? []) {
|
|
51
|
+
map.set(opt.value, opt)
|
|
52
|
+
}
|
|
53
|
+
return map
|
|
54
|
+
}, [options])
|
|
55
|
+
|
|
56
|
+
const filtered = useMemo(() => {
|
|
57
|
+
if (!options) return []
|
|
58
|
+
if (!inputValue) return options
|
|
59
|
+
const lower = inputValue.toLowerCase()
|
|
60
|
+
return options.filter(
|
|
61
|
+
(opt) =>
|
|
62
|
+
opt.label.toLowerCase().includes(lower) ||
|
|
63
|
+
opt.description?.toLowerCase().includes(lower),
|
|
64
|
+
)
|
|
65
|
+
}, [options, inputValue])
|
|
66
|
+
|
|
67
|
+
if (children) {
|
|
68
|
+
return (
|
|
69
|
+
<ComboboxPrimitive multiple disabled={disabled}>
|
|
70
|
+
{children}
|
|
71
|
+
</ComboboxPrimitive>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const removeValue = (v: string) => {
|
|
76
|
+
onValueChange?.(value.filter((x) => x !== v))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className={cn('flex flex-col gap-1', className)}>
|
|
81
|
+
{label && (
|
|
82
|
+
<span className="text-xs font-medium uppercase tracking-wider">
|
|
83
|
+
{label}
|
|
84
|
+
</span>
|
|
85
|
+
)}
|
|
86
|
+
<ComboboxPrimitive
|
|
87
|
+
multiple
|
|
88
|
+
value={value}
|
|
89
|
+
onValueChange={(val) => onValueChange?.(val as string[])}
|
|
90
|
+
disabled={disabled}
|
|
91
|
+
>
|
|
92
|
+
{/* Selected chips + inline search input */}
|
|
93
|
+
<div ref={anchorRef} className="flex min-h-8 flex-wrap items-center gap-1 border border-input bg-transparent px-2.5 py-1 text-xs">
|
|
94
|
+
{value.map((v) => {
|
|
95
|
+
const opt = optionsByValue.get(v)
|
|
96
|
+
return (
|
|
97
|
+
<span
|
|
98
|
+
key={v}
|
|
99
|
+
className="flex items-center gap-1 bg-muted px-1.5 py-0.5 text-xs font-medium whitespace-nowrap"
|
|
100
|
+
>
|
|
101
|
+
{opt?.icon && <span className="shrink-0">{opt.icon}</span>}
|
|
102
|
+
{opt?.label ?? v}
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
onClick={(e) => {
|
|
106
|
+
e.stopPropagation()
|
|
107
|
+
removeValue(v)
|
|
108
|
+
}}
|
|
109
|
+
className="opacity-50 hover:opacity-100 cursor-pointer bg-transparent border-none p-0"
|
|
110
|
+
aria-label={`Remove ${opt?.label ?? v}`}
|
|
111
|
+
>
|
|
112
|
+
<XIcon className="w-3 h-3" />
|
|
113
|
+
</button>
|
|
114
|
+
</span>
|
|
115
|
+
)
|
|
116
|
+
})}
|
|
117
|
+
<ComboboxChipsInput
|
|
118
|
+
placeholder={value.length === 0 ? placeholder : ''}
|
|
119
|
+
value={inputValue}
|
|
120
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setInputValue(e.target.value)}
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
<ComboboxContent anchor={anchorRef}>
|
|
124
|
+
<ComboboxList>
|
|
125
|
+
{filtered.map((opt) => (
|
|
126
|
+
<ComboboxItem key={opt.value} value={opt.value}>
|
|
127
|
+
{opt.icon && (
|
|
128
|
+
<span className="shrink-0">{opt.icon}</span>
|
|
129
|
+
)}
|
|
130
|
+
<span className="flex-1 min-w-0">
|
|
131
|
+
<span className="text-xs">{opt.label}</span>
|
|
132
|
+
{opt.description && (
|
|
133
|
+
<span className="block text-[10px] text-muted-foreground">
|
|
134
|
+
{opt.description}
|
|
135
|
+
</span>
|
|
136
|
+
)}
|
|
137
|
+
</span>
|
|
138
|
+
</ComboboxItem>
|
|
139
|
+
))}
|
|
140
|
+
</ComboboxList>
|
|
141
|
+
<ComboboxEmpty>{emptyMessage}</ComboboxEmpty>
|
|
142
|
+
</ComboboxContent>
|
|
143
|
+
</ComboboxPrimitive>
|
|
144
|
+
</div>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export {
|
|
149
|
+
Combobox,
|
|
150
|
+
ComboboxContent,
|
|
151
|
+
ComboboxEmpty,
|
|
152
|
+
ComboboxItem,
|
|
153
|
+
ComboboxList,
|
|
154
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import {
|
|
5
|
+
Command,
|
|
6
|
+
CommandInput,
|
|
7
|
+
CommandList,
|
|
8
|
+
CommandEmpty,
|
|
9
|
+
CommandGroup,
|
|
10
|
+
CommandItem,
|
|
11
|
+
CommandSeparator,
|
|
12
|
+
CommandShortcut,
|
|
13
|
+
} from '@/components/ui/command'
|
|
14
|
+
import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
|
|
15
|
+
import { cn } from '@/lib/utils'
|
|
16
|
+
|
|
17
|
+
function CommandDialog({
|
|
18
|
+
open,
|
|
19
|
+
onOpenChange,
|
|
20
|
+
children,
|
|
21
|
+
}: {
|
|
22
|
+
open?: boolean
|
|
23
|
+
onOpenChange?: (open: boolean) => void
|
|
24
|
+
children?: React.ReactNode
|
|
25
|
+
}) {
|
|
26
|
+
return (
|
|
27
|
+
<DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
|
|
28
|
+
<DialogPrimitive.Portal>
|
|
29
|
+
<DialogPrimitive.Backdrop className="fixed inset-0 z-50 bg-black/10 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0" />
|
|
30
|
+
<DialogPrimitive.Popup className="fixed top-[20%] left-1/2 z-50 w-full max-w-lg -translate-x-1/2 rounded-[var(--radius)] border-[length:var(--border-width)] border-foreground bg-popover text-popover-foreground shadow-none outline-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95">
|
|
31
|
+
<Command className="border-0">
|
|
32
|
+
{children}
|
|
33
|
+
</Command>
|
|
34
|
+
</DialogPrimitive.Popup>
|
|
35
|
+
</DialogPrimitive.Portal>
|
|
36
|
+
</DialogPrimitive.Root>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
Command,
|
|
42
|
+
CommandDialog,
|
|
43
|
+
CommandInput,
|
|
44
|
+
CommandList,
|
|
45
|
+
CommandEmpty,
|
|
46
|
+
CommandGroup,
|
|
47
|
+
CommandItem,
|
|
48
|
+
CommandSeparator,
|
|
49
|
+
CommandShortcut,
|
|
50
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { useMutation } from 'convex/react'
|
|
2
|
+
import { Check } from 'lucide-react'
|
|
3
|
+
import { BlockLoader } from '@/components/block-loader'
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
import { FormInput } from '@/components/form-input'
|
|
6
|
+
import { Button } from '@/components/button'
|
|
7
|
+
import { CAL_LINK, COMPANY_EMAIL, COMPANY_PHONE } from '@/lib/constants'
|
|
8
|
+
import { api } from '../../convex/_generated/api'
|
|
9
|
+
|
|
10
|
+
interface ContactFooterProps {
|
|
11
|
+
/**
|
|
12
|
+
* Override the Cal.com booking link
|
|
13
|
+
* @default CAL_LINK
|
|
14
|
+
*/
|
|
15
|
+
calLink?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ReceiptDivider() {
|
|
19
|
+
return (
|
|
20
|
+
<div className="text-xs my-3 tracking-[-1px] h-[1em] relative text-center overflow-hidden">
|
|
21
|
+
<span className="absolute left-1/2 top-0 -translate-x-1/2 whitespace-nowrap tracking-[-1px]">
|
|
22
|
+
..................................................
|
|
23
|
+
</span>
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function SectionHeader({ children }: { children: React.ReactNode }) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="text-[11px] font-bold mb-2 text-center py-[3px] border-t border-b bg-[var(--border-color)] text-[var(--receipt-bg)]">
|
|
31
|
+
{children}
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ContactFooter({ calLink = CAL_LINK }: ContactFooterProps) {
|
|
37
|
+
return (
|
|
38
|
+
<footer className="mt-12 max-w-[1152px] mx-auto px-6">
|
|
39
|
+
<div className="bg-[var(--receipt-bg)] border border-b-0">
|
|
40
|
+
<div className="max-w-[576px] mx-auto px-6 pt-6 pb-12">
|
|
41
|
+
<div className="text-center h-12 mb-3">
|
|
42
|
+
<div className="text-base font-bold mb-1"># CONTACT</div>
|
|
43
|
+
<div className="text-xs uppercase">request follow up</div>
|
|
44
|
+
<div className="text-[10px] text-[var(--muted-color)] mt-0.5 h-4">
|
|
45
|
+
or{' '}
|
|
46
|
+
<a href={calLink} className="underline">
|
|
47
|
+
book a call instead
|
|
48
|
+
</a>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<ReceiptDivider />
|
|
53
|
+
|
|
54
|
+
<SectionHeader>YOUR DETAILS</SectionHeader>
|
|
55
|
+
|
|
56
|
+
<FollowUpForm calLink={calLink} />
|
|
57
|
+
|
|
58
|
+
<div className="mt-6">
|
|
59
|
+
<ReceiptDivider />
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div className="text-center pt-4 text-[11px]">
|
|
63
|
+
<div className="mb-4">Thank You</div>
|
|
64
|
+
<div className="text-[var(--muted-color)] text-[10px] uppercase mb-1">
|
|
65
|
+
© {new Date().getFullYear()} LEITWARE
|
|
66
|
+
</div>
|
|
67
|
+
<div className="text-[var(--muted-color)] text-[10px] mb-1">
|
|
68
|
+
<a href={`mailto:${COMPANY_EMAIL}`} className="no-underline hover:underline">
|
|
69
|
+
{COMPANY_EMAIL}
|
|
70
|
+
</a>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="text-[var(--muted-color)] text-[10px] mb-2">
|
|
73
|
+
<a href={`tel:${COMPANY_PHONE}`} className="no-underline hover:underline">
|
|
74
|
+
+44 778 004 2494
|
|
75
|
+
</a>
|
|
76
|
+
</div>
|
|
77
|
+
<div className="flex gap-4 justify-center">
|
|
78
|
+
<a
|
|
79
|
+
href="/terms-and-conditions"
|
|
80
|
+
className="text-[var(--muted-color)] text-[10px] uppercase no-underline hover:underline"
|
|
81
|
+
>
|
|
82
|
+
Terms & Conditions
|
|
83
|
+
</a>
|
|
84
|
+
<a
|
|
85
|
+
href="/privacy"
|
|
86
|
+
className="text-[var(--muted-color)] text-[10px] uppercase no-underline hover:underline"
|
|
87
|
+
>
|
|
88
|
+
Privacy Policy
|
|
89
|
+
</a>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</footer>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function FollowUpForm({ calLink = CAL_LINK }: { calLink?: string }) {
|
|
99
|
+
const submitFollowUp = useMutation(api.myFunctions.submitFollowUp)
|
|
100
|
+
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
101
|
+
const [isSubmitted, setIsSubmitted] = useState(false)
|
|
102
|
+
const [error, setError] = useState<string | null>(null)
|
|
103
|
+
|
|
104
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
105
|
+
e.preventDefault()
|
|
106
|
+
setIsSubmitting(true)
|
|
107
|
+
setError(null)
|
|
108
|
+
|
|
109
|
+
const formData = new FormData(e.currentTarget)
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await submitFollowUp({
|
|
113
|
+
email: formData.get('email') as string,
|
|
114
|
+
name: formData.get('name') as string,
|
|
115
|
+
companyName: formData.get('companyName') as string,
|
|
116
|
+
website: (formData.get('website') as string) || undefined,
|
|
117
|
+
useCases: formData.get('useCases') as string,
|
|
118
|
+
otherDetails: (formData.get('otherDetails') as string) || undefined,
|
|
119
|
+
})
|
|
120
|
+
setIsSubmitted(true)
|
|
121
|
+
} catch {
|
|
122
|
+
setError('Something went wrong. Please try again.')
|
|
123
|
+
} finally {
|
|
124
|
+
setIsSubmitting(false)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (isSubmitted) {
|
|
129
|
+
return (
|
|
130
|
+
<div className="text-center py-8">
|
|
131
|
+
<Check className="w-6 h-6 mx-auto mb-3" />
|
|
132
|
+
<div className="font-bold uppercase text-xs">Received!</div>
|
|
133
|
+
<div className="text-[var(--muted-color)] text-[11px] mt-1">We'll be in touch soon.</div>
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<form onSubmit={handleSubmit}>
|
|
140
|
+
<div className="grid grid-cols-2 gap-3 mb-3">
|
|
141
|
+
<FormInput label="Name" required name="name" autoComplete="name" />
|
|
142
|
+
<FormInput label="Email" required name="email" type="email" autoComplete="email" />
|
|
143
|
+
</div>
|
|
144
|
+
<div className="grid grid-cols-2 gap-3 mb-3">
|
|
145
|
+
<FormInput label="Company" required name="companyName" autoComplete="organization" />
|
|
146
|
+
<FormInput label="Website" name="website" type="url" placeholder="https://..." autoComplete="url" />
|
|
147
|
+
</div>
|
|
148
|
+
<div className="mb-3">
|
|
149
|
+
<label htmlFor="useCases" className="text-xs font-medium uppercase tracking-wider">
|
|
150
|
+
What do you want to automate?<span className="text-destructive ml-0.5">*</span>
|
|
151
|
+
</label>
|
|
152
|
+
<textarea
|
|
153
|
+
id="useCases"
|
|
154
|
+
name="useCases"
|
|
155
|
+
required
|
|
156
|
+
rows={3}
|
|
157
|
+
className="w-full p-2 text-xs border bg-[var(--receipt-bg)] text-[var(--text-color)] resize-none mt-1"
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
<div className="mb-4">
|
|
161
|
+
<label htmlFor="otherDetails" className="text-xs font-medium uppercase tracking-wider">
|
|
162
|
+
Other Details
|
|
163
|
+
</label>
|
|
164
|
+
<textarea
|
|
165
|
+
id="otherDetails"
|
|
166
|
+
name="otherDetails"
|
|
167
|
+
rows={2}
|
|
168
|
+
className="w-full p-2 text-xs border bg-[var(--receipt-bg)] text-[var(--text-color)] resize-none mt-1"
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
{error && <p className="text-[11px] text-destructive mb-3">{error}</p>}
|
|
172
|
+
<Button type="submit" disabled={isSubmitting} className="w-full">
|
|
173
|
+
{isSubmitting ? (
|
|
174
|
+
<>
|
|
175
|
+
<BlockLoader />
|
|
176
|
+
Processing...
|
|
177
|
+
</>
|
|
178
|
+
) : (
|
|
179
|
+
'Request Follow Up'
|
|
180
|
+
)}
|
|
181
|
+
</Button>
|
|
182
|
+
<div className="text-center mt-3">
|
|
183
|
+
<Button
|
|
184
|
+
render={<a href={calLink} />}
|
|
185
|
+
variant="link"
|
|
186
|
+
className="text-[10px] text-muted-foreground"
|
|
187
|
+
>
|
|
188
|
+
or book a call instead →
|
|
189
|
+
</Button>
|
|
190
|
+
</div>
|
|
191
|
+
</form>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ContextMenu,
|
|
3
|
+
ContextMenuTrigger,
|
|
4
|
+
ContextMenuPortal,
|
|
5
|
+
ContextMenuContent,
|
|
6
|
+
ContextMenuItem,
|
|
7
|
+
ContextMenuCheckboxItem,
|
|
8
|
+
ContextMenuRadioGroup,
|
|
9
|
+
ContextMenuRadioItem,
|
|
10
|
+
ContextMenuLabel,
|
|
11
|
+
ContextMenuSeparator,
|
|
12
|
+
ContextMenuShortcut,
|
|
13
|
+
ContextMenuSub,
|
|
14
|
+
ContextMenuSubTrigger,
|
|
15
|
+
ContextMenuSubContent,
|
|
16
|
+
} from '@/components/ui/context-menu'
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Dialog as DialogPrimitiveBase } from '@base-ui/react/dialog'
|
|
2
|
+
import type * as React from 'react'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DialogClose,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogFooter,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
Dialog as DialogPrimitive,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
DialogTrigger,
|
|
13
|
+
} from '@/components/ui/dialog'
|
|
14
|
+
|
|
15
|
+
export interface DialogProps extends DialogPrimitiveBase.Root.Props {
|
|
16
|
+
trigger?: React.ReactNode
|
|
17
|
+
title?: React.ReactNode
|
|
18
|
+
description?: React.ReactNode
|
|
19
|
+
footer?: React.ReactNode
|
|
20
|
+
showCloseButton?: boolean
|
|
21
|
+
contentClassName?: string
|
|
22
|
+
children?: React.ReactNode
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function Dialog({
|
|
26
|
+
trigger,
|
|
27
|
+
title,
|
|
28
|
+
description,
|
|
29
|
+
footer,
|
|
30
|
+
showCloseButton = true,
|
|
31
|
+
contentClassName,
|
|
32
|
+
children,
|
|
33
|
+
...dialogProps
|
|
34
|
+
}: DialogProps) {
|
|
35
|
+
// Opinionated API: trigger + title + description + content + footer
|
|
36
|
+
if (trigger || title) {
|
|
37
|
+
return (
|
|
38
|
+
<DialogPrimitive {...dialogProps}>
|
|
39
|
+
{trigger && <DialogTrigger>{trigger}</DialogTrigger>}
|
|
40
|
+
<DialogContent showCloseButton={showCloseButton} className={contentClassName}>
|
|
41
|
+
{(title || description) && (
|
|
42
|
+
<DialogHeader>
|
|
43
|
+
{title && <DialogTitle>{title}</DialogTitle>}
|
|
44
|
+
{description && <DialogDescription>{description}</DialogDescription>}
|
|
45
|
+
</DialogHeader>
|
|
46
|
+
)}
|
|
47
|
+
{children}
|
|
48
|
+
{footer && <DialogFooter>{footer}</DialogFooter>}
|
|
49
|
+
</DialogContent>
|
|
50
|
+
</DialogPrimitive>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Fallback: children-based composition
|
|
55
|
+
return <DialogPrimitive {...dialogProps}>{children}</DialogPrimitive>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export {
|
|
59
|
+
Dialog,
|
|
60
|
+
DialogClose,
|
|
61
|
+
DialogContent,
|
|
62
|
+
DialogDescription,
|
|
63
|
+
DialogFooter,
|
|
64
|
+
DialogHeader,
|
|
65
|
+
DialogTitle,
|
|
66
|
+
DialogTrigger,
|
|
67
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Menu as MenuPrimitiveBase } from '@base-ui/react/menu'
|
|
2
|
+
import type * as React from 'react'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenuContent,
|
|
6
|
+
DropdownMenuItem,
|
|
7
|
+
DropdownMenu as DropdownMenuPrimitive,
|
|
8
|
+
DropdownMenuSeparator,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
} from '@/components/ui/dropdown-menu'
|
|
11
|
+
|
|
12
|
+
export interface DropdownMenuItemData {
|
|
13
|
+
label: React.ReactNode
|
|
14
|
+
onClick?: () => void
|
|
15
|
+
icon?: React.ReactNode
|
|
16
|
+
variant?: 'default' | 'destructive'
|
|
17
|
+
disabled?: boolean
|
|
18
|
+
separator?: false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DropdownMenuSeparatorData {
|
|
22
|
+
separator: true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type DropdownMenuEntry = DropdownMenuItemData | DropdownMenuSeparatorData
|
|
26
|
+
|
|
27
|
+
export interface DropdownMenuProps {
|
|
28
|
+
trigger: React.ReactNode
|
|
29
|
+
items?: DropdownMenuEntry[]
|
|
30
|
+
align?: 'start' | 'center' | 'end'
|
|
31
|
+
side?: 'top' | 'bottom' | 'left' | 'right'
|
|
32
|
+
contentClassName?: string
|
|
33
|
+
children?: React.ReactNode
|
|
34
|
+
open?: boolean
|
|
35
|
+
onOpenChange?: (open: boolean) => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function DropdownMenu({
|
|
39
|
+
trigger,
|
|
40
|
+
items,
|
|
41
|
+
align = 'start',
|
|
42
|
+
side = 'bottom',
|
|
43
|
+
contentClassName,
|
|
44
|
+
children,
|
|
45
|
+
open,
|
|
46
|
+
onOpenChange,
|
|
47
|
+
}: DropdownMenuProps) {
|
|
48
|
+
const rootProps: MenuPrimitiveBase.Root.Props = {}
|
|
49
|
+
if (open !== undefined) rootProps.open = open
|
|
50
|
+
if (onOpenChange !== undefined) rootProps.onOpenChange = onOpenChange
|
|
51
|
+
|
|
52
|
+
if (items) {
|
|
53
|
+
return (
|
|
54
|
+
<DropdownMenuPrimitive {...rootProps}>
|
|
55
|
+
<DropdownMenuTrigger>{trigger}</DropdownMenuTrigger>
|
|
56
|
+
<DropdownMenuContent align={align} side={side} className={contentClassName}>
|
|
57
|
+
{items.map((entry, i) => {
|
|
58
|
+
if (entry.separator) {
|
|
59
|
+
return <DropdownMenuSeparator key={`sep-${i}`} />
|
|
60
|
+
}
|
|
61
|
+
return (
|
|
62
|
+
<DropdownMenuItem
|
|
63
|
+
key={`item-${i}`}
|
|
64
|
+
onSelect={entry.onClick}
|
|
65
|
+
variant={entry.variant}
|
|
66
|
+
disabled={entry.disabled}
|
|
67
|
+
>
|
|
68
|
+
{entry.icon}
|
|
69
|
+
{entry.label}
|
|
70
|
+
</DropdownMenuItem>
|
|
71
|
+
)
|
|
72
|
+
})}
|
|
73
|
+
</DropdownMenuContent>
|
|
74
|
+
</DropdownMenuPrimitive>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<DropdownMenuPrimitive {...rootProps}>
|
|
80
|
+
<DropdownMenuTrigger>{trigger}</DropdownMenuTrigger>
|
|
81
|
+
<DropdownMenuContent align={align} side={side} className={contentClassName}>
|
|
82
|
+
{children}
|
|
83
|
+
</DropdownMenuContent>
|
|
84
|
+
</DropdownMenuPrimitive>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Re-export sub-components for advanced use
|
|
89
|
+
export {
|
|
90
|
+
DropdownMenu,
|
|
91
|
+
DropdownMenuContent,
|
|
92
|
+
DropdownMenuItem,
|
|
93
|
+
DropdownMenuSeparator,
|
|
94
|
+
DropdownMenuTrigger,
|
|
95
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
import { Input } from '@/components/ui/input'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
|
|
6
|
+
export interface FormInputProps extends Omit<React.ComponentProps<'input'>, 'size'> {
|
|
7
|
+
label?: string
|
|
8
|
+
description?: string
|
|
9
|
+
error?: string
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
required?: boolean
|
|
12
|
+
className?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function FormInput({
|
|
16
|
+
label,
|
|
17
|
+
description,
|
|
18
|
+
error,
|
|
19
|
+
disabled = false,
|
|
20
|
+
required = false,
|
|
21
|
+
className,
|
|
22
|
+
id,
|
|
23
|
+
...inputProps
|
|
24
|
+
}: FormInputProps) {
|
|
25
|
+
const generatedId = React.useId()
|
|
26
|
+
const inputId = id || generatedId
|
|
27
|
+
const hasError = Boolean(error)
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className={cn('flex flex-col gap-1', className)} data-invalid={hasError || undefined}>
|
|
31
|
+
{label && (
|
|
32
|
+
<label htmlFor={inputId} className="text-xs font-medium uppercase tracking-wider">
|
|
33
|
+
{label}
|
|
34
|
+
{required && <span className="text-destructive ml-0.5">*</span>}
|
|
35
|
+
</label>
|
|
36
|
+
)}
|
|
37
|
+
<Input
|
|
38
|
+
{...inputProps}
|
|
39
|
+
id={inputId}
|
|
40
|
+
disabled={disabled}
|
|
41
|
+
required={required}
|
|
42
|
+
aria-invalid={hasError}
|
|
43
|
+
aria-required={required}
|
|
44
|
+
aria-describedby={
|
|
45
|
+
[description ? `${inputId}-desc` : null, error ? `${inputId}-error` : null]
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.join(' ') || undefined
|
|
48
|
+
}
|
|
49
|
+
/>
|
|
50
|
+
{description && (
|
|
51
|
+
<p id={`${inputId}-desc`} className="text-[10px] text-muted-foreground">
|
|
52
|
+
{description}
|
|
53
|
+
</p>
|
|
54
|
+
)}
|
|
55
|
+
{error && (
|
|
56
|
+
<p id={`${inputId}-error`} className="text-[10px] text-destructive">
|
|
57
|
+
{error}
|
|
58
|
+
</p>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { FormInput }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Label } from '@/components/ui/label'
|