@svgrid/create 1.1.0 → 2.0.2

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/LICENSE ADDED
@@ -0,0 +1,46 @@
1
+ This repository is a multi-package workspace. Different packages ship
2
+ under different licenses. The authoritative license for each package is
3
+ the LICENSE file inside that package directory.
4
+
5
+ ------------------------------------------------------------------------
6
+ Per-package licensing
7
+ ------------------------------------------------------------------------
8
+
9
+ packages/grid/ MIT (terms below, and packages/grid/LICENSE)
10
+ packages/enterprise/ Commercial - see packages/enterprise/LICENSE
11
+ packages/mcp/ Commercial - see packages/mcp/LICENSE
12
+ website/ Proprietary - see website/LICENSE
13
+
14
+ Only `@svgrid/grid` is open source. Everything else in this
15
+ repository (the Pro feature pack, the MCP server, the marketing +
16
+ docs website, and any other package added later that does not ship
17
+ its own MIT LICENSE) is proprietary and may not be copied, modified,
18
+ or redistributed without a paid license. Source is visible in this
19
+ repository for transparency and for paying customers; visibility does
20
+ not grant a license.
21
+
22
+ ------------------------------------------------------------------------
23
+ MIT License (applies ONLY to packages/grid)
24
+ ------------------------------------------------------------------------
25
+
26
+ MIT License
27
+
28
+ Copyright (c) 2026 jQWidgets Ltd
29
+
30
+ Permission is hereby granted, free of charge, to any person obtaining a copy
31
+ of this software and associated documentation files (the "Software"), to deal
32
+ in the Software without restriction, including without limitation the rights
33
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
34
+ copies of the Software, and to permit persons to whom the Software is
35
+ furnished to do so, subject to the following conditions:
36
+
37
+ The above copyright notice and this permission notice shall be included in all
38
+ copies or substantial portions of the Software.
39
+
40
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
42
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
43
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
44
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
45
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
46
+ SOFTWARE.
package/README.md CHANGED
@@ -1,49 +1,58 @@
1
- # @svgrid/create
2
-
3
- Scaffold a [Svelte](https://svelte.dev) app powered by
4
- [SvGrid](https://www.svgrid.com) - the modern Svelte 5 data grid - in one
5
- command.
6
-
7
- ```bash
8
- npm create @svgrid@latest
9
- # or
10
- pnpm create @svgrid
11
- # or
12
- yarn create @svgrid
13
- ```
14
-
15
- Interactive by default. Or pass a directory and template directly:
16
-
17
- ```bash
18
- npm create @svgrid@latest my-app -- --template admin-dashboard
19
- pnpm create @svgrid my-app -t minimal
20
- ```
21
-
22
- ## Templates
23
-
24
- | Template | Stack | Best for |
25
- | --- | --- | --- |
26
- | `minimal` | Vite + Svelte 5 + SvGrid | Dropping a grid into something quickly |
27
- | `admin-dashboard` | SvelteKit + Tailwind + SvGrid, deploy to Vercel | A real dashboard / internal tool |
28
-
29
- ## Options
30
-
31
- | Flag | Description |
32
- | --- | --- |
33
- | `--template`, `-t` | `minimal` or `admin-dashboard` |
34
- | `--force`, `-f` | Scaffold into a non-empty directory |
35
- | `--help`, `-h` | Show usage |
36
-
37
- Then:
38
-
39
- ```bash
40
- cd my-app
41
- npm install
42
- npm run dev
43
- ```
44
-
45
- All templates use the free MIT `@svgrid/grid` core. Add
46
- [`@svgrid/enterprise`](https://www.svgrid.com/pricing) for Excel/PDF export, import,
47
- print, pivot, and AI helpers.
48
-
49
- SvGrid(TM) is a trademark of jQWidgets Ltd. This package is MIT-licensed.
1
+ # @svgrid/create
2
+
3
+ Scaffold a [Svelte](https://svelte.dev) app powered by
4
+ [SvGrid](https://www.svgrid.com) - the modern Svelte 5 data grid - in one
5
+ command.
6
+
7
+ ```bash
8
+ npm create @svgrid@latest
9
+ # or
10
+ pnpm create @svgrid
11
+ # or
12
+ yarn create @svgrid
13
+ ```
14
+
15
+ Interactive by default. Or pass a directory and template directly:
16
+
17
+ ```bash
18
+ npm create @svgrid@latest my-app -- --template admin-dashboard
19
+ pnpm create @svgrid my-app -t minimal
20
+ ```
21
+
22
+ ## Templates
23
+
24
+ | Template | Stack | Best for |
25
+ | --- | --- | --- |
26
+ | `minimal` | Vite + Svelte 5 + SvGrid | Dropping a grid into something quickly |
27
+ | `admin-dashboard` | SvelteKit + Tailwind + SvGrid, deploy to Vercel | A real dashboard / internal tool |
28
+
29
+ ## Options
30
+
31
+ | Flag | Description |
32
+ | --- | --- |
33
+ | `--template`, `-t` | `minimal` or `admin-dashboard` |
34
+ | `--theme <id>` | `admin-dashboard` only. One of `@svgrid/grid`'s 19 built-in presets - shadcn, Tailwind, Material, Excel, Fluent, and more (default: `tailwind`) |
35
+ | `--dark` / `--light` | `admin-dashboard` only. Start in dark or light mode (default: `dark`) |
36
+ | `--force`, `-f` | Scaffold into a non-empty directory |
37
+ | `--help`, `-h` | Show usage |
38
+
39
+ Choosing `admin-dashboard` interactively also prompts for a theme and light/dark
40
+ mode - or skip the prompts with flags:
41
+
42
+ ```bash
43
+ npm create @svgrid@latest my-app -- -t admin-dashboard --theme material --light
44
+ ```
45
+
46
+ Then:
47
+
48
+ ```bash
49
+ cd my-app
50
+ npm install
51
+ npm run dev
52
+ ```
53
+
54
+ All templates use the free MIT `@svgrid/grid` core. Add
55
+ [`@svgrid/enterprise`](https://www.svgrid.com/pricing) for Excel/PDF export, import,
56
+ print, pivot, and AI helpers.
57
+
58
+ SvGrid(TM) is a trademark of jQWidgets Ltd. This package is MIT-licensed.
package/index.mjs CHANGED
@@ -1,228 +1,360 @@
1
- #!/usr/bin/env node
2
- // @svgrid/create - scaffold a Svelte app powered by SvGrid.
3
- //
4
- // npm create @svgrid@latest # interactive
5
- // pnpm create @svgrid # interactive
6
- // npm create @svgrid@latest my-app -- --template admin-dashboard
7
- // npm create @svgrid@latest my-app -- -t minimal
8
- //
9
- // Zero runtime dependencies - Node built-ins only. Copies a bundled template,
10
- // renames `_`-prefixed dotfiles, and rewrites the project name.
11
-
12
- import { cp, mkdir, readdir, rename, stat, readFile, writeFile } from 'node:fs/promises'
13
- import { existsSync } from 'node:fs'
14
- import { dirname, join, basename, resolve, isAbsolute } from 'node:path'
15
- import { fileURLToPath } from 'node:url'
16
- import { createInterface } from 'node:readline/promises'
17
- import { stdin, stdout } from 'node:process'
18
-
19
- const __dirname = dirname(fileURLToPath(import.meta.url))
20
-
21
- const TEMPLATES = {
22
- minimal: {
23
- label: 'Minimal - Vite + Svelte 5 + SvGrid, one page',
24
- bundled: join(__dirname, 'templates', 'minimal'),
25
- },
26
- 'admin-dashboard': {
27
- label: 'Admin dashboard - SvelteKit shell, multiple grids, deploy to Vercel',
28
- bundled: join(__dirname, 'templates', 'admin-dashboard'),
29
- // When running from the monorepo before `prepack` has synced the bundled
30
- // copy, fall back to the canonical source.
31
- fallback: join(__dirname, '..', '..', 'templates', 'sveltekit-admin-dashboard'),
32
- },
33
- }
34
-
35
- const RENAME_BACK = new Map([
36
- ['_gitignore', '.gitignore'],
37
- ['_npmrc', '.npmrc'],
38
- ['_env.example', '.env.example'],
39
- ['_package.json', 'package.json'],
40
- ])
41
-
42
- const c = {
43
- reset: '\x1b[0m',
44
- bold: '\x1b[1m',
45
- dim: '\x1b[2m',
46
- green: '\x1b[32m',
47
- cyan: '\x1b[36m',
48
- red: '\x1b[31m',
49
- yellow: '\x1b[33m',
50
- }
51
- const color = stdout.isTTY ? (k, s) => `${c[k]}${s}${c.reset}` : (_k, s) => s
52
-
53
- function parseArgs(argv) {
54
- const args = { _: [], template: null, force: false, help: false }
55
- for (let i = 0; i < argv.length; i++) {
56
- const a = argv[i]
57
- if (a === '--help' || a === '-h') args.help = true
58
- else if (a === '--force' || a === '-f') args.force = true
59
- else if (a === '--template' || a === '-t') args.template = argv[++i]
60
- else if (a.startsWith('--template=')) args.template = a.slice('--template='.length)
61
- else if (!a.startsWith('-')) args._.push(a)
62
- }
63
- return args
64
- }
65
-
66
- function printHelp() {
67
- stdout.write(`
68
- ${color('bold', '@svgrid/create')} - scaffold a Svelte app powered by SvGrid
69
-
70
- ${color('bold', 'Usage')}
71
- npm create @svgrid@latest [dir] -- [--template <name>] [--force]
72
-
73
- ${color('bold', 'Templates')}
74
- ${Object.entries(TEMPLATES)
75
- .map(([k, t]) => ` ${color('cyan', k.padEnd(16))} ${t.label.replace(/^\S+\s+-\s+/, '')}`)
76
- .join('\n')}
77
-
78
- ${color('bold', 'Examples')}
79
- npm create @svgrid@latest
80
- npm create @svgrid@latest my-app -- --template admin-dashboard
81
- pnpm create @svgrid my-app -t minimal
82
- `)
83
- }
84
-
85
- function sanitizeName(name) {
86
- return (
87
- name
88
- .trim()
89
- .toLowerCase()
90
- .replace(/[^a-z0-9._-]+/g, '-')
91
- .replace(/^[-_.]+|[-_.]+$/g, '') || 'sv-grid-app'
92
- )
93
- }
94
-
95
- function resolveTemplateDir(key) {
96
- const t = TEMPLATES[key]
97
- if (existsSync(t.bundled)) return t.bundled
98
- if (t.fallback && existsSync(t.fallback)) return t.fallback
99
- return null
100
- }
101
-
102
- async function copyTemplate(srcDir, destDir) {
103
- // Match only on the path RELATIVE to the template root. The absolute `src`
104
- // includes the install location (e.g. `.../node_modules/@svgrid/create/...`),
105
- // so testing it directly would match `node_modules` and skip the whole copy.
106
- const skip = /(^|[\\/])(node_modules|\.svelte-kit|\.vercel|build|dist)([\\/]|$)/
107
- await cp(srcDir, destDir, {
108
- recursive: true,
109
- filter: (src) => !skip.test(src.slice(srcDir.length)),
110
- })
111
- // Rename `_`-prefixed files back to their real dotfile names.
112
- await renameBack(destDir)
113
- }
114
-
115
- async function renameBack(dir) {
116
- for (const entry of await readdir(dir)) {
117
- const full = join(dir, entry)
118
- if ((await stat(full)).isDirectory()) {
119
- await renameBack(full)
120
- } else if (RENAME_BACK.has(entry)) {
121
- await rename(full, join(dir, RENAME_BACK.get(entry)))
122
- }
123
- }
124
- }
125
-
126
- async function setProjectName(destDir, name) {
127
- const pkgPath = join(destDir, 'package.json')
128
- if (!existsSync(pkgPath)) return
129
- try {
130
- const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
131
- pkg.name = name
132
- await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
133
- } catch {
134
- // leave the template's name if it isn't valid JSON for some reason
135
- }
136
- }
137
-
138
- async function isEmptyDir(dir) {
139
- if (!existsSync(dir)) return true
140
- const entries = await readdir(dir)
141
- return entries.filter((e) => e !== '.git').length === 0
142
- }
143
-
144
- async function main() {
145
- const args = parseArgs(process.argv.slice(2))
146
- if (args.help) return printHelp()
147
-
148
- const interactive = stdin.isTTY && stdout.isTTY
149
- let rl = null
150
- const ask = async (q, def) => {
151
- if (!interactive) return def
152
- rl ??= createInterface({ input: stdin, output: stdout })
153
- const a = (await rl.question(`${q} ${color('dim', `(${def})`)} `)).trim()
154
- return a || def
155
- }
156
-
157
- stdout.write(`\n${color('bold', '◆ @svgrid/create')} ${color('dim', 'Svelte + SvGrid')}\n\n`)
158
-
159
- // 1. Target directory.
160
- let target = args._[0]
161
- if (!target) target = await ask('Project directory:', 'my-sv-grid-app')
162
- const destDir = resolve(process.cwd(), target)
163
- const projectName = sanitizeName(basename(destDir))
164
-
165
- // 2. Template.
166
- let template = args.template
167
- if (!template) {
168
- if (interactive) {
169
- stdout.write(`\n Templates:\n`)
170
- Object.entries(TEMPLATES).forEach(([k, t], i) => {
171
- stdout.write(` ${color('cyan', String(i + 1))}. ${t.label}\n`)
172
- })
173
- const pick = await ask('\nChoose a template (1-2):', '1')
174
- template = Object.keys(TEMPLATES)[Number(pick) - 1] ?? 'minimal'
175
- } else {
176
- template = 'minimal'
177
- }
178
- }
179
- if (!TEMPLATES[template]) {
180
- if (rl) rl.close()
181
- stdout.write(
182
- `\n${color('red', '✖')} Unknown template "${template}". Choose: ${Object.keys(TEMPLATES).join(', ')}\n`,
183
- )
184
- process.exit(1)
185
- }
186
-
187
- // 3. Safety: don't clobber a non-empty directory.
188
- if (!(await isEmptyDir(destDir)) && !args.force) {
189
- const ok = await ask(
190
- `\n${color('yellow', '!')} ${target} is not empty. Continue and overwrite files? (y/N)`,
191
- 'N',
192
- )
193
- if (!/^y(es)?$/i.test(ok)) {
194
- if (rl) rl.close()
195
- stdout.write(`${color('red', '')} Aborted.\n`)
196
- process.exit(1)
197
- }
198
- }
199
-
200
- const srcDir = resolveTemplateDir(template)
201
- if (!srcDir) {
202
- if (rl) rl.close()
203
- stdout.write(
204
- `\n${color('red', '✖')} Template "${template}" is not available in this build.\n`,
205
- )
206
- process.exit(1)
207
- }
208
-
209
- // 4. Scaffold.
210
- await mkdir(destDir, { recursive: true })
211
- await copyTemplate(srcDir, destDir)
212
- await setProjectName(destDir, projectName)
213
- if (rl) rl.close()
214
-
215
- // 5. Next steps.
216
- const rel = isAbsolute(target) || target.startsWith('.') ? target : `./${target}`
217
- stdout.write(`\n${color('green', '✔')} Scaffolded ${color('bold', projectName)} (${template}) into ${rel}\n\n`)
218
- stdout.write(`${color('bold', 'Next steps')}\n`)
219
- stdout.write(` cd ${target}\n`)
220
- stdout.write(` npm install\n`)
221
- stdout.write(` npm run dev\n\n`)
222
- stdout.write(`${color('dim', 'Docs:')} https://www.svgrid.com/docs ${color('dim', 'Pro:')} https://www.svgrid.com/pricing\n\n`)
223
- }
224
-
225
- main().catch((err) => {
226
- console.error(err)
227
- process.exit(1)
228
- })
1
+ #!/usr/bin/env node
2
+ // @svgrid/create - scaffold a Svelte app powered by SvGrid.
3
+ //
4
+ // npm create @svgrid@latest # interactive
5
+ // pnpm create @svgrid # interactive
6
+ // npm create @svgrid@latest my-app -- --template admin-dashboard
7
+ // npm create @svgrid@latest my-app -- -t minimal
8
+ //
9
+ // Zero runtime dependencies - Node built-ins only. Copies a bundled template,
10
+ // renames `_`-prefixed dotfiles, and rewrites the project name.
11
+
12
+ import { cp, mkdir, readdir, rename, stat, readFile, writeFile } from 'node:fs/promises'
13
+ import { existsSync } from 'node:fs'
14
+ import { dirname, join, basename, resolve, isAbsolute } from 'node:path'
15
+ import { fileURLToPath } from 'node:url'
16
+ import { createInterface } from 'node:readline/promises'
17
+ import { stdin, stdout } from 'node:process'
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url))
20
+
21
+ const TEMPLATES = {
22
+ minimal: {
23
+ label: 'Minimal - Vite + Svelte 5 + SvGrid, one page',
24
+ bundled: join(__dirname, 'templates', 'minimal'),
25
+ },
26
+ 'admin-dashboard': {
27
+ label: 'Admin dashboard - SvelteKit shell, multiple grids, deploy to Vercel',
28
+ bundled: join(__dirname, 'templates', 'admin-dashboard'),
29
+ // When running from the monorepo before `prepack` has synced the bundled
30
+ // copy, fall back to the canonical source.
31
+ fallback: join(__dirname, '..', '..', 'templates', 'sveltekit-admin-dashboard'),
32
+ },
33
+ }
34
+
35
+ const RENAME_BACK = new Map([
36
+ ['_gitignore', '.gitignore'],
37
+ ['_npmrc', '.npmrc'],
38
+ ['_env.example', '.env.example'],
39
+ ['_package.json', 'package.json'],
40
+ ])
41
+
42
+ const c = {
43
+ reset: '\x1b[0m',
44
+ bold: '\x1b[1m',
45
+ dim: '\x1b[2m',
46
+ green: '\x1b[32m',
47
+ cyan: '\x1b[36m',
48
+ red: '\x1b[31m',
49
+ yellow: '\x1b[33m',
50
+ }
51
+ const color = stdout.isTTY ? (k, s) => `${c[k]}${s}${c.reset}` : (_k, s) => s
52
+
53
+ function parseArgs(argv) {
54
+ const args = { _: [], template: null, force: false, help: false, theme: null, mode: null }
55
+ for (let i = 0; i < argv.length; i++) {
56
+ const a = argv[i]
57
+ if (a === '--help' || a === '-h') args.help = true
58
+ else if (a === '--force' || a === '-f') args.force = true
59
+ else if (a === '--template' || a === '-t') args.template = argv[++i]
60
+ else if (a.startsWith('--template=')) args.template = a.slice('--template='.length)
61
+ else if (a === '--theme') args.theme = argv[++i]
62
+ else if (a.startsWith('--theme=')) args.theme = a.slice('--theme='.length)
63
+ else if (a === '--dark') args.mode = 'dark'
64
+ else if (a === '--light') args.mode = 'light'
65
+ else if (!a.startsWith('-')) args._.push(a)
66
+ }
67
+ return args
68
+ }
69
+
70
+ function printHelp() {
71
+ stdout.write(`
72
+ ${color('bold', '@svgrid/create')} - scaffold a Svelte app powered by SvGrid
73
+
74
+ ${color('bold', 'Usage')}
75
+ npm create @svgrid@latest [dir] -- [--template <name>] [--force]
76
+
77
+ ${color('bold', 'Templates')}
78
+ ${Object.entries(TEMPLATES)
79
+ .map(([k, t]) => ` ${color('cyan', k.padEnd(16))} ${t.label.replace(/^\S+\s+-\s+/, '')}`)
80
+ .join('\n')}
81
+
82
+ ${color('bold', 'Options')}
83
+ --theme <id> admin-dashboard only. One of: ${THEME_IDS.join(', ')} (default: tailwind).
84
+ --dark / --light admin-dashboard only. Start in dark or light mode (default: dark).
85
+ --force Scaffold into a non-empty directory.
86
+
87
+ ${color('bold', 'Examples')}
88
+ npm create @svgrid@latest
89
+ npm create @svgrid@latest my-app -- --template admin-dashboard
90
+ npm create @svgrid@latest my-app -- -t admin-dashboard --theme material --light
91
+ pnpm create @svgrid my-app -t minimal
92
+ `)
93
+ }
94
+
95
+ const THEME_IDS = [
96
+ 'shadcn', 'tailwind', 'material', 'excel', 'fluent', 'carbon', 'sap',
97
+ 'salesforce', 'atlassian', 'github', 'antd', 'ag-alpine', 'bootstrap',
98
+ 'vercel', 'linear', 'notion', 'nord', 'dracula', 'catppuccin',
99
+ ]
100
+
101
+ /**
102
+ * Resolve the theme + mode to apply (admin-dashboard only): from `--theme` /
103
+ * `--dark` / `--light` flags, or (in a TTY) an interactive prompt; otherwise
104
+ * left untouched (the template's own dark-by-default look stands). Reuses
105
+ * `@svgrid/grid/themes`'s own preset list, so the picker can never drift from
106
+ * the canonical set.
107
+ */
108
+ async function promptTheme(args, ask, interactive) {
109
+ let themes
110
+ try {
111
+ themes = await import('@svgrid/grid/themes')
112
+ } catch {
113
+ return null // theming is best-effort; the template's own default stands in.
114
+ }
115
+
116
+ let themeId = args.theme ? args.theme.trim().toLowerCase() : null
117
+ if (themeId && !themes.getThemePreset(themeId)) {
118
+ stdout.write(`${color('yellow', '!')} Unknown theme "${themeId}" - using tailwind. (${THEME_IDS.join(', ')})\n`)
119
+ themeId = null
120
+ }
121
+ if (!themeId && interactive) {
122
+ const list = themes.themePresets.map((t, i) => ` ${i + 1}. ${t.name} ${color('dim', `(${t.id})`)}`).join('\n')
123
+ stdout.write(`\n${color('bold', 'Theme')}\n${list}\n`)
124
+ const choice = await ask('Pick a number or id:', 'tailwind')
125
+ const byIndex = themes.themePresets[Number(choice) - 1]
126
+ themeId = byIndex ? byIndex.id : (themes.getThemePreset(choice.trim().toLowerCase())?.id ?? 'tailwind')
127
+ }
128
+ themeId ??= 'tailwind'
129
+
130
+ let mode = args.mode
131
+ if (!mode && interactive) {
132
+ const a = await ask('Light or dark mode? (light/dark)', 'dark')
133
+ mode = /^l(ight)?$/i.test(a) ? 'light' : 'dark'
134
+ }
135
+ mode ??= 'dark' // matches the template's existing default - only patched when 'light' is chosen.
136
+
137
+ return { themeId, mode, name: themes.getThemePreset(themeId)?.name ?? themeId, themes }
138
+ }
139
+
140
+ /** Rewrite `app.css` (between the `svgrid-theme` markers) with the resolved
141
+ * `--sg-*` tokens for the chosen theme. */
142
+ async function applyTheme(destDir, choice) {
143
+ if (!choice) return
144
+ const { themeId, themes } = choice
145
+ const preset = themes.getThemePreset(themeId) ?? themes.defaultThemePreset
146
+ const light = themes.resolveThemeTokens(preset, 'light')
147
+ const dark = themes.resolveThemeTokens(preset, 'dark')
148
+ const block = (selector, tokens, scheme) => {
149
+ const lines = Object.entries(tokens).map(([k, v]) => ` ${k}: ${v};`).join('\n')
150
+ return `${selector} {\n${lines}\n color-scheme: ${scheme};\n}`
151
+ }
152
+ const css = `${block(':root', light, 'light')}\n${block(":root[data-theme='dark']", dark, 'dark')}`
153
+
154
+ const cssPath = join(destDir, 'src', 'app.css')
155
+ const cssText = await readFile(cssPath, 'utf8').catch(() => null)
156
+ if (cssText == null) return
157
+ const marked = /\/\* svgrid-theme:start \*\/[\s\S]*?\/\* svgrid-theme:end \*\//
158
+ if (marked.test(cssText)) {
159
+ await writeFile(cssPath, cssText.replace(marked, `/* svgrid-theme:start */\n${css}\n/* svgrid-theme:end */`))
160
+ }
161
+ }
162
+
163
+ /** admin-dashboard defaults to dark (see app.html / src/lib/theme.ts /
164
+ * +layout.svelte). Flip all three to light-by-default when 'light' is chosen
165
+ * - a no-op when 'dark' is chosen, since that already matches the template. */
166
+ async function applyMode(destDir, choice) {
167
+ if (!choice || choice.mode !== 'light') return
168
+
169
+ const htmlPath = join(destDir, 'src', 'app.html')
170
+ const html = await readFile(htmlPath, 'utf8').catch(() => null)
171
+ if (html != null) {
172
+ await writeFile(
173
+ htmlPath,
174
+ html
175
+ .replace(`data-theme="dark"`, `data-theme="light"`)
176
+ .replace(`t === 'light' ? 'light' : 'dark'`, `t === 'dark' ? 'dark' : 'light'`),
177
+ )
178
+ }
179
+
180
+ const themeTsPath = join(destDir, 'src', 'lib', 'theme.ts')
181
+ const themeTs = await readFile(themeTsPath, 'utf8').catch(() => null)
182
+ if (themeTs != null) {
183
+ await writeFile(
184
+ themeTsPath,
185
+ themeTs
186
+ .replace('Defaults to dark (matches app.html).', 'Defaults to light (matches app.html).')
187
+ .replace(`if (!browser) return 'dark'`, `if (!browser) return 'light'`)
188
+ .replace(
189
+ `return localStorage.getItem('theme') === 'light' ? 'light' : 'dark'`,
190
+ `return localStorage.getItem('theme') === 'dark' ? 'dark' : 'light'`,
191
+ ),
192
+ )
193
+ }
194
+
195
+ const layoutPath = join(destDir, 'src', 'routes', '+layout.svelte')
196
+ const layout = await readFile(layoutPath, 'utf8').catch(() => null)
197
+ if (layout != null) {
198
+ await writeFile(
199
+ layoutPath,
200
+ layout
201
+ .replace('// Theme: dark by default (see app.html), toggleable + persisted.', '// Theme: light by default (see app.html), toggleable + persisted.')
202
+ .replace(`let theme = $state<Theme>('dark')`, `let theme = $state<Theme>('light')`),
203
+ )
204
+ }
205
+ }
206
+
207
+ function sanitizeName(name) {
208
+ return (
209
+ name
210
+ .trim()
211
+ .toLowerCase()
212
+ .replace(/[^a-z0-9._-]+/g, '-')
213
+ .replace(/^[-_.]+|[-_.]+$/g, '') || 'sv-grid-app'
214
+ )
215
+ }
216
+
217
+ function resolveTemplateDir(key) {
218
+ const t = TEMPLATES[key]
219
+ if (existsSync(t.bundled)) return t.bundled
220
+ if (t.fallback && existsSync(t.fallback)) return t.fallback
221
+ return null
222
+ }
223
+
224
+ async function copyTemplate(srcDir, destDir) {
225
+ // Match only on the path RELATIVE to the template root. The absolute `src`
226
+ // includes the install location (e.g. `.../node_modules/@svgrid/create/...`),
227
+ // so testing it directly would match `node_modules` and skip the whole copy.
228
+ const skip = /(^|[\\/])(node_modules|\.svelte-kit|\.vercel|build|dist)([\\/]|$)/
229
+ await cp(srcDir, destDir, {
230
+ recursive: true,
231
+ filter: (src) => !skip.test(src.slice(srcDir.length)),
232
+ })
233
+ // Rename `_`-prefixed files back to their real dotfile names.
234
+ await renameBack(destDir)
235
+ }
236
+
237
+ async function renameBack(dir) {
238
+ for (const entry of await readdir(dir)) {
239
+ const full = join(dir, entry)
240
+ if ((await stat(full)).isDirectory()) {
241
+ await renameBack(full)
242
+ } else if (RENAME_BACK.has(entry)) {
243
+ await rename(full, join(dir, RENAME_BACK.get(entry)))
244
+ }
245
+ }
246
+ }
247
+
248
+ async function setProjectName(destDir, name) {
249
+ const pkgPath = join(destDir, 'package.json')
250
+ if (!existsSync(pkgPath)) return
251
+ try {
252
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
253
+ pkg.name = name
254
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
255
+ } catch {
256
+ // leave the template's name if it isn't valid JSON for some reason
257
+ }
258
+ }
259
+
260
+ async function isEmptyDir(dir) {
261
+ if (!existsSync(dir)) return true
262
+ const entries = await readdir(dir)
263
+ return entries.filter((e) => e !== '.git').length === 0
264
+ }
265
+
266
+ async function main() {
267
+ const args = parseArgs(process.argv.slice(2))
268
+ if (args.help) return printHelp()
269
+
270
+ const interactive = stdin.isTTY && stdout.isTTY
271
+ let rl = null
272
+ const ask = async (q, def) => {
273
+ if (!interactive) return def
274
+ rl ??= createInterface({ input: stdin, output: stdout })
275
+ const a = (await rl.question(`${q} ${color('dim', `(${def})`)} `)).trim()
276
+ return a || def
277
+ }
278
+
279
+ stdout.write(`\n${color('bold', '◆ @svgrid/create')} ${color('dim', 'Svelte + SvGrid')}\n\n`)
280
+
281
+ // 1. Target directory.
282
+ let target = args._[0]
283
+ if (!target) target = await ask('Project directory:', 'my-sv-grid-app')
284
+ const destDir = resolve(process.cwd(), target)
285
+ const projectName = sanitizeName(basename(destDir))
286
+
287
+ // 2. Template.
288
+ let template = args.template
289
+ if (!template) {
290
+ if (interactive) {
291
+ stdout.write(`\n Templates:\n`)
292
+ Object.entries(TEMPLATES).forEach(([k, t], i) => {
293
+ stdout.write(` ${color('cyan', String(i + 1))}. ${t.label}\n`)
294
+ })
295
+ const pick = await ask('\nChoose a template (1-2):', '1')
296
+ template = Object.keys(TEMPLATES)[Number(pick) - 1] ?? 'minimal'
297
+ } else {
298
+ template = 'minimal'
299
+ }
300
+ }
301
+ if (!TEMPLATES[template]) {
302
+ if (rl) rl.close()
303
+ stdout.write(
304
+ `\n${color('red', '✖')} Unknown template "${template}". Choose: ${Object.keys(TEMPLATES).join(', ')}\n`,
305
+ )
306
+ process.exit(1)
307
+ }
308
+
309
+ // 3. Safety: don't clobber a non-empty directory.
310
+ if (!(await isEmptyDir(destDir)) && !args.force) {
311
+ const ok = await ask(
312
+ `\n${color('yellow', '!')} ${target} is not empty. Continue and overwrite files? (y/N)`,
313
+ 'N',
314
+ )
315
+ if (!/^y(es)?$/i.test(ok)) {
316
+ if (rl) rl.close()
317
+ stdout.write(`${color('red', '✖')} Aborted.\n`)
318
+ process.exit(1)
319
+ }
320
+ }
321
+
322
+ const srcDir = resolveTemplateDir(template)
323
+ if (!srcDir) {
324
+ if (rl) rl.close()
325
+ stdout.write(
326
+ `\n${color('red', '✖')} Template "${template}" is not available in this build.\n`,
327
+ )
328
+ process.exit(1)
329
+ }
330
+
331
+ // 3b. Theme + light/dark mode - admin-dashboard only (minimal has no theme
332
+ // system to pick from).
333
+ const themeChoice = template === 'admin-dashboard' ? await promptTheme(args, ask, interactive) : null
334
+ if (rl) rl.close()
335
+
336
+ // 4. Scaffold.
337
+ await mkdir(destDir, { recursive: true })
338
+ await copyTemplate(srcDir, destDir)
339
+ await setProjectName(destDir, projectName)
340
+ await applyTheme(destDir, themeChoice)
341
+ await applyMode(destDir, themeChoice)
342
+
343
+ // 5. Next steps.
344
+ const rel = isAbsolute(target) || target.startsWith('.') ? target : `./${target}`
345
+ stdout.write(`\n${color('green', '✔')} Scaffolded ${color('bold', projectName)} (${template}) into ${rel}\n`)
346
+ if (themeChoice) {
347
+ stdout.write(` ${color('dim', 'theme')} ${themeChoice.name} (${themeChoice.mode})\n`)
348
+ }
349
+ stdout.write(`\n`)
350
+ stdout.write(`${color('bold', 'Next steps')}\n`)
351
+ stdout.write(` cd ${target}\n`)
352
+ stdout.write(` npm install\n`)
353
+ stdout.write(` npm run dev\n\n`)
354
+ stdout.write(`${color('dim', 'Docs:')} https://www.svgrid.com/docs ${color('dim', 'Pro:')} https://www.svgrid.com/pricing\n\n`)
355
+ }
356
+
357
+ main().catch((err) => {
358
+ console.error(err)
359
+ process.exit(1)
360
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svgrid/create",
3
- "version": "1.1.0",
3
+ "version": "2.0.2",
4
4
  "description": "Scaffold a Svelte app powered by SvGrid in one command: npm create @svgrid@latest",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,8 +31,10 @@
31
31
  "engines": {
32
32
  "node": ">=18"
33
33
  },
34
+ "dependencies": {
35
+ "@svgrid/grid": "^2.1.0"
36
+ },
34
37
  "scripts": {
35
- "sync-templates": "node sync-templates.mjs",
36
- "prepack": "node sync-templates.mjs"
38
+ "sync-templates": "node sync-templates.mjs"
37
39
  }
38
- }
40
+ }
@@ -1,176 +1,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. These are the SAME tokens the svgrid.com demos use, so
10
- * the grid looks identical here out of the box. The dashboard shell reads them
11
- * through the --app-* aliases below, so the whole app themes as one. Edit any
12
- * value to rebrand.
13
- * ------------------------------------------------------------------------- */
14
- :root {
15
- --sg-bg: #ffffff;
16
- --sg-fg: #0f172a;
17
- --sg-muted: #64748b;
18
- --sg-border: #e2e8f0;
19
- --sg-header-bg: #f1f5f9;
20
- --sg-header-fg: #0f172a;
21
- --sg-row-alt-bg: #f8fafc;
22
- --sg-row-hover-bg: #eef2ff;
23
- --sg-row-height: 36px;
24
- --sg-focus-ring: 0 0 0 2px #2563eb;
25
- --sg-selection-bg: #dbeafe;
26
- --sg-accent: #2563eb;
27
- --sg-pill-active: #dcfce7;
28
- --sg-pill-active-fg: #166534;
29
- --sg-pill-pending: #fef9c3;
30
- --sg-pill-pending-fg: #854d0e;
31
- --sg-pill-inactive: #fee2e2;
32
- --sg-pill-inactive-fg: #991b1b;
33
- --sg-input-bg: #ffffff;
34
- --sg-input-border: #cbd5e1;
35
-
36
- /* Dashboard shell aliases - map the chrome to the grid tokens. */
37
- --app-bg: var(--sg-row-alt-bg);
38
- --app-panel: var(--sg-bg);
39
- --app-border: var(--sg-border);
40
- --app-fg: var(--sg-fg);
41
- --app-muted: var(--sg-muted);
42
- --app-accent: var(--sg-accent);
43
- }
44
-
45
- html[data-theme='dark'] {
46
- --sg-bg: #181d27;
47
- --sg-fg: #e2e8f0;
48
- --sg-muted: #94a3b8;
49
- --sg-border: #374151;
50
- --sg-header-bg: #1e2433;
51
- --sg-header-fg: #e2e8f0;
52
- --sg-row-alt-bg: #1b2230;
53
- --sg-row-hover-bg: #232b3c;
54
- --sg-selection-bg: #1d3a73;
55
- --sg-pill-active: #064e3b;
56
- --sg-pill-active-fg: #6ee7b7;
57
- --sg-pill-pending: #713f12;
58
- --sg-pill-pending-fg: #fde68a;
59
- --sg-pill-inactive: #7f1d1d;
60
- --sg-pill-inactive-fg: #fecaca;
61
- --sg-input-bg: #1a2130;
62
- --sg-input-border: #2c3548;
63
- --sg-accent: #3b82f6;
64
- --sg-scrollbar-bg: #1a2130;
65
- --sg-scrollbar-border: rgba(148, 163, 184, 0.08);
66
- --sg-scrollbar-arrow: #64748b;
67
- --sg-scrollbar-arrow-hover: #e2e8f0;
68
- --sg-scrollbar-arrow-hover-bg: #2a3142;
69
- --sg-scrollbar-arrow-active: #f1f5f9;
70
- --sg-scrollbar-arrow-active-bg: #374151;
71
- --sg-scrollbar-arrow-disabled: #475569;
72
- --sg-scrollbar-thumb: #475569;
73
- --sg-scrollbar-thumb-hover: #64748b;
74
- --sg-scrollbar-thumb-active: #94a3b8;
75
- /* The shell panels sit a touch above the page in dark mode. */
76
- --app-panel: #1e2433;
77
- color-scheme: dark;
78
- }
79
-
80
- html[data-theme='light'] {
81
- color-scheme: light;
82
- }
83
-
84
- html,
85
- body {
86
- background: var(--app-bg);
87
- color: var(--app-fg);
88
- font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial,
89
- sans-serif;
90
- }
91
-
92
- /* ---------------------------------------------------------------------------
93
- * Optional polish. The grid themes itself from the --sg-* tokens above, so it
94
- * already follows light / dark with no extra CSS. These rules only add table
95
- * niceties the grid leaves to the app: cell gridlines, zebra striping, row
96
- * hover, and a styled pagination bar. Delete any you don't want.
97
- * ------------------------------------------------------------------------- */
98
-
99
- /* Cell gridlines (the grid draws none by default). */
100
- .sv-grid-column,
101
- .sv-grid-cell {
102
- border-right: 1px solid var(--sg-border) !important;
103
- border-bottom: 1px solid var(--sg-border) !important;
104
- }
105
- .sv-grid-table tr > :last-child.sv-grid-column,
106
- .sv-grid-table tr > :last-child.sv-grid-cell {
107
- border-right: 0 !important;
108
- }
109
-
110
- /* Zebra striping + row hover. */
111
- .sv-grid-table tbody tr:nth-child(even) .sv-grid-cell {
112
- background: var(--sg-row-alt-bg) !important;
113
- }
114
- .sv-grid-table tbody tr:hover .sv-grid-cell {
115
- background: var(--sg-row-hover-bg) !important;
116
- }
117
- /* Selection wins over zebra (declared after, same specificity). */
118
- .sv-grid-table tbody tr.sv-grid-row-selected .sv-grid-cell,
119
- .sv-grid-table tbody tr .sv-grid-cell[data-selected-range='true'] {
120
- background: var(--sg-selection-bg) !important;
121
- }
122
-
123
- /* Pagination bar: layout + theme (the grid ships the markup, not the styling). */
124
- .sv-grid-pagination {
125
- display: flex !important;
126
- align-items: center !important;
127
- justify-content: flex-end !important;
128
- gap: 24px !important;
129
- padding: 12px 16px !important;
130
- border: 1px solid var(--sg-border) !important;
131
- border-top: 0 !important;
132
- border-radius: 0 0 6px 6px !important;
133
- background: var(--sg-header-bg) !important;
134
- color: var(--sg-fg) !important;
135
- font-size: 13px !important;
136
- }
137
- .sv-grid-pagination-pagesize {
138
- display: inline-flex;
139
- align-items: center;
140
- gap: 8px;
141
- color: var(--sg-muted);
142
- }
143
- .sv-grid-pagination-pagesize select,
144
- .sv-grid-pagination-btn {
145
- border: 1px solid var(--sg-input-border);
146
- background: var(--sg-input-bg);
147
- color: var(--sg-fg);
148
- border-radius: 5px;
149
- height: 28px;
150
- font: inherit;
151
- font-size: 13px;
152
- cursor: pointer;
153
- }
154
- .sv-grid-pagination-btn {
155
- display: inline-flex !important;
156
- align-items: center !important;
157
- justify-content: center !important;
158
- width: 28px !important;
159
- border: 0 !important;
160
- background: transparent !important;
161
- font-size: 16px !important;
162
- }
163
- .sv-grid-pagination-btn:hover:not(:disabled) {
164
- background: var(--sg-input-bg) !important;
165
- color: var(--sg-accent) !important;
166
- }
167
- .sv-grid-pagination-btn:disabled {
168
- color: var(--sg-muted) !important;
169
- opacity: 0.4 !important;
170
- cursor: default !important;
171
- }
172
- .sv-grid-pagination-nav {
173
- display: inline-flex;
174
- align-items: center;
175
- gap: 4px;
176
- }
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 19 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
+ html,
89
+ body {
90
+ background: var(--app-bg);
91
+ color: var(--app-fg);
92
+ font-family: var(--sg-font, Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial,
93
+ sans-serif);
94
+ }
95
+
96
+ /* ---------------------------------------------------------------------------
97
+ * Optional polish. The grid themes itself from the --sg-* tokens above, so it
98
+ * already follows light / dark with no extra CSS. These rules only add table
99
+ * niceties the grid leaves to the app: cell gridlines, zebra striping, row
100
+ * hover, and a styled pagination bar. Delete any you don't want.
101
+ * ------------------------------------------------------------------------- */
102
+
103
+ /* Cell gridlines (the grid draws none by default). */
104
+ .sv-grid-column,
105
+ .sv-grid-cell {
106
+ border-right: 1px solid var(--sg-border) !important;
107
+ border-bottom: 1px solid var(--sg-border) !important;
108
+ }
109
+ .sv-grid-table tr > :last-child.sv-grid-column,
110
+ .sv-grid-table tr > :last-child.sv-grid-cell {
111
+ border-right: 0 !important;
112
+ }
113
+
114
+ /* Zebra striping + row hover. */
115
+ .sv-grid-table tbody tr:nth-child(even) .sv-grid-cell {
116
+ background: var(--sg-row-alt-bg) !important;
117
+ }
118
+ .sv-grid-table tbody tr:hover .sv-grid-cell {
119
+ background: var(--sg-row-hover-bg) !important;
120
+ }
121
+ /* Selection wins over zebra (declared after, same specificity). */
122
+ .sv-grid-table tbody tr.sv-grid-row-selected .sv-grid-cell,
123
+ .sv-grid-table tbody tr .sv-grid-cell[data-selected-range='true'] {
124
+ background: var(--sg-selection-bg) !important;
125
+ }
126
+
127
+ /* Pagination bar: layout + theme (the grid ships the markup, not the styling). */
128
+ .sv-grid-pagination {
129
+ display: flex !important;
130
+ align-items: center !important;
131
+ justify-content: flex-end !important;
132
+ gap: 24px !important;
133
+ padding: 12px 16px !important;
134
+ border: 1px solid var(--sg-border) !important;
135
+ border-top: 0 !important;
136
+ border-radius: 0 0 6px 6px !important;
137
+ background: var(--sg-header-bg) !important;
138
+ color: var(--sg-fg) !important;
139
+ font-size: 13px !important;
140
+ }
141
+ .sv-grid-pagination-pagesize {
142
+ display: inline-flex;
143
+ align-items: center;
144
+ gap: 8px;
145
+ color: var(--sg-muted);
146
+ }
147
+ .sv-grid-pagination-pagesize select,
148
+ .sv-grid-pagination-btn {
149
+ border: 1px solid var(--sg-input-border);
150
+ background: var(--sg-input-bg);
151
+ color: var(--sg-fg);
152
+ border-radius: 5px;
153
+ height: 28px;
154
+ font: inherit;
155
+ font-size: 13px;
156
+ cursor: pointer;
157
+ }
158
+ .sv-grid-pagination-btn {
159
+ display: inline-flex !important;
160
+ align-items: center !important;
161
+ justify-content: center !important;
162
+ width: 28px !important;
163
+ border: 0 !important;
164
+ background: transparent !important;
165
+ font-size: 16px !important;
166
+ }
167
+ .sv-grid-pagination-btn:hover:not(:disabled) {
168
+ background: var(--sg-input-bg) !important;
169
+ color: var(--sg-accent) !important;
170
+ }
171
+ .sv-grid-pagination-btn:disabled {
172
+ color: var(--sg-muted) !important;
173
+ opacity: 0.4 !important;
174
+ cursor: default !important;
175
+ }
176
+ .sv-grid-pagination-nav {
177
+ display: inline-flex;
178
+ align-items: center;
179
+ gap: 4px;
180
+ }