@pylonsync/create-pylon 0.3.357 → 0.3.359
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/package.json +1 -1
- package/templates/marketplace/README.md +29 -14
- package/templates/marketplace/app/error.tsx +30 -16
- package/templates/marketplace/app/globals.css +140 -24
- package/templates/marketplace/app/layout.tsx +60 -31
- package/templates/marketplace/app/listing/[id]/page.tsx +160 -60
- package/templates/marketplace/app/me/page.tsx +3 -3
- package/templates/marketplace/app/not-found.tsx +19 -10
- package/templates/marketplace/app/page.tsx +287 -97
- package/templates/marketplace/app/sell/page.tsx +11 -9
- package/templates/marketplace/app.ts +5 -4
- package/templates/marketplace/client/AuthNav.tsx +4 -8
- package/templates/marketplace/client/LiveListingStatus.tsx +60 -0
- package/templates/marketplace/client/LiveTicker.tsx +22 -10
- package/templates/marketplace/client/LoginCard.tsx +108 -62
- package/templates/marketplace/client/MarketProvider.tsx +16 -2
- package/templates/marketplace/client/MyMarket.tsx +126 -56
- package/templates/marketplace/client/OfferPanel.tsx +222 -112
- package/templates/marketplace/client/ScrollToListingsLink.tsx +29 -0
- package/templates/marketplace/client/SeedOnEmpty.tsx +7 -6
- package/templates/marketplace/client/SellForm.tsx +198 -40
- package/templates/marketplace/client/ThemeToggle.tsx +62 -0
- package/templates/marketplace/client/WatchButton.tsx +19 -8
- package/templates/marketplace/client/market.ts +10 -4
- package/templates/marketplace/components/ui/alert.tsx +66 -0
- package/templates/marketplace/components/ui/badge.tsx +5 -1
- package/templates/marketplace/components/ui/button.tsx +9 -7
- package/templates/marketplace/components/ui/card.tsx +2 -2
- package/templates/marketplace/components/ui/empty.tsx +104 -0
- package/templates/marketplace/components/ui/field.tsx +246 -0
- package/templates/marketplace/components/ui/input.tsx +2 -2
- package/templates/marketplace/components/ui/label.tsx +4 -3
- package/templates/marketplace/components/ui/native-select.tsx +62 -0
- package/templates/marketplace/components/ui/separator.tsx +26 -0
- package/templates/marketplace/components/ui/skeleton.tsx +13 -0
- package/templates/marketplace/components/ui/spinner.tsx +16 -0
- package/templates/marketplace/components/ui/textarea.tsx +2 -2
- package/templates/marketplace/{ui → components/ui}/tokens.css +1 -1
- package/templates/marketplace/components.json +20 -0
- package/templates/marketplace/functions/seedMarket.ts +14 -12
- package/templates/marketplace/lib/catalog.ts +32 -0
- package/templates/marketplace/package.json +10 -9
- package/templates/marketplace/public/images/marketplace-hero.webp +0 -0
- package/templates/marketplace/tests/example.test.ts +76 -8
- package/templates/marketplace/tsconfig.json +13 -2
- package/templates/marketplace/components/ui/select.tsx +0 -39
- package/templates/marketplace/ui/badge.tsx +0 -30
- package/templates/marketplace/ui/button.tsx +0 -49
- package/templates/marketplace/ui/card.tsx +0 -48
- package/templates/marketplace/ui/input.tsx +0 -17
- package/templates/marketplace/ui/label.tsx +0 -18
- package/templates/marketplace/ui/textarea.tsx +0 -17
- package/templates/marketplace/ui/utils.ts +0 -6
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
data-slot="empty"
|
|
9
|
+
className={cn(
|
|
10
|
+
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
data-slot="empty-header"
|
|
22
|
+
className={cn(
|
|
23
|
+
"flex max-w-sm flex-col items-center gap-2 text-center",
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const emptyMediaVariants = cva(
|
|
32
|
+
"mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
33
|
+
{
|
|
34
|
+
variants: {
|
|
35
|
+
variant: {
|
|
36
|
+
default: "bg-transparent",
|
|
37
|
+
icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
defaultVariants: {
|
|
41
|
+
variant: "default",
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
function EmptyMedia({
|
|
47
|
+
className,
|
|
48
|
+
variant = "default",
|
|
49
|
+
...props
|
|
50
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
data-slot="empty-icon"
|
|
54
|
+
data-variant={variant}
|
|
55
|
+
className={cn(emptyMediaVariants({ variant, className }))}
|
|
56
|
+
{...props}
|
|
57
|
+
/>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
data-slot="empty-title"
|
|
65
|
+
className={cn("text-lg font-medium tracking-tight", className)}
|
|
66
|
+
{...props}
|
|
67
|
+
/>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
data-slot="empty-description"
|
|
75
|
+
className={cn(
|
|
76
|
+
"text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
|
|
77
|
+
className
|
|
78
|
+
)}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
data-slot="empty-content"
|
|
88
|
+
className={cn(
|
|
89
|
+
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
|
|
90
|
+
className
|
|
91
|
+
)}
|
|
92
|
+
{...props}
|
|
93
|
+
/>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
Empty,
|
|
99
|
+
EmptyHeader,
|
|
100
|
+
EmptyTitle,
|
|
101
|
+
EmptyDescription,
|
|
102
|
+
EmptyContent,
|
|
103
|
+
EmptyMedia,
|
|
104
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { useMemo } from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
import { Label } from "@/components/ui/label"
|
|
6
|
+
import { Separator } from "@/components/ui/separator"
|
|
7
|
+
|
|
8
|
+
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
|
9
|
+
return (
|
|
10
|
+
<fieldset
|
|
11
|
+
data-slot="field-set"
|
|
12
|
+
className={cn(
|
|
13
|
+
"flex flex-col gap-6",
|
|
14
|
+
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
|
15
|
+
className
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function FieldLegend({
|
|
23
|
+
className,
|
|
24
|
+
variant = "legend",
|
|
25
|
+
...props
|
|
26
|
+
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
|
|
27
|
+
return (
|
|
28
|
+
<legend
|
|
29
|
+
data-slot="field-legend"
|
|
30
|
+
data-variant={variant}
|
|
31
|
+
className={cn(
|
|
32
|
+
"mb-3 font-medium",
|
|
33
|
+
"data-[variant=legend]:text-base",
|
|
34
|
+
"data-[variant=label]:text-sm",
|
|
35
|
+
className
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
data-slot="field-group"
|
|
46
|
+
className={cn(
|
|
47
|
+
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
|
|
48
|
+
className
|
|
49
|
+
)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const fieldVariants = cva(
|
|
56
|
+
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
|
|
57
|
+
{
|
|
58
|
+
variants: {
|
|
59
|
+
orientation: {
|
|
60
|
+
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
|
|
61
|
+
horizontal: [
|
|
62
|
+
"flex-row items-center",
|
|
63
|
+
"[&>[data-slot=field-label]]:flex-auto",
|
|
64
|
+
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
65
|
+
],
|
|
66
|
+
responsive: [
|
|
67
|
+
"flex-col @md/field-group:flex-row @md/field-group:items-center [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto",
|
|
68
|
+
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
|
|
69
|
+
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
defaultVariants: {
|
|
74
|
+
orientation: "vertical",
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
function Field({
|
|
80
|
+
className,
|
|
81
|
+
orientation = "vertical",
|
|
82
|
+
...props
|
|
83
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
role="group"
|
|
87
|
+
data-slot="field"
|
|
88
|
+
data-orientation={orientation}
|
|
89
|
+
className={cn(fieldVariants({ orientation }), className)}
|
|
90
|
+
{...props}
|
|
91
|
+
/>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
96
|
+
return (
|
|
97
|
+
<div
|
|
98
|
+
data-slot="field-content"
|
|
99
|
+
className={cn(
|
|
100
|
+
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
|
101
|
+
className
|
|
102
|
+
)}
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function FieldLabel({
|
|
109
|
+
className,
|
|
110
|
+
...props
|
|
111
|
+
}: React.ComponentProps<typeof Label>) {
|
|
112
|
+
return (
|
|
113
|
+
<Label
|
|
114
|
+
data-slot="field-label"
|
|
115
|
+
className={cn(
|
|
116
|
+
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
|
117
|
+
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
|
|
118
|
+
"has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5 dark:has-data-[state=checked]:bg-primary/10",
|
|
119
|
+
className
|
|
120
|
+
)}
|
|
121
|
+
{...props}
|
|
122
|
+
/>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
data-slot="field-label"
|
|
130
|
+
className={cn(
|
|
131
|
+
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
|
|
132
|
+
className
|
|
133
|
+
)}
|
|
134
|
+
{...props}
|
|
135
|
+
/>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
140
|
+
return (
|
|
141
|
+
<p
|
|
142
|
+
data-slot="field-description"
|
|
143
|
+
className={cn(
|
|
144
|
+
"text-sm leading-normal font-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance",
|
|
145
|
+
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
|
|
146
|
+
"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
|
|
147
|
+
className
|
|
148
|
+
)}
|
|
149
|
+
{...props}
|
|
150
|
+
/>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function FieldSeparator({
|
|
155
|
+
children,
|
|
156
|
+
className,
|
|
157
|
+
...props
|
|
158
|
+
}: React.ComponentProps<"div"> & {
|
|
159
|
+
children?: React.ReactNode
|
|
160
|
+
}) {
|
|
161
|
+
return (
|
|
162
|
+
<div
|
|
163
|
+
data-slot="field-separator"
|
|
164
|
+
data-content={!!children}
|
|
165
|
+
className={cn(
|
|
166
|
+
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
|
167
|
+
className
|
|
168
|
+
)}
|
|
169
|
+
{...props}
|
|
170
|
+
>
|
|
171
|
+
<Separator className="absolute inset-0 top-1/2" />
|
|
172
|
+
{children && (
|
|
173
|
+
<span
|
|
174
|
+
className="relative mx-auto block w-fit bg-card px-2 text-muted-foreground"
|
|
175
|
+
data-slot="field-separator-content"
|
|
176
|
+
>
|
|
177
|
+
{children}
|
|
178
|
+
</span>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function FieldError({
|
|
185
|
+
className,
|
|
186
|
+
children,
|
|
187
|
+
errors,
|
|
188
|
+
...props
|
|
189
|
+
}: React.ComponentProps<"div"> & {
|
|
190
|
+
errors?: Array<{ message?: string } | undefined>
|
|
191
|
+
}) {
|
|
192
|
+
const content = useMemo(() => {
|
|
193
|
+
if (children) {
|
|
194
|
+
return children
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!errors?.length) {
|
|
198
|
+
return null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const uniqueErrors = [
|
|
202
|
+
...new Map(errors.map((error) => [error?.message, error])).values(),
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
if (uniqueErrors?.length == 1) {
|
|
206
|
+
return uniqueErrors[0]?.message
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<ul className="ml-4 flex list-disc flex-col gap-1">
|
|
211
|
+
{uniqueErrors.map(
|
|
212
|
+
(error, index) =>
|
|
213
|
+
error?.message && <li key={index}>{error.message}</li>
|
|
214
|
+
)}
|
|
215
|
+
</ul>
|
|
216
|
+
)
|
|
217
|
+
}, [children, errors])
|
|
218
|
+
|
|
219
|
+
if (!content) {
|
|
220
|
+
return null
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<div
|
|
225
|
+
role="alert"
|
|
226
|
+
data-slot="field-error"
|
|
227
|
+
className={cn("text-sm font-normal text-destructive", className)}
|
|
228
|
+
{...props}
|
|
229
|
+
>
|
|
230
|
+
{content}
|
|
231
|
+
</div>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export {
|
|
236
|
+
Field,
|
|
237
|
+
FieldLabel,
|
|
238
|
+
FieldDescription,
|
|
239
|
+
FieldError,
|
|
240
|
+
FieldGroup,
|
|
241
|
+
FieldLegend,
|
|
242
|
+
FieldSeparator,
|
|
243
|
+
FieldSet,
|
|
244
|
+
FieldContent,
|
|
245
|
+
FieldTitle,
|
|
246
|
+
}
|
|
@@ -12,9 +12,9 @@ function Input({
|
|
|
12
12
|
type={type}
|
|
13
13
|
data-slot="input"
|
|
14
14
|
className={cn(
|
|
15
|
-
"flex h-
|
|
15
|
+
"flex min-h-11 w-full rounded-lg border border-input bg-background px-3 py-2 text-sm transition-colors",
|
|
16
16
|
"placeholder:text-muted-foreground",
|
|
17
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
|
|
17
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
18
18
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
19
19
|
// Date/time inputs render a native picker indicator that ignores the
|
|
20
20
|
// text color; nudge it so it stays visible in dark mode.
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import { Label as LabelPrimitive } from "radix-ui";
|
|
2
3
|
|
|
3
4
|
import { cn } from "@/lib/utils";
|
|
4
5
|
|
|
5
6
|
function Label({
|
|
6
7
|
className,
|
|
7
8
|
...props
|
|
8
|
-
}: React.
|
|
9
|
+
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
9
10
|
return (
|
|
10
|
-
<
|
|
11
|
+
<LabelPrimitive.Root
|
|
11
12
|
data-slot="label"
|
|
12
13
|
className={cn(
|
|
13
|
-
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-
|
|
14
|
+
"flex items-center gap-2 text-sm font-medium leading-none select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
14
15
|
className,
|
|
15
16
|
)}
|
|
16
17
|
{...props}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { ChevronDownIcon } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
function NativeSelect({
|
|
7
|
+
className,
|
|
8
|
+
size = "default",
|
|
9
|
+
...props
|
|
10
|
+
}: Omit<React.ComponentProps<"select">, "size"> & { size?: "sm" | "default" }) {
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
className="group/native-select relative w-full has-[select:disabled]:opacity-50"
|
|
14
|
+
data-slot="native-select-wrapper"
|
|
15
|
+
>
|
|
16
|
+
<select
|
|
17
|
+
data-slot="native-select"
|
|
18
|
+
data-size={size}
|
|
19
|
+
className={cn(
|
|
20
|
+
"h-9 w-full min-w-0 appearance-none rounded-md border border-input bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed data-[size=sm]:h-8 data-[size=sm]:py-1 dark:bg-input/30 dark:hover:bg-input/50",
|
|
21
|
+
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
|
22
|
+
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
<ChevronDownIcon
|
|
28
|
+
className="pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 text-muted-foreground opacity-50 select-none"
|
|
29
|
+
aria-hidden="true"
|
|
30
|
+
data-slot="native-select-icon"
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function NativeSelectOption({
|
|
37
|
+
className,
|
|
38
|
+
...props
|
|
39
|
+
}: React.ComponentProps<"option">) {
|
|
40
|
+
return (
|
|
41
|
+
<option
|
|
42
|
+
data-slot="native-select-option"
|
|
43
|
+
className={cn("bg-[Canvas] text-[CanvasText]", className)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function NativeSelectOptGroup({
|
|
50
|
+
className,
|
|
51
|
+
...props
|
|
52
|
+
}: React.ComponentProps<"optgroup">) {
|
|
53
|
+
return (
|
|
54
|
+
<optgroup
|
|
55
|
+
data-slot="native-select-optgroup"
|
|
56
|
+
className={cn("bg-[Canvas] text-[CanvasText]", className)}
|
|
57
|
+
{...props}
|
|
58
|
+
/>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { NativeSelect, NativeSelectOptGroup, NativeSelectOption }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Separator as SeparatorPrimitive } from "radix-ui"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
function Separator({
|
|
7
|
+
className,
|
|
8
|
+
orientation = "horizontal",
|
|
9
|
+
decorative = true,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
|
12
|
+
return (
|
|
13
|
+
<SeparatorPrimitive.Root
|
|
14
|
+
data-slot="separator"
|
|
15
|
+
decorative={decorative}
|
|
16
|
+
orientation={orientation}
|
|
17
|
+
className={cn(
|
|
18
|
+
"shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { Separator }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils"
|
|
2
|
+
|
|
3
|
+
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
data-slot="skeleton"
|
|
7
|
+
className={cn("animate-pulse rounded-md bg-accent", className)}
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { Skeleton }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Loader2Icon } from "lucide-react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
|
6
|
+
return (
|
|
7
|
+
<Loader2Icon
|
|
8
|
+
role="status"
|
|
9
|
+
aria-label="Loading"
|
|
10
|
+
className={cn("size-4 animate-spin", className)}
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { Spinner }
|
|
@@ -10,9 +10,9 @@ function Textarea({
|
|
|
10
10
|
<textarea
|
|
11
11
|
data-slot="textarea"
|
|
12
12
|
className={cn(
|
|
13
|
-
"flex min-h-
|
|
13
|
+
"flex min-h-24 w-full rounded-lg border border-input bg-background px-3 py-2.5 text-sm",
|
|
14
14
|
"placeholder:text-muted-foreground",
|
|
15
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
|
|
15
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
16
16
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
17
17
|
className,
|
|
18
18
|
)}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Design-token base layer (vendored from examples/_shared so the market is a
|
|
3
3
|
* self-contained, standalone-deployable example — no unpublished workspace dep).
|
|
4
4
|
*
|
|
5
|
-
* globals.css includes this with `@import "../ui/tokens.css";` AFTER
|
|
5
|
+
* globals.css includes this with `@import "../components/ui/tokens.css";` AFTER
|
|
6
6
|
* `@import "tailwindcss";` and AFTER its own `@theme` block.
|
|
7
7
|
* The tokens reference semantic colors so any example's theme cascades
|
|
8
8
|
* automatically.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": false,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "app/globals.css",
|
|
9
|
+
"baseColor": "zinc",
|
|
10
|
+
"cssVariables": true
|
|
11
|
+
},
|
|
12
|
+
"aliases": {
|
|
13
|
+
"components": "@/components",
|
|
14
|
+
"utils": "@/lib/utils",
|
|
15
|
+
"ui": "@/components/ui",
|
|
16
|
+
"lib": "@/lib",
|
|
17
|
+
"hooks": "@/hooks"
|
|
18
|
+
},
|
|
19
|
+
"iconLibrary": "lucide"
|
|
20
|
+
}
|
|
@@ -10,20 +10,21 @@ const DEMO: Array<{
|
|
|
10
10
|
price: number;
|
|
11
11
|
category: string;
|
|
12
12
|
condition: string;
|
|
13
|
+
imageUrl: string;
|
|
13
14
|
seed: string;
|
|
14
15
|
}> = [
|
|
15
|
-
{ seller: "maple-fox", title: "Herman Miller Aeron (size B)", description: "Fully loaded, posture-fit SL. Light desk use, no squeaks.", price: 540, category: "furniture", condition: "like-new", seed: "a1f3" },
|
|
16
|
-
{ seller: "amber-lynx", title: "Kodak Retina IIa rangefinder", description: "1950s folding 35mm. Clean glass, accurate shutter, leather case.", price: 185, category: "cameras", condition: "good", seed: "b7c2" },
|
|
17
|
-
{ seller: "swift-otter", title: "Specialized Allez road bike, 54cm", description: "Shimano Claris, fresh bar tape, recently tuned. Fast commuter.", price: 420, category: "bikes", condition: "good", seed: "c4e9" },
|
|
18
|
-
{ seller: "cosmic-wren", title: "Vintage Technics SL-1200 MK2", description: "Legendary direct-drive turntable. Spins true, new slipmat.", price: 650, category: "audio", condition: "good", seed: "d8a1" },
|
|
19
|
-
{ seller: "ivory-sparrow", title: "Le Creuset 5.5qt Dutch oven", description: "Flame orange, enamel intact. The one everyone wants.", price: 120, category: "kitchen", condition: "like-new", seed: "e2b6" },
|
|
20
|
-
{ seller: "slate-heron", title: 'LG 27" 4K UltraFine monitor', description: "USB-C, 60Hz, color-accurate. Box + cables included.", price: 240, category: "electronics", condition: "good", seed: "f5d0" },
|
|
21
|
-
{ seller: "maple-fox", title: "Eames-style lounge + ottoman", description: "Walnut + tan leather replica. Sturdy, very comfortable.", price: 380, category: "furniture", condition: "good", seed: "a9c4" },
|
|
22
|
-
{ seller: "amber-lynx", title: "Patagonia Black Hole 55L duffel", description: "One trip old. Weatherproof, zips perfect. Forest green.", price: 75, category: "outdoor", condition: "like-new", seed: "b3f8" },
|
|
23
|
-
{ seller: "cosmic-wren", title: "Fender Player Stratocaster, sunburst", description: "Maple neck, plays great. Small buckle rash on back.", price: 560, category: "instruments", condition: "good", seed: "d1e7" },
|
|
24
|
-
{ seller: "slate-heron", title: "iPad Air (5th gen) 64GB", description: "Space gray,
|
|
25
|
-
{ seller: "ivory-sparrow", title: "Mid-century teak sideboard", description: "Three drawers, two cabinets. Real teak, refinished top.", price: 295, category: "furniture", condition: "good", seed: "e7c3" },
|
|
26
|
-
{ seller: "swift-otter", title: "Brooks Brothers wool overcoat, 40R", description: "Charcoal herringbone, fully lined. Dry-cleaned, ready to wear.", price: 90, category: "apparel", condition: "good", seed: "c2b5" },
|
|
16
|
+
{ seller: "maple-fox", title: "Herman Miller Aeron (size B)", description: "Fully loaded, posture-fit SL. Light desk use, no squeaks.", price: 540, category: "furniture", condition: "like-new", imageUrl: "https://images.unsplash.com/photo-1758607010203-d6b7f6ec9262?auto=format&fit=crop&w=1200&q=82", seed: "a1f3" },
|
|
17
|
+
{ seller: "amber-lynx", title: "Kodak Retina IIa rangefinder", description: "1950s folding 35mm. Clean glass, accurate shutter, leather case.", price: 185, category: "cameras", condition: "good", imageUrl: "https://images.unsplash.com/photo-1516035069371-29a1b244cc32?auto=format&fit=crop&w=1200&q=82", seed: "b7c2" },
|
|
18
|
+
{ seller: "swift-otter", title: "Specialized Allez road bike, 54cm", description: "Shimano Claris, fresh bar tape, recently tuned. Fast commuter.", price: 420, category: "bikes", condition: "good", imageUrl: "https://images.unsplash.com/photo-1532298229144-0ec0c57515c7?auto=format&fit=crop&w=1200&q=82", seed: "c4e9" },
|
|
19
|
+
{ seller: "cosmic-wren", title: "Vintage Technics SL-1200 MK2", description: "Legendary direct-drive turntable. Spins true, new slipmat.", price: 650, category: "audio", condition: "good", imageUrl: "https://images.unsplash.com/photo-1599723895995-80e160d14fe4?auto=format&fit=crop&w=1200&q=82", seed: "d8a1" },
|
|
20
|
+
{ seller: "ivory-sparrow", title: "Le Creuset 5.5qt Dutch oven", description: "Flame orange, enamel intact. The one everyone wants.", price: 120, category: "kitchen", condition: "like-new", imageUrl: "https://images.unsplash.com/photo-1559137703-44aeed6161b6?auto=format&fit=crop&w=1200&q=82", seed: "e2b6" },
|
|
21
|
+
{ seller: "slate-heron", title: 'LG 27" 4K UltraFine monitor', description: "USB-C, 60Hz, color-accurate. Box + cables included.", price: 240, category: "electronics", condition: "good", imageUrl: "https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?auto=format&fit=crop&w=1200&q=82", seed: "f5d0" },
|
|
22
|
+
{ seller: "maple-fox", title: "Eames-style lounge + ottoman", description: "Walnut + tan leather replica. Sturdy, very comfortable.", price: 380, category: "furniture", condition: "good", imageUrl: "https://images.unsplash.com/photo-1567538096630-e0c55bd6374c?auto=format&fit=crop&w=1200&q=82", seed: "a9c4" },
|
|
23
|
+
{ seller: "amber-lynx", title: "Patagonia Black Hole 55L duffel", description: "One trip old. Weatherproof, zips perfect. Forest green.", price: 75, category: "outdoor", condition: "like-new", imageUrl: "https://images.unsplash.com/photo-1699319656128-6e353d6b9c7c?auto=format&fit=crop&w=1200&q=82", seed: "b3f8" },
|
|
24
|
+
{ seller: "cosmic-wren", title: "Fender Player Stratocaster, sunburst", description: "Maple neck, plays great. Small buckle rash on back.", price: 560, category: "instruments", condition: "good", imageUrl: "https://images.unsplash.com/photo-1525201548942-d8732f6617a0?auto=format&fit=crop&w=1200&q=82", seed: "d1e7" },
|
|
25
|
+
{ seller: "slate-heron", title: "iPad Air (5th gen) 64GB", description: "Space gray, excellent battery health, screen flawless.", price: 330, category: "electronics", condition: "like-new", imageUrl: "https://images.unsplash.com/photo-1544244015-0df4b3ffc6b0?auto=format&fit=crop&w=1200&q=82", seed: "f0a2" },
|
|
26
|
+
{ seller: "ivory-sparrow", title: "Mid-century teak sideboard", description: "Three drawers, two cabinets. Real teak, refinished top.", price: 295, category: "furniture", condition: "good", imageUrl: "https://images.unsplash.com/photo-1558997519-83ea9252edf8?auto=format&fit=crop&w=1200&q=82", seed: "e7c3" },
|
|
27
|
+
{ seller: "swift-otter", title: "Brooks Brothers wool overcoat, 40R", description: "Charcoal herringbone, fully lined. Dry-cleaned, ready to wear.", price: 90, category: "apparel", condition: "good", imageUrl: "https://images.unsplash.com/photo-1551488831-00ddcb6c6bd3?auto=format&fit=crop&w=1200&q=82", seed: "c2b5" },
|
|
27
28
|
];
|
|
28
29
|
|
|
29
30
|
interface SeedMarketArgs {
|
|
@@ -80,6 +81,7 @@ export default mutation<SeedMarketArgs, SeedMarketResult>({
|
|
|
80
81
|
category: d.category,
|
|
81
82
|
condition: d.condition,
|
|
82
83
|
status: "active",
|
|
84
|
+
imageUrl: d.imageUrl,
|
|
83
85
|
seed: d.seed,
|
|
84
86
|
createdAt: new Date(now - (start + n) * 7 * 60_000).toISOString(),
|
|
85
87
|
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Listing } from "../client/market";
|
|
2
|
+
|
|
3
|
+
export type CatalogSort = "latest" | "price-low" | "price-high";
|
|
4
|
+
|
|
5
|
+
export function browseListings(
|
|
6
|
+
listings: Listing[],
|
|
7
|
+
options: { category?: string; query?: string; sort?: string },
|
|
8
|
+
): Listing[] {
|
|
9
|
+
const category = options.category || "all";
|
|
10
|
+
const query = options.query?.trim().toLocaleLowerCase() ?? "";
|
|
11
|
+
const sort: CatalogSort =
|
|
12
|
+
options.sort === "price-low" || options.sort === "price-high"
|
|
13
|
+
? options.sort
|
|
14
|
+
: "latest";
|
|
15
|
+
|
|
16
|
+
const filtered = listings.filter((listing) => {
|
|
17
|
+
if (category !== "all" && listing.category !== category) return false;
|
|
18
|
+
if (!query) return true;
|
|
19
|
+
return [
|
|
20
|
+
listing.title,
|
|
21
|
+
listing.description,
|
|
22
|
+
listing.category,
|
|
23
|
+
listing.sellerName,
|
|
24
|
+
].some((value) => value.toLocaleLowerCase().includes(query));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return [...filtered].sort((a, b) => {
|
|
28
|
+
if (sort === "price-low") return a.price - b.price;
|
|
29
|
+
if (sort === "price-high") return b.price - a.price;
|
|
30
|
+
return b.createdAt.localeCompare(a.createdAt);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -10,22 +10,23 @@
|
|
|
10
10
|
"test": "pylon test"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
+
"@pylonsync/functions": "^__PYLON_VERSION__",
|
|
13
14
|
"@pylonsync/react": "^__PYLON_VERSION__",
|
|
14
15
|
"@pylonsync/sdk": "^__PYLON_VERSION__",
|
|
15
|
-
"@pylonsync/functions": "^__PYLON_VERSION__",
|
|
16
16
|
"@pylonsync/sync": "^__PYLON_VERSION__",
|
|
17
|
-
"react": "^
|
|
18
|
-
"react-
|
|
19
|
-
"
|
|
17
|
+
"@radix-ui/react-dialog": "^1.1.4",
|
|
18
|
+
"@radix-ui/react-label": "^2.1.0",
|
|
19
|
+
"@radix-ui/react-slot": "^1.1.0",
|
|
20
20
|
"@tailwindcss/cli": "^4.3.0",
|
|
21
|
-
"tw-animate-css": "^1.2.0",
|
|
22
21
|
"class-variance-authority": "^0.7.1",
|
|
23
22
|
"clsx": "^2.1.1",
|
|
24
|
-
"tailwind-merge": "^2.5.0",
|
|
25
23
|
"lucide-react": "^0.460.0",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
24
|
+
"radix-ui": "^1.6.7",
|
|
25
|
+
"react": "^19.0.0",
|
|
26
|
+
"react-dom": "^19.0.0",
|
|
27
|
+
"tailwind-merge": "^2.5.0",
|
|
28
|
+
"tailwindcss": "^4.3.0",
|
|
29
|
+
"tw-animate-css": "^1.2.0"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@pylonsync/cli": "^__PYLON_VERSION__",
|
|
Binary file
|