@svgrid/enterprise 1.0.1 → 1.2.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/src/ai.test.ts CHANGED
@@ -70,7 +70,7 @@ beforeEach(() => {
70
70
  // Default to a licensed state so the soft-gate doesn't dump noise on the
71
71
  // console during expected-failure tests. Individual cases clear it when
72
72
  // they want to assert soft-gate behaviour.
73
- setLicenseKey('SVPRO-DEV-TEST')
73
+ setLicenseKey('SVENTERPRISE-DEV-TEST')
74
74
  })
75
75
  afterEach(() => {
76
76
  setAIProvider(null)
package/src/ai.ts CHANGED
@@ -20,13 +20,13 @@
20
20
  * `AIProvider` that knows how to call OpenAI / Anthropic / a local
21
21
  * model / a server-side proxy. We never bundle a model client.
22
22
  *
23
- * License gate: every AI call routes through `assertProLicensed()`,
23
+ * License gate: every AI call routes through `assertEnterpriseLicensed()`,
24
24
  * which soft-gates (still works, adds a watermark, console nudge) when
25
25
  * no key is set so demos and evaluation stay frictionless.
26
26
  */
27
27
 
28
28
  import type { RowData, SvGridApi, TableFeatures } from '@svgrid/grid'
29
- import { assertProLicensed } from './license'
29
+ import { assertEnterpriseLicensed } from './license'
30
30
 
31
31
  // ---------------------------------------------------------------------------
32
32
  // Provider contract
@@ -230,7 +230,7 @@ export async function aiFilter<
230
230
  query: string,
231
231
  opts: AIFilterOptions = {},
232
232
  ): Promise<AIFilterResult> {
233
- assertProLicensed('AI assistant')
233
+ assertEnterpriseLicensed('AI assistant')
234
234
  const schema = buildColumnSchema(api)
235
235
  const prompt =
236
236
  `You are a data-grid filter planner. Translate the user's natural-language ` +
@@ -317,7 +317,7 @@ export async function aiSmartFill<
317
317
  api: SvGridApi<TFeatures, TData>,
318
318
  opts: AISmartFillOptions,
319
319
  ): Promise<AISmartFillResult<TValue>> {
320
- assertProLicensed('AI assistant')
320
+ assertEnterpriseLicensed('AI assistant')
321
321
  if (opts.examples.length === 0) {
322
322
  throw new Error('@svgrid/enterprise/ai: aiSmartFill requires at least one example.')
323
323
  }
@@ -408,7 +408,7 @@ export async function aiSummarize<
408
408
  api: SvGridApi<TFeatures, TData>,
409
409
  opts: AISummarizeOptions,
410
410
  ): Promise<AISummary> {
411
- assertProLicensed('AI assistant')
411
+ assertEnterpriseLicensed('AI assistant')
412
412
  const all = api.getData()
413
413
  const rows: TData[] = (() => {
414
414
  const t = opts.target
@@ -497,7 +497,7 @@ export async function aiClassify<
497
497
  api: SvGridApi<TFeatures, TData>,
498
498
  opts: AIClassifyOptions,
499
499
  ): Promise<AIClassifyResult> {
500
- assertProLicensed('AI assistant')
500
+ assertEnterpriseLicensed('AI assistant')
501
501
  const data = api.getData()
502
502
  const targets = opts.targetRowIndices ?? data.map((_, i) => i)
503
503
  const rubric = opts.classDescriptions
package/src/export.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /// <reference path="./pdfmake-shims.d.ts" />
2
2
  import type { RowData, TableFeatures } from '@svgrid/grid'
3
3
  import type { SvGridApi } from '@svgrid/grid'
4
- import { assertProLicensed } from './license'
4
+ import { assertEnterpriseLicensed } from './license'
5
5
  import { installSmartShim, type SmartDataExporterInstance } from './smart-shim'
6
6
 
7
7
  export type ExportFormat = 'xlsx' | 'pdf' | 'csv' | 'tsv' | 'html'
@@ -126,6 +126,30 @@ export type ExportOptions<TData> = {
126
126
  * Mutually exclusive with `groupBy`. xlsx only.
127
127
  */
128
128
  hierarchical?: boolean
129
+ /**
130
+ * Merged cells to write into the sheet (xlsx / pdf). Each entry spans
131
+ * `colSpan` columns and `rowSpan` rows starting at the given zero-based
132
+ * **body** row / column index (the header row is not counted). Mirrors the
133
+ * grid's own `MergeSpec` shape closely, so grid merges map straight through -
134
+ * convert a `MergeSpec` (`{ rowIndex, columnId, rowspan, colspan }`) by
135
+ * resolving `columnId` to its column index. Mutually exclusive with
136
+ * `groupBy` / `hierarchical`.
137
+ */
138
+ merges?: ReadonlyArray<ExportMerge>
139
+ }
140
+
141
+ /**
142
+ * A merged cell region for {@link ExportOptions.merges}. `row` / `col` are
143
+ * zero-based indices into the exported body (header excluded). A cell that
144
+ * spans two columns to the right is `{ row, col, colSpan: 2 }`.
145
+ */
146
+ export type ExportMerge = {
147
+ row: number
148
+ col: number
149
+ /** Columns to span (default 1). */
150
+ colSpan?: number
151
+ /** Rows to span (default 1). */
152
+ rowSpan?: number
129
153
  }
130
154
 
131
155
  let exporterCtorPromise: Promise<
@@ -398,6 +422,19 @@ function buildExporterOptions<TData>(
398
422
  // hierarchical through here and groupBy through the constructor call
399
423
  // site in exportGrid.
400
424
  if (opts.hierarchical) out.hierarchical = true
425
+ // Merged cells: translate the friendly {row, col, rowSpan, colSpan} shape to
426
+ // Smart's native {cell: [row, col], rowspan, colspan}. Skip 1x1 "merges"
427
+ // (nothing to span). Ignored when grouping/hierarchy owns the row layout.
428
+ if (opts.merges?.length && !opts.groupBy?.length && !opts.hierarchical) {
429
+ const mergedCells = opts.merges
430
+ .map((m) => ({
431
+ cell: [m.row, m.col],
432
+ rowspan: Math.max(1, Math.floor(m.rowSpan ?? 1)),
433
+ colspan: Math.max(1, Math.floor(m.colSpan ?? 1)),
434
+ }))
435
+ .filter((m) => m.rowspan > 1 || m.colspan > 1)
436
+ if (mergedCells.length) out.mergedCells = mergedCells
437
+ }
401
438
  return out
402
439
  }
403
440
 
@@ -432,7 +469,7 @@ export async function exportGrid<
432
469
  TFeatures extends TableFeatures,
433
470
  TData extends RowData,
434
471
  >(api: SvGridApi<TFeatures, TData>, opts: ExportOptions<TData>): Promise<void> {
435
- assertProLicensed('Export')
472
+ assertEnterpriseLicensed('Export')
436
473
  await ensureGlobals(opts.format)
437
474
  const Ctor = await getDataExporter()
438
475
 
@@ -43,7 +43,7 @@ function fakeApi() {
43
43
  }
44
44
 
45
45
  beforeEach(() => {
46
- setLicenseKey('SVPRO-DEV-TEST')
46
+ setLicenseKey('SVENTERPRISE-DEV-TEST')
47
47
  })
48
48
  afterEach(() => {
49
49
  clearLicenseKey()
package/src/import.ts CHANGED
@@ -25,7 +25,7 @@
25
25
  */
26
26
 
27
27
  import type { RowData, SvGridApi, TableFeatures } from '@svgrid/grid'
28
- import { assertProLicensed } from './license'
28
+ import { assertEnterpriseLicensed } from './license'
29
29
 
30
30
  // ---------------------------------------------------------------------------
31
31
  // Public types
@@ -557,7 +557,7 @@ export async function importData<
557
557
  api: SvGridApi<TFeatures, TData>,
558
558
  opts: ImportOptions<TData>,
559
559
  ): Promise<ImportResult<TData>> {
560
- assertProLicensed('Import')
560
+ assertEnterpriseLicensed('Import')
561
561
  const format = sniffFormat(opts.file, opts.format ?? 'auto')
562
562
 
563
563
  let matrix: string[][]
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ export {
4
4
  getLicenseKey,
5
5
  isLicenseKeySet,
6
6
  hasValidLicense,
7
- assertProLicensed,
7
+ assertEnterpriseLicensed,
8
8
  } from './license'
9
9
 
10
10
  export {
@@ -29,7 +29,7 @@ export {
29
29
  type ImportRowError,
30
30
  type ImportValidator,
31
31
  } from './import'
32
- export { installPro, type ProGridApi, type ProAIApi, type ProPivotApi } from './install'
32
+ export { installEnterprise, type EnterpriseGridApi, type EnterpriseAIApi, type EnterprisePivotApi } from './install'
33
33
  export {
34
34
  createStagedEditing,
35
35
  type StagedChange,
@@ -40,7 +40,7 @@ export { dismissUnlicensedNudge } from './watermark'
40
40
  export {
41
41
  showUpgradePrompt,
42
42
  dismissUpgradePrompt,
43
- type ProFeatureLabel,
43
+ type EnterpriseFeatureLabel,
44
44
  } from './upgrade-prompt'
45
45
 
46
46
  export {
@@ -56,6 +56,20 @@ export {
56
56
  type PivotValueConfig,
57
57
  } from './pivot'
58
58
 
59
+ export { default as SvPivotDesigner } from './SvPivotDesigner.svelte'
60
+ export {
61
+ ALL_AGGREGATORS,
62
+ AGG_LABEL,
63
+ EMPTY_LAYOUT,
64
+ defaultLayoutFor,
65
+ type PivotField,
66
+ type PivotValueChip,
67
+ type PivotFilterChip,
68
+ type PivotLayout,
69
+ type PivotPreset,
70
+ type Well,
71
+ } from './pivot-designer'
72
+
59
73
  export {
60
74
  // Provider plumbing
61
75
  setAIProvider,
package/src/install.ts CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  type PivotResult,
18
18
  } from './pivot'
19
19
 
20
- export type ProAIApi<TData extends RowData> = {
20
+ export type EnterpriseAIApi<TData extends RowData> = {
21
21
  /** Natural-language -> filter + sort plan (and optionally apply it). */
22
22
  filter(query: string, opts?: AIFilterOptions): Promise<AIFilterResult>
23
23
  /** Propose values for empty cells based on a few worked examples. */
@@ -32,7 +32,7 @@ export type ProAIApi<TData extends RowData> = {
32
32
  readonly _rowType?: TData
33
33
  }
34
34
 
35
- export type ProPivotApi<
35
+ export type EnterprisePivotApi<
36
36
  TFeatures extends TableFeatures,
37
37
  TData extends RowData,
38
38
  > = {
@@ -54,7 +54,7 @@ export type ProPivotApi<
54
54
  ): PivotResult<TFeatures>
55
55
  }
56
56
 
57
- export type ProGridApi<
57
+ export type EnterpriseGridApi<
58
58
  TFeatures extends TableFeatures,
59
59
  TData extends RowData,
60
60
  > = SvGridApi<TFeatures, TData> & {
@@ -68,10 +68,10 @@ export type ProGridApi<
68
68
  importData(opts: ImportOptions<TData>): Promise<ImportResult<TData>>
69
69
  /** AI helpers (Pro). All calls route through a consumer-registered
70
70
  * AIProvider via setAIProvider(); no model client is bundled. */
71
- ai: ProAIApi<TData>
71
+ ai: EnterpriseAIApi<TData>
72
72
  /** Pivot table builder (Pro). Pure - returns `{ rows, columns }` you
73
73
  * hand to a separate `<SvGrid>` instance. */
74
- pivot: ProPivotApi<TFeatures, TData>
74
+ pivot: EnterprisePivotApi<TFeatures, TData>
75
75
  }
76
76
 
77
77
  /**
@@ -84,11 +84,11 @@ export type ProGridApi<
84
84
  * the console emits a one-time nudge. Revoked or malformed keys throw on
85
85
  * first call.
86
86
  */
87
- export function installPro<
87
+ export function installEnterprise<
88
88
  TFeatures extends TableFeatures,
89
89
  TData extends RowData,
90
- >(api: SvGridApi<TFeatures, TData>): ProGridApi<TFeatures, TData> {
91
- const pro = api as ProGridApi<TFeatures, TData>
90
+ >(api: SvGridApi<TFeatures, TData>): EnterpriseGridApi<TFeatures, TData> {
91
+ const pro = api as EnterpriseGridApi<TFeatures, TData>
92
92
  pro.exportData = (opts) => exportGrid(pro, opts)
93
93
  pro.print = (opts) => printGrid(pro, opts)
94
94
  pro.importData = (opts) => importData(pro, opts)
package/src/license.ts CHANGED
@@ -8,17 +8,17 @@
8
8
  // ────────────────────────────────────────────────────────────────────
9
9
  // null (no key set) -> soft-gate: watermark + console.log,
10
10
  // feature still runs
11
- // does not start with "SVPRO-" -> throws (programmer error)
11
+ // does not start with "SVENTERPRISE-" -> throws (programmer error)
12
12
  // in REVOKED_KEYS -> throws (revoked / leaked / expired)
13
- // starts with "SVPRO-DEV" or
14
- // "SVPRO-EVAL" -> works; one-time console.info notice
15
- // any other "SVPRO-..." -> works silently (paid production)
13
+ // starts with "SVENTERPRISE-DEV" or
14
+ // "SVENTERPRISE-EVAL" -> works; one-time console.info notice
15
+ // any other "SVENTERPRISE-..." -> works silently (paid production)
16
16
 
17
17
  import { REVOKED_KEYS } from './revoked'
18
18
  import { emitUnlicensedNudge } from './watermark'
19
- import { showUpgradePrompt, type ProFeatureLabel } from './upgrade-prompt'
19
+ import { showUpgradePrompt, type EnterpriseFeatureLabel } from './upgrade-prompt'
20
20
 
21
- const VALID_PREFIX = 'SVPRO-'
21
+ const VALID_PREFIX = 'SVENTERPRISE-'
22
22
  let currentKey: string | null = null
23
23
  let noticedDev = false
24
24
 
@@ -56,7 +56,7 @@ export function hasValidLicense(): boolean {
56
56
  return true
57
57
  }
58
58
 
59
- export function assertProLicensed(feature?: ProFeatureLabel): void {
59
+ export function assertEnterpriseLicensed(feature?: EnterpriseFeatureLabel): void {
60
60
  if (currentKey == null) {
61
61
  // Soft-gate: the feature still runs, but the user gets a watermark +
62
62
  // a one-time console.log nudge directing them to pricing, plus a
@@ -68,7 +68,7 @@ export function assertProLicensed(feature?: ProFeatureLabel): void {
68
68
  }
69
69
  if (!currentKey.startsWith(VALID_PREFIX)) {
70
70
  throw new Error(
71
- '@svgrid/enterprise: invalid license key format (expected "SVPRO-..." prefix).',
71
+ '@svgrid/enterprise: invalid license key format (expected "SVENTERPRISE-..." prefix).',
72
72
  )
73
73
  }
74
74
  if (REVOKED_KEYS.has(currentKey)) {
@@ -79,7 +79,7 @@ export function assertProLicensed(feature?: ProFeatureLabel): void {
79
79
  }
80
80
  if (
81
81
  !noticedDev &&
82
- (currentKey.startsWith('SVPRO-DEV') || currentKey.startsWith('SVPRO-EVAL'))
82
+ (currentKey.startsWith('SVENTERPRISE-DEV') || currentKey.startsWith('SVENTERPRISE-EVAL'))
83
83
  ) {
84
84
  // eslint-disable-next-line no-console
85
85
  console.info(
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Types + helpers for the SvPivotDesigner component.
3
+ *
4
+ * Why a separate module: the component file (.svelte) imports these,
5
+ * and so do consumers wiring up custom presets, formatters, or wells.
6
+ * Keeping the pure-TS surface here also lets us unit-test the layout
7
+ * normalisation + serialisation without instantiating the component.
8
+ */
9
+
10
+ import type { CellFormatConfig } from '@svgrid/grid'
11
+ import type { PivotAggregatorId } from './pivot'
12
+
13
+ /** One field the designer offers in its left-rail picker.
14
+ * Dimensions land in Rows by default; measures land in Values. */
15
+ export type PivotField<T extends Record<string, unknown> = Record<string, unknown>> = {
16
+ field: keyof T & string
17
+ /** Display name shown in the picker + on chips. */
18
+ label: string
19
+ /** Default well: 'dimension' -> Rows, 'measure' -> Values. */
20
+ kind: 'dimension' | 'measure'
21
+ /** Optional group name for the field picker (e.g. 'Demographics'). */
22
+ group?: string
23
+ /** Optional formatter applied to value-well chips by default. */
24
+ format?: CellFormatConfig
25
+ /** Optional default aggregator when added to Values (default 'sum'). */
26
+ defaultAgg?: PivotAggregatorId
27
+ }
28
+
29
+ /** A single chip in the Values well. */
30
+ export type PivotValueChip = {
31
+ field: string
32
+ agg: PivotAggregatorId
33
+ label?: string
34
+ format?: CellFormatConfig
35
+ }
36
+
37
+ /** A single chip in the Filters well. `allowed === null` => all values pass. */
38
+ export type PivotFilterChip = {
39
+ field: string
40
+ /** Distinct values that pass the filter. `null` means no restriction. */
41
+ allowed: string[] | null
42
+ }
43
+
44
+ /** The full pivot layout. The designer is a controlled component that
45
+ * reads + writes this single shape via `bind:layout`. */
46
+ export type PivotLayout = {
47
+ rows: string[]
48
+ cols: string[]
49
+ values: PivotValueChip[]
50
+ filters: PivotFilterChip[]
51
+ /** Hide subtotal rows between groups. Default false. */
52
+ hideSubtotals?: boolean
53
+ /** Hide grand-total row + column. Default false. */
54
+ hideGrandTotals?: boolean
55
+ }
56
+
57
+ /** A saved layout the user can pick from the toolbar's preset menu. */
58
+ export type PivotPreset = {
59
+ name: string
60
+ layout: PivotLayout
61
+ }
62
+
63
+ /** A built-in aggregator option. Consumers can pass a subset to
64
+ * `aggregators` to restrict the chip menu (e.g. 'count' only). */
65
+ export const ALL_AGGREGATORS: PivotAggregatorId[] = [
66
+ 'sum', 'avg', 'min', 'max', 'count', 'countDistinct', 'first', 'last',
67
+ ]
68
+
69
+ /** Empty layout used by Reset + the initial state. */
70
+ export const EMPTY_LAYOUT: PivotLayout = {
71
+ rows: [], cols: [], values: [], filters: [],
72
+ }
73
+
74
+ /** Build an initial layout from a field list - useful when the
75
+ * consumer just wants "throw the first dimension in Rows + first
76
+ * measure in Values" defaults. */
77
+ export function defaultLayoutFor<T extends Record<string, unknown>>(
78
+ fields: ReadonlyArray<PivotField<T>>,
79
+ ): PivotLayout {
80
+ const dim = fields.find((f) => f.kind === 'dimension')
81
+ const meas = fields.find((f) => f.kind === 'measure')
82
+ return {
83
+ rows: dim ? [dim.field] : [],
84
+ cols: [],
85
+ values: meas
86
+ ? [{ field: meas.field, agg: meas.defaultAgg ?? 'sum', label: meas.label, format: meas.format }]
87
+ : [],
88
+ filters: [],
89
+ }
90
+ }
91
+
92
+ /** Move / add / remove a chip across the four wells. The well names
93
+ * match the layout shape. */
94
+ export type Well = 'rows' | 'cols' | 'values' | 'filters'
95
+
96
+ /** Pretty label for an aggregator id. */
97
+ export const AGG_LABEL: Record<PivotAggregatorId, string> = {
98
+ sum: 'Sum', avg: 'Average', min: 'Min', max: 'Max',
99
+ count: 'Count', countDistinct: 'Distinct count',
100
+ first: 'First', last: 'Last',
101
+ }
package/src/pivot.test.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  createPivotModel,
8
8
  filterCollapsedPivotRows,
9
9
  pivotAggregators,
10
+ type PivotConfig,
10
11
  type PivotRow,
11
12
  } from './pivot'
12
13
 
@@ -162,15 +163,42 @@ describe('createPivotModel - configuration toggles', () => {
162
163
  expect(r.rows.find((row) => row.__pivotKind === 'grandTotal')).toBeUndefined()
163
164
  })
164
165
 
165
- it('omits row subtotals when rowSubtotals=false', () => {
166
+ it('keeps group headers but blanks their values when rowSubtotals=false', () => {
166
167
  const r = createPivotModel(facts, {
167
168
  rows: ['region', 'salesPerson'],
168
169
  cols: ['quarter'],
169
170
  values: [{ field: 'amount', agg: 'sum' }],
170
171
  rowSubtotals: false,
171
172
  })
172
- // Only leaves + grand total - no `group` rows.
173
- expect(r.rows.filter((row) => row.__pivotKind === 'group')).toEqual([])
173
+ // Group header rows are STILL emitted - the grouping hierarchy is
174
+ // preserved; turning off subtotals never removes a grouping level.
175
+ const groups = r.rows.filter((row) => row.__pivotKind === 'group')
176
+ expect(groups.length).toBeGreaterThan(0)
177
+ expect(groups.map((row) => row.__pivotLabel)).toContain('AMER')
178
+ // ...but each header carries no aggregate values: the value cells are
179
+ // present (so columns line up) and explicitly null.
180
+ for (const g of groups) {
181
+ const valueKeys = Object.keys(g).filter((k) => !k.startsWith('__pivot'))
182
+ expect(valueKeys.length).toBeGreaterThan(0)
183
+ expect(valueKeys.every((k) => (g as Record<string, unknown>)[k] === null)).toBe(true)
184
+ }
185
+ })
186
+
187
+ it('keeps the grand-total value identical whether or not subtotals show', () => {
188
+ const base: PivotConfig<Sale> = {
189
+ rows: ['region', 'salesPerson'],
190
+ cols: ['quarter'],
191
+ values: [{ field: 'amount', agg: 'sum' }],
192
+ }
193
+ const withSub = createPivotModel(facts, { ...base, rowSubtotals: true })
194
+ const noSub = createPivotModel(facts, { ...base, rowSubtotals: false })
195
+ const totalValues = (m: typeof withSub) => {
196
+ const g = m.rows.find((row) => row.__pivotKind === 'grandTotal')!
197
+ return Object.fromEntries(
198
+ Object.entries(g).filter(([k]) => !k.startsWith('__pivot')),
199
+ )
200
+ }
201
+ expect(totalValues(noSub)).toEqual(totalValues(withSub))
174
202
  })
175
203
 
176
204
  it('honours a custom rowSort comparator', () => {
package/src/pivot.ts CHANGED
@@ -238,7 +238,7 @@ function buildColumnTree<TFeatures extends TableFeatures>(
238
238
  const headerCol: ColumnDef<TFeatures, PivotRow> = {
239
239
  id: '__pivotRowHeader',
240
240
  header: rowHeaderLabel,
241
- accessorFn: (row) => row.__pivotLabel,
241
+ fieldFn: (row) => row.__pivotLabel,
242
242
  width: 240,
243
243
  }
244
244
 
@@ -252,7 +252,7 @@ function buildColumnTree<TFeatures extends TableFeatures>(
252
252
  return {
253
253
  id,
254
254
  header: label,
255
- accessorFn: (row) => row[id],
255
+ fieldFn: (row) => row[id],
256
256
  format: value.format,
257
257
  width: 130,
258
258
  } as ColumnDef<TFeatures, PivotRow>
@@ -283,7 +283,7 @@ function buildColumnTree<TFeatures extends TableFeatures>(
283
283
  return {
284
284
  id,
285
285
  header: label,
286
- accessorFn: (row) => row[id],
286
+ fieldFn: (row) => row[id],
287
287
  format: value.format,
288
288
  width: 140,
289
289
  } as ColumnDef<TFeatures, PivotRow>
@@ -349,6 +349,28 @@ function computeRowValues(
349
349
  return result
350
350
  }
351
351
 
352
+ /**
353
+ * Build the same value-column keys as `computeRowValues`, but with every cell
354
+ * `null`. Used for group HEADER rows when `rowSubtotals` is off: the row keeps
355
+ * its place in the hierarchy (label + expand/collapse) yet shows no aggregate
356
+ * numbers. Cells are `null` rather than absent so consumers can distinguish a
357
+ * deliberately-blank subtotal from a missing column.
358
+ */
359
+ function blankRowValues(
360
+ colLeafPaths: ReadonlyArray<ReadonlyArray<unknown>>,
361
+ values: ReadonlyArray<PivotValueConfig<unknown>>,
362
+ includeGrandTotalCol: boolean,
363
+ ): Record<string, null> {
364
+ const result: Record<string, null> = {}
365
+ for (const colPath of colLeafPaths) {
366
+ for (let i = 0; i < values.length; i += 1) result[leafColumnId(colPath, i)] = null
367
+ }
368
+ if (includeGrandTotalCol) {
369
+ for (let i = 0; i < values.length; i += 1) result[leafColumnId(['__total'], i)] = null
370
+ }
371
+ return result
372
+ }
373
+
352
374
  function walkRowTree(
353
375
  node: AxisNode,
354
376
  out: PivotRow[],
@@ -387,11 +409,16 @@ function walkRowTree(
387
409
  return
388
410
  }
389
411
 
390
- // Group node. Emit a subtotal-style row IF requested AND this node has
391
- // a real path (not the synthetic root) AND it's not at the deepest level.
412
+ // Group node. Emit the group HEADER row whenever this node is a real group
413
+ // (not the synthetic root, not the deepest level). The header is emitted
414
+ // regardless of `rowSubtotals` so the grouping hierarchy and the
415
+ // expand/collapse parent chain stay intact; the flag only controls whether
416
+ // the header carries its aggregate (subtotal) values or renders as a
417
+ // label-only row. This matches Excel / AG Grid, where hiding subtotals never
418
+ // removes a grouping level from the row axis.
392
419
  const isRealGroup = node.level > 0 && node.level < rowFields.length
393
420
  let nextParentId: string | null = parentId
394
- if (isRealGroup && wantSubtotals) {
421
+ if (isRealGroup) {
395
422
  const id = `group__${node.pathKey}`
396
423
  out.push({
397
424
  __pivotId: id,
@@ -400,13 +427,15 @@ function walkRowTree(
400
427
  __pivotLabel: String(node.value ?? '(All)'),
401
428
  __pivotParentId: parentId,
402
429
  __pivotExpandable: true,
403
- ...computeRowValues(
404
- node.rows as ReadonlyArray<Record<string, unknown>>,
405
- colLeafPaths,
406
- colFields,
407
- values,
408
- includeGrandTotalCol,
409
- ),
430
+ ...(wantSubtotals
431
+ ? computeRowValues(
432
+ node.rows as ReadonlyArray<Record<string, unknown>>,
433
+ colLeafPaths,
434
+ colFields,
435
+ values,
436
+ includeGrandTotalCol,
437
+ )
438
+ : blankRowValues(colLeafPaths, values, includeGrandTotalCol)),
410
439
  })
411
440
  // Children of this group point at this row as their parent so a
412
441
  // collapse hides the whole subtree, not just the immediate leaves.
package/src/print.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { RowData, TableFeatures, SvGridApi } from '@svgrid/grid'
2
- import { assertProLicensed } from './license'
2
+ import { assertEnterpriseLicensed } from './license'
3
3
  import { installSmartShim } from './smart-shim'
4
4
  import type { ExportColumn } from './export'
5
5
 
@@ -51,7 +51,7 @@ export async function printGrid<
51
51
  TFeatures extends TableFeatures,
52
52
  TData extends RowData,
53
53
  >(api: SvGridApi<TFeatures, TData>, opts?: PrintOptions<TData>): Promise<void> {
54
- assertProLicensed('Print')
54
+ assertEnterpriseLicensed('Print')
55
55
  const sourceRows: ReadonlyArray<TData> = opts?.rows ?? api.getDisplayedRows()
56
56
  let cols: ReadonlyArray<ExportColumn>
57
57
  if (opts?.columns && opts.columns.length > 0) {
package/src/revoked.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // Revoked license keys. A key in this set throws on use even if it has the
2
- // valid SVPRO- prefix. Use this to invalidate keys that have been leaked,
2
+ // valid SVENTERPRISE- prefix. Use this to invalidate keys that have been leaked,
3
3
  // shared outside their seat count, or issued for trials that have expired.
4
4
  //
5
5
  // =============================================================================
@@ -44,6 +44,6 @@
44
44
  // =============================================================================
45
45
 
46
46
  export const REVOKED_KEYS: ReadonlySet<string> = new Set<string>([
47
- // 'SVPRO-EVAL-acme-202506-3F7K9P', // example - expired ACME trial
48
- // 'SVPRO-LEAKED-globex-202612-X8N4P', // example - key leaked on GitHub
47
+ // 'SVENTERPRISE-EVAL-acme-202506-3F7K9P', // example - expired ACME trial
48
+ // 'SVENTERPRISE-LEAKED-globex-202612-X8N4P', // example - key leaked on GitHub
49
49
  ])
@@ -8,7 +8,7 @@
8
8
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
9
9
  import { showUpgradePrompt, dismissUpgradePrompt } from './upgrade-prompt'
10
10
 
11
- const CARD_ATTR = 'data-@svgrid/enterprise-upgrade'
11
+ const CARD_ATTR = 'data-svgrid-enterprise-upgrade'
12
12
 
13
13
  function card(): HTMLElement | null {
14
14
  return document.querySelector(`[${CARD_ATTR}]`)
@@ -17,16 +17,16 @@
17
17
  // - Zero dependencies, inline styles - no CSS file to import, nothing to
18
18
  // bundle, works in any host app regardless of its styling.
19
19
 
20
- const CARD_ATTR = 'data-@svgrid/enterprise-upgrade'
20
+ const CARD_ATTR = 'data-svgrid-enterprise-upgrade'
21
21
  const PRICING_URL = 'https://www.svgrid.com/pricing'
22
22
  // ?ref=in-app lets us measure how many trials start from this exact prompt
23
23
  // vs. the pricing page itself - the whole point of a moment-of-intent CTA.
24
- const TRIAL_URL = 'https://www.svgrid.com/pricing?ref=in-app&utm_source=@svgrid/enterprise&utm_medium=upgrade-prompt'
24
+ const TRIAL_URL = 'https://www.svgrid.com/pricing?ref=in-app&utm_source=svgrid-enterprise&utm_medium=upgrade-prompt'
25
25
 
26
26
  let shownThisSession = false
27
27
 
28
28
  /** Human-readable labels for the Pro surfaces that gate behind a license. */
29
- export type ProFeatureLabel =
29
+ export type EnterpriseFeatureLabel =
30
30
  | 'Export'
31
31
  | 'Import'
32
32
  | 'Print'
@@ -38,7 +38,7 @@ export type ProFeatureLabel =
38
38
  * been shown this session, a card is already up, or we're on the server. Safe
39
39
  * to call on every Pro feature invocation - it self-throttles.
40
40
  */
41
- export function showUpgradePrompt(feature?: ProFeatureLabel | string): void {
41
+ export function showUpgradePrompt(feature?: EnterpriseFeatureLabel | string): void {
42
42
  if (shownThisSession) return
43
43
  if (typeof document === 'undefined' || typeof window === 'undefined') return
44
44
  if (document.querySelector(`[${CARD_ATTR}]`)) return
@@ -72,7 +72,7 @@ function close(card: HTMLElement): void {
72
72
  window.setTimeout(() => card.remove(), 220)
73
73
  }
74
74
 
75
- function buildCard(feature?: ProFeatureLabel | string): HTMLElement {
75
+ function buildCard(feature?: EnterpriseFeatureLabel | string): HTMLElement {
76
76
  const card = document.createElement('div')
77
77
  card.setAttribute(CARD_ATTR, '1')
78
78
  card.setAttribute('role', 'dialog')
@@ -9,7 +9,7 @@
9
9
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
10
10
  import { emitUnlicensedNudge, dismissUnlicensedNudge } from './watermark'
11
11
 
12
- const WATERMARK_ATTR = 'data-@svgrid/enterprise-watermark'
12
+ const WATERMARK_ATTR = 'data-svgrid-enterprise-watermark'
13
13
 
14
14
  function makeGridRoot(): HTMLElement {
15
15
  const el = document.createElement('div')