@svgrid/enterprise 3.1.25 → 3.1.27

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.
Files changed (61) hide show
  1. package/dist/cdn/svgrid-enterprise.svelte-external.js +18048 -17567
  2. package/dist/designer/assets/{GridFooter-DKMfZpL4.js → GridFooter-EyLg77jG.js} +1 -1
  3. package/dist/designer/assets/{GridMenus-DWDghydL.js → GridMenus-BAgfpDd-.js} +3 -3
  4. package/dist/designer/assets/{SvDateRangeInput-DbfmXqJj.js → SvDateRangeInput-y8PkioQk.js} +1 -1
  5. package/dist/designer/assets/SvGrid.helpers-DHkH3mHW.js +1 -0
  6. package/dist/designer/assets/SvGridCellEditor-BHxH4b5h.js +4 -0
  7. package/dist/designer/assets/{SvGridChart-DYtGHM3F.js → SvGridChart-DzCeEdlb.js} +3 -3
  8. package/dist/designer/assets/{SvGridChartBuilder-BjdQrnz5.js → SvGridChartBuilder-CsXVVKvA.js} +1 -1
  9. package/dist/designer/assets/SvGridChartPanel-Dj3IxTS2.js +4 -0
  10. package/dist/designer/assets/{SvGridChartView-Bfs0bvDb.js → SvGridChartView-Dwm4hxbW.js} +1 -1
  11. package/dist/designer/assets/{chart-export-B6bkQ3X8.js → chart-export-Dqq_eHRq.js} +4 -4
  12. package/dist/designer/assets/{chart-export-pdf-BbA28-gt.js → chart-export-pdf-Gw22jkxa.js} +1 -1
  13. package/dist/designer/assets/column-types-Bl5R-97G.js +1 -0
  14. package/dist/designer/assets/csv-injection-C4a0uulO.js +1 -0
  15. package/dist/designer/assets/export-format-NYxEY-0Y.js +6 -0
  16. package/dist/designer/assets/index-BU3ieI05.css +1 -0
  17. package/dist/designer/assets/{index-UhFdGGXv.js → index-Cim1_diz.js} +340 -330
  18. package/dist/designer/assets/src-D23PPsK1.js +1 -0
  19. package/dist/designer/assets/src-sFRH_up7.js +60 -0
  20. package/dist/designer/index.html +13 -11
  21. package/dist/export-conditional.d.ts +2 -0
  22. package/dist/export-conditional.js +2 -0
  23. package/dist/export-pdf.d.ts +142 -7
  24. package/dist/export-pdf.js +368 -44
  25. package/dist/export.d.ts +22 -4
  26. package/dist/export.js +150 -53
  27. package/dist/import.js +14 -3
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +1 -1
  30. package/dist/node/studio.js +20 -12
  31. package/dist/pdf-worker.d.ts +92 -0
  32. package/dist/pdf-worker.js +147 -0
  33. package/dist/sheet/ods-document.js +3 -1
  34. package/dist/sheet/open-file.js +4 -2
  35. package/dist/sheet/xlsx-document.d.ts +2 -2
  36. package/dist/sheet/xlsx-document.js +18 -2
  37. package/dist/studio/emit-schema.js +18 -10
  38. package/dist/version.d.ts +2 -2
  39. package/dist/version.js +2 -2
  40. package/dist/wc/{GridMenus-h7RsvcmI.js → GridMenus-BIQXB6a7.js} +1 -1
  41. package/dist/wc/{SvDateRangeInput-CJZohgBe.js → SvDateRangeInput-DHYuAJeY.js} +1 -0
  42. package/dist/wc/SvGridCellEditor-CZwhQx0a.js +584 -0
  43. package/dist/wc/{SvGridChart-DWRqlhCV.js → SvGridChart-DcfmVvzB.js} +3 -3
  44. package/dist/wc/{SvGridChartBuilder-BXW3yUR0.js → SvGridChartBuilder-1Z69mxbM.js} +2 -2
  45. package/dist/wc/{SvGridChartPanel-28n4tomx.js → SvGridChartPanel-BaVsa-T-.js} +5 -5
  46. package/dist/wc/{SvGridChartView-CbG_WrU-.js → SvGridChartView-LEC8ZNeU.js} +1 -1
  47. package/dist/wc/{chart-export-CeSADGfi.js → chart-export-C1KSVdMU.js} +29 -28
  48. package/dist/wc/{chart-export-pdf-BGko7MER.js → chart-export-pdf-CmEd365D.js} +1 -1
  49. package/dist/wc/csv-injection-Ce26_TOb.js +8 -0
  50. package/dist/wc/sv-sheet-element.js +6275 -6215
  51. package/dist/xlsx-limits.d.ts +51 -0
  52. package/dist/xlsx-limits.js +89 -0
  53. package/package.json +7 -3
  54. package/dist/designer/assets/SvGrid.helpers-BF2QRcNt.js +0 -1
  55. package/dist/designer/assets/SvGridCellEditor-od4VbndV.js +0 -4
  56. package/dist/designer/assets/SvGridChartPanel-Cm9_K7BV.js +0 -4
  57. package/dist/designer/assets/export-format-BCuqcexA.js +0 -6
  58. package/dist/designer/assets/index-CHx3F4Ij.css +0 -1
  59. package/dist/designer/assets/src-CITpcm6b.js +0 -1
  60. package/dist/designer/assets/src-fddEt37f.js +0 -58
  61. package/dist/wc/SvGridCellEditor-C7ios4i_.js +0 -582
@@ -0,0 +1,147 @@
1
+ /// <reference path="./pdfmake-shims.d.ts" />
2
+ /**
3
+ * Render a PDF export in a worker, so a large report does not freeze the
4
+ * page. pdfmake lays out and writes the whole document in one synchronous
5
+ * run, about a millisecond per row of eight columns on a desktop machine:
6
+ * a 20,000-row export held the main thread for over twenty seconds.
7
+ *
8
+ * The app owns the worker file, so any bundler builds it:
9
+ *
10
+ * ```ts
11
+ * // pdf.worker.ts
12
+ * import { servePdfWorker } from '@svgrid/enterprise/pdf-worker'
13
+ * servePdfWorker()
14
+ * ```
15
+ *
16
+ * ```ts
17
+ * await api.exportData({
18
+ * format: 'pdf',
19
+ * pdf: { worker: () => new Worker(new URL('./pdf.worker.ts', import.meta.url), { type: 'module' }) },
20
+ * })
21
+ * ```
22
+ *
23
+ * The main thread builds the document as plain data (`buildPdfDocData`) and
24
+ * posts it here with the font, if any; the worker loads pdfmake, attaches
25
+ * the layout and footer functions (`hydratePdfDoc`), renders, and posts the
26
+ * bytes back with progress along the way.
27
+ */
28
+ import { hydratePdfDoc, registerPdfFont, registerPdfFonts } from './export-pdf.js';
29
+ /** Tags this channel's messages, so a worker shared with other traffic ignores the rest. */
30
+ export const PDF_WORKER_CHANNEL = 'svgrid-pdf';
31
+ /**
32
+ * Render a document from `buildPdfDocData` with the pdfmake given. The
33
+ * worker's half of the export, exported so it can run anywhere pdfmake does.
34
+ */
35
+ export function renderPdfDoc(doc, pdfMake, opts = {}) {
36
+ if (opts.font)
37
+ registerPdfFont(pdfMake, opts.font);
38
+ const def = hydratePdfDoc(doc);
39
+ return new Promise((resolve, reject) => {
40
+ const done = (bytes) => resolve(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes));
41
+ try {
42
+ // 0.2 calls back; 0.3 returns a promise and ignores the callback.
43
+ const returned = pdfMake.createPdf(def).getBuffer(done, { progressCallback: opts.onProgress });
44
+ if (returned && typeof returned.then === 'function') {
45
+ ;
46
+ returned.then(done, reject);
47
+ }
48
+ }
49
+ catch (err) {
50
+ reject(err);
51
+ }
52
+ });
53
+ }
54
+ async function loadPdfMake() {
55
+ // @ts-ignore - "pdfmake" is an optional peerDependency
56
+ const mod = (await import('pdfmake/build/pdfmake'));
57
+ // @ts-ignore - "pdfmake" is an optional peerDependency
58
+ const vfs = await import('pdfmake/build/vfs_fonts');
59
+ const pdfMake = (mod.default ?? mod);
60
+ if (!registerPdfFonts(pdfMake, vfs)) {
61
+ throw new Error('"pdfmake/build/vfs_fonts" did not contain a font map; the installed pdfmake is not one this export knows.');
62
+ }
63
+ // createPdf reads `pdfMake.tableLayouts` off the global, and a bundled
64
+ // import never sets that global, so without this every render failed with
65
+ // "Cannot read properties of undefined (reading 'tableLayouts')". The page
66
+ // side does the same in ensureGlobals.
67
+ ;
68
+ globalThis.pdfMake = pdfMake;
69
+ return pdfMake;
70
+ }
71
+ /** Answer PDF export requests from the main thread. Call it once, in the worker. */
72
+ export function servePdfWorker(options = {}) {
73
+ const scope = options.scope ?? globalThis;
74
+ const load = options.loadPdfMake ?? loadPdfMake;
75
+ let pdfMake = null;
76
+ const reply = (message, transfer) => scope.postMessage(message, transfer);
77
+ const listener = async (event) => {
78
+ const msg = event.data;
79
+ if (!msg || msg.channel !== PDF_WORKER_CHANNEL || typeof msg.id !== 'number' || !msg.doc)
80
+ return;
81
+ const { id, doc, font } = msg;
82
+ try {
83
+ pdfMake ?? (pdfMake = load());
84
+ // pdfmake reports progress per item drawn: a 5,000-row table sent
85
+ // 79,129 messages. One per percent is enough for a progress bar.
86
+ let sent = -1;
87
+ const bytes = await renderPdfDoc(doc, await pdfMake, {
88
+ font,
89
+ onProgress: (progress) => {
90
+ if (progress < 1 && progress - sent < 0.01)
91
+ return;
92
+ sent = progress;
93
+ reply({ channel: PDF_WORKER_CHANNEL, id, progress });
94
+ },
95
+ });
96
+ // A copy of exactly these bytes, handed over rather than cloned.
97
+ const pdf = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
98
+ reply({ channel: PDF_WORKER_CHANNEL, id, pdf }, [pdf]);
99
+ }
100
+ catch (err) {
101
+ pdfMake = null;
102
+ reply({ channel: PDF_WORKER_CHANNEL, id, error: err instanceof Error ? err.message : String(err) });
103
+ }
104
+ };
105
+ scope.addEventListener('message', listener);
106
+ return { dispose: () => scope.removeEventListener('message', listener) };
107
+ }
108
+ let nextJob = 1;
109
+ /**
110
+ * The main thread's half: post the document to a fresh worker, report its
111
+ * progress, and resolve with the PDF. An abort terminates the worker.
112
+ */
113
+ export function renderPdfInWorker(makeWorker, doc, opts = {}) {
114
+ const worker = makeWorker();
115
+ const id = nextJob++;
116
+ return new Promise((resolve, reject) => {
117
+ const finish = () => {
118
+ opts.signal?.removeEventListener('abort', onAbort);
119
+ worker.terminate();
120
+ };
121
+ const onAbort = () => {
122
+ finish();
123
+ reject(new DOMException('Export aborted', 'AbortError'));
124
+ };
125
+ if (opts.signal?.aborted)
126
+ return onAbort();
127
+ opts.signal?.addEventListener('abort', onAbort, { once: true });
128
+ worker.addEventListener('message', (event) => {
129
+ const msg = event.data;
130
+ if (!msg || msg.channel !== PDF_WORKER_CHANNEL || msg.id !== id)
131
+ return;
132
+ if ('progress' in msg)
133
+ return opts.onProgress?.(msg.progress);
134
+ finish();
135
+ if ('pdf' in msg)
136
+ resolve(new Blob([msg.pdf], { type: 'application/pdf' }));
137
+ else
138
+ reject(new Error(`@svgrid/enterprise: the PDF worker failed: ${msg.error}`));
139
+ });
140
+ worker.addEventListener('error', (event) => {
141
+ finish();
142
+ reject(new Error(`@svgrid/enterprise: the PDF worker failed to start or crashed: ${event.message || 'no message'}. Check that it calls servePdfWorker().`));
143
+ });
144
+ const request = { channel: PDF_WORKER_CHANNEL, id, doc, ...(opts.font ? { font: opts.font } : {}) };
145
+ worker.postMessage(request);
146
+ });
147
+ }
@@ -17,6 +17,7 @@ import { colToLetters, lettersToCol } from './address.js';
17
17
  import { tokenize } from './tokenize.js';
18
18
  import { formatKeyAt } from './format-store.js';
19
19
  import { linkAt } from './links.js';
20
+ import { createUnzipBudget, readZipEntry } from '../xlsx-limits.js';
20
21
  import { threadAt } from './comments.js';
21
22
  import { loadZip } from './xlsx-document.js';
22
23
  import { accountingParts, accountingPattern } from './number-format.js';
@@ -747,6 +748,7 @@ function durationSeconds(text) {
747
748
  export async function documentFromOds(file, JSZip) {
748
749
  const Zip = await loadZip(JSZip);
749
750
  const zip = await Zip.loadAsync(file);
751
+ const budget = createUnzipBudget();
750
752
  const parts = {};
751
753
  const pending = [];
752
754
  zip.forEach((path, entry) => {
@@ -755,7 +757,7 @@ export async function documentFromOds(file, JSZip) {
755
757
  if (!/\.(xml)$/i.test(path))
756
758
  return;
757
759
  const clean = path.replace(/^\//, '');
758
- pending.push(entry.async('string').then((text) => { parts[clean] = text; }));
760
+ pending.push(readZipEntry(entry, 'string', budget).then((text) => { parts[clean] = text; }));
759
761
  });
760
762
  await Promise.all(pending);
761
763
  return sheetStateFromOds(parts);
@@ -14,6 +14,7 @@ import { documentFromXlsxParts, loadZip } from './xlsx-document.js';
14
14
  import { sheetStateFromOds } from './ods-document.js';
15
15
  import { sheetStateFromCsv } from './csv.js';
16
16
  import { isXlsFile, sheetStateFromXls } from './xls-document.js';
17
+ import { createUnzipBudget, readZipEntry } from '../xlsx-limits.js';
17
18
  /** A picture travels as the data URL the document holds, as in the xlsx reader. */
18
19
  const MEDIA_PREFIX = 'xl/media/';
19
20
  const mediaTypeOf = (path) => {
@@ -31,6 +32,7 @@ export async function readSpreadsheetFile(file, JSZip) {
31
32
  }
32
33
  const Zip = await loadZip(JSZip);
33
34
  const zip = await Zip.loadAsync(file);
35
+ const budget = createUnzipBudget();
34
36
  const parts = {};
35
37
  const pending = [];
36
38
  zip.forEach((path, entry) => {
@@ -38,12 +40,12 @@ export async function readSpreadsheetFile(file, JSZip) {
38
40
  return;
39
41
  const clean = path.replace(/^\//, '');
40
42
  if (clean.startsWith(MEDIA_PREFIX)) {
41
- pending.push(entry.async('base64').then((base64) => { parts[clean] = `data:${mediaTypeOf(clean)};base64,${base64}`; }));
43
+ pending.push(readZipEntry(entry, 'base64', budget).then((base64) => { parts[clean] = `data:${mediaTypeOf(clean)};base64,${base64}`; }));
42
44
  return;
43
45
  }
44
46
  if (!/\.(xml|rels|vml)$/i.test(clean))
45
47
  return;
46
- pending.push(entry.async('string').then((text) => { parts[clean] = text; }));
48
+ pending.push(readZipEntry(entry, 'string', budget).then((text) => { parts[clean] = text; }));
47
49
  });
48
50
  await Promise.all(pending);
49
51
  if (parts['content.xml'] && !parts['xl/workbook.xml'])
@@ -23,6 +23,7 @@
23
23
  * unzip them with jszip, an optional peer loaded on demand.
24
24
  */
25
25
  import type { SheetDocument, SheetState } from './document.js';
26
+ import { type ZipEntryLike } from '../xlsx-limits.js';
26
27
  export declare const isoToSerial: (iso: string) => number | null;
27
28
  export declare const serialToIso: (serial: number) => string;
28
29
  /** Excel's built-in number formats, the ones a file names by id alone. */
@@ -94,9 +95,8 @@ export type ZipCtor = {
94
95
  file(path: string): {
95
96
  async(type: 'string' | 'base64'): Promise<string>;
96
97
  } | null;
97
- forEach(fn: (path: string, entry: {
98
+ forEach(fn: (path: string, entry: ZipEntryLike & {
98
99
  dir: boolean;
99
- async(type: 'string' | 'base64'): Promise<string>;
100
100
  }) => void): void;
101
101
  }>;
102
102
  };
@@ -13,6 +13,7 @@ import { drawingPartsFor, objectsFromDrawing, rectOfRef, REL_DRAWING } from './x
13
13
  import { objectId } from './objects.js';
14
14
  import { sparklineLines, sparklineId } from './sparklines.js';
15
15
  import { autoFilterXlsx, filteredRows, filtersFromXlsx } from './filter-files.js';
16
+ import { createUnzipBudget, readZipEntry, XLSX_MAX_COLS, XLSX_MAX_FILL, XLSX_MAX_ROWS, xlsxFillError } from '../xlsx-limits.js';
16
17
  // ---------------------------------------------------------------------------
17
18
  // Shared pieces
18
19
  // ---------------------------------------------------------------------------
@@ -1282,10 +1283,24 @@ export function documentFromXlsxParts(parts) {
1282
1283
  const outlineCols = emptyOutline();
1283
1284
  if (xml) {
1284
1285
  const root = parseXml(xml, path).documentElement;
1286
+ // Rows and lines are filled up to the cell, so a reference from the
1287
+ // file decides how much is allocated: a cell outside the sheet is
1288
+ // skipped, as other malformed parts are, and the blanks are budgeted
1289
+ // (xlsx-limits.ts).
1290
+ let filled = 0;
1285
1291
  const put = (r, c, text) => {
1292
+ if (r >= XLSX_MAX_ROWS || c >= XLSX_MAX_COLS)
1293
+ return;
1294
+ if (r > cells.length)
1295
+ filled += r - cells.length;
1286
1296
  while (cells.length <= r)
1287
1297
  cells.push([]);
1288
1298
  const line = cells[r];
1299
+ if (c >= line.length) {
1300
+ filled += c - line.length;
1301
+ if (filled > XLSX_MAX_FILL)
1302
+ throw xlsxFillError();
1303
+ }
1289
1304
  while (line.length <= c)
1290
1305
  line.push('');
1291
1306
  line[c] = text;
@@ -1828,6 +1843,7 @@ export async function documentToXlsx(doc, JSZip) {
1828
1843
  export async function documentFromXlsx(file, JSZip) {
1829
1844
  const Zip = await loadZip(JSZip);
1830
1845
  const zip = await Zip.loadAsync(file);
1846
+ const budget = createUnzipBudget();
1831
1847
  const parts = {};
1832
1848
  const pending = [];
1833
1849
  zip.forEach((path, entry) => {
@@ -1837,12 +1853,12 @@ export async function documentFromXlsx(file, JSZip) {
1837
1853
  // A picture comes back as the data URL an image object holds, so the
1838
1854
  // rest of the reader never has to think about bytes.
1839
1855
  if (clean.startsWith(MEDIA_PREFIX)) {
1840
- pending.push(entry.async('base64').then((base64) => { parts[clean] = `data:${mediaTypeOf(clean)};base64,${base64}`; }));
1856
+ pending.push(readZipEntry(entry, 'base64', budget).then((base64) => { parts[clean] = `data:${mediaTypeOf(clean)};base64,${base64}`; }));
1841
1857
  return;
1842
1858
  }
1843
1859
  if (!/\.(xml|rels|vml)$/i.test(path))
1844
1860
  return;
1845
- pending.push(entry.async('string').then((text) => { parts[clean] = text; }));
1861
+ pending.push(readZipEntry(entry, 'string', budget).then((text) => { parts[clean] = text; }));
1846
1862
  });
1847
1863
  await Promise.all(pending);
1848
1864
  return documentFromXlsxParts(parts);
@@ -834,8 +834,8 @@ export function layoutFile(nav, opts = {}) {
834
834
  ? `<div class="sv-app__toolbar">
835
835
  <div class="sv-app__search">
836
836
  <svg class="sv-app__search-ic" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.35-4.35"/></svg>
837
- <input class="sv-app__search-in" type="search" placeholder="Search {brand}…" bind:value={q} bind:this={searchEl} aria-label="Search screens" />
838
- <kbd class="sv-app__kbd" aria-hidden="true">⌘K</kbd>
837
+ <input class="sv-app__search-in" type="search" placeholder="Search {brand}…" bind:value={q} aria-label="Search screens" />
838
+ <button type="button" class="sv-app__kbd" title="Command palette (⌘K / Ctrl+K)" aria-label="Open the command palette" onclick={() => (paletteOpen = true)}>⌘K</button>
839
839
  {#if results.length}
840
840
  <div class="sv-app__results">
841
841
  {#each results as r (r.href)}
@@ -881,6 +881,7 @@ export function layoutFile(nav, opts = {}) {
881
881
  </div>
882
882
  </div>
883
883
  {#if bellOpen || menuOpen}<button type="button" class="sv-app__scrim" tabindex="-1" aria-label="Close menus" onclick={() => { bellOpen = false; menuOpen = false }}></button>{/if}
884
+ <SvCommand bind:open={paletteOpen} {commands} placeholder="Go to a screen or run an action…" emptyText="No matching screen" />
884
885
  </div>\n `
885
886
  : '';
886
887
  // Supabase Auth: gate the whole app behind a client-side sign-in. SvAuthGate shows
@@ -987,7 +988,7 @@ export function layoutFile(nav, opts = {}) {
987
988
  import '../app.css'
988
989
  import '../custom.css'
989
990
  import { page } from '$app/state'
990
- import { setLicenseKey } from '@svgrid/enterprise'${opts.access ? `\n import { currentRole, canScreen } from '../lib/access'` : ''}${opts.i18n ? `\n import { t, currentLocale, locales } from '../lib/i18n'` : ''}${opts.auth ? `\n import type { LayoutData } from './$types'` : ''}${opts.supabaseAuth ? `\n import { SvAuthGate, type SupabaseAuthClientLike } from '@svgrid/enterprise'\n import { supabaseClient } from '../lib/connections'` : ''}
991
+ import { setLicenseKey } from '@svgrid/enterprise'${toolbarOn ? `\n import { goto } from '$app/navigation'\n import { SvCommand, type CommandItem } from '@svgrid/grid'` : ''}${opts.access ? `\n import { currentRole, canScreen } from '../lib/access'` : ''}${opts.i18n ? `\n import { t, currentLocale, locales } from '../lib/i18n'` : ''}${opts.auth ? `\n import type { LayoutData } from './$types'` : ''}${opts.supabaseAuth ? `\n import { SvAuthGate, type SupabaseAuthClientLike } from '@svgrid/enterprise'\n import { supabaseClient } from '../lib/connections'` : ''}
991
992
 
992
993
  // VITE_SVPRO_KEY in .env is your SvGrid license key; it removes the unlicensed watermark.
993
994
  if (import.meta.env.VITE_SVPRO_KEY) setLicenseKey(import.meta.env.VITE_SVPRO_KEY)
@@ -1013,24 +1014,30 @@ export function layoutFile(nav, opts = {}) {
1013
1014
  try { const s = localStorage.getItem('svapp:theme'); if (s === 'light' || s === 'dark') t = s } catch (_) { /* storage blocked */ }
1014
1015
  applyTheme(t)
1015
1016
  })` : ''}${sideState}${toolbarOn ? `
1016
- // App-chrome quick-search: filter the screens by label as you type.
1017
+ // App-chrome quick-search: filter the screens by label as you type. Only the
1018
+ // screens the sidebar would show${opts.access ? ' to this role' : ''}.
1017
1019
  const initials = ${JSON.stringify(initials)}
1018
1020
  const acctEmail = ${JSON.stringify(`admin@${brandSlug}.com`)}
1021
+ const visibleNav = ${opts.access ? '$derived(nav.filter((n) => !n.id || canScreen($currentRole, n.id)))' : 'nav'}
1019
1022
  let q = $state('')
1020
- const results = $derived(q.trim() ? nav.filter((n) => n.label.toLowerCase().includes(q.trim().toLowerCase())).slice(0, 6) : [])
1023
+ const results = $derived(q.trim() ? visibleNav.filter((n) => n.label.toLowerCase().includes(q.trim().toLowerCase())).slice(0, 6) : [])
1024
+ // Cmd/Ctrl+K: a palette over the same screens. SvCommand binds the key itself.
1025
+ let paletteOpen = $state(false)
1026
+ const commands: CommandItem[] = $derived([
1027
+ ...visibleNav.map((n) => ({ id: 'go:' + n.href, label: ${opts.i18n ? "n.id ? $t('nav.' + n.id, n.label) : n.label" : 'n.label'}, group: 'Go to', onRun: () => goto(n.href) })),${hasSwitch ? `
1028
+ { id: 'theme', label: theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode', group: 'Actions', onRun: toggleTheme },` : ''}
1029
+ ])
1021
1030
  let bellOpen = $state(false)
1022
1031
  let menuOpen = $state(false)
1023
- let searchEl: HTMLInputElement | null = $state(null)
1024
1032
  const notifications = [
1025
1033
  { title: 'Welcome to ' + brand, time: 'just now', color: 'var(--sg-accent, #6366f1)' },
1026
1034
  { title: 'Your workspace is ready', time: '2m ago', color: '#10b981' },
1027
1035
  { title: 'Sample data loaded', time: '5m ago', color: '#f59e0b' },
1028
1036
  ]
1029
- // Cmd/Ctrl+K focuses search; Escape closes the menus.
1037
+ // Escape closes the menus.
1030
1038
  $effect(() => {
1031
1039
  function onKey(e: KeyboardEvent) {
1032
- if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); searchEl?.focus() }
1033
- else if (e.key === 'Escape') { bellOpen = false; menuOpen = false }
1040
+ if (e.key === 'Escape') { bellOpen = false; menuOpen = false }
1034
1041
  }
1035
1042
  window.addEventListener('keydown', onKey)
1036
1043
  return () => window.removeEventListener('keydown', onKey)
@@ -1069,7 +1076,8 @@ ${styles}
1069
1076
  .sv-app__tool:hover { color: var(--sg-fg, #0f172a); border-color: color-mix(in srgb, var(--sg-fg, #0f172a) 20%, var(--sg-border, #e6e8ec)); }
1070
1077
  .sv-app__dot { position: absolute; top: 5px; right: 5px; width: 6px; height: 6px; border-radius: 50%; background: var(--sg-accent, #6366f1); box-shadow: 0 0 0 2px var(--sg-bg, #fff); }
1071
1078
  .sv-app__avatar { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; border: none; border-radius: 50%; font: inherit; font-size: 12px; font-weight: 700; color: #fff; background: var(--sg-accent, #6366f1); letter-spacing: 0.02em; cursor: pointer; }
1072
- .sv-app__kbd { position: absolute; right: 8px; font-size: 10.5px; font-weight: 600; color: var(--sg-muted, #94a3b8); background: color-mix(in srgb, var(--sg-fg, #0f172a) 6%, transparent); border: 1px solid var(--sg-border, #e6e8ec); border-radius: 5px; padding: 1px 5px; pointer-events: none; }
1079
+ .sv-app__kbd { position: absolute; right: 8px; font-size: 10.5px; font-weight: 600; color: var(--sg-muted, #94a3b8); background: color-mix(in srgb, var(--sg-fg, #0f172a) 6%, transparent); border: 1px solid var(--sg-border, #e6e8ec); border-radius: 5px; padding: 1px 5px; font-family: inherit; line-height: inherit; cursor: pointer; }
1080
+ .sv-app__kbd:hover { color: var(--sg-fg, #0f172a); }
1073
1081
  .sv-app__pop { position: relative; }
1074
1082
  .sv-app__scrim { position: fixed; inset: 0; z-index: 44; border: 0; padding: 0; background: transparent; cursor: default; }
1075
1083
  .sv-app__menu { position: absolute; top: calc(100% + 8px); right: 0; z-index: 46; min-width: 232px; padding: 6px; background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e6e8ec); border-radius: 11px; box-shadow: 0 16px 40px -10px rgba(15, 23, 42, 0.3); }
package/dist/version.d.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * `version.test.ts` asserts this matches `package.json`, so the two cannot drift.
12
12
  */
13
- export declare const SVGRID_VERSION = "3.1.25";
13
+ export declare const SVGRID_VERSION = "3.1.27";
14
14
  /**
15
15
  * The version of `@svgrid/grid`, which releases on its own line: enterprise is
16
16
  * on 3.1.x while the grid is on 3.0.x.
@@ -23,4 +23,4 @@ export declare const SVGRID_VERSION = "3.1.25";
23
23
  *
24
24
  * `version.test.ts` asserts this matches packages/grid/package.json.
25
25
  */
26
- export declare const GRID_VERSION = "3.0.22";
26
+ export declare const GRID_VERSION = "3.0.23";
package/dist/version.js CHANGED
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * `version.test.ts` asserts this matches `package.json`, so the two cannot drift.
12
12
  */
13
- export const SVGRID_VERSION = '3.1.25';
13
+ export const SVGRID_VERSION = '3.1.27';
14
14
  /**
15
15
  * The version of `@svgrid/grid`, which releases on its own line: enterprise is
16
16
  * on 3.1.x while the grid is on 3.0.x.
@@ -23,4 +23,4 @@ export const SVGRID_VERSION = '3.1.25';
23
23
  *
24
24
  * `version.test.ts` asserts this matches packages/grid/package.json.
25
25
  */
26
- export const GRID_VERSION = '3.0.22';
26
+ export const GRID_VERSION = '3.0.23';
@@ -988,7 +988,7 @@ function Mt(r, u) {
988
988
  let e = n(Y) ?? n(J);
989
989
  if (!e) return;
990
990
  let t = n(Q).find((t) => t.id === e);
991
- ue(t?.columnDef) && (n(Kt) || import("./SvDateTimePicker-DmK_QkkU.js").then((e) => y(Kt, e.default, !0)), n(qt) || import("./SvDateRangeInput-CJZohgBe.js").then((e) => y(qt, e.default, !0)));
991
+ ue(t?.columnDef) && (n(Kt) || import("./SvDateTimePicker-DmK_QkkU.js").then((e) => y(Kt, e.default, !0)), n(qt) || import("./SvDateRangeInput-DHYuAJeY.js").then((e) => y(qt, e.default, !0)));
992
992
  });
993
993
  function Jt(e, t) {
994
994
  let n = () => {
@@ -107,6 +107,7 @@ function A(t, r) {
107
107
  value: e(Se),
108
108
  disabled: L(),
109
109
  ...t,
110
+ role: "combobox",
110
111
  "aria-haspopup": "dialog",
111
112
  "aria-expanded": e(K),
112
113
  onkeydown: De