create-svadmin 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/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # create-svadmin
2
+
3
+ The official scaffolding CLI for `headless-admin-svelte` (svadmin).
4
+
5
+ Quickly bootstrap a completely configured, headless admin panel project built on Svelte 5, Shadcn Svelte, and TanStack Query.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ npx create-svadmin@latest my-admin-app
11
+ # or
12
+ bunx create-svadmin@latest my-admin-app
13
+ ```
14
+
15
+ Follow the interactive prompts to:
16
+ 1. Name your project.
17
+ 2. Choose a default Data Provider (Simple REST, Supabase, GraphQL, or Custom).
18
+
19
+ ## What's Included
20
+
21
+ The generated project is pre-configured with:
22
+ - **SvelteKit** + **Vite** (Svelte 5 Runes)
23
+ - **Tailwind CSS** + **Shadcn Svelte** UI components
24
+ - **@svadmin/core**: The headless business logic and hooks (useTable, useForm, useAuth, etc.)
25
+ - **@svadmin/ui**: Beautiful default dashboard UI, standalone CRUD buttons, and data tables.
26
+ - Pre-wired **TanStack Query** for client-state management.
27
+
28
+ ## Start Developing
29
+
30
+ Once scaffolded, `cd` into your directory, install dependencies, and start the development server:
31
+
32
+ ```bash
33
+ cd my-admin-app
34
+ bun install
35
+ bun run dev
36
+ ```
package/index.js ADDED
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import prompts from 'prompts';
7
+ import pc from 'picocolors';
8
+ import { execSync } from 'child_process';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ async function init() {
14
+ console.log(`\n${pc.cyan('Welcome to create-svadmin!')}\n`);
15
+
16
+ const response = await prompts([
17
+ {
18
+ type: 'text',
19
+ name: 'projectName',
20
+ message: 'Project name:',
21
+ initial: 'svadmin-app',
22
+ validate: (value) => {
23
+ if (!value.trim()) return 'Project name is required';
24
+ if (fs.existsSync(value.trim()) && fs.readdirSync(value.trim()).length > 0) {
25
+ return 'Directory already exists and is not empty';
26
+ }
27
+ return true;
28
+ }
29
+ },
30
+ {
31
+ type: 'select',
32
+ name: 'dataProvider',
33
+ message: 'Choose a default Data Provider:',
34
+ choices: [
35
+ { title: 'Simple REST', value: 'simple-rest', description: 'Standard JSON APIs / JSON Server' },
36
+ { title: 'Supabase', value: 'supabase', description: 'PostgreSQL Backend-as-a-Service' },
37
+ { title: 'GraphQL', value: 'graphql', description: 'Generic GraphQL endpoints' },
38
+ { title: 'None (Build Your Own)', value: 'none', description: 'Implement the DataProvider interface yourself' }
39
+ ],
40
+ initial: 0
41
+ }
42
+ ]);
43
+
44
+ if (!response.projectName) {
45
+ console.log(pc.red('Operation cancelled.'));
46
+ return;
47
+ }
48
+
49
+ const projectDir = path.resolve(process.cwd(), response.projectName.trim());
50
+
51
+ if (!fs.existsSync(projectDir)) {
52
+ fs.mkdirSync(projectDir, { recursive: true });
53
+ }
54
+
55
+ console.log(`\nScaffolding project in ${pc.green(projectDir)}...`);
56
+
57
+ // Basic template files
58
+ const templateDir = path.join(__dirname, 'template');
59
+
60
+ // We will create the template files dynamically here to avoid needing a complex nested template repo directory.
61
+ // In a real scenario, we'd copy `templateDir` contents recursively.
62
+ const packageJson = {
63
+ name: response.projectName,
64
+ version: "0.1.0",
65
+ private: true,
66
+ type: "module",
67
+ scripts: {
68
+ "dev": "vite dev",
69
+ "build": "vite build",
70
+ "preview": "vite preview",
71
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
72
+ },
73
+ dependencies: {
74
+ "@svadmin/core": "latest",
75
+ "@svadmin/ui": "latest",
76
+ "lucide-svelte": "^0.475.0",
77
+ },
78
+ devDependencies: {
79
+ "@sveltejs/adapter-auto": "^4.0.0",
80
+ "@sveltejs/kit": "^2.17.2",
81
+ "@sveltejs/vite-plugin-svelte": "^5.0.0",
82
+ "svelte": "^5.20.0",
83
+ "tailwindcss": "^3.4.17",
84
+ "vite": "^6.1.0"
85
+ }
86
+ };
87
+
88
+ if (response.dataProvider === 'simple-rest') {
89
+ packageJson.dependencies["@svadmin/simple-rest"] = "latest";
90
+ } else if (response.dataProvider === 'supabase') {
91
+ packageJson.dependencies["@svadmin/supabase"] = "latest";
92
+ packageJson.dependencies["@supabase/supabase-js"] = "^2.0.0";
93
+ } else if (response.dataProvider === 'graphql') {
94
+ packageJson.dependencies["@svadmin/graphql"] = "latest";
95
+ packageJson.dependencies["graphql-request"] = "^7.1.0";
96
+ packageJson.dependencies["graphql"] = "^16.8.0";
97
+ }
98
+
99
+ fs.writeFileSync(
100
+ path.join(projectDir, 'package.json'),
101
+ JSON.stringify(packageJson, null, 2)
102
+ );
103
+
104
+ // Copy template files
105
+ function copyDir(src, dest) {
106
+ fs.mkdirSync(dest, { recursive: true });
107
+ let entries = fs.readdirSync(src, { withFileTypes: true });
108
+
109
+ for (let entry of entries) {
110
+ let srcPath = path.join(src, entry.name);
111
+ let destPath = path.join(dest, entry.name === '_gitignore' ? '.gitignore' : entry.name);
112
+
113
+ if (entry.isDirectory()) {
114
+ copyDir(srcPath, destPath);
115
+ } else {
116
+ fs.copyFileSync(srcPath, destPath);
117
+ }
118
+ }
119
+ }
120
+
121
+ if (fs.existsSync(templateDir)) {
122
+ copyDir(templateDir, projectDir);
123
+ console.log(pc.green('✔') + ' Copied template files');
124
+ }
125
+
126
+ // Next steps instructions
127
+ console.log(`\nNext steps:`);
128
+ console.log(` 1. ${pc.cyan(`cd ${response.projectName}`)}`);
129
+ console.log(` 2. ${pc.cyan('npm install')} or ${pc.cyan('bun install')}`);
130
+ console.log(` 3. ${pc.cyan('npm run dev')}`);
131
+ console.log(`\nFor documentation, visit: ${pc.blue('https://github.com/zuohuadong/svadmin')}\n`);
132
+ }
133
+
134
+ init().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "create-svadmin",
3
+ "version": "0.1.0",
4
+ "description": "Scaffolding tool for svadmin projects",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-svadmin": "./index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node index.js"
11
+ },
12
+ "dependencies": {
13
+ "prompts": "^2.4.2",
14
+ "picocolors": "^1.0.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^20.0.0",
18
+ "@types/prompts": "^2.4.9"
19
+ },
20
+ "keywords": [
21
+ "svadmin",
22
+ "create",
23
+ "scaffold",
24
+ "cli"
25
+ ],
26
+ "author": "zuohuadong",
27
+ "license": "MIT"
28
+ }
@@ -0,0 +1,47 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ const sourceDir = path.resolve(__dirname, '../../example');
9
+ const targetDir = path.resolve(__dirname, 'template');
10
+
11
+ function copyRecursiveSync(src, dest) {
12
+ const exists = fs.existsSync(src);
13
+ const stats = exists && fs.statSync(src);
14
+ const isDirectory = exists && stats.isDirectory();
15
+
16
+ if (isDirectory) {
17
+ if (path.basename(src) === 'node_modules' || path.basename(src) === '.svelte-kit' || path.basename(src) === 'dist') return;
18
+
19
+ fs.mkdirSync(dest, { recursive: true });
20
+ fs.readdirSync(src).forEach((childItemName) => {
21
+ copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName));
22
+ });
23
+ } else {
24
+ // Skip package.json and lockfiles as they are generated by the CLI
25
+ if (['package.json', 'bun.lockb', 'pnpm-lock.yaml', 'package-lock.json'].includes(path.basename(src))) return;
26
+ fs.copyFileSync(src, dest);
27
+ }
28
+ }
29
+
30
+ // Clean and recreate template
31
+ if (fs.existsSync(targetDir)) {
32
+ fs.rmSync(targetDir, { recursive: true, force: true });
33
+ }
34
+ fs.mkdirSync(targetDir, { recursive: true });
35
+
36
+ // Copy example source
37
+ copyRecursiveSync(sourceDir, targetDir);
38
+
39
+ // Re-write package.json in the template is not needed as the CLI generates it!
40
+ // BUT we should rename .gitignore to _gitignore because npm publish ignores .gitignore
41
+ const gitignoreSrc = path.join(targetDir, '.gitignore');
42
+ const gitignoreDest = path.join(targetDir, '_gitignore');
43
+ if (fs.existsSync(gitignoreSrc)) {
44
+ fs.renameSync(gitignoreSrc, gitignoreDest);
45
+ }
46
+
47
+ console.log('Template synced successfully from /example!');
@@ -0,0 +1,3 @@
1
+ # For Supabase example (optional)
2
+ VITE_SUPABASE_URL=https://your-project.supabase.co
3
+ VITE_SUPABASE_ANON_KEY=your-anon-key
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Admin Demo — svadmin</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
10
+ </head>
11
+ <body>
12
+ <div id="app"></div>
13
+ <script type="module" src="/src/main.ts"></script>
14
+ </body>
15
+ </html>
@@ -0,0 +1,12 @@
1
+ <script lang="ts">
2
+ import { AdminApp } from '@svadmin/ui';
3
+ import { createSimpleRestDataProvider } from '@svadmin/simple-rest';
4
+ import { resources } from './resources';
5
+ import { mockAuthProvider } from './providers/mockAuth';
6
+
7
+ const dataProvider = createSimpleRestDataProvider({
8
+ apiUrl: 'https://jsonplaceholder.typicode.com',
9
+ });
10
+ </script>
11
+
12
+ <AdminApp {dataProvider} {resources} authProvider={mockAuthProvider} title="svadmin Demo" locale="en" />
@@ -0,0 +1,263 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ /* Scan workspace-linked packages — Tailwind v4 skips node_modules by default */
5
+ @source "../node_modules/@svadmin/ui";
6
+ @source "../node_modules/@svadmin/core";
7
+
8
+ @custom-variant dark (&:is(.dark *));
9
+
10
+ /* ── Tailwind theme bridge (semantic tokens → CSS variables) ── */
11
+ @theme inline {
12
+ --color-background: hsl(var(--background));
13
+ --color-foreground: hsl(var(--foreground));
14
+ --color-card: hsl(var(--card));
15
+ --color-card-foreground: hsl(var(--card-foreground));
16
+ --color-popover: hsl(var(--popover));
17
+ --color-popover-foreground: hsl(var(--popover-foreground));
18
+ --color-primary: hsl(var(--primary));
19
+ --color-primary-foreground: hsl(var(--primary-foreground));
20
+ --color-secondary: hsl(var(--secondary));
21
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
22
+ --color-muted: hsl(var(--muted));
23
+ --color-muted-foreground: hsl(var(--muted-foreground));
24
+ --color-accent: hsl(var(--accent));
25
+ --color-accent-foreground: hsl(var(--accent-foreground));
26
+ --color-destructive: hsl(var(--destructive));
27
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
28
+ --color-border: hsl(var(--border));
29
+ --color-input: hsl(var(--input));
30
+ --color-ring: hsl(var(--ring));
31
+ --color-sidebar: hsl(var(--sidebar));
32
+ --color-sidebar-foreground: hsl(var(--sidebar-foreground));
33
+ --color-sidebar-primary: hsl(var(--sidebar-primary));
34
+ --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
35
+ --color-sidebar-accent: hsl(var(--sidebar-accent));
36
+ --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
37
+ --color-sidebar-border: hsl(var(--sidebar-border));
38
+ --color-sidebar-ring: hsl(var(--sidebar-ring));
39
+ --radius-sm: calc(var(--radius) - 4px);
40
+ --radius-md: calc(var(--radius) - 2px);
41
+ --radius-lg: var(--radius);
42
+ --radius-xl: calc(var(--radius) + 4px);
43
+ }
44
+
45
+ /* ╔═══════════════════════════════════════════════════════════╗
46
+ ║ COLOR THEME PRESETS ║
47
+ ║ Each [data-theme] block only overrides --primary, ║
48
+ ║ --ring, --sidebar-primary, and accent tints. ║
49
+ ║ Neutral colors (bg, card, muted, border) are shared. ║
50
+ ╚═══════════════════════════════════════════════════════════╝ */
51
+
52
+ /* ── Shared light base ────────────────────────────────────── */
53
+ :root {
54
+ --radius: 0.75rem;
55
+ --background: 0 0% 100%;
56
+ --foreground: 222.2 84% 4.9%;
57
+ --card: 0 0% 100%;
58
+ --card-foreground: 222.2 84% 4.9%;
59
+ --popover: 0 0% 100%;
60
+ --popover-foreground: 222.2 84% 4.9%;
61
+ --secondary: 210 40% 96.1%;
62
+ --secondary-foreground: 222.2 47.4% 11.2%;
63
+ --muted: 210 40% 96.1%;
64
+ --muted-foreground: 215.4 16.3% 46.9%;
65
+ --accent: 210 40% 96.1%;
66
+ --accent-foreground: 222.2 47.4% 11.2%;
67
+ --destructive: 0 84.2% 60.2%;
68
+ --destructive-foreground: 210 40% 98%;
69
+ --border: 214.3 31.8% 91.4%;
70
+ --input: 214.3 31.8% 91.4%;
71
+ --sidebar: 0 0% 98%;
72
+ --sidebar-foreground: 222.2 84% 4.9%;
73
+ --sidebar-accent: 210 40% 96.1%;
74
+ --sidebar-accent-foreground: 222.2 47.4% 11.2%;
75
+ --sidebar-border: 214.3 31.8% 91.4%;
76
+ }
77
+
78
+ /* ── Shared dark base ─────────────────────────────────────── */
79
+ .dark {
80
+ --background: 222.2 84% 4.9%;
81
+ --foreground: 210 40% 98%;
82
+ --card: 222.2 84% 4.9%;
83
+ --card-foreground: 210 40% 98%;
84
+ --popover: 222.2 84% 4.9%;
85
+ --popover-foreground: 210 40% 98%;
86
+ --secondary: 217.2 32.6% 17.5%;
87
+ --secondary-foreground: 210 40% 98%;
88
+ --muted: 217.2 32.6% 17.5%;
89
+ --muted-foreground: 215 20.2% 65.1%;
90
+ --accent: 217.2 32.6% 17.5%;
91
+ --accent-foreground: 210 40% 98%;
92
+ --destructive: 0 62.8% 30.6%;
93
+ --destructive-foreground: 210 40% 98%;
94
+ --border: 217.2 32.6% 17.5%;
95
+ --input: 217.2 32.6% 17.5%;
96
+ --sidebar: 222.2 84% 4.9%;
97
+ --sidebar-foreground: 210 40% 98%;
98
+ --sidebar-accent: 217.2 32.6% 17.5%;
99
+ --sidebar-accent-foreground: 210 40% 98%;
100
+ --sidebar-border: 217.2 32.6% 17.5%;
101
+ }
102
+
103
+ /* ── 🔵 Blue (default) ────────────────────────────────────── */
104
+ :root,
105
+ [data-theme="blue"] {
106
+ --primary: 221.2 83.2% 53.3%;
107
+ --primary-foreground: 210 40% 98%;
108
+ --ring: 221.2 83.2% 53.3%;
109
+ --sidebar-primary: 221.2 83.2% 53.3%;
110
+ --sidebar-primary-foreground: 210 40% 98%;
111
+ --sidebar-ring: 221.2 83.2% 53.3%;
112
+ }
113
+ .dark[data-theme="blue"],
114
+ [data-theme="blue"] .dark {
115
+ --primary: 217.2 91.2% 59.8%;
116
+ --primary-foreground: 222.2 47.4% 11.2%;
117
+ --ring: 224.3 76.3% 48%;
118
+ --sidebar-primary: 217.2 91.2% 59.8%;
119
+ --sidebar-primary-foreground: 222.2 47.4% 11.2%;
120
+ --sidebar-ring: 224.3 76.3% 48%;
121
+ }
122
+
123
+ /* ── 🟢 Green ─────────────────────────────────────────────── */
124
+ [data-theme="green"] {
125
+ --primary: 142.1 76.2% 36.3%;
126
+ --primary-foreground: 355.7 100% 97.3%;
127
+ --ring: 142.1 76.2% 36.3%;
128
+ --sidebar-primary: 142.1 76.2% 36.3%;
129
+ --sidebar-primary-foreground: 355.7 100% 97.3%;
130
+ --sidebar-ring: 142.1 76.2% 36.3%;
131
+ }
132
+ [data-theme="green"] .dark,
133
+ .dark[data-theme="green"] {
134
+ --primary: 142.1 70.6% 45.3%;
135
+ --primary-foreground: 144.9 80.4% 10%;
136
+ --ring: 142.4 71.8% 29.2%;
137
+ --sidebar-primary: 142.1 70.6% 45.3%;
138
+ --sidebar-primary-foreground: 144.9 80.4% 10%;
139
+ --sidebar-ring: 142.4 71.8% 29.2%;
140
+ }
141
+
142
+ /* ── 🌹 Rose ──────────────────────────────────────────────── */
143
+ [data-theme="rose"] {
144
+ --primary: 346.8 77.2% 49.8%;
145
+ --primary-foreground: 355.7 100% 97.3%;
146
+ --ring: 346.8 77.2% 49.8%;
147
+ --sidebar-primary: 346.8 77.2% 49.8%;
148
+ --sidebar-primary-foreground: 355.7 100% 97.3%;
149
+ --sidebar-ring: 346.8 77.2% 49.8%;
150
+ }
151
+ [data-theme="rose"] .dark,
152
+ .dark[data-theme="rose"] {
153
+ --primary: 346.8 77.2% 49.8%;
154
+ --primary-foreground: 355.7 100% 97.3%;
155
+ --ring: 346.8 77.2% 49.8%;
156
+ --sidebar-primary: 346.8 77.2% 49.8%;
157
+ --sidebar-primary-foreground: 355.7 100% 97.3%;
158
+ --sidebar-ring: 346.8 77.2% 49.8%;
159
+ }
160
+
161
+ /* ── 🟠 Orange ─────────────────────────────────────────────── */
162
+ [data-theme="orange"] {
163
+ --primary: 24.6 95% 53.1%;
164
+ --primary-foreground: 60 9.1% 97.8%;
165
+ --ring: 24.6 95% 53.1%;
166
+ --sidebar-primary: 24.6 95% 53.1%;
167
+ --sidebar-primary-foreground: 60 9.1% 97.8%;
168
+ --sidebar-ring: 24.6 95% 53.1%;
169
+ }
170
+ [data-theme="orange"] .dark,
171
+ .dark[data-theme="orange"] {
172
+ --primary: 20.5 90.2% 48.2%;
173
+ --primary-foreground: 60 9.1% 97.8%;
174
+ --ring: 20.5 90.2% 48.2%;
175
+ --sidebar-primary: 20.5 90.2% 48.2%;
176
+ --sidebar-primary-foreground: 60 9.1% 97.8%;
177
+ --sidebar-ring: 20.5 90.2% 48.2%;
178
+ }
179
+
180
+ /* ── 💜 Violet ─────────────────────────────────────────────── */
181
+ [data-theme="violet"] {
182
+ --primary: 262.1 83.3% 57.8%;
183
+ --primary-foreground: 210 40% 98%;
184
+ --ring: 262.1 83.3% 57.8%;
185
+ --sidebar-primary: 262.1 83.3% 57.8%;
186
+ --sidebar-primary-foreground: 210 40% 98%;
187
+ --sidebar-ring: 262.1 83.3% 57.8%;
188
+ }
189
+ [data-theme="violet"] .dark,
190
+ .dark[data-theme="violet"] {
191
+ --primary: 263.4 70% 50.4%;
192
+ --primary-foreground: 210 40% 98%;
193
+ --ring: 263.4 70% 50.4%;
194
+ --sidebar-primary: 263.4 70% 50.4%;
195
+ --sidebar-primary-foreground: 210 40% 98%;
196
+ --sidebar-ring: 263.4 70% 50.4%;
197
+ }
198
+
199
+ /* ── ⚫ Zinc (neutral) ─────────────────────────────────────── */
200
+ [data-theme="zinc"] {
201
+ --primary: 240 5.9% 10%;
202
+ --primary-foreground: 0 0% 98%;
203
+ --ring: 240 5.9% 10%;
204
+ --sidebar-primary: 240 5.9% 10%;
205
+ --sidebar-primary-foreground: 0 0% 98%;
206
+ --sidebar-ring: 240 5.9% 10%;
207
+ }
208
+ [data-theme="zinc"] .dark,
209
+ .dark[data-theme="zinc"] {
210
+ --primary: 0 0% 98%;
211
+ --primary-foreground: 240 5.9% 10%;
212
+ --ring: 240 4.9% 83.9%;
213
+ --sidebar-primary: 0 0% 98%;
214
+ --sidebar-primary-foreground: 240 5.9% 10%;
215
+ --sidebar-ring: 240 4.9% 83.9%;
216
+ }
217
+
218
+ /* ╔═══════════════════════════════════════════════════════════╗
219
+ ║ BASE STYLES ║
220
+ ╚═══════════════════════════════════════════════════════════╝ */
221
+
222
+ @layer base {
223
+ * {
224
+ border-color: hsl(var(--border));
225
+ }
226
+ body {
227
+ background-color: hsl(var(--background));
228
+ color: hsl(var(--foreground));
229
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
230
+ @apply antialiased min-h-screen;
231
+ }
232
+ }
233
+
234
+ /* ╔═══════════════════════════════════════════════════════════╗
235
+ ║ GLASS / GRADIENT EFFECTS ║
236
+ ╚═══════════════════════════════════════════════════════════╝ */
237
+
238
+ @layer components {
239
+ .glass {
240
+ @apply bg-white/70 dark:bg-slate-900/70 backdrop-blur-lg
241
+ border border-white/20 dark:border-slate-800/30 shadow-xl;
242
+ }
243
+ .glass-card {
244
+ @apply bg-white/70 dark:bg-slate-900/70 backdrop-blur-lg
245
+ border border-white/20 dark:border-slate-800/30
246
+ shadow-xl rounded-2xl p-6
247
+ transition-all duration-300
248
+ hover:shadow-2xl hover:scale-[1.01];
249
+ }
250
+ }
251
+
252
+ @layer utilities {
253
+ .text-gradient {
254
+ @apply bg-clip-text text-transparent
255
+ bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500;
256
+ }
257
+ }
258
+
259
+ /* ── Scrollbar ─────────────────────────────────────────────── */
260
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
261
+ ::-webkit-scrollbar-track { background: transparent; }
262
+ ::-webkit-scrollbar-thumb { background: hsl(214.3 31.8% 80%); border-radius: 3px; }
263
+ ::-webkit-scrollbar-thumb:hover { background: hsl(214.3 31.8% 65%); }
@@ -0,0 +1,7 @@
1
+ import { mount } from 'svelte';
2
+ import './app.css';
3
+ import App from './App.svelte';
4
+
5
+ const app = mount(App, { target: document.getElementById('app')! });
6
+
7
+ export default app;
@@ -0,0 +1,79 @@
1
+ <script lang="ts">
2
+ import { useList } from '@svadmin/core';
3
+ import { FileText, Users, MessageCircle, Loader2 } from 'lucide-svelte';
4
+
5
+ const postsQuery = useList({ resource: 'posts', pagination: { current: 1, pageSize: 1 } });
6
+ const usersQuery = useList({ resource: 'users', pagination: { current: 1, pageSize: 1 } });
7
+ const commentsQuery = useList({ resource: 'comments', pagination: { current: 1, pageSize: 1 } });
8
+
9
+ const stats = $derived([
10
+ { label: 'Total Posts', value: $postsQuery.data?.total ?? '—', Icon: FileText, color: 'bg-blue-50 text-blue-600' },
11
+ { label: 'Total Users', value: $usersQuery.data?.total ?? '—', Icon: Users, color: 'bg-green-50 text-green-600' },
12
+ { label: 'Total Comments', value: $commentsQuery.data?.total ?? '—', Icon: MessageCircle, color: 'bg-purple-50 text-purple-600' },
13
+ ]);
14
+
15
+ const isLoading = $derived($postsQuery.isLoading || $usersQuery.isLoading || $commentsQuery.isLoading);
16
+
17
+ const recentPosts = useList({ resource: 'posts', pagination: { current: 1, pageSize: 5 } });
18
+ const recentUsers = useList({ resource: 'users', pagination: { current: 1, pageSize: 5 } });
19
+ </script>
20
+
21
+ <div class="space-y-6">
22
+ <h1 class="text-2xl font-bold text-gray-900">Dashboard</h1>
23
+
24
+ <!-- Stats -->
25
+ <div class="grid gap-4 sm:grid-cols-3">
26
+ {#each stats as stat}
27
+ <div class="flex items-center gap-4 rounded-xl border border-gray-200 bg-white p-5 shadow-sm">
28
+ <div class="flex h-12 w-12 items-center justify-center rounded-xl {stat.color}">
29
+ <stat.Icon class="h-6 w-6" />
30
+ </div>
31
+ <div>
32
+ <p class="text-sm text-gray-500">{stat.label}</p>
33
+ {#if isLoading}
34
+ <Loader2 class="mt-1 h-5 w-5 animate-spin text-gray-300" />
35
+ {:else}
36
+ <p class="text-2xl font-bold text-gray-900">{stat.value}</p>
37
+ {/if}
38
+ </div>
39
+ </div>
40
+ {/each}
41
+ </div>
42
+
43
+ <!-- Recent Data -->
44
+ <div class="grid gap-6 lg:grid-cols-2">
45
+ <div class="rounded-xl border border-gray-200 bg-white shadow-sm">
46
+ <div class="flex items-center justify-between border-b border-gray-100 px-5 py-4">
47
+ <h2 class="font-semibold text-gray-900">Recent Posts</h2>
48
+ <a href="#/posts" class="text-sm text-primary hover:underline">View all</a>
49
+ </div>
50
+ <div class="divide-y divide-gray-50">
51
+ {#each $recentPosts.data?.data ?? [] as post}
52
+ <div class="flex items-center justify-between px-5 py-3">
53
+ <div>
54
+ <p class="text-sm font-medium text-gray-900">{post.title}</p>
55
+ <p class="text-xs text-gray-500">User #{post.userId}</p>
56
+ </div>
57
+ </div>
58
+ {/each}
59
+ </div>
60
+ </div>
61
+
62
+ <div class="rounded-xl border border-gray-200 bg-white shadow-sm">
63
+ <div class="flex items-center justify-between border-b border-gray-100 px-5 py-4">
64
+ <h2 class="font-semibold text-gray-900">Users</h2>
65
+ <a href="#/users" class="text-sm text-primary hover:underline">View all</a>
66
+ </div>
67
+ <div class="divide-y divide-gray-50">
68
+ {#each $recentUsers.data?.data ?? [] as user}
69
+ <div class="flex items-center justify-between px-5 py-3">
70
+ <div>
71
+ <p class="text-sm font-medium text-gray-900">{user.name}</p>
72
+ <p class="text-xs text-gray-500">{user.email}</p>
73
+ </div>
74
+ </div>
75
+ {/each}
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </div>
@@ -0,0 +1,82 @@
1
+ <script lang="ts">
2
+ import { getAuthProvider } from '@svadmin/core';
3
+ import { navigate } from '@svadmin/core/router';
4
+ import { Loader2, Shield } from 'lucide-svelte';
5
+
6
+ const auth = getAuthProvider();
7
+
8
+ let email = $state('');
9
+ let password = $state('');
10
+ let loading = $state(false);
11
+ let error = $state<string | null>(null);
12
+
13
+ async function handleLogin(e: Event) {
14
+ e.preventDefault();
15
+ loading = true;
16
+ error = null;
17
+
18
+ const result = await auth.login({ email, password });
19
+ if (result.success) {
20
+ navigate(result.redirectTo ?? '/');
21
+ } else {
22
+ error = result.error?.message ?? 'Login failed';
23
+ }
24
+ loading = false;
25
+ }
26
+ </script>
27
+
28
+ <div class="flex min-h-screen items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100">
29
+ <div class="w-full max-w-sm space-y-6 rounded-2xl bg-white p-8 shadow-xl shadow-blue-900/5">
30
+ <!-- Logo -->
31
+ <div class="text-center">
32
+ <div class="mx-auto flex h-14 w-14 items-center justify-center rounded-xl bg-primary-50">
33
+ <Shield class="h-7 w-7 text-primary" />
34
+ </div>
35
+ <h1 class="mt-4 text-xl font-bold text-gray-900">Admin Panel</h1>
36
+ <p class="mt-1 text-sm text-gray-500">Sign in to continue</p>
37
+ </div>
38
+
39
+ <!-- Error -->
40
+ {#if error}
41
+ <div class="rounded-lg bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700">
42
+ {error}
43
+ </div>
44
+ {/if}
45
+
46
+ <!-- Form -->
47
+ <form onsubmit={handleLogin} class="space-y-4">
48
+ <div>
49
+ <label for="email" class="block text-sm font-medium text-gray-700">Email</label>
50
+ <input
51
+ id="email"
52
+ type="email"
53
+ bind:value={email}
54
+ required
55
+ class="mt-1 w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
56
+ placeholder="admin@example.com"
57
+ />
58
+ </div>
59
+ <div>
60
+ <label for="password" class="block text-sm font-medium text-gray-700">Password</label>
61
+ <input
62
+ id="password"
63
+ type="password"
64
+ bind:value={password}
65
+ required
66
+ class="mt-1 w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
67
+ placeholder="••••••••"
68
+ />
69
+ </div>
70
+ <button
71
+ type="submit"
72
+ disabled={loading}
73
+ class="flex w-full items-center justify-center gap-2 rounded-lg bg-primary py-2.5 text-sm font-medium text-white hover:bg-primary-700 disabled:opacity-50 transition"
74
+ >
75
+ {#if loading}
76
+ <Loader2 class="h-4 w-4 animate-spin" />
77
+ {/if}
78
+ Sign in
79
+ </button>
80
+ </form>
81
+ </div>
82
+ </div>
@@ -0,0 +1,73 @@
1
+ import type { AuthProvider, AuthActionResult, CheckResult, Identity } from '@svadmin/core';
2
+
3
+ /**
4
+ * Mock AuthProvider for demo/development purposes.
5
+ * Uses localStorage to simulate authentication state.
6
+ */
7
+
8
+ const STORAGE_KEY = 'svadmin_demo_auth';
9
+
10
+ function getStoredAuth(): { email: string } | null {
11
+ try {
12
+ const raw = localStorage.getItem(STORAGE_KEY);
13
+ return raw ? JSON.parse(raw) : null;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ export const mockAuthProvider: AuthProvider = {
20
+ login: async (params): Promise<AuthActionResult> => {
21
+ const { email, password } = params as { email: string; password: string };
22
+ // Simulate login — accept any email with password "demo"
23
+ if (!email) return { success: false, error: { message: 'Email is required' } };
24
+ if (password !== 'demo') return { success: false, error: { message: 'Invalid password. Use "demo".' } };
25
+
26
+ localStorage.setItem(STORAGE_KEY, JSON.stringify({ email }));
27
+ return { success: true, redirectTo: '/' };
28
+ },
29
+
30
+ logout: async (): Promise<AuthActionResult> => {
31
+ localStorage.removeItem(STORAGE_KEY);
32
+ return { success: true, redirectTo: '/login' };
33
+ },
34
+
35
+ check: async (): Promise<CheckResult> => {
36
+ const auth = getStoredAuth();
37
+ return auth
38
+ ? { authenticated: true }
39
+ : { authenticated: false, redirectTo: '/login' };
40
+ },
41
+
42
+ getIdentity: async (): Promise<Identity | null> => {
43
+ const auth = getStoredAuth();
44
+ if (!auth) return null;
45
+ return {
46
+ id: '1',
47
+ name: auth.email.split('@')[0],
48
+ email: auth.email,
49
+ avatar: `https://api.dicebear.com/7.x/initials/svg?seed=${auth.email}`,
50
+ };
51
+ },
52
+
53
+ register: async (params): Promise<AuthActionResult> => {
54
+ const { email, password } = params as { email: string; password: string };
55
+ if (!email || !password) return { success: false, error: { message: 'Email and password are required' } };
56
+ localStorage.setItem(STORAGE_KEY, JSON.stringify({ email }));
57
+ return { success: true, redirectTo: '/' };
58
+ },
59
+
60
+ forgotPassword: async (params): Promise<AuthActionResult> => {
61
+ const { email } = params as { email: string };
62
+ if (!email) return { success: false, error: { message: 'Email is required' } };
63
+ // Simulate sending reset email
64
+ return { success: true };
65
+ },
66
+
67
+ updatePassword: async (params): Promise<AuthActionResult> => {
68
+ const { password, confirmPassword } = params as { password: string; confirmPassword: string };
69
+ if (!password) return { success: false, error: { message: 'Password is required' } };
70
+ if (password !== confirmPassword) return { success: false, error: { message: 'Passwords do not match' } };
71
+ return { success: true, redirectTo: '/login' };
72
+ },
73
+ };
@@ -0,0 +1,26 @@
1
+ // Supabase client initialization
2
+
3
+ import { createClient, type SupabaseClient } from '@supabase/supabase-js';
4
+
5
+ const isDev = import.meta.env.DEV;
6
+ const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL || (isDev ? window.location.origin : '');
7
+ const SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_ANON_KEY || '';
8
+
9
+ /** List of missing required env vars */
10
+ export const missingEnvVars: string[] = [];
11
+ if (!SUPABASE_URL) missingEnvVars.push('VITE_SUPABASE_URL');
12
+ if (!SUPABASE_ANON_KEY) missingEnvVars.push('VITE_SUPABASE_ANON_KEY');
13
+
14
+ /** Whether Supabase is properly configured with required env vars */
15
+ export const isSupabaseConfigured = missingEnvVars.length === 0;
16
+
17
+ // Only create client if configured — avoid crash on missing key
18
+ export const supabaseClient: SupabaseClient | null = isSupabaseConfigured
19
+ ? createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
20
+ auth: {
21
+ persistSession: true,
22
+ autoRefreshToken: true,
23
+ detectSessionInUrl: false,
24
+ },
25
+ })
26
+ : null;
@@ -0,0 +1,60 @@
1
+ // Example resource definitions — generic demo data (JSONPlaceholder-style)
2
+
3
+ import type { ResourceDefinition } from '@svadmin/core';
4
+
5
+ export const resources: ResourceDefinition[] = [
6
+ {
7
+ name: 'posts',
8
+ label: 'Posts',
9
+ icon: 'file-text',
10
+ fields: [
11
+ { key: 'id', label: 'ID', type: 'number', showInForm: false, width: '60px' },
12
+ { key: 'title', label: 'Title', type: 'text', required: true, searchable: true },
13
+ { key: 'body', label: 'Body', type: 'textarea', showInList: false },
14
+ { key: 'userId', label: 'Author', type: 'number', width: '80px' },
15
+ ],
16
+ defaultSort: { field: 'id', order: 'desc' },
17
+ },
18
+
19
+ {
20
+ name: 'users',
21
+ label: 'Users',
22
+ icon: 'users',
23
+ canCreate: false,
24
+ canDelete: false,
25
+ fields: [
26
+ { key: 'id', label: 'ID', type: 'number', showInForm: false, width: '60px' },
27
+ { key: 'name', label: 'Name', type: 'text', searchable: true },
28
+ { key: 'username', label: 'Username', type: 'text', width: '120px' },
29
+ { key: 'email', label: 'Email', type: 'email' },
30
+ { key: 'phone', label: 'Phone', type: 'phone', showInList: false },
31
+ { key: 'website', label: 'Website', type: 'url', showInList: false },
32
+ ],
33
+ },
34
+
35
+ {
36
+ name: 'comments',
37
+ label: 'Comments',
38
+ icon: 'message-circle',
39
+ fields: [
40
+ { key: 'id', label: 'ID', type: 'number', showInForm: false, width: '60px' },
41
+ { key: 'postId', label: 'Post', type: 'number', width: '80px' },
42
+ { key: 'name', label: 'Title', type: 'text', searchable: true },
43
+ { key: 'email', label: 'Email', type: 'email', width: '180px' },
44
+ { key: 'body', label: 'Body', type: 'textarea', showInList: false },
45
+ ],
46
+ defaultSort: { field: 'id', order: 'desc' },
47
+ },
48
+
49
+ {
50
+ name: 'todos',
51
+ label: 'Todos',
52
+ icon: 'check-square',
53
+ fields: [
54
+ { key: 'id', label: 'ID', type: 'number', showInForm: false, width: '60px' },
55
+ { key: 'title', label: 'Title', type: 'text', required: true, searchable: true },
56
+ { key: 'completed', label: 'Done', type: 'boolean', width: '80px' },
57
+ { key: 'userId', label: 'User', type: 'number', width: '80px' },
58
+ ],
59
+ },
60
+ ];
@@ -0,0 +1,11 @@
1
+ /// <reference types="svelte" />
2
+ /// <reference types="vite/client" />
3
+
4
+ interface ImportMetaEnv {
5
+ readonly VITE_SUPABASE_URL: string;
6
+ readonly VITE_SUPABASE_ANON_KEY: string;
7
+ }
8
+
9
+ interface ImportMeta {
10
+ readonly env: ImportMetaEnv;
11
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "types": ["vite/client"]
5
+ },
6
+ "include": ["src/**/*.ts", "src/**/*.svelte", "vite.config.ts"]
7
+ }
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from 'vite';
2
+ import { svelte } from '@sveltejs/vite-plugin-svelte';
3
+ import tailwindcss from '@tailwindcss/vite';
4
+
5
+ export default defineConfig({
6
+ plugins: [
7
+ tailwindcss(),
8
+ svelte(),
9
+ ],
10
+ server: {
11
+ port: 5174,
12
+ },
13
+ });