@f-ewald/components 0.1.0 → 0.2.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.
@@ -0,0 +1,111 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { LitElement, css, html, nothing } from "lit";
8
+ import { customElement, property } from "lit/decorators.js";
9
+ import { repeat } from "lit/directives/repeat.js";
10
+ import { tokens } from "./tokens.js";
11
+ let instanceCount = 0;
12
+ /**
13
+ * Single-select group of full-width cards, each with a label and optional
14
+ * description — for a handful of meaningfully different choices where the
15
+ * description matters. For many short, same-shaped options (a color swatch,
16
+ * a basemap style), use `radio-pills` instead. Wraps native radio inputs for
17
+ * keyboard/a11y and fires `change` rather than relying on form submission.
18
+ *
19
+ * @element radio-cards
20
+ * @fires change - A card was selected; detail: { value }.
21
+ */
22
+ let RadioCards = class RadioCards extends LitElement {
23
+ constructor() {
24
+ super(...arguments);
25
+ /** Options to render, one card each. */
26
+ this.options = [];
27
+ /** Currently selected value. */
28
+ this.value = "";
29
+ this.#name = `radio-cards-${++instanceCount}`;
30
+ }
31
+ static { this.styles = [
32
+ tokens,
33
+ css `
34
+ :host {
35
+ display: block;
36
+ }
37
+ .options {
38
+ display: flex;
39
+ gap: 0.6rem;
40
+ flex-wrap: wrap;
41
+ }
42
+ .card {
43
+ flex: 1 1 180px;
44
+ display: flex;
45
+ align-items: flex-start;
46
+ gap: 0.5rem;
47
+ border: 1px solid var(--ui-border, #e2e8f0);
48
+ border-radius: var(--ui-radius-sm, 0.25rem);
49
+ padding: 0.6rem 0.7rem;
50
+ cursor: pointer;
51
+ font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);
52
+ font-size: var(--ui-font-size-sm, 0.75rem);
53
+ }
54
+ .card:has(input:checked) {
55
+ border-color: var(--ui-primary, #4f46e5);
56
+ background: var(--ui-surface-muted, #f8fafc);
57
+ }
58
+ .card input {
59
+ width: auto;
60
+ margin-top: 0.15rem;
61
+ accent-color: var(--ui-primary, #4f46e5);
62
+ cursor: pointer;
63
+ }
64
+ .card-label {
65
+ font-weight: 600;
66
+ color: var(--ui-text, #0f172a);
67
+ }
68
+ .card-description {
69
+ display: block;
70
+ color: var(--ui-text-muted, #64748b);
71
+ font-weight: 400;
72
+ }
73
+ `,
74
+ ]; }
75
+ #name;
76
+ _onChange(value) {
77
+ this.value = value;
78
+ this.dispatchEvent(new CustomEvent("change", { detail: { value }, bubbles: true, composed: true }));
79
+ }
80
+ render() {
81
+ return html `
82
+ <div class="options">
83
+ ${repeat(this.options, (opt) => opt.value, (opt) => html `
84
+ <label class="card">
85
+ <input
86
+ type="radio"
87
+ name=${this.#name}
88
+ ?checked=${this.value === opt.value}
89
+ @change=${() => this._onChange(opt.value)}
90
+ />
91
+ <span>
92
+ <span class="card-label">${opt.label}</span>
93
+ ${opt.description ? html `<span class="card-description">${opt.description}</span>` : nothing}
94
+ </span>
95
+ </label>
96
+ `)}
97
+ </div>
98
+ `;
99
+ }
100
+ };
101
+ __decorate([
102
+ property({ attribute: false })
103
+ ], RadioCards.prototype, "options", void 0);
104
+ __decorate([
105
+ property()
106
+ ], RadioCards.prototype, "value", void 0);
107
+ RadioCards = __decorate([
108
+ customElement("radio-cards")
109
+ ], RadioCards);
110
+ export { RadioCards };
111
+ //# sourceMappingURL=radio-cards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"radio-cards.js","sourceRoot":"","sources":["../src/radio-cards.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAQrC,IAAI,aAAa,GAAG,CAAC,CAAC;AAEtB;;;;;;;;;GASG;AAEI,IAAM,UAAU,GAAhB,MAAM,UAAW,SAAQ,UAAU;IAAnC;;QA8CL,wCAAwC;QACR,YAAO,GAAsB,EAAE,CAAC;QAChE,gCAAgC;QACpB,UAAK,GAAG,EAAE,CAAC;QAEd,UAAK,GAAG,eAAe,EAAE,aAAa,EAAE,CAAC;IA+BpD,CAAC;aAjFiB,WAAM,GAAG;QACvB,MAAM;QACN,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAwCF;KACF,AA3CqB,CA2CpB;IAOO,KAAK,CAAoC;IAE1C,SAAS,CAAC,KAAa;QAC7B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACtG,CAAC;IAEQ,MAAM;QACb,OAAO,IAAI,CAAA;;UAEL,MAAM,CACN,IAAI,CAAC,OAAO,EACZ,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,EAClB,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAA;;;;uBAIA,IAAI,CAAC,KAAK;2BACN,IAAI,CAAC,KAAK,KAAK,GAAG,CAAC,KAAK;0BACzB,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;;;2CAGd,GAAG,CAAC,KAAK;kBAClC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAA,kCAAkC,GAAG,CAAC,WAAW,SAAS,CAAC,CAAC,CAAC,OAAO;;;WAGjG,CACF;;KAEJ,CAAC;IACJ,CAAC;CACF,CAAA;AAnCiC;IAA/B,QAAQ,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;2CAAiC;AAEpD;IAAX,QAAQ,EAAE;yCAAY;AAjDZ,UAAU;IADtB,aAAa,CAAC,aAAa,CAAC;GAChB,UAAU,CAkFtB","sourcesContent":["import { LitElement, css, html, nothing } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { repeat } from \"lit/directives/repeat.js\";\nimport { tokens } from \"./tokens.js\";\n\nexport interface RadioCardOption {\n value: string;\n label: string;\n description?: string;\n}\n\nlet instanceCount = 0;\n\n/**\n * Single-select group of full-width cards, each with a label and optional\n * description — for a handful of meaningfully different choices where the\n * description matters. For many short, same-shaped options (a color swatch,\n * a basemap style), use `radio-pills` instead. Wraps native radio inputs for\n * keyboard/a11y and fires `change` rather than relying on form submission.\n *\n * @element radio-cards\n * @fires change - A card was selected; detail: { value }.\n */\n@customElement(\"radio-cards\")\nexport class RadioCards extends LitElement {\n static override styles = [\n tokens,\n css`\n :host {\n display: block;\n }\n .options {\n display: flex;\n gap: 0.6rem;\n flex-wrap: wrap;\n }\n .card {\n flex: 1 1 180px;\n display: flex;\n align-items: flex-start;\n gap: 0.5rem;\n border: 1px solid var(--ui-border, #e2e8f0);\n border-radius: var(--ui-radius-sm, 0.25rem);\n padding: 0.6rem 0.7rem;\n cursor: pointer;\n font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);\n font-size: var(--ui-font-size-sm, 0.75rem);\n }\n .card:has(input:checked) {\n border-color: var(--ui-primary, #4f46e5);\n background: var(--ui-surface-muted, #f8fafc);\n }\n .card input {\n width: auto;\n margin-top: 0.15rem;\n accent-color: var(--ui-primary, #4f46e5);\n cursor: pointer;\n }\n .card-label {\n font-weight: 600;\n color: var(--ui-text, #0f172a);\n }\n .card-description {\n display: block;\n color: var(--ui-text-muted, #64748b);\n font-weight: 400;\n }\n `,\n ];\n\n /** Options to render, one card each. */\n @property({ attribute: false }) options: RadioCardOption[] = [];\n /** Currently selected value. */\n @property() value = \"\";\n\n readonly #name = `radio-cards-${++instanceCount}`;\n\n private _onChange(value: string) {\n this.value = value;\n this.dispatchEvent(new CustomEvent(\"change\", { detail: { value }, bubbles: true, composed: true }));\n }\n\n override render() {\n return html`\n <div class=\"options\">\n ${repeat(\n this.options,\n (opt) => opt.value,\n (opt) => html`\n <label class=\"card\">\n <input\n type=\"radio\"\n name=${this.#name}\n ?checked=${this.value === opt.value}\n @change=${() => this._onChange(opt.value)}\n />\n <span>\n <span class=\"card-label\">${opt.label}</span>\n ${opt.description ? html`<span class=\"card-description\">${opt.description}</span>` : nothing}\n </span>\n </label>\n `,\n )}\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"radio-cards\": RadioCards;\n }\n}\n"]}
@@ -0,0 +1,31 @@
1
+ import { LitElement } from "lit";
2
+ export interface RadioPillOption {
3
+ value: string;
4
+ label: string;
5
+ }
6
+ /**
7
+ * Single-select group of compact pill-shaped options — for many short,
8
+ * same-shaped choices (a basemap style, a unit toggle). For a handful of
9
+ * choices where a description matters, use `radio-cards` instead. Wraps
10
+ * native radio inputs for keyboard/a11y and fires `change` rather than
11
+ * relying on form submission.
12
+ *
13
+ * @element radio-pills
14
+ * @fires change - A pill was selected; detail: { value }.
15
+ */
16
+ export declare class RadioPills extends LitElement {
17
+ #private;
18
+ static styles: import("lit").CSSResult[];
19
+ /** Options to render, one pill each. */
20
+ options: RadioPillOption[];
21
+ /** Currently selected value. */
22
+ value: string;
23
+ private _onChange;
24
+ render(): import("lit-html").TemplateResult<1>;
25
+ }
26
+ declare global {
27
+ interface HTMLElementTagNameMap {
28
+ "radio-pills": RadioPills;
29
+ }
30
+ }
31
+ //# sourceMappingURL=radio-pills.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"radio-pills.d.ts","sourceRoot":"","sources":["../src/radio-pills.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAK5C,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAID;;;;;;;;;GASG;AACH,qBACa,UAAW,SAAQ,UAAU;;IACxC,OAAgB,MAAM,4BAmCpB;IAEF,wCAAwC;IACR,OAAO,EAAE,eAAe,EAAE,CAAM;IAChE,gCAAgC;IACpB,KAAK,SAAM;IAIvB,OAAO,CAAC,SAAS;IAKR,MAAM,yCAoBd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,aAAa,EAAE,UAAU,CAAC;KAC3B;CACF"}
@@ -0,0 +1,100 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { LitElement, css, html } from "lit";
8
+ import { customElement, property } from "lit/decorators.js";
9
+ import { repeat } from "lit/directives/repeat.js";
10
+ import { tokens } from "./tokens.js";
11
+ let instanceCount = 0;
12
+ /**
13
+ * Single-select group of compact pill-shaped options — for many short,
14
+ * same-shaped choices (a basemap style, a unit toggle). For a handful of
15
+ * choices where a description matters, use `radio-cards` instead. Wraps
16
+ * native radio inputs for keyboard/a11y and fires `change` rather than
17
+ * relying on form submission.
18
+ *
19
+ * @element radio-pills
20
+ * @fires change - A pill was selected; detail: { value }.
21
+ */
22
+ let RadioPills = class RadioPills extends LitElement {
23
+ constructor() {
24
+ super(...arguments);
25
+ /** Options to render, one pill each. */
26
+ this.options = [];
27
+ /** Currently selected value. */
28
+ this.value = "";
29
+ this.#name = `radio-pills-${++instanceCount}`;
30
+ }
31
+ static { this.styles = [
32
+ tokens,
33
+ css `
34
+ :host {
35
+ display: block;
36
+ }
37
+ .options {
38
+ display: flex;
39
+ flex-wrap: wrap;
40
+ gap: 0.4rem;
41
+ }
42
+ .pill {
43
+ display: flex;
44
+ align-items: center;
45
+ gap: 0.3rem;
46
+ padding: 0.3rem 0.6rem;
47
+ border: 1px solid var(--ui-border, #e2e8f0);
48
+ border-radius: 999px;
49
+ cursor: pointer;
50
+ font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);
51
+ font-size: var(--ui-font-size-sm, 0.75rem);
52
+ color: var(--ui-text, #0f172a);
53
+ }
54
+ .pill:has(input:checked) {
55
+ border-color: var(--ui-primary, #4f46e5);
56
+ background: var(--ui-surface-muted, #f8fafc);
57
+ }
58
+ .pill input {
59
+ width: 0.85rem;
60
+ height: 0.85rem;
61
+ accent-color: var(--ui-primary, #4f46e5);
62
+ cursor: pointer;
63
+ margin: 0;
64
+ }
65
+ `,
66
+ ]; }
67
+ #name;
68
+ _onChange(value) {
69
+ this.value = value;
70
+ this.dispatchEvent(new CustomEvent("change", { detail: { value }, bubbles: true, composed: true }));
71
+ }
72
+ render() {
73
+ return html `
74
+ <div class="options">
75
+ ${repeat(this.options, (opt) => opt.value, (opt) => html `
76
+ <label class="pill">
77
+ <input
78
+ type="radio"
79
+ name=${this.#name}
80
+ ?checked=${this.value === opt.value}
81
+ @change=${() => this._onChange(opt.value)}
82
+ />
83
+ <span>${opt.label}</span>
84
+ </label>
85
+ `)}
86
+ </div>
87
+ `;
88
+ }
89
+ };
90
+ __decorate([
91
+ property({ attribute: false })
92
+ ], RadioPills.prototype, "options", void 0);
93
+ __decorate([
94
+ property()
95
+ ], RadioPills.prototype, "value", void 0);
96
+ RadioPills = __decorate([
97
+ customElement("radio-pills")
98
+ ], RadioPills);
99
+ export { RadioPills };
100
+ //# sourceMappingURL=radio-pills.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"radio-pills.js","sourceRoot":"","sources":["../src/radio-pills.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAOrC,IAAI,aAAa,GAAG,CAAC,CAAC;AAEtB;;;;;;;;;GASG;AAEI,IAAM,UAAU,GAAhB,MAAM,UAAW,SAAQ,UAAU;IAAnC;;QAsCL,wCAAwC;QACR,YAAO,GAAsB,EAAE,CAAC;QAChE,gCAAgC;QACpB,UAAK,GAAG,EAAE,CAAC;QAEd,UAAK,GAAG,eAAe,EAAE,aAAa,EAAE,CAAC;IA4BpD,CAAC;aAtEiB,WAAM,GAAG;QACvB,MAAM;QACN,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAgCF;KACF,AAnCqB,CAmCpB;IAOO,KAAK,CAAoC;IAE1C,SAAS,CAAC,KAAa;QAC7B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACtG,CAAC;IAEQ,MAAM;QACb,OAAO,IAAI,CAAA;;UAEL,MAAM,CACN,IAAI,CAAC,OAAO,EACZ,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,EAClB,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAA;;;;uBAIA,IAAI,CAAC,KAAK;2BACN,IAAI,CAAC,KAAK,KAAK,GAAG,CAAC,KAAK;0BACzB,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;;sBAEnC,GAAG,CAAC,KAAK;;WAEpB,CACF;;KAEJ,CAAC;IACJ,CAAC;CACF,CAAA;AAhCiC;IAA/B,QAAQ,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;2CAAiC;AAEpD;IAAX,QAAQ,EAAE;yCAAY;AAzCZ,UAAU;IADtB,aAAa,CAAC,aAAa,CAAC;GAChB,UAAU,CAuEtB","sourcesContent":["import { LitElement, css, html } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { repeat } from \"lit/directives/repeat.js\";\nimport { tokens } from \"./tokens.js\";\n\nexport interface RadioPillOption {\n value: string;\n label: string;\n}\n\nlet instanceCount = 0;\n\n/**\n * Single-select group of compact pill-shaped options — for many short,\n * same-shaped choices (a basemap style, a unit toggle). For a handful of\n * choices where a description matters, use `radio-cards` instead. Wraps\n * native radio inputs for keyboard/a11y and fires `change` rather than\n * relying on form submission.\n *\n * @element radio-pills\n * @fires change - A pill was selected; detail: { value }.\n */\n@customElement(\"radio-pills\")\nexport class RadioPills extends LitElement {\n static override styles = [\n tokens,\n css`\n :host {\n display: block;\n }\n .options {\n display: flex;\n flex-wrap: wrap;\n gap: 0.4rem;\n }\n .pill {\n display: flex;\n align-items: center;\n gap: 0.3rem;\n padding: 0.3rem 0.6rem;\n border: 1px solid var(--ui-border, #e2e8f0);\n border-radius: 999px;\n cursor: pointer;\n font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);\n font-size: var(--ui-font-size-sm, 0.75rem);\n color: var(--ui-text, #0f172a);\n }\n .pill:has(input:checked) {\n border-color: var(--ui-primary, #4f46e5);\n background: var(--ui-surface-muted, #f8fafc);\n }\n .pill input {\n width: 0.85rem;\n height: 0.85rem;\n accent-color: var(--ui-primary, #4f46e5);\n cursor: pointer;\n margin: 0;\n }\n `,\n ];\n\n /** Options to render, one pill each. */\n @property({ attribute: false }) options: RadioPillOption[] = [];\n /** Currently selected value. */\n @property() value = \"\";\n\n readonly #name = `radio-pills-${++instanceCount}`;\n\n private _onChange(value: string) {\n this.value = value;\n this.dispatchEvent(new CustomEvent(\"change\", { detail: { value }, bubbles: true, composed: true }));\n }\n\n override render() {\n return html`\n <div class=\"options\">\n ${repeat(\n this.options,\n (opt) => opt.value,\n (opt) => html`\n <label class=\"pill\">\n <input\n type=\"radio\"\n name=${this.#name}\n ?checked=${this.value === opt.value}\n @change=${() => this._onChange(opt.value)}\n />\n <span>${opt.label}</span>\n </label>\n `,\n )}\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"radio-pills\": RadioPills;\n }\n}\n"]}
@@ -0,0 +1,44 @@
1
+ import { LitElement } from "lit";
2
+ export type ButtonVariant = "primary" | "secondary" | "danger";
3
+ /**
4
+ * Button (or link styled as one) with an optional leading icon, in three
5
+ * visual weights. Set `href` to render an `<a>` instead of a `<button>` —
6
+ * same styling either way — for cross-page navigation that should look like
7
+ * an action button; a disabled/busy link stays a real `<a>` with
8
+ * `aria-disabled` + `pointer-events: none` rather than losing its href.
9
+ * Put the icon in the `icon` slot and the label in the default slot.
10
+ *
11
+ * Form-associated (`type="submit"`/`"reset"`): the actual `<button>` lives in
12
+ * this element's shadow root, which native HTML form association does not
13
+ * cross into from an ancestor light-DOM `<form>`. `type="submit"`/`"reset"`
14
+ * is instead wired through `ElementInternals.form` — the same mechanism
15
+ * `address-autocomplete` uses to associate with an ancestor form.
16
+ *
17
+ * @element ui-button
18
+ * @slot icon - Optional leading icon (e.g. an inline SVG).
19
+ * @slot - Button label.
20
+ */
21
+ export declare class UiButton extends LitElement {
22
+ #private;
23
+ static formAssociated: boolean;
24
+ static styles: import("lit").CSSResult[];
25
+ /** Visual weight. */
26
+ variant: ButtonVariant;
27
+ /** Renders an `<a href="...">` instead of a `<button>` when set. */
28
+ href: string | null;
29
+ /** Native button `type`. Ignored when `href` is set. */
30
+ type: "button" | "submit" | "reset";
31
+ /** Disables the control and dims it. */
32
+ disabled: boolean;
33
+ /** Shows a spinner in place of the icon slot and disables the control. */
34
+ busy: boolean;
35
+ /** Drives submit/reset on the ancestor form via ElementInternals, since a shadow-DOM button can't do it natively. */
36
+ private _onClick;
37
+ render(): import("lit-html").TemplateResult<1>;
38
+ }
39
+ declare global {
40
+ interface HTMLElementTagNameMap {
41
+ "ui-button": UiButton;
42
+ }
43
+ }
44
+ //# sourceMappingURL=ui-button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-button.d.ts","sourceRoot":"","sources":["../src/ui-button.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAK5C,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE/D;;;;;;;;;;;;;;;;;GAiBG;AACH,qBACa,QAAS,SAAQ,UAAU;;IACtC,MAAM,CAAC,cAAc,UAAQ;IAG7B,OAAgB,MAAM,4BA6DpB;IAEF,qBAAqB;IACT,OAAO,EAAE,aAAa,CAAa;IAC/C,oEAAoE;IACxD,IAAI,EAAE,MAAM,GAAG,IAAI,CAAQ;IACvC,wDAAwD;IAC5C,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAY;IAC3D,wCAAwC;IACX,QAAQ,UAAS;IAC9C,0EAA0E;IAC7C,IAAI,UAAS;IAE1C,qHAAqH;IACrH,OAAO,CAAC,QAAQ;IAKP,MAAM,yCAmBd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,WAAW,EAAE,QAAQ,CAAC;KACvB;CACF"}
@@ -0,0 +1,155 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { LitElement, css, html } from "lit";
8
+ import { customElement, property } from "lit/decorators.js";
9
+ import { iconArrowPath } from "./icons.js";
10
+ import { tokens } from "./tokens.js";
11
+ /**
12
+ * Button (or link styled as one) with an optional leading icon, in three
13
+ * visual weights. Set `href` to render an `<a>` instead of a `<button>` —
14
+ * same styling either way — for cross-page navigation that should look like
15
+ * an action button; a disabled/busy link stays a real `<a>` with
16
+ * `aria-disabled` + `pointer-events: none` rather than losing its href.
17
+ * Put the icon in the `icon` slot and the label in the default slot.
18
+ *
19
+ * Form-associated (`type="submit"`/`"reset"`): the actual `<button>` lives in
20
+ * this element's shadow root, which native HTML form association does not
21
+ * cross into from an ancestor light-DOM `<form>`. `type="submit"`/`"reset"`
22
+ * is instead wired through `ElementInternals.form` — the same mechanism
23
+ * `address-autocomplete` uses to associate with an ancestor form.
24
+ *
25
+ * @element ui-button
26
+ * @slot icon - Optional leading icon (e.g. an inline SVG).
27
+ * @slot - Button label.
28
+ */
29
+ let UiButton = class UiButton extends LitElement {
30
+ constructor() {
31
+ super(...arguments);
32
+ this.#internals = this.attachInternals();
33
+ /** Visual weight. */
34
+ this.variant = "primary";
35
+ /** Renders an `<a href="...">` instead of a `<button>` when set. */
36
+ this.href = null;
37
+ /** Native button `type`. Ignored when `href` is set. */
38
+ this.type = "button";
39
+ /** Disables the control and dims it. */
40
+ this.disabled = false;
41
+ /** Shows a spinner in place of the icon slot and disables the control. */
42
+ this.busy = false;
43
+ }
44
+ static { this.formAssociated = true; }
45
+ #internals;
46
+ static { this.styles = [
47
+ tokens,
48
+ css `
49
+ :host {
50
+ display: inline-flex;
51
+ }
52
+ .btn {
53
+ display: inline-flex;
54
+ align-items: center;
55
+ gap: 0.35rem;
56
+ border-radius: var(--ui-radius-sm, 0.25rem);
57
+ padding: 0.5rem 0.9rem;
58
+ font-size: var(--ui-font-size-sm, 0.75rem);
59
+ font-weight: 500;
60
+ font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);
61
+ cursor: pointer;
62
+ border: 1px solid transparent;
63
+ text-decoration: none;
64
+ box-sizing: border-box;
65
+ }
66
+ .btn.primary {
67
+ background: var(--ui-primary, #4f46e5);
68
+ color: #fff;
69
+ }
70
+ .btn.primary:hover:not(:disabled) {
71
+ background: var(--ui-primary-hover, #4338ca);
72
+ }
73
+ .btn.secondary {
74
+ background: none;
75
+ border-color: var(--ui-border, #e2e8f0);
76
+ color: var(--ui-text, #0f172a);
77
+ }
78
+ .btn.secondary:hover:not(:disabled) {
79
+ border-color: var(--ui-text-muted, #64748b);
80
+ }
81
+ .btn.danger {
82
+ background: var(--ui-danger, #dc2626);
83
+ color: #fff;
84
+ }
85
+ .btn.danger:hover:not(:disabled) {
86
+ background: var(--ui-danger-hover, #b91c1c);
87
+ }
88
+ .btn:disabled,
89
+ .btn[aria-disabled="true"] {
90
+ opacity: 0.6;
91
+ cursor: default;
92
+ pointer-events: none;
93
+ }
94
+ .spin {
95
+ display: inline-flex;
96
+ animation: spin 0.8s linear infinite;
97
+ }
98
+ .spin[hidden] {
99
+ display: none;
100
+ }
101
+ @keyframes spin {
102
+ to {
103
+ transform: rotate(360deg);
104
+ }
105
+ }
106
+ `,
107
+ ]; }
108
+ /** Drives submit/reset on the ancestor form via ElementInternals, since a shadow-DOM button can't do it natively. */
109
+ _onClick() {
110
+ if (this.type === "submit")
111
+ this.#internals.form?.requestSubmit();
112
+ else if (this.type === "reset")
113
+ this.#internals.form?.reset();
114
+ }
115
+ render() {
116
+ const classes = `btn ${this.variant}`;
117
+ const isDisabled = this.disabled || this.busy;
118
+ if (this.href) {
119
+ return html `
120
+ <a class=${classes} href=${this.href} aria-disabled=${isDisabled ? "true" : "false"}>
121
+ <span class="spin" ?hidden=${!this.busy}>${iconArrowPath(14)}</span>
122
+ <slot name="icon" ?hidden=${this.busy}></slot>
123
+ <slot></slot>
124
+ </a>
125
+ `;
126
+ }
127
+ return html `
128
+ <button class=${classes} type="button" ?disabled=${isDisabled} @click=${this._onClick}>
129
+ <span class="spin" ?hidden=${!this.busy}>${iconArrowPath(14)}</span>
130
+ <slot name="icon" ?hidden=${this.busy}></slot>
131
+ <slot></slot>
132
+ </button>
133
+ `;
134
+ }
135
+ };
136
+ __decorate([
137
+ property()
138
+ ], UiButton.prototype, "variant", void 0);
139
+ __decorate([
140
+ property()
141
+ ], UiButton.prototype, "href", void 0);
142
+ __decorate([
143
+ property()
144
+ ], UiButton.prototype, "type", void 0);
145
+ __decorate([
146
+ property({ type: Boolean })
147
+ ], UiButton.prototype, "disabled", void 0);
148
+ __decorate([
149
+ property({ type: Boolean })
150
+ ], UiButton.prototype, "busy", void 0);
151
+ UiButton = __decorate([
152
+ customElement("ui-button")
153
+ ], UiButton);
154
+ export { UiButton };
155
+ //# sourceMappingURL=ui-button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-button.js","sourceRoot":"","sources":["../src/ui-button.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAIrC;;;;;;;;;;;;;;;;;GAiBG;AAEI,IAAM,QAAQ,GAAd,MAAM,QAAS,SAAQ,UAAU;IAAjC;;QAGL,eAAU,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;QAgEpC,qBAAqB;QACT,YAAO,GAAkB,SAAS,CAAC;QAC/C,oEAAoE;QACxD,SAAI,GAAkB,IAAI,CAAC;QACvC,wDAAwD;QAC5C,SAAI,GAAkC,QAAQ,CAAC;QAC3D,wCAAwC;QACX,aAAQ,GAAG,KAAK,CAAC;QAC9C,0EAA0E;QAC7C,SAAI,GAAG,KAAK,CAAC;IA4B5C,CAAC;aAvGQ,mBAAc,GAAG,IAAI,AAAP,CAAQ;IAE7B,UAAU,CAA0B;aACpB,WAAM,GAAG;QACvB,MAAM;QACN,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA0DF;KACF,AA7DqB,CA6DpB;IAaF,qHAAqH;IAC7G,QAAQ;QACd,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ;YAAE,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,aAAa,EAAE,CAAC;aAC7D,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO;YAAE,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;IAChE,CAAC;IAEQ,MAAM;QACb,MAAM,OAAO,GAAG,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC;QACtC,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,IAAI,CAAC;QAC9C,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,OAAO,IAAI,CAAA;mBACE,OAAO,SAAS,IAAI,CAAC,IAAI,kBAAkB,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO;uCACpD,CAAC,IAAI,CAAC,IAAI,IAAI,aAAa,CAAC,EAAE,CAAC;sCAChC,IAAI,CAAC,IAAI;;;OAGxC,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAA;sBACO,OAAO,4BAA4B,UAAU,WAAW,IAAI,CAAC,QAAQ;qCACtD,CAAC,IAAI,CAAC,IAAI,IAAI,aAAa,CAAC,EAAE,CAAC;oCAChC,IAAI,CAAC,IAAI;;;KAGxC,CAAC;IACJ,CAAC;CACF,CAAA;AApCa;IAAX,QAAQ,EAAE;yCAAoC;AAEnC;IAAX,QAAQ,EAAE;sCAA4B;AAE3B;IAAX,QAAQ,EAAE;sCAAgD;AAE9B;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;0CAAkB;AAEjB;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;sCAAc;AA5E/B,QAAQ;IADpB,aAAa,CAAC,WAAW,CAAC;GACd,QAAQ,CAwGpB","sourcesContent":["import { LitElement, css, html } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { iconArrowPath } from \"./icons.js\";\nimport { tokens } from \"./tokens.js\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"danger\";\n\n/**\n * Button (or link styled as one) with an optional leading icon, in three\n * visual weights. Set `href` to render an `<a>` instead of a `<button>` —\n * same styling either way — for cross-page navigation that should look like\n * an action button; a disabled/busy link stays a real `<a>` with\n * `aria-disabled` + `pointer-events: none` rather than losing its href.\n * Put the icon in the `icon` slot and the label in the default slot.\n *\n * Form-associated (`type=\"submit\"`/`\"reset\"`): the actual `<button>` lives in\n * this element's shadow root, which native HTML form association does not\n * cross into from an ancestor light-DOM `<form>`. `type=\"submit\"`/`\"reset\"`\n * is instead wired through `ElementInternals.form` — the same mechanism\n * `address-autocomplete` uses to associate with an ancestor form.\n *\n * @element ui-button\n * @slot icon - Optional leading icon (e.g. an inline SVG).\n * @slot - Button label.\n */\n@customElement(\"ui-button\")\nexport class UiButton extends LitElement {\n static formAssociated = true;\n\n #internals = this.attachInternals();\n static override styles = [\n tokens,\n css`\n :host {\n display: inline-flex;\n }\n .btn {\n display: inline-flex;\n align-items: center;\n gap: 0.35rem;\n border-radius: var(--ui-radius-sm, 0.25rem);\n padding: 0.5rem 0.9rem;\n font-size: var(--ui-font-size-sm, 0.75rem);\n font-weight: 500;\n font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);\n cursor: pointer;\n border: 1px solid transparent;\n text-decoration: none;\n box-sizing: border-box;\n }\n .btn.primary {\n background: var(--ui-primary, #4f46e5);\n color: #fff;\n }\n .btn.primary:hover:not(:disabled) {\n background: var(--ui-primary-hover, #4338ca);\n }\n .btn.secondary {\n background: none;\n border-color: var(--ui-border, #e2e8f0);\n color: var(--ui-text, #0f172a);\n }\n .btn.secondary:hover:not(:disabled) {\n border-color: var(--ui-text-muted, #64748b);\n }\n .btn.danger {\n background: var(--ui-danger, #dc2626);\n color: #fff;\n }\n .btn.danger:hover:not(:disabled) {\n background: var(--ui-danger-hover, #b91c1c);\n }\n .btn:disabled,\n .btn[aria-disabled=\"true\"] {\n opacity: 0.6;\n cursor: default;\n pointer-events: none;\n }\n .spin {\n display: inline-flex;\n animation: spin 0.8s linear infinite;\n }\n .spin[hidden] {\n display: none;\n }\n @keyframes spin {\n to {\n transform: rotate(360deg);\n }\n }\n `,\n ];\n\n /** Visual weight. */\n @property() variant: ButtonVariant = \"primary\";\n /** Renders an `<a href=\"...\">` instead of a `<button>` when set. */\n @property() href: string | null = null;\n /** Native button `type`. Ignored when `href` is set. */\n @property() type: \"button\" | \"submit\" | \"reset\" = \"button\";\n /** Disables the control and dims it. */\n @property({ type: Boolean }) disabled = false;\n /** Shows a spinner in place of the icon slot and disables the control. */\n @property({ type: Boolean }) busy = false;\n\n /** Drives submit/reset on the ancestor form via ElementInternals, since a shadow-DOM button can't do it natively. */\n private _onClick() {\n if (this.type === \"submit\") this.#internals.form?.requestSubmit();\n else if (this.type === \"reset\") this.#internals.form?.reset();\n }\n\n override render() {\n const classes = `btn ${this.variant}`;\n const isDisabled = this.disabled || this.busy;\n if (this.href) {\n return html`\n <a class=${classes} href=${this.href} aria-disabled=${isDisabled ? \"true\" : \"false\"}>\n <span class=\"spin\" ?hidden=${!this.busy}>${iconArrowPath(14)}</span>\n <slot name=\"icon\" ?hidden=${this.busy}></slot>\n <slot></slot>\n </a>\n `;\n }\n return html`\n <button class=${classes} type=\"button\" ?disabled=${isDisabled} @click=${this._onClick}>\n <span class=\"spin\" ?hidden=${!this.busy}>${iconArrowPath(14)}</span>\n <slot name=\"icon\" ?hidden=${this.busy}></slot>\n <slot></slot>\n </button>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ui-button\": UiButton;\n }\n}\n"]}
@@ -0,0 +1,28 @@
1
+ import { LitElement } from "lit";
2
+ /**
3
+ * Circular avatar. Shows `src` when it loads successfully; falls back to the
4
+ * first letter of `name` (uppercased) if `src` is unset or fails to load
5
+ * (e.g. an expired OAuth profile-photo URL); falls back further to a generic
6
+ * person icon if `name` is also unset. A broken image never leaves a blank
7
+ * circle.
8
+ *
9
+ * @element user-avatar
10
+ */
11
+ export declare class UserAvatar extends LitElement {
12
+ static styles: import("lit").CSSResult[];
13
+ /** Image URL to show. Falls back to initials/icon if unset or it fails to load. */
14
+ src: string | null;
15
+ /** Source string for the fallback initial (e.g. a display name or email) — first character, uppercased. */
16
+ name: string | null;
17
+ /** Diameter in CSS pixels. */
18
+ size: number;
19
+ private _imgError;
20
+ protected willUpdate(changed: Map<string, unknown>): void;
21
+ render(): import("lit-html").TemplateResult<1>;
22
+ }
23
+ declare global {
24
+ interface HTMLElementTagNameMap {
25
+ "user-avatar": UserAvatar;
26
+ }
27
+ }
28
+ //# sourceMappingURL=user-avatar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-avatar.d.ts","sourceRoot":"","sources":["../src/user-avatar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAK5C;;;;;;;;GAQG;AACH,qBACa,UAAW,SAAQ,UAAU;IACxC,OAAgB,MAAM,4BAyBpB;IAEF,mFAAmF;IACvE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAQ;IACtC,2GAA2G;IAC/F,IAAI,EAAE,MAAM,GAAG,IAAI,CAAQ;IACvC,8BAA8B;IACF,IAAI,SAAM;IAE7B,OAAO,CAAC,SAAS,CAAS;IAEnC,UAAmB,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,QAE1D;IAEQ,MAAM,yCAgBd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,aAAa,EAAE,UAAU,CAAC;KAC3B;CACF"}
@@ -0,0 +1,95 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { LitElement, css, html } from "lit";
8
+ import { customElement, property, state } from "lit/decorators.js";
9
+ import { iconUserCircle } from "./icons.js";
10
+ import { tokens } from "./tokens.js";
11
+ /**
12
+ * Circular avatar. Shows `src` when it loads successfully; falls back to the
13
+ * first letter of `name` (uppercased) if `src` is unset or fails to load
14
+ * (e.g. an expired OAuth profile-photo URL); falls back further to a generic
15
+ * person icon if `name` is also unset. A broken image never leaves a blank
16
+ * circle.
17
+ *
18
+ * @element user-avatar
19
+ */
20
+ let UserAvatar = class UserAvatar extends LitElement {
21
+ constructor() {
22
+ super(...arguments);
23
+ /** Image URL to show. Falls back to initials/icon if unset or it fails to load. */
24
+ this.src = null;
25
+ /** Source string for the fallback initial (e.g. a display name or email) — first character, uppercased. */
26
+ this.name = null;
27
+ /** Diameter in CSS pixels. */
28
+ this.size = 32;
29
+ this._imgError = false;
30
+ }
31
+ static { this.styles = [
32
+ tokens,
33
+ css `
34
+ :host {
35
+ display: inline-flex;
36
+ }
37
+ .avatar {
38
+ border-radius: 50%;
39
+ background: var(--ui-primary, #4f46e5);
40
+ color: #fff;
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);
45
+ font-weight: 600;
46
+ overflow: hidden;
47
+ flex: 0 0 auto;
48
+ }
49
+ img {
50
+ width: 100%;
51
+ height: 100%;
52
+ border-radius: 50%;
53
+ object-fit: cover;
54
+ }
55
+ `,
56
+ ]; }
57
+ willUpdate(changed) {
58
+ if (changed.has("src"))
59
+ this._imgError = false;
60
+ }
61
+ render() {
62
+ const dim = `${this.size}px`;
63
+ const showImage = !!this.src && !this._imgError;
64
+ const initial = this.name?.trim()[0]?.toUpperCase();
65
+ return html `
66
+ <span class="avatar" style="width:${dim};height:${dim};font-size:${this.size * 0.42}px">
67
+ ${showImage
68
+ ? html `<img
69
+ src=${this.src}
70
+ alt=""
71
+ referrerpolicy="no-referrer"
72
+ @error=${() => (this._imgError = true)}
73
+ />`
74
+ : initial || iconUserCircle(Math.round(this.size * 0.65))}
75
+ </span>
76
+ `;
77
+ }
78
+ };
79
+ __decorate([
80
+ property()
81
+ ], UserAvatar.prototype, "src", void 0);
82
+ __decorate([
83
+ property()
84
+ ], UserAvatar.prototype, "name", void 0);
85
+ __decorate([
86
+ property({ type: Number })
87
+ ], UserAvatar.prototype, "size", void 0);
88
+ __decorate([
89
+ state()
90
+ ], UserAvatar.prototype, "_imgError", void 0);
91
+ UserAvatar = __decorate([
92
+ customElement("user-avatar")
93
+ ], UserAvatar);
94
+ export { UserAvatar };
95
+ //# sourceMappingURL=user-avatar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-avatar.js","sourceRoot":"","sources":["../src/user-avatar.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC;;;;;;;;GAQG;AAEI,IAAM,UAAU,GAAhB,MAAM,UAAW,SAAQ,UAAU;IAAnC;;QA4BL,mFAAmF;QACvE,QAAG,GAAkB,IAAI,CAAC;QACtC,2GAA2G;QAC/F,SAAI,GAAkB,IAAI,CAAC;QACvC,8BAA8B;QACF,SAAI,GAAG,EAAE,CAAC;QAErB,cAAS,GAAG,KAAK,CAAC;IAuBrC,CAAC;aAzDiB,WAAM,GAAG;QACvB,MAAM;QACN,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;KAsBF;KACF,AAzBqB,CAyBpB;IAWiB,UAAU,CAAC,OAA6B;QACzD,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;IACjD,CAAC;IAEQ,MAAM;QACb,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,IAAI,IAAI,CAAC;QAC7B,MAAM,SAAS,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC;QAChD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;QACpD,OAAO,IAAI,CAAA;0CAC2B,GAAG,WAAW,GAAG,cAAc,IAAI,CAAC,IAAI,GAAG,IAAI;UAC/E,SAAS;YACT,CAAC,CAAC,IAAI,CAAA;oBACI,IAAI,CAAC,GAAI;;;uBAGN,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;eACrC;YACL,CAAC,CAAC,OAAO,IAAI,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;;KAE9D,CAAC;IACJ,CAAC;CACF,CAAA;AA7Ba;IAAX,QAAQ,EAAE;uCAA2B;AAE1B;IAAX,QAAQ,EAAE;wCAA4B;AAEX;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;wCAAW;AAErB;IAAhB,KAAK,EAAE;6CAA2B;AAnCxB,UAAU;IADtB,aAAa,CAAC,aAAa,CAAC;GAChB,UAAU,CA0DtB","sourcesContent":["import { LitElement, css, html } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { iconUserCircle } from \"./icons.js\";\nimport { tokens } from \"./tokens.js\";\n\n/**\n * Circular avatar. Shows `src` when it loads successfully; falls back to the\n * first letter of `name` (uppercased) if `src` is unset or fails to load\n * (e.g. an expired OAuth profile-photo URL); falls back further to a generic\n * person icon if `name` is also unset. A broken image never leaves a blank\n * circle.\n *\n * @element user-avatar\n */\n@customElement(\"user-avatar\")\nexport class UserAvatar extends LitElement {\n static override styles = [\n tokens,\n css`\n :host {\n display: inline-flex;\n }\n .avatar {\n border-radius: 50%;\n background: var(--ui-primary, #4f46e5);\n color: #fff;\n display: flex;\n align-items: center;\n justify-content: center;\n font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);\n font-weight: 600;\n overflow: hidden;\n flex: 0 0 auto;\n }\n img {\n width: 100%;\n height: 100%;\n border-radius: 50%;\n object-fit: cover;\n }\n `,\n ];\n\n /** Image URL to show. Falls back to initials/icon if unset or it fails to load. */\n @property() src: string | null = null;\n /** Source string for the fallback initial (e.g. a display name or email) — first character, uppercased. */\n @property() name: string | null = null;\n /** Diameter in CSS pixels. */\n @property({ type: Number }) size = 32;\n\n @state() private _imgError = false;\n\n protected override willUpdate(changed: Map<string, unknown>) {\n if (changed.has(\"src\")) this._imgError = false;\n }\n\n override render() {\n const dim = `${this.size}px`;\n const showImage = !!this.src && !this._imgError;\n const initial = this.name?.trim()[0]?.toUpperCase();\n return html`\n <span class=\"avatar\" style=\"width:${dim};height:${dim};font-size:${this.size * 0.42}px\">\n ${showImage\n ? html`<img\n src=${this.src!}\n alt=\"\"\n referrerpolicy=\"no-referrer\"\n @error=${() => (this._imgError = true)}\n />`\n : initial || iconUserCircle(Math.round(this.size * 0.65))}\n </span>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"user-avatar\": UserAvatar;\n }\n}\n"]}