@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.
- package/README.md +4 -0
- package/custom-elements.json +497 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/radio-cards.d.ts +32 -0
- package/dist/radio-cards.d.ts.map +1 -0
- package/dist/radio-cards.js +111 -0
- package/dist/radio-cards.js.map +1 -0
- package/dist/radio-pills.d.ts +31 -0
- package/dist/radio-pills.d.ts.map +1 -0
- package/dist/radio-pills.js +100 -0
- package/dist/radio-pills.js.map +1 -0
- package/dist/ui-button.d.ts +34 -0
- package/dist/ui-button.d.ts.map +1 -0
- package/dist/ui-button.js +139 -0
- package/dist/ui-button.js.map +1 -0
- package/dist/user-avatar.d.ts +28 -0
- package/dist/user-avatar.d.ts.map +1 -0
- package/dist/user-avatar.js +95 -0
- package/dist/user-avatar.js.map +1 -0
- package/docs/radio-cards.md +58 -0
- package/docs/radio-pills.md +56 -0
- package/docs/ui-button.md +61 -0
- package/docs/user-avatar.md +42 -0
- package/llms.txt +100 -0
- package/package.json +1 -1
|
@@ -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` |
|