@svgrid/create 2.5.0 → 2.7.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 +16 -7
- package/index.mjs +482 -403
- package/package.json +2 -2
- package/templates/admin-dashboard/src/app.css +184 -184
- package/templates/headless/README.md +65 -0
- package/templates/headless/_gitignore +4 -0
- package/templates/headless/_package.json +22 -0
- package/templates/headless/index.html +12 -0
- package/templates/headless/src/App.svelte +159 -0
- package/templates/headless/src/app.css +153 -0
- package/templates/headless/src/main.ts +7 -0
- package/templates/headless/src/vite-env.d.ts +2 -0
- package/templates/headless/svelte.config.js +5 -0
- package/templates/headless/tsconfig.json +14 -0
- package/templates/headless/vite.config.js +6 -0
- package/templates/minimal/src/app.css +1 -1
- 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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/* ---------------------------------------------------------------------------
|
|
2
|
+
* Your stylesheet. All of it.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here comes from @svgrid/grid - there is no preset import, no --sg-*
|
|
5
|
+
* token, no theme to pick. The headless entry point ships an engine and no CSS,
|
|
6
|
+
* so the table below is styled the same way any other table in your app would
|
|
7
|
+
* be. Rename the variables, drop them for Tailwind classes, delete this file
|
|
8
|
+
* and start over: none of it affects sorting or filtering.
|
|
9
|
+
*
|
|
10
|
+
* If you'd rather inherit a themed grid than write one, the other starters use
|
|
11
|
+
* the <SvGrid> renderer and its 20 presets:
|
|
12
|
+
* npm create @svgrid@latest my-app -- --template minimal
|
|
13
|
+
* ------------------------------------------------------------------------- */
|
|
14
|
+
|
|
15
|
+
:root {
|
|
16
|
+
--app-bg: #ffffff;
|
|
17
|
+
--app-surface: #fafaf9;
|
|
18
|
+
--app-fg: #1c1917;
|
|
19
|
+
--app-muted: #78716c;
|
|
20
|
+
--app-border: #e7e5e4;
|
|
21
|
+
--app-accent: #c2410c;
|
|
22
|
+
--app-hover: #f5f5f4;
|
|
23
|
+
color-scheme: light;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Dark mode follows the OS. Swap this for a data-theme attribute and a toggle
|
|
27
|
+
if you want the choice to be the user's. */
|
|
28
|
+
@media (prefers-color-scheme: dark) {
|
|
29
|
+
:root {
|
|
30
|
+
--app-bg: #1c1917;
|
|
31
|
+
--app-surface: #292524;
|
|
32
|
+
--app-fg: #fafaf9;
|
|
33
|
+
--app-muted: #a8a29e;
|
|
34
|
+
--app-border: #44403c;
|
|
35
|
+
--app-accent: #fb923c;
|
|
36
|
+
--app-hover: #292524;
|
|
37
|
+
color-scheme: dark;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
body {
|
|
42
|
+
margin: 0;
|
|
43
|
+
min-height: 100vh;
|
|
44
|
+
background: var(--app-bg);
|
|
45
|
+
color: var(--app-fg);
|
|
46
|
+
font-family: system-ui, -apple-system, Segoe UI, sans-serif;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
main {
|
|
50
|
+
max-width: 720px;
|
|
51
|
+
margin: 3rem auto;
|
|
52
|
+
padding: 0 1rem;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
h1 {
|
|
56
|
+
margin: 0 0 0.5rem;
|
|
57
|
+
font-size: 1.4rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
header p {
|
|
61
|
+
margin: 0 0 1.5rem;
|
|
62
|
+
color: var(--app-muted);
|
|
63
|
+
line-height: 1.6;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
code {
|
|
67
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
68
|
+
font-size: 0.9em;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.search {
|
|
72
|
+
width: 260px;
|
|
73
|
+
margin-bottom: 1rem;
|
|
74
|
+
padding: 0.45rem 0.7rem;
|
|
75
|
+
border: 1px solid var(--app-border);
|
|
76
|
+
border-radius: 6px;
|
|
77
|
+
background: var(--app-bg);
|
|
78
|
+
color: var(--app-fg);
|
|
79
|
+
font: inherit;
|
|
80
|
+
font-size: 0.9rem;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.search:focus-visible {
|
|
84
|
+
outline: 2px solid var(--app-accent);
|
|
85
|
+
outline-offset: 1px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
table {
|
|
89
|
+
width: 100%;
|
|
90
|
+
border-collapse: collapse;
|
|
91
|
+
font-size: 0.9rem;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
th {
|
|
95
|
+
padding: 0;
|
|
96
|
+
background: var(--app-surface);
|
|
97
|
+
border-bottom: 2px solid var(--app-border);
|
|
98
|
+
text-align: left;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* The header is a button so keyboard users can sort too - the engine does not
|
|
102
|
+
care what element calls toggleSort. */
|
|
103
|
+
th button {
|
|
104
|
+
width: 100%;
|
|
105
|
+
padding: 0.6rem 0.75rem;
|
|
106
|
+
border: 0;
|
|
107
|
+
background: none;
|
|
108
|
+
color: inherit;
|
|
109
|
+
font: inherit;
|
|
110
|
+
font-weight: 600;
|
|
111
|
+
text-align: inherit;
|
|
112
|
+
cursor: pointer;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
th button:hover {
|
|
116
|
+
color: var(--app-accent);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
th button:focus-visible {
|
|
120
|
+
outline: 2px solid var(--app-accent);
|
|
121
|
+
outline-offset: -2px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.indicator {
|
|
125
|
+
color: var(--app-accent);
|
|
126
|
+
font-size: 0.7em;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
td {
|
|
130
|
+
padding: 0.6rem 0.75rem;
|
|
131
|
+
border-bottom: 1px solid var(--app-border);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
tbody tr:hover td {
|
|
135
|
+
background: var(--app-hover);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.numeric {
|
|
139
|
+
text-align: right;
|
|
140
|
+
font-variant-numeric: tabular-nums;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.empty {
|
|
144
|
+
padding: 1.5rem;
|
|
145
|
+
color: var(--app-muted);
|
|
146
|
+
text-align: center;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.count {
|
|
150
|
+
margin-top: 0.75rem;
|
|
151
|
+
color: var(--app-muted);
|
|
152
|
+
font-size: 0.8rem;
|
|
153
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
7
|
+
"resolveJsonModule": true,
|
|
8
|
+
"isolatedModules": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noEmit": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.svelte"]
|
|
14
|
+
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Token reference: https://svgrid.com/docs/getting-started/5-theme-and-density
|
|
14
14
|
* ------------------------------------------------------------------------- */
|
|
15
15
|
/* svgrid-theme:start */
|
|
16
|
-
@import '@svgrid/grid/themes/
|
|
16
|
+
@import '@svgrid/grid/themes/ember.css';
|
|
17
17
|
/* svgrid-theme:end */
|
|
18
18
|
|
|
19
19
|
/* Which way the page leans, so scrollbars, form controls and the canvas behind
|
|
@@ -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/ember.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 = 'ember'
|
|
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
|
+
});
|