@latty-ds/web 0.10.3 → 1.0.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 (101) hide show
  1. package/custom-elements.json +1970 -229
  2. package/dist/components/accordion/index.js +36 -2
  3. package/dist/components/alert/alert.d.ts +29 -6
  4. package/dist/components/alert/alert.types.d.ts +5 -0
  5. package/dist/components/alert/index.js +127 -20
  6. package/dist/components/avatar/avatar.d.ts +2 -0
  7. package/dist/components/avatar/index.js +10 -3
  8. package/dist/components/badge/index.js +1 -1
  9. package/dist/components/breadcrumb/breadcrumb.d.ts +4 -0
  10. package/dist/components/breadcrumb/breadcrumb.types.d.ts +5 -0
  11. package/dist/components/breadcrumb/index.js +20 -3
  12. package/dist/components/button/button.d.ts +33 -0
  13. package/dist/components/button/index.js +138 -15
  14. package/dist/components/calendar/calendar.d.ts +15 -7
  15. package/dist/components/calendar/calendar.types.d.ts +17 -1
  16. package/dist/components/calendar/index.js +80 -27
  17. package/dist/components/checkbox/checkbox.d.ts +14 -2
  18. package/dist/components/checkbox/checkbox.types.d.ts +5 -0
  19. package/dist/components/checkbox/index.js +82 -5
  20. package/dist/components/chip/chip.d.ts +4 -1
  21. package/dist/components/chip/chip.types.d.ts +5 -0
  22. package/dist/components/chip/index.js +29 -1
  23. package/dist/components/color-input/color-input.d.ts +11 -2
  24. package/dist/components/color-input/color-input.types.d.ts +5 -0
  25. package/dist/components/color-input/index.js +92 -8
  26. package/dist/components/combobox/combobox.d.ts +18 -2
  27. package/dist/components/combobox/combobox.types.d.ts +7 -0
  28. package/dist/components/combobox/index.js +164 -23
  29. package/dist/components/date-input/date-input.d.ts +22 -7
  30. package/dist/components/date-input/date-input.types.d.ts +7 -0
  31. package/dist/components/date-input/index.js +218 -55
  32. package/dist/components/datepicker/datepicker.d.ts +9 -1
  33. package/dist/components/datepicker/index.js +108 -15
  34. package/dist/components/dialog/dialog.d.ts +39 -19
  35. package/dist/components/dialog/dialog.types.d.ts +5 -0
  36. package/dist/components/dialog/index.js +240 -93
  37. package/dist/components/divider/index.js +2 -2
  38. package/dist/components/dropdown/dropdown-item.d.ts +1 -1
  39. package/dist/components/dropdown/dropdown.d.ts +4 -0
  40. package/dist/components/dropdown/index.js +43 -3
  41. package/dist/components/icon-button/icon-button.d.ts +4 -0
  42. package/dist/components/icon-button/index.js +39 -4
  43. package/dist/components/image/image.d.ts +7 -7
  44. package/dist/components/image/index.js +6 -11
  45. package/dist/components/list/index.js +14 -4
  46. package/dist/components/nav/index.js +37 -17
  47. package/dist/components/nav/nav-item.d.ts +8 -8
  48. package/dist/components/pagination/index.js +57 -16
  49. package/dist/components/pagination/pagination.d.ts +8 -1
  50. package/dist/components/pagination/pagination.types.d.ts +15 -0
  51. package/dist/components/progress/index.js +44 -6
  52. package/dist/components/progress/progress.d.ts +12 -0
  53. package/dist/components/radio/index.js +148 -9
  54. package/dist/components/radio/radio.d.ts +34 -2
  55. package/dist/components/radio/radio.types.d.ts +5 -0
  56. package/dist/components/radio-group/index.js +120 -29
  57. package/dist/components/radio-group/radio-group.d.ts +27 -2
  58. package/dist/components/select/index.js +197 -44
  59. package/dist/components/select/select.d.ts +38 -4
  60. package/dist/components/select/select.types.d.ts +5 -0
  61. package/dist/components/shared/aria-forwarding.d.ts +1 -1
  62. package/dist/components/shared/form-default.d.ts +30 -0
  63. package/dist/components/shared/form-disabled.d.ts +40 -0
  64. package/dist/components/shared/form-field.d.ts +7 -0
  65. package/dist/components/shared/form-validity.d.ts +17 -0
  66. package/dist/components/shared/roving.d.ts +11 -0
  67. package/dist/components/shared/scroll-lock.d.ts +2 -0
  68. package/dist/components/sidepanel/index.js +278 -75
  69. package/dist/components/sidepanel/sidepanel.d.ts +32 -10
  70. package/dist/components/sidepanel/sidepanel.types.d.ts +10 -1
  71. package/dist/components/skeleton/index.js +3 -8
  72. package/dist/components/slider/index.js +307 -75
  73. package/dist/components/slider/slider.d.ts +16 -3
  74. package/dist/components/snackbar/index.js +82 -9
  75. package/dist/components/snackbar/snackbar.d.ts +17 -1
  76. package/dist/components/snackbar/snackbar.types.d.ts +5 -0
  77. package/dist/components/surface/index.js +8 -0
  78. package/dist/components/switch/index.js +101 -5
  79. package/dist/components/switch/switch.d.ts +13 -2
  80. package/dist/components/switch/switch.types.d.ts +5 -0
  81. package/dist/components/tab/index.js +15 -14
  82. package/dist/components/tab/tab.d.ts +0 -1
  83. package/dist/components/tab-group/index.js +102 -30
  84. package/dist/components/tab-group/tab-group.d.ts +17 -2
  85. package/dist/components/table/index.js +105 -61
  86. package/dist/components/table/table.d.ts +8 -2
  87. package/dist/components/table/table.types.d.ts +4 -3
  88. package/dist/components/text/index.js +16 -1
  89. package/dist/components/text/text.d.ts +1 -1
  90. package/dist/components/textfield/index.js +243 -35
  91. package/dist/components/textfield/textfield.d.ts +24 -2
  92. package/dist/components/textfield/textfield.types.d.ts +11 -0
  93. package/dist/components/tooltip/index.js +176 -49
  94. package/dist/components/tooltip/tooltip.d.ts +17 -0
  95. package/dist/components/tooltip/tooltip.types.d.ts +5 -1
  96. package/dist/index.cjs +2467 -692
  97. package/dist/index.js +2478 -699
  98. package/dist/manifest.json +202 -32
  99. package/dist/utils/dev-warn.d.ts +5 -0
  100. package/dist/utils/index.d.ts +1 -0
  101. package/package.json +4 -8
@@ -9,6 +9,11 @@ var __decorateClass = (decorators, target, key, kind) => {
9
9
  return result;
10
10
  };
11
11
 
12
+ // src/components/shared/roving.ts
13
+ var enabledItems = (items) => items.filter((item) => !item.disabled);
14
+ var rovingTargetOf = (enabled, value) => enabled.find((item) => item.value === value) ?? enabled[0];
15
+ var stepIndex = (index, length, delta) => (Math.max(index, 0) + delta + length) % length;
16
+
12
17
  // src/components/tab-group/tab-group.ts
13
18
  import { html as html3 } from "lit";
14
19
 
@@ -142,7 +147,7 @@ var tabStyles = css2`
142
147
  }
143
148
 
144
149
  :host([disabled]) .tab {
145
- color: var(--lt-color-neutral-300);
150
+ color: var(--lt-text-disabled);
146
151
  cursor: not-allowed;
147
152
  opacity: 0.5;
148
153
  }
@@ -185,6 +190,17 @@ var tabStyles = css2`
185
190
  box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
186
191
  color: var(--lt-text-primary);
187
192
  }
193
+
194
+ /* ── Reduced motion ─────────────────────────────────────────────────────── */
195
+
196
+ @media (prefers-reduced-motion: reduce) {
197
+ /* :host([data-variant='pills']) .tab must be listed explicitly, not just .tab — it outranks a bare .tab in
198
+ specificity (an extra :host() attribute selector), so it would otherwise keep its own transition. */
199
+ .tab,
200
+ :host([data-variant='pills']) .tab {
201
+ transition: none;
202
+ }
203
+ }
188
204
  `;
189
205
 
190
206
  // src/components/tab/tab.ts
@@ -198,21 +214,13 @@ var Tab = class extends ThemeableElement {
198
214
  this.active = false;
199
215
  this.disabled = false;
200
216
  this.size = "md";
201
- this._handleHostClick = () => {
202
- if (this.disabled) return;
203
- this.dispatchEvent(
204
- new CustomEvent("tab-click", {
205
- detail: { value: this.value },
206
- bubbles: true,
207
- composed: true
208
- })
209
- );
210
- };
217
+ // Enter/Space activate the tab exactly like a mouse click: a native `click` on the host, which
218
+ // `lt-tab-group` (or any consumer) listens for. There is no separate tab event.
211
219
  this._handleHostKeydown = (e) => {
212
220
  if (this.disabled) return;
213
221
  if (e.key === "Enter" || e.key === " ") {
214
222
  e.preventDefault();
215
- this._handleHostClick();
223
+ this.click();
216
224
  }
217
225
  };
218
226
  }
@@ -220,12 +228,10 @@ var Tab = class extends ThemeableElement {
220
228
  super.connectedCallback();
221
229
  this.setAttribute("role", "tab");
222
230
  this.tabIndex = 0;
223
- this.addEventListener("click", this._handleHostClick);
224
231
  this.addEventListener("keydown", this._handleHostKeydown);
225
232
  }
226
233
  disconnectedCallback() {
227
234
  super.disconnectedCallback();
228
- this.removeEventListener("click", this._handleHostClick);
229
235
  this.removeEventListener("keydown", this._handleHostKeydown);
230
236
  }
231
237
  updated(changedProperties) {
@@ -308,31 +314,64 @@ TabPanel = __decorateClass([
308
314
  ], TabPanel);
309
315
 
310
316
  // src/components/tab-group/tab-group.ts
317
+ var groupCount = 0;
311
318
  var TabGroup = class extends ThemeableElement {
312
319
  constructor() {
313
320
  super(...arguments);
314
321
  this.value = "";
315
322
  this.appearance = "default";
316
323
  this.size = "md";
324
+ // Scopes generated tab/panel ids to this instance, so two groups on the same page whose tabs happen to share a
325
+ // `value` (e.g. two groups each with a "settings" tab) don't collide.
326
+ this._uid = `lt-tab-group-${groupCount++}`;
327
+ // Reacts to a slotted tab's `disabled` attribute changing after the fact (e.g. a consumer disabling a tab
328
+ // dynamically, with no accompanying change to this group's own value/size/appearance), so the roving tabindex
329
+ // and keyboard navigation immediately stop landing on it.
330
+ this._tabObserver = new MutationObserver(() => this.updateTabs());
317
331
  /**
318
332
  * Handles click events from tabs.
319
333
  * @private
320
334
  */
321
335
  this.handleTabClick = (e) => {
322
- const tab = e.target;
323
- if (tab.disabled) return;
324
- this.value = tab.value;
325
- this.updateTabs();
326
- this.updatePanels();
327
- this.dispatchEvent(
328
- new CustomEvent("change", {
329
- detail: { value: this.value },
330
- bubbles: true,
331
- composed: true
332
- })
333
- );
336
+ this._select(e.currentTarget);
337
+ };
338
+ /**
339
+ * ArrowLeft/Right move the roving tab stop (and, per the automatic-activation model, select the tab); Home/End
340
+ * jump to the first/last enabled tab. Disabled tabs are skipped entirely, matching the radiogroup/listbox
341
+ * patterns elsewhere in the DS.
342
+ * @private
343
+ */
344
+ this.handleTabKeydown = (e) => {
345
+ const enabled = enabledItems(this.tabs ?? []);
346
+ if (enabled.length === 0) return;
347
+ const index = enabled.indexOf(e.currentTarget);
348
+ let next;
349
+ switch (e.key) {
350
+ case "ArrowRight":
351
+ next = enabled[stepIndex(index, enabled.length, 1)];
352
+ break;
353
+ case "ArrowLeft":
354
+ next = enabled[stepIndex(index, enabled.length, -1)];
355
+ break;
356
+ case "Home":
357
+ next = enabled[0];
358
+ break;
359
+ case "End":
360
+ next = enabled[enabled.length - 1];
361
+ break;
362
+ }
363
+ if (!next) return;
364
+ e.preventDefault();
365
+ this._select(next);
366
+ next.focus();
334
367
  };
335
368
  }
369
+ connectedCallback() {
370
+ super.connectedCallback();
371
+ this._tabObserver.observe(this, { attributes: true, attributeFilter: ["disabled"], subtree: true });
372
+ this.updateTabs();
373
+ this.updatePanels();
374
+ }
336
375
  /**
337
376
  * Lifecycle: Called after the element's DOM has been updated.
338
377
  */
@@ -341,24 +380,29 @@ var TabGroup = class extends ThemeableElement {
341
380
  this.updatePanels();
342
381
  }
343
382
  /**
344
- * Updates all tabs with the correct active state and size.
383
+ * Updates all tabs with the correct active/roving-tabindex state, size, and ARIA wiring.
345
384
  * @private
346
385
  */
347
386
  updateTabs() {
348
387
  if (!this.tabs) return;
388
+ const enabled = enabledItems(this.tabs);
389
+ const rovingTarget = rovingTargetOf(enabled, this.value);
349
390
  this.tabs.forEach((tab) => {
350
391
  tab.size = this.size;
351
392
  tab.active = tab.value === this.value;
393
+ tab.tabIndex = tab === rovingTarget ? 0 : -1;
394
+ if (!tab.id) tab.id = `${this._uid}-tab-${tab.value}`;
352
395
  if (this.appearance === "pills") {
353
396
  tab.setAttribute("data-variant", "pills");
354
397
  } else {
355
398
  tab.removeAttribute("data-variant");
356
399
  }
357
- tab.addEventListener("tab-click", this.handleTabClick);
400
+ tab.addEventListener("click", this.handleTabClick);
401
+ tab.addEventListener("keydown", this.handleTabKeydown);
358
402
  });
359
403
  }
360
404
  /**
361
- * Updates panel visibility based on selected value.
405
+ * Updates panel visibility and ARIA wiring based on selected value.
362
406
  * @private
363
407
  */
364
408
  updatePanels() {
@@ -370,8 +414,34 @@ var TabGroup = class extends ThemeableElement {
370
414
  } else {
371
415
  panel.removeAttribute("active");
372
416
  }
417
+ if (!panel.id) panel.id = `${this._uid}-panel-${panelValue}`;
418
+ panel.setAttribute("role", "tabpanel");
419
+ panel.setAttribute("aria-labelledby", `${this._uid}-tab-${panelValue}`);
420
+ if (!panel.hasAttribute("tabindex")) panel.setAttribute("tabindex", "0");
421
+ });
422
+ this.tabs?.forEach((tab) => {
423
+ const panel = this.panels.find((p) => (p.getAttribute("value") ?? p.getAttribute("data-value")) === tab.value);
424
+ if (panel) tab.setAttribute("aria-controls", panel.id);
425
+ else tab.removeAttribute("aria-controls");
373
426
  });
374
427
  }
428
+ /**
429
+ * Selects a tab (used by both the click and keyboard-navigation handlers below).
430
+ * @private
431
+ */
432
+ _select(tab) {
433
+ if (tab.disabled || tab.value === this.value) return;
434
+ this.value = tab.value;
435
+ this.updateTabs();
436
+ this.updatePanels();
437
+ this.dispatchEvent(
438
+ new CustomEvent("change", {
439
+ detail: { value: this.value },
440
+ bubbles: true,
441
+ composed: true
442
+ })
443
+ );
444
+ }
375
445
  /**
376
446
  * Handles slot changes.
377
447
  * @private
@@ -385,9 +455,11 @@ var TabGroup = class extends ThemeableElement {
385
455
  */
386
456
  disconnectedCallback() {
387
457
  super.disconnectedCallback();
458
+ this._tabObserver.disconnect();
388
459
  if (this.tabs) {
389
460
  this.tabs.forEach((tab) => {
390
- tab.removeEventListener("tab-click", this.handleTabClick);
461
+ tab.removeEventListener("click", this.handleTabClick);
462
+ tab.removeEventListener("keydown", this.handleTabKeydown);
391
463
  });
392
464
  }
393
465
  }
@@ -63,25 +63,40 @@ export declare class TabGroup extends ThemeableElement {
63
63
  * @private
64
64
  */
65
65
  private panels;
66
+ private readonly _uid;
67
+ private readonly _tabObserver;
68
+ connectedCallback(): void;
66
69
  /**
67
70
  * Lifecycle: Called after the element's DOM has been updated.
68
71
  */
69
72
  firstUpdated(): void;
70
73
  /**
71
- * Updates all tabs with the correct active state and size.
74
+ * Updates all tabs with the correct active/roving-tabindex state, size, and ARIA wiring.
72
75
  * @private
73
76
  */
74
77
  private updateTabs;
75
78
  /**
76
- * Updates panel visibility based on selected value.
79
+ * Updates panel visibility and ARIA wiring based on selected value.
77
80
  * @private
78
81
  */
79
82
  private updatePanels;
83
+ /**
84
+ * Selects a tab (used by both the click and keyboard-navigation handlers below).
85
+ * @private
86
+ */
87
+ private _select;
80
88
  /**
81
89
  * Handles click events from tabs.
82
90
  * @private
83
91
  */
84
92
  private handleTabClick;
93
+ /**
94
+ * ArrowLeft/Right move the roving tab stop (and, per the automatic-activation model, select the tab); Home/End
95
+ * jump to the first/last enabled tab. Disabled tabs are skipped entirely, matching the radiogroup/listbox
96
+ * patterns elsewhere in the DS.
97
+ * @private
98
+ */
99
+ private handleTabKeydown;
85
100
  /**
86
101
  * Handles slot changes.
87
102
  * @private
@@ -51,6 +51,8 @@ var tableStyles = css`
51
51
  }
52
52
 
53
53
  .table-container {
54
+ /* The loading overlay is absolutely positioned, so it needs this as its containing block. */
55
+ position: relative;
54
56
  overflow-x: auto;
55
57
  width: 100%;
56
58
  }
@@ -73,38 +75,46 @@ var tableStyles = css`
73
75
  th {
74
76
  color: var(--lt-text-default);
75
77
  font-weight: 600;
76
- text-align: left;
78
+ text-align: start;
77
79
  white-space: nowrap;
78
80
  }
79
81
 
80
82
  th[data-align='center'] {
81
83
  text-align: center;
82
84
  }
83
- th[data-align='right'] {
84
- text-align: right;
85
+ th[data-align='end'] {
86
+ text-align: end;
85
87
  }
86
88
 
87
89
  /* Sortable headers */
88
90
  th.sortable {
89
91
  cursor: pointer;
92
+ padding: 0;
90
93
  user-select: none;
91
- transition: background-color 120ms ease;
92
94
  }
93
95
 
94
96
  th.sortable:hover {
95
97
  background: var(--lt-bg-overlay);
96
98
  }
97
99
 
100
+ /* The button (not the th) carries the density padding, so it — not just a slice of it — is the click target. */
98
101
  th.sortable .header-content {
99
102
  align-items: center;
103
+ background: none;
104
+ border: none;
105
+ color: inherit;
106
+ cursor: pointer;
100
107
  display: flex;
108
+ font: inherit;
101
109
  gap: var(--lt-spacing-2);
110
+ transition: background-color 120ms ease;
111
+ width: 100%;
102
112
  }
103
113
 
104
114
  th[data-align='center'] .header-content {
105
115
  justify-content: center;
106
116
  }
107
- th[data-align='right'] .header-content {
117
+ th[data-align='end'] .header-content {
108
118
  justify-content: flex-end;
109
119
  }
110
120
 
@@ -155,8 +165,8 @@ var tableStyles = css`
155
165
  td[data-align='center'] {
156
166
  text-align: center;
157
167
  }
158
- td[data-align='right'] {
159
- text-align: right;
168
+ td[data-align='end'] {
169
+ text-align: end;
160
170
  }
161
171
 
162
172
  /* Density variants */
@@ -175,11 +185,30 @@ var tableStyles = css`
175
185
  padding: var(--lt-spacing-4) var(--lt-spacing-5);
176
186
  }
177
187
 
188
+ /* A sortable th's own padding moves onto its button (below), so the whole visible cell — not just the area
189
+ inside the th's padding — is the click target. Matches the density rules above in specificity and comes
190
+ after them in source order, so it wins the cascade regardless of selector order elsewhere in this file. */
191
+ :host th.sortable {
192
+ padding: 0;
193
+ }
194
+
195
+ :host([density='compact']) th.sortable .header-content {
196
+ padding: var(--lt-spacing-2) var(--lt-spacing-3);
197
+ }
198
+
199
+ :host([density='normal']) th.sortable .header-content {
200
+ padding: var(--lt-spacing-3) var(--lt-spacing-4);
201
+ }
202
+
203
+ :host([density='comfortable']) th.sortable .header-content {
204
+ padding: var(--lt-spacing-4) var(--lt-spacing-5);
205
+ }
206
+
178
207
  /* Sticky columns */
179
208
  th.sticky {
180
209
  background: var(--lt-bg-surface);
181
210
  box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
182
- left: 0;
211
+ inset-inline-start: 0;
183
212
  position: sticky;
184
213
  z-index: 2;
185
214
  }
@@ -187,7 +216,7 @@ var tableStyles = css`
187
216
  td.sticky {
188
217
  background: var(--lt-bg-default);
189
218
  box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
190
- left: 0;
219
+ inset-inline-start: 0;
191
220
  position: sticky;
192
221
  z-index: 1;
193
222
  }
@@ -211,12 +240,12 @@ var tableStyles = css`
211
240
 
212
241
  :host([bordered]) th,
213
242
  :host([bordered]) td {
214
- border-right: 1px solid var(--lt-border-default);
243
+ border-inline-end: 1px solid var(--lt-border-default);
215
244
  }
216
245
 
217
246
  :host([bordered]) th:last-child,
218
247
  :host([bordered]) td:last-child {
219
- border-right: none;
248
+ border-inline-end: none;
220
249
  }
221
250
 
222
251
  /* Empty state */
@@ -229,11 +258,11 @@ var tableStyles = css`
229
258
  /* Loading state */
230
259
  .loading-overlay {
231
260
  align-items: center;
232
- background: rgba(255, 255, 255, 0.8);
261
+ background: color-mix(in srgb, var(--lt-bg-default) 80%, transparent);
233
262
  display: flex;
234
263
  height: 100%;
235
264
  justify-content: center;
236
- left: 0;
265
+ inset-inline-start: 0;
237
266
  position: absolute;
238
267
  top: 0;
239
268
  width: 100%;
@@ -273,7 +302,7 @@ var tableStyles = css`
273
302
  gap: var(--lt-spacing-2);
274
303
  grid-template-columns: 120px 1fr;
275
304
  padding: var(--lt-spacing-2) 0;
276
- text-align: left;
305
+ text-align: start;
277
306
  }
278
307
 
279
308
  :host([responsive-mode='stack']) td::before {
@@ -293,14 +322,30 @@ var tableStyles = css`
293
322
  }
294
323
  }
295
324
 
296
- th.sortable:focus-visible {
325
+ th.sortable .header-content:focus-visible {
297
326
  outline: 2px solid var(--lt-interactive-primary-bg);
298
327
  outline-offset: -2px;
299
328
  }
329
+
330
+ /* ── Reduced motion ─────────────────────────────────────────────────────── */
331
+
332
+ @media (prefers-reduced-motion: reduce) {
333
+ th.sortable .header-content,
334
+ .sort-icon,
335
+ tbody tr {
336
+ transition: none;
337
+ }
338
+ }
300
339
  `;
301
340
 
302
341
  // src/components/table/table.ts
303
342
  import "@latty-ds/icons";
343
+ var compareValues = (a, b) => {
344
+ if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
345
+ if (typeof a === "number" && typeof b === "number") return a - b;
346
+ if (typeof a === "bigint" && typeof b === "bigint") return a < b ? -1 : a > b ? 1 : 0;
347
+ return String(a).localeCompare(String(b), void 0, { numeric: true });
348
+ };
304
349
  var jsonArrayConverter = {
305
350
  fromAttribute(value) {
306
351
  if (value === null) return [];
@@ -324,6 +369,7 @@ var Table = class extends ThemeableElement {
324
369
  this.bordered = false;
325
370
  this.loading = false;
326
371
  this.emptyMessage = "No data available";
372
+ this.label = "";
327
373
  this.rowKey = "";
328
374
  this.internalSort = null;
329
375
  this.sortedData = [];
@@ -365,12 +411,7 @@ var Table = class extends ThemeableElement {
365
411
  if (aValue == null && bValue == null) return 0;
366
412
  if (aValue == null) return 1;
367
413
  if (bValue == null) return -1;
368
- let comparison = 0;
369
- if (typeof aValue === "number" && typeof bValue === "number") {
370
- comparison = aValue - bValue;
371
- } else {
372
- comparison = String(aValue).localeCompare(String(bValue));
373
- }
414
+ const comparison = compareValues(aValue, bValue);
374
415
  return this.currentSort.direction === "asc" ? comparison : -comparison;
375
416
  });
376
417
  this.sortedData = sorted;
@@ -393,7 +434,7 @@ var Table = class extends ThemeableElement {
393
434
  this.internalSort = newSort;
394
435
  }
395
436
  this.dispatchEvent(
396
- new CustomEvent("sort-change", {
437
+ new CustomEvent("sortchange", {
397
438
  bubbles: true,
398
439
  composed: true,
399
440
  detail: {
@@ -446,15 +487,7 @@ var Table = class extends ThemeableElement {
446
487
  sticky: !!column.sticky,
447
488
  "hide-on-mobile": !!column.hideOnMobile
448
489
  })}
449
- data-align=${column.align || "left"}
450
- @click=${() => this.handleHeaderClick(column)}
451
- @keydown=${(e) => {
452
- if (column.sortable && (e.key === "Enter" || e.key === " ")) {
453
- e.preventDefault();
454
- this.handleHeaderClick(column);
455
- }
456
- }}
457
- tabindex=${column.sortable ? "0" : "-1"}
490
+ data-align=${column.align || "start"}
458
491
  aria-sort=${column.sortable ? this.currentSort?.key === column.key ? this.currentSort.direction === "asc" ? "ascending" : "descending" : "none" : nothing}
459
492
  style=${styleMap({
460
493
  ...column.width ? { width: column.width } : {},
@@ -462,10 +495,20 @@ var Table = class extends ThemeableElement {
462
495
  })}
463
496
  >
464
497
  ${column.sortable ? html`
465
- <div class="header-content">
498
+ <button
499
+ type="button"
500
+ class="header-content"
501
+ @click=${() => this.handleHeaderClick(column)}
502
+ @keydown=${(e) => {
503
+ if (e.key === "Enter" || e.key === " ") {
504
+ e.preventDefault();
505
+ this.handleHeaderClick(column);
506
+ }
507
+ }}
508
+ >
466
509
  <span>${column.label}</span>
467
510
  ${this.renderSortIcon(column)}
468
- </div>
511
+ </button>
469
512
  ` : column.label}
470
513
  </th>
471
514
  `
@@ -483,46 +526,44 @@ var Table = class extends ThemeableElement {
483
526
  return html`
484
527
  <tbody>
485
528
  <tr>
486
- <td colspan=${columns.length} class="empty-state">${this.emptyMessage}</td>
529
+ <td colspan=${Math.max(columns.length, 1)} class="empty-state">${this.emptyMessage}</td>
487
530
  </tr>
488
531
  </tbody>
489
532
  `;
490
533
  }
491
- return html`
492
- <tbody>
493
- ${repeat(
494
- this.sortedData,
495
- (row) => this.rowKey ? row[this.rowKey] : JSON.stringify(row),
496
- (row, index) => html`
497
- <tr>
498
- ${columns.map(
499
- (column) => html`
500
- <td
501
- class=${classMap({
502
- sticky: !!column.sticky,
503
- "hide-on-mobile": !!column.hideOnMobile
504
- })}
505
- data-align=${column.align || "left"}
506
- data-label=${column.label}
507
- style=${styleMap({
508
- ...column.width ? { width: column.width } : {},
509
- ...column.minWidth ? { minWidth: column.minWidth } : {}
510
- })}
511
- >
512
- ${this.renderCell(column, row, index)}
513
- </td>
514
- `
515
- )}
516
- </tr>
534
+ const renderRow = (row, index) => html`
535
+ <tr>
536
+ ${columns.map(
537
+ (column) => html`
538
+ <td
539
+ class=${classMap({
540
+ sticky: !!column.sticky,
541
+ "hide-on-mobile": !!column.hideOnMobile
542
+ })}
543
+ data-align=${column.align || "start"}
544
+ data-label=${column.label}
545
+ style=${styleMap({
546
+ ...column.width ? { width: column.width } : {},
547
+ ...column.minWidth ? { minWidth: column.minWidth } : {}
548
+ })}
549
+ >
550
+ ${this.renderCell(column, row, index)}
551
+ </td>
517
552
  `
518
553
  )}
554
+ </tr>
555
+ `;
556
+ const rows = this.rowKey ? repeat(this.sortedData, (row) => row[this.rowKey], renderRow) : this.sortedData.map(renderRow);
557
+ return html`
558
+ <tbody>
559
+ ${rows}
519
560
  </tbody>
520
561
  `;
521
562
  }
522
563
  render() {
523
564
  return html`
524
565
  <div class="table-container">
525
- <table>
566
+ <table aria-label=${this.label || nothing}>
526
567
  ${this.renderHeader()} ${this.renderBody()}
527
568
  </table>
528
569
  ${this.loading ? html`
@@ -562,6 +603,9 @@ __decorateClass([
562
603
  __decorateClass([
563
604
  property2({ attribute: "empty-message" })
564
605
  ], Table.prototype, "emptyMessage", 2);
606
+ __decorateClass([
607
+ property2({ reflect: true })
608
+ ], Table.prototype, "label", 2);
565
609
  __decorateClass([
566
610
  property2({ attribute: "row-key" })
567
611
  ], Table.prototype, "rowKey", 2);
@@ -19,14 +19,14 @@ import '@latty-ds/icons';
19
19
  * - Hide columns on mobile
20
20
  * - Accessible with keyboard navigation
21
21
  *
22
- * @fires sort-change - Fired when sort changes
22
+ * @fires sortchange - Fired when sort changes
23
23
  *
24
24
  * @example
25
25
  * ```typescript
26
26
  * const columns = [
27
27
  * { key: 'name', label: 'Name', sortable: true },
28
28
  * { key: 'email', label: 'Email', sortable: true },
29
- * { key: 'age', label: 'Age', sortable: true, align: 'right' }
29
+ * { key: 'age', label: 'Age', sortable: true, align: 'end' }
30
30
  * ];
31
31
  *
32
32
  * const data = [
@@ -104,6 +104,12 @@ export declare class Table<T = Record<string, unknown>> extends ThemeableElement
104
104
  * @default 'No data available'
105
105
  */
106
106
  emptyMessage: string;
107
+ /**
108
+ * Accessible name for the table. Required when a page has more than one table, since column labels alone
109
+ * don't tell assistive tech which table it's in.
110
+ * @default ''
111
+ */
112
+ label: string;
107
113
  /**
108
114
  * Field name to use as a stable row identity key for efficient DOM reuse during sorts/updates.
109
115
  * When omitted, falls back to the row's JSON representation.
@@ -18,9 +18,10 @@ export type TableDensity = 'compact' | 'normal' | 'comfortable';
18
18
  */
19
19
  export type SortDirection = 'asc' | 'desc' | null;
20
20
  /**
21
- * Alignment for cell content.
21
+ * Alignment for cell content. Logical, not physical — `'end'` means "the trailing edge of the reading
22
+ * direction" (right in LTR, left in RTL), matching CSS `text-align: end` rather than `text-align: right`.
22
23
  */
23
- export type CellAlign = 'left' | 'center' | 'right';
24
+ export type CellAlign = 'start' | 'center' | 'end';
24
25
  /**
25
26
  * Configuration for a table column.
26
27
  */
@@ -54,7 +55,7 @@ export interface TableColumn<T = Record<string, unknown>> {
54
55
  minWidth?: string;
55
56
  /**
56
57
  * Text alignment for this column.
57
- * @default 'left'
58
+ * @default 'start'
58
59
  */
59
60
  align?: CellAlign;
60
61
  /**
@@ -200,6 +200,20 @@ var textStyles = css`
200
200
  `;
201
201
 
202
202
  // src/components/text/text.ts
203
+ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
204
+ "h1",
205
+ "h2",
206
+ "h3",
207
+ "h4",
208
+ "h5",
209
+ "h6",
210
+ "p",
211
+ "span",
212
+ "div",
213
+ "label",
214
+ "strong",
215
+ "em"
216
+ ]);
203
217
  var VARIANT_TAG = {
204
218
  "display-2xl": "p",
205
219
  "display-xl": "p",
@@ -224,7 +238,8 @@ var Text = class extends ThemeableElement {
224
238
  this.as = "";
225
239
  }
226
240
  render() {
227
- const tag = unsafeStatic(this.as || VARIANT_TAG[this.variant] || "p");
241
+ const requested = this.as && ALLOWED_TAGS.has(this.as) ? this.as : "";
242
+ const tag = unsafeStatic(requested || VARIANT_TAG[this.variant] || "p");
228
243
  return html`<${tag} part="base"><slot></slot></${tag}>`;
229
244
  }
230
245
  };