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 +36 -0
- package/index.js +134 -0
- package/package.json +28 -0
- package/sync-template.js +47 -0
- package/template/.env.example +3 -0
- package/template/index.html +15 -0
- package/template/src/App.svelte +12 -0
- package/template/src/app.css +263 -0
- package/template/src/main.ts +7 -0
- package/template/src/pages/Dashboard.svelte +79 -0
- package/template/src/pages/Login.svelte +82 -0
- package/template/src/providers/mockAuth.ts +73 -0
- package/template/src/providers/supabase.ts +26 -0
- package/template/src/resources.ts +60 -0
- package/template/src/vite-env.d.ts +11 -0
- package/template/tsconfig.json +7 -0
- package/template/vite.config.ts +13 -0
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
|
+
}
|
package/sync-template.js
ADDED
|
@@ -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,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,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,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
|
+
});
|