@svgrid/create 2.4.0 → 2.6.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 +8 -5
- package/index.mjs +67 -4
- package/package.json +2 -2
- package/templates/admin-dashboard/src/app.css +184 -180
- package/templates/admin-dashboard/src/routes/+layout.svelte +3 -3
- package/templates/sveltekit/README.md +44 -0
- package/templates/sveltekit/_gitignore +23 -0
- package/templates/sveltekit/_package.json +26 -0
- package/templates/sveltekit/src/app.css +15 -0
- package/templates/sveltekit/src/app.d.ts +13 -0
- package/templates/sveltekit/src/app.html +22 -0
- package/templates/sveltekit/src/lib/people.ts +22 -0
- package/templates/sveltekit/src/lib/theme.svelte.ts +87 -0
- package/templates/sveltekit/src/routes/+layout.svelte +78 -0
- package/templates/sveltekit/src/routes/+page.svelte +7 -0
- package/templates/sveltekit/src/routes/people/+page.server.ts +16 -0
- package/templates/sveltekit/src/routes/people/+page.svelte +51 -0
- package/templates/sveltekit/tsconfig.json +20 -0
- package/templates/sveltekit/vite.config.ts +20 -0
package/README.md
CHANGED
|
@@ -43,19 +43,20 @@ pnpm create @svgrid my-app -t minimal
|
|
|
43
43
|
| Template | Stack | Best for |
|
|
44
44
|
| --- | --- | --- |
|
|
45
45
|
| `minimal` | Vite + Svelte 5 + SvGrid | Dropping a grid into something quickly |
|
|
46
|
+
| `sveltekit` | SvelteKit, server load + form actions | Learning how a grid fits SvelteKit, or starting one |
|
|
46
47
|
| `admin-dashboard` | SvelteKit + Tailwind + SvGrid, deploy to Vercel | A real dashboard / internal tool |
|
|
47
48
|
|
|
48
49
|
## Options
|
|
49
50
|
|
|
50
51
|
| Flag | Description |
|
|
51
52
|
| --- | --- |
|
|
52
|
-
| `--template`, `-t` | `minimal` or `admin-dashboard` |
|
|
53
|
+
| `--template`, `-t` | `minimal`, `sveltekit` or `admin-dashboard` |
|
|
53
54
|
| `--theme <id>` | One of `@svgrid/grid`'s 20 built-in presets - shadcn, Tailwind, Material, Excel, Fluent, and more (default: `tailwind`) |
|
|
54
|
-
| `--dark` / `--light` |
|
|
55
|
+
| `--dark` / `--light` | Pin the starting mode. `minimal` and `sveltekit` follow the visitor's OS when neither is given |
|
|
55
56
|
| `--force`, `-f` | Scaffold into a non-empty directory |
|
|
56
57
|
| `--help`, `-h` | Show usage |
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
Every template prompts for a theme and a light/dark mode when run
|
|
59
60
|
interactively, or takes them as flags:
|
|
60
61
|
|
|
61
62
|
```bash
|
|
@@ -63,8 +64,10 @@ npm create @svgrid@latest my-app -- -t admin-dashboard --theme material --light
|
|
|
63
64
|
npm create @svgrid@latest my-app -- -t minimal --theme nord --dark
|
|
64
65
|
```
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
something you are stuck with.
|
|
67
|
+
All three ship a working toggle, so the choice is a starting point rather than
|
|
68
|
+
something you are stuck with. `sveltekit` goes further and puts all 20 presets
|
|
69
|
+
in a picker in the header, so you can try them against your own data without
|
|
70
|
+
rescaffolding.
|
|
68
71
|
|
|
69
72
|
Then:
|
|
70
73
|
|
package/index.mjs
CHANGED
|
@@ -23,6 +23,10 @@ const TEMPLATES = {
|
|
|
23
23
|
label: 'Minimal - Vite + Svelte 5 + SvGrid, one page',
|
|
24
24
|
bundled: join(__dirname, 'templates', 'minimal'),
|
|
25
25
|
},
|
|
26
|
+
sveltekit: {
|
|
27
|
+
label: 'SvelteKit - server load, URL-driven sort, form-action edits, theme picker',
|
|
28
|
+
bundled: join(__dirname, 'templates', 'sveltekit'),
|
|
29
|
+
},
|
|
26
30
|
'admin-dashboard': {
|
|
27
31
|
label: 'Admin dashboard - SvelteKit shell, multiple grids, deploy to Vercel',
|
|
28
32
|
bundled: join(__dirname, 'templates', 'admin-dashboard'),
|
|
@@ -81,7 +85,8 @@ ${Object.entries(TEMPLATES)
|
|
|
81
85
|
|
|
82
86
|
${color('bold', 'Options')}
|
|
83
87
|
--theme <id> One of: ${THEME_IDS.join(', ')} (default: tailwind).
|
|
84
|
-
--dark / --light
|
|
88
|
+
--dark / --light Pin the starting mode. Left out, minimal and sveltekit
|
|
89
|
+
follow the visitor's OS; admin-dashboard starts dark.
|
|
85
90
|
--force Scaffold into a non-empty directory.
|
|
86
91
|
|
|
87
92
|
${color('bold', 'Examples')}
|
|
@@ -175,6 +180,62 @@ async function applyTheme(destDir, choice) {
|
|
|
175
180
|
if (marked.test(cssText)) {
|
|
176
181
|
await writeFile(cssPath, cssText.replace(marked, `/* svgrid-theme:start */\n${css}\n/* svgrid-theme:end */`))
|
|
177
182
|
}
|
|
183
|
+
|
|
184
|
+
// Templates with a RUNTIME theme picker keep the starting selection in TS, so
|
|
185
|
+
// the picker opens on the theme that was scaffolded rather than disagreeing
|
|
186
|
+
// with the stylesheet above. Absent in templates without a picker, where the
|
|
187
|
+
// missing file makes this a no-op.
|
|
188
|
+
const tsPath = join(destDir, 'src', 'lib', 'theme.svelte.ts')
|
|
189
|
+
const tsText = await readFile(tsPath, 'utf8').catch(() => null)
|
|
190
|
+
if (tsText == null) return
|
|
191
|
+
const tsMarked = /\/\* svgrid-initial-theme:start \*\/([\s\S]*?)\/\* svgrid-initial-theme:end \*\//
|
|
192
|
+
if (!tsMarked.test(tsText)) return
|
|
193
|
+
// Only the preset here. The mode is left to applyModeSvelteKit, which pins it
|
|
194
|
+
// only when one was actually asked for.
|
|
195
|
+
const body = tsText.match(tsMarked)[1].replace(
|
|
196
|
+
/export const INITIAL_THEME = '[^']*'/,
|
|
197
|
+
`export const INITIAL_THEME = '${preset.id}'`,
|
|
198
|
+
)
|
|
199
|
+
await writeFile(
|
|
200
|
+
tsPath,
|
|
201
|
+
tsText.replace(
|
|
202
|
+
tsMarked,
|
|
203
|
+
`/* svgrid-initial-theme:start */${body}/* svgrid-initial-theme:end */`,
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** sveltekit settles its start mode in an inline script in `src/app.html`,
|
|
209
|
+
* falling back to the OS preference, exactly as minimal does. Pin it only when
|
|
210
|
+
* light or dark was actually asked for. The picker in the layout reads the
|
|
211
|
+
* attribute that script sets, so both halves have to agree. */
|
|
212
|
+
async function applyModeSvelteKit(destDir, choice) {
|
|
213
|
+
if (!choice || !choice.explicitMode) return
|
|
214
|
+
|
|
215
|
+
const htmlPath = join(destDir, 'src', 'app.html')
|
|
216
|
+
const html = await readFile(htmlPath, 'utf8').catch(() => null)
|
|
217
|
+
if (html != null) {
|
|
218
|
+
await writeFile(
|
|
219
|
+
htmlPath,
|
|
220
|
+
html
|
|
221
|
+
.replace(`<html lang="en" data-theme="light">`, `<html lang="en" data-theme="${choice.mode}">`)
|
|
222
|
+
.replace(
|
|
223
|
+
`var fallback = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'`,
|
|
224
|
+
`var fallback = '${choice.mode}'`,
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const tsPath = join(destDir, 'src', 'lib', 'theme.svelte.ts')
|
|
230
|
+
const ts = await readFile(tsPath, 'utf8').catch(() => null)
|
|
231
|
+
if (ts == null) return
|
|
232
|
+
await writeFile(
|
|
233
|
+
tsPath,
|
|
234
|
+
ts.replace(
|
|
235
|
+
/export const INITIAL_MODE: ThemeMode = '[^']*'/,
|
|
236
|
+
`export const INITIAL_MODE: ThemeMode = '${choice.mode}'`,
|
|
237
|
+
),
|
|
238
|
+
)
|
|
178
239
|
}
|
|
179
240
|
|
|
180
241
|
/** admin-dashboard defaults to dark (see app.html / src/lib/theme.ts /
|
|
@@ -330,7 +391,7 @@ async function main() {
|
|
|
330
391
|
Object.entries(TEMPLATES).forEach(([, t], i) => {
|
|
331
392
|
stdout.write(` ${color('cyan', String(i + 1))}. ${t.label}\n`)
|
|
332
393
|
})
|
|
333
|
-
const pick = await ask(
|
|
394
|
+
const pick = await ask(`\nChoose a template (1-${Object.keys(TEMPLATES).length}):`, '1')
|
|
334
395
|
template = Object.keys(TEMPLATES)[Number(pick) - 1] ?? 'minimal'
|
|
335
396
|
} else {
|
|
336
397
|
template = 'minimal'
|
|
@@ -378,13 +439,15 @@ async function main() {
|
|
|
378
439
|
await applyTheme(destDir, themeChoice)
|
|
379
440
|
await applyMode(destDir, themeChoice)
|
|
380
441
|
await applyModeMinimal(destDir, themeChoice)
|
|
442
|
+
await applyModeSvelteKit(destDir, themeChoice)
|
|
381
443
|
|
|
382
444
|
// 5. Next steps.
|
|
383
445
|
const rel = isAbsolute(target) || target.startsWith('.') ? target : `./${target}`
|
|
384
446
|
stdout.write(`\n${color('green', '✔')} Scaffolded ${color('bold', projectName)} (${template}) into ${rel}\n`)
|
|
385
447
|
if (themeChoice) {
|
|
386
|
-
// minimal only
|
|
387
|
-
// OS preference at load, so reporting "dark" here would be a
|
|
448
|
+
// minimal and sveltekit only pin a mode when one was asked for; otherwise
|
|
449
|
+
// they read the OS preference at load, so reporting "dark" here would be a
|
|
450
|
+
// lie. admin-dashboard is dark by default either way.
|
|
388
451
|
const pinned = themeChoice.explicitMode || template === 'admin-dashboard'
|
|
389
452
|
const mode = pinned ? themeChoice.mode : 'follows your OS'
|
|
390
453
|
stdout.write(` ${color('dim', 'theme')} ${themeChoice.name} (${mode})\n`)
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"type": "commercial",
|
|
5
5
|
"url": "https://svgrid.com/pricing"
|
|
6
6
|
},
|
|
7
|
-
"version": "2.
|
|
7
|
+
"version": "2.6.0",
|
|
8
8
|
"description": "Scaffold a Svelte app powered by SvGrid in one command: npm create @svgrid@latest",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"license": "MIT",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"node": ">=18"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@svgrid/grid": "^2.
|
|
39
|
+
"@svgrid/grid": "^2.6.9"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"sync-templates": "node sync-templates.mjs"
|
|
@@ -1,180 +1,184 @@
|
|
|
1
|
-
@import 'tailwindcss';
|
|
2
|
-
|
|
3
|
-
/* Tailwind's `dark:` modifier follows the `data-theme` attribute the layout
|
|
4
|
-
* writes onto <html>, so utility classes and the grid's own --sg-* tokens
|
|
5
|
-
* switch together. */
|
|
6
|
-
@custom-variant dark (&:where(html[data-theme='dark'], html[data-theme='dark'] *));
|
|
7
|
-
|
|
8
|
-
/* ---------------------------------------------------------------------------
|
|
9
|
-
* SvGrid theme tokens - one of @svgrid/grid's 20 built-in presets (pick one
|
|
10
|
-
* with `npm create @svgrid@latest -- --theme <id> [--dark]`, or edit the
|
|
11
|
-
* values below by hand). The dashboard shell reads them through the --app-*
|
|
12
|
-
* aliases further down, so the whole app themes as one, not just the grid.
|
|
13
|
-
* ------------------------------------------------------------------------- */
|
|
14
|
-
/* svgrid-theme:start */
|
|
15
|
-
:root {
|
|
16
|
-
--sg-bg: #ffffff;
|
|
17
|
-
--sg-fg: #0f172a;
|
|
18
|
-
--sg-muted: #64748b;
|
|
19
|
-
--sg-border: #e2e8f0;
|
|
20
|
-
--sg-header-bg: #f8fafc;
|
|
21
|
-
--sg-header-fg: #0f172a;
|
|
22
|
-
--sg-bg-subtle: #f8fafc;
|
|
23
|
-
--sg-row-alt-bg: #ffffff;
|
|
24
|
-
--sg-row-hover-bg: #f1f5f9;
|
|
25
|
-
--sg-selection-bg: #e0e7ff;
|
|
26
|
-
--sg-input-bg: #ffffff;
|
|
27
|
-
--sg-input-border: #e2e8f0;
|
|
28
|
-
--sg-accent: #4f46e5;
|
|
29
|
-
--sg-on-accent: #ffffff;
|
|
30
|
-
--sg-radius: 6px;
|
|
31
|
-
--sg-font: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
32
|
-
--sg-scrollbar-bg: #f8fafc;
|
|
33
|
-
--sg-scrollbar-border: #e2e8f0;
|
|
34
|
-
--sg-scrollbar-thumb: #64748b;
|
|
35
|
-
--sg-scrollbar-thumb-hover: #0f172a;
|
|
36
|
-
--sg-scrollbar-thumb-active: #0f172a;
|
|
37
|
-
--sg-scrollbar-arrow: #64748b;
|
|
38
|
-
--sg-scrollbar-arrow-hover: #0f172a;
|
|
39
|
-
--sg-scrollbar-arrow-hover-bg: #f1f5f9;
|
|
40
|
-
--sg-scrollbar-arrow-active: #0f172a;
|
|
41
|
-
--sg-scrollbar-arrow-active-bg: #e0e7ff;
|
|
42
|
-
--sg-scrollbar-arrow-disabled: #e2e8f0;
|
|
43
|
-
color-scheme: light;
|
|
44
|
-
}
|
|
45
|
-
:root[data-theme='dark'] {
|
|
46
|
-
--sg-bg: #0f172a;
|
|
47
|
-
--sg-fg: #f8fafc;
|
|
48
|
-
--sg-muted: #94a3b8;
|
|
49
|
-
--sg-border: #334155;
|
|
50
|
-
--sg-header-bg: #1e293b;
|
|
51
|
-
--sg-header-fg: #f8fafc;
|
|
52
|
-
--sg-bg-subtle: #1e293b;
|
|
53
|
-
--sg-row-alt-bg: #0f172a;
|
|
54
|
-
--sg-row-hover-bg: #1e293b;
|
|
55
|
-
--sg-selection-bg: #312e81;
|
|
56
|
-
--sg-input-bg: #0f172a;
|
|
57
|
-
--sg-input-border: #334155;
|
|
58
|
-
--sg-accent: #818cf8;
|
|
59
|
-
--sg-on-accent: #18181b;
|
|
60
|
-
--sg-radius: 6px;
|
|
61
|
-
--sg-font: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
62
|
-
--sg-scrollbar-bg: #1e293b;
|
|
63
|
-
--sg-scrollbar-border: #334155;
|
|
64
|
-
--sg-scrollbar-thumb: #94a3b8;
|
|
65
|
-
--sg-scrollbar-thumb-hover: #f8fafc;
|
|
66
|
-
--sg-scrollbar-thumb-active: #f8fafc;
|
|
67
|
-
--sg-scrollbar-arrow: #94a3b8;
|
|
68
|
-
--sg-scrollbar-arrow-hover: #f8fafc;
|
|
69
|
-
--sg-scrollbar-arrow-hover-bg: #1e293b;
|
|
70
|
-
--sg-scrollbar-arrow-active: #f8fafc;
|
|
71
|
-
--sg-scrollbar-arrow-active-bg: #312e81;
|
|
72
|
-
--sg-scrollbar-arrow-disabled: #334155;
|
|
73
|
-
color-scheme: dark;
|
|
74
|
-
}
|
|
75
|
-
/* svgrid-theme:end */
|
|
76
|
-
|
|
77
|
-
/* Dashboard shell aliases - map the chrome to the grid tokens above, so any
|
|
78
|
-
* theme (light or dark, whichever preset) reskins the whole page as one. */
|
|
79
|
-
:root {
|
|
80
|
-
--app-bg: var(--sg-row-alt-bg);
|
|
81
|
-
--app-panel: var(--sg-bg);
|
|
82
|
-
--app-border: var(--sg-border);
|
|
83
|
-
--app-fg: var(--sg-fg);
|
|
84
|
-
--app-muted: var(--sg-muted);
|
|
85
|
-
--app-accent: var(--sg-accent);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
*
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
.sv-grid-
|
|
110
|
-
|
|
111
|
-
border-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.sv-grid-table tbody tr
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
background:
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
.sv-grid-pagination-btn:disabled {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
/* Tailwind's `dark:` modifier follows the `data-theme` attribute the layout
|
|
4
|
+
* writes onto <html>, so utility classes and the grid's own --sg-* tokens
|
|
5
|
+
* switch together. */
|
|
6
|
+
@custom-variant dark (&:where(html[data-theme='dark'], html[data-theme='dark'] *));
|
|
7
|
+
|
|
8
|
+
/* ---------------------------------------------------------------------------
|
|
9
|
+
* SvGrid theme tokens - one of @svgrid/grid's 20 built-in presets (pick one
|
|
10
|
+
* with `npm create @svgrid@latest -- --theme <id> [--dark]`, or edit the
|
|
11
|
+
* values below by hand). The dashboard shell reads them through the --app-*
|
|
12
|
+
* aliases further down, so the whole app themes as one, not just the grid.
|
|
13
|
+
* ------------------------------------------------------------------------- */
|
|
14
|
+
/* svgrid-theme:start */
|
|
15
|
+
:root {
|
|
16
|
+
--sg-bg: #ffffff;
|
|
17
|
+
--sg-fg: #0f172a;
|
|
18
|
+
--sg-muted: #64748b;
|
|
19
|
+
--sg-border: #e2e8f0;
|
|
20
|
+
--sg-header-bg: #f8fafc;
|
|
21
|
+
--sg-header-fg: #0f172a;
|
|
22
|
+
--sg-bg-subtle: #f8fafc;
|
|
23
|
+
--sg-row-alt-bg: #ffffff;
|
|
24
|
+
--sg-row-hover-bg: #f1f5f9;
|
|
25
|
+
--sg-selection-bg: #e0e7ff;
|
|
26
|
+
--sg-input-bg: #ffffff;
|
|
27
|
+
--sg-input-border: #e2e8f0;
|
|
28
|
+
--sg-accent: #4f46e5;
|
|
29
|
+
--sg-on-accent: #ffffff;
|
|
30
|
+
--sg-radius: 6px;
|
|
31
|
+
--sg-font: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
32
|
+
--sg-scrollbar-bg: #f8fafc;
|
|
33
|
+
--sg-scrollbar-border: #e2e8f0;
|
|
34
|
+
--sg-scrollbar-thumb: #64748b;
|
|
35
|
+
--sg-scrollbar-thumb-hover: #0f172a;
|
|
36
|
+
--sg-scrollbar-thumb-active: #0f172a;
|
|
37
|
+
--sg-scrollbar-arrow: #64748b;
|
|
38
|
+
--sg-scrollbar-arrow-hover: #0f172a;
|
|
39
|
+
--sg-scrollbar-arrow-hover-bg: #f1f5f9;
|
|
40
|
+
--sg-scrollbar-arrow-active: #0f172a;
|
|
41
|
+
--sg-scrollbar-arrow-active-bg: #e0e7ff;
|
|
42
|
+
--sg-scrollbar-arrow-disabled: #e2e8f0;
|
|
43
|
+
color-scheme: light;
|
|
44
|
+
}
|
|
45
|
+
:root[data-theme='dark'] {
|
|
46
|
+
--sg-bg: #0f172a;
|
|
47
|
+
--sg-fg: #f8fafc;
|
|
48
|
+
--sg-muted: #94a3b8;
|
|
49
|
+
--sg-border: #334155;
|
|
50
|
+
--sg-header-bg: #1e293b;
|
|
51
|
+
--sg-header-fg: #f8fafc;
|
|
52
|
+
--sg-bg-subtle: #1e293b;
|
|
53
|
+
--sg-row-alt-bg: #0f172a;
|
|
54
|
+
--sg-row-hover-bg: #1e293b;
|
|
55
|
+
--sg-selection-bg: #312e81;
|
|
56
|
+
--sg-input-bg: #0f172a;
|
|
57
|
+
--sg-input-border: #334155;
|
|
58
|
+
--sg-accent: #818cf8;
|
|
59
|
+
--sg-on-accent: #18181b;
|
|
60
|
+
--sg-radius: 6px;
|
|
61
|
+
--sg-font: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
62
|
+
--sg-scrollbar-bg: #1e293b;
|
|
63
|
+
--sg-scrollbar-border: #334155;
|
|
64
|
+
--sg-scrollbar-thumb: #94a3b8;
|
|
65
|
+
--sg-scrollbar-thumb-hover: #f8fafc;
|
|
66
|
+
--sg-scrollbar-thumb-active: #f8fafc;
|
|
67
|
+
--sg-scrollbar-arrow: #94a3b8;
|
|
68
|
+
--sg-scrollbar-arrow-hover: #f8fafc;
|
|
69
|
+
--sg-scrollbar-arrow-hover-bg: #1e293b;
|
|
70
|
+
--sg-scrollbar-arrow-active: #f8fafc;
|
|
71
|
+
--sg-scrollbar-arrow-active-bg: #312e81;
|
|
72
|
+
--sg-scrollbar-arrow-disabled: #334155;
|
|
73
|
+
color-scheme: dark;
|
|
74
|
+
}
|
|
75
|
+
/* svgrid-theme:end */
|
|
76
|
+
|
|
77
|
+
/* Dashboard shell aliases - map the chrome to the grid tokens above, so any
|
|
78
|
+
* theme (light or dark, whichever preset) reskins the whole page as one. */
|
|
79
|
+
:root {
|
|
80
|
+
--app-bg: var(--sg-row-alt-bg);
|
|
81
|
+
--app-panel: var(--sg-bg);
|
|
82
|
+
--app-border: var(--sg-border);
|
|
83
|
+
--app-fg: var(--sg-fg);
|
|
84
|
+
--app-muted: var(--sg-muted);
|
|
85
|
+
--app-accent: var(--sg-accent);
|
|
86
|
+
/* Text that sits ON the accent. Every preset declares this, and it is NOT
|
|
87
|
+
always white: shadcn dark uses a near-white accent (#fafafa) with near-black
|
|
88
|
+
text. Hardcoding #fff here made the active nav item invisible. */
|
|
89
|
+
--app-on-accent: var(--sg-on-accent);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
html,
|
|
93
|
+
body {
|
|
94
|
+
background: var(--app-bg);
|
|
95
|
+
color: var(--app-fg);
|
|
96
|
+
font-family: var(--sg-font, Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial,
|
|
97
|
+
sans-serif);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ---------------------------------------------------------------------------
|
|
101
|
+
* Optional polish. The grid themes itself from the --sg-* tokens above, so it
|
|
102
|
+
* already follows light / dark with no extra CSS. These rules only add table
|
|
103
|
+
* niceties the grid leaves to the app: cell gridlines, zebra striping, row
|
|
104
|
+
* hover, and a styled pagination bar. Delete any you don't want.
|
|
105
|
+
* ------------------------------------------------------------------------- */
|
|
106
|
+
|
|
107
|
+
/* Cell gridlines (the grid draws none by default). */
|
|
108
|
+
.sv-grid-column,
|
|
109
|
+
.sv-grid-cell {
|
|
110
|
+
border-right: 1px solid var(--sg-border) !important;
|
|
111
|
+
border-bottom: 1px solid var(--sg-border) !important;
|
|
112
|
+
}
|
|
113
|
+
.sv-grid-table tr > :last-child.sv-grid-column,
|
|
114
|
+
.sv-grid-table tr > :last-child.sv-grid-cell {
|
|
115
|
+
border-right: 0 !important;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Zebra striping + row hover. */
|
|
119
|
+
.sv-grid-table tbody tr:nth-child(even) .sv-grid-cell {
|
|
120
|
+
background: var(--sg-row-alt-bg) !important;
|
|
121
|
+
}
|
|
122
|
+
.sv-grid-table tbody tr:hover .sv-grid-cell {
|
|
123
|
+
background: var(--sg-row-hover-bg) !important;
|
|
124
|
+
}
|
|
125
|
+
/* Selection wins over zebra (declared after, same specificity). */
|
|
126
|
+
.sv-grid-table tbody tr.sv-grid-row-selected .sv-grid-cell,
|
|
127
|
+
.sv-grid-table tbody tr .sv-grid-cell[data-selected-range='true'] {
|
|
128
|
+
background: var(--sg-selection-bg) !important;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Pagination bar: layout + theme (the grid ships the markup, not the styling). */
|
|
132
|
+
.sv-grid-pagination {
|
|
133
|
+
display: flex !important;
|
|
134
|
+
align-items: center !important;
|
|
135
|
+
justify-content: flex-end !important;
|
|
136
|
+
gap: 24px !important;
|
|
137
|
+
padding: 12px 16px !important;
|
|
138
|
+
border: 1px solid var(--sg-border) !important;
|
|
139
|
+
border-top: 0 !important;
|
|
140
|
+
border-radius: 0 0 6px 6px !important;
|
|
141
|
+
background: var(--sg-header-bg) !important;
|
|
142
|
+
color: var(--sg-fg) !important;
|
|
143
|
+
font-size: 13px !important;
|
|
144
|
+
}
|
|
145
|
+
.sv-grid-pagination-pagesize {
|
|
146
|
+
display: inline-flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
gap: 8px;
|
|
149
|
+
color: var(--sg-muted);
|
|
150
|
+
}
|
|
151
|
+
.sv-grid-pagination-pagesize select,
|
|
152
|
+
.sv-grid-pagination-btn {
|
|
153
|
+
border: 1px solid var(--sg-input-border);
|
|
154
|
+
background: var(--sg-input-bg);
|
|
155
|
+
color: var(--sg-fg);
|
|
156
|
+
border-radius: 5px;
|
|
157
|
+
height: 28px;
|
|
158
|
+
font: inherit;
|
|
159
|
+
font-size: 13px;
|
|
160
|
+
cursor: pointer;
|
|
161
|
+
}
|
|
162
|
+
.sv-grid-pagination-btn {
|
|
163
|
+
display: inline-flex !important;
|
|
164
|
+
align-items: center !important;
|
|
165
|
+
justify-content: center !important;
|
|
166
|
+
width: 28px !important;
|
|
167
|
+
border: 0 !important;
|
|
168
|
+
background: transparent !important;
|
|
169
|
+
font-size: 16px !important;
|
|
170
|
+
}
|
|
171
|
+
.sv-grid-pagination-btn:hover:not(:disabled) {
|
|
172
|
+
background: var(--sg-input-bg) !important;
|
|
173
|
+
color: var(--sg-accent) !important;
|
|
174
|
+
}
|
|
175
|
+
.sv-grid-pagination-btn:disabled {
|
|
176
|
+
color: var(--sg-muted) !important;
|
|
177
|
+
opacity: 0.4 !important;
|
|
178
|
+
cursor: default !important;
|
|
179
|
+
}
|
|
180
|
+
.sv-grid-pagination-nav {
|
|
181
|
+
display: inline-flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
gap: 4px;
|
|
184
|
+
}
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
>
|
|
37
37
|
<div class="flex items-center gap-2 px-5 py-4">
|
|
38
38
|
<span
|
|
39
|
-
class="grid h-8 w-8 place-items-center rounded-lg font-bold
|
|
40
|
-
style="background: var(--app-accent);">S</span
|
|
39
|
+
class="grid h-8 w-8 place-items-center rounded-lg font-bold"
|
|
40
|
+
style="background: var(--app-accent); color: var(--app-on-accent);">S</span
|
|
41
41
|
>
|
|
42
42
|
<div class="leading-tight">
|
|
43
43
|
<p class="text-sm font-bold" style="color: var(--app-fg);">SvGrid Admin</p>
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
href={item.href}
|
|
52
52
|
class="mb-1 flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors"
|
|
53
53
|
style={isActive(item.href)
|
|
54
|
-
? 'background: var(--app-accent); color:
|
|
54
|
+
? 'background: var(--app-accent); color: var(--app-on-accent);'
|
|
55
55
|
: 'color: var(--app-fg);'}
|
|
56
56
|
>
|
|
57
57
|
<span class="w-5 text-center">{item.icon}</span>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# SvGrid + SvelteKit sample
|
|
2
|
+
|
|
3
|
+
A grid whose rows are loaded on the server, sorted from the URL, and edited
|
|
4
|
+
through a form action - the three things that are different about running a grid
|
|
5
|
+
in SvelteKit rather than a plain Vite SPA.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev # http://localhost:5173/people
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## What to try
|
|
13
|
+
|
|
14
|
+
1. **Click the `Year` header.** The URL becomes `?sort=year&dir=asc`. Copy that
|
|
15
|
+
link into a new tab - it opens already sorted, because the server did it.
|
|
16
|
+
2. **Double-click a name, change it, press Enter, then reload.** The edit went
|
|
17
|
+
through the form action in `+page.server.ts` and survived.
|
|
18
|
+
3. **Switch the theme** with the picker in the header. All 20 built-in presets,
|
|
19
|
+
light and dark, applied live.
|
|
20
|
+
4. **`curl localhost:5173/people`.** The rows are in the HTML, not injected by
|
|
21
|
+
JS afterwards. That is what a crawler sees.
|
|
22
|
+
|
|
23
|
+
## Where things are
|
|
24
|
+
|
|
25
|
+
| File | Does |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `src/lib/people.ts` | Stands in for your database. Swap for real queries. |
|
|
28
|
+
| `src/routes/people/+page.server.ts` | `load` sorts from the query string; the `rename` action takes the edit. |
|
|
29
|
+
| `src/routes/people/+page.svelte` | The grid. `externalSort` because the server owns the ordering. |
|
|
30
|
+
| `src/lib/theme.svelte.ts` | Runtime theme switching via `resolveThemeTokens`. |
|
|
31
|
+
| `src/app.css` | Imports one preset so the first paint is themed before JS runs. |
|
|
32
|
+
|
|
33
|
+
## Themes
|
|
34
|
+
|
|
35
|
+
Pick a starting theme when you scaffold:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm create @svgrid@latest my-app -- --template sveltekit --theme dracula --dark
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or change it at runtime with the header picker. The picker writes the preset's
|
|
42
|
+
`--sg-*` custom properties onto `<html>`; nothing rebuilds.
|
|
43
|
+
|
|
44
|
+
Full guide: https://svgrid.com/docs/getting-started/sveltekit/
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
node_modules
|
|
2
|
+
|
|
3
|
+
# Output
|
|
4
|
+
.output
|
|
5
|
+
.vercel
|
|
6
|
+
.netlify
|
|
7
|
+
.wrangler
|
|
8
|
+
/.svelte-kit
|
|
9
|
+
/build
|
|
10
|
+
|
|
11
|
+
# OS
|
|
12
|
+
.DS_Store
|
|
13
|
+
Thumbs.db
|
|
14
|
+
|
|
15
|
+
# Env
|
|
16
|
+
.env
|
|
17
|
+
.env.*
|
|
18
|
+
!.env.example
|
|
19
|
+
!.env.test
|
|
20
|
+
|
|
21
|
+
# Vite
|
|
22
|
+
vite.config.js.timestamp-*
|
|
23
|
+
vite.config.ts.timestamp-*
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "svgrid-sveltekit-sample",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite dev",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"prepare": "svelte-kit sync || echo ''",
|
|
11
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
12
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@sveltejs/adapter-auto": "^7.0.1",
|
|
16
|
+
"@sveltejs/kit": "^2.63.0",
|
|
17
|
+
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
18
|
+
"svelte": "^5.56.1",
|
|
19
|
+
"svelte-check": "^4.6.0",
|
|
20
|
+
"typescript": "^6.0.3",
|
|
21
|
+
"vite": "^8.0.16"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@svgrid/grid": "^2.6.8"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* SvGrid theme. One of the 20 presets @svgrid/grid ships.
|
|
2
|
+
*
|
|
3
|
+
* Importing it as a stylesheet means the FIRST paint - and the server-rendered
|
|
4
|
+
* HTML - is already themed, before any JavaScript runs. The theme picker in the
|
|
5
|
+
* layout overrides these values at runtime by setting the same --sg-* custom
|
|
6
|
+
* properties on <html>.
|
|
7
|
+
*
|
|
8
|
+
* `npm create @svgrid@latest -- --theme <id>` rewrites the line between the
|
|
9
|
+
* markers, so pick a starting theme at scaffold time if you prefer. */
|
|
10
|
+
/* svgrid-theme:start */
|
|
11
|
+
@import '@svgrid/grid/themes/tailwind.css';
|
|
12
|
+
/* svgrid-theme:end */
|
|
13
|
+
|
|
14
|
+
* { box-sizing: border-box; }
|
|
15
|
+
body { margin: 0; }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// See https://svelte.dev/docs/kit/types#app.d.ts
|
|
2
|
+
// for information about these interfaces
|
|
3
|
+
declare global {
|
|
4
|
+
namespace App {
|
|
5
|
+
// interface Error {}
|
|
6
|
+
// interface Locals {}
|
|
7
|
+
// interface PageData {}
|
|
8
|
+
// interface PageState {}
|
|
9
|
+
// interface Platform {}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" data-theme="light">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="text-scale" content="scale" />
|
|
7
|
+
<script>
|
|
8
|
+
// Settle the theme before the first paint. Without this the server-rendered
|
|
9
|
+
// HTML shows one palette and the picker in +layout.svelte swaps it a frame
|
|
10
|
+
// later. With nothing saved yet we follow the OS.
|
|
11
|
+
try {
|
|
12
|
+
var saved = JSON.parse(localStorage.getItem('svgrid-theme') || 'null')
|
|
13
|
+
var fallback = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
14
|
+
document.documentElement.dataset.theme = (saved && saved.mode) || fallback
|
|
15
|
+
} catch (e) {}
|
|
16
|
+
</script>
|
|
17
|
+
%sveltekit.head%
|
|
18
|
+
</head>
|
|
19
|
+
<body data-sveltekit-preload-data="hover">
|
|
20
|
+
<div style="display: contents">%sveltekit.body%</div>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type Person = { id: number; name: string; role: string; year: number }
|
|
2
|
+
|
|
3
|
+
// Stands in for your database. Mutating a module-level array is fine for a
|
|
4
|
+
// tutorial; swap it for real queries and the rest of the page is unchanged.
|
|
5
|
+
const people: Person[] = [
|
|
6
|
+
{ id: 1, name: 'Ada Lovelace', role: 'Mathematician', year: 1843 },
|
|
7
|
+
{ id: 2, name: 'Grace Hopper', role: 'Rear Admiral', year: 1952 },
|
|
8
|
+
{ id: 3, name: 'Karen Sparck Jones', role: 'Computer Scientist', year: 1972 },
|
|
9
|
+
{ id: 4, name: 'Barbara Liskov', role: 'Computer Scientist', year: 1968 },
|
|
10
|
+
{ id: 5, name: 'Margaret Hamilton', role: 'Software Engineer', year: 1969 },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
export function listPeople(sortBy: keyof Person = 'name', desc = false): Person[] {
|
|
14
|
+
const rows = [...people]
|
|
15
|
+
rows.sort((a, b) => (a[sortBy] > b[sortBy] ? 1 : a[sortBy] < b[sortBy] ? -1 : 0))
|
|
16
|
+
return desc ? rows.reverse() : rows
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function renamePerson(id: number, name: string): void {
|
|
20
|
+
const row = people.find((p) => p.id === id)
|
|
21
|
+
if (row) row.name = name
|
|
22
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime theme switching.
|
|
3
|
+
*
|
|
4
|
+
* `@svgrid/grid/themes` ships 20 presets and a `resolveThemeTokens(preset, mode)`
|
|
5
|
+
* helper that returns the `--sg-*` custom properties for a given preset and
|
|
6
|
+
* light/dark mode. Writing those onto `<html>` re-themes the grid live - there
|
|
7
|
+
* is nothing to rebuild and no stylesheet to swap.
|
|
8
|
+
*
|
|
9
|
+
* `app.css` imports one preset as a stylesheet so the very first paint (and the
|
|
10
|
+
* server-rendered HTML) already has a theme before any JS runs. The values set
|
|
11
|
+
* here override it once the user picks something.
|
|
12
|
+
*/
|
|
13
|
+
import {
|
|
14
|
+
getThemePreset,
|
|
15
|
+
resolveThemeTokens,
|
|
16
|
+
themePresets,
|
|
17
|
+
type ThemeMode,
|
|
18
|
+
} from '@svgrid/grid/themes'
|
|
19
|
+
|
|
20
|
+
/** Every preset, for the picker. */
|
|
21
|
+
export const presets = themePresets.map((p) => ({ id: p.id, name: p.name }))
|
|
22
|
+
|
|
23
|
+
const STORAGE_KEY = 'svgrid-theme'
|
|
24
|
+
|
|
25
|
+
// `npm create @svgrid@latest -- --theme <id> [--dark|--light]` patches the two
|
|
26
|
+
// values between these markers so the scaffolded app starts on the theme you
|
|
27
|
+
// asked for. INITIAL_MODE is only the fallback: a saved choice wins over it, and
|
|
28
|
+
// so does the OS preference when nobody has pinned a mode. The inline script in
|
|
29
|
+
// `app.html` settles the same question before the first paint.
|
|
30
|
+
/* svgrid-initial-theme:start */
|
|
31
|
+
export const INITIAL_THEME = 'tailwind'
|
|
32
|
+
export const INITIAL_MODE: ThemeMode = 'light'
|
|
33
|
+
/* svgrid-initial-theme:end */
|
|
34
|
+
|
|
35
|
+
type Saved = { id: string; mode: ThemeMode }
|
|
36
|
+
|
|
37
|
+
function restore(): Saved {
|
|
38
|
+
const fallback: Saved = { id: INITIAL_THEME, mode: INITIAL_MODE }
|
|
39
|
+
if (typeof document === 'undefined') return fallback
|
|
40
|
+
// Trust whatever app.html's inline script settled on, so the picker agrees
|
|
41
|
+
// with what is already on screen.
|
|
42
|
+
const painted = document.documentElement.dataset.theme
|
|
43
|
+
if (painted === 'dark' || painted === 'light') fallback.mode = painted
|
|
44
|
+
try {
|
|
45
|
+
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? 'null') as Saved | null
|
|
46
|
+
if (!saved || typeof saved.id !== 'string') return fallback
|
|
47
|
+
if (!getThemePreset(saved.id)) return fallback
|
|
48
|
+
return { id: saved.id, mode: saved.mode === 'dark' ? 'dark' : 'light' }
|
|
49
|
+
} catch {
|
|
50
|
+
return fallback
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class ThemeState {
|
|
55
|
+
#initial = restore()
|
|
56
|
+
id = $state(this.#initial.id)
|
|
57
|
+
mode = $state<ThemeMode>(this.#initial.mode)
|
|
58
|
+
|
|
59
|
+
/** Push the current selection onto <html> as CSS custom properties. */
|
|
60
|
+
apply() {
|
|
61
|
+
if (typeof document === 'undefined') return
|
|
62
|
+
const tokens = resolveThemeTokens(getThemePreset(this.id) ?? getThemePreset(INITIAL_THEME)!, this.mode)
|
|
63
|
+
const root = document.documentElement
|
|
64
|
+
for (const [key, value] of Object.entries(tokens)) root.style.setProperty(key, value)
|
|
65
|
+
// Lets the browser theme form controls and scrollbars to match, and keeps
|
|
66
|
+
// any `[data-theme='dark']` rules in your own CSS in step.
|
|
67
|
+
root.style.colorScheme = this.mode
|
|
68
|
+
root.dataset.theme = this.mode
|
|
69
|
+
try {
|
|
70
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify({ id: this.id, mode: this.mode }))
|
|
71
|
+
} catch {
|
|
72
|
+
// Private mode, quota, a blocked origin - not worth breaking the page over.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
set(id: string, mode: ThemeMode = this.mode) {
|
|
77
|
+
this.id = id
|
|
78
|
+
this.mode = mode
|
|
79
|
+
this.apply()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
toggleMode() {
|
|
83
|
+
this.set(this.id, this.mode === 'dark' ? 'light' : 'dark')
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const theme = new ThemeState()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import '../app.css'
|
|
3
|
+
import { theme, presets } from '$lib/theme.svelte'
|
|
4
|
+
|
|
5
|
+
let { children } = $props()
|
|
6
|
+
|
|
7
|
+
// Apply on mount so the picker's starting value wins over the stylesheet, and
|
|
8
|
+
// on every later change. Runs client-side only - the server-rendered HTML is
|
|
9
|
+
// already themed by the stylesheet import in app.css.
|
|
10
|
+
$effect(() => {
|
|
11
|
+
theme.apply()
|
|
12
|
+
})
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<header class="bar">
|
|
16
|
+
<strong>SvGrid + SvelteKit</strong>
|
|
17
|
+
|
|
18
|
+
<label>
|
|
19
|
+
Theme
|
|
20
|
+
<select
|
|
21
|
+
value={theme.id}
|
|
22
|
+
onchange={(e) => theme.set(e.currentTarget.value)}
|
|
23
|
+
aria-label="Grid theme"
|
|
24
|
+
>
|
|
25
|
+
{#each presets as p (p.id)}
|
|
26
|
+
<option value={p.id}>{p.name}</option>
|
|
27
|
+
{/each}
|
|
28
|
+
</select>
|
|
29
|
+
</label>
|
|
30
|
+
|
|
31
|
+
<button type="button" onclick={() => theme.toggleMode()} aria-pressed={theme.mode === 'dark'}>
|
|
32
|
+
{theme.mode === 'dark' ? 'Dark' : 'Light'}
|
|
33
|
+
</button>
|
|
34
|
+
</header>
|
|
35
|
+
|
|
36
|
+
<main>
|
|
37
|
+
{@render children?.()}
|
|
38
|
+
</main>
|
|
39
|
+
|
|
40
|
+
<style>
|
|
41
|
+
.bar {
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
gap: 1rem;
|
|
45
|
+
flex-wrap: wrap;
|
|
46
|
+
padding: 0.75rem 1.25rem;
|
|
47
|
+
border-bottom: 1px solid var(--sg-border);
|
|
48
|
+
background: var(--sg-header-bg);
|
|
49
|
+
color: var(--sg-header-fg);
|
|
50
|
+
font-family: system-ui, sans-serif;
|
|
51
|
+
}
|
|
52
|
+
.bar label {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 0.4rem;
|
|
56
|
+
font-size: 0.875rem;
|
|
57
|
+
}
|
|
58
|
+
.bar select,
|
|
59
|
+
.bar button {
|
|
60
|
+
font: inherit;
|
|
61
|
+
padding: 0.3rem 0.5rem;
|
|
62
|
+
border-radius: 6px;
|
|
63
|
+
border: 1px solid var(--sg-border);
|
|
64
|
+
background: var(--sg-bg);
|
|
65
|
+
color: var(--sg-fg);
|
|
66
|
+
}
|
|
67
|
+
.bar button {
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
min-width: 4.5rem;
|
|
70
|
+
}
|
|
71
|
+
main {
|
|
72
|
+
padding: 1.25rem;
|
|
73
|
+
font-family: system-ui, sans-serif;
|
|
74
|
+
color: var(--sg-fg);
|
|
75
|
+
background: var(--sg-bg);
|
|
76
|
+
min-height: 100vh;
|
|
77
|
+
}
|
|
78
|
+
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<h1>SvGrid + SvelteKit</h1>
|
|
2
|
+
<p>
|
|
3
|
+
The sample lives at <a href="/people">/people</a> - a grid whose rows are
|
|
4
|
+
loaded in <code>+page.server.ts</code>, sorted from the URL, and edited through
|
|
5
|
+
a form action.
|
|
6
|
+
</p>
|
|
7
|
+
<p>Use the picker in the header to switch the grid theme while it runs.</p>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Actions, PageServerLoad } from './$types'
|
|
2
|
+
import { listPeople, renamePerson, type Person } from '$lib/people'
|
|
3
|
+
|
|
4
|
+
export const load: PageServerLoad = ({ url }) => {
|
|
5
|
+
const sortBy = (url.searchParams.get('sort') ?? 'name') as keyof Person
|
|
6
|
+
const desc = url.searchParams.get('dir') === 'desc'
|
|
7
|
+
return { rows: listPeople(sortBy, desc), sortBy, desc }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const actions: Actions = {
|
|
11
|
+
rename: async ({ request }) => {
|
|
12
|
+
const data = await request.formData()
|
|
13
|
+
renamePerson(Number(data.get('id')), String(data.get('name')))
|
|
14
|
+
return { success: true }
|
|
15
|
+
},
|
|
16
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { goto } from '$app/navigation'
|
|
3
|
+
import { page } from '$app/state'
|
|
4
|
+
import { SvGrid, type GridColumns } from '@svgrid/grid'
|
|
5
|
+
import type { Person } from '$lib/people'
|
|
6
|
+
|
|
7
|
+
let { data } = $props()
|
|
8
|
+
|
|
9
|
+
const columns: GridColumns<Person> = [
|
|
10
|
+
{ field: 'name', header: 'Name', editable: true },
|
|
11
|
+
{ field: 'role', header: 'Role' },
|
|
12
|
+
{ field: 'year', header: 'Year' },
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
// Header click -> URL -> server sorts -> load returns ordered rows.
|
|
16
|
+
function onSortingChange(sorting: Array<{ id: string; desc: boolean }>) {
|
|
17
|
+
const next = new URL(page.url)
|
|
18
|
+
if (sorting.length === 0) {
|
|
19
|
+
next.searchParams.delete('sort')
|
|
20
|
+
next.searchParams.delete('dir')
|
|
21
|
+
} else {
|
|
22
|
+
next.searchParams.set('sort', sorting[0]!.id)
|
|
23
|
+
next.searchParams.set('dir', sorting[0]!.desc ? 'desc' : 'asc')
|
|
24
|
+
}
|
|
25
|
+
goto(next, { keepFocus: true, noScroll: true })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Committed edit -> form action -> database.
|
|
29
|
+
async function onCellValueChange(e: { row: Person; columnId: string; newValue: unknown }) {
|
|
30
|
+
if (e.columnId !== 'name') return
|
|
31
|
+
const body = new FormData()
|
|
32
|
+
body.set('id', String(e.row.id))
|
|
33
|
+
body.set('name', String(e.newValue))
|
|
34
|
+
await fetch('?/rename', { method: 'POST', body })
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<h1>People</h1>
|
|
39
|
+
<p>Click a header to sort - the order lives in the URL. Double-click a name to edit it.</p>
|
|
40
|
+
|
|
41
|
+
<SvGrid
|
|
42
|
+
data={data.rows}
|
|
43
|
+
{columns}
|
|
44
|
+
sortable
|
|
45
|
+
editable
|
|
46
|
+
externalSort
|
|
47
|
+
initialSorting={[{ id: data.sortBy, desc: data.desc }]}
|
|
48
|
+
{onSortingChange}
|
|
49
|
+
{onCellValueChange}
|
|
50
|
+
containerHeight={320}
|
|
51
|
+
/>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./.svelte-kit/tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rewriteRelativeImportExtensions": true,
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"checkJs": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"moduleResolution": "bundler"
|
|
14
|
+
}
|
|
15
|
+
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
|
16
|
+
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
|
17
|
+
//
|
|
18
|
+
// To make changes to top-level options such as include and exclude, we recommend extending
|
|
19
|
+
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import adapter from '@sveltejs/adapter-auto';
|
|
2
|
+
import { sveltekit } from '@sveltejs/kit/vite';
|
|
3
|
+
import { defineConfig } from 'vite';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [
|
|
7
|
+
sveltekit({
|
|
8
|
+
compilerOptions: {
|
|
9
|
+
// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
|
|
10
|
+
runes: ({ filename }) =>
|
|
11
|
+
filename.split(/[/\\]/).includes('node_modules') ? undefined : true
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
|
15
|
+
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
|
16
|
+
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
|
17
|
+
adapter: adapter()
|
|
18
|
+
})
|
|
19
|
+
]
|
|
20
|
+
});
|