scoped-search-bar 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # [0.2.0](https://github.com/doberkofler/scoped-search-bar/compare/v0.1.0...v0.2.0) (2026-07-19)
2
+
3
+ ### Features
4
+
5
+ * add react wrapper ([522941b](https://github.com/doberkofler/scoped-search-bar/commit/522941b4555683e87b02a277d00d912fa0a9be5b))
6
+
7
+ # [0.1.0](https://github.com/doberkofler/scoped-search-bar/compare/e19b0e57d29cdd50370cb1a358811dccddc02073...v0.1.0) (2026-07-18)
8
+
9
+ ### Features
10
+
11
+ * release scoped search bar ([e19b0e5](https://github.com/doberkofler/scoped-search-bar/commit/e19b0e57d29cdd50370cb1a358811dccddc02073))
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2014-2018 Dieter Oberkofler <dieter.oberkofler@gmail.com>
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
13
+ all 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
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # scoped-search-bar
2
+
3
+ A dependency-free TypeScript search component that combines a text input, a multi-select scope dropdown, and async submit handling.
4
+
5
+ ![Scoped search bar demo](https://raw.githubusercontent.com/doberkofler/scoped-search-bar/main/docs/images/scoped-search-bar-demo.png)
6
+
7
+ ## Features
8
+
9
+ - Native TypeScript, HTML, and CSS. No React, MUI, or runtime framework dependency.
10
+ - Strongly typed public API with an instance lifecycle.
11
+ - Multi-select scope menu with keyboard navigation and ARIA state.
12
+ - Async `onSearch` support with built-in pending/disabled state.
13
+ - Enter-key and button submission.
14
+ - Scoped CSS classes and custom properties for theming.
15
+ - Vite demo, Vitest browser tests, Playwright e2e tests, TypeDoc API docs, and screenshot generation.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install scoped-search-bar
21
+ ```
22
+
23
+ ```ts
24
+ import {ScopedSearchBar, type SearchScope} from 'scoped-search-bar';
25
+ import 'scoped-search-bar/styles/scoped-search-bar.css';
26
+
27
+ const scopes: SearchScope[] = [
28
+ {id: 'west-coast', label: 'West Coast'},
29
+ {id: 'midwest', label: 'Midwest'},
30
+ {id: 'europe', label: 'Europe'},
31
+ ];
32
+
33
+ const search = new ScopedSearchBar(document.querySelector('#search')!, {
34
+ scopes,
35
+ initialSelectedIds: ['west-coast', 'europe'],
36
+ initialSearchTerm: 'react',
37
+ onSearch: async (term, selectedScopeIds) => {
38
+ console.log(term, selectedScopeIds);
39
+ },
40
+ });
41
+ ```
42
+
43
+ ## React Usage
44
+
45
+ Use the React adapter from the dedicated subpath so vanilla consumers do not load React:
46
+
47
+ ```tsx
48
+ import {ScopedSearchBar} from 'scoped-search-bar/react';
49
+ import 'scoped-search-bar/styles/scoped-search-bar.css';
50
+
51
+ const scopes = [
52
+ {id: 'articles', label: 'Articles'},
53
+ {id: 'users', label: 'Users'},
54
+ {id: 'docs', label: 'Docs'},
55
+ ];
56
+
57
+ export function Search() {
58
+ return (
59
+ <ScopedSearchBar
60
+ scopes={scopes}
61
+ initialSelectedIds={['articles']}
62
+ initialSearchTerm="react"
63
+ onSearch={async (term, selectedScopeIds) => {
64
+ console.log(term, selectedScopeIds);
65
+ }}
66
+ />
67
+ );
68
+ }
69
+ ```
70
+
71
+ The adapter creates the native component on mount and destroys it on unmount. Mount-time options mirror `ScopedSearchBarOptions`; `scopes`, `disabled`, `searchTerm`, and `selectedIds` are synchronized after mount through native setters.
72
+
73
+ ## API
74
+
75
+ ```ts
76
+ export type SearchScope = {
77
+ readonly id: string;
78
+ readonly label: string;
79
+ };
80
+
81
+ export type ScopedSearchBarOptions = {
82
+ readonly scopes: readonly SearchScope[];
83
+ readonly initialSelectedIds?: readonly string[];
84
+ readonly initialSearchTerm?: string;
85
+ readonly onSearch: (term: string, selectedScopeIds: readonly string[]) => void | Promise<void>;
86
+ readonly placeholder?: string;
87
+ readonly inputLabel?: string;
88
+ readonly searchButtonLabel?: string;
89
+ readonly searchingButtonLabel?: string;
90
+ readonly scopeSelectorLabel?: string;
91
+ readonly clearScopesLabel?: string;
92
+ readonly scopeLabel?: (context: {count: number; total: number; selectedIds: readonly string[]}) => string;
93
+ readonly disabled?: boolean;
94
+ readonly className?: string;
95
+ readonly id?: string;
96
+ readonly menuMaxHeight?: number;
97
+ };
98
+ ```
99
+
100
+ Instance methods:
101
+
102
+ - `search()` - submits the current term and selected scopes.
103
+ - `openMenu()` / `closeMenu()` - controls the scope menu.
104
+ - `clearScopes()` - clears all selected scopes.
105
+ - `setScopes(scopes)` - replaces available scopes and drops unknown selected IDs.
106
+ - `setSearchTerm(term)` - updates the input value.
107
+ - `setSelectedIds(ids)` - updates selected IDs after normalization.
108
+ - `setDisabled(disabled)` - enables/disables all controls.
109
+ - `getSearchTerm()` / `getSelectedIds()` - returns current state.
110
+ - `destroy()` - removes DOM and listeners. Later method calls throw.
111
+
112
+ ## Development
113
+
114
+ ```bash
115
+ pnpm install
116
+ pnpm run dev
117
+ pnpm run ci
118
+ pnpm run integration-test
119
+ pnpm run screenshot
120
+ ```
121
+
122
+ See [`docs/guide.md`](./docs/guide.md) for full usage and customization details.
@@ -0,0 +1,2 @@
1
+ import { a as ScopeLabelFormatter, c as ScopedSearchBarInstance, d as createScopedSearchBar, i as RequiredVisualOptions, l as ScopedSearchBarOptions, n as MaybePromise, o as ScopeLabelFormatterContext, r as OnSearch, s as ScopedSearchBar, t as DEFAULT_SCOPED_SEARCH_BAR_OPTIONS, u as SearchScope } from "./scoped-search-bar-D0ppSWb6.mjs";
2
+ export { DEFAULT_SCOPED_SEARCH_BAR_OPTIONS, type MaybePromise, type OnSearch, type RequiredVisualOptions, type ScopeLabelFormatter, type ScopeLabelFormatterContext, ScopedSearchBar, type ScopedSearchBarInstance, type ScopedSearchBarOptions, type SearchScope, createScopedSearchBar };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { n as ScopedSearchBar, r as createScopedSearchBar, t as DEFAULT_SCOPED_SEARCH_BAR_OPTIONS } from "./scoped-search-bar-D8zVpZ4L.mjs";
2
+ export { DEFAULT_SCOPED_SEARCH_BAR_OPTIONS, ScopedSearchBar, createScopedSearchBar };
@@ -0,0 +1,18 @@
1
+ import { c as ScopedSearchBarInstance, l as ScopedSearchBarOptions } from "./scoped-search-bar-D0ppSWb6.mjs";
2
+ //#region src/lib/react.d.ts
3
+ type ScopedSearchBarReactProps = ScopedSearchBarOptions & {
4
+ /** Search term synced into the native input after mount. User edits remain internal unless this prop changes. */
5
+ readonly searchTerm?: string;
6
+ /** Selected scope IDs synced into the native selector after mount. User edits remain internal unless this prop changes. */
7
+ readonly selectedIds?: readonly string[];
8
+ };
9
+ /** React adapter for the native ScopedSearchBar component. */
10
+ declare const ScopedSearchBar: import("react").ForwardRefExoticComponent<ScopedSearchBarOptions & {
11
+ /** Search term synced into the native input after mount. User edits remain internal unless this prop changes. */
12
+ readonly searchTerm?: string;
13
+ /** Selected scope IDs synced into the native selector after mount. User edits remain internal unless this prop changes. */
14
+ readonly selectedIds?: readonly string[];
15
+ } & import("react").RefAttributes<ScopedSearchBarInstance>>;
16
+ //#endregion
17
+ export { ScopedSearchBar, ScopedSearchBarReactProps };
18
+ //# sourceMappingURL=react.d.mts.map
package/dist/react.mjs ADDED
@@ -0,0 +1,60 @@
1
+ import { n as ScopedSearchBar$1 } from "./scoped-search-bar-D8zVpZ4L.mjs";
2
+ import { forwardRef, useEffect, useRef } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/lib/react.tsx
5
+ function setForwardedRef(ref, value) {
6
+ if (typeof ref === "function") {
7
+ ref(value);
8
+ return;
9
+ }
10
+ if (ref !== null) ref.current = value;
11
+ }
12
+ /** React adapter for the native ScopedSearchBar component. */
13
+ const ScopedSearchBar = forwardRef((props, ref) => {
14
+ const { searchTerm, selectedIds, scopes, disabled, onSearch, ...mountOptions } = props;
15
+ const hostRef = useRef(null);
16
+ const instanceRef = useRef(null);
17
+ const onSearchRef = useRef(onSearch);
18
+ useEffect(() => {
19
+ onSearchRef.current = onSearch;
20
+ }, [onSearch]);
21
+ useEffect(() => {
22
+ const host = hostRef.current;
23
+ if (host === null) return;
24
+ const instance = new ScopedSearchBar$1(host, {
25
+ ...mountOptions,
26
+ ...disabled === void 0 ? {} : { disabled },
27
+ ...searchTerm === void 0 ? {} : { initialSearchTerm: searchTerm },
28
+ ...selectedIds === void 0 ? {} : { initialSelectedIds: selectedIds },
29
+ scopes,
30
+ onSearch: async (term, selectedScopeIds) => {
31
+ await onSearchRef.current(term, selectedScopeIds);
32
+ }
33
+ });
34
+ instanceRef.current = instance;
35
+ setForwardedRef(ref, instance);
36
+ return () => {
37
+ instanceRef.current = null;
38
+ setForwardedRef(ref, null);
39
+ instance.destroy();
40
+ };
41
+ }, []);
42
+ useEffect(() => {
43
+ instanceRef.current?.setScopes(scopes);
44
+ }, [scopes]);
45
+ useEffect(() => {
46
+ if (disabled !== void 0) instanceRef.current?.setDisabled(disabled);
47
+ }, [disabled]);
48
+ useEffect(() => {
49
+ if (searchTerm !== void 0) instanceRef.current?.setSearchTerm(searchTerm);
50
+ }, [searchTerm]);
51
+ useEffect(() => {
52
+ if (selectedIds !== void 0) instanceRef.current?.setSelectedIds(selectedIds);
53
+ }, [selectedIds]);
54
+ return /* @__PURE__ */ jsx("div", { ref: hostRef });
55
+ });
56
+ ScopedSearchBar.displayName = "ScopedSearchBar";
57
+ //#endregion
58
+ export { ScopedSearchBar };
59
+
60
+ //# sourceMappingURL=react.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.mjs","names":["NativeScopedSearchBar"],"sources":["../src/lib/react.tsx"],"sourcesContent":["import {forwardRef, useEffect, useRef, type ForwardedRef, type ReactElement} from 'react';\n\nimport {ScopedSearchBar as NativeScopedSearchBar, type OnSearch, type ScopedSearchBarInstance, type ScopedSearchBarOptions} from './scoped-search-bar.ts';\n\nexport type ScopedSearchBarReactProps = ScopedSearchBarOptions & {\n\t/** Search term synced into the native input after mount. User edits remain internal unless this prop changes. */\n\treadonly searchTerm?: string;\n\t/** Selected scope IDs synced into the native selector after mount. User edits remain internal unless this prop changes. */\n\treadonly selectedIds?: readonly string[];\n};\n\nfunction setForwardedRef(ref: ForwardedRef<ScopedSearchBarInstance>, value: ScopedSearchBarInstance | null): void {\n\tif (typeof ref === 'function') {\n\t\tref(value);\n\t\treturn;\n\t}\n\tif (ref !== null) {\n\t\tref.current = value;\n\t}\n}\n\n/** React adapter for the native ScopedSearchBar component. */\nexport const ScopedSearchBar = forwardRef<ScopedSearchBarInstance, ScopedSearchBarReactProps>((props, ref): ReactElement => {\n\tconst {searchTerm, selectedIds, scopes, disabled, onSearch, ...mountOptions} = props;\n\tconst hostRef = useRef<HTMLDivElement | null>(null);\n\tconst instanceRef = useRef<ScopedSearchBarInstance | null>(null);\n\tconst onSearchRef = useRef<OnSearch>(onSearch);\n\n\tuseEffect((): void => {\n\t\tonSearchRef.current = onSearch;\n\t}, [onSearch]);\n\n\tuseEffect(() => {\n\t\tconst host = hostRef.current;\n\t\tif (host === null) {\n\t\t\treturn;\n\t\t}\n\t\tconst options: ScopedSearchBarOptions = {\n\t\t\t...mountOptions,\n\t\t\t...(disabled === undefined ? {} : {disabled}),\n\t\t\t...(searchTerm === undefined ? {} : {initialSearchTerm: searchTerm}),\n\t\t\t...(selectedIds === undefined ? {} : {initialSelectedIds: selectedIds}),\n\t\t\tscopes,\n\t\t\tonSearch: async (term, selectedScopeIds) => {\n\t\t\t\tawait onSearchRef.current(term, selectedScopeIds);\n\t\t\t},\n\t\t};\n\n\t\tconst instance = new NativeScopedSearchBar(host, options);\n\t\tinstanceRef.current = instance;\n\t\tsetForwardedRef(ref, instance);\n\n\t\treturn (): void => {\n\t\t\tinstanceRef.current = null;\n\t\t\tsetForwardedRef(ref, null);\n\t\t\tinstance.destroy();\n\t\t};\n\t\t// Mount-only options are intentionally captured once. Mutable props are synced below through native setters.\n\t\t// oxlint-disable-next-line react-hooks/exhaustive-deps\n\t}, []);\n\n\tuseEffect(() => {\n\t\tinstanceRef.current?.setScopes(scopes);\n\t}, [scopes]);\n\n\tuseEffect(() => {\n\t\tif (disabled !== undefined) {\n\t\t\tinstanceRef.current?.setDisabled(disabled);\n\t\t}\n\t}, [disabled]);\n\n\tuseEffect(() => {\n\t\tif (searchTerm !== undefined) {\n\t\t\tinstanceRef.current?.setSearchTerm(searchTerm);\n\t\t}\n\t}, [searchTerm]);\n\n\tuseEffect(() => {\n\t\tif (selectedIds !== undefined) {\n\t\t\tinstanceRef.current?.setSelectedIds(selectedIds);\n\t\t}\n\t}, [selectedIds]);\n\n\treturn <div ref={hostRef} />;\n});\n\nScopedSearchBar.displayName = 'ScopedSearchBar';\n"],"mappings":";;;;AAWA,SAAS,gBAAgB,KAA4C,OAA6C;CACjH,IAAI,OAAO,QAAQ,YAAY;EAC9B,IAAI,KAAK;EACT;CACD;CACA,IAAI,QAAQ,MACX,IAAI,UAAU;AAEhB;;AAGA,MAAa,kBAAkB,YAAgE,OAAO,QAAsB;CAC3H,MAAM,EAAC,YAAY,aAAa,QAAQ,UAAU,UAAU,GAAG,iBAAgB;CAC/E,MAAM,UAAU,OAA8B,IAAI;CAClD,MAAM,cAAc,OAAuC,IAAI;CAC/D,MAAM,cAAc,OAAiB,QAAQ;CAE7C,gBAAsB;EACrB,YAAY,UAAU;CACvB,GAAG,CAAC,QAAQ,CAAC;CAEb,gBAAgB;EACf,MAAM,OAAO,QAAQ;EACrB,IAAI,SAAS,MACZ;EAaD,MAAM,WAAW,IAAIA,kBAAsB,MAAM;GAVhD,GAAG;GACH,GAAI,aAAa,KAAA,IAAY,CAAC,IAAI,EAAC,SAAQ;GAC3C,GAAI,eAAe,KAAA,IAAY,CAAC,IAAI,EAAC,mBAAmB,WAAU;GAClE,GAAI,gBAAgB,KAAA,IAAY,CAAC,IAAI,EAAC,oBAAoB,YAAW;GACrE;GACA,UAAU,OAAO,MAAM,qBAAqB;IAC3C,MAAM,YAAY,QAAQ,MAAM,gBAAgB;GACjD;EAGsD,CAAC;EACxD,YAAY,UAAU;EACtB,gBAAgB,KAAK,QAAQ;EAE7B,aAAmB;GAClB,YAAY,UAAU;GACtB,gBAAgB,KAAK,IAAI;GACzB,SAAS,QAAQ;EAClB;CAGD,GAAG,CAAC,CAAC;CAEL,gBAAgB;EACf,YAAY,SAAS,UAAU,MAAM;CACtC,GAAG,CAAC,MAAM,CAAC;CAEX,gBAAgB;EACf,IAAI,aAAa,KAAA,GAChB,YAAY,SAAS,YAAY,QAAQ;CAE3C,GAAG,CAAC,QAAQ,CAAC;CAEb,gBAAgB;EACf,IAAI,eAAe,KAAA,GAClB,YAAY,SAAS,cAAc,UAAU;CAE/C,GAAG,CAAC,UAAU,CAAC;CAEf,gBAAgB;EACf,IAAI,gBAAgB,KAAA,GACnB,YAAY,SAAS,eAAe,WAAW;CAEjD,GAAG,CAAC,WAAW,CAAC;CAEhB,OAAO,oBAAC,OAAD,EAAK,KAAK,QAAU,CAAA;AAC5B,CAAC;AAED,gBAAgB,cAAc"}
@@ -0,0 +1,83 @@
1
+ //#region src/lib/scoped-search-bar.d.ts
2
+ type MaybePromise<T> = T | Promise<T>;
3
+ /** A selectable search scope shown in the dropdown menu. */
4
+ type SearchScope = {
5
+ readonly id: string;
6
+ readonly label: string;
7
+ };
8
+ type ScopeLabelFormatterContext = {
9
+ readonly count: number;
10
+ readonly total: number;
11
+ readonly selectedIds: readonly string[];
12
+ };
13
+ type ScopeLabelFormatter = (context: ScopeLabelFormatterContext) => string;
14
+ type OnSearch = (term: string, selectedScopeIds: readonly string[]) => MaybePromise<void>;
15
+ type ScopedSearchBarOptions = {
16
+ /** Scopes available in the selector menu. IDs must be unique. */
17
+ readonly scopes: readonly SearchScope[];
18
+ /** Scope IDs selected on initial render. Unknown IDs are ignored. */
19
+ readonly initialSelectedIds?: readonly string[];
20
+ /** Initial search input value. */
21
+ readonly initialSearchTerm?: string;
22
+ /** Called when the user clicks Search or presses Enter in the input. */
23
+ readonly onSearch: OnSearch;
24
+ /** Placeholder shown in the search input. */
25
+ readonly placeholder?: string;
26
+ /** Accessible label for the search input. */
27
+ readonly inputLabel?: string;
28
+ /** Text shown in the submit button while idle. */
29
+ readonly searchButtonLabel?: string;
30
+ /** Text shown in the submit button while onSearch is pending. */
31
+ readonly searchingButtonLabel?: string;
32
+ /** Accessible label for the scope selector chip. */
33
+ readonly scopeSelectorLabel?: string;
34
+ /** Accessible label for the scope clear action. */
35
+ readonly clearScopesLabel?: string;
36
+ /** Formats the selector chip label. */
37
+ readonly scopeLabel?: ScopeLabelFormatter;
38
+ /** Disables all interactive controls. */
39
+ readonly disabled?: boolean;
40
+ /** Adds an extra class to the root element. */
41
+ readonly className?: string;
42
+ /** Optional id prefix used for input/menu ARIA relationships. */
43
+ readonly id?: string;
44
+ /** Maximum menu height in pixels before vertical scrolling. */
45
+ readonly menuMaxHeight?: number;
46
+ };
47
+ type ScopedSearchBarInstance = {
48
+ readonly element: HTMLElement;
49
+ destroy: () => void;
50
+ search: () => Promise<void>;
51
+ clearScopes: () => void;
52
+ closeMenu: () => void;
53
+ openMenu: () => void;
54
+ setDisabled: (disabled: boolean) => void;
55
+ setScopes: (scopes: readonly SearchScope[]) => void;
56
+ setSearchTerm: (term: string) => void;
57
+ setSelectedIds: (ids: readonly string[]) => void;
58
+ getSearchTerm: () => string;
59
+ getSelectedIds: () => readonly string[];
60
+ };
61
+ type RequiredVisualOptions = Required<Pick<ScopedSearchBarOptions, 'placeholder' | 'inputLabel' | 'searchButtonLabel' | 'searchingButtonLabel' | 'scopeSelectorLabel' | 'clearScopesLabel' | 'menuMaxHeight'>>;
62
+ declare const DEFAULT_SCOPED_SEARCH_BAR_OPTIONS: RequiredVisualOptions;
63
+ /** Native, dependency-free scoped search input with multi-select filtering. */
64
+ declare class ScopedSearchBar implements ScopedSearchBarInstance {
65
+ #private;
66
+ readonly element: HTMLElement;
67
+ constructor(container: HTMLElement, options: ScopedSearchBarOptions);
68
+ openMenu(): void;
69
+ closeMenu(): void;
70
+ clearScopes(): void;
71
+ search(): Promise<void>;
72
+ setDisabled(disabled: boolean): void;
73
+ setScopes(scopes: readonly SearchScope[]): void;
74
+ setSearchTerm(term: string): void;
75
+ setSelectedIds(ids: readonly string[]): void;
76
+ getSearchTerm(): string;
77
+ getSelectedIds(): readonly string[];
78
+ destroy(): void;
79
+ }
80
+ declare function createScopedSearchBar(container: HTMLElement, options: ScopedSearchBarOptions): ScopedSearchBarInstance;
81
+ //#endregion
82
+ export { ScopeLabelFormatter as a, ScopedSearchBarInstance as c, createScopedSearchBar as d, RequiredVisualOptions as i, ScopedSearchBarOptions as l, MaybePromise as n, ScopeLabelFormatterContext as o, OnSearch as r, ScopedSearchBar as s, DEFAULT_SCOPED_SEARCH_BAR_OPTIONS as t, SearchScope as u };
83
+ //# sourceMappingURL=scoped-search-bar-D0ppSWb6.d.mts.map
@@ -0,0 +1,310 @@
1
+ //#region src/lib/scoped-search-bar.ts
2
+ const DEFAULT_SCOPED_SEARCH_BAR_OPTIONS = {
3
+ placeholder: "Search for topics, articles, users...",
4
+ inputLabel: "Search",
5
+ searchButtonLabel: "Search",
6
+ searchingButtonLabel: "Searching...",
7
+ scopeSelectorLabel: "Choose search scopes",
8
+ clearScopesLabel: "Clear selected scopes",
9
+ menuMaxHeight: 320
10
+ };
11
+ let instanceCounter = 0;
12
+ function defaultScopeLabel({ count }) {
13
+ if (count === 0) return "All Areas";
14
+ return `${count} ${count === 1 ? "Area" : "Areas"}`;
15
+ }
16
+ function createSvgIcon(pathData, className) {
17
+ const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
18
+ icon.setAttribute("class", className);
19
+ icon.setAttribute("viewBox", "0 0 24 24");
20
+ icon.setAttribute("aria-hidden", "true");
21
+ icon.setAttribute("focusable", "false");
22
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
23
+ path.setAttribute("d", pathData);
24
+ icon.append(path);
25
+ return icon;
26
+ }
27
+ function uniqueScopes(scopes) {
28
+ const seen = /* @__PURE__ */ new Set();
29
+ const result = [];
30
+ for (const scope of scopes) {
31
+ if (seen.has(scope.id)) throw new Error(`Duplicate search scope id: ${scope.id}`);
32
+ seen.add(scope.id);
33
+ result.push(scope);
34
+ }
35
+ return result;
36
+ }
37
+ function normalizeIds(ids, scopes) {
38
+ const validIds = new Set(scopes.map((scope) => scope.id));
39
+ const normalized = [];
40
+ for (const id of ids) if (validIds.has(id) && !normalized.includes(id)) normalized.push(id);
41
+ return normalized;
42
+ }
43
+ /** Native, dependency-free scoped search input with multi-select filtering. */
44
+ var ScopedSearchBar = class {
45
+ element;
46
+ #anchorButton;
47
+ #clearButton;
48
+ #input;
49
+ #menu;
50
+ #options;
51
+ #searchButton;
52
+ #scopes;
53
+ #selectedIds;
54
+ #term;
55
+ #isDisabled;
56
+ #isDestroyed = false;
57
+ #isMenuOpen = false;
58
+ #isSearching = false;
59
+ #documentClickController = new AbortController();
60
+ constructor(container, options) {
61
+ this.#options = options;
62
+ this.#scopes = uniqueScopes(options.scopes);
63
+ this.#selectedIds = normalizeIds(options.initialSelectedIds ?? [], this.#scopes);
64
+ this.#term = options.initialSearchTerm ?? "";
65
+ this.#isDisabled = options.disabled ?? false;
66
+ const idPrefix = options.id ?? `scoped-search-bar-${++instanceCounter}`;
67
+ this.element = document.createElement("div");
68
+ this.element.className = ["scoped-search-bar", options.className].filter(Boolean).join(" ");
69
+ this.element.dataset["component"] = "ScopedSearchBar";
70
+ const control = document.createElement("div");
71
+ control.className = "scoped-search-bar__control";
72
+ const searchIcon = createSvgIcon("M9.5 3a6.5 6.5 0 0 1 5.17 10.44l4.45 4.44-1.42 1.42-4.44-4.45A6.5 6.5 0 1 1 9.5 3Zm0 2a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9Z", "scoped-search-bar__search-icon");
73
+ this.#input = document.createElement("input");
74
+ this.#input.id = `${idPrefix}-input`;
75
+ this.#input.className = "scoped-search-bar__input";
76
+ this.#input.type = "search";
77
+ this.#input.autocomplete = "off";
78
+ this.#input.placeholder = options.placeholder ?? DEFAULT_SCOPED_SEARCH_BAR_OPTIONS.placeholder;
79
+ this.#input.setAttribute("aria-label", options.inputLabel ?? DEFAULT_SCOPED_SEARCH_BAR_OPTIONS.inputLabel);
80
+ this.#input.value = this.#term;
81
+ const actions = document.createElement("div");
82
+ actions.className = "scoped-search-bar__actions";
83
+ this.#anchorButton = document.createElement("button");
84
+ this.#anchorButton.className = "scoped-search-bar__scope-chip";
85
+ this.#anchorButton.type = "button";
86
+ this.#anchorButton.setAttribute("aria-haspopup", "menu");
87
+ this.#anchorButton.setAttribute("aria-controls", `${idPrefix}-menu`);
88
+ this.#anchorButton.setAttribute("aria-expanded", "false");
89
+ this.#anchorButton.setAttribute("aria-label", options.scopeSelectorLabel ?? DEFAULT_SCOPED_SEARCH_BAR_OPTIONS.scopeSelectorLabel);
90
+ this.#clearButton = document.createElement("button");
91
+ this.#clearButton.className = "scoped-search-bar__clear-scopes";
92
+ this.#clearButton.type = "button";
93
+ this.#clearButton.setAttribute("aria-label", options.clearScopesLabel ?? DEFAULT_SCOPED_SEARCH_BAR_OPTIONS.clearScopesLabel);
94
+ this.#clearButton.append(createSvgIcon("M6.4 5 5 6.4 10.6 12 5 17.6 6.4 19 12 13.4 17.6 19 19 17.6 13.4 12 19 6.4 17.6 5 12 10.6 6.4 5Z", "scoped-search-bar__clear-icon"));
95
+ this.#searchButton = document.createElement("button");
96
+ this.#searchButton.className = "scoped-search-bar__submit";
97
+ this.#searchButton.type = "button";
98
+ this.#searchButton.append(createSvgIcon("M9.5 3a6.5 6.5 0 0 1 5.17 10.44l4.45 4.44-1.42 1.42-4.44-4.45A6.5 6.5 0 1 1 9.5 3Zm0 2a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9Z", "scoped-search-bar__submit-icon"), document.createTextNode(options.searchButtonLabel ?? DEFAULT_SCOPED_SEARCH_BAR_OPTIONS.searchButtonLabel));
99
+ actions.append(this.#anchorButton, this.#clearButton, this.#searchButton);
100
+ control.append(searchIcon, this.#input, actions);
101
+ this.#menu = document.createElement("div");
102
+ this.#menu.id = `${idPrefix}-menu`;
103
+ this.#menu.className = "scoped-search-bar__menu";
104
+ this.#menu.role = "menu";
105
+ this.#menu.hidden = true;
106
+ this.#menu.style.maxHeight = `${options.menuMaxHeight ?? DEFAULT_SCOPED_SEARCH_BAR_OPTIONS.menuMaxHeight}px`;
107
+ this.element.append(control, this.#menu);
108
+ container.replaceChildren(this.element);
109
+ this.#bindEvents();
110
+ this.#render();
111
+ }
112
+ openMenu() {
113
+ this.#assertActive();
114
+ if (this.#isDisabled || this.#isSearching) return;
115
+ this.#isMenuOpen = true;
116
+ this.#renderMenuState();
117
+ }
118
+ closeMenu() {
119
+ this.#assertActive();
120
+ this.#isMenuOpen = false;
121
+ this.#renderMenuState();
122
+ }
123
+ clearScopes() {
124
+ this.#assertActive();
125
+ if (this.#isDisabled || this.#isSearching) return;
126
+ this.#selectedIds = [];
127
+ this.#render();
128
+ }
129
+ async search() {
130
+ this.#assertActive();
131
+ if (this.#isDisabled || this.#isSearching) return;
132
+ this.#isSearching = true;
133
+ this.#renderDisabledState();
134
+ try {
135
+ await this.#options.onSearch(this.#term, [...this.#selectedIds]);
136
+ } finally {
137
+ this.#isSearching = false;
138
+ this.#renderDisabledState();
139
+ }
140
+ }
141
+ setDisabled(disabled) {
142
+ this.#assertActive();
143
+ this.#isDisabled = disabled;
144
+ if (disabled) this.#isMenuOpen = false;
145
+ this.#render();
146
+ }
147
+ setScopes(scopes) {
148
+ this.#assertActive();
149
+ this.#scopes = uniqueScopes(scopes);
150
+ this.#selectedIds = normalizeIds(this.#selectedIds, this.#scopes);
151
+ this.#render();
152
+ }
153
+ setSearchTerm(term) {
154
+ this.#assertActive();
155
+ this.#term = term;
156
+ this.#input.value = term;
157
+ }
158
+ setSelectedIds(ids) {
159
+ this.#assertActive();
160
+ this.#selectedIds = normalizeIds(ids, this.#scopes);
161
+ this.#render();
162
+ }
163
+ getSearchTerm() {
164
+ this.#assertActive();
165
+ return this.#term;
166
+ }
167
+ getSelectedIds() {
168
+ this.#assertActive();
169
+ return [...this.#selectedIds];
170
+ }
171
+ destroy() {
172
+ if (this.#isDestroyed) return;
173
+ this.#documentClickController.abort();
174
+ this.element.remove();
175
+ this.#isDestroyed = true;
176
+ }
177
+ #bindEvents() {
178
+ this.#input.addEventListener("input", () => {
179
+ this.#term = this.#input.value;
180
+ });
181
+ this.#input.addEventListener("keydown", (event) => {
182
+ if (event.key === "Enter") {
183
+ event.preventDefault();
184
+ this.search();
185
+ }
186
+ if (event.key === "Escape" && this.#isMenuOpen) this.closeMenu();
187
+ });
188
+ this.#anchorButton.addEventListener("click", () => {
189
+ if (this.#isMenuOpen) {
190
+ this.closeMenu();
191
+ return;
192
+ }
193
+ this.openMenu();
194
+ });
195
+ this.#anchorButton.addEventListener("keydown", (event) => {
196
+ if (event.key === "ArrowDown") {
197
+ event.preventDefault();
198
+ this.openMenu();
199
+ this.#focusMenuItem(0);
200
+ }
201
+ });
202
+ this.#clearButton.addEventListener("click", (event) => {
203
+ event.stopPropagation();
204
+ this.clearScopes();
205
+ });
206
+ this.#searchButton.addEventListener("click", () => {
207
+ this.search();
208
+ });
209
+ document.addEventListener("click", (event) => {
210
+ if (!this.element.contains(event.target)) this.closeMenu();
211
+ }, { signal: this.#documentClickController.signal });
212
+ }
213
+ #render() {
214
+ this.#renderChipLabel();
215
+ this.#renderMenuItems();
216
+ this.#renderDisabledState();
217
+ this.#renderMenuState();
218
+ }
219
+ #renderChipLabel() {
220
+ const formatter = this.#options.scopeLabel ?? defaultScopeLabel;
221
+ this.#anchorButton.textContent = formatter({
222
+ count: this.#selectedIds.length,
223
+ total: this.#scopes.length,
224
+ selectedIds: [...this.#selectedIds]
225
+ });
226
+ this.#clearButton.hidden = this.#selectedIds.length === 0;
227
+ }
228
+ #renderMenuItems() {
229
+ this.#menu.replaceChildren();
230
+ for (const [index, scope] of this.#scopes.entries()) {
231
+ const item = document.createElement("button");
232
+ item.className = "scoped-search-bar__menu-item";
233
+ item.type = "button";
234
+ item.role = "menuitemcheckbox";
235
+ item.dataset["scopeId"] = scope.id;
236
+ item.setAttribute("aria-checked", this.#selectedIds.includes(scope.id) ? "true" : "false");
237
+ item.tabIndex = -1;
238
+ const checkbox = document.createElement("span");
239
+ checkbox.className = "scoped-search-bar__checkbox";
240
+ checkbox.setAttribute("aria-hidden", "true");
241
+ if (this.#selectedIds.includes(scope.id)) checkbox.append(createSvgIcon("M8.6 15.6 4.4 11.4 3 12.8 8.6 18.4 21 6 19.6 4.6 8.6 15.6Z", "scoped-search-bar__check-icon"));
242
+ const label = document.createElement("span");
243
+ label.className = "scoped-search-bar__menu-label";
244
+ label.textContent = scope.label;
245
+ item.append(checkbox, label);
246
+ item.addEventListener("click", () => {
247
+ this.#toggleScope(scope.id);
248
+ });
249
+ item.addEventListener("keydown", (event) => {
250
+ this.#handleMenuKeydown(event, index);
251
+ });
252
+ this.#menu.append(item);
253
+ }
254
+ }
255
+ #renderDisabledState() {
256
+ const disabled = this.#isDisabled || this.#isSearching;
257
+ this.element.classList.toggle("scoped-search-bar--disabled", disabled);
258
+ this.element.classList.toggle("scoped-search-bar--searching", this.#isSearching);
259
+ this.#input.disabled = disabled;
260
+ this.#anchorButton.disabled = disabled;
261
+ this.#clearButton.disabled = disabled;
262
+ this.#searchButton.disabled = disabled;
263
+ this.#searchButton.replaceChildren(createSvgIcon("M9.5 3a6.5 6.5 0 0 1 5.17 10.44l4.45 4.44-1.42 1.42-4.44-4.45A6.5 6.5 0 1 1 9.5 3Zm0 2a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9Z", "scoped-search-bar__submit-icon"), document.createTextNode(this.#isSearching ? this.#options.searchingButtonLabel ?? DEFAULT_SCOPED_SEARCH_BAR_OPTIONS.searchingButtonLabel : this.#options.searchButtonLabel ?? DEFAULT_SCOPED_SEARCH_BAR_OPTIONS.searchButtonLabel));
264
+ if (disabled) {
265
+ this.#isMenuOpen = false;
266
+ this.#renderMenuState();
267
+ }
268
+ }
269
+ #renderMenuState() {
270
+ this.#menu.hidden = !this.#isMenuOpen;
271
+ this.element.classList.toggle("scoped-search-bar--menu-open", this.#isMenuOpen);
272
+ this.#anchorButton.setAttribute("aria-expanded", this.#isMenuOpen ? "true" : "false");
273
+ }
274
+ #toggleScope(id) {
275
+ this.#selectedIds = this.#selectedIds.includes(id) ? this.#selectedIds.filter((selectedId) => selectedId !== id) : [...this.#selectedIds, id];
276
+ this.#render();
277
+ }
278
+ #handleMenuKeydown(event, index) {
279
+ if (event.key === "Escape") {
280
+ event.preventDefault();
281
+ this.closeMenu();
282
+ this.#anchorButton.focus();
283
+ return;
284
+ }
285
+ if (event.key === "ArrowDown") {
286
+ event.preventDefault();
287
+ this.#focusMenuItem(index + 1);
288
+ return;
289
+ }
290
+ if (event.key === "ArrowUp") {
291
+ event.preventDefault();
292
+ this.#focusMenuItem(index - 1);
293
+ }
294
+ }
295
+ #focusMenuItem(index) {
296
+ const items = [...this.#menu.querySelectorAll(".scoped-search-bar__menu-item")];
297
+ if (items.length === 0) return;
298
+ items[(index + items.length) % items.length]?.focus();
299
+ }
300
+ #assertActive() {
301
+ if (this.#isDestroyed) throw new Error("ScopedSearchBar instance has been destroyed");
302
+ }
303
+ };
304
+ function createScopedSearchBar(container, options) {
305
+ return new ScopedSearchBar(container, options);
306
+ }
307
+ //#endregion
308
+ export { ScopedSearchBar as n, createScopedSearchBar as r, DEFAULT_SCOPED_SEARCH_BAR_OPTIONS as t };
309
+
310
+ //# sourceMappingURL=scoped-search-bar-D8zVpZ4L.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scoped-search-bar-D8zVpZ4L.mjs","names":["#anchorButton","#clearButton","#input","#menu","#options","#searchButton","#documentClickController","#scopes","#selectedIds","#term","#isDisabled","#bindEvents","#render","#assertActive","#isSearching","#isMenuOpen","#renderMenuState","#renderDisabledState","#isDestroyed","#focusMenuItem","#renderChipLabel","#renderMenuItems","#toggleScope","#handleMenuKeydown"],"sources":["../src/lib/scoped-search-bar.ts"],"sourcesContent":["export type MaybePromise<T> = T | Promise<T>;\n\n/** A selectable search scope shown in the dropdown menu. */\nexport type SearchScope = {\n\treadonly id: string;\n\treadonly label: string;\n};\n\nexport type ScopeLabelFormatterContext = {\n\treadonly count: number;\n\treadonly total: number;\n\treadonly selectedIds: readonly string[];\n};\n\nexport type ScopeLabelFormatter = (context: ScopeLabelFormatterContext) => string;\nexport type OnSearch = (term: string, selectedScopeIds: readonly string[]) => MaybePromise<void>;\n\nexport type ScopedSearchBarOptions = {\n\t/** Scopes available in the selector menu. IDs must be unique. */\n\treadonly scopes: readonly SearchScope[];\n\t/** Scope IDs selected on initial render. Unknown IDs are ignored. */\n\treadonly initialSelectedIds?: readonly string[];\n\t/** Initial search input value. */\n\treadonly initialSearchTerm?: string;\n\t/** Called when the user clicks Search or presses Enter in the input. */\n\treadonly onSearch: OnSearch;\n\t/** Placeholder shown in the search input. */\n\treadonly placeholder?: string;\n\t/** Accessible label for the search input. */\n\treadonly inputLabel?: string;\n\t/** Text shown in the submit button while idle. */\n\treadonly searchButtonLabel?: string;\n\t/** Text shown in the submit button while onSearch is pending. */\n\treadonly searchingButtonLabel?: string;\n\t/** Accessible label for the scope selector chip. */\n\treadonly scopeSelectorLabel?: string;\n\t/** Accessible label for the scope clear action. */\n\treadonly clearScopesLabel?: string;\n\t/** Formats the selector chip label. */\n\treadonly scopeLabel?: ScopeLabelFormatter;\n\t/** Disables all interactive controls. */\n\treadonly disabled?: boolean;\n\t/** Adds an extra class to the root element. */\n\treadonly className?: string;\n\t/** Optional id prefix used for input/menu ARIA relationships. */\n\treadonly id?: string;\n\t/** Maximum menu height in pixels before vertical scrolling. */\n\treadonly menuMaxHeight?: number;\n};\n\nexport type ScopedSearchBarInstance = {\n\treadonly element: HTMLElement;\n\tdestroy: () => void;\n\tsearch: () => Promise<void>;\n\tclearScopes: () => void;\n\tcloseMenu: () => void;\n\topenMenu: () => void;\n\tsetDisabled: (disabled: boolean) => void;\n\tsetScopes: (scopes: readonly SearchScope[]) => void;\n\tsetSearchTerm: (term: string) => void;\n\tsetSelectedIds: (ids: readonly string[]) => void;\n\tgetSearchTerm: () => string;\n\tgetSelectedIds: () => readonly string[];\n};\n\nexport type RequiredVisualOptions = Required<\n\tPick<\n\t\tScopedSearchBarOptions,\n\t\t'placeholder' | 'inputLabel' | 'searchButtonLabel' | 'searchingButtonLabel' | 'scopeSelectorLabel' | 'clearScopesLabel' | 'menuMaxHeight'\n\t>\n>;\n\nexport const DEFAULT_SCOPED_SEARCH_BAR_OPTIONS: RequiredVisualOptions = {\n\tplaceholder: 'Search for topics, articles, users...',\n\tinputLabel: 'Search',\n\tsearchButtonLabel: 'Search',\n\tsearchingButtonLabel: 'Searching...',\n\tscopeSelectorLabel: 'Choose search scopes',\n\tclearScopesLabel: 'Clear selected scopes',\n\tmenuMaxHeight: 320,\n};\n\nlet instanceCounter = 0;\n\nfunction defaultScopeLabel({count}: ScopeLabelFormatterContext): string {\n\tif (count === 0) {\n\t\treturn 'All Areas';\n\t}\n\treturn `${count} ${count === 1 ? 'Area' : 'Areas'}`;\n}\n\nfunction createSvgIcon(pathData: string, className: string): SVGSVGElement {\n\tconst icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n\ticon.setAttribute('class', className);\n\ticon.setAttribute('viewBox', '0 0 24 24');\n\ticon.setAttribute('aria-hidden', 'true');\n\ticon.setAttribute('focusable', 'false');\n\n\tconst path = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n\tpath.setAttribute('d', pathData);\n\ticon.append(path);\n\treturn icon;\n}\n\nfunction uniqueScopes(scopes: readonly SearchScope[]): SearchScope[] {\n\tconst seen = new Set<string>();\n\tconst result: SearchScope[] = [];\n\tfor (const scope of scopes) {\n\t\tif (seen.has(scope.id)) {\n\t\t\tthrow new Error(`Duplicate search scope id: ${scope.id}`);\n\t\t}\n\t\tseen.add(scope.id);\n\t\tresult.push(scope);\n\t}\n\treturn result;\n}\n\nfunction normalizeIds(ids: readonly string[], scopes: readonly SearchScope[]): string[] {\n\tconst validIds = new Set(scopes.map((scope) => scope.id));\n\tconst normalized: string[] = [];\n\tfor (const id of ids) {\n\t\tif (validIds.has(id) && !normalized.includes(id)) {\n\t\t\tnormalized.push(id);\n\t\t}\n\t}\n\treturn normalized;\n}\n\n/** Native, dependency-free scoped search input with multi-select filtering. */\nexport class ScopedSearchBar implements ScopedSearchBarInstance {\n\tpublic readonly element: HTMLElement;\n\n\treadonly #anchorButton: HTMLButtonElement;\n\treadonly #clearButton: HTMLButtonElement;\n\treadonly #input: HTMLInputElement;\n\treadonly #menu: HTMLDivElement;\n\treadonly #options: ScopedSearchBarOptions;\n\treadonly #searchButton: HTMLButtonElement;\n\t#scopes: SearchScope[];\n\t#selectedIds: string[];\n\t#term: string;\n\t#isDisabled: boolean;\n\t#isDestroyed = false;\n\t#isMenuOpen = false;\n\t#isSearching = false;\n\treadonly #documentClickController = new AbortController();\n\n\tpublic constructor(container: HTMLElement, options: ScopedSearchBarOptions) {\n\t\tthis.#options = options;\n\t\tthis.#scopes = uniqueScopes(options.scopes);\n\t\tthis.#selectedIds = normalizeIds(options.initialSelectedIds ?? [], this.#scopes);\n\t\tthis.#term = options.initialSearchTerm ?? '';\n\t\tthis.#isDisabled = options.disabled ?? false;\n\n\t\tconst idPrefix = options.id ?? `scoped-search-bar-${++instanceCounter}`;\n\t\tthis.element = document.createElement('div');\n\t\tthis.element.className = ['scoped-search-bar', options.className].filter(Boolean).join(' ');\n\t\tthis.element.dataset['component'] = 'ScopedSearchBar';\n\n\t\tconst control = document.createElement('div');\n\t\tcontrol.className = 'scoped-search-bar__control';\n\n\t\tconst searchIcon = createSvgIcon(\n\t\t\t'M9.5 3a6.5 6.5 0 0 1 5.17 10.44l4.45 4.44-1.42 1.42-4.44-4.45A6.5 6.5 0 1 1 9.5 3Zm0 2a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9Z',\n\t\t\t'scoped-search-bar__search-icon',\n\t\t);\n\n\t\tthis.#input = document.createElement('input');\n\t\tthis.#input.id = `${idPrefix}-input`;\n\t\tthis.#input.className = 'scoped-search-bar__input';\n\t\tthis.#input.type = 'search';\n\t\tthis.#input.autocomplete = 'off';\n\t\tthis.#input.placeholder = options.placeholder ?? DEFAULT_SCOPED_SEARCH_BAR_OPTIONS.placeholder;\n\t\tthis.#input.setAttribute('aria-label', options.inputLabel ?? DEFAULT_SCOPED_SEARCH_BAR_OPTIONS.inputLabel);\n\t\tthis.#input.value = this.#term;\n\n\t\tconst actions = document.createElement('div');\n\t\tactions.className = 'scoped-search-bar__actions';\n\n\t\tthis.#anchorButton = document.createElement('button');\n\t\tthis.#anchorButton.className = 'scoped-search-bar__scope-chip';\n\t\tthis.#anchorButton.type = 'button';\n\t\tthis.#anchorButton.setAttribute('aria-haspopup', 'menu');\n\t\tthis.#anchorButton.setAttribute('aria-controls', `${idPrefix}-menu`);\n\t\tthis.#anchorButton.setAttribute('aria-expanded', 'false');\n\t\tthis.#anchorButton.setAttribute('aria-label', options.scopeSelectorLabel ?? DEFAULT_SCOPED_SEARCH_BAR_OPTIONS.scopeSelectorLabel);\n\n\t\tthis.#clearButton = document.createElement('button');\n\t\tthis.#clearButton.className = 'scoped-search-bar__clear-scopes';\n\t\tthis.#clearButton.type = 'button';\n\t\tthis.#clearButton.setAttribute('aria-label', options.clearScopesLabel ?? DEFAULT_SCOPED_SEARCH_BAR_OPTIONS.clearScopesLabel);\n\t\tthis.#clearButton.append(\n\t\t\tcreateSvgIcon('M6.4 5 5 6.4 10.6 12 5 17.6 6.4 19 12 13.4 17.6 19 19 17.6 13.4 12 19 6.4 17.6 5 12 10.6 6.4 5Z', 'scoped-search-bar__clear-icon'),\n\t\t);\n\n\t\tthis.#searchButton = document.createElement('button');\n\t\tthis.#searchButton.className = 'scoped-search-bar__submit';\n\t\tthis.#searchButton.type = 'button';\n\t\tthis.#searchButton.append(\n\t\t\tcreateSvgIcon(\n\t\t\t\t'M9.5 3a6.5 6.5 0 0 1 5.17 10.44l4.45 4.44-1.42 1.42-4.44-4.45A6.5 6.5 0 1 1 9.5 3Zm0 2a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9Z',\n\t\t\t\t'scoped-search-bar__submit-icon',\n\t\t\t),\n\t\t\tdocument.createTextNode(options.searchButtonLabel ?? DEFAULT_SCOPED_SEARCH_BAR_OPTIONS.searchButtonLabel),\n\t\t);\n\n\t\tactions.append(this.#anchorButton, this.#clearButton, this.#searchButton);\n\t\tcontrol.append(searchIcon, this.#input, actions);\n\n\t\tthis.#menu = document.createElement('div');\n\t\tthis.#menu.id = `${idPrefix}-menu`;\n\t\tthis.#menu.className = 'scoped-search-bar__menu';\n\t\tthis.#menu.role = 'menu';\n\t\tthis.#menu.hidden = true;\n\t\tthis.#menu.style.maxHeight = `${options.menuMaxHeight ?? DEFAULT_SCOPED_SEARCH_BAR_OPTIONS.menuMaxHeight}px`;\n\n\t\tthis.element.append(control, this.#menu);\n\t\tcontainer.replaceChildren(this.element);\n\n\t\tthis.#bindEvents();\n\t\tthis.#render();\n\t}\n\n\tpublic openMenu(): void {\n\t\tthis.#assertActive();\n\t\tif (this.#isDisabled || this.#isSearching) {\n\t\t\treturn;\n\t\t}\n\t\tthis.#isMenuOpen = true;\n\t\tthis.#renderMenuState();\n\t}\n\n\tpublic closeMenu(): void {\n\t\tthis.#assertActive();\n\t\tthis.#isMenuOpen = false;\n\t\tthis.#renderMenuState();\n\t}\n\n\tpublic clearScopes(): void {\n\t\tthis.#assertActive();\n\t\tif (this.#isDisabled || this.#isSearching) {\n\t\t\treturn;\n\t\t}\n\t\tthis.#selectedIds = [];\n\t\tthis.#render();\n\t}\n\n\tpublic async search(): Promise<void> {\n\t\tthis.#assertActive();\n\t\tif (this.#isDisabled || this.#isSearching) {\n\t\t\treturn;\n\t\t}\n\t\tthis.#isSearching = true;\n\t\tthis.#renderDisabledState();\n\t\ttry {\n\t\t\tawait this.#options.onSearch(this.#term, [...this.#selectedIds]);\n\t\t} finally {\n\t\t\tthis.#isSearching = false;\n\t\t\tthis.#renderDisabledState();\n\t\t}\n\t}\n\n\tpublic setDisabled(disabled: boolean): void {\n\t\tthis.#assertActive();\n\t\tthis.#isDisabled = disabled;\n\t\tif (disabled) {\n\t\t\tthis.#isMenuOpen = false;\n\t\t}\n\t\tthis.#render();\n\t}\n\n\tpublic setScopes(scopes: readonly SearchScope[]): void {\n\t\tthis.#assertActive();\n\t\tthis.#scopes = uniqueScopes(scopes);\n\t\tthis.#selectedIds = normalizeIds(this.#selectedIds, this.#scopes);\n\t\tthis.#render();\n\t}\n\n\tpublic setSearchTerm(term: string): void {\n\t\tthis.#assertActive();\n\t\tthis.#term = term;\n\t\tthis.#input.value = term;\n\t}\n\n\tpublic setSelectedIds(ids: readonly string[]): void {\n\t\tthis.#assertActive();\n\t\tthis.#selectedIds = normalizeIds(ids, this.#scopes);\n\t\tthis.#render();\n\t}\n\n\tpublic getSearchTerm(): string {\n\t\tthis.#assertActive();\n\t\treturn this.#term;\n\t}\n\n\tpublic getSelectedIds(): readonly string[] {\n\t\tthis.#assertActive();\n\t\treturn [...this.#selectedIds];\n\t}\n\n\tpublic destroy(): void {\n\t\tif (this.#isDestroyed) {\n\t\t\treturn;\n\t\t}\n\t\tthis.#documentClickController.abort();\n\t\tthis.element.remove();\n\t\tthis.#isDestroyed = true;\n\t}\n\n\t#bindEvents(): void {\n\t\tthis.#input.addEventListener('input', () => {\n\t\t\tthis.#term = this.#input.value;\n\t\t});\n\t\tthis.#input.addEventListener('keydown', (event) => {\n\t\t\tif (event.key === 'Enter') {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tvoid this.search();\n\t\t\t}\n\t\t\tif (event.key === 'Escape' && this.#isMenuOpen) {\n\t\t\t\tthis.closeMenu();\n\t\t\t}\n\t\t});\n\n\t\tthis.#anchorButton.addEventListener('click', () => {\n\t\t\tif (this.#isMenuOpen) {\n\t\t\t\tthis.closeMenu();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.openMenu();\n\t\t});\n\t\tthis.#anchorButton.addEventListener('keydown', (event) => {\n\t\t\tif (event.key === 'ArrowDown') {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tthis.openMenu();\n\t\t\t\tthis.#focusMenuItem(0);\n\t\t\t}\n\t\t});\n\n\t\tthis.#clearButton.addEventListener('click', (event) => {\n\t\t\tevent.stopPropagation();\n\t\t\tthis.clearScopes();\n\t\t});\n\t\tthis.#searchButton.addEventListener('click', () => {\n\t\t\tvoid this.search();\n\t\t});\n\n\t\tdocument.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\tif (!this.element.contains(event.target as Node)) {\n\t\t\t\t\tthis.closeMenu();\n\t\t\t\t}\n\t\t\t},\n\t\t\t{signal: this.#documentClickController.signal},\n\t\t);\n\t}\n\n\t#render(): void {\n\t\tthis.#renderChipLabel();\n\t\tthis.#renderMenuItems();\n\t\tthis.#renderDisabledState();\n\t\tthis.#renderMenuState();\n\t}\n\n\t#renderChipLabel(): void {\n\t\tconst formatter = this.#options.scopeLabel ?? defaultScopeLabel;\n\t\tthis.#anchorButton.textContent = formatter({count: this.#selectedIds.length, total: this.#scopes.length, selectedIds: [...this.#selectedIds]});\n\t\tthis.#clearButton.hidden = this.#selectedIds.length === 0;\n\t}\n\n\t#renderMenuItems(): void {\n\t\tthis.#menu.replaceChildren();\n\t\tfor (const [index, scope] of this.#scopes.entries()) {\n\t\t\tconst item = document.createElement('button');\n\t\t\titem.className = 'scoped-search-bar__menu-item';\n\t\t\titem.type = 'button';\n\t\t\titem.role = 'menuitemcheckbox';\n\t\t\titem.dataset['scopeId'] = scope.id;\n\t\t\titem.setAttribute('aria-checked', this.#selectedIds.includes(scope.id) ? 'true' : 'false');\n\t\t\titem.tabIndex = -1;\n\n\t\t\tconst checkbox = document.createElement('span');\n\t\t\tcheckbox.className = 'scoped-search-bar__checkbox';\n\t\t\tcheckbox.setAttribute('aria-hidden', 'true');\n\t\t\tif (this.#selectedIds.includes(scope.id)) {\n\t\t\t\tcheckbox.append(createSvgIcon('M8.6 15.6 4.4 11.4 3 12.8 8.6 18.4 21 6 19.6 4.6 8.6 15.6Z', 'scoped-search-bar__check-icon'));\n\t\t\t}\n\n\t\t\tconst label = document.createElement('span');\n\t\t\tlabel.className = 'scoped-search-bar__menu-label';\n\t\t\tlabel.textContent = scope.label;\n\n\t\t\titem.append(checkbox, label);\n\t\t\titem.addEventListener('click', () => {\n\t\t\t\tthis.#toggleScope(scope.id);\n\t\t\t});\n\t\t\titem.addEventListener('keydown', (event) => {\n\t\t\t\tthis.#handleMenuKeydown(event, index);\n\t\t\t});\n\t\t\tthis.#menu.append(item);\n\t\t}\n\t}\n\n\t#renderDisabledState(): void {\n\t\tconst disabled = this.#isDisabled || this.#isSearching;\n\t\tthis.element.classList.toggle('scoped-search-bar--disabled', disabled);\n\t\tthis.element.classList.toggle('scoped-search-bar--searching', this.#isSearching);\n\t\tthis.#input.disabled = disabled;\n\t\tthis.#anchorButton.disabled = disabled;\n\t\tthis.#clearButton.disabled = disabled;\n\t\tthis.#searchButton.disabled = disabled;\n\t\tthis.#searchButton.replaceChildren(\n\t\t\tcreateSvgIcon(\n\t\t\t\t'M9.5 3a6.5 6.5 0 0 1 5.17 10.44l4.45 4.44-1.42 1.42-4.44-4.45A6.5 6.5 0 1 1 9.5 3Zm0 2a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9Z',\n\t\t\t\t'scoped-search-bar__submit-icon',\n\t\t\t),\n\t\t\tdocument.createTextNode(\n\t\t\t\tthis.#isSearching\n\t\t\t\t\t? (this.#options.searchingButtonLabel ?? DEFAULT_SCOPED_SEARCH_BAR_OPTIONS.searchingButtonLabel)\n\t\t\t\t\t: (this.#options.searchButtonLabel ?? DEFAULT_SCOPED_SEARCH_BAR_OPTIONS.searchButtonLabel),\n\t\t\t),\n\t\t);\n\t\tif (disabled) {\n\t\t\tthis.#isMenuOpen = false;\n\t\t\tthis.#renderMenuState();\n\t\t}\n\t}\n\n\t#renderMenuState(): void {\n\t\tthis.#menu.hidden = !this.#isMenuOpen;\n\t\tthis.element.classList.toggle('scoped-search-bar--menu-open', this.#isMenuOpen);\n\t\tthis.#anchorButton.setAttribute('aria-expanded', this.#isMenuOpen ? 'true' : 'false');\n\t}\n\n\t#toggleScope(id: string): void {\n\t\tthis.#selectedIds = this.#selectedIds.includes(id) ? this.#selectedIds.filter((selectedId) => selectedId !== id) : [...this.#selectedIds, id];\n\t\tthis.#render();\n\t}\n\n\t#handleMenuKeydown(event: KeyboardEvent, index: number): void {\n\t\tif (event.key === 'Escape') {\n\t\t\tevent.preventDefault();\n\t\t\tthis.closeMenu();\n\t\t\tthis.#anchorButton.focus();\n\t\t\treturn;\n\t\t}\n\t\tif (event.key === 'ArrowDown') {\n\t\t\tevent.preventDefault();\n\t\t\tthis.#focusMenuItem(index + 1);\n\t\t\treturn;\n\t\t}\n\t\tif (event.key === 'ArrowUp') {\n\t\t\tevent.preventDefault();\n\t\t\tthis.#focusMenuItem(index - 1);\n\t\t}\n\t}\n\n\t#focusMenuItem(index: number): void {\n\t\tconst items = [...this.#menu.querySelectorAll<HTMLButtonElement>('.scoped-search-bar__menu-item')];\n\t\tif (items.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst normalizedIndex = (index + items.length) % items.length;\n\t\titems[normalizedIndex]?.focus();\n\t}\n\n\t#assertActive(): void {\n\t\tif (this.#isDestroyed) {\n\t\t\tthrow new Error('ScopedSearchBar instance has been destroyed');\n\t\t}\n\t}\n}\n\nexport function createScopedSearchBar(container: HTMLElement, options: ScopedSearchBarOptions): ScopedSearchBarInstance {\n\treturn new ScopedSearchBar(container, options);\n}\n"],"mappings":"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"}
@@ -0,0 +1,233 @@
1
+ :root {
2
+ --scoped-search-bar-font-family: inter, "Segoe UI", system-ui, sans-serif;
3
+ --scoped-search-bar-bg: #fff;
4
+ --scoped-search-bar-bg-muted: #f6f8fb;
5
+ --scoped-search-bar-border: #0876d8;
6
+ --scoped-search-bar-border-muted: #d7dee8;
7
+ --scoped-search-bar-text: #17212f;
8
+ --scoped-search-bar-text-muted: #677489;
9
+ --scoped-search-bar-primary: #0a4f8f;
10
+ --scoped-search-bar-primary-strong: #073b6f;
11
+ --scoped-search-bar-primary-soft: #d9ecff;
12
+ --scoped-search-bar-shadow: 0 14px 32px rgba(9, 30, 66, 0.16);
13
+ --scoped-search-bar-focus: 0 0 0 3px rgba(8, 118, 216, 0.22);
14
+ --scoped-search-bar-radius: 999px;
15
+ }
16
+
17
+ .scoped-search-bar {
18
+ position: relative;
19
+ width: min(100%, 980px);
20
+ font-family: var(--scoped-search-bar-font-family);
21
+ color: var(--scoped-search-bar-text);
22
+ }
23
+
24
+ .scoped-search-bar__control {
25
+ display: flex;
26
+ align-items: center;
27
+ gap: 12px;
28
+ min-height: 64px;
29
+ padding: 8px 10px 8px 20px;
30
+ background: var(--scoped-search-bar-bg);
31
+ border: 2px solid var(--scoped-search-bar-border);
32
+ border-radius: var(--scoped-search-bar-radius);
33
+ box-shadow: var(--scoped-search-bar-shadow);
34
+ }
35
+
36
+ .scoped-search-bar__control:focus-within {
37
+ box-shadow: var(--scoped-search-bar-shadow), var(--scoped-search-bar-focus);
38
+ }
39
+
40
+ .scoped-search-bar__search-icon {
41
+ flex: 0 0 auto;
42
+ width: 28px;
43
+ height: 28px;
44
+ fill: #7a8492;
45
+ }
46
+
47
+ .scoped-search-bar__input {
48
+ min-width: 120px;
49
+ flex: 1 1 auto;
50
+ border: 0;
51
+ outline: 0;
52
+ font: inherit;
53
+ font-size: 18px;
54
+ color: var(--scoped-search-bar-text);
55
+ background: transparent;
56
+ }
57
+
58
+ .scoped-search-bar__input::placeholder {
59
+ color: var(--scoped-search-bar-text-muted);
60
+ }
61
+
62
+ .scoped-search-bar__actions {
63
+ display: flex;
64
+ align-items: center;
65
+ gap: 10px;
66
+ flex: 0 0 auto;
67
+ }
68
+
69
+ .scoped-search-bar__scope-chip,
70
+ .scoped-search-bar__clear-scopes,
71
+ .scoped-search-bar__submit,
72
+ .scoped-search-bar__menu-item {
73
+ font: inherit;
74
+ }
75
+
76
+ .scoped-search-bar__scope-chip {
77
+ min-height: 34px;
78
+ padding: 0 14px;
79
+ border: 0;
80
+ border-radius: 999px;
81
+ color: var(--scoped-search-bar-text);
82
+ background: var(--scoped-search-bar-primary-soft);
83
+ cursor: pointer;
84
+ white-space: nowrap;
85
+ }
86
+
87
+ .scoped-search-bar__scope-chip:hover {
88
+ background: #c5e1ff;
89
+ }
90
+
91
+ .scoped-search-bar__clear-scopes {
92
+ display: inline-flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ width: 26px;
96
+ height: 26px;
97
+ margin-left: -18px;
98
+ border: 0;
99
+ border-radius: 999px;
100
+ background: transparent;
101
+ cursor: pointer;
102
+ }
103
+
104
+ .scoped-search-bar__clear-scopes[hidden] {
105
+ display: none;
106
+ }
107
+
108
+ .scoped-search-bar__clear-icon {
109
+ width: 16px;
110
+ height: 16px;
111
+ fill: #4f647a;
112
+ }
113
+
114
+ .scoped-search-bar__submit {
115
+ display: inline-flex;
116
+ align-items: center;
117
+ justify-content: center;
118
+ gap: 8px;
119
+ min-height: 48px;
120
+ padding: 0 22px;
121
+ border: 0;
122
+ border-radius: 999px;
123
+ color: #fff;
124
+ background: linear-gradient(180deg, var(--scoped-search-bar-primary), var(--scoped-search-bar-primary-strong));
125
+ box-shadow: 0 8px 18px rgba(10, 79, 143, 0.28);
126
+ cursor: pointer;
127
+ white-space: nowrap;
128
+ }
129
+
130
+ .scoped-search-bar__submit:hover {
131
+ filter: brightness(1.04);
132
+ }
133
+
134
+ .scoped-search-bar__submit-icon {
135
+ width: 24px;
136
+ height: 24px;
137
+ fill: currentcolor;
138
+ }
139
+
140
+ .scoped-search-bar__menu {
141
+ position: absolute;
142
+ top: calc(100% + 10px);
143
+ right: 120px;
144
+ z-index: 20;
145
+ min-width: 220px;
146
+ overflow-y: auto;
147
+ padding: 10px 0;
148
+ background: var(--scoped-search-bar-bg);
149
+ border: 1px solid var(--scoped-search-bar-border-muted);
150
+ border-radius: 8px;
151
+ box-shadow: var(--scoped-search-bar-shadow);
152
+ }
153
+
154
+ .scoped-search-bar__menu-item {
155
+ display: flex;
156
+ align-items: center;
157
+ gap: 12px;
158
+ width: 100%;
159
+ padding: 8px 18px;
160
+ border: 0;
161
+ color: var(--scoped-search-bar-text);
162
+ background: transparent;
163
+ font-size: 18px;
164
+ text-align: left;
165
+ cursor: pointer;
166
+ }
167
+
168
+ .scoped-search-bar__menu-item:hover,
169
+ .scoped-search-bar__menu-item:focus-visible {
170
+ background: var(--scoped-search-bar-bg-muted);
171
+ outline: 0;
172
+ }
173
+
174
+ .scoped-search-bar__checkbox {
175
+ display: inline-flex;
176
+ align-items: center;
177
+ justify-content: center;
178
+ width: 22px;
179
+ height: 22px;
180
+ border: 2px solid #707987;
181
+ border-radius: 3px;
182
+ background: #fff;
183
+ }
184
+
185
+ .scoped-search-bar__menu-item[aria-checked="true"] .scoped-search-bar__checkbox {
186
+ border-color: var(--scoped-search-bar-primary);
187
+ background: var(--scoped-search-bar-primary);
188
+ }
189
+
190
+ .scoped-search-bar__check-icon {
191
+ width: 16px;
192
+ height: 16px;
193
+ fill: #fff;
194
+ }
195
+
196
+ .scoped-search-bar--disabled {
197
+ opacity: 0.68;
198
+ }
199
+
200
+ .scoped-search-bar--disabled .scoped-search-bar__scope-chip,
201
+ .scoped-search-bar--disabled .scoped-search-bar__clear-scopes,
202
+ .scoped-search-bar--disabled .scoped-search-bar__submit {
203
+ cursor: not-allowed;
204
+ }
205
+
206
+ @media (width <= 700px) {
207
+ .scoped-search-bar__control {
208
+ align-items: stretch;
209
+ flex-wrap: wrap;
210
+ border-radius: 28px;
211
+ padding: 12px;
212
+ }
213
+
214
+ .scoped-search-bar__search-icon {
215
+ margin-top: 10px;
216
+ }
217
+
218
+ .scoped-search-bar__input {
219
+ min-height: 44px;
220
+ font-size: 16px;
221
+ }
222
+
223
+ .scoped-search-bar__actions {
224
+ width: 100%;
225
+ justify-content: flex-end;
226
+ }
227
+
228
+ .scoped-search-bar__menu {
229
+ right: 12px;
230
+ left: 12px;
231
+ min-width: auto;
232
+ }
233
+ }
package/package.json ADDED
@@ -0,0 +1,98 @@
1
+ {
2
+ "name": "scoped-search-bar",
3
+ "version": "0.2.0",
4
+ "description": "A dependency-free TypeScript scoped search input with multi-select filters",
5
+ "keywords": [
6
+ "component",
7
+ "filter",
8
+ "search",
9
+ "typescript",
10
+ "vanilla"
11
+ ],
12
+ "homepage": "https://doberkofler.github.io/scoped-search-bar/",
13
+ "bugs": {
14
+ "url": "https://github.com/doberkofler/scoped-search-bar/issues"
15
+ },
16
+ "license": "MIT",
17
+ "author": "doberkofler",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/doberkofler/scoped-search-bar.git"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "CHANGELOG.md"
25
+ ],
26
+ "type": "module",
27
+ "main": "./dist/index.mjs",
28
+ "module": "./dist/index.mjs",
29
+ "types": "./dist/index.d.mts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.mts",
33
+ "import": "./dist/index.mjs",
34
+ "default": "./dist/index.mjs"
35
+ },
36
+ "./react": {
37
+ "types": "./dist/react.d.mts",
38
+ "import": "./dist/react.mjs",
39
+ "default": "./dist/react.mjs"
40
+ },
41
+ "./styles/scoped-search-bar.css": "./dist/styles/scoped-search-bar.css"
42
+ },
43
+ "devDependencies": {
44
+ "@commitlint/cli": "21.2.1",
45
+ "@commitlint/config-conventional": "21.2.0",
46
+ "@playwright/test": "1.61.1",
47
+ "@tsdown/css": "0.22.9",
48
+ "@types/node": "26.1.1",
49
+ "@types/react": "19.2.17",
50
+ "@types/react-dom": "19.2.3",
51
+ "@vitest/browser": "4.1.10",
52
+ "@vitest/browser-playwright": "4.1.10",
53
+ "@vitest/coverage-istanbul": "4.1.10",
54
+ "@vitest/coverage-v8": "4.1.10",
55
+ "conventional-changelog": "8.1.0",
56
+ "conventional-changelog-angular": "9.2.1",
57
+ "eslint-plugin-regexp": "3.1.1",
58
+ "husky": "9.1.7",
59
+ "oxfmt": "0.59.0",
60
+ "oxlint": "1.74.0",
61
+ "oxlint-tsgolint": "0.25.0",
62
+ "playwright": "1.61.1",
63
+ "react": "19.2.7",
64
+ "react-dom": "19.2.7",
65
+ "release-it": "20.2.1",
66
+ "stylelint": "17.14.0",
67
+ "stylelint-config-standard": "40.0.0",
68
+ "tsdown": "0.22.9",
69
+ "typedoc": "0.28.20",
70
+ "typescript": "6.0.3",
71
+ "vite": "8.1.5",
72
+ "vitest": "4.1.10"
73
+ },
74
+ "peerDependencies": {
75
+ "react": ">=18.0.0"
76
+ },
77
+ "scripts": {
78
+ "typecheck": "tsc --noEmit",
79
+ "lint": "oxlint",
80
+ "lint:css": "stylelint 'src/{demo,styles}/**/*.css'",
81
+ "lint:css:fix": "stylelint --fix 'src/{demo,styles}/**/*.css'",
82
+ "format": "oxfmt --write && stylelint --fix 'src/{demo,styles}/**/*.css'",
83
+ "format:check": "oxfmt --check",
84
+ "ci": "pnpm run typecheck && pnpm run lint && pnpm run lint:css && pnpm run format:check && pnpm run build:lib && pnpm run build && pnpm run docs:api && pnpm run test",
85
+ "test": "vitest run",
86
+ "release": "release-it",
87
+ "create-changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
88
+ "dev": "vite",
89
+ "build": "vite build",
90
+ "build:lib": "tsdown",
91
+ "build:demo": "vite build",
92
+ "docs:api": "typedoc",
93
+ "preview": "vite preview",
94
+ "test:ui": "vitest",
95
+ "integration-test": "playwright test",
96
+ "screenshot": "node scripts/screenshot.mjs"
97
+ }
98
+ }