snice 7.7.0 → 7.8.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.
Files changed (216) hide show
  1. package/adapters/react/SniceProvider.js +1 -1
  2. package/adapters/react/SniceRouter.js +1 -1
  3. package/adapters/react/calendar.d.ts +1 -0
  4. package/adapters/react/calendar.d.ts.map +1 -1
  5. package/adapters/react/calendar.js +1 -1
  6. package/adapters/react/calendar.js.map +1 -1
  7. package/adapters/react/calendar.tsx +2 -1
  8. package/adapters/react/matchRoute.js +1 -1
  9. package/adapters/react/useRequestHandler.js +1 -1
  10. package/bin/analyzer-contracts.js +8 -2
  11. package/custom-elements.json +5 -1
  12. package/dist/cdn/accordion/snice-accordion.js +1 -1
  13. package/dist/cdn/action-bar/snice-action-bar.js +1 -1
  14. package/dist/cdn/activity-feed/snice-activity-feed.js +1 -1
  15. package/dist/cdn/alert/snice-alert.js +1 -1
  16. package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
  17. package/dist/cdn/approval-flow/snice-approval-flow.js +1 -1
  18. package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
  19. package/dist/cdn/availability/snice-availability.js +1 -1
  20. package/dist/cdn/avatar/snice-avatar.js +1 -1
  21. package/dist/cdn/avatar-group/snice-avatar-group.js +1 -1
  22. package/dist/cdn/badge/snice-badge.js +1 -1
  23. package/dist/cdn/banner/snice-banner.js +1 -1
  24. package/dist/cdn/binpack/snice-binpack.js +1 -1
  25. package/dist/cdn/book/snice-book.js +1 -1
  26. package/dist/cdn/booking/snice-booking.js +1 -1
  27. package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
  28. package/dist/cdn/button/snice-button.js +1 -1
  29. package/dist/cdn/calendar/README.md +2 -2
  30. package/dist/cdn/calendar/snice-calendar.js +26 -2
  31. package/dist/cdn/calendar/snice-calendar.js.map +1 -1
  32. package/dist/cdn/calendar/snice-calendar.min.js +2 -2
  33. package/dist/cdn/calendar/snice-calendar.min.js.map +1 -1
  34. package/dist/cdn/camera/snice-camera.js +1 -1
  35. package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
  36. package/dist/cdn/candlestick/snice-candlestick.js +1 -1
  37. package/dist/cdn/card/snice-card.js +1 -1
  38. package/dist/cdn/carousel/snice-carousel.js +1 -1
  39. package/dist/cdn/cart/snice-cart.js +1 -1
  40. package/dist/cdn/chart/snice-chart.js +1 -1
  41. package/dist/cdn/chat/snice-chat.js +1 -1
  42. package/dist/cdn/checkbox/snice-checkbox.js +1 -1
  43. package/dist/cdn/chip/snice-chip.js +1 -1
  44. package/dist/cdn/code-block/snice-code-block.js +1 -1
  45. package/dist/cdn/color-display/snice-color-display.js +1 -1
  46. package/dist/cdn/color-picker/snice-color-picker.js +1 -1
  47. package/dist/cdn/command-palette/snice-command-palette.js +1 -1
  48. package/dist/cdn/comments/snice-comments.js +1 -1
  49. package/dist/cdn/countdown/snice-countdown.js +1 -1
  50. package/dist/cdn/cropper/snice-cropper.js +1 -1
  51. package/dist/cdn/data-card/snice-data-card.js +1 -1
  52. package/dist/cdn/date-picker/snice-date-picker.js +1 -1
  53. package/dist/cdn/date-range-picker/snice-date-range-picker.js +1 -1
  54. package/dist/cdn/date-time-picker/snice-date-time-picker.js +1 -1
  55. package/dist/cdn/diff/snice-diff.js +1 -1
  56. package/dist/cdn/divider/snice-divider.js +1 -1
  57. package/dist/cdn/doc/snice-doc.js +1 -1
  58. package/dist/cdn/draw/snice-draw.js +1 -1
  59. package/dist/cdn/drawer/snice-drawer.js +1 -1
  60. package/dist/cdn/empty-state/snice-empty-state.js +1 -1
  61. package/dist/cdn/estimate/snice-estimate.js +1 -1
  62. package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
  63. package/dist/cdn/file-upload/snice-file-upload.js +1 -1
  64. package/dist/cdn/flip-card/snice-flip-card.js +1 -1
  65. package/dist/cdn/flow/snice-flow.js +1 -1
  66. package/dist/cdn/form-layout/snice-form-layout.js +1 -1
  67. package/dist/cdn/funnel/snice-funnel.js +1 -1
  68. package/dist/cdn/gantt/snice-gantt.js +1 -1
  69. package/dist/cdn/gauge/snice-gauge.js +1 -1
  70. package/dist/cdn/grid/snice-grid.js +1 -1
  71. package/dist/cdn/heatmap/snice-heatmap.js +1 -1
  72. package/dist/cdn/image/snice-image.js +1 -1
  73. package/dist/cdn/input/snice-input.js +1 -1
  74. package/dist/cdn/invoice/snice-invoice.js +1 -1
  75. package/dist/cdn/kanban/snice-kanban.js +1 -1
  76. package/dist/cdn/key-value/snice-key-value.js +1 -1
  77. package/dist/cdn/kpi/snice-kpi.js +1 -1
  78. package/dist/cdn/layout/snice-layout.js +1 -1
  79. package/dist/cdn/leaderboard/snice-leaderboard.js +1 -1
  80. package/dist/cdn/link/snice-link.js +1 -1
  81. package/dist/cdn/link-preview/snice-link-preview.js +1 -1
  82. package/dist/cdn/list/snice-list.js +1 -1
  83. package/dist/cdn/location/snice-location.js +1 -1
  84. package/dist/cdn/login/snice-login.js +1 -1
  85. package/dist/cdn/map/snice-map.js +1 -1
  86. package/dist/cdn/markdown/snice-markdown.js +1 -1
  87. package/dist/cdn/masonry/snice-masonry.js +1 -1
  88. package/dist/cdn/menu/snice-menu.js +1 -1
  89. package/dist/cdn/message-strip/snice-message-strip.js +1 -1
  90. package/dist/cdn/modal/snice-modal.js +1 -1
  91. package/dist/cdn/music-player/snice-music-player.js +1 -1
  92. package/dist/cdn/nav/snice-nav.js +1 -1
  93. package/dist/cdn/network-graph/snice-network-graph.js +1 -1
  94. package/dist/cdn/notification-center/snice-notification-center.js +1 -1
  95. package/dist/cdn/order-tracker/snice-order-tracker.js +1 -1
  96. package/dist/cdn/org-chart/snice-org-chart.js +1 -1
  97. package/dist/cdn/pagination/snice-pagination.js +1 -1
  98. package/dist/cdn/paint/snice-paint.js +1 -1
  99. package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
  100. package/dist/cdn/permission-matrix/snice-permission-matrix.js +1 -1
  101. package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
  102. package/dist/cdn/popover/snice-popover.js +1 -1
  103. package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
  104. package/dist/cdn/product-card/snice-product-card.js +1 -1
  105. package/dist/cdn/progress/snice-progress.js +1 -1
  106. package/dist/cdn/progress-ring/snice-progress-ring.js +1 -1
  107. package/dist/cdn/qr-code/snice-qr-code.js +1 -1
  108. package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
  109. package/dist/cdn/radio/snice-radio.js +1 -1
  110. package/dist/cdn/range-slider/snice-range-slider.js +1 -1
  111. package/dist/cdn/rating/snice-rating.js +1 -1
  112. package/dist/cdn/receipt/snice-receipt.js +1 -1
  113. package/dist/cdn/recipe/snice-recipe.js +1 -1
  114. package/dist/cdn/runtime/README.md +2 -2
  115. package/dist/cdn/runtime/snice-runtime.esm.js +130 -31
  116. package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
  117. package/dist/cdn/runtime/snice-runtime.esm.min.js +1 -1
  118. package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
  119. package/dist/cdn/runtime/snice-runtime.js +130 -31
  120. package/dist/cdn/runtime/snice-runtime.js.map +1 -1
  121. package/dist/cdn/runtime/snice-runtime.min.js +1 -1
  122. package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
  123. package/dist/cdn/sankey/snice-sankey.js +1 -1
  124. package/dist/cdn/segmented-control/snice-segmented-control.js +1 -1
  125. package/dist/cdn/select/snice-select.js +1 -1
  126. package/dist/cdn/skeleton/snice-skeleton.js +1 -1
  127. package/dist/cdn/slider/snice-slider.js +1 -1
  128. package/dist/cdn/sortable/snice-sortable.js +1 -1
  129. package/dist/cdn/sparkline/snice-sparkline.js +1 -1
  130. package/dist/cdn/spinner/snice-spinner.js +1 -1
  131. package/dist/cdn/split-button/snice-split-button.js +1 -1
  132. package/dist/cdn/split-pane/snice-split-pane.js +1 -1
  133. package/dist/cdn/spotlight/snice-spotlight.js +1 -1
  134. package/dist/cdn/stat-group/snice-stat-group.js +1 -1
  135. package/dist/cdn/step-input/snice-step-input.js +1 -1
  136. package/dist/cdn/stepper/snice-stepper.js +1 -1
  137. package/dist/cdn/switch/snice-switch.js +1 -1
  138. package/dist/cdn/table/README.md +2 -2
  139. package/dist/cdn/table/snice-table.esm.js +157 -33
  140. package/dist/cdn/table/snice-table.esm.js.map +1 -1
  141. package/dist/cdn/table/snice-table.esm.min.js +4 -4
  142. package/dist/cdn/table/snice-table.esm.min.js.map +1 -1
  143. package/dist/cdn/table/snice-table.js +157 -33
  144. package/dist/cdn/table/snice-table.js.map +1 -1
  145. package/dist/cdn/table/snice-table.min.js +33 -33
  146. package/dist/cdn/table/snice-table.min.js.map +1 -1
  147. package/dist/cdn/tabs/snice-tabs.js +1 -1
  148. package/dist/cdn/tag/snice-tag.js +1 -1
  149. package/dist/cdn/tag-input/README.md +2 -2
  150. package/dist/cdn/tag-input/snice-tag-input.js +10 -16
  151. package/dist/cdn/tag-input/snice-tag-input.js.map +1 -1
  152. package/dist/cdn/tag-input/snice-tag-input.min.js +2 -2
  153. package/dist/cdn/tag-input/snice-tag-input.min.js.map +1 -1
  154. package/dist/cdn/terminal/snice-terminal.js +1 -1
  155. package/dist/cdn/testimonial/snice-testimonial.js +1 -1
  156. package/dist/cdn/textarea/snice-textarea.js +1 -1
  157. package/dist/cdn/time-picker/snice-time-picker.js +1 -1
  158. package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
  159. package/dist/cdn/timeline/snice-timeline.js +1 -1
  160. package/dist/cdn/timer/snice-timer.js +1 -1
  161. package/dist/cdn/toast/snice-toast.js +1 -1
  162. package/dist/cdn/tooltip/snice-tooltip.js +1 -1
  163. package/dist/cdn/tree/snice-tree.js +1 -1
  164. package/dist/cdn/treemap/snice-treemap.js +1 -1
  165. package/dist/cdn/user-card/snice-user-card.js +1 -1
  166. package/dist/cdn/video-player/snice-video-player.js +1 -1
  167. package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
  168. package/dist/cdn/waterfall/snice-waterfall.js +1 -1
  169. package/dist/cdn/weather/snice-weather.js +1 -1
  170. package/dist/cdn/work-order/snice-work-order.js +1 -1
  171. package/dist/components/calendar/snice-calendar.d.ts +2 -0
  172. package/dist/components/calendar/snice-calendar.js +25 -1
  173. package/dist/components/calendar/snice-calendar.js.map +1 -1
  174. package/dist/components/table/snice-row.d.ts +6 -0
  175. package/dist/components/table/snice-row.js +11 -3
  176. package/dist/components/table/snice-row.js.map +1 -1
  177. package/dist/components/table/snice-table.d.ts +31 -3
  178. package/dist/components/table/snice-table.js +89 -16
  179. package/dist/components/table/snice-table.js.map +1 -1
  180. package/dist/components/table/table-cell-presentation.d.ts +11 -0
  181. package/dist/components/table/table-cell-presentation.js +25 -8
  182. package/dist/components/table/table-cell-presentation.js.map +1 -1
  183. package/dist/components/table/table-export.d.ts +6 -0
  184. package/dist/components/table/table-export.js +11 -2
  185. package/dist/components/table/table-export.js.map +1 -1
  186. package/dist/components/table/table-master-detail.d.ts +11 -2
  187. package/dist/components/table/table-master-detail.js +20 -3
  188. package/dist/components/table/table-master-detail.js.map +1 -1
  189. package/dist/components/tag-input/snice-tag-input.d.ts +2 -2
  190. package/dist/components/tag-input/snice-tag-input.js +9 -15
  191. package/dist/components/tag-input/snice-tag-input.js.map +1 -1
  192. package/dist/index.cjs +127 -28
  193. package/dist/index.cjs.map +1 -1
  194. package/dist/index.esm.js +127 -28
  195. package/dist/index.esm.js.map +1 -1
  196. package/dist/index.iife.js +127 -28
  197. package/dist/index.iife.js.map +1 -1
  198. package/dist/react/SniceProvider.js +1 -1
  199. package/dist/react/SniceRouter.js +1 -1
  200. package/dist/react/index.js +1 -1
  201. package/dist/react/matchRoute.js +1 -1
  202. package/dist/react/useRequestHandler.js +1 -1
  203. package/dist/request-response.d.ts +24 -1
  204. package/dist/symbols.cjs +1 -1
  205. package/dist/symbols.esm.js +1 -1
  206. package/dist/testing-dom.cjs +1 -1
  207. package/dist/testing-dom.esm.js +1 -1
  208. package/dist/transitions.cjs +1 -1
  209. package/dist/transitions.esm.js +1 -1
  210. package/docs/ai/components/calendar.md +8 -0
  211. package/docs/ai/components/tag-input.md +4 -0
  212. package/docs/ai/controllers.md +22 -6
  213. package/docs/components/calendar.md +24 -0
  214. package/docs/components/tag-input.md +6 -1
  215. package/docs/controllers.md +57 -6
  216. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.6.0
2
+ * snice v7.7.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -4274,6 +4274,17 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
4274
4274
  * Give every standalone specialized table cell the common ColumnDefinition
4275
4275
  * presentation contract. The observer reapplies after Snice's differential
4276
4276
  * renderer changes shadow content; equality guards prevent mutation loops.
4277
+ *
4278
+ * Display formatters
4279
+ * ------------------
4280
+ * `formatter` is the column's display formatter and `valueFormatter` its
4281
+ * fallback, so `formatter` wins. `formatterOverride` says the cell does NOT
4282
+ * apply `column.formatter` itself, so this module owns both formatters for it.
4283
+ *
4284
+ * A host marked `formatted` was handed its display text as `value` by the
4285
+ * table, which already resolved the pipeline. Its content is that text
4286
+ * verbatim — the marker exists so a rating/progress/link/tag cell, which
4287
+ * renders structure rather than text, still shows the formatted string.
4277
4288
  */
4278
4289
  function installCellPresentation(host, formatterOverride = false) {
4279
4290
  if (installed.has(host))
@@ -4320,15 +4331,21 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
4320
4331
  ? column.tooltip(host.value, host.rowData)
4321
4332
  : (host.value == null ? '' : String(host.value));
4322
4333
  }
4323
- if (formatterOverride || column.valueFormatter) {
4324
- const formatter = formatterOverride
4325
- ? (column.formatter || column.valueFormatter)
4326
- : column.valueFormatter;
4334
+ // `formatter` beats `valueFormatter`. A cell that applies `formatter`
4335
+ // itself has already painted it, so the fallback must not overwrite that
4336
+ // content — only a formatter-less column falls through to valueFormatter.
4337
+ const formatter = column.formatter
4338
+ ? (formatterOverride ? column.formatter : null)
4339
+ : column.valueFormatter;
4340
+ // Pre-formatted hosts read the attribute, not the property: a cell whose
4341
+ // value property is typed (boolean, number) coerces the display string.
4342
+ const display = host.hasAttribute('formatted')
4343
+ ? (host.getAttribute('value') ?? '')
4344
+ : (formatter ? String(formatter(host.value, host.rowData) ?? '') : null);
4345
+ if (display !== null) {
4327
4346
  const content = host.shadowRoot?.querySelector('[part~="content"]');
4328
- if (formatter && content) {
4329
- const value = String(formatter(host.value, host.rowData) ?? '');
4330
- if (content.textContent !== value || content.childElementCount > 0)
4331
- content.textContent = value;
4347
+ if (content && (content.textContent !== display || content.childElementCount > 0)) {
4348
+ content.textContent = display;
4332
4349
  }
4333
4350
  }
4334
4351
  }
@@ -9421,6 +9438,15 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
9421
9438
  this.updateRowAttributes();
9422
9439
  this.configureCells();
9423
9440
  }
9441
+ /**
9442
+ * The column's working value for this row: `valueGetter` runs for display
9443
+ * here exactly as it does on the table's own render path, so a column whose
9444
+ * key names a server field rather than a row field still paints.
9445
+ */
9446
+ cellValue(column) {
9447
+ const raw = this.data?.[column.key];
9448
+ return column.valueGetter ? column.valueGetter(raw, this.data) : raw;
9449
+ }
9424
9450
  configureCells() {
9425
9451
  if (!this.columns) {
9426
9452
  return;
@@ -9428,7 +9454,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
9428
9454
  this.columns.forEach((column, index) => {
9429
9455
  const cellElement = this.shadowRoot?.querySelector(`[data-column-index="${index}"]`);
9430
9456
  if (cellElement) {
9431
- const value = this.data[column.key];
9457
+ const value = this.cellValue(column);
9432
9458
  if (typeof column.renderCell === 'function') {
9433
9459
  const output = column.renderCell(value, this.data, column);
9434
9460
  const replacement = output instanceof HTMLElement ? output : document.createElement('span');
@@ -9440,7 +9466,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
9440
9466
  return;
9441
9467
  }
9442
9468
  const formatter = column.formatter || column.valueFormatter;
9443
- cellElement.value = formatter ? formatter(value, this.data) : value;
9469
+ cellElement.value = formatter ? String(formatter(value, this.data) ?? '') : value;
9444
9470
  cellElement.column = formatter
9445
9471
  ? { ...column, type: 'text', formatter: undefined, valueFormatter: undefined }
9446
9472
  : column;
@@ -9530,7 +9556,6 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
9530
9556
  `;
9531
9557
  }
9532
9558
  renderCell(column, columnIndex) {
9533
- this.data[column.key];
9534
9559
  const cellComponent = this.getCellComponent(column.formatter || column.valueFormatter ? 'text' : column.type);
9535
9560
  return snice.html /*html*/ `
9536
9561
  <div class="cell cell--${column.type}" part="cell" style="${this.getCellStyles(column)}">
@@ -11144,7 +11169,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
11144
11169
  // Data rows
11145
11170
  for (const row of data) {
11146
11171
  const cells = exportColumns.map(c => {
11147
- const value = row[c.key];
11172
+ const value = this.cellValue(row, c);
11148
11173
  return this.escapeCSV(value == null ? '' : String(value), delimiter);
11149
11174
  });
11150
11175
  rows.push(cells.join(delimiter));
@@ -11256,7 +11281,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
11256
11281
  if (rows.length === 0)
11257
11282
  return false;
11258
11283
  const text = rows.map(row => columns.map(c => {
11259
- const value = row[c.key];
11284
+ const value = this.cellValue(row, c);
11260
11285
  return useFormatted ? this.formatValue(value, row, c) : this.rawValue(value);
11261
11286
  }).join(delimiter)).join('\n');
11262
11287
  try {
@@ -11281,6 +11306,15 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
11281
11306
  }
11282
11307
  return value;
11283
11308
  }
11309
+ /**
11310
+ * The column's working value for a row: `valueGetter` "runs for display,
11311
+ * sort, aggregation" — and for export, which must ship what the table shows,
11312
+ * not a row field the column may not even have.
11313
+ */
11314
+ cellValue(row, column) {
11315
+ const raw = row?.[column.key];
11316
+ return column.valueGetter ? column.valueGetter(raw, row) : raw;
11317
+ }
11284
11318
  rawValue(value) {
11285
11319
  if (value == null)
11286
11320
  return '';
@@ -11385,6 +11419,8 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
11385
11419
  this.options = null;
11386
11420
  this.tableElement = null;
11387
11421
  this.contentCache = new Map();
11422
+ /** Last value signature seen per row, so in-place mutations evict the cache. */
11423
+ this.fingerprints = new Map();
11388
11424
  this.rowByIndex = new Map();
11389
11425
  this.measuredHeights = new Map();
11390
11426
  this.onHeightChange = null;
@@ -11393,18 +11429,33 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
11393
11429
  this.tableElement = tableEl;
11394
11430
  this.options = options;
11395
11431
  this.contentCache.clear();
11432
+ this.fingerprints.clear();
11396
11433
  this.rowByIndex.clear();
11397
11434
  this.measuredHeights.clear();
11398
11435
  }
11399
- /** Eagerly construct detail content when lazy=false. */
11400
- prepare(rows) {
11436
+ /**
11437
+ * Eagerly construct detail content when lazy=false.
11438
+ *
11439
+ * `fingerprint` (the table's per-row value signature) also evicts rows whose
11440
+ * fields were mutated in place: the cache is keyed by row identity, so an
11441
+ * unchanged identity would otherwise keep serving a panel built from the
11442
+ * pre-mutation values.
11443
+ */
11444
+ prepare(rows, fingerprint) {
11401
11445
  if (!this.options)
11402
11446
  return;
11403
11447
  this.rowByIndex = new Map(rows.map((row, index) => [index, row]));
11404
11448
  const currentRows = new Set(rows);
11405
- for (const row of this.contentCache.keys()) {
11449
+ for (const row of [...this.contentCache.keys()]) {
11406
11450
  if (!currentRows.has(row))
11407
11451
  this.contentCache.delete(row);
11452
+ else if (fingerprint && this.fingerprints.get(row) !== fingerprint(row)) {
11453
+ this.contentCache.delete(row);
11454
+ this.measuredHeights.delete(row);
11455
+ }
11456
+ }
11457
+ if (fingerprint) {
11458
+ this.fingerprints = new Map(rows.map(row => [row, fingerprint(row)]));
11408
11459
  }
11409
11460
  for (const row of this.measuredHeights.keys()) {
11410
11461
  if (!currentRows.has(row))
@@ -15009,8 +15060,14 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15009
15060
  if (this.settingSortedData)
15010
15061
  return;
15011
15062
  this.unsortedData = [...this.data];
15063
+ // C3: rows delivered while a client-side sort is active must land in that
15064
+ // sorted order — the currentSort watcher only fires when the sort itself
15065
+ // changes, so without this the body silently contradicts the header. In
15066
+ // remote mode the server owns the ordering.
15067
+ if (this.mode !== 'remote' && this.currentSort.length > 0)
15068
+ this.applyLocalSortOrder();
15012
15069
  this.rebuildRowIndex();
15013
- this.masterDetail.prepare(this.data);
15070
+ this.masterDetail.prepare(this.data, row => this.rowValueSignature(row));
15014
15071
  if (this.settingDataImperative)
15015
15072
  return; // imperative setData(): caller renders
15016
15073
  this.scheduleRender('body');
@@ -15466,6 +15523,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15466
15523
  // Still render pagination
15467
15524
  if (this.pagination)
15468
15525
  this.renderPagination();
15526
+ this.updateSelectAllState();
15469
15527
  return;
15470
15528
  }
15471
15529
  if (this.data.length === 0 && this.columns.length > 0) {
@@ -15682,6 +15740,10 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15682
15740
  this.reconcileRows(entries);
15683
15741
  // Re-establish grid role + roving tabindex after the body changed.
15684
15742
  this.keyboard.refresh();
15743
+ // The select-all control describes the *filtered* row set, so it goes stale
15744
+ // whenever the denominator moves (rows delivered, filter cleared) even
15745
+ // though `selectedRows` — its only other trigger — never changed.
15746
+ this.updateSelectAllState();
15685
15747
  // Render pagination after body
15686
15748
  if (this.pagination) {
15687
15749
  this.renderPagination();
@@ -15814,15 +15876,41 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15814
15876
  `tree=${this.treeData.isEnabled() ? this.treeData.getGroupColumn() : ''}`,
15815
15877
  ].join('|');
15816
15878
  }
15879
+ /**
15880
+ * A fingerprint of a row's own field values, so a row object that was mutated
15881
+ * IN PLACE (the same identity re-delivered by a poll/websocket patch, or an
15882
+ * array the app edits) is rebuilt instead of recycled with stale cells.
15883
+ *
15884
+ * Tradeoff, deliberately chosen: this walks the row's OWN, SHALLOW fields
15885
+ * rather than running each column's valueGetter/formatter. It therefore costs
15886
+ * one string build per rendered row — never a user callback, so call counts
15887
+ * for `valueGetter` stay exactly one per row per render — and it still catches
15888
+ * getter-derived columns, because a getter reads the fields this covers.
15889
+ * Nested objects collapse to their String() form, so a mutation buried inside
15890
+ * a nested object (or an object rendered by a JSON/progress cell) is not
15891
+ * detected; those callers should hand over a new row object. Recycling keeps
15892
+ * its payoff: rows whose values did not change are still reused untouched.
15893
+ */
15894
+ rowValueSignature(rowData) {
15895
+ if (rowData === null || typeof rowData !== 'object')
15896
+ return String(rowData);
15897
+ const parts = [];
15898
+ for (const key of Object.keys(rowData)) {
15899
+ const value = rowData[key];
15900
+ parts.push(`${key}=${value === null || value === undefined ? '' : String(value)}`);
15901
+ }
15902
+ // A separator keeps {a:'1b'} from colliding with {a:'1',b:''}.
15903
+ return parts.join('|');
15904
+ }
15817
15905
  /**
15818
15906
  * The per-row rebuild signature. Selection and the plain data-index are
15819
15907
  * re-stamped on reuse (not here) so they never force a rebuild; this captures
15820
- * only what a re-stamp cannot repair: tree node state, per-row height, and —
15821
- * when a feature embeds the index in a row-scoped closure (row DnD drag index,
15822
- * master-detail toggle index) — the index itself.
15908
+ * the row's cell values plus what a re-stamp cannot repair: tree node state,
15909
+ * per-row height, and — when a feature embeds the index in a row-scoped
15910
+ * closure (row DnD drag index, master-detail toggle index) — the index itself.
15823
15911
  */
15824
15912
  rowSignature(rowData, index, structuralSig, treeRow) {
15825
- const parts = [structuralSig];
15913
+ const parts = [structuralSig, `v:${this.rowValueSignature(rowData)}`];
15826
15914
  if (treeRow)
15827
15915
  parts.push(`t:${treeRow.depth}:${treeRow.expanded ? 1 : 0}:${treeRow.hasChildren ? 1 : 0}`);
15828
15916
  if (this.rowHeightCallback)
@@ -16019,6 +16107,15 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16019
16107
  const active = page === this.currentPage ? ' pagination__btn--active' : '';
16020
16108
  return `<button class="pagination__btn pagination__page${active}" data-page="${page}">${page}</button>`;
16021
16109
  }
16110
+ /**
16111
+ * The column's display text, or null when the column declares no display
16112
+ * formatter. `formatter` (row-aware) is the display formatter and
16113
+ * `valueFormatter` its documented fallback, so `formatter` wins.
16114
+ */
16115
+ cellDisplayText(column, value, row) {
16116
+ const formatter = column.formatter || column.valueFormatter;
16117
+ return formatter ? String(formatter(value, row) ?? '') : null;
16118
+ }
16022
16119
  /** Create a cell element safely using createElement + setAttribute */
16023
16120
  createCellElement(column, value, row) {
16024
16121
  // E2: a custom renderCell bypasses the type-based cell entirely. A string
@@ -16043,22 +16140,38 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16043
16140
  if (column.align)
16044
16141
  el.setAttribute('align', column.align);
16045
16142
  el.setAttribute('in-table', 'true');
16143
+ // A declared display formatter makes the cell's value its DISPLAY text:
16144
+ // the pipeline (valueGetter -> formatter, valueFormatter as the fallback)
16145
+ // is resolved here, once, so the `value` channel a caller reads is what the
16146
+ // cell actually shows. The cell is marked `formatted` and handed a column
16147
+ // without the formatters, which keeps it from formatting again and tells
16148
+ // the shared presentation contract to paint the text over any type-specific
16149
+ // rendering (a rating/progress/link cell cannot render display text).
16150
+ const displayText = this.cellDisplayText(column, value, row);
16151
+ const cellColumn = displayText === null
16152
+ ? column
16153
+ : { ...column, formatter: undefined, valueFormatter: undefined };
16046
16154
  // Set the property directly so arrays/objects reach progress, sparkline,
16047
16155
  // JSON, action, and tag cells without a lossy attribute conversion. Keep a
16048
16156
  // primitive attribute for declarative inspection and CSS selectors.
16049
- if (column.type === 'sparkline' && value !== null && typeof value === 'object') {
16157
+ if (displayText !== null) {
16158
+ el.setAttribute('formatted', '');
16159
+ el.value = displayText;
16160
+ el.setAttribute('value', displayText);
16161
+ }
16162
+ else if (column.type === 'sparkline' && value !== null && typeof value === 'object') {
16050
16163
  el.setAttribute('value', JSON.stringify(value));
16051
16164
  if (Array.isArray(value))
16052
16165
  el.data = value;
16053
16166
  }
16054
16167
  else {
16055
16168
  el.value = value ?? '';
16056
- }
16057
- if (value === null || typeof value !== 'object') {
16058
- el.setAttribute('value', String(value ?? ''));
16169
+ if (value === null || typeof value !== 'object') {
16170
+ el.setAttribute('value', String(value ?? ''));
16171
+ }
16059
16172
  }
16060
16173
  // Column definition as property (for formatter access etc.)
16061
- el.column = column;
16174
+ el.column = cellColumn;
16062
16175
  // Row-aware formatters, conditional formatting, and action-cell events all
16063
16176
  // consume the originating row through the cell's property API.
16064
16177
  el.rowData = row ?? null;
@@ -16348,7 +16461,12 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16348
16461
  this.sortLocalData();
16349
16462
  }
16350
16463
  }
16351
- sortLocalData() {
16464
+ /**
16465
+ * Reorder `this.data` to the current client-side sort (or back to delivery
16466
+ * order when the sort was cleared). Split out of sortLocalData so a fresh
16467
+ * delivery can land sorted without re-entering the render/index bookkeeping.
16468
+ */
16469
+ applyLocalSortOrder() {
16352
16470
  if (!this.unsortedData.length) {
16353
16471
  this.unsortedData = [...this.data];
16354
16472
  }
@@ -16363,16 +16481,19 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16363
16481
  for (const { column, direction } of this.currentSort) {
16364
16482
  const colDef = this.columns.find(c => c.key === column);
16365
16483
  const customComparator = colDef?.sortComparator;
16484
+ // The value pipeline feeds BOTH sort paths: valueGetter "runs for
16485
+ // display, sort, aggregation", so a custom comparator sees the same
16486
+ // derived value the cell shows (remote columns routinely key on the
16487
+ // server's sort field and bridge the display with a getter).
16488
+ const aVal = colDef?.valueGetter ? colDef.valueGetter(a[column], a) : a[column];
16489
+ const bVal = colDef?.valueGetter ? colDef.valueGetter(b[column], b) : b[column];
16366
16490
  if (customComparator) {
16367
- const cmp = customComparator(a[column], b[column], direction);
16491
+ const cmp = customComparator(aVal, bVal, direction);
16368
16492
  if (cmp !== 0)
16369
16493
  return cmp;
16370
16494
  }
16371
16495
  else {
16372
- // Use value pipeline getter if available
16373
- const aVal = colDef?.valueGetter ? colDef.valueGetter(a[column], a) : (a[column] ?? '');
16374
- const bVal = colDef?.valueGetter ? colDef.valueGetter(b[column], b) : (b[column] ?? '');
16375
- const cmp = String(aVal).localeCompare(String(bVal), undefined, { numeric: true });
16496
+ const cmp = String(aVal ?? '').localeCompare(String(bVal ?? ''), undefined, { numeric: true });
16376
16497
  if (cmp !== 0)
16377
16498
  return direction === 'asc' ? cmp : -cmp;
16378
16499
  }
@@ -16381,6 +16502,9 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16381
16502
  });
16382
16503
  }
16383
16504
  this.settingSortedData = false;
16505
+ }
16506
+ sortLocalData() {
16507
+ this.applyLocalSortOrder();
16384
16508
  // Sort replaced this.data (new order + reference) — resync the row-index
16385
16509
  // map and drop the stale filtered snapshot before re-rendering. Render is
16386
16510
  // queued (not synchronous): sortLocalData is reachable from the currentSort