@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
@@ -49,6 +49,10 @@ export declare class IconButton extends ThemeableElement {
49
49
  size: IconButtonSize;
50
50
  /** Whether the button is disabled. */
51
51
  disabled: boolean;
52
+ protected firstUpdated(): void;
53
+ connectedCallback(): void;
54
+ disconnectedCallback(): void;
55
+ private _blockClickWhileDisabled;
52
56
  /** When set, renders an `<a>` tag instead of `<button>`. */
53
57
  href: string;
54
58
  /** Forwarded to the anchor's `target` attribute when `href` is set. */
@@ -36,6 +36,13 @@ __decorateClass([
36
36
  property({ reflect: true })
37
37
  ], ThemeableElement.prototype, "theme", 2);
38
38
 
39
+ // src/utils/dev-warn.ts
40
+ var devWarn = (tag, message) => {
41
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
42
+ console.warn(`[${tag}] ${message}`);
43
+ }
44
+ };
45
+
39
46
  // src/components/icon-button/icon-button.styles.ts
40
47
  import { css } from "lit";
41
48
  var iconButtonStyles = css`
@@ -168,11 +175,11 @@ var iconButtonStyles = css`
168
175
  --_active-bg: var(--lt-interactive-secondary-bg-active);
169
176
  }
170
177
  :host([appearance='filled'][variant='neutral']) {
171
- --_bg: var(--lt-color-neutral-500);
178
+ --_bg: var(--lt-interactive-neutral-bg);
172
179
  --_color: var(--lt-color-neutral-50);
173
- --_hover-bg: var(--lt-color-neutral-700);
180
+ --_hover-bg: var(--lt-interactive-neutral-bg-hover);
174
181
  --_hover-color: var(--lt-color-neutral-50);
175
- --_active-bg: var(--lt-color-neutral-800);
182
+ --_active-bg: var(--lt-interactive-neutral-bg-active);
176
183
  }
177
184
  :host([appearance='filled'][variant='success']) {
178
185
  --_bg: var(--lt-interactive-success-bg);
@@ -282,6 +289,14 @@ var iconButtonStyles = css`
282
289
  :host([round]) [part='base'] {
283
290
  border-radius: 50%;
284
291
  }
292
+
293
+ /* ── Reduced motion ─────────────────────────────────────────────────────── */
294
+
295
+ @media (prefers-reduced-motion: reduce) {
296
+ [part='base'] {
297
+ transition: none;
298
+ }
299
+ }
285
300
  `;
286
301
 
287
302
  // src/components/shared/aria-forwarding.ts
@@ -290,7 +305,8 @@ var FORWARDED_ARIA_ATTRIBUTES = [
290
305
  "aria-expanded",
291
306
  "aria-haspopup",
292
307
  "aria-controls",
293
- "aria-current"
308
+ "aria-current",
309
+ "aria-description"
294
310
  ];
295
311
  var AriaForwardingController = class {
296
312
  constructor(host, getTarget) {
@@ -330,11 +346,30 @@ var IconButton = class extends ThemeableElement {
330
346
  this.bordered = false;
331
347
  this.size = "md";
332
348
  this.disabled = false;
349
+ this._blockClickWhileDisabled = (e) => {
350
+ if (this.disabled) {
351
+ e.preventDefault();
352
+ e.stopImmediatePropagation();
353
+ }
354
+ };
333
355
  this.href = "";
334
356
  this.target = "";
335
357
  this.rel = "";
336
358
  this.round = false;
337
359
  }
360
+ firstUpdated() {
361
+ if (!this.label && !this.textContent?.trim()) {
362
+ devWarn("lt-icon-button", "no label or visible text \u2014 set `label` so screen readers can name the button");
363
+ }
364
+ }
365
+ connectedCallback() {
366
+ super.connectedCallback();
367
+ this.addEventListener("click", this._blockClickWhileDisabled, { capture: true });
368
+ }
369
+ disconnectedCallback() {
370
+ super.disconnectedCallback();
371
+ this.removeEventListener("click", this._blockClickWhileDisabled, { capture: true });
372
+ }
338
373
  _inner() {
339
374
  return this.icon ? html`<lt-icon name=${this.icon}></lt-icon>` : html`<slot></slot>`;
340
375
  }
@@ -5,18 +5,19 @@ import type { ImageLoading } from './image.types';
5
5
  * A thin wrapper around `<img>` with rounded corners, responsive fill support,
6
6
  * native lazy loading, and an error fallback mechanism.
7
7
  *
8
- * Native `load`/`error` events don't cross the shadow boundary, so they are
9
- * re-dispatched as composed `lt-load`/`lt-error` custom events. When a load
10
- * fails, the `fallback` slot is shown in place of the broken image; setting a
11
- * new `src` retries.
8
+ * The inner `<img>`'s `load`/`error` events don't leave the shadow root, so they
9
+ * are re-dispatched on the host under the same native names, with the `src` in
10
+ * `detail`. Like the native ones they don't bubble, so listen on `<lt-image>`
11
+ * itself. When a load fails, the `fallback` slot is shown in place of the broken
12
+ * image; setting a new `src` retries.
12
13
  *
13
14
  * @element lt-image
14
15
  * @part image - The underlying `<img>` element
15
16
  *
16
17
  * @slot fallback - Content shown in place of the image when loading fails
17
18
  *
18
- * @fires {CustomEvent<{ src: string }>} lt-load - Fired when the image loads successfully
19
- * @fires {CustomEvent<{ src: string }>} lt-error - Fired when the image fails to load
19
+ * @fires {CustomEvent<{ src: string }>} load - Fired when the image loads successfully
20
+ * @fires {CustomEvent<{ src: string }>} error - Fired when the image fails to load
20
21
  *
21
22
  * @example
22
23
  * ```html
@@ -50,7 +51,6 @@ export declare class Image extends ThemeableElement {
50
51
  private _errored;
51
52
  private get _borderRadius();
52
53
  willUpdate(changed: PropertyValues<this>): void;
53
- private _dispatch;
54
54
  private _handleLoad;
55
55
  private _handleError;
56
56
  render(): import("lit").TemplateResult<1>;
@@ -83,21 +83,16 @@ var Image = class extends ThemeableElement {
83
83
  willUpdate(changed) {
84
84
  if (changed.has("src")) this._errored = false;
85
85
  }
86
- _dispatch(name) {
87
- this.dispatchEvent(
88
- new CustomEvent(name, {
89
- detail: { src: this.src },
90
- bubbles: true,
91
- composed: true
92
- })
93
- );
94
- }
86
+ // Literal event names (not a shared helper taking `name`): the manifest analyzer reads names from
87
+ // `new CustomEvent('…')` call sites, and a variable there produced a bogus event called "name".
88
+ // Non-bubbling and non-composed, exactly like the native <img> events they mirror — a bubbling
89
+ // `error` would also reach window-level error handlers (Sentry and friends) as a spurious error.
95
90
  _handleLoad() {
96
- this._dispatch("lt-load");
91
+ this.dispatchEvent(new CustomEvent("load", { detail: { src: this.src } }));
97
92
  }
98
93
  _handleError() {
99
94
  this._errored = true;
100
- this._dispatch("lt-error");
95
+ this.dispatchEvent(new CustomEvent("error", { detail: { src: this.src } }));
101
96
  }
102
97
  render() {
103
98
  if (this._errored) {
@@ -49,7 +49,8 @@ var listStyles = css`
49
49
 
50
50
  [part='list'] {
51
51
  margin: 0;
52
- padding: 0 0 0 var(--lt-spacing-5);
52
+ padding-block: 0;
53
+ padding-inline: var(--lt-spacing-5) 0;
53
54
  list-style-position: outside;
54
55
  list-style-type: disc;
55
56
  color: var(--lt-text-default);
@@ -73,14 +74,14 @@ var listStyles = css`
73
74
  display: block;
74
75
  margin-top: var(--lt-spacing-2);
75
76
  margin-bottom: var(--lt-spacing-2);
76
- padding-left: var(--lt-spacing-5);
77
+ padding-inline-start: var(--lt-spacing-5);
77
78
  font-size: 0.9em;
78
79
  }
79
80
 
80
81
  /* No marker */
81
82
  :host([no-marker]) [part='list'] {
82
83
  list-style: none;
83
- padding-left: 0;
84
+ padding-inline-start: 0;
84
85
  }
85
86
 
86
87
  /* Divider */
@@ -168,7 +169,7 @@ var listItemStyles = css2`
168
169
 
169
170
  lt-icon {
170
171
  flex-shrink: 0;
171
- color: var(--lt-color-neutral-500);
172
+ color: var(--lt-text-muted);
172
173
  font-size: inherit;
173
174
  }
174
175
 
@@ -212,6 +213,15 @@ var listItemStyles = css2`
212
213
  opacity: 0.6;
213
214
  pointer-events: none;
214
215
  }
216
+
217
+ /* ── Reduced motion ─────────────────────────────────────────────────────── */
218
+
219
+ @media (prefers-reduced-motion: reduce) {
220
+ a.inner,
221
+ button.inner {
222
+ transition: none;
223
+ }
224
+ }
215
225
  `;
216
226
 
217
227
  // src/components/list/list-item.ts
@@ -85,7 +85,7 @@ var navItemStyles = css`
85
85
  font-size: var(--lt-nav-item-font-size, 0.9375rem);
86
86
  font-family: inherit;
87
87
  text-decoration: none;
88
- text-align: left;
88
+ text-align: start;
89
89
  transition: background 120ms;
90
90
  white-space: nowrap;
91
91
  }
@@ -138,7 +138,7 @@ var navItemStyles = css`
138
138
  }
139
139
 
140
140
  .children-inner {
141
- padding-left: var(--lt-spacing-4);
141
+ padding-inline-start: var(--lt-spacing-4);
142
142
  padding-top: var(--lt-spacing-1);
143
143
  padding-bottom: var(--lt-spacing-1);
144
144
  display: flex;
@@ -155,11 +155,11 @@ var navItemStyles = css`
155
155
  :host([orientation='horizontal']) .children {
156
156
  position: absolute;
157
157
  top: 100%;
158
- left: 0;
158
+ inset-inline-start: 0;
159
159
  min-width: 160px;
160
160
  background: var(--lt-bg-default);
161
- color: var(--lt-color-neutral-700);
162
- border: var(--lt-border-width, 1px) solid var(--lt-color-neutral-200);
161
+ color: var(--lt-text-neutral);
162
+ border: var(--lt-border-width, 1px) solid var(--lt-border-default);
163
163
  border-radius: 8px;
164
164
  box-shadow:
165
165
  0 4px 6px -1px rgb(0 0 0 / 0.08),
@@ -176,6 +176,24 @@ var navItemStyles = css`
176
176
  :host([orientation='horizontal']) .children-inner {
177
177
  padding: var(--lt-spacing-2);
178
178
  }
179
+
180
+ /* ── Reduced motion ─────────────────────────────────────────────────────── */
181
+
182
+ @media (prefers-reduced-motion: reduce) {
183
+ .item-trigger,
184
+ .chevron {
185
+ transition: none;
186
+ }
187
+
188
+ /* .children's height transition can't just be removed — nav-item.ts's _handleTransitionEnd() only finalizes
189
+ the open height to 'auto' (letting it respond to later content changes) once the CSS transition actually
190
+ completes; transition:none fires no transitionend at all, leaving it stuck at a fixed pixel height
191
+ forever. A 1ms duration is imperceptible (satisfies the reduced-motion intent) but still fires the event
192
+ the component depends on — same reasoning as lt-accordion's .content. */
193
+ .children {
194
+ transition: height 1ms;
195
+ }
196
+ }
179
197
  `;
180
198
 
181
199
  // src/components/nav/nav-item.ts
@@ -204,6 +222,10 @@ var NavItem = class extends ThemeableElement {
204
222
  }
205
223
  });
206
224
  this.requestUpdate();
225
+ void this.updateComplete.then(() => this._syncInitialHeight());
226
+ }
227
+ _syncInitialHeight() {
228
+ if (this.open && this.orientation === "vertical" && this._childrenEl) this._childrenEl.style.height = "auto";
207
229
  }
208
230
  updated(changed) {
209
231
  if (changed.has("open") && this._childrenEl && this._childrenInnerEl && this.orientation === "vertical") {
@@ -225,21 +247,15 @@ var NavItem = class extends ThemeableElement {
225
247
  }
226
248
  }
227
249
  _handleTriggerClick(e) {
228
- if (this.disabled) return;
229
- const proceed = this.dispatchEvent(
230
- new CustomEvent("nav-item-click", {
231
- detail: { href: this.href, label: this.label },
232
- bubbles: true,
233
- composed: true,
234
- cancelable: true
235
- })
236
- );
237
- if (!proceed) e.preventDefault();
250
+ if (this.disabled) {
251
+ e.preventDefault();
252
+ return;
253
+ }
238
254
  if (this._hasChildren) {
239
255
  e.preventDefault();
240
256
  this.open = !this.open;
241
257
  this.dispatchEvent(
242
- new CustomEvent("nav-collapse", {
258
+ new CustomEvent("toggle", {
243
259
  detail: { open: this.open },
244
260
  bubbles: true,
245
261
  composed: true
@@ -260,6 +276,8 @@ var NavItem = class extends ThemeableElement {
260
276
  class="item-trigger"
261
277
  href=${this.href}
262
278
  aria-current=${ariaCurrent}
279
+ aria-disabled=${this.disabled ? "true" : nothing}
280
+ tabindex=${this.disabled ? "-1" : nothing}
263
281
  @click=${this._handleTriggerClick}
264
282
  >${inner}</a
265
283
  >`;
@@ -267,6 +285,8 @@ var NavItem = class extends ThemeableElement {
267
285
  return html`<button
268
286
  class="item-trigger"
269
287
  aria-current=${ariaCurrent}
288
+ aria-expanded=${showChevron ? this.open : nothing}
289
+ aria-controls=${showChevron ? "children" : nothing}
270
290
  @click=${this._handleTriggerClick}
271
291
  ?disabled=${this.disabled}
272
292
  >
@@ -278,7 +298,7 @@ var NavItem = class extends ThemeableElement {
278
298
  <div class="item-root">
279
299
  ${this._renderTrigger()}
280
300
  ${this._hasChildren ? html`
281
- <div class="children" @transitionend=${this._handleTransitionEnd}>
301
+ <div id="children" class="children" @transitionend=${this._handleTransitionEnd}>
282
302
  <div class="children-inner">
283
303
  <slot @slotchange=${this._onSlotChange}></slot>
284
304
  </div>
@@ -9,17 +9,16 @@ import '@latty-ds/icons';
9
9
  * and nested children appear as a dropdown panel. In vertical mode, nested children
10
10
  * collapse/expand inline.
11
11
  *
12
- * SPA router integration: `nav-item-click` is cancelable — call `event.preventDefault()`
13
- * to stop the anchor's native navigation, navigate programmatically, and drive the
14
- * highlighted item via the `active` property (which also sets `aria-current="page"`).
12
+ * SPA router integration: items use the native `click` — listen for it on the `<lt-nav>` (or the item),
13
+ * call `event.preventDefault()` to stop the anchor's native navigation, and read the item from
14
+ * `event.target.closest('lt-nav-item')` (its `href` and `label`). Modifier keys (`event.metaKey`, …) come
15
+ * through as on any link. Drive the highlighted item via the `active` property (which also sets
16
+ * `aria-current="page"`).
15
17
  *
16
18
  * @element lt-nav-item
17
19
  *
18
- * @fires {CustomEvent<{ href: string; label: string }>} nav-item-click - Fired when the item
19
- * is clicked (even when it has no href). Cancelable: `preventDefault()` stops the native
20
- * anchor navigation so an SPA router can handle it.
21
- * @fires {CustomEvent<{ open: boolean }>} nav-collapse - Fired when a collapsible item
22
- * is opened or closed.
20
+ * @fires {CustomEvent<{ open: boolean }>} toggle - Fired when a collapsible item is opened or
21
+ * closed, like the native `toggle` of `<details>`.
23
22
  *
24
23
  * @slot - Nested `<lt-nav-item>` children (makes this item a collapsible group)
25
24
  *
@@ -71,6 +70,7 @@ export declare class NavItem extends ThemeableElement {
71
70
  private _childrenInnerEl?;
72
71
  private _hasChildren;
73
72
  private _onSlotChange;
73
+ private _syncInitialHeight;
74
74
  protected updated(changed: Map<string, unknown>): void;
75
75
  private _handleTransitionEnd;
76
76
  private _handleTriggerClick;
@@ -99,7 +99,7 @@ var paginationStyles = css`
99
99
  button[aria-current='page'] {
100
100
  background: var(--lt-bg-primary-subtle);
101
101
  border-color: var(--lt-border-primary);
102
- color: var(--lt-color-primary-700);
102
+ color: var(--lt-text-primary);
103
103
  font-weight: 600;
104
104
  }
105
105
 
@@ -109,15 +109,23 @@ var paginationStyles = css`
109
109
  }
110
110
 
111
111
  button:disabled {
112
- color: var(--lt-color-neutral-400);
112
+ color: var(--lt-text-disabled);
113
113
  cursor: not-allowed;
114
114
  }
115
115
 
116
116
  :host([disabled]) button {
117
- color: var(--lt-color-neutral-400);
117
+ color: var(--lt-text-disabled);
118
118
  cursor: not-allowed;
119
119
  pointer-events: none;
120
120
  }
121
+
122
+ /* ── Reduced motion ─────────────────────────────────────────────────────── */
123
+
124
+ @media (prefers-reduced-motion: reduce) {
125
+ button {
126
+ transition: none;
127
+ }
128
+ }
121
129
  `;
122
130
 
123
131
  // src/utils/dispatch.ts
@@ -125,6 +133,14 @@ var dispatch = (host, name, detail, init) => host.dispatchEvent(new CustomEvent(
125
133
 
126
134
  // src/components/pagination/pagination.ts
127
135
  import "@latty-ds/icons";
136
+ var DEFAULT_LABELS = {
137
+ navigation: "Pagination",
138
+ firstPage: "First page",
139
+ previousPage: "Previous page",
140
+ page: (n) => `Page ${n}`,
141
+ nextPage: "Next page",
142
+ lastPage: "Last page"
143
+ };
128
144
  var Pagination = class extends ThemeableElement {
129
145
  constructor() {
130
146
  super(...arguments);
@@ -132,15 +148,29 @@ var Pagination = class extends ThemeableElement {
132
148
  this.totalPages = 1;
133
149
  this.size = "md";
134
150
  this.disabled = false;
151
+ this.labels = {};
152
+ }
153
+ get _labels() {
154
+ return { ...DEFAULT_LABELS, ...this.labels };
155
+ }
156
+ /** `total-pages` as a whole number >= 1: NaN, zero and negatives collapse to a single page. */
157
+ get _total() {
158
+ const total = Math.floor(Number(this.totalPages));
159
+ return Number.isFinite(total) && total >= 1 ? total : 1;
160
+ }
161
+ /** `page` clamped into 1.._total, so `page="99"` on a 10-page set is the last page, not "no page selected". */
162
+ get _current() {
163
+ const page = Math.floor(Number(this.page));
164
+ return Number.isFinite(page) ? Math.min(Math.max(page, 1), this._total) : 1;
135
165
  }
136
166
  _go(page) {
137
- if (this.disabled || page < 1 || page > this.totalPages || page === this.page) return;
167
+ if (this.disabled || page < 1 || page > this._total || page === this._current) return;
138
168
  this.page = page;
139
169
  dispatch(this, "change", { page });
140
170
  }
141
171
  _pages() {
142
- const total = this.totalPages;
143
- const current = this.page;
172
+ const total = this._total;
173
+ const current = this._current;
144
174
  if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
145
175
  const pages = [1];
146
176
  if (current > 3) pages.push("\u2026");
@@ -153,25 +183,25 @@ var Pagination = class extends ThemeableElement {
153
183
  }
154
184
  render() {
155
185
  const pages = this._pages();
156
- const atFirst = this.page <= 1;
157
- const atLast = this.page >= this.totalPages;
186
+ const atFirst = this._current <= 1;
187
+ const atLast = this._current >= this._total;
158
188
  return html`
159
- <nav aria-label="Pagination">
160
- <button aria-label="First page" ?disabled=${atFirst || this.disabled} @click=${() => this._go(1)}>
189
+ <nav aria-label=${this._labels.navigation}>
190
+ <button aria-label=${this._labels.firstPage} ?disabled=${atFirst || this.disabled} @click=${() => this._go(1)}>
161
191
  <lt-icon name="double-caret-left" size="sm"></lt-icon>
162
192
  </button>
163
193
  <button
164
- aria-label="Previous page"
194
+ aria-label=${this._labels.previousPage}
165
195
  ?disabled=${atFirst || this.disabled}
166
- @click=${() => this._go(this.page - 1)}
196
+ @click=${() => this._go(this._current - 1)}
167
197
  >
168
198
  <lt-icon name="caret-left" size="sm"></lt-icon>
169
199
  </button>
170
200
 
171
201
  ${pages.map(
172
202
  (p) => p === "\u2026" ? html`<button class="ellipsis" aria-hidden="true" tabindex="-1">…</button>` : html`<button
173
- aria-label="Page ${p}"
174
- aria-current=${p === this.page ? "page" : nothing}
203
+ aria-label=${this._labels.page(p)}
204
+ aria-current=${p === this._current ? "page" : nothing}
175
205
  ?disabled=${this.disabled}
176
206
  @click=${() => this._go(p)}
177
207
  >
@@ -179,10 +209,18 @@ var Pagination = class extends ThemeableElement {
179
209
  </button>`
180
210
  )}
181
211
 
182
- <button aria-label="Next page" ?disabled=${atLast || this.disabled} @click=${() => this._go(this.page + 1)}>
212
+ <button
213
+ aria-label=${this._labels.nextPage}
214
+ ?disabled=${atLast || this.disabled}
215
+ @click=${() => this._go(this._current + 1)}
216
+ >
183
217
  <lt-icon name="caret-right" size="sm"></lt-icon>
184
218
  </button>
185
- <button aria-label="Last page" ?disabled=${atLast || this.disabled} @click=${() => this._go(this.totalPages)}>
219
+ <button
220
+ aria-label=${this._labels.lastPage}
221
+ ?disabled=${atLast || this.disabled}
222
+ @click=${() => this._go(this._total)}
223
+ >
186
224
  <lt-icon name="double-caret-right" size="sm"></lt-icon>
187
225
  </button>
188
226
  </nav>
@@ -202,6 +240,9 @@ __decorateClass([
202
240
  __decorateClass([
203
241
  property2({ type: Boolean, reflect: true })
204
242
  ], Pagination.prototype, "disabled", 2);
243
+ __decorateClass([
244
+ property2({ type: Object })
245
+ ], Pagination.prototype, "labels", 2);
205
246
  Pagination = __decorateClass([
206
247
  customElement("lt-pagination")
207
248
  ], Pagination);
@@ -1,5 +1,5 @@
1
1
  import { ThemeableElement } from '../../base';
2
- import type { PaginationSize } from './pagination.types';
2
+ import type { PaginationSize, PaginationLabels } from './pagination.types';
3
3
  import '@latty-ds/icons';
4
4
  /**
5
5
  * Page navigation control for paged data sets.
@@ -28,6 +28,13 @@ export declare class Pagination extends ThemeableElement {
28
28
  size: PaginationSize;
29
29
  /** Disable all controls. */
30
30
  disabled: boolean;
31
+ /** Overridable UI text (nav landmark label, control button labels), for i18n. English defaults — override only the keys you need. */
32
+ labels: Partial<PaginationLabels>;
33
+ private get _labels();
34
+ /** `total-pages` as a whole number >= 1: NaN, zero and negatives collapse to a single page. */
35
+ private get _total();
36
+ /** `page` clamped into 1.._total, so `page="99"` on a 10-page set is the last page, not "no page selected". */
37
+ private get _current();
31
38
  private _go;
32
39
  private _pages;
33
40
  render(): import("lit").TemplateResult<1>;
@@ -1 +1,16 @@
1
1
  export type PaginationSize = 'sm' | 'md' | 'lg';
2
+ /** Overridable UI text for `lt-pagination`, for i18n. */
3
+ export interface PaginationLabels {
4
+ /** Accessible landmark label for the pagination `<nav>`. */
5
+ navigation: string;
6
+ /** Accessible label for the "jump to first page" button. */
7
+ firstPage: string;
8
+ /** Accessible label for the "go to previous page" button. */
9
+ previousPage: string;
10
+ /** Accessible label for a numbered page button, given its 1-indexed page number. */
11
+ page: (n: number) => string;
12
+ /** Accessible label for the "go to next page" button. */
13
+ nextPage: string;
14
+ /** Accessible label for the "jump to last page" button. */
15
+ lastPage: string;
16
+ }
@@ -34,6 +34,13 @@ __decorateClass([
34
34
  property({ reflect: true })
35
35
  ], ThemeableElement.prototype, "theme", 2);
36
36
 
37
+ // src/utils/dev-warn.ts
38
+ var devWarn = (tag, message) => {
39
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
40
+ console.warn(`[${tag}] ${message}`);
41
+ }
42
+ };
43
+
37
44
  // src/components/progress/progress.ts
38
45
  import { customElement, property as property2 } from "lit/decorators.js";
39
46
 
@@ -61,11 +68,11 @@ var progressStyles = css`
61
68
  --_fill-color: var(--lt-interactive-error-bg);
62
69
  }
63
70
  :host([variant='neutral']) {
64
- --_fill-color: var(--lt-color-neutral-600);
71
+ --_fill-color: var(--lt-interactive-neutral-bg);
65
72
  }
66
73
 
67
74
  [part='track'] {
68
- background: var(--lt-progress-track-color, var(--lt-color-neutral-200));
75
+ background: var(--lt-progress-track-color, var(--lt-bg-overlay));
69
76
  border-radius: 999px;
70
77
  overflow: hidden;
71
78
  width: 100%;
@@ -115,6 +122,23 @@ var progressStyles = css`
115
122
  width: 100%;
116
123
  }
117
124
  }
125
+
126
+ /* ── Forced colors (Windows High Contrast) ─────────────────────────────────
127
+ Under forced-colors, both the track's neutral background and the fill's variant-coloured background are
128
+ flattened to Canvas, with no border on the track to fall back on — the whole bar disappears. A border on
129
+ the track (its boundary) plus a Highlight-coloured fill (the OS's own "active/selected" system color,
130
+ exempt from the flattening) keeps the progress amount legible regardless of which variant was set —
131
+ forced-colors mode overrides variant branding anyway, by design. ───────────────────────────────────────── */
132
+
133
+ @media (forced-colors: active) {
134
+ [part='track'] {
135
+ border: 1px solid CanvasText;
136
+ }
137
+
138
+ [part='fill'] {
139
+ background: Highlight;
140
+ }
141
+ }
118
142
  `;
119
143
 
120
144
  // src/components/progress/progress.ts
@@ -125,20 +149,28 @@ var Progress = class extends ThemeableElement {
125
149
  this.variant = "primary";
126
150
  this.size = "md";
127
151
  this.label = "";
152
+ this.max = 100;
153
+ this.valueText = "";
128
154
  this.indeterminate = false;
129
155
  }
156
+ firstUpdated() {
157
+ if (!this.label) devWarn("lt-progress", "no `label` \u2014 set one so assistive tech can name the progress bar");
158
+ }
130
159
  render() {
131
- const pct = Math.min(100, Math.max(0, this.value));
160
+ const max = Number.isFinite(this.max) && this.max > 0 ? this.max : 100;
161
+ const value = Number.isFinite(this.value) ? Math.min(max, Math.max(0, this.value)) : 0;
162
+ const fillPct = value / max * 100;
132
163
  return html`
133
164
  <div
134
165
  part="track"
135
166
  role="progressbar"
136
167
  aria-label=${this.label || nothing}
137
- aria-valuenow=${this.indeterminate ? nothing : pct}
168
+ aria-valuenow=${this.indeterminate ? nothing : value}
138
169
  aria-valuemin="0"
139
- aria-valuemax="100"
170
+ aria-valuemax=${max}
171
+ aria-valuetext=${this.indeterminate || !this.valueText ? nothing : this.valueText}
140
172
  >
141
- <div part="fill" style=${this.indeterminate ? nothing : `width:${pct}%`}></div>
173
+ <div part="fill" style=${this.indeterminate ? nothing : `width:${fillPct}%`}></div>
142
174
  </div>
143
175
  `;
144
176
  }
@@ -156,6 +188,12 @@ __decorateClass([
156
188
  __decorateClass([
157
189
  property2({ reflect: true })
158
190
  ], Progress.prototype, "label", 2);
191
+ __decorateClass([
192
+ property2({ type: Number })
193
+ ], Progress.prototype, "max", 2);
194
+ __decorateClass([
195
+ property2({ attribute: "value-text" })
196
+ ], Progress.prototype, "valueText", 2);
159
197
  __decorateClass([
160
198
  property2({ type: Boolean, reflect: true })
161
199
  ], Progress.prototype, "indeterminate", 2);
@@ -35,10 +35,22 @@ export declare class Progress extends ThemeableElement {
35
35
  * @default ''
36
36
  */
37
37
  label: string;
38
+ /**
39
+ * The value `value` is measured against (0–100 becomes 0–max). Ignored when `indeterminate` is true.
40
+ * @default 100
41
+ */
42
+ max: number;
43
+ /**
44
+ * Overrides the spoken value with a custom string (e.g. "3 of 5 steps") for scales a bare percentage
45
+ * doesn't describe well. Leave unset to let `aria-valuenow`/`aria-valuemax` speak for themselves.
46
+ * @default ''
47
+ */
48
+ valueText: string;
38
49
  /**
39
50
  * When true the bar animates continuously without a set value.
40
51
  * @default false
41
52
  */
42
53
  indeterminate: boolean;
54
+ protected firstUpdated(): void;
43
55
  render(): import("lit").TemplateResult<1>;
44
56
  }