@chai-ui/react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +207 -0
- package/LICENSE +21 -0
- package/README.md +265 -0
- package/dist/ChaiProvider.d.ts +49 -0
- package/dist/ChaiProvider.d.ts.map +1 -0
- package/dist/ChaiProvider.js +54 -0
- package/dist/ChaiProvider.js.map +1 -0
- package/dist/Composer.d.ts +169 -0
- package/dist/Composer.d.ts.map +1 -0
- package/dist/Composer.js +389 -0
- package/dist/Composer.js.map +1 -0
- package/dist/MediaAnalyzer.d.ts +102 -0
- package/dist/MediaAnalyzer.d.ts.map +1 -0
- package/dist/MediaAnalyzer.js +250 -0
- package/dist/MediaAnalyzer.js.map +1 -0
- package/dist/ResultCard.d.ts +55 -0
- package/dist/ResultCard.d.ts.map +1 -0
- package/dist/ResultCard.js +409 -0
- package/dist/ResultCard.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/primitives/Pagination.d.ts +19 -0
- package/dist/primitives/Pagination.d.ts.map +1 -0
- package/dist/primitives/Pagination.js +13 -0
- package/dist/primitives/Pagination.js.map +1 -0
- package/dist/primitives/SearchMenu.d.ts +69 -0
- package/dist/primitives/SearchMenu.d.ts.map +1 -0
- package/dist/primitives/SearchMenu.js +88 -0
- package/dist/primitives/SearchMenu.js.map +1 -0
- package/dist/primitives/Toggle.d.ts +9 -0
- package/dist/primitives/Toggle.d.ts.map +1 -0
- package/dist/primitives/Toggle.js +28 -0
- package/dist/primitives/Toggle.js.map +1 -0
- package/dist/primitives/index.d.ts +4 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +4 -0
- package/dist/primitives/index.js.map +1 -0
- package/dist/style.css +1860 -0
- package/dist/use-latest.d.ts +7 -0
- package/dist/use-latest.d.ts.map +1 -0
- package/dist/use-latest.js +17 -0
- package/dist/use-latest.js.map +1 -0
- package/dist/useComposer.d.ts +75 -0
- package/dist/useComposer.d.ts.map +1 -0
- package/dist/useComposer.js +191 -0
- package/dist/useComposer.js.map +1 -0
- package/dist/useMediaAnalyzer.d.ts +37 -0
- package/dist/useMediaAnalyzer.d.ts.map +1 -0
- package/dist/useMediaAnalyzer.js +114 -0
- package/dist/useMediaAnalyzer.js.map +1 -0
- package/package.json +88 -0
- package/src/ChaiProvider.tsx +88 -0
- package/src/Composer.tsx +961 -0
- package/src/MediaAnalyzer.tsx +527 -0
- package/src/ResultCard.tsx +980 -0
- package/src/index.ts +36 -0
- package/src/primitives/Pagination.tsx +41 -0
- package/src/primitives/SearchMenu.tsx +284 -0
- package/src/primitives/Toggle.tsx +43 -0
- package/src/primitives/index.ts +3 -0
- package/src/style.css +1860 -0
- package/src/use-latest.ts +18 -0
- package/src/useComposer.ts +289 -0
- package/src/useMediaAnalyzer.ts +157 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export * from "./primitives/index.js";
|
|
2
|
+
export * from "./Composer.js";
|
|
3
|
+
export * from "./MediaAnalyzer.js";
|
|
4
|
+
export * from "./ResultCard.js";
|
|
5
|
+
export * from "./useComposer.js";
|
|
6
|
+
export * from "./ChaiProvider.js";
|
|
7
|
+
export * from "./useMediaAnalyzer.js";
|
|
8
|
+
|
|
9
|
+
// Re-export the core engines and types so a consumer typically only needs
|
|
10
|
+
// to import from @chai-ui/react.
|
|
11
|
+
export {
|
|
12
|
+
createFalEngine,
|
|
13
|
+
createOpenRouterEngine,
|
|
14
|
+
createMockEngine,
|
|
15
|
+
mockEngine,
|
|
16
|
+
GenerationError,
|
|
17
|
+
DEFAULT_MEDIA_ANALYSIS_PROMPT_LENGTH,
|
|
18
|
+
MEDIA_ANALYSIS_PROMPTS,
|
|
19
|
+
MEDIA_ANALYSIS_PROMPT_LENGTHS,
|
|
20
|
+
} from "@chai-ui/core";
|
|
21
|
+
export type {
|
|
22
|
+
DroppedMedia,
|
|
23
|
+
Evaluation,
|
|
24
|
+
GenerationEngine,
|
|
25
|
+
GenerationUseCase,
|
|
26
|
+
MediaAnalysisKind,
|
|
27
|
+
MediaAnalysisPromptLength,
|
|
28
|
+
MediaKind,
|
|
29
|
+
ModelOption,
|
|
30
|
+
ParameterOption,
|
|
31
|
+
Request,
|
|
32
|
+
Result,
|
|
33
|
+
ResultStatus,
|
|
34
|
+
Run,
|
|
35
|
+
Usage,
|
|
36
|
+
} from "@chai-ui/core";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
|
|
3
|
+
interface PaginationProps {
|
|
4
|
+
count: number;
|
|
5
|
+
index: number;
|
|
6
|
+
onChange: (index: number) => void;
|
|
7
|
+
/** Accessible name for the nav landmark, e.g. "Results". */
|
|
8
|
+
label: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Progress-style pager (reference: the Figma "Pagination" asset, e.g. the
|
|
14
|
+
* Result Card mock) — a pill that fills from page 1 up to
|
|
15
|
+
* the current page under a row of plain dots, not independent per-page
|
|
16
|
+
* dots with a stretched active one. A standalone primitive, not part of
|
|
17
|
+
* any one component, so every paginated surface renders the same pager
|
|
18
|
+
* rather than each growing its own.
|
|
19
|
+
*/
|
|
20
|
+
export function Pagination({ count, index, onChange, label, disabled }: PaginationProps) {
|
|
21
|
+
return (
|
|
22
|
+
<nav
|
|
23
|
+
className="chai-pagination"
|
|
24
|
+
aria-label={label}
|
|
25
|
+
style={{ "--chai-pagination-fill": `${((index + 1) / count) * 100}%` } as CSSProperties}
|
|
26
|
+
>
|
|
27
|
+
<span className="chai-pagination__track" aria-hidden="true" />
|
|
28
|
+
{Array.from({ length: count }, (_, i) => (
|
|
29
|
+
<button
|
|
30
|
+
key={i}
|
|
31
|
+
type="button"
|
|
32
|
+
className={`chai-pagination__dot${i <= index ? " chai-pagination__dot--filled" : ""}`}
|
|
33
|
+
aria-current={i === index}
|
|
34
|
+
aria-label={`Result ${i + 1} of ${count}`}
|
|
35
|
+
disabled={disabled}
|
|
36
|
+
onClick={() => onChange(i)}
|
|
37
|
+
/>
|
|
38
|
+
))}
|
|
39
|
+
</nav>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { createComponent } from "@lit/react";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field.js";
|
|
5
|
+
import { MdCheckbox } from "@material/web/checkbox/checkbox.js";
|
|
6
|
+
import { Toggle } from "./Toggle.js";
|
|
7
|
+
|
|
8
|
+
const MdOutlinedTextFieldElement = createComponent({
|
|
9
|
+
react: React,
|
|
10
|
+
tagName: "md-outlined-text-field",
|
|
11
|
+
elementClass: MdOutlinedTextField,
|
|
12
|
+
events: { onInput: "input" },
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* MD3's `<md-checkbox>`, shown as a leading checkbox on each row in
|
|
17
|
+
* multi-select mode — MD3's own pattern for picking several items from a
|
|
18
|
+
* list. Display only: the row button stays the one interactive, selectable
|
|
19
|
+
* element (a focusable checkbox inside a button would be two controls in
|
|
20
|
+
* one place), so the checkbox is `aria-hidden`, out of the tab order and
|
|
21
|
+
* ignores pointer events.
|
|
22
|
+
*/
|
|
23
|
+
const MdCheckboxElement = createComponent({
|
|
24
|
+
react: React,
|
|
25
|
+
tagName: "md-checkbox",
|
|
26
|
+
elementClass: MdCheckbox,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/** MD3 "check" symbol (Figma) — marking the selected row in any option list below. */
|
|
30
|
+
function CheckIcon() {
|
|
31
|
+
return (
|
|
32
|
+
<svg viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
|
33
|
+
<path
|
|
34
|
+
d="M7.1625 13.5L2.8875 9.225L3.95625 8.15625L7.1625 11.3625L14.0438 4.48125L15.1125 5.55L7.1625 13.5Z"
|
|
35
|
+
fill="currentColor"
|
|
36
|
+
/>
|
|
37
|
+
</svg>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SearchMenuOption {
|
|
42
|
+
id: string;
|
|
43
|
+
/** Shown on the trigger once this option is selected, and used for search-filtering. Keep this the short/canonical name. */
|
|
44
|
+
label: string;
|
|
45
|
+
/**
|
|
46
|
+
* Shown in the option row itself instead of `label`, if given — e.g. a
|
|
47
|
+
* longer, clarifying variant ("Terse (short)") that would read as
|
|
48
|
+
* cluttered on a closed trigger button but is genuinely useful while
|
|
49
|
+
* still choosing (reference: Media Analyzer's "Prompt length" menu).
|
|
50
|
+
* Falls back to `label` when omitted.
|
|
51
|
+
*/
|
|
52
|
+
rowLabel?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One labeled group of options, rendered under its own header with a divider before the next group (reference: Media Analyzer's "Select model" menu — Image/Video/Audio sections). */
|
|
56
|
+
export interface SearchMenuSection<T extends SearchMenuOption> {
|
|
57
|
+
label: string;
|
|
58
|
+
options: T[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A toggle pinned above every group, in its own bordered/tinted block with
|
|
63
|
+
* a floating label — e.g. Media Analyzer's "Auto-select model" (reference:
|
|
64
|
+
* the design mock). While `value` is true, every option row
|
|
65
|
+
* below becomes non-interactive — grayed out, `disabled` for real — but
|
|
66
|
+
* still shows its checkmark on whichever option is the menu's own `value`
|
|
67
|
+
* (the last *manual* pick), so switching the toggle back off returns to
|
|
68
|
+
* exactly that one instead of losing it.
|
|
69
|
+
*/
|
|
70
|
+
export interface SearchMenuToggleHeader {
|
|
71
|
+
label: string;
|
|
72
|
+
value: boolean;
|
|
73
|
+
onChange: (next: boolean) => void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface SearchMenuProps<T extends SearchMenuOption> {
|
|
77
|
+
/** A flat option list. Ignored if `sections` is given. */
|
|
78
|
+
options?: T[];
|
|
79
|
+
/** Grouped options, each under its own header with a divider between groups — an alternative to `options` for a menu that needs section dividers by category. */
|
|
80
|
+
sections?: SearchMenuSection<T>[];
|
|
81
|
+
toggleHeader?: SearchMenuToggleHeader;
|
|
82
|
+
/** The currently effective option's id, if any — checkmarked in the list. An array with `multiple`. */
|
|
83
|
+
value: string | string[] | null;
|
|
84
|
+
/** Several options can be checked at once: each row gets a leading checkbox (MD3's multi-select list pattern), a row click toggles it and the panel stays open. `onSelect` still fires with the clicked option; the caller owns adding or removing it. */
|
|
85
|
+
multiple?: boolean;
|
|
86
|
+
/** Text shown on the trigger button. */
|
|
87
|
+
triggerLabel: string;
|
|
88
|
+
/** Floating label on the search field and the panel's aria-label. */
|
|
89
|
+
menuLabel: string;
|
|
90
|
+
onSelect: (option: T) => void;
|
|
91
|
+
disabled?: boolean;
|
|
92
|
+
/** Show the search field in the panel. Default true. Set false for a short, fixed option list (reference: Composer's Model/Aspect ratio triggers) where searching adds nothing. */
|
|
93
|
+
searchable?: boolean;
|
|
94
|
+
/** "outlined" (default) matches `.chai-btn--outlined`; "text" drops the border entirely — chevron + label only (reference: Composer's Model/Aspect ratio triggers). */
|
|
95
|
+
triggerVariant?: "outlined" | "text";
|
|
96
|
+
/** Panel width in px (default 280) — widen for a menu whose option labels run long; anything still too long past that ellipsizes rather than wrapping or overflowing. */
|
|
97
|
+
panelWidth?: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The searchable menu-button pattern behind every Model/option menu
|
|
102
|
+
* (reference: Media Analyze - Model Menu) — a trigger styled as MD3's outlined button, opening an anchored panel with
|
|
103
|
+
* a search field (a real `<md-outlined-text-field>`, for its floating-label
|
|
104
|
+
* + icon-slot behavior) over a plain option list. Hand-rolled rather than
|
|
105
|
+
* `<md-menu>`: this needs to live inline as a simple absolutely-positioned
|
|
106
|
+
* panel under the trigger, not `<md-menu>`'s own corner-anchored popover
|
|
107
|
+
* positioning model — same reasoning SegmentedButton documents for why it
|
|
108
|
+
* isn't a `@material/web` custom element either.
|
|
109
|
+
*/
|
|
110
|
+
export function SearchMenu<T extends SearchMenuOption>({
|
|
111
|
+
options,
|
|
112
|
+
sections,
|
|
113
|
+
toggleHeader,
|
|
114
|
+
value,
|
|
115
|
+
multiple = false,
|
|
116
|
+
triggerLabel,
|
|
117
|
+
menuLabel,
|
|
118
|
+
onSelect,
|
|
119
|
+
disabled,
|
|
120
|
+
searchable = true,
|
|
121
|
+
triggerVariant = "outlined",
|
|
122
|
+
panelWidth = 280,
|
|
123
|
+
}: SearchMenuProps<T>) {
|
|
124
|
+
const [open, setOpen] = useState(false);
|
|
125
|
+
const [search, setSearch] = useState("");
|
|
126
|
+
// Closing also clears the search, so the menu reopens on the full list.
|
|
127
|
+
const close = useCallback(() => {
|
|
128
|
+
setOpen(false);
|
|
129
|
+
setSearch("");
|
|
130
|
+
}, []);
|
|
131
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
132
|
+
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (!open) return;
|
|
135
|
+
function handlePointerDown(e: PointerEvent) {
|
|
136
|
+
if (!rootRef.current?.contains(e.target as Node)) close();
|
|
137
|
+
}
|
|
138
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
139
|
+
if (e.key === "Escape") close();
|
|
140
|
+
}
|
|
141
|
+
document.addEventListener("pointerdown", handlePointerDown);
|
|
142
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
143
|
+
return () => {
|
|
144
|
+
document.removeEventListener("pointerdown", handlePointerDown);
|
|
145
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
146
|
+
};
|
|
147
|
+
}, [open, close]);
|
|
148
|
+
|
|
149
|
+
const groups = useMemo(() => {
|
|
150
|
+
const q = search.trim().toLowerCase();
|
|
151
|
+
const raw = sections ?? [{ label: "", options: options ?? [] }];
|
|
152
|
+
if (!q) return raw;
|
|
153
|
+
return raw
|
|
154
|
+
.map((g) => ({
|
|
155
|
+
...g,
|
|
156
|
+
options: g.options.filter(
|
|
157
|
+
(o) => o.label.toLowerCase().includes(q) || o.rowLabel?.toLowerCase().includes(q)
|
|
158
|
+
),
|
|
159
|
+
}))
|
|
160
|
+
.filter((g) => g.options.length > 0);
|
|
161
|
+
}, [options, sections, search]);
|
|
162
|
+
const hasAnyOption = groups.some((g) => g.options.length > 0);
|
|
163
|
+
const isSelected = (id: string) => (Array.isArray(value) ? value.includes(id) : id === value);
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div className="chai-search-menu" ref={rootRef}>
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
className={`${triggerVariant === "outlined" ? "chai-btn chai-btn--outlined" : "chai-search-menu__trigger--text"} chai-search-menu__trigger${
|
|
170
|
+
open ? " chai-search-menu__trigger--open" : ""
|
|
171
|
+
}`}
|
|
172
|
+
disabled={disabled}
|
|
173
|
+
aria-expanded={open}
|
|
174
|
+
onClick={() => (open ? close() : setOpen(true))}
|
|
175
|
+
>
|
|
176
|
+
<span className="chai-search-menu__trigger-label" title={triggerLabel}>
|
|
177
|
+
{triggerLabel}
|
|
178
|
+
</span>
|
|
179
|
+
{/* Material Symbols "keyboard_arrow_down" (real path, from the
|
|
180
|
+
@material-symbols/svg-400 package — not hand-traced), trailing
|
|
181
|
+
per the mock's own reference code (`TrailingElement` wrapping
|
|
182
|
+
the icon on both `Aspect_ratio_select` and `model_select`), not
|
|
183
|
+
leading like `ModelMenu`'s older trigger. */}
|
|
184
|
+
<svg
|
|
185
|
+
className={`chai-search-menu__chevron${open ? " chai-search-menu__chevron--open" : ""}`}
|
|
186
|
+
viewBox="0 -960 960 960"
|
|
187
|
+
aria-hidden="true"
|
|
188
|
+
>
|
|
189
|
+
<path d="M480-344 240-584l43-43 197 197 197-197 43 43-240 240Z" fill="currentColor" />
|
|
190
|
+
</svg>
|
|
191
|
+
</button>
|
|
192
|
+
|
|
193
|
+
{open && (
|
|
194
|
+
<div
|
|
195
|
+
className="chai-search-menu__panel"
|
|
196
|
+
role="group"
|
|
197
|
+
aria-label={menuLabel}
|
|
198
|
+
style={{ width: panelWidth }}
|
|
199
|
+
>
|
|
200
|
+
{toggleHeader && (
|
|
201
|
+
<div className="chai-search-menu__toggle-header">
|
|
202
|
+
<span className="chai-search-menu__toggle-header-label">{menuLabel}</span>
|
|
203
|
+
<span className="chai-search-menu__toggle-header-row">
|
|
204
|
+
<span className="chai-search-menu__toggle-header-text">{toggleHeader.label}</span>
|
|
205
|
+
<Toggle value={toggleHeader.value} onChange={toggleHeader.onChange} ariaLabel={toggleHeader.label} />
|
|
206
|
+
</span>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
{searchable && (
|
|
210
|
+
<MdOutlinedTextFieldElement
|
|
211
|
+
className="chai-search-menu__search"
|
|
212
|
+
label={menuLabel}
|
|
213
|
+
placeholder="Input"
|
|
214
|
+
value={search}
|
|
215
|
+
onInput={(e) => setSearch((e.target as MdOutlinedTextField).value)}
|
|
216
|
+
>
|
|
217
|
+
<span slot="leading-icon" className="chai-search-menu__search-icon">
|
|
218
|
+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
219
|
+
<circle cx="11" cy="11" r="6.5" stroke="currentColor" strokeWidth="1.5" />
|
|
220
|
+
<path d="m20 20-3.6-3.6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
221
|
+
</svg>
|
|
222
|
+
</span>
|
|
223
|
+
{search && (
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
slot="trailing-icon"
|
|
227
|
+
className="chai-search-menu__clear"
|
|
228
|
+
aria-label="Clear search"
|
|
229
|
+
onClick={() => setSearch("")}
|
|
230
|
+
>
|
|
231
|
+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
232
|
+
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="1.5" />
|
|
233
|
+
<path d="m9 9 6 6m0-6-6 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
234
|
+
</svg>
|
|
235
|
+
</button>
|
|
236
|
+
)}
|
|
237
|
+
</MdOutlinedTextFieldElement>
|
|
238
|
+
)}
|
|
239
|
+
|
|
240
|
+
<ul className="chai-search-menu__list">
|
|
241
|
+
{groups.map((group, i) => (
|
|
242
|
+
<li key={group.label || i}>
|
|
243
|
+
{group.label && <p className="chai-search-menu__section-label">{group.label}</p>}
|
|
244
|
+
<ul className="chai-search-menu__section-list" aria-label={group.label || undefined}>
|
|
245
|
+
{group.options.map((o) => (
|
|
246
|
+
<li key={o.id}>
|
|
247
|
+
{/* Plain buttons, reached with Tab: `aria-pressed` says which are picked. */}
|
|
248
|
+
<button
|
|
249
|
+
type="button"
|
|
250
|
+
aria-pressed={isSelected(o.id)}
|
|
251
|
+
disabled={toggleHeader?.value}
|
|
252
|
+
className={`chai-search-menu__option${
|
|
253
|
+
!multiple && isSelected(o.id) ? " chai-search-menu__option--selected" : ""
|
|
254
|
+
}`}
|
|
255
|
+
onClick={() => {
|
|
256
|
+
onSelect(o);
|
|
257
|
+
if (!multiple) close();
|
|
258
|
+
}}
|
|
259
|
+
>
|
|
260
|
+
{multiple && (
|
|
261
|
+
<MdCheckboxElement
|
|
262
|
+
className="chai-search-menu__checkbox"
|
|
263
|
+
checked={isSelected(o.id)}
|
|
264
|
+
disabled={toggleHeader?.value}
|
|
265
|
+
tabIndex={-1}
|
|
266
|
+
aria-hidden="true"
|
|
267
|
+
/>
|
|
268
|
+
)}
|
|
269
|
+
<span className="chai-search-menu__option-label">{o.rowLabel ?? o.label}</span>
|
|
270
|
+
{!multiple && isSelected(o.id) && <CheckIcon />}
|
|
271
|
+
</button>
|
|
272
|
+
</li>
|
|
273
|
+
))}
|
|
274
|
+
</ul>
|
|
275
|
+
{i < groups.length - 1 && <hr className="chai-search-menu__divider" role="separator" />}
|
|
276
|
+
</li>
|
|
277
|
+
))}
|
|
278
|
+
{!hasAnyOption && <li className="chai-search-menu__empty">No matches.</li>}
|
|
279
|
+
</ul>
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createComponent } from "@lit/react";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { MdSwitch } from "@material/web/switch/switch.js";
|
|
4
|
+
import type { PrimitiveProps } from "@chai-ui/core";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `<md-switch>` is a Lit web component: its `selected` state is a JS
|
|
8
|
+
* property (not an HTML attribute) and it reports changes via a native
|
|
9
|
+
* `change` event, not a React synthetic one. Plain JSX on a custom-element
|
|
10
|
+
* tag can't bind either correctly — React would try to set `selected` as a
|
|
11
|
+
* string attribute and would never see the DOM `change` event as an
|
|
12
|
+
* `onChange` prop. `@lit/react`'s `createComponent` does that
|
|
13
|
+
* property/attribute distinction and event-listener wiring once, here, so
|
|
14
|
+
* this stays a plain function component with no local state, refs, or
|
|
15
|
+
* effects of its own.
|
|
16
|
+
*/
|
|
17
|
+
const MdSwitchElement = createComponent({
|
|
18
|
+
react: React,
|
|
19
|
+
tagName: "md-switch",
|
|
20
|
+
elementClass: MdSwitch,
|
|
21
|
+
events: {
|
|
22
|
+
onChange: "change",
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export interface ToggleProps extends PrimitiveProps<boolean> {
|
|
27
|
+
/** The switch's accessible name, e.g. "Auto-select model". Required unless something else labels it. */
|
|
28
|
+
ariaLabel?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** MD3 adapter (§12.2, Adapter) for the CHAI primitive contract — wraps `<md-switch>`. `chai-switch` is the hook `style.css` uses to color every switch from Chai tokens — see that rule's own comment. */
|
|
32
|
+
export function Toggle({ value, onChange, disabled, ariaLabel }: ToggleProps) {
|
|
33
|
+
return (
|
|
34
|
+
<MdSwitchElement
|
|
35
|
+
className="chai-switch"
|
|
36
|
+
aria-label={ariaLabel}
|
|
37
|
+
selected={value}
|
|
38
|
+
disabled={disabled}
|
|
39
|
+
showOnlySelectedIcon
|
|
40
|
+
onChange={(e) => onChange((e.target as MdSwitch).selected)}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|