@svadmin/lite 0.13.0 → 0.14.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.
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { useTranslation } from '@svadmin/core/i18n';
2
3
  export interface KanbanCard {
3
4
  id: string;
4
5
  title: string;
@@ -17,6 +18,10 @@
17
18
  columns: KanbanColumn[];
18
19
  cards?: KanbanCard[];
19
20
  formAction?: string;
21
+ loading?: boolean;
22
+ error?: string | undefined;
23
+ retryHref?: string;
24
+ ariaLabel?: string;
20
25
  class?: string;
21
26
  }
22
27
 
@@ -24,14 +29,54 @@
24
29
  columns = [],
25
30
  cards = [],
26
31
  formAction = '',
32
+ loading = false,
33
+ error,
34
+ retryHref,
35
+ ariaLabel,
27
36
  class: className = '',
28
37
  }: Props = $props();
38
+ const i18n = useTranslation();
39
+ function text(value: unknown): value is string {
40
+ return typeof value === 'string' && value.trim().length > 0 && value.length <= 1000;
41
+ }
42
+ const validationError = $derived.by(() => {
43
+ if (!Array.isArray(columns) || !Array.isArray(cards)) return 'kanban.invalidData';
44
+ if (columns.length > 200 || cards.length > 1000) return 'kanban.limit';
45
+ if (columns.some(column => !column || !text(column.id) || !text(column.title))) return 'kanban.invalidData';
46
+ const ids = new Set(columns.map(column => column.id));
47
+ if (ids.size !== columns.length) return 'kanban.invalidData';
48
+ const cardIds = new Set<string>();
49
+ for (const card of cards) {
50
+ if (!card || !text(card.id) || !text(card.title) || !ids.has(card.columnId) || cardIds.has(card.id)
51
+ || (card.description !== undefined && (typeof card.description !== 'string' || card.description.length > 10_000))
52
+ || (card.tags !== undefined && (!Array.isArray(card.tags) || card.tags.length > 100 || !card.tags.every(text)))
53
+ || (card.priority !== undefined && !['low', 'medium', 'high', 'urgent'].includes(card.priority))) return 'kanban.invalidData';
54
+ cardIds.add(card.id);
55
+ }
56
+ return '';
57
+ });
58
+ const displayError = $derived(error || (validationError ? i18n.t(validationError) : ''));
59
+ function localPath(value: string | undefined): string | undefined {
60
+ return value && (value.startsWith('/') || value.startsWith('?'))
61
+ && !/^\/[\\/]/.test(value) && !value.includes('\\')
62
+ && ![...value].some(char => char.charCodeAt(0) <= 32 || char.charCodeAt(0) === 127) ? value : undefined;
63
+ }
64
+ const safeAction = $derived(localPath(formAction));
65
+ const safeRetry = $derived(localPath(retryHref));
29
66
  </script>
30
67
 
31
- <div class="sv-lite-kanban-board {className}">
68
+ <div class="sv-lite-kanban-board {className}" role="region" aria-label={ariaLabel ?? i18n.t('kanban.label')} aria-busy={loading}>
69
+ {#if loading}
70
+ <div role="status">{i18n.t('common.loading')}</div>
71
+ {:else if displayError}
72
+ <div role="alert">{displayError}</div>
73
+ {#if error && safeRetry}<a href={safeRetry}>{i18n.t('common.retry')}</a>{/if}
74
+ {:else if columns.length === 0}
75
+ <div role="status">{i18n.t('common.noData')}</div>
76
+ {:else}
32
77
  {#each columns as col (col.id)}
33
78
  {@const colCards = cards.filter((c) => c.columnId === col.id)}
34
- <div class="sv-lite-kanban-column">
79
+ <div class="sv-lite-kanban-column" role="region" aria-label={col.title}>
35
80
  <div class="sv-lite-column-header">
36
81
  <span class="sv-lite-column-title">{col.title}</span>
37
82
  <span class="sv-lite-column-count">({colCards.length})</span>
@@ -39,7 +84,7 @@
39
84
 
40
85
  <div class="sv-lite-column-cards">
41
86
  {#each colCards as card (card.id)}
42
- <div class="sv-lite-kanban-card">
87
+ <div class="sv-lite-kanban-card" role="group" aria-label={card.title}>
43
88
  <div class="sv-lite-card-title">{card.title}</div>
44
89
  {#if card.description}
45
90
  <div class="sv-lite-card-desc">{card.description}</div>
@@ -48,23 +93,26 @@
48
93
  {#if card.priority}
49
94
  <span class="sv-lite-priority-badge sv-lite-{card.priority}">{card.priority}</span>
50
95
  {/if}
51
- {#each card.tags ?? [] as tag (tag)}
96
+ {#each card.tags ?? [] as tag, index (index)}
52
97
  <span class="sv-lite-tag-badge">#{tag}</span>
53
98
  {/each}
54
99
  </div>
55
100
  </div>
101
+ {:else}
102
+ <div role="status">{i18n.t('common.noData')}</div>
56
103
  {/each}
57
104
  </div>
58
105
 
59
- {#if formAction}
60
- <form method="POST" action={formAction} class="sv-lite-card-form">
106
+ {#if safeAction && cards.length < 1000}
107
+ <form method="POST" action={safeAction} class="sv-lite-card-form" aria-label={i18n.t('kanban.addTo', { column: col.title })}>
61
108
  <input type="hidden" name="columnId" value={col.id} />
62
- <input type="text" name="title" placeholder="+ Add card..." class="sv-lite-card-input" />
63
- <button type="submit" class="sv-lite-card-btn">Add</button>
109
+ <input type="text" name="title" aria-label={i18n.t('kanban.cardTitle')} placeholder={i18n.t('kanban.cardTitle')} required maxlength={1000} class="sv-lite-card-input" />
110
+ <button type="submit" class="sv-lite-card-btn">{i18n.t('common.add')}</button>
64
111
  </form>
65
112
  {/if}
66
113
  </div>
67
114
  {/each}
115
+ {/if}
68
116
  </div>
69
117
 
70
118
  <style>
@@ -14,6 +14,10 @@ interface Props {
14
14
  columns: KanbanColumn[];
15
15
  cards?: KanbanCard[];
16
16
  formAction?: string;
17
+ loading?: boolean;
18
+ error?: string | undefined;
19
+ retryHref?: string;
20
+ ariaLabel?: string;
17
21
  class?: string;
18
22
  }
19
23
  declare const LiteKanbanBoard: import("svelte").Component<Props, {}, "">;
@@ -1,8 +1,11 @@
1
1
  <script lang="ts">
2
- export type AggregationFn = 'sum' | 'count' | 'avg' | 'min' | 'max';
2
+ import { useTranslation } from '@svadmin/core/i18n';
3
+ import { buildPivot, buildPivotDrilldown, decodePivotResult, formatPivotValue } from '@svadmin/core/pivot';
4
+ export type AggregationFn = import('@svadmin/core/pivot').PivotAggregation;
3
5
 
4
6
  interface Props {
5
7
  data?: Record<string, unknown>[];
8
+ aggregateData?: unknown;
6
9
  rowField: string;
7
10
  rowLabel?: string;
8
11
  columnField: string;
@@ -10,11 +13,23 @@
10
13
  valueField: string;
11
14
  valueLabel?: string;
12
15
  aggregator?: AggregationFn;
16
+ formatValue?: (value: number) => string;
17
+ loading?: boolean;
18
+ error?: string | undefined;
19
+ retryHref?: string;
20
+ exportHref?: string;
21
+ drilldownHref?: (input: import('@svadmin/core/pivot').PivotDrilldown) => string | undefined;
22
+ resource?: string;
23
+ scopeKey?: string;
24
+ filters?: import('@svadmin/core').Filter[];
25
+ meta?: Record<string, unknown>;
26
+ ariaLabel?: string;
13
27
  class?: string;
14
28
  }
15
29
 
16
30
  let {
17
31
  data = [],
32
+ aggregateData,
18
33
  rowField,
19
34
  rowLabel = rowField,
20
35
  columnField,
@@ -22,91 +37,106 @@
22
37
  valueField,
23
38
  valueLabel = valueField,
24
39
  aggregator = 'sum',
40
+ formatValue,
41
+ loading = false,
42
+ error,
43
+ retryHref,
44
+ exportHref,
45
+ drilldownHref,
46
+ resource,
47
+ scopeKey,
48
+ filters,
49
+ meta,
50
+ ariaLabel,
25
51
  class: className = '',
26
52
  }: Props = $props();
27
53
 
28
- const distinctRowValues = $derived(
29
- Array.from(new Set(data.map((d) => String(d[rowField] ?? '—')))).sort()
30
- );
31
-
32
- const distinctColValues = $derived(
33
- Array.from(new Set(data.map((d) => String(d[columnField] ?? '—')))).sort()
34
- );
35
-
36
- function computeAgg(vals: number[]): number {
37
- if (vals.length === 0) return 0;
38
- if (aggregator === 'count') return vals.length;
39
- if (aggregator === 'sum') return vals.reduce((a, b) => a + b, 0);
40
- if (aggregator === 'avg') return vals.reduce((a, b) => a + b, 0) / vals.length;
41
- if (aggregator === 'min') return Math.min(...vals);
42
- if (aggregator === 'max') return Math.max(...vals);
43
- return 0;
54
+ const i18n = useTranslation();
55
+ const pivot = $derived(aggregateData !== undefined ? decodePivotResult(aggregateData)
56
+ : buildPivot(loading || error ? [] : data, { rowField, columnField, valueField, aggregator }));
57
+ const displayError = $derived(error || (pivot.error ? i18n.t(`pivot.${pivot.error}`) : ''));
58
+ // 重试仅接受站内绝对路径,避免在服务端页面输出可执行协议。
59
+ const safeRetryHref = $derived(safeHref(retryHref));
60
+ const safeExportHref = $derived(safeHref(exportHref));
61
+ function safeHref(value: string | undefined): string | undefined {
62
+ return value?.startsWith('/') && !/^\/[\\/]/.test(value)
63
+ && !value.includes('\\')
64
+ && ![...value].some(char => char.charCodeAt(0) <= 32 || char.charCodeAt(0) === 127) ? value : undefined;
44
65
  }
45
-
46
- function getCellValue(r: string, c: string): number {
47
- const vals = data
48
- .filter((d) => String(d[rowField] ?? '—') === r && String(d[columnField] ?? '—') === c)
49
- .map((d) => Number(d[valueField]) || 0);
50
- return computeAgg(vals);
66
+ function cellHref(rowKey: string, columnKey: string): string | undefined {
67
+ if (!drilldownHref || !pivot.cells.get(rowKey)?.has(columnKey)) return;
68
+ const row = pivot.rows.find(item => item.key === rowKey);
69
+ const column = pivot.columns.find(item => item.key === columnKey);
70
+ const query = { resource, rowField, columnField, valueField, aggregator,
71
+ ...(filters === undefined ? {} : { filters }), ...(meta === undefined ? {} : { meta }) };
72
+ const input = buildPivotDrilldown(query, scopeKey, row, column);
73
+ try {
74
+ return safeHref(input ? drilldownHref(input) : undefined);
75
+ } catch {
76
+ return;
77
+ }
51
78
  }
52
-
53
- function getRowTotal(r: string): number {
54
- const vals = data
55
- .filter((d) => String(d[rowField] ?? '—') === r)
56
- .map((d) => Number(d[valueField]) || 0);
57
- return computeAgg(vals);
79
+ function display(value: number | null | undefined): string {
80
+ return formatPivotValue(value, formatValue ?? (v => v.toLocaleString(i18n.locale, { maximumFractionDigits: 2 })));
58
81
  }
59
-
60
- function getColTotal(c: string): number {
61
- const vals = data
62
- .filter((d) => String(d[columnField] ?? '—') === c)
63
- .map((d) => Number(d[valueField]) || 0);
64
- return computeAgg(vals);
65
- }
66
-
67
- const grandTotal = $derived(
68
- computeAgg(data.map((d) => Number(d[valueField]) || 0))
69
- );
70
82
  </script>
71
83
 
72
- <div class="sv-lite-pivot-container {className}">
84
+ <div class="sv-lite-pivot-container {className}" aria-busy={loading}>
73
85
  <div class="sv-lite-pivot-header">
74
- <strong>Pivot Analysis</strong> ({rowLabel} × {columnLabel})
75
- <span class="sv-lite-pivot-sub">Aggregator: {aggregator.toUpperCase()} ({valueLabel})</span>
86
+ <strong>{i18n.t('pivot.title')}</strong> ({rowLabel} × {columnLabel})
87
+ <span class="sv-lite-pivot-sub">{i18n.t(`pivot.${aggregator}`)} ({valueLabel})</span>
88
+ {#if safeExportHref}<a href={safeExportHref}>{i18n.t('common.export')}</a>{/if}
76
89
  </div>
77
90
 
78
- <table class="sv-lite-pivot-table">
91
+ {#if loading}
92
+ <div role="status">{i18n.t('common.loading')}</div>
93
+ {:else if displayError}
94
+ <div role="alert">{displayError}</div>
95
+ {#if error && safeRetryHref}<a href={safeRetryHref}>{i18n.t('common.retry')}</a>{/if}
96
+ {:else if pivot.rows.length === 0}
97
+ <div role="status">{i18n.t('common.noData')}</div>
98
+ {:else}
99
+ <table class="sv-lite-pivot-table" aria-label={ariaLabel ?? `${i18n.t('pivot.title')}: ${rowLabel} / ${columnLabel} / ${valueLabel}`}>
79
100
  <thead>
80
101
  <tr>
81
- <th class="sv-lite-th-dim">{rowLabel} \ {columnLabel}</th>
82
- {#each distinctColValues as col (col)}
83
- <th>{col}</th>
102
+ <th scope="col" class="sv-lite-th-dim">{rowLabel} \ {columnLabel}</th>
103
+ {#each pivot.columns as col (col.key)}
104
+ <th scope="col">{col.label}</th>
84
105
  {/each}
85
- <th class="sv-lite-th-total">Total</th>
106
+ <th scope="col" class="sv-lite-th-total">{i18n.t('pivot.total')}</th>
86
107
  </tr>
87
108
  </thead>
88
109
  <tbody>
89
- {#each distinctRowValues as row (row)}
110
+ {#each pivot.rows as row (row.key)}
90
111
  <tr>
91
- <td class="sv-lite-td-dim">{row}</td>
92
- {#each distinctColValues as col (col)}
93
- {@const val = getCellValue(row, col)}
94
- <td class="sv-lite-td-val">{val.toLocaleString(undefined, { maximumFractionDigits: 2 })}</td>
112
+ <th scope="row" class="sv-lite-td-dim">{row.label}</th>
113
+ {#each pivot.columns as col (col.key)}
114
+ {@const href = cellHref(row.key, col.key)}
115
+ <td class="sv-lite-td-val">
116
+ {#if href}
117
+ <a href={href} aria-label={`${i18n.t('pivot.drilldown')}: ${row.label} / ${col.label}`}>
118
+ {display(pivot.cells.get(row.key)?.get(col.key))}
119
+ </a>
120
+ {:else}
121
+ {display(pivot.cells.get(row.key)?.get(col.key))}
122
+ {/if}
123
+ </td>
95
124
  {/each}
96
- <td class="sv-lite-td-rowtotal">{getRowTotal(row).toLocaleString(undefined, { maximumFractionDigits: 2 })}</td>
125
+ <td class="sv-lite-td-rowtotal">{display(pivot.rowTotals.get(row.key))}</td>
97
126
  </tr>
98
127
  {/each}
99
128
  </tbody>
100
129
  <tfoot>
101
130
  <tr>
102
- <td class="sv-lite-td-dim">Grand Total</td>
103
- {#each distinctColValues as col (col)}
104
- <td class="sv-lite-td-val">{getColTotal(col).toLocaleString(undefined, { maximumFractionDigits: 2 })}</td>
131
+ <th scope="row" class="sv-lite-td-dim">{i18n.t('pivot.grandTotal')}</th>
132
+ {#each pivot.columns as col (col.key)}
133
+ <td class="sv-lite-td-val">{display(pivot.columnTotals.get(col.key))}</td>
105
134
  {/each}
106
- <td class="sv-lite-td-grandtotal">{grandTotal.toLocaleString(undefined, { maximumFractionDigits: 2 })}</td>
135
+ <td class="sv-lite-td-grandtotal">{display(pivot.grandTotal)}</td>
107
136
  </tr>
108
137
  </tfoot>
109
138
  </table>
139
+ {/if}
110
140
  </div>
111
141
 
112
142
  <style>
@@ -1,6 +1,7 @@
1
- export type AggregationFn = 'sum' | 'count' | 'avg' | 'min' | 'max';
1
+ export type AggregationFn = import('@svadmin/core/pivot').PivotAggregation;
2
2
  interface Props {
3
3
  data?: Record<string, unknown>[];
4
+ aggregateData?: unknown;
4
5
  rowField: string;
5
6
  rowLabel?: string;
6
7
  columnField: string;
@@ -8,6 +9,17 @@ interface Props {
8
9
  valueField: string;
9
10
  valueLabel?: string;
10
11
  aggregator?: AggregationFn;
12
+ formatValue?: (value: number) => string;
13
+ loading?: boolean;
14
+ error?: string | undefined;
15
+ retryHref?: string;
16
+ exportHref?: string;
17
+ drilldownHref?: (input: import('@svadmin/core/pivot').PivotDrilldown) => string | undefined;
18
+ resource?: string;
19
+ scopeKey?: string;
20
+ filters?: import('@svadmin/core').Filter[];
21
+ meta?: Record<string, unknown>;
22
+ ariaLabel?: string;
11
23
  class?: string;
12
24
  }
13
25
  declare const LitePivotTable: import("svelte").Component<Props, {}, "">;
@@ -1,4 +1,7 @@
1
1
  <script lang="ts">
2
+ import { useTranslation } from '@svadmin/core/i18n';
3
+ const i18n = useTranslation();
4
+
2
5
  export interface SheetData {
3
6
  id: string;
4
7
  name: string;
@@ -11,6 +14,8 @@
11
14
  sheets?: SheetData[];
12
15
  activeSheetId?: string;
13
16
  formAction?: string;
17
+ readonly?: boolean;
18
+ limits?: Partial<{ maxRows: number; maxCols: number; maxCells: number; maxSheets: number; maxCellLength: number }>;
14
19
  class?: string;
15
20
  }
16
21
 
@@ -26,25 +31,63 @@
26
31
  ],
27
32
  activeSheetId = 'sheet1',
28
33
  formAction = '',
34
+ readonly = false,
35
+ limits: limitOverrides = {},
29
36
  class: className = '',
30
37
  }: Props = $props();
31
38
 
32
- const currentSheet = $derived(
33
- sheets.find((s) => s.id === activeSheetId) ?? sheets[0]
34
- );
39
+ const limits = $derived.by(() => {
40
+ const bounded = (value: number | undefined, ceiling: number) =>
41
+ typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? Math.min(value, ceiling) : ceiling;
42
+ return {
43
+ maxRows: bounded(limitOverrides.maxRows, 1000),
44
+ maxCols: bounded(limitOverrides.maxCols, 100),
45
+ maxCells: bounded(limitOverrides.maxCells, 10_000),
46
+ maxSheets: bounded(limitOverrides.maxSheets, 32),
47
+ maxCellLength: bounded(limitOverrides.maxCellLength, 10_000),
48
+ };
49
+ });
50
+ const validWorkbook = $derived(sheets.length <= limits.maxSheets && new Set(sheets.map(sheet => sheet.id)).size === sheets.length);
51
+ const currentSheet = $derived(validWorkbook ? (sheets.find(s => s.id === activeSheetId) ?? sheets[0]) : undefined);
52
+ const canRender = $derived.by(() => {
53
+ if (!currentSheet || !currentSheet.cells || typeof currentSheet.cells !== 'object'
54
+ || !Number.isSafeInteger(currentSheet.rows) || currentSheet.rows < 1 || currentSheet.rows > limits.maxRows
55
+ || !Number.isSafeInteger(currentSheet.cols) || currentSheet.cols < 1 || currentSheet.cols > limits.maxCols
56
+ || currentSheet.rows * currentSheet.cols > limits.maxCells) return false;
57
+ let textLength = 0;
58
+ for (let row = 1; row <= currentSheet.rows; row++) {
59
+ for (let col = 0; col < currentSheet.cols; col++) {
60
+ const key = `${getColName(col)}${row}`;
61
+ const raw = Object.hasOwn(currentSheet.cells, key) ? currentSheet.cells[key] : '';
62
+ if (typeof raw !== 'string' || raw.length > limits.maxCellLength) return false;
63
+ textLength += raw.length;
64
+ if (textLength > 1_000_000) return false;
65
+ }
66
+ }
67
+ return true;
68
+ });
35
69
 
36
70
  function getColName(index: number): string {
37
- return String.fromCharCode(index + 65);
71
+ let result = '';
72
+ for (let i = index; i >= 0; i = Math.floor(i / 26) - 1) {
73
+ result = String.fromCharCode(i % 26 + 65) + result;
74
+ }
75
+ return result;
38
76
  }
39
77
  </script>
40
78
 
41
79
  <div class="sv-lite-sheet-container {className}">
42
80
  <div class="sv-lite-sheet-header">
43
- <strong>Spreadsheet: {currentSheet?.name ?? 'Sheet'}</strong>
81
+ <strong>{i18n.t('spreadsheet.title')}: {currentSheet?.name ?? ''}</strong>
44
82
  </div>
45
83
 
46
- {#if currentSheet}
84
+ {#if !validWorkbook || (currentSheet && !canRender)}
85
+ <p role="alert">{i18n.t('spreadsheet.invalid')}</p>
86
+ {:else if !currentSheet}
87
+ <p role="status">{i18n.t('spreadsheet.empty')}</p>
88
+ {:else}
47
89
  <form method="POST" action={formAction}>
90
+ <input type="hidden" name="sheetId" value={currentSheet.id} />
48
91
  <table class="sv-lite-sheet-table">
49
92
  <thead>
50
93
  <tr>
@@ -64,8 +107,11 @@
64
107
  <td class="sv-lite-sheet-td">
65
108
  <input
66
109
  type="text"
110
+ {readonly}
111
+ aria-label={key}
112
+ maxlength={limits.maxCellLength}
67
113
  name={`cell_${key}`}
68
- value={currentSheet.cells[key] ?? ''}
114
+ value={Object.hasOwn(currentSheet.cells, key) ? currentSheet.cells[key] ?? '' : ''}
69
115
  class="sv-lite-cell-input"
70
116
  />
71
117
  </td>
@@ -75,9 +121,9 @@
75
121
  </tbody>
76
122
  </table>
77
123
 
78
- {#if formAction}
124
+ {#if formAction && !readonly}
79
125
  <div class="sv-lite-sheet-footer">
80
- <button type="submit" class="sv-lite-sheet-save">Save Spreadsheet</button>
126
+ <button type="submit" class="sv-lite-sheet-save">{i18n.t('common.save')}</button>
81
127
  </div>
82
128
  {/if}
83
129
  </form>
@@ -9,6 +9,14 @@ interface Props {
9
9
  sheets?: SheetData[];
10
10
  activeSheetId?: string;
11
11
  formAction?: string;
12
+ readonly?: boolean;
13
+ limits?: Partial<{
14
+ maxRows: number;
15
+ maxCols: number;
16
+ maxCells: number;
17
+ maxSheets: number;
18
+ maxCellLength: number;
19
+ }>;
12
20
  class?: string;
13
21
  }
14
22
  declare const LiteSpreadsheetView: import("svelte").Component<Props, {}, "">;
package/dist/index.d.ts CHANGED
@@ -14,6 +14,8 @@ export { default as LiteLogin } from './components/LiteLogin.svelte';
14
14
  export { default as LiteShow } from './components/LiteShow.svelte';
15
15
  export { default as LiteSearch } from './components/LiteSearch.svelte';
16
16
  export { default as LiteAlert } from './components/LiteAlert.svelte';
17
+ export { default as LiteErrorSummary } from './components/LiteErrorSummary.svelte';
18
+ export type { LiteFormErrorEntry } from './components/LiteErrorSummary.svelte';
17
19
  export { default as LitePermissionMatrix } from './components/LitePermissionMatrix.svelte';
18
20
  export { default as LiteAuditLog } from './components/LiteAuditLog.svelte';
19
21
  export { default as LiteArrayField } from './components/LiteArrayField.svelte';
@@ -57,6 +59,8 @@ export { default as LiteActivityFeed } from './components/LiteActivityFeed.svelt
57
59
  export { default as LitePresenceAvatarGroup } from './components/LitePresenceAvatarGroup.svelte';
58
60
  export { default as LitePrintableBill } from './components/LitePrintableBill.svelte';
59
61
  export { default as LiteJsonSchemaForm } from './components/LiteJsonSchemaForm.svelte';
62
+ export { decodeSchemaFormArrayAction, decodeSchemaFormData, decodeSchemaFormSubmission, encodeSchemaFormArrayAction, schemaFormActionName, schemaFormFieldName, } from './schema-form-data';
63
+ export type { SchemaFormArrayAction, SchemaFormSchema } from './schema-form-data';
60
64
  export { default as LiteMentionsInput } from './components/LiteMentionsInput.svelte';
61
65
  export { default as LiteKanbanBoard } from './components/LiteKanbanBoard.svelte';
62
66
  export { default as LitePivotTable } from './components/LitePivotTable.svelte';
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ export { default as LiteLogin } from './components/LiteLogin.svelte';
18
18
  export { default as LiteShow } from './components/LiteShow.svelte';
19
19
  export { default as LiteSearch } from './components/LiteSearch.svelte';
20
20
  export { default as LiteAlert } from './components/LiteAlert.svelte';
21
+ export { default as LiteErrorSummary } from './components/LiteErrorSummary.svelte';
21
22
  export { default as LitePermissionMatrix } from './components/LitePermissionMatrix.svelte';
22
23
  export { default as LiteAuditLog } from './components/LiteAuditLog.svelte';
23
24
  export { default as LiteArrayField } from './components/LiteArrayField.svelte';
@@ -57,6 +58,7 @@ export { default as LiteActivityFeed } from './components/LiteActivityFeed.svelt
57
58
  export { default as LitePresenceAvatarGroup } from './components/LitePresenceAvatarGroup.svelte';
58
59
  export { default as LitePrintableBill } from './components/LitePrintableBill.svelte';
59
60
  export { default as LiteJsonSchemaForm } from './components/LiteJsonSchemaForm.svelte';
61
+ export { decodeSchemaFormArrayAction, decodeSchemaFormData, decodeSchemaFormSubmission, encodeSchemaFormArrayAction, schemaFormActionName, schemaFormFieldName, } from './schema-form-data';
60
62
  export { default as LiteMentionsInput } from './components/LiteMentionsInput.svelte';
61
63
  export { default as LiteKanbanBoard } from './components/LiteKanbanBoard.svelte';
62
64
  export { default as LitePivotTable } from './components/LitePivotTable.svelte';
@@ -0,0 +1,18 @@
1
+ import type { SchemaFormSchema } from '@svadmin/core/schema-form';
2
+ export type { SchemaFormSchema } from '@svadmin/core/schema-form';
3
+ export declare function schemaFormFieldName(path: readonly string[]): string;
4
+ export declare const schemaFormActionName = "schemaFormAction";
5
+ export type SchemaFormArrayAction = {
6
+ type: 'add' | 'remove';
7
+ path: string[];
8
+ index?: number;
9
+ };
10
+ export declare function encodeSchemaFormArrayAction(action: SchemaFormArrayAction): string;
11
+ export declare function decodeSchemaFormArrayAction(form: FormData): SchemaFormArrayAction | undefined;
12
+ /** 按可信 Schema 解码并验证支持的约束;不替代资源授权和业务命令校验。 */
13
+ export declare function decodeSchemaFormData(schema: SchemaFormSchema, form: FormData): Record<string, unknown>;
14
+ /** 数组动作只生成下一份草稿;只有 submit 分支才可进入业务写入。 */
15
+ export declare function decodeSchemaFormSubmission(schema: SchemaFormSchema, form: FormData): {
16
+ kind: 'draft' | 'submit';
17
+ data: Record<string, unknown>;
18
+ };