@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.
- package/custom-elements.json +272 -41
- package/dist/components/accordion/index.js +4 -4
- package/dist/components/alert/index.js +39 -5
- package/dist/components/avatar/index.js +1 -1
- package/dist/components/badge/index.js +6 -2
- package/dist/components/breadcrumb/index.js +1 -1
- package/dist/components/button/button.d.ts +7 -2
- package/dist/components/button/button.types.d.ts +3 -2
- package/dist/components/button/index.js +83 -4
- package/dist/components/calendar/index.js +3 -3
- package/dist/components/checkbox/index.js +2 -1
- package/dist/components/chip/index.js +6 -2
- package/dist/components/color-input/index.js +6 -3
- package/dist/components/combobox/index.js +4 -4
- package/dist/components/date-input/index.js +6 -6
- package/dist/components/datepicker/index.js +3 -3
- package/dist/components/dialog/index.js +40 -6
- package/dist/components/divider/index.js +1 -1
- package/dist/components/dropdown/index.js +5 -5
- package/dist/components/icon-button/icon-button.d.ts +4 -0
- package/dist/components/icon-button/index.js +35 -1
- package/dist/components/image/image.d.ts +32 -1
- package/dist/components/image/image.types.d.ts +7 -0
- package/dist/components/image/index.js +41 -2
- package/dist/components/list/index.js +86 -7
- package/dist/components/list/list-item.d.ts +35 -0
- package/dist/components/nav/index.js +19 -5
- package/dist/components/nav/nav-item.d.ts +7 -1
- package/dist/components/pagination/index.js +1 -1
- package/dist/components/radio/index.js +2 -1
- package/dist/components/radio-group/index.js +2 -2
- package/dist/components/select/index.js +6 -6
- package/dist/components/shared/aria-forwarding.d.ts +43 -0
- package/dist/components/sidepanel/index.js +36 -2
- package/dist/components/slider/index.js +2 -2
- package/dist/components/snackbar/index.js +4 -4
- package/dist/components/surface/index.js +3 -3
- package/dist/components/switch/index.js +2 -1
- package/dist/components/tab/index.js +1 -1
- package/dist/components/tab-group/index.js +2 -2
- package/dist/components/table/index.js +3 -3
- package/dist/components/text/index.js +1 -1
- package/dist/components/textfield/index.js +7 -3
- package/dist/components/tooltip/index.js +1 -1
- package/dist/index.cjs +294 -67
- package/dist/index.js +336 -106
- package/dist/manifest.json +45 -3
- 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 ? { "--
|
|
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-
|
|
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-
|
|
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 ? { "--
|
|
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
|
|
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
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
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
|
|
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 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-
|
|
114
|
+
font-family: var(--lt-typography-fontFamily, 'Hanken Grotesk', sans-serif);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/* ── Display ─────────────────────────────────────────────────────────── */
|