@pylonsync/create-pylon 0.3.242 → 0.3.244
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/bin/create-pylon.js +2 -2
- package/package.json +1 -1
- package/templates/ssr/app/counter/page.tsx +16 -10
- package/templates/ssr/app/globals.css +136 -0
- package/templates/ssr/app/layout.tsx +12 -9
- package/templates/ssr/app/page.tsx +60 -30
- package/templates/ssr/components/ui/button.tsx +56 -0
- package/templates/ssr/components/ui/card.tsx +90 -0
- package/templates/ssr/components.json +20 -0
- package/templates/ssr/lib/utils.ts +10 -0
- package/templates/ssr/package.json +7 -1
- package/templates/ssr/tsconfig.json +6 -2
package/bin/create-pylon.js
CHANGED
|
@@ -173,12 +173,12 @@ if (!flags.template) {
|
|
|
173
173
|
process.stdout.write(`\n${lines}\n`);
|
|
174
174
|
const ans = (
|
|
175
175
|
await rl.question(
|
|
176
|
-
`Template (${TEMPLATES_AVAILABLE.join(", ")}) [
|
|
176
|
+
`Template (${TEMPLATES_AVAILABLE.join(", ")}) [ssr]: `,
|
|
177
177
|
)
|
|
178
178
|
)
|
|
179
179
|
.trim()
|
|
180
180
|
.toLowerCase();
|
|
181
|
-
flags.template = TEMPLATES_AVAILABLE.includes(ans) ? ans : "
|
|
181
|
+
flags.template = TEMPLATES_AVAILABLE.includes(ans) ? ans : "ssr";
|
|
182
182
|
}
|
|
183
183
|
// `unified` templates (ssr) are a single app, not a monorepo — they take
|
|
184
184
|
// no platforms. Skip the platform prompt + validation for them entirely.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pylonsync/create-pylon",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.244",
|
|
4
4
|
"description": "Scaffold a new Pylon app — realtime backend + web/mobile/expo frontends in one command. Run via `npm create @pylonsync/pylon@latest`.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { Button } from "@/components/ui/button";
|
|
2
3
|
|
|
3
4
|
interface PageProps {
|
|
4
5
|
url: string;
|
|
@@ -8,40 +9,45 @@ interface PageProps {
|
|
|
8
9
|
// `app/counter/page.tsx` → `/counter`. This page is server-rendered AND
|
|
9
10
|
// interactive: the HTML arrives with the initial count already in it (try
|
|
10
11
|
// /counter?start=10), then the per-route chunk hydrates and useState takes
|
|
11
|
-
// over. No client/server split to manage — it's one component.
|
|
12
|
+
// over. No client/server split to manage — it's one component. The buttons
|
|
13
|
+
// are shadcn/ui `Button`s, hydrated in place.
|
|
12
14
|
export default function CounterPage({ searchParams }: PageProps) {
|
|
13
15
|
const start = Number(searchParams.start ?? "0") || 0;
|
|
14
16
|
const [count, setCount] = React.useState(start);
|
|
15
17
|
return (
|
|
16
18
|
<div className="space-y-6">
|
|
17
19
|
<h1 className="text-2xl font-semibold tracking-tight">Counter</h1>
|
|
18
|
-
<p className="text-
|
|
20
|
+
<p className="text-muted-foreground">
|
|
19
21
|
Rendered on the server, hydrated in the browser. The buttons work
|
|
20
22
|
because the page's JS chunk hydrated this exact markup.
|
|
21
23
|
</p>
|
|
22
24
|
<div className="flex items-center gap-4">
|
|
23
|
-
<
|
|
25
|
+
<Button
|
|
26
|
+
variant="outline"
|
|
27
|
+
size="icon"
|
|
24
28
|
onClick={() => setCount((c) => c - 1)}
|
|
25
|
-
|
|
29
|
+
aria-label="Decrement"
|
|
26
30
|
>
|
|
27
31
|
−
|
|
28
|
-
</
|
|
32
|
+
</Button>
|
|
29
33
|
<span className="min-w-12 text-center text-2xl font-semibold tabular-nums">
|
|
30
34
|
{count}
|
|
31
35
|
</span>
|
|
32
|
-
<
|
|
36
|
+
<Button
|
|
37
|
+
variant="outline"
|
|
38
|
+
size="icon"
|
|
33
39
|
onClick={() => setCount((c) => c + 1)}
|
|
34
|
-
|
|
40
|
+
aria-label="Increment"
|
|
35
41
|
>
|
|
36
42
|
+
|
|
37
|
-
</
|
|
43
|
+
</Button>
|
|
38
44
|
</div>
|
|
39
|
-
<p className="text-xs text-
|
|
45
|
+
<p className="text-xs text-muted-foreground">
|
|
40
46
|
Initial value comes from <code>?start=</code> — search params flow
|
|
41
47
|
through SSR. Try{" "}
|
|
42
48
|
<a
|
|
43
49
|
href="/counter?start=10"
|
|
44
|
-
className="text-
|
|
50
|
+
className="text-primary underline-offset-4 hover:underline"
|
|
45
51
|
>
|
|
46
52
|
/counter?start=10
|
|
47
53
|
</a>
|
|
@@ -1,3 +1,139 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
2
3
|
|
|
4
|
+
/* Tailwind v4 scans these globs for class names. `components/` is here so
|
|
5
|
+
shadcn/ui component classes are seen — add more @source lines if you put
|
|
6
|
+
markup elsewhere. */
|
|
3
7
|
@source "../app/**/*.{tsx,ts,jsx,js}";
|
|
8
|
+
@source "../components/**/*.{tsx,ts,jsx,js}";
|
|
9
|
+
|
|
10
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
11
|
+
|
|
12
|
+
/* shadcn/ui design tokens (new-york / zinc). Edit these to re-theme the
|
|
13
|
+
whole app; `npx shadcn@latest add <component>` drops new components that
|
|
14
|
+
consume the same variables. Toggle dark mode by putting `class="dark"`
|
|
15
|
+
on <html>. */
|
|
16
|
+
:root {
|
|
17
|
+
--radius: 0.625rem;
|
|
18
|
+
--background: oklch(1 0 0);
|
|
19
|
+
--foreground: oklch(0.141 0.005 285.823);
|
|
20
|
+
--card: oklch(1 0 0);
|
|
21
|
+
--card-foreground: oklch(0.141 0.005 285.823);
|
|
22
|
+
--popover: oklch(1 0 0);
|
|
23
|
+
--popover-foreground: oklch(0.141 0.005 285.823);
|
|
24
|
+
--primary: oklch(0.21 0.006 285.885);
|
|
25
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
26
|
+
--secondary: oklch(0.967 0.001 286.375);
|
|
27
|
+
--secondary-foreground: oklch(0.21 0.006 285.885);
|
|
28
|
+
--muted: oklch(0.967 0.001 286.375);
|
|
29
|
+
--muted-foreground: oklch(0.552 0.016 285.938);
|
|
30
|
+
--accent: oklch(0.967 0.001 286.375);
|
|
31
|
+
--accent-foreground: oklch(0.21 0.006 285.885);
|
|
32
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
33
|
+
--border: oklch(0.92 0.004 286.32);
|
|
34
|
+
--input: oklch(0.92 0.004 286.32);
|
|
35
|
+
--ring: oklch(0.705 0.015 286.067);
|
|
36
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
37
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
38
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
39
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
40
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
41
|
+
--sidebar: oklch(0.985 0 0);
|
|
42
|
+
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
|
43
|
+
--sidebar-primary: oklch(0.21 0.006 285.885);
|
|
44
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
45
|
+
--sidebar-accent: oklch(0.967 0.001 286.375);
|
|
46
|
+
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
|
47
|
+
--sidebar-border: oklch(0.92 0.004 286.32);
|
|
48
|
+
--sidebar-ring: oklch(0.705 0.015 286.067);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.dark {
|
|
52
|
+
--background: oklch(0.141 0.005 285.823);
|
|
53
|
+
--foreground: oklch(0.985 0 0);
|
|
54
|
+
--card: oklch(0.21 0.006 285.885);
|
|
55
|
+
--card-foreground: oklch(0.985 0 0);
|
|
56
|
+
--popover: oklch(0.21 0.006 285.885);
|
|
57
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
58
|
+
--primary: oklch(0.92 0.004 286.32);
|
|
59
|
+
--primary-foreground: oklch(0.21 0.006 285.885);
|
|
60
|
+
--secondary: oklch(0.274 0.006 286.033);
|
|
61
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
62
|
+
--muted: oklch(0.274 0.006 286.033);
|
|
63
|
+
--muted-foreground: oklch(0.705 0.015 286.067);
|
|
64
|
+
--accent: oklch(0.274 0.006 286.033);
|
|
65
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
66
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
67
|
+
--border: oklch(1 0 0 / 10%);
|
|
68
|
+
--input: oklch(1 0 0 / 15%);
|
|
69
|
+
--ring: oklch(0.552 0.016 285.938);
|
|
70
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
71
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
72
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
73
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
74
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
75
|
+
--sidebar: oklch(0.21 0.006 285.885);
|
|
76
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
77
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
78
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
79
|
+
--sidebar-accent: oklch(0.274 0.006 286.033);
|
|
80
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
81
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
82
|
+
--sidebar-ring: oklch(0.552 0.016 285.938);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@theme inline {
|
|
86
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
87
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
88
|
+
--radius-lg: var(--radius);
|
|
89
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
90
|
+
--color-background: var(--background);
|
|
91
|
+
--color-foreground: var(--foreground);
|
|
92
|
+
--color-card: var(--card);
|
|
93
|
+
--color-card-foreground: var(--card-foreground);
|
|
94
|
+
--color-popover: var(--popover);
|
|
95
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
96
|
+
--color-primary: var(--primary);
|
|
97
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
98
|
+
--color-secondary: var(--secondary);
|
|
99
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
100
|
+
--color-muted: var(--muted);
|
|
101
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
102
|
+
--color-accent: var(--accent);
|
|
103
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
104
|
+
--color-destructive: var(--destructive);
|
|
105
|
+
--color-border: var(--border);
|
|
106
|
+
--color-input: var(--input);
|
|
107
|
+
--color-ring: var(--ring);
|
|
108
|
+
--color-chart-1: var(--chart-1);
|
|
109
|
+
--color-chart-2: var(--chart-2);
|
|
110
|
+
--color-chart-3: var(--chart-3);
|
|
111
|
+
--color-chart-4: var(--chart-4);
|
|
112
|
+
--color-chart-5: var(--chart-5);
|
|
113
|
+
--color-sidebar: var(--sidebar);
|
|
114
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
115
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
116
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
117
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
118
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
119
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
120
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@layer base {
|
|
124
|
+
*,
|
|
125
|
+
::after,
|
|
126
|
+
::before,
|
|
127
|
+
::backdrop,
|
|
128
|
+
::file-selector-button {
|
|
129
|
+
border-color: var(--color-border, currentColor);
|
|
130
|
+
outline-color: var(--color-ring);
|
|
131
|
+
}
|
|
132
|
+
body {
|
|
133
|
+
background-color: var(--color-background);
|
|
134
|
+
color: var(--color-foreground);
|
|
135
|
+
}
|
|
136
|
+
button {
|
|
137
|
+
cursor: pointer;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -21,8 +21,11 @@ interface LayoutProps {
|
|
|
21
21
|
// SSR runtime on every render — server-side, before the HTML is sent.
|
|
22
22
|
export default function RootLayout({ children, url, auth }: LayoutProps) {
|
|
23
23
|
const signedIn = Boolean(auth?.user_id);
|
|
24
|
+
// Add `className="dark"` to this <html> to flip every shadcn token to its
|
|
25
|
+
// dark value. The classes below use semantic tokens (bg-background,
|
|
26
|
+
// text-foreground, …) so the whole UI re-themes from app/globals.css.
|
|
24
27
|
return (
|
|
25
|
-
<html lang="en"
|
|
28
|
+
<html lang="en">
|
|
26
29
|
<head>
|
|
27
30
|
<meta charSet="utf-8" />
|
|
28
31
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
@@ -31,24 +34,24 @@ export default function RootLayout({ children, url, auth }: LayoutProps) {
|
|
|
31
34
|
stylesheet link is injected here automatically — nothing to
|
|
32
35
|
wire up. */}
|
|
33
36
|
</head>
|
|
34
|
-
<body className="min-h-screen text-
|
|
35
|
-
<header className="sticky top-0 z-10 border-b
|
|
37
|
+
<body className="min-h-screen bg-background text-foreground antialiased">
|
|
38
|
+
<header className="sticky top-0 z-10 border-b bg-background/80 backdrop-blur">
|
|
36
39
|
<div className="mx-auto flex max-w-3xl items-center justify-between px-4 py-3">
|
|
37
40
|
<Link
|
|
38
41
|
href="/"
|
|
39
|
-
className="text-sm font-semibold tracking-tight hover:text-
|
|
42
|
+
className="text-sm font-semibold tracking-tight hover:text-muted-foreground"
|
|
40
43
|
>
|
|
41
44
|
__APP_NAME__
|
|
42
45
|
</Link>
|
|
43
|
-
<nav className="flex items-center gap-4 text-sm text-
|
|
44
|
-
<Link href="/" className="hover:text-
|
|
46
|
+
<nav className="flex items-center gap-4 text-sm text-muted-foreground">
|
|
47
|
+
<Link href="/" className="hover:text-foreground">
|
|
45
48
|
Home
|
|
46
49
|
</Link>
|
|
47
|
-
<Link href="/counter" className="hover:text-
|
|
50
|
+
<Link href="/counter" className="hover:text-foreground">
|
|
48
51
|
Counter
|
|
49
52
|
</Link>
|
|
50
53
|
<span
|
|
51
|
-
className={signedIn ? "text-emerald-600" : "text-
|
|
54
|
+
className={signedIn ? "text-emerald-600" : "text-muted-foreground/60"}
|
|
52
55
|
title={url}
|
|
53
56
|
>
|
|
54
57
|
{signedIn ? `· ${auth.user_id}` : "· anon"}
|
|
@@ -57,7 +60,7 @@ export default function RootLayout({ children, url, auth }: LayoutProps) {
|
|
|
57
60
|
</div>
|
|
58
61
|
</header>
|
|
59
62
|
<main className="mx-auto max-w-3xl px-4 py-10">{children}</main>
|
|
60
|
-
<footer className="border-t
|
|
63
|
+
<footer className="border-t py-6 text-center text-xs text-muted-foreground">
|
|
61
64
|
Rendered by Pylon · one server, one port
|
|
62
65
|
</footer>
|
|
63
66
|
</body>
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Link } from "@pylonsync/react";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
} from "@/components/ui/card";
|
|
3
11
|
|
|
4
12
|
interface PageProps {
|
|
5
13
|
url: string;
|
|
@@ -8,47 +16,69 @@ interface PageProps {
|
|
|
8
16
|
// `app/page.tsx` → `/`. Pages receive `{ url, auth, searchParams }` from
|
|
9
17
|
// the SSR runtime. This renders to HTML on the server; the per-route
|
|
10
18
|
// chunk hydrates it in the browser so interactive pages (see /counter)
|
|
11
|
-
// just work.
|
|
19
|
+
// just work. shadcn/ui is pre-wired — `Button`/`Card` resolve through the
|
|
20
|
+
// `@/` alias and add more with `npx shadcn@latest add <component>`.
|
|
12
21
|
export default function IndexPage({ url }: PageProps) {
|
|
13
22
|
return (
|
|
14
23
|
<div className="space-y-8">
|
|
15
24
|
<section>
|
|
16
25
|
<h1 className="text-3xl font-semibold tracking-tight">__APP_NAME__</h1>
|
|
17
|
-
<p className="mt-2 text-
|
|
26
|
+
<p className="mt-2 text-muted-foreground">
|
|
18
27
|
A full-stack Pylon app. Server-rendered React, file-based routes,
|
|
19
28
|
a synced database, and a typed client — served from one binary on
|
|
20
|
-
one port. No Next.js, no separate API server.
|
|
29
|
+
one port. No Next.js, no separate API server. Styled with Tailwind
|
|
30
|
+
v4 and shadcn/ui out of the box.
|
|
21
31
|
</p>
|
|
22
32
|
</section>
|
|
23
33
|
|
|
24
|
-
<
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
34
|
+
<Card>
|
|
35
|
+
<CardHeader>
|
|
36
|
+
<CardTitle>Next steps</CardTitle>
|
|
37
|
+
<CardDescription>
|
|
38
|
+
Everything below is already wired — just edit the files.
|
|
39
|
+
</CardDescription>
|
|
40
|
+
</CardHeader>
|
|
41
|
+
<CardContent>
|
|
42
|
+
<ul className="space-y-2 text-sm text-foreground/80">
|
|
43
|
+
<li>
|
|
44
|
+
Add a route: drop a file at{" "}
|
|
45
|
+
<code className="rounded bg-muted px-1">app/about/page.tsx</code>{" "}
|
|
46
|
+
and visit <code className="rounded bg-muted px-1">/about</code>.
|
|
47
|
+
</li>
|
|
48
|
+
<li>
|
|
49
|
+
Add data: edit{" "}
|
|
50
|
+
<code className="rounded bg-muted px-1">app.ts</code> — every{" "}
|
|
51
|
+
<code className="rounded bg-muted px-1">entity()</code> gets a
|
|
52
|
+
REST + realtime API and a typed client automatically.
|
|
53
|
+
</li>
|
|
54
|
+
<li>
|
|
55
|
+
Add a component:{" "}
|
|
56
|
+
<code className="rounded bg-muted px-1">
|
|
57
|
+
npx shadcn@latest add dialog
|
|
58
|
+
</code>{" "}
|
|
59
|
+
drops it into{" "}
|
|
60
|
+
<code className="rounded bg-muted px-1">components/ui</code>.
|
|
61
|
+
</li>
|
|
62
|
+
</ul>
|
|
63
|
+
</CardContent>
|
|
64
|
+
</Card>
|
|
65
|
+
|
|
66
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
67
|
+
<Button asChild>
|
|
68
|
+
<Link href="/counter">See hydration in action →</Link>
|
|
69
|
+
</Button>
|
|
70
|
+
<Button asChild variant="outline">
|
|
71
|
+
<a
|
|
72
|
+
href="https://docs.pylon.dev"
|
|
73
|
+
target="_blank"
|
|
74
|
+
rel="noreferrer noopener"
|
|
75
|
+
>
|
|
76
|
+
Read the docs
|
|
77
|
+
</a>
|
|
78
|
+
</Button>
|
|
79
|
+
</div>
|
|
50
80
|
|
|
51
|
-
<p className="text-xs text-
|
|
81
|
+
<p className="text-xs text-muted-foreground">
|
|
52
82
|
You're at <code>{url}</code>. Edit{" "}
|
|
53
83
|
<code>app/page.tsx</code> and save — the page reloads instantly.
|
|
54
84
|
</p>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
13
|
+
destructive:
|
|
14
|
+
"bg-destructive text-white hover:bg-destructive/90",
|
|
15
|
+
outline:
|
|
16
|
+
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
17
|
+
secondary:
|
|
18
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
19
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
20
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
default: "h-9 px-4 py-2",
|
|
24
|
+
sm: "h-8 rounded-md px-3 text-xs",
|
|
25
|
+
lg: "h-10 rounded-md px-8",
|
|
26
|
+
icon: "h-9 w-9",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: {
|
|
30
|
+
variant: "default",
|
|
31
|
+
size: "default",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export interface ButtonProps
|
|
37
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
38
|
+
VariantProps<typeof buttonVariants> {
|
|
39
|
+
asChild?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
43
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
44
|
+
const Comp = asChild ? Slot : "button";
|
|
45
|
+
return (
|
|
46
|
+
<Comp
|
|
47
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
48
|
+
ref={ref}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
Button.displayName = "Button";
|
|
55
|
+
|
|
56
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
data-slot="card"
|
|
9
|
+
className={cn(
|
|
10
|
+
"rounded-xl border bg-card text-card-foreground shadow-sm",
|
|
11
|
+
className,
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function CardHeader({
|
|
19
|
+
className,
|
|
20
|
+
...props
|
|
21
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
data-slot="card-header"
|
|
25
|
+
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function CardTitle({
|
|
32
|
+
className,
|
|
33
|
+
...props
|
|
34
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
data-slot="card-title"
|
|
38
|
+
className={cn("font-semibold leading-none tracking-tight", className)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function CardDescription({
|
|
45
|
+
className,
|
|
46
|
+
...props
|
|
47
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
data-slot="card-description"
|
|
51
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function CardContent({
|
|
58
|
+
className,
|
|
59
|
+
...props
|
|
60
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
data-slot="card-content"
|
|
64
|
+
className={cn("p-6 pt-0", className)}
|
|
65
|
+
{...props}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function CardFooter({
|
|
71
|
+
className,
|
|
72
|
+
...props
|
|
73
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
data-slot="card-footer"
|
|
77
|
+
className={cn("flex items-center p-6 pt-0", className)}
|
|
78
|
+
{...props}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export {
|
|
84
|
+
Card,
|
|
85
|
+
CardHeader,
|
|
86
|
+
CardFooter,
|
|
87
|
+
CardTitle,
|
|
88
|
+
CardDescription,
|
|
89
|
+
CardContent,
|
|
90
|
+
};
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ClassValue, clsx } from "clsx";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
|
|
4
|
+
// `cn` — the shadcn class merger. clsx resolves conditional/array class
|
|
5
|
+
// inputs; tailwind-merge then dedupes conflicting Tailwind utilities so
|
|
6
|
+
// the last one wins (e.g. `cn("px-2", "px-4")` → "px-4"). Every shadcn
|
|
7
|
+
// component routes its className through this.
|
|
8
|
+
export function cn(...inputs: ClassValue[]) {
|
|
9
|
+
return twMerge(clsx(inputs));
|
|
10
|
+
}
|
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
"react": "^19.0.0",
|
|
16
16
|
"react-dom": "^19.0.0",
|
|
17
17
|
"tailwindcss": "^4.3.0",
|
|
18
|
-
"@tailwindcss/cli": "^4.3.0"
|
|
18
|
+
"@tailwindcss/cli": "^4.3.0",
|
|
19
|
+
"tw-animate-css": "^1.2.0",
|
|
20
|
+
"class-variance-authority": "^0.7.1",
|
|
21
|
+
"clsx": "^2.1.1",
|
|
22
|
+
"tailwind-merge": "^2.5.0",
|
|
23
|
+
"lucide-react": "^0.460.0",
|
|
24
|
+
"@radix-ui/react-slot": "^1.1.0"
|
|
19
25
|
},
|
|
20
26
|
"devDependencies": {
|
|
21
27
|
"@pylonsync/cli": "^__PYLON_VERSION__",
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
"strict": true,
|
|
9
9
|
"skipLibCheck": true,
|
|
10
10
|
"lib": ["ES2022", "DOM"],
|
|
11
|
-
"types": ["react", "react-dom"]
|
|
11
|
+
"types": ["react", "react-dom"],
|
|
12
|
+
"baseUrl": ".",
|
|
13
|
+
"paths": {
|
|
14
|
+
"@/*": ["./*"]
|
|
15
|
+
}
|
|
12
16
|
},
|
|
13
|
-
"include": ["app.ts", "app/**/*"]
|
|
17
|
+
"include": ["app.ts", "app/**/*", "components/**/*", "lib/**/*"]
|
|
14
18
|
}
|