@f-ewald/components 0.5.0 → 0.7.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 +61 -0
- package/custom-elements.json +3808 -930
- package/dist/chat-message.d.ts +39 -0
- package/dist/chat-message.d.ts.map +1 -0
- package/dist/chat-message.js +155 -0
- package/dist/chat-message.js.map +1 -0
- package/dist/data-table.d.ts +39 -0
- package/dist/data-table.d.ts.map +1 -0
- package/dist/data-table.js +119 -0
- package/dist/data-table.js.map +1 -0
- package/dist/dropdown-button.d.ts +37 -0
- package/dist/dropdown-button.d.ts.map +1 -0
- package/dist/dropdown-button.js +219 -0
- package/dist/dropdown-button.js.map +1 -0
- package/dist/editable-text.d.ts +52 -0
- package/dist/editable-text.d.ts.map +1 -0
- package/dist/editable-text.js +224 -0
- package/dist/editable-text.js.map +1 -0
- package/dist/form-select.d.ts +46 -0
- package/dist/form-select.d.ts.map +1 -0
- package/dist/form-select.js +245 -0
- package/dist/form-select.js.map +1 -0
- package/dist/frame-box.d.ts +22 -0
- package/dist/frame-box.d.ts.map +1 -0
- package/dist/frame-box.js +68 -0
- package/dist/frame-box.js.map +1 -0
- package/dist/gallery-item-variant.d.ts +21 -0
- package/dist/gallery-item-variant.d.ts.map +1 -0
- package/dist/gallery-item-variant.js +44 -0
- package/dist/gallery-item-variant.js.map +1 -0
- package/dist/gallery-item.d.ts +28 -0
- package/dist/gallery-item.d.ts.map +1 -0
- package/dist/gallery-item.js +54 -0
- package/dist/gallery-item.js.map +1 -0
- package/dist/icon-button.d.ts +27 -0
- package/dist/icon-button.d.ts.map +1 -0
- package/dist/icon-button.js +83 -0
- package/dist/icon-button.js.map +1 -0
- package/dist/icons.d.ts +8 -0
- package/dist/icons.d.ts.map +1 -1
- package/dist/icons.js +8 -0
- package/dist/icons.js.map +1 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/dist/live-timer.d.ts +31 -0
- package/dist/live-timer.d.ts.map +1 -0
- package/dist/live-timer.js +78 -0
- package/dist/live-timer.js.map +1 -0
- package/dist/mcp-server.d.ts +3 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +82 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/photo-gallery.d.ts +117 -0
- package/dist/photo-gallery.d.ts.map +1 -0
- package/dist/photo-gallery.js +620 -0
- package/dist/photo-gallery.js.map +1 -0
- package/dist/popover-panel.d.ts +37 -0
- package/dist/popover-panel.d.ts.map +1 -0
- package/dist/popover-panel.js +184 -0
- package/dist/popover-panel.js.map +1 -0
- package/dist/status-pill.d.ts +24 -0
- package/dist/status-pill.d.ts.map +1 -0
- package/dist/status-pill.js +101 -0
- package/dist/status-pill.js.map +1 -0
- package/dist/tile-grid.d.ts +32 -0
- package/dist/tile-grid.d.ts.map +1 -0
- package/dist/tile-grid.js +102 -0
- package/dist/tile-grid.js.map +1 -0
- package/dist/tokens.css +39 -0
- package/dist/tokens.d.ts +28 -3
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +42 -43
- package/dist/tokens.js.map +1 -1
- package/dist/user-avatar.d.ts +5 -2
- package/dist/user-avatar.d.ts.map +1 -1
- package/dist/user-avatar.js +22 -6
- package/dist/user-avatar.js.map +1 -1
- package/dist/utils/duration.d.ts +12 -0
- package/dist/utils/duration.d.ts.map +1 -0
- package/dist/utils/duration.js +27 -0
- package/dist/utils/duration.js.map +1 -0
- package/docs/chat-message.md +59 -0
- package/docs/data-table.md +63 -0
- package/docs/dropdown-button.md +61 -0
- package/docs/editable-text.md +51 -0
- package/docs/form-select.md +70 -0
- package/docs/frame-box.md +43 -0
- package/docs/gallery-item-variant.md +37 -0
- package/docs/gallery-item.md +47 -0
- package/docs/icon-button.md +52 -0
- package/docs/live-timer.md +39 -0
- package/docs/mcp-evaluation.md +18 -11
- package/docs/photo-gallery.md +77 -0
- package/docs/popover-panel.md +70 -0
- package/docs/status-pill.md +43 -0
- package/docs/tile-grid.md +58 -0
- package/docs/user-avatar.md +1 -1
- package/llms.txt +357 -1
- package/package.json +10 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dropdown-button.js","sourceRoot":"","sources":["../src/dropdown-button.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, css, html, nothing, type PropertyValues } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { iconChevronRight } from \"./icons.js\";\nimport { tokens } from \"./tokens.js\";\n\n/** A single menu action. */\nexport interface DropdownOption {\n value: string;\n label: string;\n}\n\n/**\n * A primary-styled button with a label and chevron that opens an anchored\n * menu of actions — essentially `form-select` minus \"current value\"\n * semantics: a menu, not a select. Use for a set of mutually exclusive\n * next-step actions (e.g. a failed task's Retry / Close / Backlog).\n *\n * @element dropdown-button\n * @fires select - Fired with `{ value: string }` when a menu item is picked.\n */\n@customElement(\"dropdown-button\")\nexport class DropdownButton extends LitElement {\n static override styles = [\n tokens,\n css`\n :host {\n display: inline-block;\n position: relative;\n font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);\n }\n button.trigger {\n display: flex;\n align-items: center;\n gap: 0.35rem;\n font: inherit;\n font-size: var(--ui-font-size-sm, 0.75rem);\n font-weight: 500;\n color: #fff;\n background: var(--ui-primary, #4f46e5);\n border: 1px solid transparent;\n border-radius: var(--ui-radius-sm, 0.25rem);\n padding: 0.5rem 0.9rem;\n cursor: pointer;\n }\n button.trigger:hover:not(:disabled) {\n background: var(--ui-primary-hover, #4338ca);\n }\n button.trigger:disabled {\n cursor: not-allowed;\n opacity: 0.6;\n }\n button.trigger:focus-visible {\n outline: none;\n box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));\n }\n .chevron {\n display: flex;\n transform: rotate(90deg);\n transition: transform 0.1s ease;\n }\n :host([open]) .chevron {\n transform: rotate(-90deg);\n }\n ul.options {\n position: absolute;\n top: 100%;\n right: 0;\n z-index: 10;\n min-width: 100%;\n max-height: 40vh;\n overflow-y: auto;\n margin: 2px 0 0;\n padding: 4px 0;\n list-style: none;\n white-space: nowrap;\n background: var(--ui-surface, #fff);\n border: 1px solid var(--ui-border, #e2e8f0);\n border-radius: var(--ui-radius-sm, 0.25rem);\n box-shadow: var(--ui-shadow, 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1));\n }\n li {\n padding: 0.35rem 0.6rem;\n cursor: pointer;\n color: var(--ui-text, #0f172a);\n }\n li.active,\n li:hover {\n background: var(--ui-surface-muted, #f8fafc);\n }\n `,\n ];\n\n /** The trigger button's label. */\n @property() label = \"\";\n /** The menu's actions. */\n @property({ attribute: false }) options: DropdownOption[] = [];\n /** Disables the trigger, preventing the menu from opening. */\n @property({ type: Boolean }) disabled = false;\n\n @state() private _open = false;\n @state() private _activeIndex = -1;\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n window.removeEventListener(\"mousedown\", this.#onWindowMousedown, true);\n }\n\n protected override updated(changed: PropertyValues): void {\n if (!changed.has(\"_open\")) return;\n this.toggleAttribute(\"open\", this._open);\n if (this._open) {\n window.addEventListener(\"mousedown\", this.#onWindowMousedown, true);\n } else {\n window.removeEventListener(\"mousedown\", this.#onWindowMousedown, true);\n }\n }\n\n #onWindowMousedown = (e: MouseEvent): void => {\n if (!e.composedPath().includes(this)) this._open = false;\n };\n\n #toggle(): void {\n if (this.disabled) return;\n this._open = !this._open;\n if (this._open) this._activeIndex = 0;\n }\n\n #select(option: DropdownOption): void {\n this._open = false;\n this.dispatchEvent(new CustomEvent(\"select\", { detail: { value: option.value } }));\n }\n\n #onTriggerKeydown(e: KeyboardEvent): void {\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\" || e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n if (!this._open) {\n this.#toggle();\n return;\n }\n if (e.key === \"ArrowDown\") this.#moveActive(1);\n else if (e.key === \"ArrowUp\") this.#moveActive(-1);\n else this.#confirmActive();\n } else if (e.key === \"Escape\" && this._open) {\n e.preventDefault();\n this._open = false;\n }\n }\n\n #moveActive(delta: number): void {\n if (this.options.length === 0) return;\n const n = this.options.length;\n this._activeIndex = (this._activeIndex + delta + n) % n;\n }\n\n #confirmActive(): void {\n const option = this.options[this._activeIndex];\n if (option) this.#select(option);\n }\n\n private renderMenu() {\n if (!this._open) return nothing;\n return html`\n <ul class=\"options\" role=\"menu\">\n ${this.options.map(\n (o, i) => html`\n <li\n role=\"menuitem\"\n class=${i === this._activeIndex ? \"active\" : \"\"}\n @mousedown=${(e: MouseEvent) => {\n e.preventDefault();\n this.#select(o);\n }}\n >\n ${o.label}\n </li>\n `,\n )}\n </ul>\n `;\n }\n\n override render() {\n return html`\n <button\n type=\"button\"\n class=\"trigger\"\n aria-haspopup=\"menu\"\n aria-expanded=${this._open}\n ?disabled=${this.disabled}\n @click=${() => this.#toggle()}\n @keydown=${(e: KeyboardEvent) => this.#onTriggerKeydown(e)}\n >\n <span>${this.label}</span>\n <span class=\"chevron\">${iconChevronRight(14)}</span>\n </button>\n ${this.renderMenu()}\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"dropdown-button\": DropdownButton;\n }\n}\n"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { LitElement } from "lit";
|
|
2
|
+
/**
|
|
3
|
+
* Jira/GitHub-style click-to-edit text: a display span that turns into an
|
|
4
|
+
* `<input>` (or auto-growing `<textarea>` when `multiline`) on click. The
|
|
5
|
+
* input/textarea inherits the host's font, so a title wrapped in an `<h1>`
|
|
6
|
+
* edits at title size.
|
|
7
|
+
*
|
|
8
|
+
* @element editable-text
|
|
9
|
+
* @fires change - Fired with `{ value: string }` when the committed value differs from the previous one.
|
|
10
|
+
*/
|
|
11
|
+
export declare class EditableText extends LitElement {
|
|
12
|
+
static styles: import("lit").CSSResult[];
|
|
13
|
+
/** Current text. */
|
|
14
|
+
value: string;
|
|
15
|
+
/** `false` renders an `<input>`, `true` an auto-growing `<textarea>`. */
|
|
16
|
+
multiline: boolean;
|
|
17
|
+
/** Muted placeholder text shown when `value` is empty, and as the input's placeholder. */
|
|
18
|
+
placeholder: string;
|
|
19
|
+
/** Disables entering edit mode. */
|
|
20
|
+
readonly: boolean;
|
|
21
|
+
/** `aria-label` applied to the input/textarea. */
|
|
22
|
+
label: string;
|
|
23
|
+
private _editing;
|
|
24
|
+
private _draft;
|
|
25
|
+
private _field?;
|
|
26
|
+
/** Enters edit mode, seeding the draft from the current value. */
|
|
27
|
+
private _startEdit;
|
|
28
|
+
/** Commits the draft (reverting an empty single-line commit) and exits edit mode.
|
|
29
|
+
*
|
|
30
|
+
* Guarded on `_editing`: removing the focused input/textarea from the DOM
|
|
31
|
+
* (which happens as soon as `_editing` flips to false) makes the browser
|
|
32
|
+
* fire a native `blur` on it, re-invoking this handler a second time after
|
|
33
|
+
* a cancel already ran — the guard makes that second call a no-op instead
|
|
34
|
+
* of re-committing the stale draft.
|
|
35
|
+
*/
|
|
36
|
+
private _commit;
|
|
37
|
+
/** Discards the draft and exits edit mode without committing. */
|
|
38
|
+
private _cancel;
|
|
39
|
+
private _onInput;
|
|
40
|
+
private _onSingleLineKeydown;
|
|
41
|
+
private _onMultilineKeydown;
|
|
42
|
+
/** Grows the textarea's height to fit its content. */
|
|
43
|
+
private _resize;
|
|
44
|
+
protected updated(changed: Map<string, unknown>): void;
|
|
45
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
46
|
+
}
|
|
47
|
+
declare global {
|
|
48
|
+
interface HTMLElementTagNameMap {
|
|
49
|
+
"editable-text": EditableText;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=editable-text.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editable-text.d.ts","sourceRoot":"","sources":["../src/editable-text.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,MAAM,KAAK,CAAC;AAIrD;;;;;;;;GAQG;AACH,qBACa,YAAa,SAAQ,UAAU;IAC1C,OAAgB,MAAM,4BAuDpB;IAEF,oBAAoB;IACR,KAAK,SAAM;IACvB,yEAAyE;IAC5C,SAAS,UAAS;IAC/C,0FAA0F;IAC9E,WAAW,SAAM;IAC7B,mCAAmC;IACN,QAAQ,UAAS;IAC9C,kDAAkD;IACtC,KAAK,SAAM;IAEd,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAM;IAEH,OAAO,CAAC,MAAM,CAAC,CAAyC;IAElF,kEAAkE;IAClE,OAAO,CAAC,UAAU;IAMlB;;;;;;;OAOG;IACH,OAAO,CAAC,OAAO;IAUf,iEAAiE;IACjE,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,QAAQ;IAKhB,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,mBAAmB;IAU3B,sDAAsD;IACtD,OAAO,CAAC,OAAO;IAOf,UAAmB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,QASvD;IAEQ,MAAM,yCA4Bd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,eAAe,EAAE,YAAY,CAAC;KAC/B;CACF"}
|
|
@@ -0,0 +1,224 @@
|
|
|
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, query, state } from "lit/decorators.js";
|
|
9
|
+
import { tokens } from "./tokens.js";
|
|
10
|
+
/**
|
|
11
|
+
* Jira/GitHub-style click-to-edit text: a display span that turns into an
|
|
12
|
+
* `<input>` (or auto-growing `<textarea>` when `multiline`) on click. The
|
|
13
|
+
* input/textarea inherits the host's font, so a title wrapped in an `<h1>`
|
|
14
|
+
* edits at title size.
|
|
15
|
+
*
|
|
16
|
+
* @element editable-text
|
|
17
|
+
* @fires change - Fired with `{ value: string }` when the committed value differs from the previous one.
|
|
18
|
+
*/
|
|
19
|
+
let EditableText = class EditableText extends LitElement {
|
|
20
|
+
constructor() {
|
|
21
|
+
super(...arguments);
|
|
22
|
+
/** Current text. */
|
|
23
|
+
this.value = "";
|
|
24
|
+
/** `false` renders an `<input>`, `true` an auto-growing `<textarea>`. */
|
|
25
|
+
this.multiline = false;
|
|
26
|
+
/** Muted placeholder text shown when `value` is empty, and as the input's placeholder. */
|
|
27
|
+
this.placeholder = "";
|
|
28
|
+
/** Disables entering edit mode. */
|
|
29
|
+
this.readonly = false;
|
|
30
|
+
/** `aria-label` applied to the input/textarea. */
|
|
31
|
+
this.label = "";
|
|
32
|
+
this._editing = false;
|
|
33
|
+
this._draft = "";
|
|
34
|
+
}
|
|
35
|
+
static { this.styles = [
|
|
36
|
+
tokens,
|
|
37
|
+
css `
|
|
38
|
+
:host {
|
|
39
|
+
display: block;
|
|
40
|
+
font-family: inherit;
|
|
41
|
+
font-size: inherit;
|
|
42
|
+
font-weight: inherit;
|
|
43
|
+
line-height: inherit;
|
|
44
|
+
}
|
|
45
|
+
.display {
|
|
46
|
+
cursor: text;
|
|
47
|
+
border-radius: var(--ui-radius-sm, 0.25rem);
|
|
48
|
+
padding: 0.1rem 0.3rem;
|
|
49
|
+
margin: -0.1rem -0.3rem;
|
|
50
|
+
}
|
|
51
|
+
.display.multiline {
|
|
52
|
+
white-space: pre-wrap;
|
|
53
|
+
}
|
|
54
|
+
.display:hover {
|
|
55
|
+
background: var(--ui-surface-muted, #f8fafc);
|
|
56
|
+
}
|
|
57
|
+
.display.empty {
|
|
58
|
+
color: var(--ui-text-muted, #64748b);
|
|
59
|
+
}
|
|
60
|
+
.display.readonly {
|
|
61
|
+
cursor: inherit;
|
|
62
|
+
}
|
|
63
|
+
.display.readonly:hover {
|
|
64
|
+
background: none;
|
|
65
|
+
}
|
|
66
|
+
input,
|
|
67
|
+
textarea {
|
|
68
|
+
display: block;
|
|
69
|
+
width: 100%;
|
|
70
|
+
box-sizing: border-box;
|
|
71
|
+
font: inherit;
|
|
72
|
+
color: inherit;
|
|
73
|
+
background: var(--ui-surface, #fff);
|
|
74
|
+
border: 1px solid var(--ui-border, #e2e8f0);
|
|
75
|
+
border-radius: var(--ui-radius-sm, 0.25rem);
|
|
76
|
+
padding: 0.1rem 0.3rem;
|
|
77
|
+
margin: -0.1rem -0.3rem;
|
|
78
|
+
outline: none;
|
|
79
|
+
}
|
|
80
|
+
input:focus,
|
|
81
|
+
textarea:focus {
|
|
82
|
+
border-color: var(--ui-primary, #4f46e5);
|
|
83
|
+
box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));
|
|
84
|
+
}
|
|
85
|
+
textarea {
|
|
86
|
+
resize: none;
|
|
87
|
+
overflow: hidden;
|
|
88
|
+
}
|
|
89
|
+
`,
|
|
90
|
+
]; }
|
|
91
|
+
/** Enters edit mode, seeding the draft from the current value. */
|
|
92
|
+
_startEdit() {
|
|
93
|
+
if (this.readonly)
|
|
94
|
+
return;
|
|
95
|
+
this._draft = this.value;
|
|
96
|
+
this._editing = true;
|
|
97
|
+
}
|
|
98
|
+
/** Commits the draft (reverting an empty single-line commit) and exits edit mode.
|
|
99
|
+
*
|
|
100
|
+
* Guarded on `_editing`: removing the focused input/textarea from the DOM
|
|
101
|
+
* (which happens as soon as `_editing` flips to false) makes the browser
|
|
102
|
+
* fire a native `blur` on it, re-invoking this handler a second time after
|
|
103
|
+
* a cancel already ran — the guard makes that second call a no-op instead
|
|
104
|
+
* of re-committing the stale draft.
|
|
105
|
+
*/
|
|
106
|
+
_commit() {
|
|
107
|
+
if (!this._editing)
|
|
108
|
+
return;
|
|
109
|
+
const next = this._draft;
|
|
110
|
+
this._editing = false;
|
|
111
|
+
if (!this.multiline && next.trim() === "")
|
|
112
|
+
return;
|
|
113
|
+
if (next === this.value)
|
|
114
|
+
return;
|
|
115
|
+
this.value = next;
|
|
116
|
+
this.dispatchEvent(new CustomEvent("change", { detail: { value: next } }));
|
|
117
|
+
}
|
|
118
|
+
/** Discards the draft and exits edit mode without committing. */
|
|
119
|
+
_cancel() {
|
|
120
|
+
this._editing = false;
|
|
121
|
+
}
|
|
122
|
+
_onInput(e) {
|
|
123
|
+
this._draft = e.target.value;
|
|
124
|
+
if (this.multiline)
|
|
125
|
+
this._resize();
|
|
126
|
+
}
|
|
127
|
+
_onSingleLineKeydown(e) {
|
|
128
|
+
if (e.key === "Enter") {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
this._commit();
|
|
131
|
+
}
|
|
132
|
+
else if (e.key === "Escape") {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
this._cancel();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
_onMultilineKeydown(e) {
|
|
138
|
+
if (e.key === "Escape") {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
this._cancel();
|
|
141
|
+
}
|
|
142
|
+
else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
this._commit();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Grows the textarea's height to fit its content. */
|
|
148
|
+
_resize() {
|
|
149
|
+
const field = this._field;
|
|
150
|
+
if (!field)
|
|
151
|
+
return;
|
|
152
|
+
field.style.height = "auto";
|
|
153
|
+
field.style.height = `${field.scrollHeight}px`;
|
|
154
|
+
}
|
|
155
|
+
updated(changed) {
|
|
156
|
+
if (changed.has("_editing") && this._editing) {
|
|
157
|
+
const field = this._field;
|
|
158
|
+
if (!field)
|
|
159
|
+
return;
|
|
160
|
+
field.focus();
|
|
161
|
+
const end = field.value.length;
|
|
162
|
+
field.setSelectionRange(end, end);
|
|
163
|
+
if (this.multiline)
|
|
164
|
+
this._resize();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
render() {
|
|
168
|
+
if (this._editing) {
|
|
169
|
+
return this.multiline
|
|
170
|
+
? html `<textarea
|
|
171
|
+
rows="3"
|
|
172
|
+
.value=${this._draft}
|
|
173
|
+
placeholder=${this.placeholder}
|
|
174
|
+
aria-label=${this.label || nothing}
|
|
175
|
+
@input=${this._onInput}
|
|
176
|
+
@keydown=${this._onMultilineKeydown}
|
|
177
|
+
@blur=${this._commit}
|
|
178
|
+
></textarea>`
|
|
179
|
+
: html `<input
|
|
180
|
+
type="text"
|
|
181
|
+
.value=${this._draft}
|
|
182
|
+
placeholder=${this.placeholder}
|
|
183
|
+
aria-label=${this.label || nothing}
|
|
184
|
+
@input=${this._onInput}
|
|
185
|
+
@keydown=${this._onSingleLineKeydown}
|
|
186
|
+
@blur=${this._commit}
|
|
187
|
+
/>`;
|
|
188
|
+
}
|
|
189
|
+
const isEmpty = this.value.trim() === "";
|
|
190
|
+
const classes = `display ${this.multiline ? "multiline" : ""} ${isEmpty ? "empty" : ""} ${this.readonly ? "readonly" : ""}`;
|
|
191
|
+
return html `<span class=${classes} @click=${this._startEdit}
|
|
192
|
+
>${isEmpty ? this.placeholder : this.value}</span
|
|
193
|
+
>`;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
__decorate([
|
|
197
|
+
property()
|
|
198
|
+
], EditableText.prototype, "value", void 0);
|
|
199
|
+
__decorate([
|
|
200
|
+
property({ type: Boolean })
|
|
201
|
+
], EditableText.prototype, "multiline", void 0);
|
|
202
|
+
__decorate([
|
|
203
|
+
property()
|
|
204
|
+
], EditableText.prototype, "placeholder", void 0);
|
|
205
|
+
__decorate([
|
|
206
|
+
property({ type: Boolean })
|
|
207
|
+
], EditableText.prototype, "readonly", void 0);
|
|
208
|
+
__decorate([
|
|
209
|
+
property()
|
|
210
|
+
], EditableText.prototype, "label", void 0);
|
|
211
|
+
__decorate([
|
|
212
|
+
state()
|
|
213
|
+
], EditableText.prototype, "_editing", void 0);
|
|
214
|
+
__decorate([
|
|
215
|
+
state()
|
|
216
|
+
], EditableText.prototype, "_draft", void 0);
|
|
217
|
+
__decorate([
|
|
218
|
+
query("input, textarea")
|
|
219
|
+
], EditableText.prototype, "_field", void 0);
|
|
220
|
+
EditableText = __decorate([
|
|
221
|
+
customElement("editable-text")
|
|
222
|
+
], EditableText);
|
|
223
|
+
export { EditableText };
|
|
224
|
+
//# sourceMappingURL=editable-text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editable-text.js","sourceRoot":"","sources":["../src/editable-text.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, css, html, nothing } from \"lit\";\nimport { customElement, property, query, state } from \"lit/decorators.js\";\nimport { tokens } from \"./tokens.js\";\n\n/**\n * Jira/GitHub-style click-to-edit text: a display span that turns into an\n * `<input>` (or auto-growing `<textarea>` when `multiline`) on click. The\n * input/textarea inherits the host's font, so a title wrapped in an `<h1>`\n * edits at title size.\n *\n * @element editable-text\n * @fires change - Fired with `{ value: string }` when the committed value differs from the previous one.\n */\n@customElement(\"editable-text\")\nexport class EditableText extends LitElement {\n static override styles = [\n tokens,\n css`\n :host {\n display: block;\n font-family: inherit;\n font-size: inherit;\n font-weight: inherit;\n line-height: inherit;\n }\n .display {\n cursor: text;\n border-radius: var(--ui-radius-sm, 0.25rem);\n padding: 0.1rem 0.3rem;\n margin: -0.1rem -0.3rem;\n }\n .display.multiline {\n white-space: pre-wrap;\n }\n .display:hover {\n background: var(--ui-surface-muted, #f8fafc);\n }\n .display.empty {\n color: var(--ui-text-muted, #64748b);\n }\n .display.readonly {\n cursor: inherit;\n }\n .display.readonly:hover {\n background: none;\n }\n input,\n textarea {\n display: block;\n width: 100%;\n box-sizing: border-box;\n font: inherit;\n color: inherit;\n background: var(--ui-surface, #fff);\n border: 1px solid var(--ui-border, #e2e8f0);\n border-radius: var(--ui-radius-sm, 0.25rem);\n padding: 0.1rem 0.3rem;\n margin: -0.1rem -0.3rem;\n outline: none;\n }\n input:focus,\n textarea:focus {\n border-color: var(--ui-primary, #4f46e5);\n box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));\n }\n textarea {\n resize: none;\n overflow: hidden;\n }\n `,\n ];\n\n /** Current text. */\n @property() value = \"\";\n /** `false` renders an `<input>`, `true` an auto-growing `<textarea>`. */\n @property({ type: Boolean }) multiline = false;\n /** Muted placeholder text shown when `value` is empty, and as the input's placeholder. */\n @property() placeholder = \"\";\n /** Disables entering edit mode. */\n @property({ type: Boolean }) readonly = false;\n /** `aria-label` applied to the input/textarea. */\n @property() label = \"\";\n\n @state() private _editing = false;\n @state() private _draft = \"\";\n\n @query(\"input, textarea\") private _field?: HTMLInputElement | HTMLTextAreaElement;\n\n /** Enters edit mode, seeding the draft from the current value. */\n private _startEdit() {\n if (this.readonly) return;\n this._draft = this.value;\n this._editing = true;\n }\n\n /** Commits the draft (reverting an empty single-line commit) and exits edit mode.\n *\n * Guarded on `_editing`: removing the focused input/textarea from the DOM\n * (which happens as soon as `_editing` flips to false) makes the browser\n * fire a native `blur` on it, re-invoking this handler a second time after\n * a cancel already ran — the guard makes that second call a no-op instead\n * of re-committing the stale draft.\n */\n private _commit() {\n if (!this._editing) return;\n const next = this._draft;\n this._editing = false;\n if (!this.multiline && next.trim() === \"\") return;\n if (next === this.value) return;\n this.value = next;\n this.dispatchEvent(new CustomEvent(\"change\", { detail: { value: next } }));\n }\n\n /** Discards the draft and exits edit mode without committing. */\n private _cancel() {\n this._editing = false;\n }\n\n private _onInput(e: Event) {\n this._draft = (e.target as HTMLInputElement | HTMLTextAreaElement).value;\n if (this.multiline) this._resize();\n }\n\n private _onSingleLineKeydown(e: KeyboardEvent) {\n if (e.key === \"Enter\") {\n e.preventDefault();\n this._commit();\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n this._cancel();\n }\n }\n\n private _onMultilineKeydown(e: KeyboardEvent) {\n if (e.key === \"Escape\") {\n e.preventDefault();\n this._cancel();\n } else if (e.key === \"Enter\" && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n this._commit();\n }\n }\n\n /** Grows the textarea's height to fit its content. */\n private _resize() {\n const field = this._field;\n if (!field) return;\n field.style.height = \"auto\";\n field.style.height = `${field.scrollHeight}px`;\n }\n\n protected override updated(changed: Map<string, unknown>) {\n if (changed.has(\"_editing\") && this._editing) {\n const field = this._field;\n if (!field) return;\n field.focus();\n const end = field.value.length;\n field.setSelectionRange(end, end);\n if (this.multiline) this._resize();\n }\n }\n\n override render() {\n if (this._editing) {\n return this.multiline\n ? html`<textarea\n rows=\"3\"\n .value=${this._draft}\n placeholder=${this.placeholder}\n aria-label=${this.label || nothing}\n @input=${this._onInput}\n @keydown=${this._onMultilineKeydown}\n @blur=${this._commit}\n ></textarea>`\n : html`<input\n type=\"text\"\n .value=${this._draft}\n placeholder=${this.placeholder}\n aria-label=${this.label || nothing}\n @input=${this._onInput}\n @keydown=${this._onSingleLineKeydown}\n @blur=${this._commit}\n />`;\n }\n\n const isEmpty = this.value.trim() === \"\";\n const classes = `display ${this.multiline ? \"multiline\" : \"\"} ${isEmpty ? \"empty\" : \"\"} ${this.readonly ? \"readonly\" : \"\"}`;\n return html`<span class=${classes} @click=${this._startEdit}\n >${isEmpty ? this.placeholder : this.value}</span\n >`;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"editable-text\": EditableText;\n }\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { LitElement, type PropertyValues } from "lit";
|
|
2
|
+
/** A single selectable option. */
|
|
3
|
+
export interface SelectOption {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A styled dropdown select: a trigger button showing the current option's
|
|
9
|
+
* label, opening a listbox popover on click. Drop-in generic replacement for
|
|
10
|
+
* a native `<select>` wherever consistent cross-browser styling and a
|
|
11
|
+
* `change` event carrying `{ value }` are wanted (e.g. a task's status
|
|
12
|
+
* picker).
|
|
13
|
+
*
|
|
14
|
+
* The trigger fills its host's width (`justify-content: space-between`
|
|
15
|
+
* pushes the chevron to the far edge), but the host itself stays
|
|
16
|
+
* `display: inline-block` — so usages that never size the host (a filter
|
|
17
|
+
* bar, a status picker) keep shrink-to-fit auto-width unchanged. To make an
|
|
18
|
+
* instance full-width, size the host itself: `form-select { width: 100%; }`.
|
|
19
|
+
*
|
|
20
|
+
* @element form-select
|
|
21
|
+
* @fires change - Fired with `{ value: string }` when a different option is picked.
|
|
22
|
+
*/
|
|
23
|
+
export declare class FormSelect extends LitElement {
|
|
24
|
+
#private;
|
|
25
|
+
static styles: import("lit").CSSResult[];
|
|
26
|
+
/** The full list of selectable options. */
|
|
27
|
+
options: SelectOption[];
|
|
28
|
+
/** Currently selected value; must match one of `options[].value`. */
|
|
29
|
+
value: string;
|
|
30
|
+
/** `aria-label` applied to the trigger button. */
|
|
31
|
+
label: string;
|
|
32
|
+
/** Disables the trigger, preventing the popover from opening. */
|
|
33
|
+
disabled: boolean;
|
|
34
|
+
private _open;
|
|
35
|
+
private _activeIndex;
|
|
36
|
+
disconnectedCallback(): void;
|
|
37
|
+
protected updated(changed: PropertyValues): void;
|
|
38
|
+
private renderListbox;
|
|
39
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
40
|
+
}
|
|
41
|
+
declare global {
|
|
42
|
+
interface HTMLElementTagNameMap {
|
|
43
|
+
"form-select": FormSelect;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=form-select.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form-select.d.ts","sourceRoot":"","sources":["../src/form-select.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAK1E,kCAAkC;AAClC,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBACa,UAAW,SAAQ,UAAU;;IACxC,OAAgB,MAAM,4BA2EpB;IAEF,2CAA2C;IACX,OAAO,EAAE,YAAY,EAAE,CAAM;IAC7D,qEAAqE;IACzD,KAAK,SAAM;IACvB,kDAAkD;IACtC,KAAK,SAAM;IACvB,iEAAiE;IACpC,QAAQ,UAAS;IAErC,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,YAAY,CAAM;IAE1B,oBAAoB,IAAI,IAAI,CAGpC;IAED,UAAmB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAQxD;IAmDD,OAAO,CAAC,aAAa;IAuBZ,MAAM,yCAkBd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,aAAa,EAAE,UAAU,CAAC;KAC3B;CACF"}
|
|
@@ -0,0 +1,245 @@
|
|
|
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, state } from "lit/decorators.js";
|
|
9
|
+
import { iconChevronRight } from "./icons.js";
|
|
10
|
+
import { tokens } from "./tokens.js";
|
|
11
|
+
/**
|
|
12
|
+
* A styled dropdown select: a trigger button showing the current option's
|
|
13
|
+
* label, opening a listbox popover on click. Drop-in generic replacement for
|
|
14
|
+
* a native `<select>` wherever consistent cross-browser styling and a
|
|
15
|
+
* `change` event carrying `{ value }` are wanted (e.g. a task's status
|
|
16
|
+
* picker).
|
|
17
|
+
*
|
|
18
|
+
* The trigger fills its host's width (`justify-content: space-between`
|
|
19
|
+
* pushes the chevron to the far edge), but the host itself stays
|
|
20
|
+
* `display: inline-block` — so usages that never size the host (a filter
|
|
21
|
+
* bar, a status picker) keep shrink-to-fit auto-width unchanged. To make an
|
|
22
|
+
* instance full-width, size the host itself: `form-select { width: 100%; }`.
|
|
23
|
+
*
|
|
24
|
+
* @element form-select
|
|
25
|
+
* @fires change - Fired with `{ value: string }` when a different option is picked.
|
|
26
|
+
*/
|
|
27
|
+
let FormSelect = class FormSelect extends LitElement {
|
|
28
|
+
constructor() {
|
|
29
|
+
super(...arguments);
|
|
30
|
+
/** The full list of selectable options. */
|
|
31
|
+
this.options = [];
|
|
32
|
+
/** Currently selected value; must match one of `options[].value`. */
|
|
33
|
+
this.value = "";
|
|
34
|
+
/** `aria-label` applied to the trigger button. */
|
|
35
|
+
this.label = "";
|
|
36
|
+
/** Disables the trigger, preventing the popover from opening. */
|
|
37
|
+
this.disabled = false;
|
|
38
|
+
this._open = false;
|
|
39
|
+
this._activeIndex = -1;
|
|
40
|
+
this.#onWindowMousedown = (e) => {
|
|
41
|
+
if (!e.composedPath().includes(this))
|
|
42
|
+
this._open = false;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
static { this.styles = [
|
|
46
|
+
tokens,
|
|
47
|
+
css `
|
|
48
|
+
:host {
|
|
49
|
+
display: inline-block;
|
|
50
|
+
position: relative;
|
|
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
|
+
button.trigger {
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: space-between;
|
|
58
|
+
gap: 0.35rem;
|
|
59
|
+
width: 100%;
|
|
60
|
+
box-sizing: border-box;
|
|
61
|
+
font: inherit;
|
|
62
|
+
color: var(--ui-text, #0f172a);
|
|
63
|
+
background: var(--ui-surface, #fff);
|
|
64
|
+
border: 1px solid var(--ui-border, #e2e8f0);
|
|
65
|
+
border-radius: var(--ui-radius-sm, 0.25rem);
|
|
66
|
+
padding: 0.3rem 0.5rem;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
}
|
|
69
|
+
button.trigger:hover {
|
|
70
|
+
background: var(--ui-surface-muted, #f8fafc);
|
|
71
|
+
}
|
|
72
|
+
button.trigger:disabled {
|
|
73
|
+
cursor: not-allowed;
|
|
74
|
+
opacity: 0.6;
|
|
75
|
+
}
|
|
76
|
+
button.trigger:focus-visible {
|
|
77
|
+
outline: none;
|
|
78
|
+
border-color: var(--ui-primary, #4f46e5);
|
|
79
|
+
box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));
|
|
80
|
+
}
|
|
81
|
+
.chevron {
|
|
82
|
+
display: flex;
|
|
83
|
+
color: var(--ui-text-muted, #64748b);
|
|
84
|
+
transform: rotate(90deg);
|
|
85
|
+
transition: transform 0.1s ease;
|
|
86
|
+
}
|
|
87
|
+
:host([open]) .chevron {
|
|
88
|
+
transform: rotate(-90deg);
|
|
89
|
+
}
|
|
90
|
+
ul.options {
|
|
91
|
+
position: absolute;
|
|
92
|
+
top: 100%;
|
|
93
|
+
left: 0;
|
|
94
|
+
z-index: 10;
|
|
95
|
+
min-width: 100%;
|
|
96
|
+
max-height: 40vh;
|
|
97
|
+
overflow-y: auto;
|
|
98
|
+
margin: 2px 0 0;
|
|
99
|
+
padding: 4px 0;
|
|
100
|
+
list-style: none;
|
|
101
|
+
white-space: nowrap;
|
|
102
|
+
background: var(--ui-surface, #fff);
|
|
103
|
+
border: 1px solid var(--ui-border, #e2e8f0);
|
|
104
|
+
border-radius: var(--ui-radius-sm, 0.25rem);
|
|
105
|
+
box-shadow: var(--ui-shadow, 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1));
|
|
106
|
+
}
|
|
107
|
+
li {
|
|
108
|
+
padding: 0.35rem 0.6rem;
|
|
109
|
+
cursor: pointer;
|
|
110
|
+
}
|
|
111
|
+
li.active,
|
|
112
|
+
li:hover {
|
|
113
|
+
background: var(--ui-surface-muted, #f8fafc);
|
|
114
|
+
}
|
|
115
|
+
li[aria-selected="true"] {
|
|
116
|
+
font-weight: 600;
|
|
117
|
+
color: var(--ui-primary, #4f46e5);
|
|
118
|
+
}
|
|
119
|
+
`,
|
|
120
|
+
]; }
|
|
121
|
+
disconnectedCallback() {
|
|
122
|
+
super.disconnectedCallback();
|
|
123
|
+
window.removeEventListener("mousedown", this.#onWindowMousedown, true);
|
|
124
|
+
}
|
|
125
|
+
updated(changed) {
|
|
126
|
+
if (!changed.has("_open"))
|
|
127
|
+
return;
|
|
128
|
+
this.toggleAttribute("open", this._open);
|
|
129
|
+
if (this._open) {
|
|
130
|
+
window.addEventListener("mousedown", this.#onWindowMousedown, true);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
window.removeEventListener("mousedown", this.#onWindowMousedown, true);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
#onWindowMousedown;
|
|
137
|
+
#toggle() {
|
|
138
|
+
if (this.disabled)
|
|
139
|
+
return;
|
|
140
|
+
this._open = !this._open;
|
|
141
|
+
if (this._open) {
|
|
142
|
+
this._activeIndex = Math.max(0, this.options.findIndex((o) => o.value === this.value));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
#select(option) {
|
|
146
|
+
this._open = false;
|
|
147
|
+
if (option.value === this.value)
|
|
148
|
+
return;
|
|
149
|
+
this.value = option.value;
|
|
150
|
+
this.dispatchEvent(new CustomEvent("change", { detail: { value: option.value } }));
|
|
151
|
+
}
|
|
152
|
+
#onTriggerKeydown(e) {
|
|
153
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Enter" || e.key === " ") {
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
if (!this._open) {
|
|
156
|
+
this.#toggle();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (e.key === "ArrowDown")
|
|
160
|
+
this.#moveActive(1);
|
|
161
|
+
else if (e.key === "ArrowUp")
|
|
162
|
+
this.#moveActive(-1);
|
|
163
|
+
else
|
|
164
|
+
this.#confirmActive();
|
|
165
|
+
}
|
|
166
|
+
else if (e.key === "Escape" && this._open) {
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
this._open = false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
#moveActive(delta) {
|
|
172
|
+
if (this.options.length === 0)
|
|
173
|
+
return;
|
|
174
|
+
const n = this.options.length;
|
|
175
|
+
this._activeIndex = (this._activeIndex + delta + n) % n;
|
|
176
|
+
}
|
|
177
|
+
#confirmActive() {
|
|
178
|
+
const option = this.options[this._activeIndex];
|
|
179
|
+
if (option)
|
|
180
|
+
this.#select(option);
|
|
181
|
+
}
|
|
182
|
+
renderListbox() {
|
|
183
|
+
if (!this._open)
|
|
184
|
+
return nothing;
|
|
185
|
+
return html `
|
|
186
|
+
<ul class="options" role="listbox">
|
|
187
|
+
${this.options.map((o, i) => html `
|
|
188
|
+
<li
|
|
189
|
+
role="option"
|
|
190
|
+
aria-selected=${o.value === this.value}
|
|
191
|
+
class=${i === this._activeIndex ? "active" : ""}
|
|
192
|
+
@mousedown=${(e) => {
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
this.#select(o);
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
${o.label}
|
|
198
|
+
</li>
|
|
199
|
+
`)}
|
|
200
|
+
</ul>
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
render() {
|
|
204
|
+
const current = this.options.find((o) => o.value === this.value);
|
|
205
|
+
return html `
|
|
206
|
+
<button
|
|
207
|
+
type="button"
|
|
208
|
+
class="trigger"
|
|
209
|
+
aria-haspopup="listbox"
|
|
210
|
+
aria-expanded=${this._open}
|
|
211
|
+
aria-label=${this.label || nothing}
|
|
212
|
+
?disabled=${this.disabled}
|
|
213
|
+
@click=${() => this.#toggle()}
|
|
214
|
+
@keydown=${(e) => this.#onTriggerKeydown(e)}
|
|
215
|
+
>
|
|
216
|
+
<span>${current?.label ?? this.value}</span>
|
|
217
|
+
<span class="chevron">${iconChevronRight(14)}</span>
|
|
218
|
+
</button>
|
|
219
|
+
${this.renderListbox()}
|
|
220
|
+
`;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
__decorate([
|
|
224
|
+
property({ attribute: false })
|
|
225
|
+
], FormSelect.prototype, "options", void 0);
|
|
226
|
+
__decorate([
|
|
227
|
+
property()
|
|
228
|
+
], FormSelect.prototype, "value", void 0);
|
|
229
|
+
__decorate([
|
|
230
|
+
property()
|
|
231
|
+
], FormSelect.prototype, "label", void 0);
|
|
232
|
+
__decorate([
|
|
233
|
+
property({ type: Boolean })
|
|
234
|
+
], FormSelect.prototype, "disabled", void 0);
|
|
235
|
+
__decorate([
|
|
236
|
+
state()
|
|
237
|
+
], FormSelect.prototype, "_open", void 0);
|
|
238
|
+
__decorate([
|
|
239
|
+
state()
|
|
240
|
+
], FormSelect.prototype, "_activeIndex", void 0);
|
|
241
|
+
FormSelect = __decorate([
|
|
242
|
+
customElement("form-select")
|
|
243
|
+
], FormSelect);
|
|
244
|
+
export { FormSelect };
|
|
245
|
+
//# sourceMappingURL=form-select.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form-select.js","sourceRoot":"","sources":["../src/form-select.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, css, html, nothing, type PropertyValues } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { iconChevronRight } from \"./icons.js\";\nimport { tokens } from \"./tokens.js\";\n\n/** A single selectable option. */\nexport interface SelectOption {\n value: string;\n label: string;\n}\n\n/**\n * A styled dropdown select: a trigger button showing the current option's\n * label, opening a listbox popover on click. Drop-in generic replacement for\n * a native `<select>` wherever consistent cross-browser styling and a\n * `change` event carrying `{ value }` are wanted (e.g. a task's status\n * picker).\n *\n * The trigger fills its host's width (`justify-content: space-between`\n * pushes the chevron to the far edge), but the host itself stays\n * `display: inline-block` — so usages that never size the host (a filter\n * bar, a status picker) keep shrink-to-fit auto-width unchanged. To make an\n * instance full-width, size the host itself: `form-select { width: 100%; }`.\n *\n * @element form-select\n * @fires change - Fired with `{ value: string }` when a different option is picked.\n */\n@customElement(\"form-select\")\nexport class FormSelect extends LitElement {\n static override styles = [\n tokens,\n css`\n :host {\n display: inline-block;\n position: relative;\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 button.trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.35rem;\n width: 100%;\n box-sizing: border-box;\n font: inherit;\n color: var(--ui-text, #0f172a);\n background: var(--ui-surface, #fff);\n border: 1px solid var(--ui-border, #e2e8f0);\n border-radius: var(--ui-radius-sm, 0.25rem);\n padding: 0.3rem 0.5rem;\n cursor: pointer;\n }\n button.trigger:hover {\n background: var(--ui-surface-muted, #f8fafc);\n }\n button.trigger:disabled {\n cursor: not-allowed;\n opacity: 0.6;\n }\n button.trigger:focus-visible {\n outline: none;\n border-color: var(--ui-primary, #4f46e5);\n box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));\n }\n .chevron {\n display: flex;\n color: var(--ui-text-muted, #64748b);\n transform: rotate(90deg);\n transition: transform 0.1s ease;\n }\n :host([open]) .chevron {\n transform: rotate(-90deg);\n }\n ul.options {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 10;\n min-width: 100%;\n max-height: 40vh;\n overflow-y: auto;\n margin: 2px 0 0;\n padding: 4px 0;\n list-style: none;\n white-space: nowrap;\n background: var(--ui-surface, #fff);\n border: 1px solid var(--ui-border, #e2e8f0);\n border-radius: var(--ui-radius-sm, 0.25rem);\n box-shadow: var(--ui-shadow, 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1));\n }\n li {\n padding: 0.35rem 0.6rem;\n cursor: pointer;\n }\n li.active,\n li:hover {\n background: var(--ui-surface-muted, #f8fafc);\n }\n li[aria-selected=\"true\"] {\n font-weight: 600;\n color: var(--ui-primary, #4f46e5);\n }\n `,\n ];\n\n /** The full list of selectable options. */\n @property({ attribute: false }) options: SelectOption[] = [];\n /** Currently selected value; must match one of `options[].value`. */\n @property() value = \"\";\n /** `aria-label` applied to the trigger button. */\n @property() label = \"\";\n /** Disables the trigger, preventing the popover from opening. */\n @property({ type: Boolean }) disabled = false;\n\n @state() private _open = false;\n @state() private _activeIndex = -1;\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n window.removeEventListener(\"mousedown\", this.#onWindowMousedown, true);\n }\n\n protected override updated(changed: PropertyValues): void {\n if (!changed.has(\"_open\")) return;\n this.toggleAttribute(\"open\", this._open);\n if (this._open) {\n window.addEventListener(\"mousedown\", this.#onWindowMousedown, true);\n } else {\n window.removeEventListener(\"mousedown\", this.#onWindowMousedown, true);\n }\n }\n\n #onWindowMousedown = (e: MouseEvent): void => {\n if (!e.composedPath().includes(this)) this._open = false;\n };\n\n #toggle(): void {\n if (this.disabled) return;\n this._open = !this._open;\n if (this._open) {\n this._activeIndex = Math.max(\n 0,\n this.options.findIndex((o) => o.value === this.value),\n );\n }\n }\n\n #select(option: SelectOption): void {\n this._open = false;\n if (option.value === this.value) return;\n this.value = option.value;\n this.dispatchEvent(new CustomEvent(\"change\", { detail: { value: option.value } }));\n }\n\n #onTriggerKeydown(e: KeyboardEvent): void {\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\" || e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n if (!this._open) {\n this.#toggle();\n return;\n }\n if (e.key === \"ArrowDown\") this.#moveActive(1);\n else if (e.key === \"ArrowUp\") this.#moveActive(-1);\n else this.#confirmActive();\n } else if (e.key === \"Escape\" && this._open) {\n e.preventDefault();\n this._open = false;\n }\n }\n\n #moveActive(delta: number): void {\n if (this.options.length === 0) return;\n const n = this.options.length;\n this._activeIndex = (this._activeIndex + delta + n) % n;\n }\n\n #confirmActive(): void {\n const option = this.options[this._activeIndex];\n if (option) this.#select(option);\n }\n\n private renderListbox() {\n if (!this._open) return nothing;\n return html`\n <ul class=\"options\" role=\"listbox\">\n ${this.options.map(\n (o, i) => html`\n <li\n role=\"option\"\n aria-selected=${o.value === this.value}\n class=${i === this._activeIndex ? \"active\" : \"\"}\n @mousedown=${(e: MouseEvent) => {\n e.preventDefault();\n this.#select(o);\n }}\n >\n ${o.label}\n </li>\n `,\n )}\n </ul>\n `;\n }\n\n override render() {\n const current = this.options.find((o) => o.value === this.value);\n return html`\n <button\n type=\"button\"\n class=\"trigger\"\n aria-haspopup=\"listbox\"\n aria-expanded=${this._open}\n aria-label=${this.label || nothing}\n ?disabled=${this.disabled}\n @click=${() => this.#toggle()}\n @keydown=${(e: KeyboardEvent) => this.#onTriggerKeydown(e)}\n >\n <span>${current?.label ?? this.value}</span>\n <span class=\"chevron\">${iconChevronRight(14)}</span>\n </button>\n ${this.renderListbox()}\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"form-select\": FormSelect;\n }\n}\n"]}
|