@f-ewald/components 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,34 @@
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
+ * @element ui-button
12
+ * @slot icon - Optional leading icon (e.g. an inline SVG).
13
+ * @slot - Button label.
14
+ */
15
+ export declare class UiButton extends LitElement {
16
+ static styles: import("lit").CSSResult[];
17
+ /** Visual weight. */
18
+ variant: ButtonVariant;
19
+ /** Renders an `<a href="...">` instead of a `<button>` when set. */
20
+ href: string | null;
21
+ /** Native button `type`. Ignored when `href` is set. */
22
+ type: "button" | "submit" | "reset";
23
+ /** Disables the control and dims it. */
24
+ disabled: boolean;
25
+ /** Shows a spinner in place of the icon slot and disables the control. */
26
+ busy: boolean;
27
+ render(): import("lit-html").TemplateResult<1>;
28
+ }
29
+ declare global {
30
+ interface HTMLElementTagNameMap {
31
+ "ui-button": UiButton;
32
+ }
33
+ }
34
+ //# 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;;;;;;;;;;;GAWG;AACH,qBACa,QAAS,SAAQ,UAAU;IACtC,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;IAEjC,MAAM,yCAmBd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,WAAW,EAAE,QAAQ,CAAC;KACvB;CACF"}
@@ -0,0 +1,139 @@
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
+ * @element ui-button
20
+ * @slot icon - Optional leading icon (e.g. an inline SVG).
21
+ * @slot - Button label.
22
+ */
23
+ let UiButton = class UiButton extends LitElement {
24
+ constructor() {
25
+ super(...arguments);
26
+ /** Visual weight. */
27
+ this.variant = "primary";
28
+ /** Renders an `<a href="...">` instead of a `<button>` when set. */
29
+ this.href = null;
30
+ /** Native button `type`. Ignored when `href` is set. */
31
+ this.type = "button";
32
+ /** Disables the control and dims it. */
33
+ this.disabled = false;
34
+ /** Shows a spinner in place of the icon slot and disables the control. */
35
+ this.busy = false;
36
+ }
37
+ static { this.styles = [
38
+ tokens,
39
+ css `
40
+ :host {
41
+ display: inline-flex;
42
+ }
43
+ .btn {
44
+ display: inline-flex;
45
+ align-items: center;
46
+ gap: 0.35rem;
47
+ border-radius: var(--ui-radius-sm, 0.25rem);
48
+ padding: 0.5rem 0.9rem;
49
+ font-size: var(--ui-font-size-sm, 0.75rem);
50
+ font-weight: 500;
51
+ font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);
52
+ cursor: pointer;
53
+ border: 1px solid transparent;
54
+ text-decoration: none;
55
+ box-sizing: border-box;
56
+ }
57
+ .btn.primary {
58
+ background: var(--ui-primary, #4f46e5);
59
+ color: #fff;
60
+ }
61
+ .btn.primary:hover:not(:disabled) {
62
+ background: var(--ui-primary-hover, #4338ca);
63
+ }
64
+ .btn.secondary {
65
+ background: none;
66
+ border-color: var(--ui-border, #e2e8f0);
67
+ color: var(--ui-text, #0f172a);
68
+ }
69
+ .btn.secondary:hover:not(:disabled) {
70
+ border-color: var(--ui-text-muted, #64748b);
71
+ }
72
+ .btn.danger {
73
+ background: var(--ui-danger, #dc2626);
74
+ color: #fff;
75
+ }
76
+ .btn.danger:hover:not(:disabled) {
77
+ background: var(--ui-danger-hover, #b91c1c);
78
+ }
79
+ .btn:disabled,
80
+ .btn[aria-disabled="true"] {
81
+ opacity: 0.6;
82
+ cursor: default;
83
+ pointer-events: none;
84
+ }
85
+ .spin {
86
+ display: inline-flex;
87
+ animation: spin 0.8s linear infinite;
88
+ }
89
+ .spin[hidden] {
90
+ display: none;
91
+ }
92
+ @keyframes spin {
93
+ to {
94
+ transform: rotate(360deg);
95
+ }
96
+ }
97
+ `,
98
+ ]; }
99
+ render() {
100
+ const classes = `btn ${this.variant}`;
101
+ const isDisabled = this.disabled || this.busy;
102
+ if (this.href) {
103
+ return html `
104
+ <a class=${classes} href=${this.href} aria-disabled=${isDisabled ? "true" : "false"}>
105
+ <span class="spin" ?hidden=${!this.busy}>${iconArrowPath(14)}</span>
106
+ <slot name="icon" ?hidden=${this.busy}></slot>
107
+ <slot></slot>
108
+ </a>
109
+ `;
110
+ }
111
+ return html `
112
+ <button class=${classes} type=${this.type} ?disabled=${isDisabled}>
113
+ <span class="spin" ?hidden=${!this.busy}>${iconArrowPath(14)}</span>
114
+ <slot name="icon" ?hidden=${this.busy}></slot>
115
+ <slot></slot>
116
+ </button>
117
+ `;
118
+ }
119
+ };
120
+ __decorate([
121
+ property()
122
+ ], UiButton.prototype, "variant", void 0);
123
+ __decorate([
124
+ property()
125
+ ], UiButton.prototype, "href", void 0);
126
+ __decorate([
127
+ property()
128
+ ], UiButton.prototype, "type", void 0);
129
+ __decorate([
130
+ property({ type: Boolean })
131
+ ], UiButton.prototype, "disabled", void 0);
132
+ __decorate([
133
+ property({ type: Boolean })
134
+ ], UiButton.prototype, "busy", void 0);
135
+ UiButton = __decorate([
136
+ customElement("ui-button")
137
+ ], UiButton);
138
+ export { UiButton };
139
+ //# 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;;;;;;;;;;;GAWG;AAEI,IAAM,QAAQ,GAAd,MAAM,QAAS,SAAQ,UAAU;IAAjC;;QAgEL,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;IAsB5C,CAAC;aA9FiB,WAAM,GAAG;QACvB,MAAM;QACN,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA0DF;KACF,AA7DqB,CA6DpB;IAaO,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,SAAS,IAAI,CAAC,IAAI,cAAc,UAAU;qCAClC,CAAC,IAAI,CAAC,IAAI,IAAI,aAAa,CAAC,EAAE,CAAC;oCAChC,IAAI,CAAC,IAAI;;;KAGxC,CAAC;IACJ,CAAC;CACF,CAAA;AA9Ba;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;AAzE/B,QAAQ;IADpB,aAAa,CAAC,WAAW,CAAC;GACd,QAAQ,CA+FpB","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 * @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 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 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=${this.type} ?disabled=${isDisabled}>\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"]}
@@ -0,0 +1,58 @@
1
+ # `<radio-cards>`
2
+
3
+ Single-select group of full-width cards, each with a label and optional
4
+ description — for a handful of meaningfully different choices where the
5
+ description matters. For many short, same-shaped options (a color swatch,
6
+ a basemap style), use `radio-pills` instead. Wraps native radio inputs for
7
+ keyboard/a11y and fires `change` rather than relying on form submission.
8
+
9
+ ## Install
10
+
11
+ ```js
12
+ import "@f-ewald/components/radio-cards.js";
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```html
18
+ <radio-cards></radio-cards>
19
+ <script type="module">
20
+ const el = document.querySelector("radio-cards");
21
+ el.options = [
22
+ { value: "simple", label: "Simple", description: "Quick-ranking view" },
23
+ { value: "detailed", label: "Detailed", description: "Every section and layer" },
24
+ ];
25
+ el.value = "simple";
26
+ el.addEventListener("change", (e) => console.log(e.detail.value));
27
+ </script>
28
+ ```
29
+
30
+ ## Attributes / properties
31
+
32
+ | Property | Attribute | Type | Default | Description |
33
+ | --- | --- | --- | --- | --- |
34
+ | `options` | _(JS property only)_ | `RadioCardOption[]` | `[]` | Options to render, one card each. |
35
+ | `value` | `value` | `string` | `""` | Currently selected value. |
36
+
37
+ ## Events
38
+
39
+ | Event | Description |
40
+ | --- | --- |
41
+ | `change` | A card was selected; detail: { value }. |
42
+
43
+ ## Slots
44
+
45
+ _None._
46
+
47
+ ## CSS custom properties
48
+
49
+ | Custom property |
50
+ | --- |
51
+ | `--ui-border` |
52
+ | `--ui-font` |
53
+ | `--ui-font-size-sm` |
54
+ | `--ui-primary` |
55
+ | `--ui-radius-sm` |
56
+ | `--ui-surface-muted` |
57
+ | `--ui-text` |
58
+ | `--ui-text-muted` |