@keenmate/web-multiselect 2.0.0-rc10 → 2.0.0-rc12

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 CHANGED
@@ -1,186 +1,186 @@
1
- # @keenmate/web-multiselect
2
-
3
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
- [![npm version](https://img.shields.io/npm/v/@keenmate/web-multiselect.svg)](https://www.npmjs.com/package/@keenmate/web-multiselect)
5
-
6
- > A lightweight, themeable multi-select web component with typeahead search, RTL support, rich content, and full keyboard navigation.
7
-
8
- ## What is it
9
-
10
- `@keenmate/web-multiselect` is a custom element (`<web-multiselect>`) that turns a list of options into a searchable, themeable multi-select dropdown. Framework-agnostic — works in React, Vue, Svelte, Blazor, plain HTML.
11
-
12
- Reads `--base-*` variables from the page if [`@keenmate/theme-designer`](https://theme-designer.keenmate.dev) is present, falls back to sensible OS-aware defaults otherwise, and ships with first-class dark-mode and per-instance theming.
13
-
14
- **Headline features:**
15
-
16
- - Declarative `<option>` / `<optgroup>` markup — no JavaScript required for simple cases.
17
- - Virtual scrolling for 10,000+ option datasets (25× faster opening, 99.8% memory reduction).
18
- - Filter or navigate search modes; async / hybrid search.
19
- - Five badge display modes (pills, count, compact, partial, none) with positioning on any side.
20
- - Full keyboard navigation, RTL language support, badge tooltips.
21
- - Custom rendering callbacks for options, badges, and group headers.
22
- - Form integration via standard hidden inputs (FormData-compatible).
23
-
24
- ## What's New in v2.0.0-rc10
25
-
26
- - **`collapse-badges-below` — container-responsive badge collapse** — A picker in a narrow column could overflow with pills even on a wide monitor, because badge behavior keyed off the *window*, not the control. This new opt-in attribute/property (off by default) makes the control watch its **own border box** via the core `resized` hook (a shared page-wide `ResizeObserver`) and collapse `badges-display-mode` to `count` ("N selected") while the box is narrower than the given pixel width, restoring the configured mode when it widens back. The override is applied to the live picker only — never written to your config — so custom modes (`compact`, `partial`) come back exactly, it survives a structural rebuild, and the hook is throttled so a drag-resize reflows a bounded number of times. It's a separate axis from `mobile-presentation` and composes with it. New demo on `examples-responsive.html`.
27
-
28
- - **`renderBadgeCallback` — own the whole badge, not just its content** — `renderBadgeContentCallback` only fills the built-in pill, so a selected item couldn't become, say, a full card. The new property-only callback returns the *entire* badge markup; the component wraps it in a `.ms__badge.ms__badge--custom` element carrying `data-value` (the modifier drops the pill's fixed height / overflow / radius so a card lays out freely) and delegates removal to any inner element with `data-action="remove"` (or the built-in `.ms__badge-remove`) — value resolved from the wrapper, so no event wiring. It falls back to the default pill when the callback returns null/empty, and `getBadgeClassCallback` classes still land on the wrapper. New Custom Rendering demo: an icon-pack picker where each selection is a card with its own Remove button.
29
-
30
- - **`enable-selected-popover` — opt out of the selected-items popover** — When you render your own selection UI (suppress badges with `badges-display-mode="none"`, keep only the in-field `[N]` counter, and drive a panel from the `change` event), the built-in popover that opened on clicking the counter or badge was pointless. This new boolean (default `true`, so nothing changes by default) gates `showPopover()` at the source — every trigger (badge click, counter click, "+X more", keyboard) becomes a no-op — and adds a `ms--no-selected-popover` host class that drops the pointer cursor so those affordances no longer advertise as openable. New Custom Rendering demo: a playlist builder that owns its selection panel end to end.
31
-
32
- - **Device-detection helpers re-exported from the package entry** — Per-device configuration used to mean pulling in the core separately. `observeEnvironment`, `classifyDevice`, `getEnvironment`, `observeViewport`, `configureBreakpoints`, and `TABLET_MIN_SHORT_SIDE` (plus the `EnvironmentSnapshot` / `DeviceClass` types) are now re-exported from `@keenmate/web-multiselect` — the same device signal the component reacts to internally — so you get it from one import and one dependency. The pattern is plain: react to the event and assign a different `actionButtons` set on mobile vs. desktop. Demonstrated in `examples-action-buttons.html` §10, which previously crammed 12 buttons into one unreadable row on phones.
33
-
34
- - **Fullscreen overlay warns when an ancestor mis-anchors it** — The phone overlay is a `position: fixed` full-viewport sheet, so a `transform` / `perspective` / `filter` / `backdrop-filter` / `will-change` on any ancestor of the host anchors it to that ancestor's box instead of the viewport — and it silently stops covering the screen. The floating dropdown already surfaced this via drift detection; the fullscreen path had no equivalent. It now checks core's containing-block heuristic when the sheet opens and, if the true offset parent is an element rather than the viewport, emits a once-per-instance `console.warn` naming the culprit and the fix. Note the asymmetry: an ancestor `transform` is harmless for the floating dropdown but breaks the sheet; `contain` / `container-type` don't break the sheet at all. Documented on the Positioning Edge Cases page (new PO05 card).
35
-
36
- - **Dropdown corners — a focused first/last row no longer pokes a square corner past the rounded panel** — The panel clips with `overflow: hidden` + `border-radius`, but a row's focus `outline` and background trace the row's own box and follow its own radius, not an ancestor's clip, so the top/bottom rows' square corners bled through the rounded panel corner. The fix rounds the inner scroll wrapper to a new themeable `--ms-dropdown-inner-border-radius` (panel radius − border width, clamped at 0) and rounds the actual top/bottom rows on every render — in DOM order (so a grouped list rounds the top group label, not the first option), with logical corners so it mirrors in RTL, keeping the scrollbar-side corners square, and staying correct under virtual scrolling.
37
-
38
- - **Example pages — coded section headings, a Data & API split, and filename alignment** — Every example section now carries a short code in its heading (page-prefix + ordinal, e.g. `DA01`, `API03`), mirroring the showcase index, with the decorative emoji removed. The old `examples-classic.html` kitchen sink was split: genuine data/API content stays in the renamed `examples-data-api.html` (DA01–05 + API01–06), and its basic/cross-cutting demos moved to a new `examples-basic.html` (BU01–08). Three more pages were renamed to match their titles (`performance` → `virtual-scrolling`, `search-index` → `external-search`, `templating` → `custom-rendering`), and `index.html` and the docs links were repointed.
39
-
40
- ## What's New in v2.0.0-rc09
41
-
42
- - **Fullscreen header — the close (✕) button no longer wraps to a second line** — On narrower phones the ✕ could drop below the search field instead of sharing its row, and only on *some* devices. The overlay header is `flex-wrap: wrap` (so the navigate-mode match-nav row can drop below), and flex chooses which items share a line from each item's flex-basis *before* shrinking — so the leading search wrapper's `flex: 1 1 auto` reserved its full content width and pushed the ✕ over the edge even though it was fully shrinkable. Switching the wrapper (and the selected-items popover header) to `flex: 1 1 0` keeps the ✕ on the header row at every width while flex-grow still fills the bar.
43
-
44
- - **Fullscreen header — the search-mode toggle and the close button are now symmetric** — With `show-search-mode-toggle` enabled, the leading magnifier/funnel toggle and the trailing ✕ sat at different distances from their edges: the ✕ is nudged toward the trailing edge but the toggle had no matching inset and used a smaller gap. The toggle now takes a mirrored `margin-inline-start` that lands its drawn glyph centre the same distance from the leading edge as the ✕'s is from the trailing edge (compensating for the toggle being a smaller chip), and its gap defaults to the header gap — measured glyph centres now both land 26.4px in.
45
-
46
- - **Fullscreen action buttons align with the rest of the content** — Select All / Clear All started ~0.4rem further out than the search field and option checkboxes, because the actions row used a uniform 0.8rem padding while the header and options use a 1.2rem horizontal gutter. The overlay now sets `--ms-actions-padding: 0.8rem 1.2rem`, so the buttons' outer edges line up on the same vertical edge as everything above them.
47
-
48
- - **Consistent Lucide `x` close icon** — `--ms-icon-remove` (the badge remove × and the fullscreen/popover close ✕) was a hand-drawn X at stroke-width 2.5; it's now Lucide's exact `x` (stroke-width 2, round joins) so the whole icon set stays Lucide-consistent with the magnifier, funnel, and `search-x`. Cosmetic only, still themeable via `--ms-icon-remove`.
49
-
50
- ## Demos & docs
51
-
52
- - 🚀 [Live demo](https://web-multiselect.keenmate.dev)
53
- - 📘 [Usage / API reference](./docs/usage.md) — attributes, properties, methods, events.
54
- - 🎨 [Theming](./docs/theming.md) — `--ms-*` variables, dark mode, cascade layers, Theme Designer integration.
55
- - 📚 [Examples / cookbook](./docs/examples.md) — rich content, async search, virtual scroll, custom rendering, forms.
56
- - ♿ [Accessibility](./docs/accessibility.md) — keyboard model, ARIA labels, focus behavior.
57
-
58
- ## Install
59
-
60
- ```bash
61
- npm install @keenmate/web-multiselect
62
- ```
63
-
64
- ## Quick start
65
-
66
- **Declarative — no JavaScript required:**
67
-
68
- ```html
69
- <script type="module">
70
- import '@keenmate/web-multiselect';
71
- </script>
72
-
73
- <web-multiselect placeholder="Pick a country">
74
- <option value="cz">Czech Republic</option>
75
- <option value="sk">Slovakia</option>
76
- <option value="at">Austria</option>
77
- </web-multiselect>
78
- ```
79
-
80
- **Programmatic — dynamic data + events:**
81
-
82
- ```html
83
- <web-multiselect id="picker" search-placeholder="Search…"></web-multiselect>
84
-
85
- <script type="module">
86
- import '@keenmate/web-multiselect';
87
-
88
- const picker = document.getElementById('picker');
89
- picker.options = [
90
- { value: 'js', label: 'JavaScript', icon: '🟨' },
91
- { value: 'ts', label: 'TypeScript', icon: '🔷' },
92
- { value: 'py', label: 'Python', icon: '🐍' }
93
- ];
94
-
95
- picker.addEventListener('change', (e) => {
96
- console.log('Selected:', e.detail.selectedValues);
97
- });
98
- </script>
99
- ```
100
-
101
- See [docs/usage.md](./docs/usage.md) for the full API and [docs/examples.md](./docs/examples.md) for advanced patterns (async data, virtual scrolling, custom rendering, form integration).
102
-
103
- ## Editor IntelliSense
104
-
105
- The package ships editor metadata so you get autocomplete and hover docs for the
106
- element's attributes, events, and all `--ms-*` CSS custom properties. All of it is
107
- generated from the component's source on every build, so it never drifts.
108
-
109
- - **JetBrains** (WebStorm / IntelliJ) — works automatically. The IDE discovers
110
- `web-types.json` via the `web-types` field in `package.json`; no setup needed.
111
- - **VS Code** — the data files ship but VS Code doesn't auto-discover them from a
112
- dependency, so point your workspace at them once in `.vscode/settings.json`:
113
-
114
- ```json
115
- {
116
- "html.customData": [
117
- "./node_modules/@keenmate/web-multiselect/vscode.html-custom-data.json"
118
- ],
119
- "css.customData": [
120
- "./node_modules/@keenmate/web-multiselect/vscode.css-custom-data.json"
121
- ]
122
- }
123
- ```
124
-
125
- `html.customData` powers tag/attribute completion on `<web-multiselect>`;
126
- `css.customData` powers completion for the `--ms-*` theming variables. Reload
127
- the window after adding them.
128
-
129
- ## Browser support
130
-
131
- Modern evergreen browsers — anything with native `customElements`, Shadow DOM, and CSS `@layer` support:
132
-
133
- - Chrome / Edge 99+
134
- - Firefox 97+
135
- - Safari 15.4+
136
-
137
- No polyfills are shipped. SSR-safe: the module imports without crashing in Node, but renders only after hydration in the browser.
138
-
139
- ## Development
140
-
141
- ```bash
142
- # Install dependencies
143
- npm install
144
-
145
- # Start dev server (HMR)
146
- npm run dev
147
-
148
- # Build for production
149
- npm run build
150
-
151
- # Create package tarball
152
- npm run package
153
-
154
- # Run tests
155
- npm run test:unit # Vitest (happy-dom) — fast logic checks
156
- npm run test:e2e # Playwright (browser) — interaction/visual
157
- npm test # both
158
- ```
159
-
160
- ## Code structure
161
-
162
- Follows the BlissFramework four-layer web-component layout:
163
-
164
- | Layer | File | Role |
165
- |-------|------|------|
166
- | Element | `src/web-component.ts` | `MultiSelectElement` — custom-element I/O wrapper, `ATTRIBUTE_TABLE`-driven |
167
- | Logic | `src/multiselect.ts` | `WebMultiSelect<T>` — framework-agnostic core |
168
- | Service | `src/tooltip.ts`, `src/virtual-scroll.ts` | single-purpose helpers (`Tooltip`, `VirtualScroll`) |
169
- | Side | `src/types.ts`, `src/logger.ts`, `src/vendor/` | types, logging, vendored deps |
170
-
171
- Two deviations from the canonical shape, both intentional:
172
-
173
- - **`MultiSelectElement extends BaseElement`, not `HTMLElement` directly.** `BaseElement` is a local `const` resolving to `HTMLElement` in the browser and to a stub class under SSR (`typeof HTMLElement === 'undefined'`), so importing the module in Node doesn't throw. A literal `grep "extends HTMLElement"` structure check will not match here by design.
174
- - **`src/vite-env.d.ts`** is a standard Vite ambient-types file, not part of the four-layer model.
175
-
176
- ## License
177
-
178
- MIT — see [LICENSE](./LICENSE).
179
-
180
- ## Built with BlissFramework
181
-
182
- Follows the [BlissFramework component guidelines](https://blissframework.dev/) for structure, theming, color-scheme, and accessibility. Per-check verifications run via `/validate-web-component`.
183
-
184
- ## Credits
185
-
186
- Created by [Keenmate](https://github.com/keenmate) as part of the Pure Admin design system.
1
+ # @keenmate/web-multiselect
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
+ [![npm version](https://img.shields.io/npm/v/@keenmate/web-multiselect.svg)](https://www.npmjs.com/package/@keenmate/web-multiselect)
5
+
6
+ > A lightweight, themeable multi-select web component with typeahead search, RTL support, rich content, and full keyboard navigation.
7
+
8
+ ## What is it
9
+
10
+ `@keenmate/web-multiselect` is a custom element (`<web-multiselect>`) that turns a list of options into a searchable, themeable multi-select dropdown. Framework-agnostic — works in React, Vue, Svelte, Blazor, plain HTML.
11
+
12
+ Reads `--base-*` variables from the page if [`@keenmate/theme-designer`](https://theme-designer.keenmate.dev) is present, falls back to sensible OS-aware defaults otherwise, and ships with first-class dark-mode and per-instance theming.
13
+
14
+ **Headline features:**
15
+
16
+ - Declarative `<option>` / `<optgroup>` markup — no JavaScript required for simple cases.
17
+ - Virtual scrolling for 10,000+ option datasets (25× faster opening, 99.8% memory reduction).
18
+ - Filter or navigate search modes; async / hybrid search.
19
+ - Five badge display modes (pills, count, compact, partial, none) with positioning on any side.
20
+ - Full keyboard navigation, RTL language support, badge tooltips.
21
+ - Custom rendering callbacks for options, badges, and group headers.
22
+ - Form integration via standard hidden inputs (FormData-compatible).
23
+
24
+ ## What's New in v2.0.0-rc12
25
+
26
+ - **`overlay-group` — one overlay open at a time, across components.** The dropdown now joins a cross-component single-active-overlay group (core `registerOverlay`): opening it dismisses every other participating overlay — other multiselects, datepickers, or any external popover that fires the `km-overlay-activated` document event — and it closes itself when another overlay in its group opens. The new `overlay-group` attribute/property scopes this to a named group (same group = coordinate, different groups = independent, unset = the default ungrouped pool). Outside-click dismissal is unchanged and always on; this only governs the open-broadcast. Fixes the old behavior where two multiselects could sit open simultaneously.
27
+
28
+ - **Icon glyphs now flow from the shared `--base-icon-*` contract.** The four `--ms-icon-*` glyphs with a shared counterpart (chevron, field-clear, badge-remove, search) are wired through the `@keenmate/base-css-variables` layer, so a single `--base-icon-*` override re-skins that affordance across every KeenMate component at once — matching how the ~95 other `--ms-*` tokens already fall back to `--base-*`. The toggle/pager chevron now flows from `--base-icon-chevron` (a Lucide angle that keeps the previous optical size); field-clear, badge-remove, and search follow their base equivalents. Each keeps its inline Lucide SVG as the standalone fallback, so default appearance is unchanged when no base layer is loaded.
29
+
30
+ - **`--ms-toggle-rotate-closed` / `--ms-toggle-rotate-open` — themeable chevron rotation.** The toggle rotates the *directional* base chevron into place (defaults `90deg` closed → down, `-90deg` open → up). A theme that supplies a **pre-oriented** glyph (one that already points down) can now opt out: set both to `0deg` for a static icon, or `0deg` / `180deg` for a down-glyph that flips up on open — without touching the base contract. The new Material Design card in `examples-theming.html` demonstrates the pairing.
31
+
32
+ - **`--ms-fullscreen-nav-btn-icon` — the pager glyph is now independent.** The fullscreen match-navigator's prev/next buttons previously masked the shared `--ms-icon-chevron`, so a theme that repointed the base chevron at a pre-oriented toggle glyph would leak it into the pager (which rotates its source ±90° and expects a right-pointing chevron). The pager now reads its own token, defaulting to `--ms-icon-chevron` — nothing changes by default, but a theme can diverge the pager glyph on its own.
33
+
34
+ - **More tokens flow from `--base-*` for dark-mode fidelity.** A follow-up audit wired five more `--ms-*` tokens that hardcoded a value where a dedicated `--base-*` counterpart exists: disabled-input background and dropdown box-shadow are now `light-dark()`-aware (they no longer stay light on dark themes), the field-clear color/hover knobs gain dedicated base hooks, and the snappy easing matches the base standard curve. All keep their prior value as the standalone fallback; transition *durations* and the z-index stack are deliberately left local.
35
+
36
+ - **Dropdown / selected-items popover no longer render 2px wider than the field.** Both panels size from `--ms-input-current-width` (the field wrapper's border-box `offsetWidth`) but were themselves `content-box`, so each added its own 1px border on top and overhung the input it anchors to. Both now use `box-sizing: border-box`, so their outer width matches the field exactly.
37
+
38
+ ## What's New in v2.0.0-rc11
39
+
40
+ - **Imperative open/close API — drive the dropdown from code.** The `<web-multiselect>` element and the underlying `WebMultiSelect` now expose `open()`, `close()`, `toggle()`, and a read/write `isOpen` property, mirroring the calendar API in web-daterangepicker. Each element method flushes pending property writes first (the same contract as `getSelected()`/`setSelected()`), so `el.options = data; el.open()` works with no `await` in between. Calling `open()` from your own button's click handler now opens *and stays open* — previously the same click bubbled to the outside-click listener and re-closed it. See the new `examples-data-api.html` §API07 demo.
41
+
42
+ - **Inline clear (✕) button — wipe the whole selection from inside the input.** A new opt-in `show-clear` attribute renders a small ✕ at the input's trailing edge that appears only while something is selected. Clicking it clears the selection and any search text, fires a single `change`, refocuses the input, and closes the selected-items popover if it was open — without popping the dropdown open. It's drawn as a themeable CSS mask icon (`--ms-input-clear-*`, whose corner radius follows `--ms-border-radius`). See the new `examples-basic.html` §BU01b demo.
43
+
44
+ - **Input decorations rebuilt as a flex "field shell" — no more overlap or text bleed.** `.ms__input-wrapper` is now the bordered field (border, background, radius, focus ring via `:focus-within`), with the `<input>`, the `[N]` counter, the ✕ clear, and the chevron as real flex children in a spaced row. Previously each was absolutely pinned by a hard-coded inset, so `show-counter` + `show-clear` collided and long text could slide under the icons. Now they space themselves via `--ms-input-gap`, long text clips cleanly inside the input's own box, and RTL mirroring falls out of the flex direction for free. Several obsolete positioning vars were removed (`--ms-input-padding`, `--ms-input-padding-right`, `--ms-toggle-right`, `--ms-counter-offset`, `--ms-input-clear-inset`, `--ms-input-clear-gutter`, `--ms-transform-center-y`).
45
+
46
+ - **Selected-items popover now lines up with the field.** `--ms-selected-popover-width` used to default to a fixed 32rem independent of the control, which looked detached under a wide field; it now defaults to `var(--ms-input-current-width)`, so the popover and the dropdown both track the field width and align under it. Set `selected-popover-width` (or the CSS var) to a fixed length to restore the old constant-width behavior.
47
+
48
+ - **Toggle chevron is now a crisp icon, not a text character.** The dropdown indicator renders the shared `--ms-icon-chevron` glyph through a CSS mask — consistent with the ✕, count-clear, and badge-remove icons — instead of the Unicode `▼`, so it no longer depends on font rendering and themes uniformly via `--ms-toggle-icon-color` / `--ms-toggle-icon-size`. It still points down when closed and rotates up when open.
49
+
50
+ ## Demos & docs
51
+
52
+ - 🚀 [Live demo](https://web-multiselect.keenmate.dev)
53
+ - 📘 [Usage / API reference](./docs/usage.md) — attributes, properties, methods, events.
54
+ - 🎨 [Theming](./docs/theming.md) — `--ms-*` variables, dark mode, cascade layers, Theme Designer integration.
55
+ - 📚 [Examples / cookbook](./docs/examples.md) — rich content, async search, virtual scroll, custom rendering, forms.
56
+ - ♿ [Accessibility](./docs/accessibility.md) — keyboard model, ARIA labels, focus behavior.
57
+
58
+ ## Install
59
+
60
+ ```bash
61
+ npm install @keenmate/web-multiselect
62
+ ```
63
+
64
+ ## Quick start
65
+
66
+ **Declarative — no JavaScript required:**
67
+
68
+ ```html
69
+ <script type="module">
70
+ import '@keenmate/web-multiselect';
71
+ </script>
72
+
73
+ <web-multiselect placeholder="Pick a country">
74
+ <option value="cz">Czech Republic</option>
75
+ <option value="sk">Slovakia</option>
76
+ <option value="at">Austria</option>
77
+ </web-multiselect>
78
+ ```
79
+
80
+ **Programmatic — dynamic data + events:**
81
+
82
+ ```html
83
+ <web-multiselect id="picker" search-placeholder="Search…"></web-multiselect>
84
+
85
+ <script type="module">
86
+ import '@keenmate/web-multiselect';
87
+
88
+ const picker = document.getElementById('picker');
89
+ picker.options = [
90
+ { value: 'js', label: 'JavaScript', icon: '🟨' },
91
+ { value: 'ts', label: 'TypeScript', icon: '🔷' },
92
+ { value: 'py', label: 'Python', icon: '🐍' }
93
+ ];
94
+
95
+ picker.addEventListener('change', (e) => {
96
+ console.log('Selected:', e.detail.selectedValues);
97
+ });
98
+ </script>
99
+ ```
100
+
101
+ See [docs/usage.md](./docs/usage.md) for the full API and [docs/examples.md](./docs/examples.md) for advanced patterns (async data, virtual scrolling, custom rendering, form integration).
102
+
103
+ ## Editor IntelliSense
104
+
105
+ The package ships editor metadata so you get autocomplete and hover docs for the
106
+ element's attributes, events, and all `--ms-*` CSS custom properties. All of it is
107
+ generated from the component's source on every build, so it never drifts.
108
+
109
+ - **JetBrains** (WebStorm / IntelliJ) — works automatically. The IDE discovers
110
+ `web-types.json` via the `web-types` field in `package.json`; no setup needed.
111
+ - **VS Code** — the data files ship but VS Code doesn't auto-discover them from a
112
+ dependency, so point your workspace at them once in `.vscode/settings.json`:
113
+
114
+ ```json
115
+ {
116
+ "html.customData": [
117
+ "./node_modules/@keenmate/web-multiselect/vscode.html-custom-data.json"
118
+ ],
119
+ "css.customData": [
120
+ "./node_modules/@keenmate/web-multiselect/vscode.css-custom-data.json"
121
+ ]
122
+ }
123
+ ```
124
+
125
+ `html.customData` powers tag/attribute completion on `<web-multiselect>`;
126
+ `css.customData` powers completion for the `--ms-*` theming variables. Reload
127
+ the window after adding them.
128
+
129
+ ## Browser support
130
+
131
+ Modern evergreen browsers — anything with native `customElements`, Shadow DOM, and CSS `@layer` support:
132
+
133
+ - Chrome / Edge 99+
134
+ - Firefox 97+
135
+ - Safari 15.4+
136
+
137
+ No polyfills are shipped. SSR-safe: the module imports without crashing in Node, but renders only after hydration in the browser.
138
+
139
+ ## Development
140
+
141
+ ```bash
142
+ # Install dependencies
143
+ npm install
144
+
145
+ # Start dev server (HMR)
146
+ npm run dev
147
+
148
+ # Build for production
149
+ npm run build
150
+
151
+ # Create package tarball
152
+ npm run package
153
+
154
+ # Run tests
155
+ npm run test:unit # Vitest (happy-dom) — fast logic checks
156
+ npm run test:e2e # Playwright (browser) — interaction/visual
157
+ npm test # both
158
+ ```
159
+
160
+ ## Code structure
161
+
162
+ Follows the BlissFramework four-layer web-component layout:
163
+
164
+ | Layer | File | Role |
165
+ |-------|------|------|
166
+ | Element | `src/web-component.ts` | `MultiSelectElement` — custom-element I/O wrapper, `ATTRIBUTE_TABLE`-driven |
167
+ | Logic | `src/multiselect.ts` | `WebMultiSelect<T>` — framework-agnostic core |
168
+ | Service | `src/tooltip.ts`, `src/virtual-scroll.ts` | single-purpose helpers (`Tooltip`, `VirtualScroll`) |
169
+ | Side | `src/types.ts`, `src/logger.ts`, `src/vendor/` | types, logging, vendored deps |
170
+
171
+ Two deviations from the canonical shape, both intentional:
172
+
173
+ - **`MultiSelectElement extends BaseElement`, not `HTMLElement` directly.** `BaseElement` is a local `const` resolving to `HTMLElement` in the browser and to a stub class under SSR (`typeof HTMLElement === 'undefined'`), so importing the module in Node doesn't throw. A literal `grep "extends HTMLElement"` structure check will not match here by design.
174
+ - **`src/vite-env.d.ts`** is a standard Vite ambient-types file, not part of the four-layer model.
175
+
176
+ ## License
177
+
178
+ MIT — see [LICENSE](./LICENSE).
179
+
180
+ ## Built with BlissFramework
181
+
182
+ Follows the [BlissFramework component guidelines](https://blissframework.dev/) for structure, theming, color-scheme, and accessibility. Per-check verifications run via `/validate-web-component`.
183
+
184
+ ## Credits
185
+
186
+ Created by [Keenmate](https://github.com/keenmate) as part of the Pure Admin design system.