@svgrid/create 2.5.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 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` | Start in dark or light mode. `minimal` follows the OS when neither is given |
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
- Either template prompts for a theme and a light/dark mode when run
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
- Both ship a working toggle, so the choice is a starting point rather than
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 Start in dark or light mode (default: dark).
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('\nChoose a template (1-2):', '1')
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 pins a mode when one was asked for; otherwise it reads the
387
- // OS preference at load, so reporting "dark" here would be a lie.
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.5.0",
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.5.0"
39
+ "@svgrid/grid": "^2.6.9"
40
40
  },
41
41
  "scripts": {
42
42
  "sync-templates": "node sync-templates.mjs"
@@ -1,184 +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
- /* 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
- }
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
+ }
@@ -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
+ });