@aceshooting/lyra-ui 0.1.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/LICENSE +21 -0
- package/README.md +98 -0
- package/dist/components/combobox/combobox.d.ts +90 -0
- package/dist/components/combobox/combobox.js +398 -0
- package/dist/components/combobox/combobox.styles.d.ts +1 -0
- package/dist/components/combobox/combobox.styles.js +148 -0
- package/dist/components/combobox/option.d.ts +32 -0
- package/dist/components/combobox/option.js +62 -0
- package/dist/components/date-picker/calendar-core.d.ts +20 -0
- package/dist/components/date-picker/calendar-core.js +71 -0
- package/dist/components/date-picker/date-input.d.ts +57 -0
- package/dist/components/date-picker/date-input.js +233 -0
- package/dist/components/date-picker/date-input.styles.d.ts +1 -0
- package/dist/components/date-picker/date-input.styles.js +67 -0
- package/dist/components/date-picker/date-picker.d.ts +64 -0
- package/dist/components/date-picker/date-picker.js +310 -0
- package/dist/components/date-picker/date-picker.styles.d.ts +1 -0
- package/dist/components/date-picker/date-picker.styles.js +101 -0
- package/dist/components/flag/flag.d.ts +35 -0
- package/dist/components/flag/flag.js +91 -0
- package/dist/components/flag/flag.styles.d.ts +1 -0
- package/dist/components/flag/flag.styles.js +27 -0
- package/dist/components/flag/language-map.d.ts +11 -0
- package/dist/components/flag/language-map.js +62 -0
- package/dist/components/sparkline/sparkline.d.ts +30 -0
- package/dist/components/sparkline/sparkline.js +74 -0
- package/dist/components/sparkline/sparkline.styles.d.ts +1 -0
- package/dist/components/sparkline/sparkline.styles.js +31 -0
- package/dist/components/toast/toast-item.d.ts +49 -0
- package/dist/components/toast/toast-item.js +123 -0
- package/dist/components/toast/toast-item.styles.d.ts +1 -0
- package/dist/components/toast/toast-item.styles.js +81 -0
- package/dist/components/toast/toast.d.ts +32 -0
- package/dist/components/toast/toast.js +48 -0
- package/dist/components/toast/toast.styles.d.ts +1 -0
- package/dist/components/toast/toast.styles.js +52 -0
- package/dist/components/toast/toaster.d.ts +26 -0
- package/dist/components/toast/toaster.js +41 -0
- package/dist/internal/a11y.d.ts +4 -0
- package/dist/internal/a11y.js +18 -0
- package/dist/internal/form-associated.d.ts +26 -0
- package/dist/internal/form-associated.js +56 -0
- package/dist/internal/lyra-element.d.ts +11 -0
- package/dist/internal/lyra-element.js +21 -0
- package/dist/internal/positioner.d.ts +10 -0
- package/dist/internal/positioner.js +21 -0
- package/dist/internal/prefix.d.ts +4 -0
- package/dist/internal/prefix.js +8 -0
- package/dist/internal/tokens.styles.d.ts +1 -0
- package/dist/internal/tokens.styles.js +35 -0
- package/dist/lyra.d.ts +26 -0
- package/dist/lyra.js +23 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aceshooting
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Lyra UI
|
|
2
|
+
|
|
3
|
+
Free, clean-room [Lit](https://lit.dev) web components — a companion to
|
|
4
|
+
[Web Awesome](https://webawesome.com) that provides open-source equivalents of several
|
|
5
|
+
Web Awesome **Pro** components, plus a few extras. Token-compatible with Web Awesome, so
|
|
6
|
+
the components look native inside a WA app, and fully usable standalone.
|
|
7
|
+
|
|
8
|
+
> **Independent project.** Not affiliated with or endorsed by Web Awesome. Component APIs
|
|
9
|
+
> intentionally mirror Web Awesome's public API (attributes, slots, events, CSS parts) so
|
|
10
|
+
> migration is a prefix rename — but every implementation here is original (clean-room).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @aceshooting/lyra-ui
|
|
16
|
+
# peer: Lit is bundled; Floating UI ships with the positioned components
|
|
17
|
+
# optional peer: @aceshooting/lyra-flags, only needed for <lyra-flag>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Import just what you use (tree-shakeable, granular entry points):
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import '@aceshooting/lyra-ui/components/combobox/combobox.js';
|
|
26
|
+
import '@aceshooting/lyra-ui/components/combobox/option.js';
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<lyra-combobox label="Fruit" with-clear>
|
|
31
|
+
<lyra-option value="a">Apple</lyra-option>
|
|
32
|
+
<lyra-option value="b">Banana</lyra-option>
|
|
33
|
+
</lyra-combobox>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
…or pull the whole library:
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
import '@aceshooting/lyra-ui';
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Imperative toast (a drop-in for `react-hot-toast`):
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { toast } from '@aceshooting/lyra-ui';
|
|
46
|
+
toast({ message: 'Saved', variant: 'success' });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Migrating from Web Awesome Pro
|
|
50
|
+
|
|
51
|
+
Everything a consumer touches is mirrored 1:1 — only the prefix differs:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
<wa-combobox value="x" multiple with-clear> → <lyra-combobox value="x" multiple with-clear>
|
|
55
|
+
<wa-date-input value="2026-07-15"> → <lyra-date-input value="2026-07-15">
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Theming
|
|
59
|
+
|
|
60
|
+
Components read Web Awesome's `--wa-*` design tokens (with `--lyra-*` fallbacks). Inside a
|
|
61
|
+
WA app they inherit your theme automatically; standalone, they use sensible defaults.
|
|
62
|
+
|
|
63
|
+
## Components
|
|
64
|
+
|
|
65
|
+
**v1 (this release)**
|
|
66
|
+
|
|
67
|
+
| Component | Mirrors | Notes |
|
|
68
|
+
|-----------|---------|-------|
|
|
69
|
+
| `<lyra-combobox>` + `<lyra-option>` | `wa-combobox` | Filterable single/multi select, form-associated |
|
|
70
|
+
| `<lyra-date-picker>` | `wa-date-picker` | Inline calendar, single + range |
|
|
71
|
+
| `<lyra-date-input>` | `wa-date-input` | Date field + calendar popover, form-associated |
|
|
72
|
+
| `<lyra-toast>` + `<lyra-toast-item>` + `toast()` | `wa-toast` / `wa-toast-item` | Stacking notifications |
|
|
73
|
+
| `<lyra-sparkline>` | `wa-sparkline` | Zero-dependency inline SVG |
|
|
74
|
+
| `<lyra-flag>` | — (extra) | Country/language flags for i18n pickers — needs the optional peer `@aceshooting/lyra-flags` |
|
|
75
|
+
|
|
76
|
+
**Roadmap** (survey-driven, see `docs/superpowers/specs/` at the repo root)
|
|
77
|
+
|
|
78
|
+
- **Dashboard atoms:** `lyra-table`, `lyra-stat`, `lyra-empty`, `lyra-skeleton`, `lyra-gauge`, `lyra-export-button`, `lyra-split`
|
|
79
|
+
- **Temporal + graph:** `lyra-time-range` + `lyra-playback`, `lyra-heatmap`, `lyra-graph`, `lyra-tree`
|
|
80
|
+
- **Charts** (mirror `wa-chart` family + box-plot/histogram; `chart.js` optional peer dep)
|
|
81
|
+
- **Bigger tracks:** `lyra-map` (maplibre), `lyra-file-input`
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
|
|
85
|
+
Run from the repo root (this package is part of a pnpm workspace):
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pnpm install
|
|
89
|
+
pnpm test # @web/test-runner + Playwright (Chromium) + axe a11y
|
|
90
|
+
pnpm lint # tsc --noEmit
|
|
91
|
+
pnpm build # tsc → dist/ (ESM + .d.ts)
|
|
92
|
+
pnpm manifest # custom-elements.json
|
|
93
|
+
pnpm docs # Vite playground, demos this package + lyra-flags together
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
[MIT](./LICENSE) © 2026 Aceshooting
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { type TemplateResult, type PropertyValues } from 'lit';
|
|
2
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
3
|
+
import { LyraOption } from './option.js';
|
|
4
|
+
import './option.js';
|
|
5
|
+
export type OptionFilter = (option: LyraOption, query: string) => boolean;
|
|
6
|
+
/**
|
|
7
|
+
* `<lyra-combobox>` — a filterable single/multi select that combines a text
|
|
8
|
+
* input with a listbox. Mirrors the core `<wa-combobox>` API under `lyra-`.
|
|
9
|
+
*
|
|
10
|
+
* Options are `<lyra-option value>` children. Emits native-style `change`/`input`
|
|
11
|
+
* (like Web Awesome) plus `lyra-show`/`lyra-hide`/`lyra-clear`.
|
|
12
|
+
*
|
|
13
|
+
* @customElement lyra-combobox
|
|
14
|
+
* @slot - `<lyra-option>` elements.
|
|
15
|
+
* @slot label - Custom label content.
|
|
16
|
+
* @slot hint - Custom hint content.
|
|
17
|
+
* @event change - The selection changed.
|
|
18
|
+
* @event input - The user typed in the filter or changed the selection.
|
|
19
|
+
* @event lyra-show - The listbox opened.
|
|
20
|
+
* @event lyra-hide - The listbox closed.
|
|
21
|
+
* @event lyra-clear - The value was cleared.
|
|
22
|
+
* @csspart combobox - The input container (positioning anchor).
|
|
23
|
+
* @csspart combobox-input - The text input.
|
|
24
|
+
* @csspart listbox - The options popover.
|
|
25
|
+
* @csspart option - An option row.
|
|
26
|
+
* @csspart tags - The multi-select tag container.
|
|
27
|
+
* @csspart tag - An individual selected tag.
|
|
28
|
+
* @csspart clear-button - The clear button.
|
|
29
|
+
* @csspart expand-icon - The dropdown indicator.
|
|
30
|
+
*/
|
|
31
|
+
export declare class LyraCombobox extends LyraElement {
|
|
32
|
+
static formAssociated: boolean;
|
|
33
|
+
static styles: import("lit").CSSResultGroup[];
|
|
34
|
+
static properties: {
|
|
35
|
+
value: {
|
|
36
|
+
noAccessor: boolean;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
multiple: boolean;
|
|
40
|
+
placeholder: string;
|
|
41
|
+
disabled: boolean;
|
|
42
|
+
required: boolean;
|
|
43
|
+
name: string;
|
|
44
|
+
label: string;
|
|
45
|
+
hint: string;
|
|
46
|
+
open: boolean;
|
|
47
|
+
withClear: boolean;
|
|
48
|
+
maxOptionsVisible: number;
|
|
49
|
+
emptyText: string;
|
|
50
|
+
filter: OptionFilter | null;
|
|
51
|
+
private query;
|
|
52
|
+
private activeIndex;
|
|
53
|
+
private options;
|
|
54
|
+
private internals;
|
|
55
|
+
private listId;
|
|
56
|
+
private cleanup?;
|
|
57
|
+
private _selected;
|
|
58
|
+
constructor();
|
|
59
|
+
/** The selected value(s): a string in single mode, a string[] in `multiple` mode. */
|
|
60
|
+
get value(): string | string[];
|
|
61
|
+
set value(next: string | string[]);
|
|
62
|
+
private syncFormValue;
|
|
63
|
+
formResetCallback(): void;
|
|
64
|
+
formDisabledCallback(disabled: boolean): void;
|
|
65
|
+
checkValidity(): boolean;
|
|
66
|
+
reportValidity(): boolean;
|
|
67
|
+
disconnectedCallback(): void;
|
|
68
|
+
private collectOptions;
|
|
69
|
+
private reflectSelected;
|
|
70
|
+
private labelFor;
|
|
71
|
+
private get filtered();
|
|
72
|
+
private get displayValue();
|
|
73
|
+
private show;
|
|
74
|
+
private hide;
|
|
75
|
+
private onDocPointer;
|
|
76
|
+
protected updated(changed: PropertyValues): void;
|
|
77
|
+
private pick;
|
|
78
|
+
private removeValue;
|
|
79
|
+
private clear;
|
|
80
|
+
private onInput;
|
|
81
|
+
private onKeyDown;
|
|
82
|
+
private onComboMouseDown;
|
|
83
|
+
private renderRows;
|
|
84
|
+
render(): TemplateResult;
|
|
85
|
+
}
|
|
86
|
+
declare global {
|
|
87
|
+
interface HTMLElementTagNameMap {
|
|
88
|
+
'lyra-combobox': LyraCombobox;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
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 { html } from 'lit';
|
|
8
|
+
import { property, state } from 'lit/decorators.js';
|
|
9
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
10
|
+
import { defineElement } from '../../internal/prefix.js';
|
|
11
|
+
import { place } from '../../internal/positioner.js';
|
|
12
|
+
import { nextId } from '../../internal/a11y.js';
|
|
13
|
+
import { styles } from './combobox.styles.js';
|
|
14
|
+
import { LyraOption } from './option.js';
|
|
15
|
+
import './option.js';
|
|
16
|
+
/**
|
|
17
|
+
* `<lyra-combobox>` — a filterable single/multi select that combines a text
|
|
18
|
+
* input with a listbox. Mirrors the core `<wa-combobox>` API under `lyra-`.
|
|
19
|
+
*
|
|
20
|
+
* Options are `<lyra-option value>` children. Emits native-style `change`/`input`
|
|
21
|
+
* (like Web Awesome) plus `lyra-show`/`lyra-hide`/`lyra-clear`.
|
|
22
|
+
*
|
|
23
|
+
* @customElement lyra-combobox
|
|
24
|
+
* @slot - `<lyra-option>` elements.
|
|
25
|
+
* @slot label - Custom label content.
|
|
26
|
+
* @slot hint - Custom hint content.
|
|
27
|
+
* @event change - The selection changed.
|
|
28
|
+
* @event input - The user typed in the filter or changed the selection.
|
|
29
|
+
* @event lyra-show - The listbox opened.
|
|
30
|
+
* @event lyra-hide - The listbox closed.
|
|
31
|
+
* @event lyra-clear - The value was cleared.
|
|
32
|
+
* @csspart combobox - The input container (positioning anchor).
|
|
33
|
+
* @csspart combobox-input - The text input.
|
|
34
|
+
* @csspart listbox - The options popover.
|
|
35
|
+
* @csspart option - An option row.
|
|
36
|
+
* @csspart tags - The multi-select tag container.
|
|
37
|
+
* @csspart tag - An individual selected tag.
|
|
38
|
+
* @csspart clear-button - The clear button.
|
|
39
|
+
* @csspart expand-icon - The dropdown indicator.
|
|
40
|
+
*/
|
|
41
|
+
export class LyraCombobox extends LyraElement {
|
|
42
|
+
static { this.formAssociated = true; }
|
|
43
|
+
static { this.styles = [LyraElement.styles, styles]; }
|
|
44
|
+
static { this.properties = {
|
|
45
|
+
value: { noAccessor: true },
|
|
46
|
+
}; }
|
|
47
|
+
constructor() {
|
|
48
|
+
super();
|
|
49
|
+
this.multiple = false;
|
|
50
|
+
this.placeholder = '';
|
|
51
|
+
this.disabled = false;
|
|
52
|
+
this.required = false;
|
|
53
|
+
this.name = '';
|
|
54
|
+
this.label = '';
|
|
55
|
+
this.hint = '';
|
|
56
|
+
this.open = false;
|
|
57
|
+
this.withClear = false;
|
|
58
|
+
this.maxOptionsVisible = 3;
|
|
59
|
+
this.emptyText = 'No results';
|
|
60
|
+
this.filter = null;
|
|
61
|
+
this.query = '';
|
|
62
|
+
this.activeIndex = -1;
|
|
63
|
+
this.options = [];
|
|
64
|
+
this.listId = nextId('combobox-list');
|
|
65
|
+
this._selected = [];
|
|
66
|
+
this.collectOptions = (e) => {
|
|
67
|
+
const slot = e.target;
|
|
68
|
+
this.options = slot
|
|
69
|
+
.assignedElements({ flatten: true })
|
|
70
|
+
.filter((el) => el instanceof LyraOption);
|
|
71
|
+
this.reflectSelected();
|
|
72
|
+
};
|
|
73
|
+
this.onDocPointer = (e) => {
|
|
74
|
+
if (!e.composedPath().includes(this))
|
|
75
|
+
this.hide();
|
|
76
|
+
};
|
|
77
|
+
this.onInput = (e) => {
|
|
78
|
+
this.query = e.target.value;
|
|
79
|
+
this.activeIndex = -1;
|
|
80
|
+
this.show();
|
|
81
|
+
this.emit('input');
|
|
82
|
+
};
|
|
83
|
+
this.onKeyDown = (e) => {
|
|
84
|
+
const opts = this.filtered.filter((o) => !o.disabled);
|
|
85
|
+
switch (e.key) {
|
|
86
|
+
case 'ArrowDown':
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
if (!this.open)
|
|
89
|
+
return this.show();
|
|
90
|
+
this.activeIndex = Math.min(opts.length - 1, this.activeIndex + 1);
|
|
91
|
+
break;
|
|
92
|
+
case 'ArrowUp':
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
this.activeIndex = Math.max(0, this.activeIndex - 1);
|
|
95
|
+
break;
|
|
96
|
+
case 'Enter':
|
|
97
|
+
if (this.open && this.activeIndex >= 0 && opts[this.activeIndex]) {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
this.pick(opts[this.activeIndex]);
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
case 'Escape':
|
|
103
|
+
if (this.open) {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
this.hide();
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
case 'Home':
|
|
109
|
+
if (this.open) {
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
this.activeIndex = 0;
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
case 'End':
|
|
115
|
+
if (this.open) {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
this.activeIndex = opts.length - 1;
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
case 'Backspace':
|
|
121
|
+
if (this.multiple && !this.query && this._selected.length) {
|
|
122
|
+
this.removeValue(this._selected[this._selected.length - 1]);
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
this.onComboMouseDown = (e) => {
|
|
128
|
+
if (this.disabled)
|
|
129
|
+
return;
|
|
130
|
+
if (e.target.closest('button'))
|
|
131
|
+
return;
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
this.renderRoot.querySelector('[part="combobox-input"]')?.focus();
|
|
134
|
+
this.show();
|
|
135
|
+
};
|
|
136
|
+
this.internals = this.attachInternals();
|
|
137
|
+
}
|
|
138
|
+
/** The selected value(s): a string in single mode, a string[] in `multiple` mode. */
|
|
139
|
+
get value() {
|
|
140
|
+
return this.multiple ? [...this._selected] : (this._selected[0] ?? '');
|
|
141
|
+
}
|
|
142
|
+
set value(next) {
|
|
143
|
+
const old = this._selected;
|
|
144
|
+
this._selected = Array.isArray(next) ? [...next] : next ? [next] : [];
|
|
145
|
+
this.syncFormValue();
|
|
146
|
+
this.reflectSelected();
|
|
147
|
+
this.requestUpdate('value', old);
|
|
148
|
+
}
|
|
149
|
+
syncFormValue() {
|
|
150
|
+
if (this.multiple) {
|
|
151
|
+
const fd = new FormData();
|
|
152
|
+
const key = this.name || 'value';
|
|
153
|
+
for (const v of this._selected)
|
|
154
|
+
fd.append(key, v);
|
|
155
|
+
this.internals.setFormValue(fd);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
this.internals.setFormValue(this._selected[0] ?? '');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
formResetCallback() {
|
|
162
|
+
this.value = [];
|
|
163
|
+
this.query = '';
|
|
164
|
+
}
|
|
165
|
+
formDisabledCallback(disabled) {
|
|
166
|
+
this.disabled = disabled;
|
|
167
|
+
}
|
|
168
|
+
checkValidity() {
|
|
169
|
+
return this.internals.checkValidity();
|
|
170
|
+
}
|
|
171
|
+
reportValidity() {
|
|
172
|
+
return this.internals.reportValidity();
|
|
173
|
+
}
|
|
174
|
+
disconnectedCallback() {
|
|
175
|
+
super.disconnectedCallback();
|
|
176
|
+
this.cleanup?.();
|
|
177
|
+
document.removeEventListener('pointerdown', this.onDocPointer);
|
|
178
|
+
}
|
|
179
|
+
reflectSelected() {
|
|
180
|
+
const sel = new Set(this._selected);
|
|
181
|
+
for (const o of this.options)
|
|
182
|
+
o.selected = sel.has(o.value);
|
|
183
|
+
}
|
|
184
|
+
labelFor(value) {
|
|
185
|
+
return this.options.find((o) => o.value === value)?.label ?? value;
|
|
186
|
+
}
|
|
187
|
+
get filtered() {
|
|
188
|
+
const q = this.query.trim().toLowerCase();
|
|
189
|
+
const selectedLabel = !this.multiple ? (this.labelFor(this._selected[0] ?? '') ?? '') : '';
|
|
190
|
+
const effective = q && q === selectedLabel.toLowerCase() ? '' : q;
|
|
191
|
+
if (!effective)
|
|
192
|
+
return this.options;
|
|
193
|
+
const fn = this.filter ??
|
|
194
|
+
((o, query) => o.label.toLowerCase().includes(query) || o.searchText.toLowerCase().includes(query));
|
|
195
|
+
return this.options.filter((o) => fn(o, effective));
|
|
196
|
+
}
|
|
197
|
+
get displayValue() {
|
|
198
|
+
if (this.multiple || this.open)
|
|
199
|
+
return this.query;
|
|
200
|
+
return this._selected[0] ? this.labelFor(this._selected[0]) : '';
|
|
201
|
+
}
|
|
202
|
+
show() {
|
|
203
|
+
if (this.open || this.disabled)
|
|
204
|
+
return;
|
|
205
|
+
this.open = true;
|
|
206
|
+
this.emit('lyra-show');
|
|
207
|
+
document.addEventListener('pointerdown', this.onDocPointer);
|
|
208
|
+
}
|
|
209
|
+
hide() {
|
|
210
|
+
if (!this.open)
|
|
211
|
+
return;
|
|
212
|
+
this.open = false;
|
|
213
|
+
this.activeIndex = -1;
|
|
214
|
+
this.emit('lyra-hide');
|
|
215
|
+
document.removeEventListener('pointerdown', this.onDocPointer);
|
|
216
|
+
}
|
|
217
|
+
updated(changed) {
|
|
218
|
+
if (changed.has('open')) {
|
|
219
|
+
this.cleanup?.();
|
|
220
|
+
this.cleanup = undefined;
|
|
221
|
+
if (this.open) {
|
|
222
|
+
const anchor = this.renderRoot.querySelector('[part="combobox"]');
|
|
223
|
+
const listbox = this.renderRoot.querySelector('[part="listbox"]');
|
|
224
|
+
if (anchor && listbox)
|
|
225
|
+
this.cleanup = place(anchor, listbox);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
pick(option) {
|
|
230
|
+
if (option.disabled)
|
|
231
|
+
return;
|
|
232
|
+
if (this.multiple) {
|
|
233
|
+
const set = new Set(this._selected);
|
|
234
|
+
set.has(option.value) ? set.delete(option.value) : set.add(option.value);
|
|
235
|
+
this.value = [...set];
|
|
236
|
+
this.query = '';
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
this.value = option.value;
|
|
240
|
+
this.query = '';
|
|
241
|
+
this.hide();
|
|
242
|
+
}
|
|
243
|
+
this.emit('input');
|
|
244
|
+
this.emit('change');
|
|
245
|
+
}
|
|
246
|
+
removeValue(value) {
|
|
247
|
+
this.value = this._selected.filter((v) => v !== value);
|
|
248
|
+
this.emit('change');
|
|
249
|
+
}
|
|
250
|
+
clear() {
|
|
251
|
+
this.value = [];
|
|
252
|
+
this.query = '';
|
|
253
|
+
this.emit('lyra-clear');
|
|
254
|
+
this.emit('change');
|
|
255
|
+
}
|
|
256
|
+
renderRows(filtered, activeId) {
|
|
257
|
+
const rows = [];
|
|
258
|
+
let currentGroup;
|
|
259
|
+
let optIndex = 0;
|
|
260
|
+
filtered.forEach((o) => {
|
|
261
|
+
if (o.group !== currentGroup) {
|
|
262
|
+
currentGroup = o.group;
|
|
263
|
+
if (currentGroup)
|
|
264
|
+
rows.push(html `<div class="group-label">${currentGroup}</div>`);
|
|
265
|
+
}
|
|
266
|
+
const id = `${this.listId}-opt-${optIndex++}`;
|
|
267
|
+
const selected = this._selected.includes(o.value);
|
|
268
|
+
rows.push(html `<div
|
|
269
|
+
part="option"
|
|
270
|
+
id=${id}
|
|
271
|
+
role="option"
|
|
272
|
+
aria-selected=${selected ? 'true' : 'false'}
|
|
273
|
+
aria-disabled=${o.disabled ? 'true' : 'false'}
|
|
274
|
+
?data-active=${id === activeId}
|
|
275
|
+
@mousedown=${(e) => e.preventDefault()}
|
|
276
|
+
@click=${() => this.pick(o)}
|
|
277
|
+
>
|
|
278
|
+
<span>${o.label}</span>
|
|
279
|
+
</div>`);
|
|
280
|
+
});
|
|
281
|
+
return rows;
|
|
282
|
+
}
|
|
283
|
+
render() {
|
|
284
|
+
const filtered = this.filtered;
|
|
285
|
+
const activeOpts = filtered.filter((o) => !o.disabled);
|
|
286
|
+
const active = this.activeIndex >= 0 ? activeOpts[this.activeIndex] : undefined;
|
|
287
|
+
const activeId = active ? `${this.listId}-opt-${filtered.indexOf(active)}` : '';
|
|
288
|
+
const shownTags = this.multiple ? this._selected.slice(0, this.maxOptionsVisible) : [];
|
|
289
|
+
const extra = this.multiple ? this._selected.length - shownTags.length : 0;
|
|
290
|
+
const hasValue = this._selected.length > 0;
|
|
291
|
+
return html `
|
|
292
|
+
<div part="form-control">
|
|
293
|
+
<label part="form-control-label">${this.label}<slot name="label"></slot></label>
|
|
294
|
+
<div part="combobox" @mousedown=${this.onComboMouseDown}>
|
|
295
|
+
<div part="tags" style="display:contents">
|
|
296
|
+
${shownTags.map((v) => html `<span part="tag"
|
|
297
|
+
>${this.labelFor(v)}<button
|
|
298
|
+
part="tag__remove-button"
|
|
299
|
+
type="button"
|
|
300
|
+
aria-label="Remove ${this.labelFor(v)}"
|
|
301
|
+
@click=${(e) => {
|
|
302
|
+
e.stopPropagation();
|
|
303
|
+
this.removeValue(v);
|
|
304
|
+
}}
|
|
305
|
+
>
|
|
306
|
+
×</button
|
|
307
|
+
></span
|
|
308
|
+
>`)}
|
|
309
|
+
${extra > 0 ? html `<span part="tag">+${extra}</span>` : ''}
|
|
310
|
+
</div>
|
|
311
|
+
<input
|
|
312
|
+
part="combobox-input"
|
|
313
|
+
role="combobox"
|
|
314
|
+
aria-label=${this.label || this.placeholder || 'Combobox'}
|
|
315
|
+
aria-expanded=${this.open ? 'true' : 'false'}
|
|
316
|
+
aria-controls=${this.listId}
|
|
317
|
+
aria-activedescendant=${activeId}
|
|
318
|
+
aria-autocomplete="list"
|
|
319
|
+
autocomplete="off"
|
|
320
|
+
.value=${this.displayValue}
|
|
321
|
+
placeholder=${hasValue && !this.multiple ? '' : this.placeholder}
|
|
322
|
+
?disabled=${this.disabled}
|
|
323
|
+
@input=${this.onInput}
|
|
324
|
+
@keydown=${this.onKeyDown}
|
|
325
|
+
@focus=${() => this.show()}
|
|
326
|
+
/>
|
|
327
|
+
${this.withClear && hasValue
|
|
328
|
+
? html `<button
|
|
329
|
+
part="clear-button"
|
|
330
|
+
type="button"
|
|
331
|
+
aria-label="Clear"
|
|
332
|
+
@click=${(e) => {
|
|
333
|
+
e.stopPropagation();
|
|
334
|
+
this.clear();
|
|
335
|
+
}}
|
|
336
|
+
>
|
|
337
|
+
×
|
|
338
|
+
</button>`
|
|
339
|
+
: ''}
|
|
340
|
+
<span part="expand-icon" aria-hidden="true">▾</span>
|
|
341
|
+
</div>
|
|
342
|
+
<div part="listbox" id=${this.listId} role="listbox" aria-multiselectable=${this.multiple ? 'true' : 'false'}>
|
|
343
|
+
${filtered.length === 0
|
|
344
|
+
? html `<div class="empty">${this.emptyText}</div>`
|
|
345
|
+
: this.renderRows(filtered, activeId)}
|
|
346
|
+
</div>
|
|
347
|
+
<div part="hint">${this.hint}<slot name="hint"></slot></div>
|
|
348
|
+
</div>
|
|
349
|
+
<slot @slotchange=${this.collectOptions} hidden></slot>
|
|
350
|
+
`;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
__decorate([
|
|
354
|
+
property({ type: Boolean, reflect: true })
|
|
355
|
+
], LyraCombobox.prototype, "multiple", void 0);
|
|
356
|
+
__decorate([
|
|
357
|
+
property()
|
|
358
|
+
], LyraCombobox.prototype, "placeholder", void 0);
|
|
359
|
+
__decorate([
|
|
360
|
+
property({ type: Boolean, reflect: true })
|
|
361
|
+
], LyraCombobox.prototype, "disabled", void 0);
|
|
362
|
+
__decorate([
|
|
363
|
+
property({ type: Boolean, reflect: true })
|
|
364
|
+
], LyraCombobox.prototype, "required", void 0);
|
|
365
|
+
__decorate([
|
|
366
|
+
property()
|
|
367
|
+
], LyraCombobox.prototype, "name", void 0);
|
|
368
|
+
__decorate([
|
|
369
|
+
property()
|
|
370
|
+
], LyraCombobox.prototype, "label", void 0);
|
|
371
|
+
__decorate([
|
|
372
|
+
property()
|
|
373
|
+
], LyraCombobox.prototype, "hint", void 0);
|
|
374
|
+
__decorate([
|
|
375
|
+
property({ type: Boolean, reflect: true })
|
|
376
|
+
], LyraCombobox.prototype, "open", void 0);
|
|
377
|
+
__decorate([
|
|
378
|
+
property({ type: Boolean, attribute: 'with-clear' })
|
|
379
|
+
], LyraCombobox.prototype, "withClear", void 0);
|
|
380
|
+
__decorate([
|
|
381
|
+
property({ attribute: 'max-options-visible', type: Number })
|
|
382
|
+
], LyraCombobox.prototype, "maxOptionsVisible", void 0);
|
|
383
|
+
__decorate([
|
|
384
|
+
property({ attribute: 'empty-text' })
|
|
385
|
+
], LyraCombobox.prototype, "emptyText", void 0);
|
|
386
|
+
__decorate([
|
|
387
|
+
property({ attribute: false })
|
|
388
|
+
], LyraCombobox.prototype, "filter", void 0);
|
|
389
|
+
__decorate([
|
|
390
|
+
state()
|
|
391
|
+
], LyraCombobox.prototype, "query", void 0);
|
|
392
|
+
__decorate([
|
|
393
|
+
state()
|
|
394
|
+
], LyraCombobox.prototype, "activeIndex", void 0);
|
|
395
|
+
__decorate([
|
|
396
|
+
state()
|
|
397
|
+
], LyraCombobox.prototype, "options", void 0);
|
|
398
|
+
defineElement('combobox', LyraCombobox);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import("lit").CSSResult;
|