@juspay/svelte-ui-components 2.80.9 → 2.81.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.
@@ -0,0 +1,48 @@
1
+ import type { JSONValue } from 'type-decoder';
2
+ import type { InputDataType } from '../types';
3
+ import type { IconStackItem } from '../IconStack/properties';
4
+ import type { TableActionGroupCellData, TableAvatarStackCellData, TableButtonCellData, TableCompareCellData, TableInputCellData, TableLinkCellData, TablePopupMenuCellData, TableSelectCellData, TableTagArrayCellItem, TableTagCellData, TableCellValue } from './properties';
5
+ /**
6
+ * Structural narrowing helpers for the built-in cell renderers. Each renderer
7
+ * accepts an arbitrary `TableCellValue` and renders its structured shape only
8
+ * when the value actually matches — scalar values fall back to plain text, so
9
+ * mixed columns (e.g. compare rows next to plain rows) are well-defined.
10
+ * Shapes are rebuilt field by field from the raw JSON rather than asserted.
11
+ */
12
+ export declare const asJsonObject: (value: TableCellValue) => {
13
+ [key: string]: JSONValue;
14
+ } | null;
15
+ export declare const asTagCellData: (value: TableCellValue) => TableTagCellData | null;
16
+ export declare const asTagArrayItems: (value: TableCellValue) => TableTagArrayCellItem[] | null;
17
+ export declare const asAvatarStackData: (value: TableCellValue) => TableAvatarStackCellData | null;
18
+ export declare const asCompareCellData: (value: TableCellValue) => TableCompareCellData | null;
19
+ export declare const asLinkCellData: (value: TableCellValue) => TableLinkCellData | null;
20
+ export declare const asSelectCellData: (value: TableCellValue) => TableSelectCellData | null;
21
+ export declare const asInputCellData: (value: TableCellValue) => TableInputCellData | null;
22
+ /**
23
+ * Narrows a free-form `dataType` string from cell data to the `Input`
24
+ * component's `InputDataType` union, falling back to `'text'` for any
25
+ * unrecognised value.
26
+ */
27
+ export declare const asInputDataType: (value: string | null) => InputDataType;
28
+ export declare const asButtonCellData: (value: TableCellValue) => TableButtonCellData | null;
29
+ export declare const asActionGroupCellData: (value: TableCellValue) => TableActionGroupCellData | null;
30
+ export declare const asPopupMenuCellData: (value: TableCellValue) => TablePopupMenuCellData | null;
31
+ /** Fallback text rendering for a cell value that failed structural narrowing. */
32
+ export declare const cellValueToText: (value: TableCellValue) => string;
33
+ /**
34
+ * First display character for an avatar chip. Falls back to the last
35
+ * alphanumeric character of the id when the label is missing, so each chip
36
+ * renders a distinct character instead of a row of identical placeholders.
37
+ * Indexes by codepoint, not UTF-16 unit, so a non-BMP first character (emoji)
38
+ * renders whole instead of as half a surrogate pair.
39
+ */
40
+ export declare const avatarInitial: (item: {
41
+ id: string;
42
+ label?: string;
43
+ }) => string;
44
+ /** Visible initials chips plus the "+N" overflow count for an avatar stack. */
45
+ export declare const buildAvatarStack: (data: TableAvatarStackCellData) => {
46
+ icons: IconStackItem[];
47
+ rest: number;
48
+ };
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Structural narrowing helpers for the built-in cell renderers. Each renderer
3
+ * accepts an arbitrary `TableCellValue` and renders its structured shape only
4
+ * when the value actually matches — scalar values fall back to plain text, so
5
+ * mixed columns (e.g. compare rows next to plain rows) are well-defined.
6
+ * Shapes are rebuilt field by field from the raw JSON rather than asserted.
7
+ */
8
+ export const asJsonObject = (value) => {
9
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
10
+ return value;
11
+ }
12
+ return null;
13
+ };
14
+ export const asTagCellData = (value) => {
15
+ const record = asJsonObject(value);
16
+ if (record === null || typeof record.text !== 'string') {
17
+ return null;
18
+ }
19
+ const tagData = { text: record.text };
20
+ if (typeof record.classes === 'string') {
21
+ tagData.classes = record.classes;
22
+ }
23
+ if (typeof record.dismissible === 'boolean') {
24
+ tagData.dismissible = record.dismissible;
25
+ }
26
+ if (typeof record.testId === 'string') {
27
+ tagData.testId = record.testId;
28
+ }
29
+ return tagData;
30
+ };
31
+ export const asTagArrayItems = (value) => {
32
+ if (!Array.isArray(value)) {
33
+ return null;
34
+ }
35
+ const items = [];
36
+ for (const rawItem of value) {
37
+ const record = asJsonObject(rawItem);
38
+ if (record === null || typeof record.text !== 'string') {
39
+ return null;
40
+ }
41
+ const item = { text: record.text };
42
+ if (typeof record.classes === 'string') {
43
+ item.classes = record.classes;
44
+ }
45
+ items.push(item);
46
+ }
47
+ return items;
48
+ };
49
+ export const asAvatarStackData = (value) => {
50
+ const record = asJsonObject(value);
51
+ if (record === null || !Array.isArray(record.items)) {
52
+ return null;
53
+ }
54
+ const items = [];
55
+ for (const rawItem of record.items) {
56
+ const itemRecord = asJsonObject(rawItem);
57
+ if (itemRecord === null || typeof itemRecord.id !== 'string') {
58
+ return null;
59
+ }
60
+ const item = { id: itemRecord.id };
61
+ if (typeof itemRecord.label === 'string') {
62
+ item.label = itemRecord.label;
63
+ }
64
+ items.push(item);
65
+ }
66
+ const stackData = { items };
67
+ if (typeof record.max === 'number') {
68
+ stackData.max = record.max;
69
+ }
70
+ return stackData;
71
+ };
72
+ export const asCompareCellData = (value) => {
73
+ const record = asJsonObject(value);
74
+ if (record === null) {
75
+ return null;
76
+ }
77
+ const compareData = {};
78
+ if (typeof record.primary === 'string') {
79
+ compareData.primary = record.primary;
80
+ }
81
+ if (typeof record.comparison === 'string') {
82
+ compareData.comparison = record.comparison;
83
+ }
84
+ if (typeof record.trendPercent === 'number') {
85
+ compareData.trendPercent = record.trendPercent;
86
+ }
87
+ if (typeof record.trendLabel === 'string') {
88
+ compareData.trendLabel = record.trendLabel;
89
+ }
90
+ return compareData;
91
+ };
92
+ export const asLinkCellData = (value) => {
93
+ if (typeof value === 'string' && value.length > 0) {
94
+ return { url: value };
95
+ }
96
+ const record = asJsonObject(value);
97
+ if (record === null || typeof record.url !== 'string') {
98
+ return null;
99
+ }
100
+ const linkData = { url: record.url };
101
+ if (typeof record.label === 'string') {
102
+ linkData.label = record.label;
103
+ }
104
+ if (typeof record.copyable === 'boolean') {
105
+ linkData.copyable = record.copyable;
106
+ }
107
+ return linkData;
108
+ };
109
+ export const asSelectCellData = (value) => {
110
+ const record = asJsonObject(value);
111
+ if (record === null || !Array.isArray(record.options)) {
112
+ return null;
113
+ }
114
+ const options = [];
115
+ for (const rawOption of record.options) {
116
+ const optionRecord = asJsonObject(rawOption);
117
+ if (optionRecord === null ||
118
+ typeof optionRecord.id !== 'string' ||
119
+ typeof optionRecord.label !== 'string') {
120
+ return null;
121
+ }
122
+ options.push({ id: optionRecord.id, label: optionRecord.label });
123
+ }
124
+ const selectData = { options };
125
+ if (typeof record.selectedId === 'string') {
126
+ selectData.selectedId = record.selectedId;
127
+ }
128
+ if (typeof record.placeholder === 'string') {
129
+ selectData.placeholder = record.placeholder;
130
+ }
131
+ if (typeof record.disabled === 'boolean') {
132
+ selectData.disabled = record.disabled;
133
+ }
134
+ if (typeof record.testId === 'string') {
135
+ selectData.testId = record.testId;
136
+ }
137
+ return selectData;
138
+ };
139
+ export const asInputCellData = (value) => {
140
+ const record = asJsonObject(value);
141
+ if (record === null) {
142
+ return null;
143
+ }
144
+ const inputData = {};
145
+ if (typeof record.value === 'string') {
146
+ inputData.value = record.value;
147
+ }
148
+ if (typeof record.placeholder === 'string') {
149
+ inputData.placeholder = record.placeholder;
150
+ }
151
+ if (typeof record.disabled === 'boolean') {
152
+ inputData.disabled = record.disabled;
153
+ }
154
+ if (typeof record.testId === 'string') {
155
+ inputData.testId = record.testId;
156
+ }
157
+ if (typeof record.dataType === 'string') {
158
+ inputData.dataType = record.dataType;
159
+ }
160
+ if (typeof record.validationPattern === 'string') {
161
+ inputData.validationPattern = record.validationPattern;
162
+ }
163
+ if (typeof record.onErrorMessage === 'string') {
164
+ inputData.onErrorMessage = record.onErrorMessage;
165
+ }
166
+ return inputData;
167
+ };
168
+ /**
169
+ * Narrows a free-form `dataType` string from cell data to the `Input`
170
+ * component's `InputDataType` union, falling back to `'text'` for any
171
+ * unrecognised value.
172
+ */
173
+ export const asInputDataType = (value) => {
174
+ switch (value) {
175
+ case 'tel':
176
+ case 'password':
177
+ case 'email':
178
+ case 'number':
179
+ return value;
180
+ default:
181
+ return 'text';
182
+ }
183
+ };
184
+ export const asButtonCellData = (value) => {
185
+ const record = asJsonObject(value);
186
+ if (record === null || typeof record.text !== 'string') {
187
+ return null;
188
+ }
189
+ const buttonData = { text: record.text };
190
+ if (typeof record.disabled === 'boolean') {
191
+ buttonData.disabled = record.disabled;
192
+ }
193
+ if (typeof record.classes === 'string') {
194
+ buttonData.classes = record.classes;
195
+ }
196
+ if (typeof record.testId === 'string') {
197
+ buttonData.testId = record.testId;
198
+ }
199
+ return buttonData;
200
+ };
201
+ const asMenuItemsData = (rawItems) => {
202
+ if (!Array.isArray(rawItems)) {
203
+ return null;
204
+ }
205
+ const menuItems = [];
206
+ for (const rawItem of rawItems) {
207
+ const record = asJsonObject(rawItem);
208
+ if (record === null || typeof record.id !== 'string') {
209
+ return null;
210
+ }
211
+ const item = { id: record.id };
212
+ if (typeof record.label === 'string') {
213
+ item.label = record.label;
214
+ }
215
+ if (typeof record.danger === 'boolean') {
216
+ item.danger = record.danger;
217
+ }
218
+ if (typeof record.separator === 'boolean') {
219
+ item.separator = record.separator;
220
+ }
221
+ menuItems.push(item);
222
+ }
223
+ return menuItems;
224
+ };
225
+ export const asActionGroupCellData = (value) => {
226
+ const record = asJsonObject(value);
227
+ if (record === null) {
228
+ return null;
229
+ }
230
+ const primaryButton = asButtonCellData(record.primaryButton ?? null);
231
+ const menuItems = asMenuItemsData(record.menuItems ?? null);
232
+ if (primaryButton === null && menuItems === null) {
233
+ return null;
234
+ }
235
+ const actionData = {};
236
+ if (primaryButton !== null) {
237
+ actionData.primaryButton = primaryButton;
238
+ }
239
+ if (menuItems !== null) {
240
+ actionData.menuItems = menuItems;
241
+ }
242
+ return actionData;
243
+ };
244
+ export const asPopupMenuCellData = (value) => {
245
+ const record = asJsonObject(value);
246
+ if (record === null) {
247
+ return null;
248
+ }
249
+ const items = asMenuItemsData(record.items ?? null);
250
+ if (items === null || items.length === 0) {
251
+ return null;
252
+ }
253
+ const popupData = { items };
254
+ if (typeof record.ariaLabel === 'string') {
255
+ popupData.ariaLabel = record.ariaLabel;
256
+ }
257
+ return popupData;
258
+ };
259
+ /** Fallback text rendering for a cell value that failed structural narrowing. */
260
+ export const cellValueToText = (value) => {
261
+ if (value === null || typeof value === 'undefined' || value === '') {
262
+ return '-';
263
+ }
264
+ if (typeof value === 'object') {
265
+ return '-';
266
+ }
267
+ return String(value);
268
+ };
269
+ /**
270
+ * First display character for an avatar chip. Falls back to the last
271
+ * alphanumeric character of the id when the label is missing, so each chip
272
+ * renders a distinct character instead of a row of identical placeholders.
273
+ * Indexes by codepoint, not UTF-16 unit, so a non-BMP first character (emoji)
274
+ * renders whole instead of as half a surrogate pair.
275
+ */
276
+ export const avatarInitial = (item) => {
277
+ if (item.label && Array.from(item.label).length > 0) {
278
+ return Array.from(item.label)[0].toUpperCase();
279
+ }
280
+ const tail = item.id.replace(/[^A-Za-z0-9]/g, '').slice(-1);
281
+ return tail.toUpperCase() || '#';
282
+ };
283
+ /** Visible initials chips plus the "+N" overflow count for an avatar stack. */
284
+ export const buildAvatarStack = (data) => {
285
+ const max = data.max ?? 4;
286
+ const items = data.items ?? [];
287
+ const visible = items.slice(0, max);
288
+ return {
289
+ icons: visible.map((item) => ({ type: 'text', content: avatarInitial(item) })),
290
+ rest: items.length - visible.length
291
+ };
292
+ };
@@ -0,0 +1,24 @@
1
+ import type { JSONValue } from 'type-decoder';
2
+ import type { TableColumn, TableRow } from './properties';
3
+ /**
4
+ * Result of projecting the keyed column model onto Table's positional engine.
5
+ *
6
+ * - `tableHeaders` — column labels, in column order.
7
+ * - `tableData` — one positional array per row, cells in column order; a key
8
+ * missing from a row projects to `null` (renders as an empty cell).
9
+ * - `sortableColumns` — indices of columns whose `sortable` is not `false`,
10
+ * or `null` when no column opts out (preserving the table-wide default of
11
+ * "all columns sortable").
12
+ */
13
+ export type NormalizedColumns = {
14
+ tableHeaders: string[];
15
+ tableData: Array<JSONValue[]>;
16
+ sortableColumns: number[] | null;
17
+ };
18
+ /**
19
+ * Projects the keyed `columns`/`rows` model onto the positional
20
+ * `tableHeaders`/`tableData` shape that Table's sort/search/selection engine
21
+ * operates on. Pure and deterministic — exported so consumers can unit-test
22
+ * their own column/row assembly against the exact projection Table uses.
23
+ */
24
+ export declare const normalizeColumns: (columns: TableColumn[], rows: TableRow[]) => NormalizedColumns;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Projects the keyed `columns`/`rows` model onto the positional
3
+ * `tableHeaders`/`tableData` shape that Table's sort/search/selection engine
4
+ * operates on. Pure and deterministic — exported so consumers can unit-test
5
+ * their own column/row assembly against the exact projection Table uses.
6
+ */
7
+ export const normalizeColumns = (columns, rows) => {
8
+ const tableHeaders = columns.map((column) => column.label);
9
+ const tableData = rows.map((row) => columns.map((column) => {
10
+ const cellValue = row[column.id];
11
+ return cellValue ?? null;
12
+ }));
13
+ const hasSortOptOut = columns.some((column) => column.sortable === false);
14
+ const sortableColumns = hasSortOptOut
15
+ ? columns.reduce((sortableIndices, column, columnIndex) => {
16
+ if (column.sortable !== false) {
17
+ sortableIndices.push(columnIndex);
18
+ }
19
+ return sortableIndices;
20
+ }, [])
21
+ : null;
22
+ return { tableHeaders, tableData, sortableColumns };
23
+ };
@@ -1,6 +1,218 @@
1
1
  import type { JSONValue } from 'type-decoder';
2
2
  import type { Snippet } from 'svelte';
3
3
  export type SortDirection = 'asc' | 'desc';
4
+ /**
5
+ * Built-in cell renderer vocabulary for the keyed column model.
6
+ *
7
+ * `'text'` (the default) renders the cell value as plain text through the
8
+ * existing engine; `'custom'` delegates rendering entirely to the column's
9
+ * own `cell` snippet. The remaining types are built-in renderers composed
10
+ * purely from library primitives — each expects its matching `Table*CellData`
11
+ * shape as the cell value and falls back to plain text for scalar values.
12
+ */
13
+ export type TableColumnType = 'text' | 'tag' | 'text-tag' | 'two-line-text' | 'icon-label' | 'image-two-line-text' | 'tag-array' | 'avatar-stack' | 'compare' | 'toggle' | 'link' | 'select' | 'input' | 'button' | 'action-group' | 'popup-menu' | 'custom';
14
+ /**
15
+ * Cell value for the keyed row model: plain JSON. Structured cell shapes
16
+ * (`TableTagCellData`, `TableCompareCellData`, …) are JSON-compatible
17
+ * objects, so rows stay serializable; behavior (e.g. a toggle handler)
18
+ * lives on the column, never in row data.
19
+ */
20
+ export type TableCellValue = JSONValue;
21
+ /** Cell shape for `type: 'tag'` — a single Pill. */
22
+ export type TableTagCellData = {
23
+ text: string;
24
+ /** CSS classes forwarded to the Pill (the consumer owns tone mapping). */
25
+ classes?: string;
26
+ dismissible?: boolean;
27
+ testId?: string;
28
+ };
29
+ /** Cell shape for `type: 'text-tag'` — text with an optional trailing Pill. */
30
+ export type TableTextTagCellData = {
31
+ text: string;
32
+ tag?: TableTagCellData;
33
+ };
34
+ /** Cell shape for `type: 'two-line-text'` — primary over secondary line. */
35
+ export type TableTwoLineTextCellData = {
36
+ text1?: string;
37
+ text2?: string;
38
+ };
39
+ /** Cell shape for `type: 'icon-label'` — leading image(s) plus a label. */
40
+ export type TableIconLabelCellData = {
41
+ icons?: string[];
42
+ label?: string;
43
+ };
44
+ /** Cell shape for `type: 'image-two-line-text'` — thumbnail plus two lines. */
45
+ export type TableImageTwoLineTextCellData = {
46
+ imageUrl?: string;
47
+ text1?: string;
48
+ text2?: string;
49
+ };
50
+ /** One chip of a `type: 'tag-array'` cell. */
51
+ export type TableTagArrayCellItem = {
52
+ text: string;
53
+ classes?: string;
54
+ };
55
+ /** Cell shape for `type: 'avatar-stack'` — initials chips with overflow. */
56
+ export type TableAvatarStackCellData = {
57
+ items: Array<{
58
+ id: string;
59
+ label?: string;
60
+ }>;
61
+ /** Chips rendered before collapsing into a "+N" overflow. Default 4. */
62
+ max?: number;
63
+ };
64
+ /**
65
+ * Cell shape for `type: 'compare'` — a primary value over a comparison value
66
+ * with an optional trend row (percent with up/down arrow, or a plain label).
67
+ * Scalar cell values in the same column render as plain text, so compare and
68
+ * plain rows coexist.
69
+ */
70
+ export type TableCompareCellData = {
71
+ primary?: string;
72
+ comparison?: string;
73
+ trendPercent?: number;
74
+ trendLabel?: string;
75
+ };
76
+ /** Cell shape for `type: 'toggle'` — the handler lives on the column. */
77
+ export type TableToggleCellData = {
78
+ checked?: boolean;
79
+ ariaLabel?: string;
80
+ testId?: string;
81
+ };
82
+ /** Cell shape for `type: 'link'` — external link with an optional copy affordance. */
83
+ export type TableLinkCellData = {
84
+ url: string;
85
+ /** Visible text; defaults to the url. */
86
+ label?: string;
87
+ /** Renders a copy-to-clipboard button next to the link. Default true. */
88
+ copyable?: boolean;
89
+ };
90
+ /** Cell shape for `type: 'select'` — the handler lives on the column. */
91
+ export type TableSelectCellData = {
92
+ options: Array<{
93
+ id: string;
94
+ label: string;
95
+ }>;
96
+ selectedId?: string;
97
+ placeholder?: string;
98
+ disabled?: boolean;
99
+ testId?: string;
100
+ };
101
+ /** Cell shape for `type: 'input'` — the handler lives on the column. */
102
+ export type TableInputCellData = {
103
+ value?: string;
104
+ placeholder?: string;
105
+ disabled?: boolean;
106
+ testId?: string;
107
+ /** Input dataType forwarded to the rendered Input ('text' | 'tel' | 'number' | …). */
108
+ dataType?: string;
109
+ /**
110
+ * Live-validation pattern forwarded to the rendered Input. A RegExp SOURCE
111
+ * string (cell data must stay JSON-safe) — compiled at render time.
112
+ */
113
+ validationPattern?: string;
114
+ /** Inline error message shown when the validation pattern rejects the value. */
115
+ onErrorMessage?: string;
116
+ };
117
+ /** Cell shape for `type: 'button'` — the handler lives on the column. */
118
+ export type TableButtonCellData = {
119
+ text: string;
120
+ disabled?: boolean;
121
+ /** CSS classes forwarded to the Button (the consumer owns variant mapping). */
122
+ classes?: string;
123
+ testId?: string;
124
+ };
125
+ /** One overflow-menu entry for action-group / popup-menu cells (JSON-safe). */
126
+ export type TableMenuItemData = {
127
+ id: string;
128
+ /** Visible label; defaults to the id. */
129
+ label?: string;
130
+ danger?: boolean;
131
+ separator?: boolean;
132
+ };
133
+ /** Cell shape for `type: 'action-group'` — primary button plus overflow menu. */
134
+ export type TableActionGroupCellData = {
135
+ primaryButton?: TableButtonCellData;
136
+ menuItems?: TableMenuItemData[];
137
+ };
138
+ /** Cell shape for `type: 'popup-menu'` — a kebab-triggered row menu. */
139
+ export type TablePopupMenuCellData = {
140
+ items: TableMenuItemData[];
141
+ ariaLabel?: string;
142
+ };
143
+ /**
144
+ * Per-column header filter dropdown. Table renders the dropdown mechanics
145
+ * (a Menu beside the header label with the current selection highlighted);
146
+ * the options, selection state, and filtering itself belong to the consumer.
147
+ * Selecting the already-selected option clears the filter (emits `null`).
148
+ */
149
+ export type TableColumnFilterConfig = {
150
+ options: Array<{
151
+ label: string;
152
+ value: string;
153
+ }>;
154
+ selectedValue?: string | null;
155
+ onFilterChange?: (value: string | null) => void;
156
+ };
157
+ /**
158
+ * Keyed row shape for the keyed column model: cell values addressed by
159
+ * `TableColumn.id` instead of array position. Keys missing from a row render
160
+ * as empty cells.
161
+ */
162
+ export type TableRow = Record<string, TableCellValue>;
163
+ /**
164
+ * Column definition for the keyed column model.
165
+ *
166
+ * - `id` — key into each `TableRow` for this column's cell value.
167
+ * - `label` — header text (fills the same role as a `tableHeaders` entry).
168
+ * - `type` — built-in renderer selection; defaults to `'text'`.
169
+ * - `sortable` — per-column sort opt-out; defaults to the table-wide
170
+ * `sortable` prop. Equivalent to listing/omitting the column's index in
171
+ * `sortableColumns`.
172
+ * - `testId` — `data-pw` attribute emitted on this column's header cell.
173
+ * - `cell` — column-scoped renderer snippet receiving the full keyed row and
174
+ * the row index. Takes precedence over the table-wide `cell` snippet for
175
+ * this column; required when `type` is `'custom'`.
176
+ * - `onToggle`/`onSelect`/`onInput`/`onButtonClick`/`onPrimaryAction`/`onMenuAction`
177
+ * — change/action handlers for the matching interactive cell types. Behavior
178
+ * lives on the column so row data stays plain JSON.
179
+ */
180
+ export type TableColumn = {
181
+ id: string;
182
+ label: string;
183
+ type?: TableColumnType;
184
+ sortable?: boolean;
185
+ testId?: string;
186
+ cell?: Snippet<[TableRow, number]>;
187
+ /** Header tooltip text, shown on hover over the column label. */
188
+ tooltip?: string;
189
+ /**
190
+ * Horizontal alignment for this column's header and body cells. When unset,
191
+ * cells follow the table-wide `--table-text-align` (left by default).
192
+ */
193
+ align?: 'left' | 'center' | 'right';
194
+ /**
195
+ * Caps the column width (any CSS length). Overflowing scalar cell text
196
+ * ellipsizes with the full value available on the native title tooltip.
197
+ */
198
+ maxWidth?: string;
199
+ /** Opt-in header filter dropdown (see TableColumnFilterConfig). */
200
+ filter?: TableColumnFilterConfig;
201
+ /**
202
+ * Extracts the comparable value for client-side sorting of this column —
203
+ * the seam for currency/date/locale-aware sorting without that logic
204
+ * entering the library. When absent, the built-in comparator is used on
205
+ * the cell value itself.
206
+ */
207
+ getSortValue?: (row: TableRow, rowIndex: number) => string | number | boolean;
208
+ /** `checked` is the NEW state after the flip, not the pre-click value. */
209
+ onToggle?: (rowIndex: number, checked: boolean) => void;
210
+ onSelect?: (rowIndex: number, selectedId: string) => void;
211
+ onInput?: (rowIndex: number, value: string) => void;
212
+ onButtonClick?: (rowIndex: number) => void;
213
+ onPrimaryAction?: (rowIndex: number) => void;
214
+ onMenuAction?: (rowIndex: number, itemId: string) => void;
215
+ };
4
216
  /**
5
217
  * Configuration for row checkbox selection (C2-1).
6
218
  *
@@ -26,6 +238,47 @@ export type TableCheckboxSelectionConfig = {
26
238
  onSelectionChange?: (selectedIds: Set<string>) => void;
27
239
  getRowId?: (row: JSONValue[], rowIndex: number) => string;
28
240
  disabledRowIds?: Set<string>;
241
+ /**
242
+ * Controlled-selection overlay. Omitted: today's uncontrolled behavior via
243
+ * an internal set, unchanged. Provided: Table renders selection FROM this
244
+ * set and never mutates it — `onSelectionChange` reports the would-be next
245
+ * set and the consumer decides. Required for cross-page-persistent
246
+ * selection under server pagination.
247
+ */
248
+ selectedIds?: Set<string>;
249
+ /**
250
+ * Generic DOM-attribute spread onto each row checkbox (`rowIndex` is `-1`
251
+ * for the header select-all). An escape hatch for consumer-specific
252
+ * attributes (e.g. native test IDs) without the library learning them.
253
+ */
254
+ getRowAttributes?: (rowId: string, rowIndex: number) => Record<string, string>;
255
+ };
256
+ /**
257
+ * Built-in paginator config. `'client'` mode slices rows internally;
258
+ * `'server'` mode leaves the supplied rows untouched (they are the current
259
+ * page) and drives the paginator from `page`/`totalItems`/`hasMore`.
260
+ * A consumer `paginatorSlot` takes precedence over the built-in paginator.
261
+ */
262
+ export type TablePaginationConfig = {
263
+ mode?: 'client' | 'server';
264
+ /** 1-indexed current page. Server mode: controlled by the consumer. */
265
+ page?: number;
266
+ /** Rows per page. Default 10. */
267
+ pageSize?: number;
268
+ /** Page-size selector options. Default [10, 25, 50, 100]; `[]` hides the selector. */
269
+ pageSizeOptions?: number[];
270
+ /** Total row count (server mode). Client mode derives it from the data. */
271
+ totalItems?: number;
272
+ /** Cursor-mode hint forwarded to the paginator's load-more affordance. */
273
+ hasMore?: boolean;
274
+ /** Disables the paginator and page-size selector during a fetch. */
275
+ isLoading?: boolean;
276
+ /** Range text override; default "{from}-{to} of {total}". */
277
+ rangeLabel?: (from: number, to: number, total: number) => string;
278
+ onPageChange?: (page: number) => void;
279
+ onPageSizeChange?: (pageSize: number) => void;
280
+ onLoadMore?: () => void;
281
+ testId?: string;
29
282
  };
30
283
  /**
31
284
  * Configuration for the built-in search bar (C2-3).
@@ -45,6 +298,35 @@ export type OptionalTableProperties = {
45
298
  tableTitle?: string | null;
46
299
  tableHeaders?: string[];
47
300
  tableData?: Array<JSONValue[]>;
301
+ /**
302
+ * Keyed column model (preferred): column definitions addressed by id.
303
+ * When provided, `columns`/`rows` are normalized internally into the
304
+ * positional `tableHeaders`/`tableData` shape and drive the same engine —
305
+ * the positional props are ignored for that instance. Omit both to keep
306
+ * the positional API exactly as before.
307
+ */
308
+ columns?: TableColumn[];
309
+ /** Keyed row data, addressed by `TableColumn.id`. Used with `columns`. */
310
+ rows?: TableRow[];
311
+ /**
312
+ * `'client'` (default) sorts rows internally on header click, exactly as
313
+ * before. `'server'` keeps the header sort UI and `onSort` callback but
314
+ * skips the internal reorder — the consumer re-orders the data itself
315
+ * (e.g. via a server query).
316
+ */
317
+ sortMode?: 'client' | 'server';
318
+ /** Built-in paginator (see TablePaginationConfig). */
319
+ pagination?: TablePaginationConfig;
320
+ /**
321
+ * Bulk-action bar rendered above the table while the checkbox selection is
322
+ * non-empty. The library owns only placement; buttons, labels, and actions
323
+ * are entirely consumer-rendered content.
324
+ */
325
+ toolbarSlot?: Snippet<[{
326
+ selectedIds: Set<string>;
327
+ }]>;
328
+ /** Prepends a sequential row-number column (1-based, pagination-aware). */
329
+ rowNumberColumn?: boolean;
48
330
  sortable?: boolean;
49
331
  sortableColumns?: number[];
50
332
  stickyHeader?: boolean;