@clubedge/create-clubedge-app 0.2.0 → 0.3.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/CHANGELOG.md +16 -0
- package/README.md +50 -35
- package/bin/create.mjs +2 -466
- package/dist/cli/args.js +49 -0
- package/dist/cli/ui.js +85 -0
- package/dist/core/customize.js +56 -0
- package/dist/core/execute.js +60 -0
- package/dist/core/manifest.js +81 -0
- package/dist/core/plan.js +67 -0
- package/dist/core/source.js +107 -0
- package/dist/index.js +125 -0
- package/dist/package-info.js +8 -0
- package/dist/steps/git.js +11 -0
- package/dist/steps/install.js +19 -0
- package/dist/utils/fs.js +97 -0
- package/dist/utils/names.js +14 -0
- package/dist/utils/process.js +49 -0
- package/package.json +21 -5
- package/template/.dockerignore +8 -0
- package/template/.env.example +25 -0
- package/template/.gitattributes +6 -0
- package/template/.github/ISSUE_TEMPLATE/bug_report.yml +35 -0
- package/template/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/template/.github/ISSUE_TEMPLATE/feature_request.yml +24 -0
- package/template/.github/pull_request_template.md +20 -0
- package/template/.github/workflows/ci.yml +49 -0
- package/template/.prettierignore +1 -0
- package/template/.prettierrc.json +1 -0
- package/template/AGENTS.md +31 -0
- package/template/CODE_OF_CONDUCT.md +22 -0
- package/template/CONTRIBUTING.md +53 -0
- package/template/Dockerfile +32 -0
- package/template/LICENSE +201 -0
- package/template/NOTICE +6 -0
- package/template/PUBLISHING.md +27 -0
- package/template/README.md +157 -0
- package/template/SECURITY.md +15 -0
- package/template/SETUP.md +174 -0
- package/template/SUPPORT.md +8 -0
- package/template/_gitignore +13 -0
- package/template/apps/web/AGENTS.md +9 -0
- package/template/apps/web/components.json +21 -0
- package/template/apps/web/e2e/auth.spec.ts +37 -0
- package/template/apps/web/e2e/home.spec.ts +15 -0
- package/template/apps/web/next-env.d.ts +7 -0
- package/template/apps/web/next.config.ts +32 -0
- package/template/apps/web/package.json +33 -0
- package/template/apps/web/postcss.config.mjs +3 -0
- package/template/apps/web/public/.gitkeep +0 -0
- package/template/apps/web/public/.well-known/logo.svg +1 -0
- package/template/apps/web/src/app/actions/auth.ts +85 -0
- package/template/apps/web/src/app/api/health/route.ts +11 -0
- package/template/apps/web/src/app/auth/callback/route.ts +21 -0
- package/template/apps/web/src/app/dashboard/_components/account-button.tsx +34 -0
- package/template/apps/web/src/app/dashboard/_components/app-sidebar.tsx +91 -0
- package/template/apps/web/src/app/dashboard/page.tsx +352 -0
- package/template/apps/web/src/app/favicon.ico +0 -0
- package/template/apps/web/src/app/layout.tsx +36 -0
- package/template/apps/web/src/app/login/_components/auth-form-controls.tsx +88 -0
- package/template/apps/web/src/app/login/page.tsx +149 -0
- package/template/apps/web/src/app/page.tsx +229 -0
- package/template/apps/web/src/app/theme-provider.tsx +43 -0
- package/template/apps/web/src/app/theme-toggle.tsx +23 -0
- package/template/apps/web/src/components/branding/clubedge-mark.tsx +25 -0
- package/template/apps/web/src/components/landing/copy-command.tsx +48 -0
- package/template/apps/web/src/config/site.json +11 -0
- package/template/apps/web/src/config/site.ts +20 -0
- package/template/apps/web/src/env/client.ts +22 -0
- package/template/apps/web/src/env/server.test.ts +35 -0
- package/template/apps/web/src/env/server.ts +45 -0
- package/template/apps/web/src/lib/login-url.test.ts +16 -0
- package/template/apps/web/src/lib/login-url.ts +16 -0
- package/template/apps/web/src/lib/utils.ts +1 -0
- package/template/apps/web/src/proxy.ts +31 -0
- package/template/apps/web/src/server/auth.ts +69 -0
- package/template/apps/web/src/server/cache.ts +38 -0
- package/template/apps/web/src/server/db.ts +16 -0
- package/template/apps/web/src/server/storage.ts +39 -0
- package/template/apps/web/src/server/users.ts +17 -0
- package/template/apps/web/tsconfig.json +36 -0
- package/template/clubedge.template.json +13 -0
- package/template/docker-compose.yml +8 -0
- package/template/eslint.config.mjs +63 -0
- package/template/package.json +47 -0
- package/template/packages/auth/package.json +23 -0
- package/template/packages/auth/src/index.ts +9 -0
- package/template/packages/auth/src/supabase.test.ts +126 -0
- package/template/packages/auth/src/supabase.ts +65 -0
- package/template/packages/auth/src/types.ts +59 -0
- package/template/packages/auth/tsconfig.json +4 -0
- package/template/packages/cache/package.json +20 -0
- package/template/packages/cache/src/index.test.ts +67 -0
- package/template/packages/cache/src/index.ts +19 -0
- package/template/packages/cache/src/rate-limit.test.ts +52 -0
- package/template/packages/cache/src/rate-limit.ts +74 -0
- package/template/packages/cache/src/redis.ts +106 -0
- package/template/packages/cache/tsconfig.json +4 -0
- package/template/packages/core/package.json +17 -0
- package/template/packages/core/src/client-ip.test.ts +19 -0
- package/template/packages/core/src/client-ip.ts +11 -0
- package/template/packages/core/src/errors.test.ts +20 -0
- package/template/packages/core/src/errors.ts +20 -0
- package/template/packages/core/src/index.ts +4 -0
- package/template/packages/core/src/redirect.test.ts +27 -0
- package/template/packages/core/src/redirect.ts +20 -0
- package/template/packages/core/src/result.ts +10 -0
- package/template/packages/core/tsconfig.json +4 -0
- package/template/packages/db/drizzle/0000_initial.sql +10 -0
- package/template/packages/db/drizzle/meta/_journal.json +13 -0
- package/template/packages/db/drizzle.config.ts +9 -0
- package/template/packages/db/package.json +30 -0
- package/template/packages/db/scripts/load-env.ts +14 -0
- package/template/packages/db/scripts/migrate.ts +11 -0
- package/template/packages/db/scripts/seed.ts +14 -0
- package/template/packages/db/src/client.ts +22 -0
- package/template/packages/db/src/index.ts +4 -0
- package/template/packages/db/src/schema/index.ts +1 -0
- package/template/packages/db/src/schema/users.ts +14 -0
- package/template/packages/db/src/users.ts +16 -0
- package/template/packages/db/tsconfig.json +4 -0
- package/template/packages/storage/package.json +24 -0
- package/template/packages/storage/src/index.ts +1 -0
- package/template/packages/storage/src/s3.test.ts +86 -0
- package/template/packages/storage/src/s3.ts +86 -0
- package/template/packages/storage/src/supabase.ts +69 -0
- package/template/packages/storage/src/types.ts +18 -0
- package/template/packages/storage/tsconfig.json +4 -0
- package/template/packages/ui/components.json +21 -0
- package/template/packages/ui/package.json +32 -0
- package/template/packages/ui/src/components/badge.tsx +48 -0
- package/template/packages/ui/src/components/breadcrumb.tsx +102 -0
- package/template/packages/ui/src/components/button.tsx +57 -0
- package/template/packages/ui/src/components/card.tsx +87 -0
- package/template/packages/ui/src/components/input.tsx +19 -0
- package/template/packages/ui/src/components/label.tsx +19 -0
- package/template/packages/ui/src/components/separator.tsx +20 -0
- package/template/packages/ui/src/components/sheet.tsx +125 -0
- package/template/packages/ui/src/components/sidebar.tsx +692 -0
- package/template/packages/ui/src/components/skeleton.tsx +13 -0
- package/template/packages/ui/src/components/switch.tsx +31 -0
- package/template/packages/ui/src/components/table.tsx +88 -0
- package/template/packages/ui/src/components/tooltip.tsx +53 -0
- package/template/packages/ui/src/hooks/use-mobile.ts +19 -0
- package/template/packages/ui/src/lib/utils.ts +1 -0
- package/template/packages/ui/src/styles/globals.css +126 -0
- package/template/packages/ui/tsconfig.json +19 -0
- package/template/playwright.config.ts +19 -0
- package/template/pnpm-lock.yaml +3624 -0
- package/template/pnpm-workspace.yaml +3 -0
- package/template/scripts/start-standalone.mjs +37 -0
- package/template/tsconfig.base.json +16 -0
- package/template/turbo.json +17 -0
- package/template/vitest.config.ts +8 -0
- package/template.lock.json +142 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const send = vi.fn();
|
|
4
|
+
|
|
5
|
+
vi.mock("@aws-sdk/client-s3", () => {
|
|
6
|
+
class Command {
|
|
7
|
+
constructor(public readonly input: Record<string, unknown>) {}
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
S3Client: vi.fn(function () {
|
|
11
|
+
return { send };
|
|
12
|
+
}),
|
|
13
|
+
PutObjectCommand: class extends Command {},
|
|
14
|
+
GetObjectCommand: class extends Command {},
|
|
15
|
+
DeleteObjectCommand: class extends Command {},
|
|
16
|
+
HeadObjectCommand: class extends Command {},
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
vi.mock("@aws-sdk/s3-request-presigner", () => ({
|
|
20
|
+
getSignedUrl: vi.fn(async () => "https://signed.example/object"),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
const { S3Client } = await import("@aws-sdk/client-s3");
|
|
24
|
+
const { createS3Storage } = await import("./s3");
|
|
25
|
+
|
|
26
|
+
describe("createS3Storage", () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("does not create a client until the first operation", async () => {
|
|
32
|
+
const storage = createS3Storage({ bucket: "files", region: "auto" });
|
|
33
|
+
expect(S3Client).not.toHaveBeenCalled();
|
|
34
|
+
send.mockResolvedValueOnce({});
|
|
35
|
+
await storage.delete("a.txt");
|
|
36
|
+
await storage.delete("b.txt");
|
|
37
|
+
expect(S3Client).toHaveBeenCalledTimes(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("uploads to the configured bucket and builds the public URL", async () => {
|
|
41
|
+
const storage = createS3Storage({
|
|
42
|
+
bucket: "files",
|
|
43
|
+
region: "auto",
|
|
44
|
+
publicUrl: "https://cdn.example/",
|
|
45
|
+
});
|
|
46
|
+
send.mockResolvedValueOnce({});
|
|
47
|
+
|
|
48
|
+
const result = await storage.upload({
|
|
49
|
+
key: "avatars/1.png",
|
|
50
|
+
body: new Uint8Array([1, 2, 3]),
|
|
51
|
+
contentType: "image/png",
|
|
52
|
+
contentLength: 3,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(send.mock.calls[0][0].input).toMatchObject({
|
|
56
|
+
Bucket: "files",
|
|
57
|
+
Key: "avatars/1.png",
|
|
58
|
+
ContentType: "image/png",
|
|
59
|
+
ContentLength: 3,
|
|
60
|
+
});
|
|
61
|
+
expect(result).toEqual({ key: "avatars/1.png", url: "https://cdn.example/avatars/1.png" });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("fails clearly when the bucket is not configured", async () => {
|
|
65
|
+
const storage = createS3Storage({ region: "auto" });
|
|
66
|
+
await expect(storage.exists("a.txt")).rejects.toThrow("STORAGE_BUCKET must be configured");
|
|
67
|
+
expect(send).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("treats a missing object as not existing and rethrows other errors", async () => {
|
|
71
|
+
const storage = createS3Storage({ bucket: "files", region: "auto" });
|
|
72
|
+
send.mockRejectedValueOnce(Object.assign(new Error("missing"), { name: "NotFound" }));
|
|
73
|
+
expect(await storage.exists("a.txt")).toBe(false);
|
|
74
|
+
|
|
75
|
+
send.mockRejectedValueOnce(Object.assign(new Error("denied"), { name: "AccessDenied" }));
|
|
76
|
+
await expect(storage.exists("a.txt")).rejects.toThrow("denied");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("signs download URLs with a default five-minute expiry", async () => {
|
|
80
|
+
const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
|
|
81
|
+
const storage = createS3Storage({ bucket: "files", region: "auto" });
|
|
82
|
+
|
|
83
|
+
expect(await storage.getSignedUrl("a.txt")).toBe("https://signed.example/object");
|
|
84
|
+
expect(vi.mocked(getSignedUrl).mock.calls[0][2]).toEqual({ expiresIn: 300 });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DeleteObjectCommand,
|
|
3
|
+
GetObjectCommand,
|
|
4
|
+
HeadObjectCommand,
|
|
5
|
+
PutObjectCommand,
|
|
6
|
+
S3Client,
|
|
7
|
+
} from "@aws-sdk/client-s3";
|
|
8
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
9
|
+
import type { StorageProvider, UploadInput } from "./types";
|
|
10
|
+
|
|
11
|
+
export interface S3StorageConfig {
|
|
12
|
+
/** Optional so the app can start without storage; operations fail until it is set. */
|
|
13
|
+
bucket?: string;
|
|
14
|
+
region: string;
|
|
15
|
+
endpoint?: string;
|
|
16
|
+
accessKeyId?: string;
|
|
17
|
+
secretAccessKey?: string;
|
|
18
|
+
/** Public base URL for uploaded objects, such as a CDN or R2 public bucket. */
|
|
19
|
+
publicUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function bodyForS3(body: UploadInput["body"]) {
|
|
23
|
+
if (body instanceof Blob) return body.stream();
|
|
24
|
+
if (body instanceof ArrayBuffer) return new Uint8Array(body);
|
|
25
|
+
return body;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Works with AWS S3 and S3-compatible services such as Cloudflare R2 and MinIO. */
|
|
29
|
+
export function createS3Storage(config: S3StorageConfig): StorageProvider {
|
|
30
|
+
let s3Client: S3Client | undefined;
|
|
31
|
+
|
|
32
|
+
function client() {
|
|
33
|
+
s3Client ??= new S3Client({
|
|
34
|
+
region: config.region,
|
|
35
|
+
...(config.endpoint ? { endpoint: config.endpoint } : {}),
|
|
36
|
+
credentials:
|
|
37
|
+
config.accessKeyId && config.secretAccessKey
|
|
38
|
+
? { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey }
|
|
39
|
+
: undefined,
|
|
40
|
+
});
|
|
41
|
+
return s3Client;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function bucket() {
|
|
45
|
+
if (!config.bucket) throw new Error("STORAGE_BUCKET must be configured for S3 storage.");
|
|
46
|
+
return config.bucket;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
async upload({ key, body, contentType, contentLength }) {
|
|
51
|
+
await client().send(
|
|
52
|
+
new PutObjectCommand({
|
|
53
|
+
Bucket: bucket(),
|
|
54
|
+
Key: key,
|
|
55
|
+
Body: bodyForS3(body),
|
|
56
|
+
ContentType: contentType,
|
|
57
|
+
...(contentLength ? { ContentLength: contentLength } : {}),
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
const url = config.publicUrl ? `${config.publicUrl.replace(/\/$/, "")}/${key}` : undefined;
|
|
61
|
+
return { key, url };
|
|
62
|
+
},
|
|
63
|
+
async download(key) {
|
|
64
|
+
const result = await client().send(new GetObjectCommand({ Bucket: bucket(), Key: key }));
|
|
65
|
+
if (!result.Body) throw new Error(`Storage object not found: ${key}`);
|
|
66
|
+
return result.Body.transformToWebStream() as ReadableStream<Uint8Array>;
|
|
67
|
+
},
|
|
68
|
+
async delete(key) {
|
|
69
|
+
await client().send(new DeleteObjectCommand({ Bucket: bucket(), Key: key }));
|
|
70
|
+
},
|
|
71
|
+
async exists(key) {
|
|
72
|
+
try {
|
|
73
|
+
await client().send(new HeadObjectCommand({ Bucket: bucket(), Key: key }));
|
|
74
|
+
return true;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error && typeof error === "object" && "name" in error && error.name === "NotFound")
|
|
77
|
+
return false;
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
async getSignedUrl(key, options) {
|
|
82
|
+
const command = new GetObjectCommand({ Bucket: bucket(), Key: key });
|
|
83
|
+
return getSignedUrl(client(), command, { expiresIn: options?.expiresInSeconds ?? 300 });
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
import type { StorageProvider, UploadInput } from "./types";
|
|
3
|
+
|
|
4
|
+
export interface SupabaseStorageConfig {
|
|
5
|
+
/** Optional so the app can start without storage; operations fail until it is set. */
|
|
6
|
+
bucket?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Returns a Supabase client for the current request. Passing the user's session client
|
|
9
|
+
* keeps Storage row-level security policies in effect.
|
|
10
|
+
*/
|
|
11
|
+
getClient: () => Promise<Pick<SupabaseClient, "storage">> | Pick<SupabaseClient, "storage">;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toBlob(body: UploadInput["body"], type: string) {
|
|
15
|
+
if (body instanceof Blob) return body;
|
|
16
|
+
if (body instanceof ArrayBuffer) return new Blob([body], { type });
|
|
17
|
+
if (body instanceof Uint8Array) return new Blob([body.slice().buffer], { type });
|
|
18
|
+
throw new Error("Supabase Storage uploads require a Blob or byte array.");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createSupabaseStorage({
|
|
22
|
+
bucket,
|
|
23
|
+
getClient,
|
|
24
|
+
}: SupabaseStorageConfig): StorageProvider {
|
|
25
|
+
function bucketName() {
|
|
26
|
+
if (!bucket) throw new Error("SUPABASE_STORAGE_BUCKET must be configured.");
|
|
27
|
+
return bucket;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
async upload({ key, body, contentType }) {
|
|
32
|
+
const supabase = await getClient();
|
|
33
|
+
const { error } = await supabase.storage
|
|
34
|
+
.from(bucketName())
|
|
35
|
+
.upload(key, toBlob(body, contentType), { contentType, upsert: false });
|
|
36
|
+
if (error) throw error;
|
|
37
|
+
return { key };
|
|
38
|
+
},
|
|
39
|
+
async download(key) {
|
|
40
|
+
const supabase = await getClient();
|
|
41
|
+
const { data, error } = await supabase.storage.from(bucketName()).download(key);
|
|
42
|
+
if (error) throw error;
|
|
43
|
+
return data.stream() as ReadableStream<Uint8Array>;
|
|
44
|
+
},
|
|
45
|
+
async delete(key) {
|
|
46
|
+
const supabase = await getClient();
|
|
47
|
+
const { error } = await supabase.storage.from(bucketName()).remove([key]);
|
|
48
|
+
if (error) throw error;
|
|
49
|
+
},
|
|
50
|
+
async exists(key) {
|
|
51
|
+
const supabase = await getClient();
|
|
52
|
+
const pieces = key.split("/");
|
|
53
|
+
const name = pieces.pop();
|
|
54
|
+
const { data, error } = await supabase.storage
|
|
55
|
+
.from(bucketName())
|
|
56
|
+
.list(pieces.join("/"), { search: name });
|
|
57
|
+
if (error) throw error;
|
|
58
|
+
return data.some((file) => file.name === name);
|
|
59
|
+
},
|
|
60
|
+
async getSignedUrl(key, options) {
|
|
61
|
+
const supabase = await getClient();
|
|
62
|
+
const { data, error } = await supabase.storage
|
|
63
|
+
.from(bucketName())
|
|
64
|
+
.createSignedUrl(key, options?.expiresInSeconds ?? 300);
|
|
65
|
+
if (error) throw error;
|
|
66
|
+
return data.signedUrl;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface UploadInput {
|
|
2
|
+
key: string;
|
|
3
|
+
body: ReadableStream<Uint8Array> | Blob | ArrayBuffer | Uint8Array;
|
|
4
|
+
contentType: string;
|
|
5
|
+
contentLength?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SignedUrlOptions {
|
|
9
|
+
expiresInSeconds?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface StorageProvider {
|
|
13
|
+
upload(input: UploadInput): Promise<{ key: string; url?: string }>;
|
|
14
|
+
download(key: string): Promise<ReadableStream<Uint8Array>>;
|
|
15
|
+
delete(key: string): Promise<void>;
|
|
16
|
+
exists(key: string): Promise<boolean>;
|
|
17
|
+
getSignedUrl(key: string, options?: SignedUrlOptions): Promise<string>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "base-nova",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/styles/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true
|
|
11
|
+
},
|
|
12
|
+
"aliases": {
|
|
13
|
+
"components": "@clubedge/ui/components",
|
|
14
|
+
"utils": "@clubedge/ui/lib/utils",
|
|
15
|
+
"ui": "@clubedge/ui/components",
|
|
16
|
+
"lib": "@clubedge/ui/lib",
|
|
17
|
+
"hooks": "@clubedge/ui/hooks"
|
|
18
|
+
},
|
|
19
|
+
"iconLibrary": "lucide",
|
|
20
|
+
"rtl": true
|
|
21
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clubedge/ui",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./components/*": "./src/components/*.tsx",
|
|
9
|
+
"./hooks/*": "./src/hooks/*.ts",
|
|
10
|
+
"./lib/*": "./src/lib/*.ts",
|
|
11
|
+
"./globals.css": "./src/styles/globals.css"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@base-ui/react": "^1.8.0",
|
|
15
|
+
"class-variance-authority": "^0.7.1",
|
|
16
|
+
"cn": "^0.4.0",
|
|
17
|
+
"lucide-react": "^0.468.0",
|
|
18
|
+
"tw-animate-css": "^1.4.0"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"react": "^19.0.0",
|
|
22
|
+
"tailwindcss": "^4.1.13"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/react": "^19.1.13",
|
|
26
|
+
"@types/react-dom": "^19.1.9",
|
|
27
|
+
"typescript": "^5.9.2"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"typecheck": "tsc --noEmit"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { cn } from "cn";
|
|
5
|
+
|
|
6
|
+
const badgeVariants = cva(
|
|
7
|
+
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
12
|
+
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
13
|
+
destructive:
|
|
14
|
+
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
|
15
|
+
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
|
16
|
+
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
|
17
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: {
|
|
21
|
+
variant: "default",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
function Badge({
|
|
27
|
+
className,
|
|
28
|
+
variant = "default",
|
|
29
|
+
render,
|
|
30
|
+
...props
|
|
31
|
+
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
|
32
|
+
return useRender({
|
|
33
|
+
defaultTagName: "span",
|
|
34
|
+
props: mergeProps<"span">(
|
|
35
|
+
{
|
|
36
|
+
className: cn(badgeVariants({ variant }), className),
|
|
37
|
+
},
|
|
38
|
+
props,
|
|
39
|
+
),
|
|
40
|
+
render,
|
|
41
|
+
state: {
|
|
42
|
+
slot: "badge",
|
|
43
|
+
variant,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
3
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
4
|
+
import { cn } from "cn";
|
|
5
|
+
import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) {
|
|
8
|
+
return (
|
|
9
|
+
<nav aria-label="breadcrumb" data-slot="breadcrumb" className={cn(className)} {...props} />
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
|
14
|
+
return (
|
|
15
|
+
<ol
|
|
16
|
+
data-slot="breadcrumb-list"
|
|
17
|
+
className={cn(
|
|
18
|
+
"flex flex-wrap items-center gap-1.5 text-sm wrap-break-word text-muted-foreground",
|
|
19
|
+
className,
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
|
27
|
+
return (
|
|
28
|
+
<li
|
|
29
|
+
data-slot="breadcrumb-item"
|
|
30
|
+
className={cn("inline-flex items-center gap-1", className)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function BreadcrumbLink({ className, render, ...props }: useRender.ComponentProps<"a">) {
|
|
37
|
+
return useRender({
|
|
38
|
+
defaultTagName: "a",
|
|
39
|
+
props: mergeProps<"a">(
|
|
40
|
+
{
|
|
41
|
+
className: cn("transition-colors hover:text-foreground", className),
|
|
42
|
+
},
|
|
43
|
+
props,
|
|
44
|
+
),
|
|
45
|
+
render,
|
|
46
|
+
state: {
|
|
47
|
+
slot: "breadcrumb-link",
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
|
53
|
+
return (
|
|
54
|
+
<span
|
|
55
|
+
data-slot="breadcrumb-page"
|
|
56
|
+
role="link"
|
|
57
|
+
aria-disabled="true"
|
|
58
|
+
aria-current="page"
|
|
59
|
+
className={cn("font-normal text-foreground", className)}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<"li">) {
|
|
66
|
+
return (
|
|
67
|
+
<li
|
|
68
|
+
data-slot="breadcrumb-separator"
|
|
69
|
+
role="presentation"
|
|
70
|
+
aria-hidden="true"
|
|
71
|
+
className={cn("[&>svg]:size-3.5", className)}
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
{children ?? <ChevronRightIcon className="rtl:rotate-180" />}
|
|
75
|
+
</li>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<"span">) {
|
|
80
|
+
return (
|
|
81
|
+
<span
|
|
82
|
+
data-slot="breadcrumb-ellipsis"
|
|
83
|
+
role="presentation"
|
|
84
|
+
aria-hidden="true"
|
|
85
|
+
className={cn("flex size-5 items-center justify-center [&>svg]:size-4", className)}
|
|
86
|
+
{...props}
|
|
87
|
+
>
|
|
88
|
+
<MoreHorizontalIcon />
|
|
89
|
+
<span className="sr-only">More</span>
|
|
90
|
+
</span>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
Breadcrumb,
|
|
96
|
+
BreadcrumbList,
|
|
97
|
+
BreadcrumbItem,
|
|
98
|
+
BreadcrumbLink,
|
|
99
|
+
BreadcrumbPage,
|
|
100
|
+
BreadcrumbSeparator,
|
|
101
|
+
BreadcrumbEllipsis,
|
|
102
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "cn";
|
|
4
|
+
|
|
5
|
+
const buttonVariants = cva(
|
|
6
|
+
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
|
11
|
+
outline:
|
|
12
|
+
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
|
13
|
+
secondary:
|
|
14
|
+
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
15
|
+
ghost:
|
|
16
|
+
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
|
17
|
+
destructive:
|
|
18
|
+
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
|
19
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
20
|
+
},
|
|
21
|
+
size: {
|
|
22
|
+
default:
|
|
23
|
+
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
|
|
24
|
+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
25
|
+
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
26
|
+
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
|
|
27
|
+
icon: "size-8",
|
|
28
|
+
"icon-xs":
|
|
29
|
+
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
30
|
+
"icon-sm":
|
|
31
|
+
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
|
32
|
+
"icon-lg": "size-9",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
defaultVariants: {
|
|
36
|
+
variant: "default",
|
|
37
|
+
size: "default",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
function Button({
|
|
43
|
+
className,
|
|
44
|
+
variant = "default",
|
|
45
|
+
size = "default",
|
|
46
|
+
...props
|
|
47
|
+
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
|
48
|
+
return (
|
|
49
|
+
<ButtonPrimitive
|
|
50
|
+
data-slot="button"
|
|
51
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "cn";
|
|
3
|
+
|
|
4
|
+
function Card({
|
|
5
|
+
className,
|
|
6
|
+
size = "default",
|
|
7
|
+
...props
|
|
8
|
+
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
data-slot="card"
|
|
12
|
+
data-size={size}
|
|
13
|
+
className={cn(
|
|
14
|
+
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
|
15
|
+
className,
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
data-slot="card-header"
|
|
26
|
+
className={cn(
|
|
27
|
+
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
|
|
28
|
+
className,
|
|
29
|
+
)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
data-slot="card-title"
|
|
39
|
+
className={cn(
|
|
40
|
+
"text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
|
|
41
|
+
className,
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
data-slot="card-description"
|
|
52
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
data-slot="card-action"
|
|
62
|
+
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
69
|
+
return (
|
|
70
|
+
<div data-slot="card-content" className={cn("px-(--card-spacing)", className)} {...props} />
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
data-slot="card-footer"
|
|
78
|
+
className={cn(
|
|
79
|
+
"flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
|
|
80
|
+
className,
|
|
81
|
+
)}
|
|
82
|
+
{...props}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Input as InputPrimitive } from "@base-ui/react/input";
|
|
3
|
+
import { cn } from "cn";
|
|
4
|
+
|
|
5
|
+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
6
|
+
return (
|
|
7
|
+
<InputPrimitive
|
|
8
|
+
type={type}
|
|
9
|
+
data-slot="input"
|
|
10
|
+
className={cn(
|
|
11
|
+
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
|
12
|
+
className,
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { Input };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "cn";
|
|
5
|
+
|
|
6
|
+
function Label({ className, ...props }: React.ComponentProps<"label">) {
|
|
7
|
+
return (
|
|
8
|
+
<label
|
|
9
|
+
data-slot="label"
|
|
10
|
+
className={cn(
|
|
11
|
+
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
12
|
+
className,
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { Label };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
|
|
4
|
+
import { cn } from "cn";
|
|
5
|
+
|
|
6
|
+
function Separator({ className, orientation = "horizontal", ...props }: SeparatorPrimitive.Props) {
|
|
7
|
+
return (
|
|
8
|
+
<SeparatorPrimitive
|
|
9
|
+
data-slot="separator"
|
|
10
|
+
orientation={orientation}
|
|
11
|
+
className={cn(
|
|
12
|
+
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
|
13
|
+
className,
|
|
14
|
+
)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { Separator };
|