@keenmate/web-multiselect 2.0.0-rc09 → 2.0.0-rc11

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,176 +1,188 @@
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-rc09
25
-
26
- - **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.
27
-
28
- - **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.
29
-
30
- - **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.
31
-
32
- - **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`.
33
-
34
- ## What's New in v2.0.0-rc08
35
-
36
- - **Keyboard hook — redefine key behavior with `keydownCallback`** — A new property-only callback that runs on every keydown *before* the built-in handling, receiving the raw event, the current state (open, presentation, search term, focused option, filtered options, selection), and a `controller` — an imperative facade mirroring every built-in action (`focusNext/Previous/First/Last`, `focusPageUp/Down`, `focusNextMatch/PreviousMatch`, `focusIndex`, `toggleFocused`, `toggleValue`, `selectValue`, `deselectValue`, `open`, `close`, `setSearch`, `clearSearch`). Return `true` to fully own a key (you call `preventDefault`); return falsy to fall through to the defaults. Use it to remap keys (Vim `j`/`k`), add shortcuts (`Ctrl`+`A` → select all, `Ctrl`+`I` → invert), or suppress a default — the same veto-hook shape KM components share. Reactive, no reinit; set it as `el.keydownCallback = …`.
37
-
38
- - **`Home` / `End` no longer steal the caret in the search box** Both keys were intercepted unconditionally to jump list focus to the first/last option, so pressing `Home` to move the caret to the start of the search text jumped the list instead breaking normal text-field muscle memory. They're now caret-aware: in an editable search field the key moves the caret first, and only navigates the list when the caret is already at that end (or the box is empty / has no editable caret; an active selection is left to the browser). So `Home` moves the caret to the start, and a second `Home` (already there) jumps to the first option. Empty-box `Home`/`End` navigation is unchanged.
39
-
40
- ## Demos & docs
41
-
42
- - 🚀 [Live demo](https://web-multiselect.keenmate.dev)
43
- - 📘 [Usage / API reference](./docs/usage.md) — attributes, properties, methods, events.
44
- - 🎨 [Theming](./docs/theming.md) — `--ms-*` variables, dark mode, cascade layers, Theme Designer integration.
45
- - 📚 [Examples / cookbook](./docs/examples.md) — rich content, async search, virtual scroll, custom rendering, forms.
46
- - [Accessibility](./docs/accessibility.md)keyboard model, ARIA labels, focus behavior.
47
-
48
- ## Install
49
-
50
- ```bash
51
- npm install @keenmate/web-multiselect
52
- ```
53
-
54
- ## Quick start
55
-
56
- **Declarativeno JavaScript required:**
57
-
58
- ```html
59
- <script type="module">
60
- import '@keenmate/web-multiselect';
61
- </script>
62
-
63
- <web-multiselect placeholder="Pick a country">
64
- <option value="cz">Czech Republic</option>
65
- <option value="sk">Slovakia</option>
66
- <option value="at">Austria</option>
67
- </web-multiselect>
68
- ```
69
-
70
- **Programmatic — dynamic data + events:**
71
-
72
- ```html
73
- <web-multiselect id="picker" search-placeholder="Search…"></web-multiselect>
74
-
75
- <script type="module">
76
- import '@keenmate/web-multiselect';
77
-
78
- const picker = document.getElementById('picker');
79
- picker.options = [
80
- { value: 'js', label: 'JavaScript', icon: '🟨' },
81
- { value: 'ts', label: 'TypeScript', icon: '🔷' },
82
- { value: 'py', label: 'Python', icon: '🐍' }
83
- ];
84
-
85
- picker.addEventListener('change', (e) => {
86
- console.log('Selected:', e.detail.selectedValues);
87
- });
88
- </script>
89
- ```
90
-
91
- 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).
92
-
93
- ## Editor IntelliSense
94
-
95
- The package ships editor metadata so you get autocomplete and hover docs for the
96
- element's attributes, events, and all `--ms-*` CSS custom properties. All of it is
97
- generated from the component's source on every build, so it never drifts.
98
-
99
- - **JetBrains** (WebStorm / IntelliJ) — works automatically. The IDE discovers
100
- `web-types.json` via the `web-types` field in `package.json`; no setup needed.
101
- - **VS Code** — the data files ship but VS Code doesn't auto-discover them from a
102
- dependency, so point your workspace at them once in `.vscode/settings.json`:
103
-
104
- ```json
105
- {
106
- "html.customData": [
107
- "./node_modules/@keenmate/web-multiselect/vscode.html-custom-data.json"
108
- ],
109
- "css.customData": [
110
- "./node_modules/@keenmate/web-multiselect/vscode.css-custom-data.json"
111
- ]
112
- }
113
- ```
114
-
115
- `html.customData` powers tag/attribute completion on `<web-multiselect>`;
116
- `css.customData` powers completion for the `--ms-*` theming variables. Reload
117
- the window after adding them.
118
-
119
- ## Browser support
120
-
121
- Modern evergreen browsers — anything with native `customElements`, Shadow DOM, and CSS `@layer` support:
122
-
123
- - Chrome / Edge 99+
124
- - Firefox 97+
125
- - Safari 15.4+
126
-
127
- No polyfills are shipped. SSR-safe: the module imports without crashing in Node, but renders only after hydration in the browser.
128
-
129
- ## Development
130
-
131
- ```bash
132
- # Install dependencies
133
- npm install
134
-
135
- # Start dev server (HMR)
136
- npm run dev
137
-
138
- # Build for production
139
- npm run build
140
-
141
- # Create package tarball
142
- npm run package
143
-
144
- # Run tests
145
- npm run test:unit # Vitest (happy-dom) — fast logic checks
146
- npm run test:e2e # Playwright (browser) — interaction/visual
147
- npm test # both
148
- ```
149
-
150
- ## Code structure
151
-
152
- Follows the BlissFramework four-layer web-component layout:
153
-
154
- | Layer | File | Role |
155
- |-------|------|------|
156
- | Element | `src/web-component.ts` | `MultiSelectElement` — custom-element I/O wrapper, `ATTRIBUTE_TABLE`-driven |
157
- | Logic | `src/multiselect.ts` | `WebMultiSelect<T>` framework-agnostic core |
158
- | Service | `src/tooltip.ts`, `src/virtual-scroll.ts` | single-purpose helpers (`Tooltip`, `VirtualScroll`) |
159
- | Side | `src/types.ts`, `src/logger.ts`, `src/vendor/` | types, logging, vendored deps |
160
-
161
- Two deviations from the canonical shape, both intentional:
162
-
163
- - **`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.
164
- - **`src/vite-env.d.ts`** is a standard Vite ambient-types file, not part of the four-layer model.
165
-
166
- ## License
167
-
168
- MITsee [LICENSE](./LICENSE).
169
-
170
- ## Built with BlissFramework
171
-
172
- Follows the [BlissFramework component guidelines](https://blissframework.dev/) for structure, theming, color-scheme, and accessibility. Per-check verifications run via `/validate-web-component`.
173
-
174
- ## Credits
175
-
176
- 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-rc11
25
+
26
+ - **Imperative open/close APIdrive 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.
27
+
28
+ - **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.
29
+
30
+ - **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`).
31
+
32
+ - **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.
33
+
34
+ - **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.
35
+
36
+ ## What's New in v2.0.0-rc10
37
+
38
+ - **`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`.
39
+
40
+ - **`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.
41
+
42
+ - **`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.
43
+
44
+ - **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.
45
+
46
+ - **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).
47
+
48
+ - **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.
49
+
50
+ - **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.
51
+
52
+ ## Demos & docs
53
+
54
+ - 🚀 [Live demo](https://web-multiselect.keenmate.dev)
55
+ - 📘 [Usage / API reference](./docs/usage.md) — attributes, properties, methods, events.
56
+ - 🎨 [Theming](./docs/theming.md) `--ms-*` variables, dark mode, cascade layers, Theme Designer integration.
57
+ - 📚 [Examples / cookbook](./docs/examples.md) — rich content, async search, virtual scroll, custom rendering, forms.
58
+ - ♿ [Accessibility](./docs/accessibility.md) — keyboard model, ARIA labels, focus behavior.
59
+
60
+ ## Install
61
+
62
+ ```bash
63
+ npm install @keenmate/web-multiselect
64
+ ```
65
+
66
+ ## Quick start
67
+
68
+ **Declarative — no JavaScript required:**
69
+
70
+ ```html
71
+ <script type="module">
72
+ import '@keenmate/web-multiselect';
73
+ </script>
74
+
75
+ <web-multiselect placeholder="Pick a country">
76
+ <option value="cz">Czech Republic</option>
77
+ <option value="sk">Slovakia</option>
78
+ <option value="at">Austria</option>
79
+ </web-multiselect>
80
+ ```
81
+
82
+ **Programmatic dynamic data + events:**
83
+
84
+ ```html
85
+ <web-multiselect id="picker" search-placeholder="Search…"></web-multiselect>
86
+
87
+ <script type="module">
88
+ import '@keenmate/web-multiselect';
89
+
90
+ const picker = document.getElementById('picker');
91
+ picker.options = [
92
+ { value: 'js', label: 'JavaScript', icon: '🟨' },
93
+ { value: 'ts', label: 'TypeScript', icon: '🔷' },
94
+ { value: 'py', label: 'Python', icon: '🐍' }
95
+ ];
96
+
97
+ picker.addEventListener('change', (e) => {
98
+ console.log('Selected:', e.detail.selectedValues);
99
+ });
100
+ </script>
101
+ ```
102
+
103
+ 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).
104
+
105
+ ## Editor IntelliSense
106
+
107
+ The package ships editor metadata so you get autocomplete and hover docs for the
108
+ element's attributes, events, and all `--ms-*` CSS custom properties. All of it is
109
+ generated from the component's source on every build, so it never drifts.
110
+
111
+ - **JetBrains** (WebStorm / IntelliJ) — works automatically. The IDE discovers
112
+ `web-types.json` via the `web-types` field in `package.json`; no setup needed.
113
+ - **VS Code** — the data files ship but VS Code doesn't auto-discover them from a
114
+ dependency, so point your workspace at them once in `.vscode/settings.json`:
115
+
116
+ ```json
117
+ {
118
+ "html.customData": [
119
+ "./node_modules/@keenmate/web-multiselect/vscode.html-custom-data.json"
120
+ ],
121
+ "css.customData": [
122
+ "./node_modules/@keenmate/web-multiselect/vscode.css-custom-data.json"
123
+ ]
124
+ }
125
+ ```
126
+
127
+ `html.customData` powers tag/attribute completion on `<web-multiselect>`;
128
+ `css.customData` powers completion for the `--ms-*` theming variables. Reload
129
+ the window after adding them.
130
+
131
+ ## Browser support
132
+
133
+ Modern evergreen browsers — anything with native `customElements`, Shadow DOM, and CSS `@layer` support:
134
+
135
+ - Chrome / Edge 99+
136
+ - Firefox 97+
137
+ - Safari 15.4+
138
+
139
+ No polyfills are shipped. SSR-safe: the module imports without crashing in Node, but renders only after hydration in the browser.
140
+
141
+ ## Development
142
+
143
+ ```bash
144
+ # Install dependencies
145
+ npm install
146
+
147
+ # Start dev server (HMR)
148
+ npm run dev
149
+
150
+ # Build for production
151
+ npm run build
152
+
153
+ # Create package tarball
154
+ npm run package
155
+
156
+ # Run tests
157
+ npm run test:unit # Vitest (happy-dom)fast logic checks
158
+ npm run test:e2e # Playwright (browser) — interaction/visual
159
+ npm test # both
160
+ ```
161
+
162
+ ## Code structure
163
+
164
+ Follows the BlissFramework four-layer web-component layout:
165
+
166
+ | Layer | File | Role |
167
+ |-------|------|------|
168
+ | Element | `src/web-component.ts` | `MultiSelectElement` custom-element I/O wrapper, `ATTRIBUTE_TABLE`-driven |
169
+ | Logic | `src/multiselect.ts` | `WebMultiSelect<T>` — framework-agnostic core |
170
+ | Service | `src/tooltip.ts`, `src/virtual-scroll.ts` | single-purpose helpers (`Tooltip`, `VirtualScroll`) |
171
+ | Side | `src/types.ts`, `src/logger.ts`, `src/vendor/` | types, logging, vendored deps |
172
+
173
+ Two deviations from the canonical shape, both intentional:
174
+
175
+ - **`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.
176
+ - **`src/vite-env.d.ts`** is a standard Vite ambient-types file, not part of the four-layer model.
177
+
178
+ ## License
179
+
180
+ MIT — see [LICENSE](./LICENSE).
181
+
182
+ ## Built with BlissFramework
183
+
184
+ Follows the [BlissFramework component guidelines](https://blissframework.dev/) for structure, theming, color-scheme, and accessibility. Per-check verifications run via `/validate-web-component`.
185
+
186
+ ## Credits
187
+
188
+ Created by [Keenmate](https://github.com/keenmate) as part of the Pure Admin design system.