@svgrid/create 1.0.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 +49 -0
- package/index.mjs +225 -0
- package/package.json +40 -0
- package/templates/admin-dashboard/README.md +77 -0
- package/templates/admin-dashboard/_env.example +3 -0
- package/templates/admin-dashboard/_gitignore +9 -0
- package/templates/admin-dashboard/_npmrc +1 -0
- package/templates/admin-dashboard/_package.json +27 -0
- package/templates/admin-dashboard/src/app.css +18 -0
- package/templates/admin-dashboard/src/app.d.ts +12 -0
- package/templates/admin-dashboard/src/app.html +16 -0
- package/templates/admin-dashboard/src/lib/StatCard.svelte +20 -0
- package/templates/admin-dashboard/src/lib/data.ts +84 -0
- package/templates/admin-dashboard/src/lib/types.ts +25 -0
- package/templates/admin-dashboard/src/routes/+layout.svelte +79 -0
- package/templates/admin-dashboard/src/routes/+layout.ts +4 -0
- package/templates/admin-dashboard/src/routes/+page.svelte +87 -0
- package/templates/admin-dashboard/src/routes/customers/+page.svelte +75 -0
- package/templates/admin-dashboard/src/routes/orders/+page.svelte +89 -0
- package/templates/admin-dashboard/static/favicon.svg +10 -0
- package/templates/admin-dashboard/svelte.config.js +14 -0
- package/templates/admin-dashboard/tsconfig.json +14 -0
- package/templates/admin-dashboard/vite.config.ts +7 -0
- package/templates/minimal/README.md +21 -0
- package/templates/minimal/_gitignore +4 -0
- package/templates/minimal/_package.json +19 -0
- package/templates/minimal/index.html +12 -0
- package/templates/minimal/src/App.svelte +61 -0
- package/templates/minimal/src/main.js +6 -0
- package/templates/minimal/svelte.config.js +5 -0
- package/templates/minimal/vite.config.js +6 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @svgrid/create
|
|
2
|
+
|
|
3
|
+
Scaffold a [Svelte](https://svelte.dev) app powered by
|
|
4
|
+
[SvGrid](https://www.svgrid.com) - the modern Svelte 5 data grid - in one
|
|
5
|
+
command.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm create sv-grid@latest
|
|
9
|
+
# or
|
|
10
|
+
pnpm create sv-grid
|
|
11
|
+
# or
|
|
12
|
+
yarn create sv-grid
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Interactive by default. Or pass a directory and template directly:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm create sv-grid@latest my-app -- --template admin-dashboard
|
|
19
|
+
pnpm create sv-grid my-app -t minimal
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Templates
|
|
23
|
+
|
|
24
|
+
| Template | Stack | Best for |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| `minimal` | Vite + Svelte 5 + SvGrid | Dropping a grid into something quickly |
|
|
27
|
+
| `admin-dashboard` | SvelteKit + Tailwind + SvGrid, deploy to Vercel | A real dashboard / internal tool |
|
|
28
|
+
|
|
29
|
+
## Options
|
|
30
|
+
|
|
31
|
+
| Flag | Description |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| `--template`, `-t` | `minimal` or `admin-dashboard` |
|
|
34
|
+
| `--force`, `-f` | Scaffold into a non-empty directory |
|
|
35
|
+
| `--help`, `-h` | Show usage |
|
|
36
|
+
|
|
37
|
+
Then:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd my-app
|
|
41
|
+
npm install
|
|
42
|
+
npm run dev
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
All templates use the free MIT `@svgrid/grid` core. Add
|
|
46
|
+
[`@svgrid/enterprise`](https://www.svgrid.com/pricing) for Excel/PDF export, import,
|
|
47
|
+
print, pivot, and AI helpers.
|
|
48
|
+
|
|
49
|
+
SvGrid(TM) is a trademark of jQWidgets Ltd. This package is MIT-licensed.
|
package/index.mjs
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @svgrid/create - scaffold a Svelte app powered by SvGrid.
|
|
3
|
+
//
|
|
4
|
+
// npm create sv-grid@latest # interactive
|
|
5
|
+
// pnpm create sv-grid # interactive
|
|
6
|
+
// npm create sv-grid@latest my-app -- --template admin-dashboard
|
|
7
|
+
// npm create sv-grid@latest my-app -- -t minimal
|
|
8
|
+
//
|
|
9
|
+
// Zero runtime dependencies - Node built-ins only. Copies a bundled template,
|
|
10
|
+
// renames `_`-prefixed dotfiles, and rewrites the project name.
|
|
11
|
+
|
|
12
|
+
import { cp, mkdir, readdir, rename, stat, readFile, writeFile } from 'node:fs/promises'
|
|
13
|
+
import { existsSync } from 'node:fs'
|
|
14
|
+
import { dirname, join, basename, resolve, isAbsolute } from 'node:path'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
16
|
+
import { createInterface } from 'node:readline/promises'
|
|
17
|
+
import { stdin, stdout } from 'node:process'
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
20
|
+
|
|
21
|
+
const TEMPLATES = {
|
|
22
|
+
minimal: {
|
|
23
|
+
label: 'Minimal - Vite + Svelte 5 + SvGrid, one page',
|
|
24
|
+
bundled: join(__dirname, 'templates', 'minimal'),
|
|
25
|
+
},
|
|
26
|
+
'admin-dashboard': {
|
|
27
|
+
label: 'Admin dashboard - SvelteKit shell, multiple grids, deploy to Vercel',
|
|
28
|
+
bundled: join(__dirname, 'templates', 'admin-dashboard'),
|
|
29
|
+
// When running from the monorepo before `prepack` has synced the bundled
|
|
30
|
+
// copy, fall back to the canonical source.
|
|
31
|
+
fallback: join(__dirname, '..', '..', 'templates', 'sveltekit-admin-dashboard'),
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const RENAME_BACK = new Map([
|
|
36
|
+
['_gitignore', '.gitignore'],
|
|
37
|
+
['_npmrc', '.npmrc'],
|
|
38
|
+
['_env.example', '.env.example'],
|
|
39
|
+
['_package.json', 'package.json'],
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
const c = {
|
|
43
|
+
reset: '\x1b[0m',
|
|
44
|
+
bold: '\x1b[1m',
|
|
45
|
+
dim: '\x1b[2m',
|
|
46
|
+
green: '\x1b[32m',
|
|
47
|
+
cyan: '\x1b[36m',
|
|
48
|
+
red: '\x1b[31m',
|
|
49
|
+
yellow: '\x1b[33m',
|
|
50
|
+
}
|
|
51
|
+
const color = stdout.isTTY ? (k, s) => `${c[k]}${s}${c.reset}` : (_k, s) => s
|
|
52
|
+
|
|
53
|
+
function parseArgs(argv) {
|
|
54
|
+
const args = { _: [], template: null, force: false, help: false }
|
|
55
|
+
for (let i = 0; i < argv.length; i++) {
|
|
56
|
+
const a = argv[i]
|
|
57
|
+
if (a === '--help' || a === '-h') args.help = true
|
|
58
|
+
else if (a === '--force' || a === '-f') args.force = true
|
|
59
|
+
else if (a === '--template' || a === '-t') args.template = argv[++i]
|
|
60
|
+
else if (a.startsWith('--template=')) args.template = a.slice('--template='.length)
|
|
61
|
+
else if (!a.startsWith('-')) args._.push(a)
|
|
62
|
+
}
|
|
63
|
+
return args
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function printHelp() {
|
|
67
|
+
stdout.write(`
|
|
68
|
+
${color('bold', '@svgrid/create')} - scaffold a Svelte app powered by SvGrid
|
|
69
|
+
|
|
70
|
+
${color('bold', 'Usage')}
|
|
71
|
+
npm create sv-grid@latest [dir] -- [--template <name>] [--force]
|
|
72
|
+
|
|
73
|
+
${color('bold', 'Templates')}
|
|
74
|
+
${Object.entries(TEMPLATES)
|
|
75
|
+
.map(([k, t]) => ` ${color('cyan', k.padEnd(16))} ${t.label.replace(/^\S+\s+-\s+/, '')}`)
|
|
76
|
+
.join('\n')}
|
|
77
|
+
|
|
78
|
+
${color('bold', 'Examples')}
|
|
79
|
+
npm create sv-grid@latest
|
|
80
|
+
npm create sv-grid@latest my-app -- --template admin-dashboard
|
|
81
|
+
pnpm create sv-grid my-app -t minimal
|
|
82
|
+
`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sanitizeName(name) {
|
|
86
|
+
return (
|
|
87
|
+
name
|
|
88
|
+
.trim()
|
|
89
|
+
.toLowerCase()
|
|
90
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
91
|
+
.replace(/^[-_.]+|[-_.]+$/g, '') || 'sv-grid-app'
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolveTemplateDir(key) {
|
|
96
|
+
const t = TEMPLATES[key]
|
|
97
|
+
if (existsSync(t.bundled)) return t.bundled
|
|
98
|
+
if (t.fallback && existsSync(t.fallback)) return t.fallback
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function copyTemplate(srcDir, destDir) {
|
|
103
|
+
await cp(srcDir, destDir, {
|
|
104
|
+
recursive: true,
|
|
105
|
+
filter: (src) =>
|
|
106
|
+
!/[\\/](node_modules|\.svelte-kit|\.vercel|build|dist)([\\/]|$)/.test(src),
|
|
107
|
+
})
|
|
108
|
+
// Rename `_`-prefixed files back to their real dotfile names.
|
|
109
|
+
await renameBack(destDir)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function renameBack(dir) {
|
|
113
|
+
for (const entry of await readdir(dir)) {
|
|
114
|
+
const full = join(dir, entry)
|
|
115
|
+
if ((await stat(full)).isDirectory()) {
|
|
116
|
+
await renameBack(full)
|
|
117
|
+
} else if (RENAME_BACK.has(entry)) {
|
|
118
|
+
await rename(full, join(dir, RENAME_BACK.get(entry)))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function setProjectName(destDir, name) {
|
|
124
|
+
const pkgPath = join(destDir, 'package.json')
|
|
125
|
+
if (!existsSync(pkgPath)) return
|
|
126
|
+
try {
|
|
127
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
128
|
+
pkg.name = name
|
|
129
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
130
|
+
} catch {
|
|
131
|
+
// leave the template's name if it isn't valid JSON for some reason
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function isEmptyDir(dir) {
|
|
136
|
+
if (!existsSync(dir)) return true
|
|
137
|
+
const entries = await readdir(dir)
|
|
138
|
+
return entries.filter((e) => e !== '.git').length === 0
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function main() {
|
|
142
|
+
const args = parseArgs(process.argv.slice(2))
|
|
143
|
+
if (args.help) return printHelp()
|
|
144
|
+
|
|
145
|
+
const interactive = stdin.isTTY && stdout.isTTY
|
|
146
|
+
let rl = null
|
|
147
|
+
const ask = async (q, def) => {
|
|
148
|
+
if (!interactive) return def
|
|
149
|
+
rl ??= createInterface({ input: stdin, output: stdout })
|
|
150
|
+
const a = (await rl.question(`${q} ${color('dim', `(${def})`)} `)).trim()
|
|
151
|
+
return a || def
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
stdout.write(`\n${color('bold', '◆ @svgrid/create')} ${color('dim', 'Svelte + SvGrid')}\n\n`)
|
|
155
|
+
|
|
156
|
+
// 1. Target directory.
|
|
157
|
+
let target = args._[0]
|
|
158
|
+
if (!target) target = await ask('Project directory:', 'my-sv-grid-app')
|
|
159
|
+
const destDir = resolve(process.cwd(), target)
|
|
160
|
+
const projectName = sanitizeName(basename(destDir))
|
|
161
|
+
|
|
162
|
+
// 2. Template.
|
|
163
|
+
let template = args.template
|
|
164
|
+
if (!template) {
|
|
165
|
+
if (interactive) {
|
|
166
|
+
stdout.write(`\n Templates:\n`)
|
|
167
|
+
Object.entries(TEMPLATES).forEach(([k, t], i) => {
|
|
168
|
+
stdout.write(` ${color('cyan', String(i + 1))}. ${t.label}\n`)
|
|
169
|
+
})
|
|
170
|
+
const pick = await ask('\nChoose a template (1-2):', '1')
|
|
171
|
+
template = Object.keys(TEMPLATES)[Number(pick) - 1] ?? 'minimal'
|
|
172
|
+
} else {
|
|
173
|
+
template = 'minimal'
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (!TEMPLATES[template]) {
|
|
177
|
+
if (rl) rl.close()
|
|
178
|
+
stdout.write(
|
|
179
|
+
`\n${color('red', '✖')} Unknown template "${template}". Choose: ${Object.keys(TEMPLATES).join(', ')}\n`,
|
|
180
|
+
)
|
|
181
|
+
process.exit(1)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 3. Safety: don't clobber a non-empty directory.
|
|
185
|
+
if (!(await isEmptyDir(destDir)) && !args.force) {
|
|
186
|
+
const ok = await ask(
|
|
187
|
+
`\n${color('yellow', '!')} ${target} is not empty. Continue and overwrite files? (y/N)`,
|
|
188
|
+
'N',
|
|
189
|
+
)
|
|
190
|
+
if (!/^y(es)?$/i.test(ok)) {
|
|
191
|
+
if (rl) rl.close()
|
|
192
|
+
stdout.write(`${color('red', '✖')} Aborted.\n`)
|
|
193
|
+
process.exit(1)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const srcDir = resolveTemplateDir(template)
|
|
198
|
+
if (!srcDir) {
|
|
199
|
+
if (rl) rl.close()
|
|
200
|
+
stdout.write(
|
|
201
|
+
`\n${color('red', '✖')} Template "${template}" is not available in this build.\n`,
|
|
202
|
+
)
|
|
203
|
+
process.exit(1)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 4. Scaffold.
|
|
207
|
+
await mkdir(destDir, { recursive: true })
|
|
208
|
+
await copyTemplate(srcDir, destDir)
|
|
209
|
+
await setProjectName(destDir, projectName)
|
|
210
|
+
if (rl) rl.close()
|
|
211
|
+
|
|
212
|
+
// 5. Next steps.
|
|
213
|
+
const rel = isAbsolute(target) || target.startsWith('.') ? target : `./${target}`
|
|
214
|
+
stdout.write(`\n${color('green', '✔')} Scaffolded ${color('bold', projectName)} (${template}) into ${rel}\n\n`)
|
|
215
|
+
stdout.write(`${color('bold', 'Next steps')}\n`)
|
|
216
|
+
stdout.write(` cd ${target}\n`)
|
|
217
|
+
stdout.write(` npm install\n`)
|
|
218
|
+
stdout.write(` npm run dev\n\n`)
|
|
219
|
+
stdout.write(`${color('dim', 'Docs:')} https://www.svgrid.com/docs ${color('dim', 'Pro:')} https://www.svgrid.com/pricing\n\n`)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
main().catch((err) => {
|
|
223
|
+
console.error(err)
|
|
224
|
+
process.exit(1)
|
|
225
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@svgrid/create",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Scaffold a Svelte app powered by SvGrid in one command: npm create sv-grid@latest",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "jQWidgets Ltd",
|
|
8
|
+
"homepage": "https://www.svgrid.com",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/sv-grid/sv-grid.git",
|
|
12
|
+
"directory": "packages/create-sv-grid"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"svelte",
|
|
16
|
+
"sveltekit",
|
|
17
|
+
"data-grid",
|
|
18
|
+
"datagrid",
|
|
19
|
+
"table",
|
|
20
|
+
"sv-grid",
|
|
21
|
+
"create",
|
|
22
|
+
"scaffold",
|
|
23
|
+
"starter",
|
|
24
|
+
"template"
|
|
25
|
+
],
|
|
26
|
+
"bin": {
|
|
27
|
+
"@svgrid/create": "index.mjs"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"index.mjs",
|
|
31
|
+
"templates"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"sync-templates": "node sync-templates.mjs",
|
|
38
|
+
"prepack": "node sync-templates.mjs"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# SvGrid Admin Dashboard (SvelteKit starter)
|
|
2
|
+
|
|
3
|
+
A production-ready admin dashboard built with **[SvelteKit](https://svelte.dev/docs/kit)**
|
|
4
|
+
and **[SvGrid](https://www.svgrid.com)** - the modern Svelte 5 data grid.
|
|
5
|
+
Sortable, filterable, editable grids; KPI cards; a sidebar shell; prerendered
|
|
6
|
+
to static HTML for SEO; one-click deploy to Vercel.
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
## Deploy
|
|
11
|
+
|
|
12
|
+
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fsv-grid%2Fsv-grid%2Ftree%2Fmain%2Ftemplates%2Fsveltekit-admin-dashboard&project-name=sv-grid-admin&repository-name=sv-grid-admin)
|
|
13
|
+
|
|
14
|
+
> Deploying from this monorepo subfolder? In the Vercel import step set the
|
|
15
|
+
> **Root Directory** to `templates/sveltekit-admin-dashboard`. Or scaffold a
|
|
16
|
+
> standalone copy first (recommended):
|
|
17
|
+
>
|
|
18
|
+
> ```bash
|
|
19
|
+
> npm create sv-grid@latest my-admin -- --template admin-dashboard
|
|
20
|
+
> ```
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install
|
|
26
|
+
npm run dev # http://localhost:5173
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm run build # static + serverless output via adapter-vercel
|
|
31
|
+
npm run preview # preview the production build
|
|
32
|
+
npm run check # svelte-check (types)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## What's inside
|
|
36
|
+
|
|
37
|
+
| Path | What it shows |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `src/routes/+layout.svelte` | App shell: sidebar nav + top bar |
|
|
40
|
+
| `src/routes/+page.svelte` | Overview: KPI cards + a recent-orders grid |
|
|
41
|
+
| `src/routes/orders/+page.svelte` | Full grid: sort, filter, **row select**, inline edit, pagination |
|
|
42
|
+
| `src/routes/customers/+page.svelte` | Grid with **column grouping** (drag a column to the group bar) |
|
|
43
|
+
| `src/lib/data.ts` | Deterministic sample data - swap for your API / `load()` |
|
|
44
|
+
| `src/lib/types.ts` | `Order` / `Customer` row types |
|
|
45
|
+
|
|
46
|
+
The grids mount client-side (`{#if browser}`) while the shell and headings
|
|
47
|
+
prerender to crawlable HTML - good for SEO and instant first paint. Every
|
|
48
|
+
route is `prerender = true` (see `src/routes/+layout.ts`); delete that line
|
|
49
|
+
on any route that needs SSR or runtime data.
|
|
50
|
+
|
|
51
|
+
## Wiring your own data
|
|
52
|
+
|
|
53
|
+
Replace the calls to `makeOrders()` / `makeCustomers()` in each route with a
|
|
54
|
+
SvelteKit [`load`](https://svelte.dev/docs/kit/load) function:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// src/routes/orders/+page.ts
|
|
58
|
+
export async function load({ fetch }) {
|
|
59
|
+
const orders = await fetch('/api/orders').then((r) => r.json())
|
|
60
|
+
return { orders }
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Then read `data.orders` in the page via `let { data } = $props()`.
|
|
65
|
+
|
|
66
|
+
## Upgrade to Pro
|
|
67
|
+
|
|
68
|
+
This starter uses the free MIT **`@svgrid/grid`** core. For Excel/PDF
|
|
69
|
+
export, data import, printing, pivot, and the AI helpers, add
|
|
70
|
+
[`@svgrid/enterprise`](https://www.svgrid.com/pricing) and call `installPro(api)` on
|
|
71
|
+
the grid's API. Pro runs in evaluation with a watermark - no key needed to try
|
|
72
|
+
it.
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
This template is MIT-licensed - use it as the basis for your own app.
|
|
77
|
+
SvGrid(TM) is a trademark of jQWidgets Ltd.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
engine-strict=true
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sv-grid-admin-dashboard",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Production-ready SvelteKit admin dashboard built on SvGrid (Svelte 5 data grid).",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite dev",
|
|
9
|
+
"build": "vite build",
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@sveltejs/adapter-vercel": "^5.5.0",
|
|
15
|
+
"@sveltejs/kit": "^2.15.0",
|
|
16
|
+
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
17
|
+
"@tailwindcss/vite": "^4.2.4",
|
|
18
|
+
"svelte": "^5.55.5",
|
|
19
|
+
"svelte-check": "^4.4.6",
|
|
20
|
+
"tailwindcss": "^4.2.4",
|
|
21
|
+
"typescript": "^5.7.0",
|
|
22
|
+
"vite": "^8.0.10"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@svgrid/grid": "^1.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
/* SvGrid ships its own scoped styles. These custom properties let you theme
|
|
4
|
+
the grid to match the dashboard shell. Tweak freely. */
|
|
5
|
+
:root {
|
|
6
|
+
--app-bg: #f8fafc;
|
|
7
|
+
--app-panel: #ffffff;
|
|
8
|
+
--app-border: #e2e8f0;
|
|
9
|
+
--app-fg: #0f172a;
|
|
10
|
+
--app-muted: #64748b;
|
|
11
|
+
--app-accent: #4f46e5;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
html,
|
|
15
|
+
body {
|
|
16
|
+
background: var(--app-bg);
|
|
17
|
+
color: var(--app-fg);
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" class="h-full">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<meta
|
|
8
|
+
name="description"
|
|
9
|
+
content="Admin dashboard starter built with SvelteKit and SvGrid - the Svelte 5 data grid."
|
|
10
|
+
/>
|
|
11
|
+
%sveltekit.head%
|
|
12
|
+
</head>
|
|
13
|
+
<body data-sveltekit-preload-data="hover" class="h-full">
|
|
14
|
+
<div style="display: contents">%sveltekit.body%</div>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
label,
|
|
4
|
+
value,
|
|
5
|
+
hint = '',
|
|
6
|
+
}: { label: string; value: string; hint?: string } = $props()
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<div
|
|
10
|
+
class="rounded-xl border bg-white p-4 shadow-sm"
|
|
11
|
+
style="border-color: var(--app-border);"
|
|
12
|
+
>
|
|
13
|
+
<p class="text-xs font-medium uppercase tracking-wide" style="color: var(--app-muted);">
|
|
14
|
+
{label}
|
|
15
|
+
</p>
|
|
16
|
+
<p class="mt-1 text-2xl font-bold" style="color: var(--app-fg);">{value}</p>
|
|
17
|
+
{#if hint}
|
|
18
|
+
<p class="mt-1 text-xs" style="color: var(--app-muted);">{hint}</p>
|
|
19
|
+
{/if}
|
|
20
|
+
</div>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Customer, Order, OrderStatus } from './types'
|
|
2
|
+
|
|
3
|
+
// Deterministic pseudo-random generator so the sample data is stable across
|
|
4
|
+
// reloads and SSR/CSR (no hydration mismatch). Swap any of this out for your
|
|
5
|
+
// own API / load function.
|
|
6
|
+
function mulberry32(seed: number): () => number {
|
|
7
|
+
let a = seed
|
|
8
|
+
return () => {
|
|
9
|
+
a |= 0
|
|
10
|
+
a = (a + 0x6d2b79f5) | 0
|
|
11
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a)
|
|
12
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
|
13
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const FIRST = ['Ava', 'Liam', 'Noah', 'Emma', 'Mia', 'Lucas', 'Sofia', 'Ethan', 'Aria', 'Leo', 'Zoe', 'Kai']
|
|
18
|
+
const LAST = ['Carter', 'Nguyen', 'Patel', 'Kim', 'Garcia', 'Müller', 'Rossi', 'Silva', 'Okafor', 'Haddad', 'Novak', 'Tanaka']
|
|
19
|
+
const COMPANIES = ['Northwind', 'Acme', 'Globex', 'Initech', 'Umbrella', 'Hooli', 'Vandelay', 'Stark Co', 'Wayne LLC', 'Soylent']
|
|
20
|
+
const PRODUCTS = ['Starter Plan', 'Team Plan', 'Enterprise Plan', 'Add-on: Storage', 'Add-on: Seats', 'Onboarding']
|
|
21
|
+
const COUNTRIES = ['US', 'UK', 'DE', 'FR', 'JP', 'BR', 'CA', 'AU', 'IN', 'NL']
|
|
22
|
+
const STATUSES: OrderStatus[] = ['paid', 'paid', 'paid', 'pending', 'refunded', 'failed']
|
|
23
|
+
const PLANS: Customer['plan'][] = ['Free', 'Pro', 'Pro', 'Enterprise']
|
|
24
|
+
|
|
25
|
+
function isoDaysAgo(rng: () => number, maxDays: number): string {
|
|
26
|
+
const d = new Date()
|
|
27
|
+
d.setDate(d.getDate() - Math.floor(rng() * maxDays))
|
|
28
|
+
return d.toISOString().slice(0, 10)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function makeOrders(count = 240): Order[] {
|
|
32
|
+
const rng = mulberry32(42)
|
|
33
|
+
return Array.from({ length: count }, (_, i) => {
|
|
34
|
+
const customer = `${FIRST[Math.floor(rng() * FIRST.length)]} ${LAST[Math.floor(rng() * LAST.length)]}`
|
|
35
|
+
const quantity = 1 + Math.floor(rng() * 6)
|
|
36
|
+
const unit = [19, 49, 99, 149, 299][Math.floor(rng() * 5)]
|
|
37
|
+
return {
|
|
38
|
+
id: `ORD-${(10000 + i).toString()}`,
|
|
39
|
+
customer,
|
|
40
|
+
email: `${customer.toLowerCase().replace(/[^a-z]/g, '.')}@example.com`,
|
|
41
|
+
product: PRODUCTS[Math.floor(rng() * PRODUCTS.length)],
|
|
42
|
+
status: STATUSES[Math.floor(rng() * STATUSES.length)],
|
|
43
|
+
quantity,
|
|
44
|
+
total: quantity * unit,
|
|
45
|
+
country: COUNTRIES[Math.floor(rng() * COUNTRIES.length)],
|
|
46
|
+
date: isoDaysAgo(rng, 120),
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function makeCustomers(count = 120): Customer[] {
|
|
52
|
+
const rng = mulberry32(7)
|
|
53
|
+
return Array.from({ length: count }, (_, i) => {
|
|
54
|
+
const name = `${FIRST[Math.floor(rng() * FIRST.length)]} ${LAST[Math.floor(rng() * LAST.length)]}`
|
|
55
|
+
const plan = PLANS[Math.floor(rng() * PLANS.length)]
|
|
56
|
+
const seats = plan === 'Enterprise' ? 10 + Math.floor(rng() * 90) : plan === 'Pro' ? 1 + Math.floor(rng() * 10) : 1
|
|
57
|
+
const perSeat = plan === 'Enterprise' ? 40 : plan === 'Pro' ? 15 : 0
|
|
58
|
+
return {
|
|
59
|
+
id: `CUS-${(2000 + i).toString()}`,
|
|
60
|
+
name,
|
|
61
|
+
email: `${name.toLowerCase().replace(/[^a-z]/g, '.')}@example.com`,
|
|
62
|
+
company: COMPANIES[Math.floor(rng() * COMPANIES.length)],
|
|
63
|
+
plan,
|
|
64
|
+
mrr: seats * perSeat,
|
|
65
|
+
seats,
|
|
66
|
+
active: rng() > 0.18,
|
|
67
|
+
joined: isoDaysAgo(rng, 900),
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Headline KPIs derived from the sample data - feeds the dashboard cards. */
|
|
73
|
+
export function summarize(orders: Order[], customers: Customer[]) {
|
|
74
|
+
const revenue = orders.filter((o) => o.status === 'paid').reduce((s, o) => s + o.total, 0)
|
|
75
|
+
const mrr = customers.filter((c) => c.active).reduce((s, c) => s + c.mrr, 0)
|
|
76
|
+
const pending = orders.filter((o) => o.status === 'pending').length
|
|
77
|
+
return {
|
|
78
|
+
revenue,
|
|
79
|
+
mrr,
|
|
80
|
+
pending,
|
|
81
|
+
customers: customers.filter((c) => c.active).length,
|
|
82
|
+
orders: orders.length,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type OrderStatus = 'paid' | 'pending' | 'refunded' | 'failed'
|
|
2
|
+
|
|
3
|
+
export interface Order {
|
|
4
|
+
id: string
|
|
5
|
+
customer: string
|
|
6
|
+
email: string
|
|
7
|
+
product: string
|
|
8
|
+
status: OrderStatus
|
|
9
|
+
quantity: number
|
|
10
|
+
total: number
|
|
11
|
+
country: string
|
|
12
|
+
date: string // ISO yyyy-mm-dd
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Customer {
|
|
16
|
+
id: string
|
|
17
|
+
name: string
|
|
18
|
+
email: string
|
|
19
|
+
company: string
|
|
20
|
+
plan: 'Free' | 'Pro' | 'Enterprise'
|
|
21
|
+
mrr: number
|
|
22
|
+
seats: number
|
|
23
|
+
active: boolean
|
|
24
|
+
joined: string // ISO yyyy-mm-dd
|
|
25
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import '../app.css'
|
|
3
|
+
import { page } from '$app/state'
|
|
4
|
+
|
|
5
|
+
let { children } = $props()
|
|
6
|
+
|
|
7
|
+
const nav = [
|
|
8
|
+
{ href: '/', label: 'Overview', icon: '▦' },
|
|
9
|
+
{ href: '/orders', label: 'Orders', icon: '🧾' },
|
|
10
|
+
{ href: '/customers', label: 'Customers', icon: '👥' },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
function isActive(href: string): boolean {
|
|
14
|
+
return href === '/' ? page.url.pathname === '/' : page.url.pathname.startsWith(href)
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<div class="flex h-full min-h-screen">
|
|
19
|
+
<!-- Sidebar -->
|
|
20
|
+
<aside
|
|
21
|
+
class="hidden w-60 shrink-0 flex-col border-r bg-white md:flex"
|
|
22
|
+
style="border-color: var(--app-border);"
|
|
23
|
+
>
|
|
24
|
+
<div class="flex items-center gap-2 px-5 py-4">
|
|
25
|
+
<span
|
|
26
|
+
class="grid h-8 w-8 place-items-center rounded-lg font-bold text-white"
|
|
27
|
+
style="background: var(--app-accent);">S</span
|
|
28
|
+
>
|
|
29
|
+
<div class="leading-tight">
|
|
30
|
+
<p class="text-sm font-bold" style="color: var(--app-fg);">SvGrid Admin</p>
|
|
31
|
+
<p class="text-[11px]" style="color: var(--app-muted);">Dashboard starter</p>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<nav class="mt-2 flex-1 px-3">
|
|
36
|
+
{#each nav as item}
|
|
37
|
+
<a
|
|
38
|
+
href={item.href}
|
|
39
|
+
class="mb-1 flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors"
|
|
40
|
+
style={isActive(item.href)
|
|
41
|
+
? 'background: var(--app-accent); color: #fff;'
|
|
42
|
+
: 'color: var(--app-fg);'}
|
|
43
|
+
>
|
|
44
|
+
<span class="w-5 text-center">{item.icon}</span>
|
|
45
|
+
{item.label}
|
|
46
|
+
</a>
|
|
47
|
+
{/each}
|
|
48
|
+
</nav>
|
|
49
|
+
|
|
50
|
+
<div class="px-5 py-4 text-[11px]" style="color: var(--app-muted);">
|
|
51
|
+
Built with
|
|
52
|
+
<a class="underline" href="https://www.svgrid.com" target="_blank" rel="noopener">SvGrid</a>
|
|
53
|
+
+ SvelteKit
|
|
54
|
+
</div>
|
|
55
|
+
</aside>
|
|
56
|
+
|
|
57
|
+
<!-- Main -->
|
|
58
|
+
<div class="flex min-w-0 flex-1 flex-col">
|
|
59
|
+
<header
|
|
60
|
+
class="flex items-center justify-between border-b bg-white px-6 py-3"
|
|
61
|
+
style="border-color: var(--app-border);"
|
|
62
|
+
>
|
|
63
|
+
<h1 class="text-base font-semibold" style="color: var(--app-fg);">
|
|
64
|
+
{nav.find((n) => isActive(n.href))?.label ?? 'Dashboard'}
|
|
65
|
+
</h1>
|
|
66
|
+
<a
|
|
67
|
+
href="https://www.svgrid.com/docs"
|
|
68
|
+
target="_blank"
|
|
69
|
+
rel="noopener"
|
|
70
|
+
class="rounded-lg border px-3 py-1.5 text-sm font-medium"
|
|
71
|
+
style="border-color: var(--app-border); color: var(--app-fg);">Docs</a
|
|
72
|
+
>
|
|
73
|
+
</header>
|
|
74
|
+
|
|
75
|
+
<main class="min-w-0 flex-1 p-6">
|
|
76
|
+
{@render children()}
|
|
77
|
+
</main>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { browser } from '$app/environment'
|
|
3
|
+
import {
|
|
4
|
+
SvGrid,
|
|
5
|
+
tableFeatures,
|
|
6
|
+
rowSortingFeature,
|
|
7
|
+
columnFilteringFeature,
|
|
8
|
+
type ColumnDef,
|
|
9
|
+
} from '@svgrid/grid'
|
|
10
|
+
import StatCard from '$lib/StatCard.svelte'
|
|
11
|
+
import { makeOrders, makeCustomers, summarize } from '$lib/data'
|
|
12
|
+
import type { Order } from '$lib/types'
|
|
13
|
+
|
|
14
|
+
const orders = makeOrders()
|
|
15
|
+
const customers = makeCustomers()
|
|
16
|
+
const kpis = summarize(orders, customers)
|
|
17
|
+
|
|
18
|
+
const usd = (n: number) =>
|
|
19
|
+
n.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 })
|
|
20
|
+
|
|
21
|
+
// Most recent 8 orders for the overview table.
|
|
22
|
+
const recent = $state(
|
|
23
|
+
[...orders].sort((a, b) => b.date.localeCompare(a.date)).slice(0, 8),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })
|
|
27
|
+
|
|
28
|
+
const columns: ColumnDef<typeof features, Order>[] = [
|
|
29
|
+
{ field: 'id', header: 'Order', width: 110 },
|
|
30
|
+
{ field: 'customer', header: 'Customer', width: 160 },
|
|
31
|
+
{ field: 'product', header: 'Product', width: 150 },
|
|
32
|
+
{ field: 'status', header: 'Status', width: 110 },
|
|
33
|
+
{
|
|
34
|
+
field: 'total',
|
|
35
|
+
header: 'Total',
|
|
36
|
+
width: 110,
|
|
37
|
+
align: 'right',
|
|
38
|
+
format: { type: 'currency', currency: 'USD' },
|
|
39
|
+
},
|
|
40
|
+
{ field: 'date', header: 'Date', width: 120, format: { type: 'date', pattern: 'y-m-d' } },
|
|
41
|
+
]
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<svelte:head>
|
|
45
|
+
<title>Overview · SvGrid Admin</title>
|
|
46
|
+
</svelte:head>
|
|
47
|
+
|
|
48
|
+
<section class="space-y-6">
|
|
49
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
50
|
+
<StatCard label="Revenue (paid)" value={usd(kpis.revenue)} hint="all-time, sample data" />
|
|
51
|
+
<StatCard label="MRR" value={usd(kpis.mrr)} hint={`${kpis.customers} active customers`} />
|
|
52
|
+
<StatCard label="Orders" value={kpis.orders.toString()} hint={`${kpis.pending} pending`} />
|
|
53
|
+
<StatCard label="Active customers" value={kpis.customers.toString()} />
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="rounded-xl border bg-white p-4 shadow-sm" style="border-color: var(--app-border);">
|
|
57
|
+
<div class="mb-3 flex items-center justify-between">
|
|
58
|
+
<h2 class="text-sm font-semibold" style="color: var(--app-fg);">Recent orders</h2>
|
|
59
|
+
<a class="text-sm font-medium" style="color: var(--app-accent);" href="/orders">View all →</a>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div style="height: 360px;">
|
|
63
|
+
{#if browser}
|
|
64
|
+
<SvGrid
|
|
65
|
+
data={recent}
|
|
66
|
+
{columns}
|
|
67
|
+
{features}
|
|
68
|
+
filterMode="menu"
|
|
69
|
+
showRowNumbers={false}
|
|
70
|
+
showPagination={false}
|
|
71
|
+
rowHeight={40}
|
|
72
|
+
containerHeight="100%"
|
|
73
|
+
fitColumns={true}
|
|
74
|
+
/>
|
|
75
|
+
{:else}
|
|
76
|
+
<!-- Prerendered placeholder: real, crawlable rows for SEO -->
|
|
77
|
+
<ul class="text-sm" style="color: var(--app-muted);">
|
|
78
|
+
{#each recent as o}
|
|
79
|
+
<li class="border-b py-1" style="border-color: var(--app-border);">
|
|
80
|
+
{o.id} · {o.customer} · {o.product} · {usd(o.total)}
|
|
81
|
+
</li>
|
|
82
|
+
{/each}
|
|
83
|
+
</ul>
|
|
84
|
+
{/if}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</section>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { browser } from '$app/environment'
|
|
3
|
+
import {
|
|
4
|
+
SvGrid,
|
|
5
|
+
tableFeatures,
|
|
6
|
+
rowSortingFeature,
|
|
7
|
+
columnFilteringFeature,
|
|
8
|
+
columnGroupingFeature,
|
|
9
|
+
type ColumnDef,
|
|
10
|
+
} from '@svgrid/grid'
|
|
11
|
+
import { makeCustomers } from '$lib/data'
|
|
12
|
+
import type { Customer } from '$lib/types'
|
|
13
|
+
|
|
14
|
+
const features = tableFeatures({
|
|
15
|
+
rowSortingFeature,
|
|
16
|
+
columnFilteringFeature,
|
|
17
|
+
columnGroupingFeature,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
let rows = $state<Customer[]>(makeCustomers())
|
|
21
|
+
|
|
22
|
+
const columns: ColumnDef<typeof features, Customer>[] = [
|
|
23
|
+
{ field: 'id', header: 'ID', width: 100 },
|
|
24
|
+
{ field: 'name', header: 'Name', editorType: 'text', width: 150 },
|
|
25
|
+
{ field: 'company', header: 'Company', width: 130 },
|
|
26
|
+
{ field: 'plan', header: 'Plan', width: 120 },
|
|
27
|
+
{
|
|
28
|
+
field: 'seats',
|
|
29
|
+
header: 'Seats',
|
|
30
|
+
width: 90,
|
|
31
|
+
align: 'right',
|
|
32
|
+
format: { type: 'number', options: { maximumFractionDigits: 0 } },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
field: 'mrr',
|
|
36
|
+
header: 'MRR',
|
|
37
|
+
width: 110,
|
|
38
|
+
align: 'right',
|
|
39
|
+
format: { type: 'currency', currency: 'USD' },
|
|
40
|
+
},
|
|
41
|
+
{ field: 'active', header: 'Active', width: 90, editorType: 'checkbox' },
|
|
42
|
+
{ field: 'joined', header: 'Joined', width: 120, format: { type: 'date', pattern: 'y-m-d' } },
|
|
43
|
+
]
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<svelte:head>
|
|
47
|
+
<title>Customers · SvGrid Admin</title>
|
|
48
|
+
</svelte:head>
|
|
49
|
+
|
|
50
|
+
<section class="flex h-full flex-col gap-3">
|
|
51
|
+
<p class="text-sm" style="color: var(--app-muted);">
|
|
52
|
+
{rows.length} customers · drag the <strong>Plan</strong> column into the group bar to group by plan.
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
<div class="min-h-0 flex-1 rounded-xl border bg-white p-2 shadow-sm" style="border-color: var(--app-border);">
|
|
56
|
+
{#if browser}
|
|
57
|
+
<SvGrid
|
|
58
|
+
data={rows}
|
|
59
|
+
{columns}
|
|
60
|
+
{features}
|
|
61
|
+
filterMode="menu"
|
|
62
|
+
showRowNumbers={true}
|
|
63
|
+
showPagination={true}
|
|
64
|
+
pageSize={50}
|
|
65
|
+
showGroupingControls={true}
|
|
66
|
+
enableInlineEditing={true}
|
|
67
|
+
rowHeight={38}
|
|
68
|
+
containerHeight="100%"
|
|
69
|
+
fitColumns={true}
|
|
70
|
+
/>
|
|
71
|
+
{:else}
|
|
72
|
+
<p class="p-4 text-sm" style="color: var(--app-muted);">Loading {rows.length} customers…</p>
|
|
73
|
+
{/if}
|
|
74
|
+
</div>
|
|
75
|
+
</section>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { browser } from '$app/environment'
|
|
3
|
+
import {
|
|
4
|
+
SvGrid,
|
|
5
|
+
tableFeatures,
|
|
6
|
+
rowSortingFeature,
|
|
7
|
+
columnFilteringFeature,
|
|
8
|
+
rowSelectionFeature,
|
|
9
|
+
type ColumnDef,
|
|
10
|
+
type SvGridApi,
|
|
11
|
+
} from '@svgrid/grid'
|
|
12
|
+
import { makeOrders } from '$lib/data'
|
|
13
|
+
import type { Order } from '$lib/types'
|
|
14
|
+
|
|
15
|
+
const features = tableFeatures({
|
|
16
|
+
rowSortingFeature,
|
|
17
|
+
columnFilteringFeature,
|
|
18
|
+
rowSelectionFeature,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
let rows = $state<Order[]>(makeOrders())
|
|
22
|
+
let api = $state<SvGridApi<typeof features, Order> | null>(null)
|
|
23
|
+
let selectedCount = $state(0)
|
|
24
|
+
|
|
25
|
+
const columns: ColumnDef<typeof features, Order>[] = [
|
|
26
|
+
{ field: 'id', header: 'Order', width: 110 },
|
|
27
|
+
{ field: 'customer', header: 'Customer', editorType: 'text', width: 150 },
|
|
28
|
+
{ field: 'email', header: 'Email', editorType: 'text', width: 190 },
|
|
29
|
+
{ field: 'product', header: 'Product', editorType: 'text', width: 150 },
|
|
30
|
+
{ field: 'status', header: 'Status', width: 110 },
|
|
31
|
+
{
|
|
32
|
+
field: 'quantity',
|
|
33
|
+
header: 'Qty',
|
|
34
|
+
editorType: 'number',
|
|
35
|
+
width: 80,
|
|
36
|
+
align: 'right',
|
|
37
|
+
format: { type: 'number', options: { maximumFractionDigits: 0 } },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
field: 'total',
|
|
41
|
+
header: 'Total',
|
|
42
|
+
width: 120,
|
|
43
|
+
align: 'right',
|
|
44
|
+
format: { type: 'currency', currency: 'USD' },
|
|
45
|
+
},
|
|
46
|
+
{ field: 'country', header: 'Country', width: 90 },
|
|
47
|
+
{ field: 'date', header: 'Date', width: 120, format: { type: 'date', pattern: 'y-m-d' } },
|
|
48
|
+
]
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<svelte:head>
|
|
52
|
+
<title>Orders · SvGrid Admin</title>
|
|
53
|
+
</svelte:head>
|
|
54
|
+
|
|
55
|
+
<section class="flex h-full flex-col gap-3">
|
|
56
|
+
<div class="flex items-center justify-between">
|
|
57
|
+
<p class="text-sm" style="color: var(--app-muted);">
|
|
58
|
+
{rows.length} orders · sort, filter, select, edit inline, paginate.
|
|
59
|
+
{#if selectedCount > 0}
|
|
60
|
+
<span class="font-medium" style="color: var(--app-accent);">{selectedCount} selected</span>
|
|
61
|
+
{/if}
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="min-h-0 flex-1 rounded-xl border bg-white p-2 shadow-sm" style="border-color: var(--app-border);">
|
|
66
|
+
{#if browser}
|
|
67
|
+
<SvGrid
|
|
68
|
+
data={rows}
|
|
69
|
+
{columns}
|
|
70
|
+
{features}
|
|
71
|
+
filterMode="menu"
|
|
72
|
+
selectionMode="row"
|
|
73
|
+
showRowSelection={true}
|
|
74
|
+
showRowNumbers={true}
|
|
75
|
+
showPagination={true}
|
|
76
|
+
pageSize={25}
|
|
77
|
+
enableInlineEditing={true}
|
|
78
|
+
rowHeight={38}
|
|
79
|
+
containerHeight="100%"
|
|
80
|
+
fitColumns={true}
|
|
81
|
+
getRowId={(o: Order) => o.id}
|
|
82
|
+
onApiReady={(next) => (api = next)}
|
|
83
|
+
onRowSelectionChange={(_e, sel) => (selectedCount = sel.length)}
|
|
84
|
+
/>
|
|
85
|
+
{:else}
|
|
86
|
+
<p class="p-4 text-sm" style="color: var(--app-muted);">Loading {rows.length} orders…</p>
|
|
87
|
+
{/if}
|
|
88
|
+
</div>
|
|
89
|
+
</section>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
|
+
<rect width="32" height="32" rx="7" fill="#4f46e5" />
|
|
3
|
+
<g fill="#fff">
|
|
4
|
+
<rect x="6" y="7" width="20" height="4" rx="1.2" />
|
|
5
|
+
<rect x="6" y="14" width="9" height="4" rx="1.2" />
|
|
6
|
+
<rect x="17" y="14" width="9" height="4" rx="1.2" opacity="0.7" />
|
|
7
|
+
<rect x="6" y="21" width="9" height="4" rx="1.2" opacity="0.7" />
|
|
8
|
+
<rect x="17" y="21" width="9" height="4" rx="1.2" />
|
|
9
|
+
</g>
|
|
10
|
+
</svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import adapter from '@sveltejs/adapter-vercel'
|
|
2
|
+
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
|
3
|
+
|
|
4
|
+
/** @type {import('@sveltejs/kit').Config} */
|
|
5
|
+
const config = {
|
|
6
|
+
preprocess: vitePreprocess(),
|
|
7
|
+
kit: {
|
|
8
|
+
// adapter-vercel ships zero-config to Vercel. Swap for adapter-auto,
|
|
9
|
+
// adapter-static, adapter-node, etc. if you deploy elsewhere.
|
|
10
|
+
adapter: adapter(),
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default config
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./.svelte-kit/tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"allowJs": true,
|
|
5
|
+
"checkJs": true,
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"forceConsistentCasingInFileNames": true,
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"moduleResolution": "bundler"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# SvGrid app
|
|
2
|
+
|
|
3
|
+
A minimal [Vite](https://vite.dev) + [Svelte 5](https://svelte.dev) app wired
|
|
4
|
+
to [SvGrid](https://www.svgrid.com), the Svelte data grid.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install
|
|
8
|
+
npm run dev # http://localhost:5173
|
|
9
|
+
npm run build
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Open `src/App.svelte` and edit the `rows` / `columns` to use your own data.
|
|
13
|
+
|
|
14
|
+
## Next steps
|
|
15
|
+
|
|
16
|
+
- Add more features: `rowExpandingFeature`, `columnGroupingFeature`, pagination, pinned rows. See the [docs](https://www.svgrid.com/docs).
|
|
17
|
+
- Need Excel/PDF export, import, print, pivot, or AI helpers? Add [`@svgrid/enterprise`](https://www.svgrid.com/pricing).
|
|
18
|
+
- Want a full app shell with routing? Scaffold the admin starter instead:
|
|
19
|
+
`npm create sv-grid@latest my-admin -- --template admin-dashboard`
|
|
20
|
+
|
|
21
|
+
SvGrid(TM) is a trademark of jQWidgets Ltd. This template is MIT-licensed.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sv-grid-app",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
13
|
+
"svelte": "^5.55.5",
|
|
14
|
+
"vite": "^8.0.10"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@svgrid/grid": "^1.0.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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>SvGrid app</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app"></div>
|
|
10
|
+
<script type="module" src="/src/main.js"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import {
|
|
3
|
+
SvGrid,
|
|
4
|
+
tableFeatures,
|
|
5
|
+
rowSortingFeature,
|
|
6
|
+
columnFilteringFeature,
|
|
7
|
+
rowSelectionFeature,
|
|
8
|
+
} from '@svgrid/grid'
|
|
9
|
+
|
|
10
|
+
const features = tableFeatures({
|
|
11
|
+
rowSortingFeature,
|
|
12
|
+
columnFilteringFeature,
|
|
13
|
+
rowSelectionFeature,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
// Your data. Swap for a fetch() in onMount, a store, or props.
|
|
17
|
+
let rows = $state([
|
|
18
|
+
{ id: 1, name: 'Ada Lovelace', team: 'Engineering', salary: 145000, active: true },
|
|
19
|
+
{ id: 2, name: 'Alan Turing', team: 'Research', salary: 160000, active: true },
|
|
20
|
+
{ id: 3, name: 'Grace Hopper', team: 'Engineering', salary: 152000, active: false },
|
|
21
|
+
{ id: 4, name: 'Katherine Johnson', team: 'Data', salary: 138000, active: true },
|
|
22
|
+
{ id: 5, name: 'Edsger Dijkstra', team: 'Research', salary: 149000, active: false },
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
const columns = [
|
|
26
|
+
{ field: 'name', header: 'Name', editorType: 'text', width: 200 },
|
|
27
|
+
{ field: 'team', header: 'Team', editorType: 'text', width: 150 },
|
|
28
|
+
{
|
|
29
|
+
field: 'salary',
|
|
30
|
+
header: 'Salary',
|
|
31
|
+
width: 130,
|
|
32
|
+
align: 'right',
|
|
33
|
+
format: { type: 'currency', currency: 'USD' },
|
|
34
|
+
},
|
|
35
|
+
{ field: 'active', header: 'Active', editorType: 'checkbox', width: 90 },
|
|
36
|
+
]
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<main style="max-width: 720px; margin: 3rem auto; font-family: system-ui, sans-serif;">
|
|
40
|
+
<h1 style="font-size: 1.4rem;">SvGrid</h1>
|
|
41
|
+
<p style="color:#64748b;">
|
|
42
|
+
Sort, filter, select, and double-click a cell to edit. Edit
|
|
43
|
+
<code>src/App.svelte</code> to make it yours.
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
<div style="height: 320px;">
|
|
47
|
+
<SvGrid
|
|
48
|
+
data={rows}
|
|
49
|
+
{columns}
|
|
50
|
+
{features}
|
|
51
|
+
filterMode="menu"
|
|
52
|
+
selectionMode="row"
|
|
53
|
+
showRowSelection={true}
|
|
54
|
+
showRowNumbers={true}
|
|
55
|
+
enableInlineEditing={true}
|
|
56
|
+
rowHeight={38}
|
|
57
|
+
containerHeight="100%"
|
|
58
|
+
fitColumns={true}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
</main>
|