@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.
Files changed (66) hide show
  1. package/DESIGN.md +207 -0
  2. package/LICENSE +21 -0
  3. package/README.md +265 -0
  4. package/dist/ChaiProvider.d.ts +49 -0
  5. package/dist/ChaiProvider.d.ts.map +1 -0
  6. package/dist/ChaiProvider.js +54 -0
  7. package/dist/ChaiProvider.js.map +1 -0
  8. package/dist/Composer.d.ts +169 -0
  9. package/dist/Composer.d.ts.map +1 -0
  10. package/dist/Composer.js +389 -0
  11. package/dist/Composer.js.map +1 -0
  12. package/dist/MediaAnalyzer.d.ts +102 -0
  13. package/dist/MediaAnalyzer.d.ts.map +1 -0
  14. package/dist/MediaAnalyzer.js +250 -0
  15. package/dist/MediaAnalyzer.js.map +1 -0
  16. package/dist/ResultCard.d.ts +55 -0
  17. package/dist/ResultCard.d.ts.map +1 -0
  18. package/dist/ResultCard.js +409 -0
  19. package/dist/ResultCard.js.map +1 -0
  20. package/dist/index.d.ts +10 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +11 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/primitives/Pagination.d.ts +19 -0
  25. package/dist/primitives/Pagination.d.ts.map +1 -0
  26. package/dist/primitives/Pagination.js +13 -0
  27. package/dist/primitives/Pagination.js.map +1 -0
  28. package/dist/primitives/SearchMenu.d.ts +69 -0
  29. package/dist/primitives/SearchMenu.d.ts.map +1 -0
  30. package/dist/primitives/SearchMenu.js +88 -0
  31. package/dist/primitives/SearchMenu.js.map +1 -0
  32. package/dist/primitives/Toggle.d.ts +9 -0
  33. package/dist/primitives/Toggle.d.ts.map +1 -0
  34. package/dist/primitives/Toggle.js +28 -0
  35. package/dist/primitives/Toggle.js.map +1 -0
  36. package/dist/primitives/index.d.ts +4 -0
  37. package/dist/primitives/index.d.ts.map +1 -0
  38. package/dist/primitives/index.js +4 -0
  39. package/dist/primitives/index.js.map +1 -0
  40. package/dist/style.css +1860 -0
  41. package/dist/use-latest.d.ts +7 -0
  42. package/dist/use-latest.d.ts.map +1 -0
  43. package/dist/use-latest.js +17 -0
  44. package/dist/use-latest.js.map +1 -0
  45. package/dist/useComposer.d.ts +75 -0
  46. package/dist/useComposer.d.ts.map +1 -0
  47. package/dist/useComposer.js +191 -0
  48. package/dist/useComposer.js.map +1 -0
  49. package/dist/useMediaAnalyzer.d.ts +37 -0
  50. package/dist/useMediaAnalyzer.d.ts.map +1 -0
  51. package/dist/useMediaAnalyzer.js +114 -0
  52. package/dist/useMediaAnalyzer.js.map +1 -0
  53. package/package.json +88 -0
  54. package/src/ChaiProvider.tsx +88 -0
  55. package/src/Composer.tsx +961 -0
  56. package/src/MediaAnalyzer.tsx +527 -0
  57. package/src/ResultCard.tsx +980 -0
  58. package/src/index.ts +36 -0
  59. package/src/primitives/Pagination.tsx +41 -0
  60. package/src/primitives/SearchMenu.tsx +284 -0
  61. package/src/primitives/Toggle.tsx +43 -0
  62. package/src/primitives/index.ts +3 -0
  63. package/src/style.css +1860 -0
  64. package/src/use-latest.ts +18 -0
  65. package/src/useComposer.ts +289 -0
  66. 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
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./Toggle.js";
2
+ export * from "./SearchMenu.js";
3
+ export * from "./Pagination.js";