@ds-mo/ui 14.3.0 → 14.4.1

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 (74) hide show
  1. package/dist/.package-ready.json +1 -1
  2. package/dist/agent-patterns.json +1 -1
  3. package/dist/agent.json +43 -11
  4. package/dist/angular/ds-pagination.d.ts +1 -1
  5. package/dist/angular/ds-pagination.js +3 -3
  6. package/dist/angular/ds-select.d.ts +1 -1
  7. package/dist/angular/ds-select.js +3 -3
  8. package/dist/components/ds-bar-nav.js +1 -1
  9. package/dist/components/ds-bar-title.js +1 -1
  10. package/dist/components/ds-bar-workflow.js +1 -1
  11. package/dist/components/ds-menu.js +1 -1
  12. package/dist/components/ds-mobile-header.js +1 -1
  13. package/dist/components/ds-mobile-section-switcher.js +1 -1
  14. package/dist/components/ds-pagination.js +1 -1
  15. package/dist/components/ds-panel-nav.js +1 -1
  16. package/dist/components/ds-panel-tool-header.js +1 -1
  17. package/dist/components/ds-panel-tool-search.js +1 -1
  18. package/dist/components/ds-panel-tools.js +1 -1
  19. package/dist/components/ds-select.js +1 -1
  20. package/dist/components/ds-shell-app.js +1 -1
  21. package/dist/components/ds-shell-page.js +1 -1
  22. package/dist/components/ds-shell-tools.js +1 -1
  23. package/dist/components/ds-swatch-picker.js +1 -1
  24. package/dist/components/ds-table.js +1 -1
  25. package/dist/components/ds-table.js.map +1 -1
  26. package/dist/components/ds-toast.js +1 -1
  27. package/dist/components/ds-typing-indicator.js +1 -1
  28. package/dist/components/{p-Bxe6532y.js → p-B2hqUkg-.js} +2 -2
  29. package/dist/components/{p-qA4eKZkW.js → p-BGGm1NB1.js} +2 -2
  30. package/dist/components/{p-CW3sRYuf.js → p-BXNLaPbw.js} +2 -2
  31. package/dist/components/{p-Cjf54_Cq.js → p-Ba1l3Yot.js} +2 -2
  32. package/dist/components/{p-CkWVI16a.js → p-BiIdszv1.js} +2 -2
  33. package/dist/components/{p-DwqUW4Rg.js → p-CRLRjtsc.js} +2 -2
  34. package/dist/components/p-CRLRjtsc.js.map +1 -0
  35. package/dist/components/{p-Dh_TVLnz.js → p-CXqVVdW2.js} +2 -2
  36. package/dist/components/{p-CWsziQQY.js → p-Ccq1RmOb.js} +2 -2
  37. package/dist/components/p-CvpsV-jC.js +2 -0
  38. package/dist/components/p-CvpsV-jC.js.map +1 -0
  39. package/dist/components/{p-Cpqd69kg.js → p-DgWGfscR.js} +2 -2
  40. package/dist/components/{p-BiBXCjjo.js → p-DjqZWBnM.js} +2 -2
  41. package/dist/components/{p-C68-8tlp.js → p-o1v7Choa.js} +2 -2
  42. package/dist/components/{p-BSBQ5kGe.js → p-w64TmQd-.js} +2 -2
  43. package/dist/docs/components.json +250 -13
  44. package/dist/mcp-data/registry/pagination.json +120 -13
  45. package/dist/mcp-data/registry/registry.json +42 -10
  46. package/dist/mcp-data/registry/select.json +20 -2
  47. package/dist/mcp-data/registry/table.json +19 -11
  48. package/dist/styles/table.css +11 -0
  49. package/dist/types/components/Pagination/Pagination.d.ts +15 -1
  50. package/dist/types/components/Pagination/index.d.ts +1 -1
  51. package/dist/types/components/Pagination/pagination-model.d.ts +3 -2
  52. package/dist/types/components/Pagination/pagination-types.d.ts +13 -1
  53. package/dist/types/components/Select/Select.d.ts +3 -0
  54. package/dist/types/components/Select/index.d.ts +1 -1
  55. package/dist/types/components/Table/Table.d.ts +13 -0
  56. package/dist/types/components/Table/index.d.ts +1 -1
  57. package/dist/types/components/Table/table-pagination-fit.d.ts +8 -0
  58. package/dist/types/components/Table/table-types.d.ts +30 -1
  59. package/dist/types/components.d.ts +83 -8
  60. package/package.json +1 -1
  61. package/dist/components/p-D1ZI_10t.js +0 -2
  62. package/dist/components/p-D1ZI_10t.js.map +0 -1
  63. package/dist/components/p-DwqUW4Rg.js.map +0 -1
  64. /package/dist/components/{p-Bxe6532y.js.map → p-B2hqUkg-.js.map} +0 -0
  65. /package/dist/components/{p-qA4eKZkW.js.map → p-BGGm1NB1.js.map} +0 -0
  66. /package/dist/components/{p-CW3sRYuf.js.map → p-BXNLaPbw.js.map} +0 -0
  67. /package/dist/components/{p-Cjf54_Cq.js.map → p-Ba1l3Yot.js.map} +0 -0
  68. /package/dist/components/{p-CkWVI16a.js.map → p-BiIdszv1.js.map} +0 -0
  69. /package/dist/components/{p-Dh_TVLnz.js.map → p-CXqVVdW2.js.map} +0 -0
  70. /package/dist/components/{p-CWsziQQY.js.map → p-Ccq1RmOb.js.map} +0 -0
  71. /package/dist/components/{p-Cpqd69kg.js.map → p-DgWGfscR.js.map} +0 -0
  72. /package/dist/components/{p-BiBXCjjo.js.map → p-DjqZWBnM.js.map} +0 -0
  73. /package/dist/components/{p-C68-8tlp.js.map → p-o1v7Choa.js.map} +0 -0
  74. /package/dist/components/{p-BSBQ5kGe.js.map → p-w64TmQd-.js.map} +0 -0
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
3
  "name": "pagination",
4
4
  "title": "Pagination",
5
- "description": "Controlled page-size, range, and first/previous/next/last navigation for application-owned data windows.",
5
+ "description": "Controlled fixed or fitted page-size, compact range, and first/previous/next/last navigation for application-owned data windows.",
6
6
  "type": "registry:ui",
7
7
  "meta": {
8
8
  "library": "@ds-mo/ui",
@@ -13,7 +13,7 @@
13
13
  "intent": {
14
14
  "audience": "general",
15
15
  "status": "stable",
16
- "summary": "Controlled page-size, range, and first/previous/next/last navigation for application-owned data windows.",
16
+ "summary": "Controlled fixed or fitted page-size, compact range, and first/previous/next/last navigation for application-owned data windows.",
17
17
  "useWhen": [
18
18
  "A result set is divided into discrete pages and the application owns fetching or slicing each page.",
19
19
  "A table or list needs a compact page-size selector, result range, and boundary-aware navigation."
@@ -28,18 +28,20 @@
28
28
  "component:ds-button-unfilled"
29
29
  ],
30
30
  "accessibility": [
31
- "The component renders a named navigation region, labels its page-size combobox, and gives every icon-only boundary control an explicit page action name.",
32
- "The visible range and page position use tabular numerals and are repeated in a polite atomic status update with the localized item noun.",
31
+ "The component renders a named navigation region, gives its compact page-size noun a complete accessible label, and gives every icon-only boundary control an explicit page action name.",
32
+ "The visible page-size cluster states the localized noun, selected size, and total, for example Rows: 25 of 500. The page position uses compact ‘of’ phrasing, for example 1 of 20. A polite atomic status update retains the complete current range and ‘Page’ phrasing with the localized item noun.",
33
+ "Left and Right Arrow request the adjacent page while focus is within pagination navigation controls. The page-size combobox retains its own arrow-key behavior.",
33
34
  "Inactive boundary controls and every control during loading remain non-interactive while retaining their accessible names."
34
35
  ],
35
36
  "states": [
36
- "pageIndex, pageSize, totalItems, and pageSizeOptions are controlled inputs. dsChange reports the complete next pagination state, previous index and size, and whether the request changed the page or page size; it never mutates controlled inputs.",
37
+ "pageIndex, numeric effective pageSize, pageSizeMode, totalItems, and pageSizeOptions are controlled inputs. dsChange reports the complete next pagination state, previous index, size, and mode, and whether the request changed the page, page size, or fitted capacity; it never mutates controlled inputs.",
37
38
  "Page-size changes request the first page. Out-of-range page indexes and invalid numeric inputs normalize only for presentation; the owner remains authoritative.",
38
39
  "Zero results display 0–0 of 0 and Page 1 of 1. A partial final page caps the visible range at totalItems.",
39
- "loading preserves the full pagination surface, applies aria-busy to the navigation region, and prevents page or page-size requests."
40
+ "loading preserves the full pagination surface, applies aria-busy to the navigation region, and prevents page or page-size requests.",
41
+ "fitToPage appends a Fit to page choice. fitPageSize supplies its effective whole-item capacity; the closed trigger says Fit while the range continues to expose the resulting count. Pagination does not measure layouts itself."
40
42
  ],
41
43
  "responsiveBehavior": [
42
- "The compact controls remain on one line. When inline space is insufficient, the pagination surface scrolls horizontally rather than wrapping labels away from their controls."
44
+ "The compact controls remain on one line with noun, selected size, total, a vertical divider, and page position. When inline space is still insufficient, the pagination surface scrolls horizontally rather than wrapping labels away from their controls."
43
45
  ],
44
46
  "frameworkCaveats": {
45
47
  "customElements": [
@@ -61,6 +63,41 @@
61
63
  },
62
64
  "api": {
63
65
  "props": {
66
+ "fitPageSize": {
67
+ "type": "number | undefined",
68
+ "resolvedType": "number | undefined",
69
+ "attribute": "fit-page-size",
70
+ "required": false,
71
+ "mutable": false,
72
+ "description": "Effective whole-item capacity to request when Fit is selected."
73
+ },
74
+ "fitPageSizeLabel": {
75
+ "type": "string",
76
+ "resolvedType": "string",
77
+ "attribute": "fit-page-size-label",
78
+ "default": "'Fit to page'",
79
+ "required": false,
80
+ "mutable": false,
81
+ "description": "Full choice-list label for Fit."
82
+ },
83
+ "fitPageSizeTriggerLabel": {
84
+ "type": "string",
85
+ "resolvedType": "string",
86
+ "attribute": "fit-page-size-trigger-label",
87
+ "default": "'Fit'",
88
+ "required": false,
89
+ "mutable": false,
90
+ "description": "Compact closed-trigger label while Fit is selected."
91
+ },
92
+ "fitToPage": {
93
+ "type": "boolean",
94
+ "resolvedType": "boolean",
95
+ "attribute": "fit-to-page",
96
+ "default": "false",
97
+ "required": false,
98
+ "mutable": false,
99
+ "description": "Include the Fit to page choice."
100
+ },
64
101
  "itemLabel": {
65
102
  "type": "string",
66
103
  "resolvedType": "string",
@@ -106,15 +143,32 @@
106
143
  "mutable": false,
107
144
  "description": "Controlled number of top-level items on a full page."
108
145
  },
146
+ "pageSizeAriaLabel": {
147
+ "type": "string | undefined",
148
+ "resolvedType": "string | undefined",
149
+ "attribute": "page-size-aria-label",
150
+ "required": false,
151
+ "mutable": false,
152
+ "description": "Accessible page-size control label. Defaults to “{pageSizeLabel} per page”."
153
+ },
109
154
  "pageSizeLabel": {
110
155
  "type": "string",
111
156
  "resolvedType": "string",
112
157
  "attribute": "page-size-label",
113
- "default": "'Items per page'",
158
+ "default": "'Items'",
114
159
  "required": false,
115
160
  "mutable": false,
116
161
  "description": "Visible page-size control label."
117
162
  },
163
+ "pageSizeMode": {
164
+ "type": "PaginationPageSizeMode",
165
+ "resolvedType": "\"fit\" | \"fixed\"",
166
+ "attribute": "page-size-mode",
167
+ "default": "'fixed'",
168
+ "required": false,
169
+ "mutable": false,
170
+ "description": "Whether pageSize is a fixed choice or a measured Fit snapshot."
171
+ },
118
172
  "pageSizeOptions": {
119
173
  "type": "number[]",
120
174
  "resolvedType": "number[]",
@@ -147,6 +201,41 @@
147
201
  "slots": []
148
202
  },
149
203
  "props": {
204
+ "fitPageSize": {
205
+ "type": "number | undefined",
206
+ "resolvedType": "number | undefined",
207
+ "attribute": "fit-page-size",
208
+ "required": false,
209
+ "mutable": false,
210
+ "description": "Effective whole-item capacity to request when Fit is selected."
211
+ },
212
+ "fitPageSizeLabel": {
213
+ "type": "string",
214
+ "resolvedType": "string",
215
+ "attribute": "fit-page-size-label",
216
+ "default": "'Fit to page'",
217
+ "required": false,
218
+ "mutable": false,
219
+ "description": "Full choice-list label for Fit."
220
+ },
221
+ "fitPageSizeTriggerLabel": {
222
+ "type": "string",
223
+ "resolvedType": "string",
224
+ "attribute": "fit-page-size-trigger-label",
225
+ "default": "'Fit'",
226
+ "required": false,
227
+ "mutable": false,
228
+ "description": "Compact closed-trigger label while Fit is selected."
229
+ },
230
+ "fitToPage": {
231
+ "type": "boolean",
232
+ "resolvedType": "boolean",
233
+ "attribute": "fit-to-page",
234
+ "default": "false",
235
+ "required": false,
236
+ "mutable": false,
237
+ "description": "Include the Fit to page choice."
238
+ },
150
239
  "itemLabel": {
151
240
  "type": "string",
152
241
  "resolvedType": "string",
@@ -192,15 +281,32 @@
192
281
  "mutable": false,
193
282
  "description": "Controlled number of top-level items on a full page."
194
283
  },
284
+ "pageSizeAriaLabel": {
285
+ "type": "string | undefined",
286
+ "resolvedType": "string | undefined",
287
+ "attribute": "page-size-aria-label",
288
+ "required": false,
289
+ "mutable": false,
290
+ "description": "Accessible page-size control label. Defaults to “{pageSizeLabel} per page”."
291
+ },
195
292
  "pageSizeLabel": {
196
293
  "type": "string",
197
294
  "resolvedType": "string",
198
295
  "attribute": "page-size-label",
199
- "default": "'Items per page'",
296
+ "default": "'Items'",
200
297
  "required": false,
201
298
  "mutable": false,
202
299
  "description": "Visible page-size control label."
203
300
  },
301
+ "pageSizeMode": {
302
+ "type": "PaginationPageSizeMode",
303
+ "resolvedType": "\"fit\" | \"fixed\"",
304
+ "attribute": "page-size-mode",
305
+ "default": "'fixed'",
306
+ "required": false,
307
+ "mutable": false,
308
+ "description": "Whether pageSize is a fixed choice or a measured Fit snapshot."
309
+ },
204
310
  "pageSizeOptions": {
205
311
  "type": "number[]",
206
312
  "resolvedType": "number[]",
@@ -267,28 +373,29 @@
267
373
  ],
268
374
  "registryDependencies": [
269
375
  "button-unfilled",
376
+ "divider",
270
377
  "select",
271
378
  "text"
272
379
  ],
273
380
  "files": [
274
381
  {
275
382
  "path": "src/wc/components/Pagination/Pagination.css",
276
- "content": "@import '../../utils/visually-hidden.css';\n\n:host {\n display: block;\n box-sizing: border-box;\n min-width: 0;\n max-width: 100%;\n overflow-x: auto;\n scrollbar-width: none;\n}\n\n:host::-webkit-scrollbar {\n display: none;\n}\n\n.pagination,\n.pagination__page-size,\n.pagination__navigation {\n display: flex;\n align-items: center;\n box-sizing: border-box;\n}\n\n.pagination {\n width: max-content;\n min-width: 100%;\n gap: var(--dimension-space-200);\n}\n\n.pagination__page-size,\n.pagination__navigation {\n flex: 0 0 auto;\n gap: var(--dimension-space-050);\n}\n\n.pagination__range,\n.pagination__page,\n.pagination__label {\n display: inline-flex;\n align-items: center;\n flex: 0 0 auto;\n white-space: nowrap;\n}\n\n.pagination__page {\n justify-content: center;\n min-inline-size: calc(var(--dimension-size-800) + var(--dimension-space-200));\n padding-inline: var(--dimension-space-050);\n}\n",
383
+ "content": "@import '../../utils/visually-hidden.css';\n\n:host {\n display: block;\n box-sizing: border-box;\n min-width: 0;\n max-width: 100%;\n overflow-x: auto;\n scrollbar-width: none;\n}\n\n:host::-webkit-scrollbar {\n display: none;\n}\n\n.pagination,\n.pagination__page-size,\n.pagination__navigation {\n display: flex;\n align-items: center;\n box-sizing: border-box;\n}\n\n.pagination {\n width: max-content;\n min-width: 100%;\n gap: var(--dimension-space-200);\n}\n\n.pagination__page-size,\n.pagination__navigation {\n flex: 0 0 auto;\n}\n\n.pagination__page-size {\n gap: var(--dimension-space-100);\n padding-inline-end: var(--dimension-space-100);\n}\n\n.pagination__navigation {\n gap: var(--dimension-space-050);\n}\n\n.pagination__total,\n.pagination__page,\n.pagination__label {\n display: inline-flex;\n align-items: center;\n flex: 0 0 auto;\n white-space: nowrap;\n}\n\n.pagination__page {\n justify-content: center;\n min-inline-size: 0;\n padding-inline: var(--dimension-space-100);\n}\n",
277
384
  "type": "registry:ui"
278
385
  },
279
386
  {
280
387
  "path": "src/wc/components/Pagination/Pagination.tsx",
281
- "content": "import { Component, Event, EventEmitter, h, Host, Prop } from '@stencil/core';\nimport { resolvePaginationState } from './pagination-model';\nimport type { PaginationChangeDetail } from './pagination-types';\n\nlet paginationId = 0;\n\n@Component({\n tag: 'ds-pagination',\n styleUrl: 'Pagination.css',\n scoped: true,\n})\nexport class Pagination {\n /** Controlled zero-based page index. */\n @Prop() pageIndex: number = 0;\n /** Controlled number of top-level items on a full page. */\n @Prop() pageSize: number = 25;\n /** Controlled total number of top-level items across every page. */\n @Prop() totalItems: number = 0;\n /** Available page sizes. Assign arrays through JavaScript. */\n @Prop() pageSizeOptions: number[] = [25, 50, 100, 200];\n /** Localized plural noun used by assistive range announcements. */\n @Prop() itemLabel: string = 'items';\n /** Visible page-size control label. */\n @Prop() pageSizeLabel: string = 'Items per page';\n /** Accessible name for the pagination navigation region. */\n @Prop() label: string = 'Pagination';\n /** Prevent interaction while the owner replaces the current data page. */\n @Prop() loading: boolean = false;\n\n /** Emits the complete next controlled state after a page or page-size request. */\n @Event() dsChange!: EventEmitter<PaginationChangeDetail>;\n\n private readonly pageSizeControlId = `ds-pagination-${++paginationId}-page-size`;\n\n private requestPage(pageIndex: number): void {\n const state = this.resolvedState;\n if (this.loading || pageIndex === state.pageIndex || pageIndex < 0 || pageIndex >= state.totalPages) {\n return;\n }\n this.dsChange.emit({\n pageIndex,\n pageSize: state.pageSize,\n totalItems: state.totalItems,\n pageSizeOptions: state.pageSizeOptions,\n itemLabel: this.itemLabel,\n pageSizeLabel: this.pageSizeLabel,\n ariaLabel: this.label,\n previousPageIndex: state.pageIndex,\n previousPageSize: state.pageSize,\n reason: 'page',\n });\n }\n\n private requestPageSize(value: string | string[]): void {\n if (this.loading || typeof value !== 'string') return;\n const pageSize = Number(value);\n const state = this.resolvedState;\n if (!Number.isFinite(pageSize) || pageSize <= 0 || pageSize === state.pageSize) return;\n this.dsChange.emit({\n pageIndex: 0,\n pageSize: Math.trunc(pageSize),\n totalItems: state.totalItems,\n pageSizeOptions: state.pageSizeOptions,\n itemLabel: this.itemLabel,\n pageSizeLabel: this.pageSizeLabel,\n ariaLabel: this.label,\n previousPageIndex: state.pageIndex,\n previousPageSize: state.pageSize,\n reason: 'page-size',\n });\n }\n\n private get resolvedState() {\n return resolvePaginationState({\n pageIndex: this.pageIndex,\n pageSize: this.pageSize,\n totalItems: this.totalItems,\n pageSizeOptions: this.pageSizeOptions,\n });\n }\n\n render() {\n const state = this.resolvedState;\n const atStart = state.pageIndex === 0;\n const atEnd = state.pageIndex === state.totalPages - 1;\n const range = `${state.firstItem}–${state.lastItem} of ${state.totalItems}`;\n const page = `Page ${state.pageIndex + 1} of ${state.totalPages}`;\n\n return (\n <Host>\n <nav class=\"pagination\" aria-label={this.label} aria-busy={this.loading ? 'true' : undefined}>\n <div class=\"pagination__page-size\">\n <ds-text\n id={`${this.pageSizeControlId}-label`}\n class=\"pagination__label\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n >\n {this.pageSizeLabel}\n </ds-text>\n <ds-select\n size=\"md\"\n inputId={this.pageSizeControlId}\n ariaLabelledby={`${this.pageSizeControlId}-label`}\n options={state.pageSizeOptions.map(value => ({ label: String(value), value: String(value) }))}\n value={String(state.pageSize)}\n allowClear={false}\n activeFill={false}\n isInactive={this.loading}\n onDsChange={event => {\n event.stopPropagation();\n this.requestPageSize(event.detail);\n }}\n />\n </div>\n <ds-text\n class=\"pagination__range\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n fontFeature=\"tabular-nums\"\n >\n {range}\n </ds-text>\n <div class=\"pagination__navigation\">\n <ds-button-unfilled\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronLeftDouble\"\n ariaLabel=\"First page\"\n hasBorder={false}\n isInactive={this.loading || atStart}\n onDsClick={() => this.requestPage(0)}\n />\n <ds-button-unfilled\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronLeft\"\n ariaLabel=\"Previous page\"\n hasBorder={false}\n isInactive={this.loading || atStart}\n onDsClick={() => this.requestPage(state.pageIndex - 1)}\n />\n <ds-text\n class=\"pagination__page\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n fontFeature=\"tabular-nums\"\n >\n {page}\n </ds-text>\n <ds-button-unfilled\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronRight\"\n ariaLabel=\"Next page\"\n hasBorder={false}\n isInactive={this.loading || atEnd}\n onDsClick={() => this.requestPage(state.pageIndex + 1)}\n />\n <ds-button-unfilled\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronRightDouble\"\n ariaLabel=\"Last page\"\n hasBorder={false}\n isInactive={this.loading || atEnd}\n onDsClick={() => this.requestPage(state.totalPages - 1)}\n />\n </div>\n <span class=\"ds-visually-hidden\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {range} {this.itemLabel}. {page}.\n </span>\n </nav>\n </Host>\n );\n }\n}\n",
388
+ "content": "import { Component, Event, EventEmitter, h, Host, Prop } from '@stencil/core';\nimport { resolvePaginationState } from './pagination-model';\nimport type {\n PaginationChangeDetail,\n PaginationPageSizeMode,\n} from './pagination-types';\n\nlet paginationId = 0;\n\n@Component({\n tag: 'ds-pagination',\n styleUrl: 'Pagination.css',\n scoped: true,\n})\nexport class Pagination {\n /** Controlled zero-based page index. */\n @Prop() pageIndex: number = 0;\n /** Controlled number of top-level items on a full page. */\n @Prop() pageSize: number = 25;\n /** Whether pageSize is a fixed choice or a measured Fit snapshot. */\n @Prop() pageSizeMode: PaginationPageSizeMode = 'fixed';\n /** Controlled total number of top-level items across every page. */\n @Prop() totalItems: number = 0;\n /** Available page sizes. Assign arrays through JavaScript. */\n @Prop() pageSizeOptions: number[] = [25, 50, 100, 200];\n /** Include the Fit to page choice. */\n @Prop() fitToPage: boolean = false;\n /** Effective whole-item capacity to request when Fit is selected. */\n @Prop() fitPageSize: number | undefined;\n /** Full choice-list label for Fit. */\n @Prop() fitPageSizeLabel: string = 'Fit to page';\n /** Compact closed-trigger label while Fit is selected. */\n @Prop() fitPageSizeTriggerLabel: string = 'Fit';\n /** Localized plural noun used by assistive range announcements. */\n @Prop() itemLabel: string = 'items';\n /** Visible page-size control label. */\n @Prop() pageSizeLabel: string = 'Items';\n /** Accessible page-size control label. Defaults to “{pageSizeLabel} per page”. */\n @Prop() pageSizeAriaLabel: string | undefined;\n /** Accessible name for the pagination navigation region. */\n @Prop() label: string = 'Pagination';\n /** Prevent interaction while the owner replaces the current data page. */\n @Prop() loading: boolean = false;\n\n /** Emits the complete next controlled state after a page or page-size request. */\n @Event() dsChange!: EventEmitter<PaginationChangeDetail>;\n\n private readonly pageSizeControlId = `ds-pagination-${++paginationId}-page-size`;\n\n private requestPage(pageIndex: number): void {\n const state = this.resolvedState;\n if (this.loading || pageIndex === state.pageIndex || pageIndex < 0 || pageIndex >= state.totalPages) {\n return;\n }\n this.dsChange.emit({\n pageIndex,\n pageSize: state.pageSize,\n pageSizeMode: state.pageSizeMode,\n totalItems: state.totalItems,\n pageSizeOptions: state.pageSizeOptions,\n fitToPage: this.fitToPage,\n fitPageSize: this.resolvedFitPageSize,\n fitPageSizeLabel: this.fitPageSizeLabel,\n fitPageSizeTriggerLabel: this.fitPageSizeTriggerLabel,\n itemLabel: this.itemLabel,\n pageSizeLabel: this.pageSizeLabel,\n ariaLabel: this.label,\n previousPageIndex: state.pageIndex,\n previousPageSize: state.pageSize,\n previousPageSizeMode: state.pageSizeMode,\n reason: 'page',\n });\n }\n\n private requestPageSize(value: string | string[]): void {\n if (this.loading || typeof value !== 'string') return;\n const state = this.resolvedState;\n const nextMode: PaginationPageSizeMode = value === 'fit' ? 'fit' : 'fixed';\n const pageSize = nextMode === 'fit' ? this.resolvedFitPageSize : Number(value);\n if (!pageSize || (pageSize === state.pageSize && nextMode === state.pageSizeMode)) return;\n this.dsChange.emit({\n pageIndex: 0,\n pageSize: Math.trunc(pageSize),\n pageSizeMode: nextMode,\n totalItems: state.totalItems,\n pageSizeOptions: state.pageSizeOptions,\n fitToPage: this.fitToPage,\n fitPageSize: this.resolvedFitPageSize,\n fitPageSizeLabel: this.fitPageSizeLabel,\n fitPageSizeTriggerLabel: this.fitPageSizeTriggerLabel,\n itemLabel: this.itemLabel,\n pageSizeLabel: this.pageSizeLabel,\n ariaLabel: this.label,\n previousPageIndex: state.pageIndex,\n previousPageSize: state.pageSize,\n previousPageSizeMode: state.pageSizeMode,\n reason: 'page-size',\n });\n }\n\n private handleKeyDown(event: KeyboardEvent): void {\n if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {\n return;\n }\n const fromChoiceControl = event.composedPath().some(node =>\n node instanceof HTMLElement &&\n (node.tagName === 'DS-SELECT' || ['INPUT', 'SELECT', 'TEXTAREA'].includes(node.tagName)),\n );\n if (fromChoiceControl || (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')) return;\n const state = this.resolvedState;\n const pageIndex = event.key === 'ArrowLeft' ? state.pageIndex - 1 : state.pageIndex + 1;\n if (this.loading || pageIndex < 0 || pageIndex >= state.totalPages) return;\n event.preventDefault();\n this.requestPage(pageIndex);\n }\n\n private get resolvedFitPageSize(): number | undefined {\n return Number.isFinite(this.fitPageSize) && Number(this.fitPageSize) > 0\n ? Math.trunc(Number(this.fitPageSize))\n : undefined;\n }\n\n private get resolvedState() {\n return resolvePaginationState({\n pageIndex: this.pageIndex,\n pageSize: this.pageSize,\n pageSizeMode: this.pageSizeMode,\n totalItems: this.totalItems,\n pageSizeOptions: this.pageSizeOptions,\n });\n }\n\n render() {\n const state = this.resolvedState;\n const atStart = state.pageIndex === 0;\n const atEnd = state.pageIndex === state.totalPages - 1;\n const total = `of ${state.totalItems}`;\n const page = `${state.pageIndex + 1} of ${state.totalPages}`;\n const announcedRange = `${state.firstItem}–${state.lastItem} of ${state.totalItems}`;\n const announcedPage = `Page ${state.pageIndex + 1} of ${state.totalPages}`;\n const pageSizeAriaLabel = this.pageSizeAriaLabel ?? `${this.pageSizeLabel} per page`;\n const options: Array<{ label: string; value: string; isInactive?: boolean }> =\n state.pageSizeOptions.map(value => ({ label: String(value), value: String(value) }));\n if (this.fitToPage) {\n options.push({\n label: this.fitPageSizeLabel,\n value: 'fit',\n isInactive: this.resolvedFitPageSize === undefined,\n });\n }\n\n return (\n <Host>\n <nav\n class=\"pagination\"\n aria-label={this.label}\n aria-busy={this.loading ? 'true' : undefined}\n onKeyDown={event => this.handleKeyDown(event)}\n >\n <div class=\"pagination__page-size\">\n <ds-text\n class=\"pagination__label\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n {this.pageSizeLabel}:\n </ds-text>\n <span id={`${this.pageSizeControlId}-label`} class=\"ds-visually-hidden\">\n {pageSizeAriaLabel}\n </span>\n <ds-select\n size=\"md\"\n inputId={this.pageSizeControlId}\n ariaLabelledby={`${this.pageSizeControlId}-label`}\n options={options}\n value={state.pageSizeMode === 'fit' ? 'fit' : String(state.pageSize)}\n triggerLabel={state.pageSizeMode === 'fit' ? this.fitPageSizeTriggerLabel : undefined}\n indicator=\"up-down\"\n allowClear={false}\n activeFill={false}\n isInactive={this.loading}\n onDsChange={event => {\n event.stopPropagation();\n this.requestPageSize(event.detail);\n }}\n />\n <ds-text\n class=\"pagination__total\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n fontFeature=\"tabular-nums\"\n >\n {total}\n </ds-text>\n </div>\n <ds-divider class=\"pagination__divider\" orientation=\"vertical\" length=\"24px\" />\n <div class=\"pagination__navigation\">\n <ds-button-unfilled\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronLeftDouble\"\n ariaLabel=\"First page\"\n hasBorder={false}\n isInactive={this.loading || atStart}\n onDsClick={() => this.requestPage(0)}\n />\n <ds-button-unfilled\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronLeft\"\n ariaLabel=\"Previous page\"\n hasBorder={false}\n isInactive={this.loading || atStart}\n onDsClick={() => this.requestPage(state.pageIndex - 1)}\n />\n <ds-text\n class=\"pagination__page\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n fontFeature=\"tabular-nums\"\n >\n {page}\n </ds-text>\n <ds-button-unfilled\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronRight\"\n ariaLabel=\"Next page\"\n hasBorder={false}\n isInactive={this.loading || atEnd}\n onDsClick={() => this.requestPage(state.pageIndex + 1)}\n />\n <ds-button-unfilled\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronRightDouble\"\n ariaLabel=\"Last page\"\n hasBorder={false}\n isInactive={this.loading || atEnd}\n onDsClick={() => this.requestPage(state.totalPages - 1)}\n />\n </div>\n <span class=\"ds-visually-hidden\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {announcedRange} {this.itemLabel}. {announcedPage}.\n </span>\n </nav>\n </Host>\n );\n }\n}\n",
282
389
  "type": "registry:ui"
283
390
  },
284
391
  {
285
392
  "path": "src/wc/components/Pagination/pagination-model.ts",
286
- "content": "import type { PaginationState } from './pagination-types';\n\nexport const DEFAULT_PAGE_SIZE_OPTIONS = [25, 50, 100, 200] as const;\n\nexport interface ResolvedPaginationState {\n pageIndex: number;\n pageSize: number;\n totalItems: number;\n totalPages: number;\n firstItem: number;\n lastItem: number;\n pageSizeOptions: number[];\n}\n\nfunction nonNegativeInteger(value: number): number {\n return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;\n}\n\nfunction positiveInteger(value: number, fallback: number): number {\n return Number.isFinite(value) && value > 0 ? Math.trunc(value) : fallback;\n}\n\nexport function resolvePaginationState(\n state: Pick<PaginationState, 'pageIndex' | 'pageSize' | 'totalItems' | 'pageSizeOptions'>,\n): ResolvedPaginationState {\n const pageSize = positiveInteger(state.pageSize, DEFAULT_PAGE_SIZE_OPTIONS[0]);\n const totalItems = nonNegativeInteger(state.totalItems);\n const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));\n const pageIndex = Math.min(nonNegativeInteger(state.pageIndex), totalPages - 1);\n const options = (state.pageSizeOptions?.length\n ? state.pageSizeOptions\n : DEFAULT_PAGE_SIZE_OPTIONS\n )\n .map(value => positiveInteger(value, 0))\n .filter(value => value > 0);\n const pageSizeOptions = [...new Set([...options, pageSize])].sort((left, right) => left - right);\n const firstItem = totalItems === 0 ? 0 : pageIndex * pageSize + 1;\n const lastItem = totalItems === 0 ? 0 : Math.min(totalItems, firstItem + pageSize - 1);\n\n return {\n pageIndex,\n pageSize,\n totalItems,\n totalPages,\n firstItem,\n lastItem,\n pageSizeOptions,\n };\n}\n",
393
+ "content": "import type { PaginationPageSizeMode, PaginationState } from './pagination-types';\n\nexport const DEFAULT_PAGE_SIZE_OPTIONS = [25, 50, 100, 200] as const;\n\nexport interface ResolvedPaginationState {\n pageIndex: number;\n pageSize: number;\n pageSizeMode: PaginationPageSizeMode;\n totalItems: number;\n totalPages: number;\n firstItem: number;\n lastItem: number;\n pageSizeOptions: number[];\n}\n\nfunction nonNegativeInteger(value: number): number {\n return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;\n}\n\nfunction positiveInteger(value: number, fallback: number): number {\n return Number.isFinite(value) && value > 0 ? Math.trunc(value) : fallback;\n}\n\nexport function resolvePaginationState(\n state: Pick<\n PaginationState,\n 'pageIndex' | 'pageSize' | 'pageSizeMode' | 'totalItems' | 'pageSizeOptions'\n >,\n): ResolvedPaginationState {\n const pageSize = positiveInteger(state.pageSize, DEFAULT_PAGE_SIZE_OPTIONS[0]);\n const pageSizeMode = state.pageSizeMode === 'fit' ? 'fit' : 'fixed';\n const totalItems = nonNegativeInteger(state.totalItems);\n const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));\n const pageIndex = Math.min(nonNegativeInteger(state.pageIndex), totalPages - 1);\n const options = (state.pageSizeOptions?.length\n ? state.pageSizeOptions\n : DEFAULT_PAGE_SIZE_OPTIONS\n )\n .map(value => positiveInteger(value, 0))\n .filter(value => value > 0);\n const pageSizeOptions = [\n ...new Set(pageSizeMode === 'fixed' ? [...options, pageSize] : options),\n ].sort((left, right) => left - right);\n const firstItem = totalItems === 0 ? 0 : pageIndex * pageSize + 1;\n const lastItem = totalItems === 0 ? 0 : Math.min(totalItems, firstItem + pageSize - 1);\n\n return {\n pageIndex,\n pageSize,\n pageSizeMode,\n totalItems,\n totalPages,\n firstItem,\n lastItem,\n pageSizeOptions,\n };\n}\n",
287
394
  "type": "registry:ui"
288
395
  },
289
396
  {
290
397
  "path": "src/wc/components/Pagination/pagination-types.ts",
291
- "content": "export type PaginationChangeReason = 'page' | 'page-size';\n\n/** Controlled pagination state shared by Pagination and pagination-aware consumers. */\nexport interface PaginationState {\n /** Zero-based page index. */\n pageIndex: number;\n /** Number of top-level items on a full page. */\n pageSize: number;\n /** Total number of top-level items across every page. */\n totalItems: number;\n /** Available page sizes. Defaults to 25, 50, 100, and 200. */\n pageSizeOptions?: number[];\n /** Localized plural noun used by assistive range announcements. */\n itemLabel?: string;\n /** Visible label for the page-size selector. */\n pageSizeLabel?: string;\n /** Accessible name for the pagination navigation region. */\n ariaLabel?: string;\n}\n\n/** Complete next controlled state emitted for a page or page-size request. */\nexport interface PaginationChangeDetail extends PaginationState {\n previousPageIndex: number;\n previousPageSize: number;\n reason: PaginationChangeReason;\n}\n",
398
+ "content": "export type PaginationChangeReason = 'page' | 'page-size' | 'fit';\nexport type PaginationPageSizeMode = 'fixed' | 'fit';\n\n/** Controlled pagination state shared by Pagination and pagination-aware consumers. */\nexport interface PaginationState {\n /** Zero-based page index. */\n pageIndex: number;\n /** Number of top-level items on a full page. */\n pageSize: number;\n /** Whether pageSize is a chosen fixed value or a measured Fit snapshot. */\n pageSizeMode?: PaginationPageSizeMode;\n /** Total number of top-level items across every page. */\n totalItems: number;\n /** Available page sizes. Defaults to 25, 50, 100, and 200. */\n pageSizeOptions?: number[];\n /** Include a Fit to page option. The owner supplies the effective fitted size. */\n fitToPage?: boolean;\n /** Effective page size proposed when Fit to page is selected. */\n fitPageSize?: number;\n /** Full option label for Fit in the open choice list. */\n fitPageSizeLabel?: string;\n /** Compact trigger label while Fit is selected. */\n fitPageSizeTriggerLabel?: string;\n /** Localized plural noun used by assistive range announcements. */\n itemLabel?: string;\n /** Visible label for the page-size selector. */\n pageSizeLabel?: string;\n /** Accessible name for the pagination navigation region. */\n ariaLabel?: string;\n}\n\n/** Complete next controlled state emitted for a page or page-size request. */\nexport interface PaginationChangeDetail extends PaginationState {\n previousPageIndex: number;\n previousPageSize: number;\n previousPageSizeMode: PaginationPageSizeMode;\n reason: PaginationChangeReason;\n}\n",
292
399
  "type": "registry:ui"
293
400
  }
294
401
  ]
@@ -5934,7 +5934,7 @@
5934
5934
  {
5935
5935
  "name": "pagination",
5936
5936
  "title": "Pagination",
5937
- "description": "Controlled page-size, range, and first/previous/next/last navigation for application-owned data windows.",
5937
+ "description": "Controlled fixed or fitted page-size, compact range, and first/previous/next/last navigation for application-owned data windows.",
5938
5938
  "type": "registry:ui",
5939
5939
  "meta": {
5940
5940
  "library": "@ds-mo/ui",
@@ -5945,7 +5945,7 @@
5945
5945
  "intent": {
5946
5946
  "audience": "general",
5947
5947
  "status": "stable",
5948
- "summary": "Controlled page-size, range, and first/previous/next/last navigation for application-owned data windows.",
5948
+ "summary": "Controlled fixed or fitted page-size, compact range, and first/previous/next/last navigation for application-owned data windows.",
5949
5949
  "useWhen": [
5950
5950
  "A result set is divided into discrete pages and the application owns fetching or slicing each page.",
5951
5951
  "A table or list needs a compact page-size selector, result range, and boundary-aware navigation."
@@ -5960,18 +5960,20 @@
5960
5960
  "component:ds-button-unfilled"
5961
5961
  ],
5962
5962
  "accessibility": [
5963
- "The component renders a named navigation region, labels its page-size combobox, and gives every icon-only boundary control an explicit page action name.",
5964
- "The visible range and page position use tabular numerals and are repeated in a polite atomic status update with the localized item noun.",
5963
+ "The component renders a named navigation region, gives its compact page-size noun a complete accessible label, and gives every icon-only boundary control an explicit page action name.",
5964
+ "The visible page-size cluster states the localized noun, selected size, and total, for example Rows: 25 of 500. The page position uses compact ‘of’ phrasing, for example 1 of 20. A polite atomic status update retains the complete current range and ‘Page’ phrasing with the localized item noun.",
5965
+ "Left and Right Arrow request the adjacent page while focus is within pagination navigation controls. The page-size combobox retains its own arrow-key behavior.",
5965
5966
  "Inactive boundary controls and every control during loading remain non-interactive while retaining their accessible names."
5966
5967
  ],
5967
5968
  "states": [
5968
- "pageIndex, pageSize, totalItems, and pageSizeOptions are controlled inputs. dsChange reports the complete next pagination state, previous index and size, and whether the request changed the page or page size; it never mutates controlled inputs.",
5969
+ "pageIndex, numeric effective pageSize, pageSizeMode, totalItems, and pageSizeOptions are controlled inputs. dsChange reports the complete next pagination state, previous index, size, and mode, and whether the request changed the page, page size, or fitted capacity; it never mutates controlled inputs.",
5969
5970
  "Page-size changes request the first page. Out-of-range page indexes and invalid numeric inputs normalize only for presentation; the owner remains authoritative.",
5970
5971
  "Zero results display 0–0 of 0 and Page 1 of 1. A partial final page caps the visible range at totalItems.",
5971
- "loading preserves the full pagination surface, applies aria-busy to the navigation region, and prevents page or page-size requests."
5972
+ "loading preserves the full pagination surface, applies aria-busy to the navigation region, and prevents page or page-size requests.",
5973
+ "fitToPage appends a Fit to page choice. fitPageSize supplies its effective whole-item capacity; the closed trigger says Fit while the range continues to expose the resulting count. Pagination does not measure layouts itself."
5972
5974
  ],
5973
5975
  "responsiveBehavior": [
5974
- "The compact controls remain on one line. When inline space is insufficient, the pagination surface scrolls horizontally rather than wrapping labels away from their controls."
5976
+ "The compact controls remain on one line with noun, selected size, total, a vertical divider, and page position. When inline space is still insufficient, the pagination surface scrolls horizontally rather than wrapping labels away from their controls."
5975
5977
  ],
5976
5978
  "frameworkCaveats": {
5977
5979
  "customElements": [
@@ -5992,6 +5994,22 @@
5992
5994
  ]
5993
5995
  },
5994
5996
  "props": {
5997
+ "fitPageSize": {
5998
+ "type": "number | undefined",
5999
+ "required": false
6000
+ },
6001
+ "fitPageSizeLabel": {
6002
+ "type": "string",
6003
+ "required": false
6004
+ },
6005
+ "fitPageSizeTriggerLabel": {
6006
+ "type": "string",
6007
+ "required": false
6008
+ },
6009
+ "fitToPage": {
6010
+ "type": "boolean",
6011
+ "required": false
6012
+ },
5995
6013
  "itemLabel": {
5996
6014
  "type": "string",
5997
6015
  "required": false
@@ -6012,10 +6030,18 @@
6012
6030
  "type": "number",
6013
6031
  "required": false
6014
6032
  },
6033
+ "pageSizeAriaLabel": {
6034
+ "type": "string | undefined",
6035
+ "required": false
6036
+ },
6015
6037
  "pageSizeLabel": {
6016
6038
  "type": "string",
6017
6039
  "required": false
6018
6040
  },
6041
+ "pageSizeMode": {
6042
+ "type": "PaginationPageSizeMode",
6043
+ "required": false
6044
+ },
6019
6045
  "pageSizeOptions": {
6020
6046
  "type": "number[]",
6021
6047
  "required": false
@@ -6044,6 +6070,7 @@
6044
6070
  ],
6045
6071
  "registryDependencies": [
6046
6072
  "button-unfilled",
6073
+ "divider",
6047
6074
  "select",
6048
6075
  "text"
6049
6076
  ]
@@ -7195,7 +7222,7 @@
7195
7222
  "When any option supplies subtext, every row uses the same subtext structure and missing values render an em dash. Subtext should disambiguate or add decision-helpful information.",
7196
7223
  "Option labels and the selected trigger label stay on one line and truncate when constrained. Option subtext may wrap to at most two lines.",
7197
7224
  "Use an option action only for a contextual menu about that user-managed option. The fixed ellipsis action appears on hover, keyboard activity, focus, and touch; expanded keeps it visible while its controlled menu is open. Activating it emits dsOptionAction without selecting the option or closing the Select popup.",
7198
- "The optional prefix icon and chevron always use the parent surface's secondary foreground color, including when a selected value promotes the label to primary.",
7225
+ "The optional prefix icon and trailing indicator always use the parent surface's secondary foreground color, including when a selected value promotes the label to primary. The default down indicator communicates disclosure; up-down is reserved for compact value steppers such as pagination page size.",
7199
7226
  "While the popup is open, the trigger holds the pressed interaction wash without changing its resting foreground or selected-value treatment.",
7200
7227
  "The shared popup search control follows the Select density, using the same text, icon, and internal spacing as a borderless trigger. Its container uses menu-item padding on every side and owns a 1px bottom divider that becomes a 1.5px bold-brand stroke while search has focus. Search placeholder text is secondary at rest and quaternary while focused because it becomes decorative once the editable field is active.",
7201
7228
  "When search contains text, the shared control shows a rounded icon-only ButtonUnfilled with the CrossCircle icon and no border, sized to remain inset within the current row density. It clears the local term, stays hidden for an empty term, and returns focus to search.",
@@ -7294,6 +7321,10 @@
7294
7321
  "type": "string | undefined",
7295
7322
  "required": false
7296
7323
  },
7324
+ "indicator": {
7325
+ "type": "SelectIndicator",
7326
+ "required": false
7327
+ },
7297
7328
  "inputId": {
7298
7329
  "type": "string | undefined",
7299
7330
  "required": false
@@ -8826,15 +8857,16 @@
8826
8857
  "Interactive rows are focusable as native rows, activate with Enter or Space, and do not steal activation from their nested selection or action controls.",
8827
8858
  "Scrollable table regions become keyboard focusable and named only when overflow exists.",
8828
8859
  "Incremental loading, newly appended row counts, failures, and end-of-results transitions are announced through a polite status region without replacing visible state. In grouped tables, announcements name the independently loading group.",
8829
- "Static and infinite tables may show an optional result summary footer when displayedCount and totalCount are finite. Pagination mode instead renders a named Pagination control, whose range changes are announced independently. The footer slot can replace summary copy, while footer-leading and footer-trailing remain available in the same compact-bar anatomy."
8860
+ "Static and infinite tables may show an optional result summary footer when displayedCount and totalCount are finite. Pagination mode instead renders a named Pagination control with a compact selected-size total, vertical divider, and page position; range changes are announced independently with complete phrasing. While focus is anywhere inside a paginated table, unmodified Left and Right Arrow request the adjacent page; editable fields, choice controls, sliders, and modified key combinations retain their own keyboard behavior. The footer slot can replace summary copy, while footer-leading and footer-trailing remain available in the same compact-bar anatomy."
8830
8861
  ],
8831
8862
  "states": [
8832
8863
  "Columns, rows, groups, grouping, sorting, selected row IDs, collapsed group IDs, dataMode, pagination, displayedCount, totalCount, and incremental-loading state are controlled inputs. Events communicate intent and never mutate those inputs. The optional header is one full-width composable table-local surface: the component owns its 48px chrome, 8px inset on every side, and sticky placement while the application owns all internal layout, controls, labels, filtering behavior, and action handling. The footer retains its compact-bar composition: application-owned leading status sits left while a controlled result summary or Pagination aligns right, before any trailing action. Content-sized tables remain natural for sparse or fixed-count results. height bounds the complete header, frame, and footer composition. fitViewport reserves the compact-state composition height in the nearest vertical scrollport while growing the visible table into that reservation as surrounding page chrome collapses.",
8833
8864
  "Grouping supports one level. The application supplies groups in a fixed product-defined order and supplies ordered member rows independently. Group section headers show an emphasized text-body-medium label followed by a middle dot and plain secondary loaded progress, for example Critical · 50 of 166. The loaded value comes from the supplied member rows and the total comes from totalCount. A localized countLabel total phrase remains available for the complete assistive progress announcement. Groups may declare an optional intent (brand, neutral, negative, warning, caution, positive) that paints an LTR gradient from the faint intent surface at the label edge into faint neutral at the trailing edge, plus a bold intent title color. Neutral groups remain solid faint neutral. Section separators are painted inside that viewport-width surface so translucent border tokens composite with each intent background. Internal sections keep their separators; a collapsed or empty section header yields its separator only when it is the table's final rendered item. A trailing visually neutral icon-only ButtonUnfilled uses the 24px md double-inset action recipe and toggles controlled collapsedGroupIds, showing ChevronUp while expanded and ChevronDown while collapsed. While any group is expanded, a ChevronDownUp action uses the xs sort-control recipe widened to a table-owned 24px lane and collapses every group. It appears at the inline-end edge of the trailing column declared with kind action when one exists; otherwise a button-hugging 24px by 16px medium-elevation surface floats at the visible scrollport's header edge so the action remains reachable while member columns scroll beneath it. With stickyHeader enabled, each contained section uses its one real section row as a native sticky header inside its real row-group boundary. The browser therefore owns pinning, push-off, scrolling, and resize layout in one pass without a duplicated lane, active-section state, transforms, or scroll-time geometry. Shared grid tracks retain exact header and member-cell alignment. The section surface also stays pinned to the visible horizontal scrollport and spans the smaller of the visible viewport or table width.",
8834
8865
  "An active member sort toggles only between ascending and descending; activating a different sortable label moves the one sorting state to that key. Applications may still clear the controlled sort state explicitly. Grouping does not introduce another interactive direction.",
8835
8866
  "Compound headers can expose multiple related labels with independent sort keys. Each label owns only its content-width interaction target while the active direction remains in the column's alignment-aware sort lane.",
8836
- "Initial loading uses row-shaped skeletons that preserve the standard single-line cell padding, row dividers, column dividers, selection lane, and sticky-lane boundaries. Empty, initial error, load-more, retry, loading-more, and terminal bands remain fixed to the visible table width while data columns scroll horizontally beneath them. Incremental loading retains existing rows and supports automatic sentinel, manual load-more, retry, and terminal states. When grouping is active, each expanded group owns its load band at the end of its member rows; internal group load bands keep their bottom divider, while a terminal group load band yields it to the table edge or footer. The global bottom-of-table loader is not rendered.",
8867
+ "Initial loading defaults to ten row-shaped skeletons and preserves row dividers, column dividers, selection lanes, and sticky boundaries. A selectable table renders a checkbox-sized control skeleton in its selection lane so loading geometry matches the real checkbox instead of a generic icon. Each column may declare representative text, multiline text, image, Tag, icon, action, or intentionally blank loading geometry; the table applies the matching resolved-cell padding and track anatomy so the placeholder row has the same height and rhythm as its data row. Columns without a declaration use one text line, while action columns infer an icon action placeholder. Empty, initial error, load-more, retry, loading-more, and terminal bands remain fixed to the visible table width while data columns scroll horizontally beneath them. Incremental loading retains existing rows and supports automatic sentinel, manual load-more, retry, and terminal states. When grouping is active, each expanded group owns its load band at the end of its member rows; internal group load bands keep their bottom divider, while a terminal group load band yields it to the table edge or footer. The global bottom-of-table loader is not rendered.",
8837
8868
  "dataMode selects the top-level window: infinite appends ungrouped rows through dsLoadMore when hasMore is true, and pagination renders the controlled page supplied by the application and emits dsPaginationChange. Infinite mode also covers fixed result sets by leaving hasMore false. In a grouped paginated table, pagination counts and windows only parent groups; each expanded parent can still append its own members through dsGroupLoadMore. The application owns requests, cancellation, cursors, deduplication, slicing, and query identity.",
8869
+ "When pagination enables Fit to page on a bounded table, the table proposes the number of complete top-level rows that fit below its column header. Grouped tables count group headers only; member rows never affect capacity. Selecting Fit captures that numeric snapshot. A changed fitIdentity requests one fresh snapshot for a new query shape, while viewport resizing alone does not change the active controlled page size.",
8838
8870
  "Rows opt into complete-row activation with interactive and emit dsRowActivate without owning navigation. Selection paints through the shared selected ::before layer; interactive hover and press paint through the shared ::after layer.",
8839
8871
  "Row selection owns the single sticky start lane. One explicitly sized application column can own the sticky end lane, or the start lane when selection is absent. Each sticky boundary owns one fixed divider and a row-clipped directional shadow that paints into the adjacent scrolling columns only while more inline content remains beyond that edge.",
8840
8872
  "Cells accept scalar content, standard primary-and-secondary or dual-primary text primitives, declarative Tag, icon-only, 16:9 Image, or ButtonUnfilled Action content, or explicit Empty and Blank absence values. Standard secondary copy defaults to the secondary foreground and may declare a semantic Text color when the application data carries intent; dual-primary copy keeps both tracks primary unless explicitly colored. Single-track text, selection, tag-only, icon-only, Action, Empty, and Blank cells resolve a 40px base contract while multi-track cells grow downward. Tag cells use the double-inset Tag recipe; the md tag-only recipe is 24px inside 8px padding, while compound tag tracks preserve their peer text alignment. Action cells primarily use an icon-only Ellipses treatment, render a 24px small ButtonUnfilled inside 8px padding, default to unbordered, and emit row, column, and action identities through dsCellAction. Image cells render a 48px-high 16:9 bordered preview inside 8px outer padding, establishing a 64px row and a 102px column recipe, and fall back to an accessible placeholder when no source is supplied. Empty means the data applies but has no value and renders an em dash with localized assistive text; Blank means the data is not applicable and renders no content. Nullish values remain backward-compatible Empty values. Stable type and variant hooks keep every cell family independently styleable.",