@latty-ds/web 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/custom-elements.json +272 -41
  2. package/dist/components/accordion/index.js +4 -4
  3. package/dist/components/alert/index.js +39 -5
  4. package/dist/components/avatar/index.js +1 -1
  5. package/dist/components/badge/index.js +6 -2
  6. package/dist/components/breadcrumb/index.js +1 -1
  7. package/dist/components/button/button.d.ts +7 -2
  8. package/dist/components/button/button.types.d.ts +3 -2
  9. package/dist/components/button/index.js +83 -4
  10. package/dist/components/calendar/index.js +3 -3
  11. package/dist/components/checkbox/index.js +2 -1
  12. package/dist/components/chip/index.js +6 -2
  13. package/dist/components/color-input/index.js +6 -3
  14. package/dist/components/combobox/index.js +4 -4
  15. package/dist/components/date-input/index.js +6 -6
  16. package/dist/components/datepicker/index.js +3 -3
  17. package/dist/components/dialog/index.js +40 -6
  18. package/dist/components/divider/index.js +1 -1
  19. package/dist/components/dropdown/index.js +5 -5
  20. package/dist/components/icon-button/icon-button.d.ts +4 -0
  21. package/dist/components/icon-button/index.js +35 -1
  22. package/dist/components/image/image.d.ts +32 -1
  23. package/dist/components/image/image.types.d.ts +7 -0
  24. package/dist/components/image/index.js +41 -2
  25. package/dist/components/list/index.js +86 -7
  26. package/dist/components/list/list-item.d.ts +35 -0
  27. package/dist/components/nav/index.js +19 -5
  28. package/dist/components/nav/nav-item.d.ts +7 -1
  29. package/dist/components/pagination/index.js +1 -1
  30. package/dist/components/radio/index.js +2 -1
  31. package/dist/components/radio-group/index.js +2 -2
  32. package/dist/components/select/index.js +6 -6
  33. package/dist/components/shared/aria-forwarding.d.ts +43 -0
  34. package/dist/components/sidepanel/index.js +36 -2
  35. package/dist/components/slider/index.js +2 -2
  36. package/dist/components/snackbar/index.js +4 -4
  37. package/dist/components/surface/index.js +3 -3
  38. package/dist/components/switch/index.js +2 -1
  39. package/dist/components/tab/index.js +1 -1
  40. package/dist/components/tab-group/index.js +2 -2
  41. package/dist/components/table/index.js +3 -3
  42. package/dist/components/text/index.js +1 -1
  43. package/dist/components/textfield/index.js +7 -3
  44. package/dist/components/tooltip/index.js +1 -1
  45. package/dist/index.cjs +294 -67
  46. package/dist/index.js +336 -106
  47. package/dist/manifest.json +45 -3
  48. package/package.json +3 -3
@@ -41,7 +41,7 @@ import { customElement as customElement4, property as property5, query } from "l
41
41
  import { css } from "lit";
42
42
  var dialogStyles = css`
43
43
  :host {
44
- font-family: 'Hanken Grotesk', sans-serif;
44
+ font-family: var(--lt-typography-fontFamily, 'Hanken Grotesk', sans-serif);
45
45
  }
46
46
 
47
47
  /* Backdrop */
@@ -200,7 +200,7 @@ var iconButtonStyles = css3`
200
200
  align-items: center;
201
201
  background: var(--lt-icon-button-bg, var(--_bg));
202
202
  border-radius: var(--lt-border-radius);
203
- border: 1px solid var(--lt-icon-button-border-color, var(--_border-color));
203
+ border: var(--lt-border-width, 1px) solid var(--lt-icon-button-border-color, var(--_border-color));
204
204
  box-sizing: border-box;
205
205
  color: var(--lt-icon-button-color, var(--_color));
206
206
  cursor: pointer;
@@ -406,11 +406,45 @@ var iconButtonStyles = css3`
406
406
  }
407
407
  `;
408
408
 
409
+ // src/components/shared/aria-forwarding.ts
410
+ var FORWARDED_ARIA_ATTRIBUTES = [
411
+ "aria-pressed",
412
+ "aria-expanded",
413
+ "aria-haspopup",
414
+ "aria-controls",
415
+ "aria-current"
416
+ ];
417
+ var AriaForwardingController = class {
418
+ constructor(host, getTarget) {
419
+ this._host = host;
420
+ this._getTarget = getTarget;
421
+ this._observer = new MutationObserver(() => this._host.requestUpdate());
422
+ host.addController(this);
423
+ }
424
+ hostConnected() {
425
+ this._observer.observe(this._host, { attributes: true, attributeFilter: [...FORWARDED_ARIA_ATTRIBUTES] });
426
+ this._host.requestUpdate();
427
+ }
428
+ hostDisconnected() {
429
+ this._observer.disconnect();
430
+ }
431
+ hostUpdated() {
432
+ const target = this._getTarget();
433
+ if (!target) return;
434
+ for (const attr of FORWARDED_ARIA_ATTRIBUTES) {
435
+ const value = this._host.getAttribute(attr);
436
+ if (value === null) target.removeAttribute(attr);
437
+ else if (target.getAttribute(attr) !== value) target.setAttribute(attr, value);
438
+ }
439
+ }
440
+ };
441
+
409
442
  // src/components/icon-button/icon-button.ts
410
443
  import "@latty-ds/icons";
411
444
  var IconButton = class extends ThemeableElement {
412
445
  constructor() {
413
446
  super(...arguments);
447
+ this._ariaForwarding = new AriaForwardingController(this, () => this.shadowRoot?.querySelector('[part="base"]'));
414
448
  this.icon = "";
415
449
  this.label = "";
416
450
  this.variant = "neutral";
@@ -493,7 +527,7 @@ import { css as css4 } from "lit";
493
527
  var surfaceStyles = css4`
494
528
  :host {
495
529
  display: block;
496
- font-family: 'Hanken Grotesk', sans-serif;
530
+ font-family: var(--lt-typography-fontFamily, 'Hanken Grotesk', sans-serif);
497
531
  }
498
532
 
499
533
  .surface {
@@ -532,7 +566,7 @@ var surfaceStyles = css4`
532
566
 
533
567
  :host([appearance='outlined']) .surface {
534
568
  background: var(--lt-surface-bg, transparent);
535
- border: 1px solid var(--lt-border-default);
569
+ border: var(--lt-border-width, 1px) solid var(--lt-border-default);
536
570
  }
537
571
 
538
572
  :host([appearance='outlined']:hover) .surface {
@@ -552,7 +586,7 @@ var Surface = class extends ThemeableElement {
552
586
  this.background = "";
553
587
  }
554
588
  render() {
555
- const style = this.background ? { "--_surface-bg": resolveColorValue(this.background) } : {};
589
+ const style = this.background ? { "--lt-surface-bg": resolveColorValue(this.background) } : {};
556
590
  return html2`
557
591
  <div class="surface" part="surface" style=${styleMap(style)}>
558
592
  <slot></slot>
@@ -596,7 +630,7 @@ var textStyles = css5`
596
630
  [part='base'] {
597
631
  margin: 0;
598
632
  color: inherit;
599
- font-family: var(--lt-typography-fontfamily);
633
+ font-family: var(--lt-typography-fontFamily, 'Hanken Grotesk', sans-serif);
600
634
  }
601
635
 
602
636
  /* ── Display ─────────────────────────────────────────────────────────── */
@@ -90,7 +90,7 @@ var dividerStyles = css`
90
90
 
91
91
  [part='label'] {
92
92
  color: var(--lt-text-subtle);
93
- font-family: var(--lt-typography-fontfamily);
93
+ font-family: var(--lt-typography-fontFamily, 'Hanken Grotesk', sans-serif);
94
94
  font-size: 0.75rem;
95
95
  font-weight: 600;
96
96
  padding-inline: var(--lt-spacing-3);
@@ -1432,7 +1432,7 @@ var dropdownStyles = [
1432
1432
  css2`
1433
1433
  :host {
1434
1434
  display: inline-block;
1435
- font-family: 'Hanken Grotesk', sans-serif;
1435
+ font-family: var(--lt-typography-fontFamily, 'Hanken Grotesk', sans-serif);
1436
1436
  }
1437
1437
 
1438
1438
  .trigger-wrap {
@@ -1487,7 +1487,7 @@ var dropdownItemStyles = css3`
1487
1487
  color: var(--lt-text-default);
1488
1488
  cursor: pointer;
1489
1489
  display: flex;
1490
- font-family: 'Hanken Grotesk', sans-serif;
1490
+ font-family: var(--lt-typography-fontFamily, 'Hanken Grotesk', sans-serif);
1491
1491
  font-size: 0.875rem;
1492
1492
  gap: var(--lt-spacing-2);
1493
1493
  padding: var(--lt-spacing-2) var(--lt-spacing-4);
@@ -1587,7 +1587,7 @@ import { css as css4 } from "lit";
1587
1587
  var surfaceStyles = css4`
1588
1588
  :host {
1589
1589
  display: block;
1590
- font-family: 'Hanken Grotesk', sans-serif;
1590
+ font-family: var(--lt-typography-fontFamily, 'Hanken Grotesk', sans-serif);
1591
1591
  }
1592
1592
 
1593
1593
  .surface {
@@ -1626,7 +1626,7 @@ var surfaceStyles = css4`
1626
1626
 
1627
1627
  :host([appearance='outlined']) .surface {
1628
1628
  background: var(--lt-surface-bg, transparent);
1629
- border: 1px solid var(--lt-border-default);
1629
+ border: var(--lt-border-width, 1px) solid var(--lt-border-default);
1630
1630
  }
1631
1631
 
1632
1632
  :host([appearance='outlined']:hover) .surface {
@@ -1643,7 +1643,7 @@ var Surface = class extends ThemeableElement {
1643
1643
  this.background = "";
1644
1644
  }
1645
1645
  render() {
1646
- const style = this.background ? { "--_surface-bg": resolveColorValue(this.background) } : {};
1646
+ const style = this.background ? { "--lt-surface-bg": resolveColorValue(this.background) } : {};
1647
1647
  return html2`
1648
1648
  <div class="surface" part="surface" style=${styleMap(style)}>
1649
1649
  <slot></slot>
@@ -4,6 +4,9 @@ import '@latty-ds/icons';
4
4
  /**
5
5
  * Icon-only button. Requires `label` for screen readers since there is no visible text.
6
6
  *
7
+ * Forwards host `aria-pressed`, `aria-expanded`, `aria-haspopup`, `aria-controls`, and
8
+ * `aria-current` onto the internal native control (e.g. for dropdown triggers).
9
+ *
7
10
  * @element lt-icon-button
8
11
  *
9
12
  * @example
@@ -18,6 +21,7 @@ import '@latty-ds/icons';
18
21
  */
19
22
  export declare class IconButton extends ThemeableElement {
20
23
  static styles: import("lit").CSSResult;
24
+ private _ariaForwarding;
21
25
  /** Icon name from the icon library. */
22
26
  icon: string;
23
27
  /** Accessible label for screen readers. Required when there is no visible text. */
@@ -55,7 +55,7 @@ var iconButtonStyles = css`
55
55
  align-items: center;
56
56
  background: var(--lt-icon-button-bg, var(--_bg));
57
57
  border-radius: var(--lt-border-radius);
58
- border: 1px solid var(--lt-icon-button-border-color, var(--_border-color));
58
+ border: var(--lt-border-width, 1px) solid var(--lt-icon-button-border-color, var(--_border-color));
59
59
  box-sizing: border-box;
60
60
  color: var(--lt-icon-button-color, var(--_color));
61
61
  cursor: pointer;
@@ -261,11 +261,45 @@ var iconButtonStyles = css`
261
261
  }
262
262
  `;
263
263
 
264
+ // src/components/shared/aria-forwarding.ts
265
+ var FORWARDED_ARIA_ATTRIBUTES = [
266
+ "aria-pressed",
267
+ "aria-expanded",
268
+ "aria-haspopup",
269
+ "aria-controls",
270
+ "aria-current"
271
+ ];
272
+ var AriaForwardingController = class {
273
+ constructor(host, getTarget) {
274
+ this._host = host;
275
+ this._getTarget = getTarget;
276
+ this._observer = new MutationObserver(() => this._host.requestUpdate());
277
+ host.addController(this);
278
+ }
279
+ hostConnected() {
280
+ this._observer.observe(this._host, { attributes: true, attributeFilter: [...FORWARDED_ARIA_ATTRIBUTES] });
281
+ this._host.requestUpdate();
282
+ }
283
+ hostDisconnected() {
284
+ this._observer.disconnect();
285
+ }
286
+ hostUpdated() {
287
+ const target = this._getTarget();
288
+ if (!target) return;
289
+ for (const attr of FORWARDED_ARIA_ATTRIBUTES) {
290
+ const value = this._host.getAttribute(attr);
291
+ if (value === null) target.removeAttribute(attr);
292
+ else if (target.getAttribute(attr) !== value) target.setAttribute(attr, value);
293
+ }
294
+ }
295
+ };
296
+
264
297
  // src/components/icon-button/icon-button.ts
265
298
  import "@latty-ds/icons";
266
299
  var IconButton = class extends ThemeableElement {
267
300
  constructor() {
268
301
  super(...arguments);
302
+ this._ariaForwarding = new AriaForwardingController(this, () => this.shadowRoot?.querySelector('[part="base"]'));
269
303
  this.icon = "";
270
304
  this.label = "";
271
305
  this.variant = "neutral";
@@ -1,9 +1,29 @@
1
+ import { type PropertyValues } from 'lit';
1
2
  import { ThemeableElement } from '../../base';
3
+ import type { ImageLoading } from './image.types';
2
4
  /**
3
- * A thin wrapper around `<img>` with rounded corners and responsive fill support.
5
+ * A thin wrapper around `<img>` with rounded corners, responsive fill support,
6
+ * native lazy loading, and an error fallback mechanism.
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.
4
12
  *
5
13
  * @element lt-image
6
14
  * @part image - The underlying `<img>` element
15
+ *
16
+ * @slot fallback - Content shown in place of the image when loading fails
17
+ *
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
20
+ *
21
+ * @example
22
+ * ```html
23
+ * <lt-image src="/cover.jpg" alt="Album cover" loading="lazy" rounded>
24
+ * <div slot="fallback">No cover available</div>
25
+ * </lt-image>
26
+ * ```
7
27
  */
8
28
  export declare class Image extends ThemeableElement {
9
29
  static styles: import("lit").CSSResult;
@@ -21,6 +41,17 @@ export declare class Image extends ThemeableElement {
21
41
  * Size the `lt-image` element externally to define the crop box.
22
42
  */
23
43
  responsive: boolean;
44
+ /**
45
+ * Forwarded to the underlying `<img loading>` attribute.
46
+ * `lazy` defers loading until the image approaches the viewport.
47
+ */
48
+ loading: ImageLoading | '';
49
+ /** Whether the current `src` failed to load — shows the `fallback` slot. */
50
+ private _errored;
24
51
  private get _borderRadius();
52
+ willUpdate(changed: PropertyValues<this>): void;
53
+ private _dispatch;
54
+ private _handleLoad;
55
+ private _handleError;
25
56
  render(): import("lit").TemplateResult<1>;
26
57
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Native lazy-loading behaviour forwarded to the underlying `<img loading>` attribute.
3
+ *
4
+ * - `eager`: Load immediately, regardless of viewport position (browser default)
5
+ * - `lazy`: Defer loading until the image approaches the viewport
6
+ */
7
+ export type ImageLoading = 'eager' | 'lazy';
@@ -35,7 +35,8 @@ __decorateClass([
35
35
  ], ThemeableElement.prototype, "theme", 2);
36
36
 
37
37
  // src/components/image/image.ts
38
- import { customElement, property as property2 } from "lit/decorators.js";
38
+ import { customElement, property as property2, state } from "lit/decorators.js";
39
+ import { ifDefined } from "lit/directives/if-defined.js";
39
40
 
40
41
  // src/components/image/image.styles.ts
41
42
  import { css } from "lit";
@@ -72,15 +73,47 @@ var Image = class extends ThemeableElement {
72
73
  this.alt = "";
73
74
  this.rounded = null;
74
75
  this.responsive = false;
76
+ this.loading = "";
77
+ this._errored = false;
75
78
  }
76
79
  get _borderRadius() {
77
80
  if (this.rounded === null) return void 0;
78
81
  return this.rounded || "var(--lt-border-radius)";
79
82
  }
83
+ willUpdate(changed) {
84
+ if (changed.has("src")) this._errored = false;
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
+ }
95
+ _handleLoad() {
96
+ this._dispatch("lt-load");
97
+ }
98
+ _handleError() {
99
+ this._errored = true;
100
+ this._dispatch("lt-error");
101
+ }
80
102
  render() {
103
+ if (this._errored) {
104
+ return html`<slot name="fallback"></slot>`;
105
+ }
81
106
  const radius = this._borderRadius;
82
107
  return html`
83
- <img part="image" src=${this.src} alt=${this.alt} style=${radius ? `border-radius: ${radius}` : ""} />
108
+ <img
109
+ part="image"
110
+ src=${this.src}
111
+ alt=${this.alt}
112
+ loading=${ifDefined(this.loading || void 0)}
113
+ style=${radius ? `border-radius: ${radius}` : ""}
114
+ @load=${this._handleLoad}
115
+ @error=${this._handleError}
116
+ />
84
117
  `;
85
118
  }
86
119
  };
@@ -97,6 +130,12 @@ __decorateClass([
97
130
  __decorateClass([
98
131
  property2({ type: Boolean, reflect: true })
99
132
  ], Image.prototype, "responsive", 2);
133
+ __decorateClass([
134
+ property2({ reflect: true })
135
+ ], Image.prototype, "loading", 2);
136
+ __decorateClass([
137
+ state()
138
+ ], Image.prototype, "_errored", 2);
100
139
  Image = __decorateClass([
101
140
  customElement("lt-image")
102
141
  ], Image);
@@ -43,7 +43,7 @@ import { css } from "lit";
43
43
  var listStyles = css`
44
44
  :host {
45
45
  display: block;
46
- font-family: 'Hanken Grotesk', sans-serif;
46
+ font-family: var(--lt-typography-fontFamily, 'Hanken Grotesk', sans-serif);
47
47
  --list-marker-color: var(--lt-color-neutral-500);
48
48
  }
49
49
 
@@ -153,6 +153,7 @@ var listStyles = css`
153
153
  // src/components/list/list-item.ts
154
154
  import { html, nothing } from "lit";
155
155
  import { customElement, property as property2 } from "lit/decorators.js";
156
+ import { ifDefined } from "lit/directives/if-defined.js";
156
157
 
157
158
  // src/components/list/list-item.styles.ts
158
159
  import { css as css2 } from "lit";
@@ -182,6 +183,47 @@ var listItemStyles = css2`
182
183
  color: var(--lt-color-neutral-500);
183
184
  font-size: inherit;
184
185
  }
186
+
187
+ /* interactive rows — native button/anchor reset + states */
188
+ a.inner,
189
+ button.inner {
190
+ background: transparent;
191
+ border: none;
192
+ border-radius: var(--lt-border-radius);
193
+ box-sizing: border-box;
194
+ color: inherit;
195
+ cursor: pointer;
196
+ font: inherit;
197
+ margin: 0;
198
+ padding: var(--lt-spacing-2);
199
+ text-align: start;
200
+ text-decoration: none;
201
+ transition: background-color 120ms ease;
202
+ width: 100%;
203
+ }
204
+
205
+ a.inner:hover:not([aria-disabled='true']),
206
+ button.inner:hover:not([disabled]) {
207
+ background: var(--lt-list-item-hover-bg, var(--lt-bg-subtle));
208
+ }
209
+
210
+ a.inner:active:not([aria-disabled='true']),
211
+ button.inner:active:not([disabled]) {
212
+ background: var(--lt-list-item-active-bg, var(--lt-bg-surface));
213
+ }
214
+
215
+ a.inner:focus-visible,
216
+ button.inner:focus-visible {
217
+ outline: 3px solid var(--lt-border-focus);
218
+ outline-offset: -3px;
219
+ }
220
+
221
+ button.inner[disabled],
222
+ a.inner[aria-disabled='true'] {
223
+ cursor: not-allowed;
224
+ opacity: 0.6;
225
+ pointer-events: none;
226
+ }
185
227
  `;
186
228
 
187
229
  // src/components/list/list-item.ts
@@ -191,19 +233,41 @@ var ListItem = class extends ThemeableElement {
191
233
  super(...arguments);
192
234
  this.iconStart = "";
193
235
  this.iconEnd = "";
236
+ this.clickable = false;
237
+ this.href = "";
238
+ this.target = "";
239
+ this.rel = "";
240
+ this.disabled = false;
194
241
  }
195
242
  connectedCallback() {
196
243
  super.connectedCallback();
197
244
  if (!this.hasAttribute("role")) this.setAttribute("role", "listitem");
198
245
  }
199
246
  render() {
200
- return html`
201
- <div class="inner">
202
- ${this.iconStart ? html`<lt-icon class="icon-start" name=${this.iconStart}></lt-icon>` : nothing}
203
- <span class="content"><slot></slot></span>
204
- ${this.iconEnd ? html`<lt-icon class="icon-end" name=${this.iconEnd}></lt-icon>` : nothing}
205
- </div>
247
+ const inner = html`
248
+ ${this.iconStart ? html`<lt-icon class="icon-start" name=${this.iconStart}></lt-icon>` : nothing}
249
+ <span class="content"><slot></slot></span>
250
+ ${this.iconEnd ? html`<lt-icon class="icon-end" name=${this.iconEnd}></lt-icon>` : nothing}
206
251
  `;
252
+ if (this.href) {
253
+ const rel = this.rel || (this.target === "_blank" ? "noopener noreferrer" : void 0);
254
+ return html`
255
+ <a
256
+ class="inner"
257
+ part="base"
258
+ href=${this.href}
259
+ target=${ifDefined(this.target || void 0)}
260
+ rel=${ifDefined(rel)}
261
+ aria-disabled=${this.disabled ? "true" : nothing}
262
+ tabindex=${this.disabled ? "-1" : nothing}
263
+ >${inner}</a
264
+ >
265
+ `;
266
+ }
267
+ if (this.clickable) {
268
+ return html`<button class="inner" part="base" type="button" ?disabled=${this.disabled}>${inner}</button>`;
269
+ }
270
+ return html`<div class="inner" part="base">${inner}</div>`;
207
271
  }
208
272
  };
209
273
  ListItem.styles = listItemStyles;
@@ -213,6 +277,21 @@ __decorateClass([
213
277
  __decorateClass([
214
278
  property2({ attribute: "icon-end" })
215
279
  ], ListItem.prototype, "iconEnd", 2);
280
+ __decorateClass([
281
+ property2({ type: Boolean, reflect: true })
282
+ ], ListItem.prototype, "clickable", 2);
283
+ __decorateClass([
284
+ property2()
285
+ ], ListItem.prototype, "href", 2);
286
+ __decorateClass([
287
+ property2()
288
+ ], ListItem.prototype, "target", 2);
289
+ __decorateClass([
290
+ property2()
291
+ ], ListItem.prototype, "rel", 2);
292
+ __decorateClass([
293
+ property2({ type: Boolean, reflect: true })
294
+ ], ListItem.prototype, "disabled", 2);
216
295
  ListItem = __decorateClass([
217
296
  customElement("lt-list-item")
218
297
  ], ListItem);
@@ -7,10 +7,16 @@ import '@latty-ds/icons';
7
7
  * cannot contain plain `<li>` elements due to browser parsing rules. By wrapping
8
8
  * the `<li>` in a custom element's shadow DOM, we can properly nest lists.
9
9
  *
10
+ * Items can also be interactive: set `clickable` to render the row as a native
11
+ * `<button>` (picker rows, settings rows), or `href` to render it as an `<a>`.
12
+ * Interactive rows get hover/active/focus states and a disabled state.
13
+ *
10
14
  * @element lt-list-item
11
15
  *
12
16
  * @slot - Default slot for list item content
13
17
  *
18
+ * @csspart base - The row container (`<div>`, `<button>` when `clickable`, or `<a>` when `href` is set)
19
+ *
14
20
  * @example
15
21
  * ```html
16
22
  * <lt-list>
@@ -29,6 +35,15 @@ import '@latty-ds/icons';
29
35
  * <lt-list-item icon-start="info" icon-end="arrow-right">Details</lt-list-item>
30
36
  * </lt-list>
31
37
  * ```
38
+ *
39
+ * @example
40
+ * Interactive rows:
41
+ * ```html
42
+ * <lt-list>
43
+ * <lt-list-item clickable>Pick me</lt-list-item>
44
+ * <lt-list-item href="/settings/profile" icon-end="arrow-right">Profile</lt-list-item>
45
+ * </lt-list>
46
+ * ```
32
47
  */
33
48
  export declare class ListItem extends ThemeableElement {
34
49
  static styles: import("lit").CSSResult;
@@ -37,5 +52,25 @@ export declare class ListItem extends ThemeableElement {
37
52
  iconStart: string;
38
53
  /** Icon name to display after the item content. */
39
54
  iconEnd: string;
55
+ /**
56
+ * Renders the row as a native `<button>` with hover/active/focus states.
57
+ * Listen for `click` on the element to react. Ignored when `href` is set.
58
+ * @default false
59
+ */
60
+ clickable: boolean;
61
+ /** When set, renders the row as an `<a>` with hover/active/focus states. */
62
+ href: string;
63
+ /** Forwarded to the anchor's `target` attribute when `href` is set. */
64
+ target: string;
65
+ /**
66
+ * Forwarded to the anchor's `rel` attribute when `href` is set.
67
+ * Defaults to `noopener noreferrer` when `target="_blank"` and `rel` is not specified.
68
+ */
69
+ rel: string;
70
+ /**
71
+ * Disables the interactive row. Only relevant with `clickable` or `href`.
72
+ * @default false
73
+ */
74
+ disabled: boolean;
40
75
  render(): import("lit").TemplateResult<1>;
41
76
  }
@@ -159,7 +159,7 @@ var navItemStyles = css`
159
159
  min-width: 160px;
160
160
  background: var(--lt-bg-default);
161
161
  color: var(--lt-color-neutral-700);
162
- border: 1px solid var(--lt-color-neutral-200);
162
+ border: var(--lt-border-width, 1px) solid var(--lt-color-neutral-200);
163
163
  border-radius: 8px;
164
164
  box-shadow:
165
165
  0 4px 6px -1px rgb(0 0 0 / 0.08),
@@ -226,13 +226,15 @@ var NavItem = class extends ThemeableElement {
226
226
  }
227
227
  _handleTriggerClick(e) {
228
228
  if (this.disabled) return;
229
- this.dispatchEvent(
229
+ const proceed = this.dispatchEvent(
230
230
  new CustomEvent("nav-item-click", {
231
231
  detail: { href: this.href, label: this.label },
232
232
  bubbles: true,
233
- composed: true
233
+ composed: true,
234
+ cancelable: true
234
235
  })
235
236
  );
237
+ if (!proceed) e.preventDefault();
236
238
  if (this._hasChildren) {
237
239
  e.preventDefault();
238
240
  this.open = !this.open;
@@ -252,10 +254,22 @@ var NavItem = class extends ThemeableElement {
252
254
  <span class="label">${this.label}</span>
253
255
  ${showChevron ? html`<lt-icon class="chevron" name="caret-down" size="sm"></lt-icon>` : nothing}
254
256
  `;
257
+ const ariaCurrent = this.active ? "page" : nothing;
255
258
  if (this.href && !this._hasChildren) {
256
- return html`<a class="item-trigger" href=${this.href} @click=${this._handleTriggerClick}>${inner}</a>`;
259
+ return html`<a
260
+ class="item-trigger"
261
+ href=${this.href}
262
+ aria-current=${ariaCurrent}
263
+ @click=${this._handleTriggerClick}
264
+ >${inner}</a
265
+ >`;
257
266
  }
258
- return html`<button class="item-trigger" @click=${this._handleTriggerClick} ?disabled=${this.disabled}>
267
+ return html`<button
268
+ class="item-trigger"
269
+ aria-current=${ariaCurrent}
270
+ @click=${this._handleTriggerClick}
271
+ ?disabled=${this.disabled}
272
+ >
259
273
  ${inner}
260
274
  </button>`;
261
275
  }
@@ -9,10 +9,15 @@ 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"`).
15
+ *
12
16
  * @element lt-nav-item
13
17
  *
14
18
  * @fires {CustomEvent<{ href: string; label: string }>} nav-item-click - Fired when the item
15
- * is clicked (even when it has no href).
19
+ * is clicked (even when it has no href). Cancelable: `preventDefault()` stops the native
20
+ * anchor navigation so an SPA router can handle it.
16
21
  * @fires {CustomEvent<{ open: boolean }>} nav-collapse - Fired when a collapsible item
17
22
  * is opened or closed.
18
23
  *
@@ -46,6 +51,7 @@ export declare class NavItem extends ThemeableElement {
46
51
  iconStart: string;
47
52
  /**
48
53
  * Whether this item is currently the active/selected page.
54
+ * Sets `aria-current="page"` on the internal trigger.
49
55
  * @default false
50
56
  */
51
57
  active: boolean;
@@ -42,7 +42,7 @@ import { css } from "lit";
42
42
  var paginationStyles = css`
43
43
  :host {
44
44
  display: inline-flex;
45
- font-family: 'Hanken Grotesk', sans-serif;
45
+ font-family: var(--lt-typography-fontFamily, 'Hanken Grotesk', sans-serif);
46
46
  }
47
47
 
48
48
  nav {
@@ -42,7 +42,7 @@ import { css } from "lit";
42
42
  var radioStyles = css`
43
43
  :host {
44
44
  display: inline-block;
45
- font-family: 'Hanken Grotesk', sans-serif;
45
+ font-family: var(--lt-typography-fontFamily, 'Hanken Grotesk', sans-serif);
46
46
  }
47
47
 
48
48
  .radio-wrapper {
@@ -246,6 +246,7 @@ var Radio = class extends ThemeableElement {
246
246
  * @private
247
247
  */
248
248
  handleChange(e) {
249
+ e.stopPropagation();
249
250
  const target = e.target;
250
251
  this.checked = target.checked;
251
252
  this.dispatchEvent(
@@ -42,7 +42,7 @@ import { css } from "lit";
42
42
  var radioGroupStyles = css`
43
43
  :host {
44
44
  display: block;
45
- font-family: 'Hanken Grotesk', sans-serif;
45
+ font-family: var(--lt-typography-fontFamily, 'Hanken Grotesk', sans-serif);
46
46
  }
47
47
 
48
48
  .radio-group {
@@ -111,7 +111,7 @@ var textStyles = css2`
111
111
  [part='base'] {
112
112
  margin: 0;
113
113
  color: inherit;
114
- font-family: var(--lt-typography-fontfamily);
114
+ font-family: var(--lt-typography-fontFamily, 'Hanken Grotesk', sans-serif);
115
115
  }
116
116
 
117
117
  /* ── Display ─────────────────────────────────────────────────────────── */