@botiverse/raft-ui 0.10.4 → 0.10.5
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 +10 -0
- package/dist/components/combobox-select/combobox-select.css +115 -0
- package/dist/{dialog-CDPAT3DH.mjs → drawer-yZD16Ybl.mjs} +442 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -445
- package/dist/style.css +130 -0
- package/dist/wip.d.mts +22 -1
- package/dist/wip.mjs +407 -5
- package/package.json +1 -1
- package/skills/raft-ui-critique/SKILL.md +3 -3
package/dist/wip.mjs
CHANGED
|
@@ -1,8 +1,54 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Check, ChevronDown, LoaderCircle, Plus, X } from "lucide-react";
|
|
3
|
-
import { useCallback, useId, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { D as DialogTitle, I as useThemeScopeProps, L as useTheme, O as DialogTrigger, a as DrawerContent, c as DrawerHeader, d as DrawerTitle, f as DrawerTrigger, g as Dialog, i as DrawerClose, k as Button, n as DrawerActions, r as DrawerBody, s as DrawerFooter, t as Drawer, v as DialogClose, y as DialogContent, z as tv } from "./drawer-yZD16Ybl.mjs";
|
|
2
|
+
import { Check, ChevronDown, LoaderCircle, Plus, Search, X } from "lucide-react";
|
|
3
|
+
import { useCallback, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
4
4
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import { Combobox } from "@base-ui/react/combobox";
|
|
6
|
+
import { Select } from "@base-ui/react/select";
|
|
7
|
+
//#region src/lib/phone-media.ts
|
|
8
|
+
/**
|
|
9
|
+
* A phone: a touch screen narrower than the desktop layout. The complement, on
|
|
10
|
+
* both axes, of the desktop query `(width >= 48rem) and (pointer: fine)`
|
|
11
|
+
* (design/foundations/spacing.md): a narrow window with a mouse keeps the
|
|
12
|
+
* desktop behaviour, and so does a tablet used by touch, which is wide.
|
|
13
|
+
*/
|
|
14
|
+
const PHONE_MEDIA_QUERY = "(width < 48rem) and (pointer: coarse)";
|
|
15
|
+
function subscribe(onStoreChange) {
|
|
16
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return () => void 0;
|
|
17
|
+
const media = window.matchMedia(PHONE_MEDIA_QUERY);
|
|
18
|
+
media.addEventListener("change", onStoreChange);
|
|
19
|
+
return () => media.removeEventListener("change", onStoreChange);
|
|
20
|
+
}
|
|
21
|
+
function getSnapshot() {
|
|
22
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(width < 48rem) and (pointer: coarse)").matches;
|
|
23
|
+
}
|
|
24
|
+
/** Whether `PHONE_MEDIA_QUERY` matches. `false` on the server and before hydration. */
|
|
25
|
+
function usePhoneMedia() {
|
|
26
|
+
return useSyncExternalStore(subscribe, getSnapshot, () => false);
|
|
27
|
+
}
|
|
28
|
+
function resolveComboboxSelectMode({ phone, optionCount, sheetThreshold = 10, create = false, remote = false }) {
|
|
29
|
+
if (!phone) return "field";
|
|
30
|
+
if (create || remote) return "sheet";
|
|
31
|
+
return optionCount > sheetThreshold ? "sheet" : "list";
|
|
32
|
+
}
|
|
33
|
+
/** Options people can pick, flat or grouped: disabled options don't count. */
|
|
34
|
+
function countSelectableOptions(groups) {
|
|
35
|
+
let count = 0;
|
|
36
|
+
for (const group of groups) for (const item of group.items) if (!item.disabled) count += 1;
|
|
37
|
+
return count;
|
|
38
|
+
}
|
|
39
|
+
/** The tallest the anchored list gets (`.r-combobox-select__list`, 16rem). */
|
|
40
|
+
const LIST_MAX_HEIGHT = 256;
|
|
41
|
+
/**
|
|
42
|
+
* The side the anchored list opens on, chosen once when it opens so it never
|
|
43
|
+
* flips while it is open (a phone's visual viewport keeps changing under it).
|
|
44
|
+
* Below, unless the room below is short of the list and there is more above.
|
|
45
|
+
*/
|
|
46
|
+
function preferredListSide(anchor, viewport, listHeight) {
|
|
47
|
+
const below = viewport.top + viewport.height - anchor.bottom;
|
|
48
|
+
const above = anchor.top - viewport.top;
|
|
49
|
+
return below >= Math.min(listHeight, LIST_MAX_HEIGHT) || below >= above ? "bottom" : "top";
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
6
52
|
//#region src/components/combobox-select/combobox-select.options.ts
|
|
7
53
|
function isOptionGroup(option) {
|
|
8
54
|
return typeof option === "object" && option !== null && "type" in option && option.type === "group" && "children" in option && Array.isArray(option.children);
|
|
@@ -30,7 +76,10 @@ const defaultMessages = {
|
|
|
30
76
|
selected: "Selected options",
|
|
31
77
|
chipDescription: "Press Backspace or Delete to remove",
|
|
32
78
|
inputDescription: "From the start of the input, press Left Arrow to focus selected options",
|
|
33
|
-
remove: (label) => `Remove ${label}
|
|
79
|
+
remove: (label) => `Remove ${label}`,
|
|
80
|
+
search: "Search",
|
|
81
|
+
done: "Done",
|
|
82
|
+
close: "Close"
|
|
34
83
|
};
|
|
35
84
|
const defaultRenderCreateLabel = (query) => `Create “${query}”`;
|
|
36
85
|
function recordLabel(record, renderLabel) {
|
|
@@ -69,6 +118,74 @@ function renderSelectedChip({ record, index, tagLimit, strings, renderLabel, ren
|
|
|
69
118
|
})]
|
|
70
119
|
}, optionKey(record.value));
|
|
71
120
|
}
|
|
121
|
+
/** An option of the phone's anchored list: the same content as the field's list. */
|
|
122
|
+
function renderSelectOption({ record, selected, loading, renderLabel, renderOption }) {
|
|
123
|
+
const content = record.option === void 0 ? record.label : renderOption?.(record.option, { selected }) ?? recordLabel(record, renderLabel);
|
|
124
|
+
return /* @__PURE__ */ jsxs(Select.Item, {
|
|
125
|
+
value: record,
|
|
126
|
+
label: record.label,
|
|
127
|
+
disabled: record.disabled || loading,
|
|
128
|
+
className: "r-combobox-select__option",
|
|
129
|
+
children: [/* @__PURE__ */ jsx(Select.ItemText, {
|
|
130
|
+
className: "r-combobox-select__text",
|
|
131
|
+
children: content
|
|
132
|
+
}), /* @__PURE__ */ jsx(Select.ItemIndicator, {
|
|
133
|
+
className: "r-combobox-select__indicator",
|
|
134
|
+
children: /* @__PURE__ */ jsx(Check, { "aria-hidden": true })
|
|
135
|
+
})]
|
|
136
|
+
}, optionKey(record.value));
|
|
137
|
+
}
|
|
138
|
+
/** Change details for the phone's own changes: the clear button outside any Base UI root, the sheet's search emptied as it closes. */
|
|
139
|
+
function syntheticDetails(reason, event) {
|
|
140
|
+
let canceled = false;
|
|
141
|
+
return {
|
|
142
|
+
reason,
|
|
143
|
+
event,
|
|
144
|
+
trigger: void 0,
|
|
145
|
+
cancel() {
|
|
146
|
+
canceled = true;
|
|
147
|
+
},
|
|
148
|
+
allowPropagation() {},
|
|
149
|
+
get isCanceled() {
|
|
150
|
+
return canceled;
|
|
151
|
+
},
|
|
152
|
+
get isPropagationAllowed() {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function assignRef(ref, value) {
|
|
158
|
+
if (typeof ref === "function") ref(value);
|
|
159
|
+
else if (ref) ref.current = value;
|
|
160
|
+
}
|
|
161
|
+
/** The text that names a field: its `aria-labelledby` elements, else its `<label>`. */
|
|
162
|
+
function readFieldLabel(element, labelledBy) {
|
|
163
|
+
if (!element) return "";
|
|
164
|
+
const doc = element.ownerDocument;
|
|
165
|
+
const fromIds = (labelledBy ?? "").split(/\s+/).filter(Boolean).map((labelId) => doc.getElementById(labelId)?.textContent?.trim() ?? "").filter(Boolean).join(" ");
|
|
166
|
+
if (fromIds) return fromIds;
|
|
167
|
+
return [...element.labels ?? []].map((label) => label.textContent?.trim() ?? "").filter(Boolean).join(" ");
|
|
168
|
+
}
|
|
169
|
+
/** The phone's anchored list keeps the side it opened on: it never flips while open. */
|
|
170
|
+
const PHONE_LIST_COLLISION = {
|
|
171
|
+
side: "none",
|
|
172
|
+
align: "shift",
|
|
173
|
+
fallbackAxisSide: "none"
|
|
174
|
+
};
|
|
175
|
+
/** The side the phone's anchored list opens on, from the room around the field. */
|
|
176
|
+
function measureListSide(anchor, groups) {
|
|
177
|
+
const view = anchor?.ownerDocument.defaultView;
|
|
178
|
+
if (!anchor || !view) return "bottom";
|
|
179
|
+
const visual = view.visualViewport;
|
|
180
|
+
const rows = groups.reduce((count, group) => count + group.items.length + (group.label == null ? 0 : 1), 0);
|
|
181
|
+
return preferredListSide(anchor.getBoundingClientRect(), visual ? {
|
|
182
|
+
top: visual.offsetTop,
|
|
183
|
+
height: visual.height
|
|
184
|
+
} : {
|
|
185
|
+
top: 0,
|
|
186
|
+
height: view.innerHeight
|
|
187
|
+
}, rows * 36 + 12);
|
|
188
|
+
}
|
|
72
189
|
/** Options-driven composition. The stable compound Combobox API stays unchanged. */
|
|
73
190
|
function isItemEqualToValue(a, b) {
|
|
74
191
|
return a.createQuery === b.createQuery && a.value === b.value;
|
|
@@ -79,7 +196,7 @@ function itemToStringLabel(record) {
|
|
|
79
196
|
function itemToStringValue(record) {
|
|
80
197
|
return String(record.value);
|
|
81
198
|
}
|
|
82
|
-
function ComboboxSelect({ options, multiple, value, defaultValue, onValueChange, labelField = "label", valueField = "value", disabledField = "disabled", fallbackOption, filterable = true, filter, remote = false, onSearch, loading = false, create = false, creatable, onCreate, clearable = false, showArrow = true, clearFilterAfterSelect = true, onClear, maxTagCount, size = "md", status, placeholder = "Select an option", renderLabel, renderOption, renderTag, renderCreateLabel = defaultRenderCreateLabel, emptyContent = "No options found", loadingContent = "Loading…", messages, inputProps, portalProps, positionerProps, popupProps, className, style, id: idProp, ref, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, inputValue, defaultInputValue, onInputValueChange, disabled, readOnly, autoHighlight = true, ...rootProps }) {
|
|
199
|
+
function ComboboxSelect({ options, multiple, value, defaultValue, onValueChange, labelField = "label", valueField = "value", disabledField = "disabled", fallbackOption, filterable = true, filter, remote = false, onSearch, loading = false, create = false, creatable, onCreate, clearable = false, showArrow = true, clearFilterAfterSelect = true, onClear, maxTagCount, size = "md", status, placeholder = "Select an option", renderLabel, renderOption, renderTag, renderCreateLabel = defaultRenderCreateLabel, emptyContent = "No options found", loadingContent = "Loading…", messages, sheetThreshold = 10, sheetTitle, inputProps, portalProps, positionerProps, popupProps, className, style, id: idProp, ref, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, inputValue, defaultInputValue, onInputValueChange, disabled, readOnly, autoHighlight = true, ...rootProps }) {
|
|
83
200
|
const generatedId = useId();
|
|
84
201
|
const id = idProp ?? generatedId;
|
|
85
202
|
const { resolvedMode } = useTheme();
|
|
@@ -348,6 +465,291 @@ function ComboboxSelect({ options, multiple, value, defaultValue, onValueChange,
|
|
|
348
465
|
]
|
|
349
466
|
});
|
|
350
467
|
const valueChange = handleValueChange;
|
|
468
|
+
const mode = resolveComboboxSelectMode({
|
|
469
|
+
phone: usePhoneMedia(),
|
|
470
|
+
optionCount: countSelectableOptions(normalized.groups),
|
|
471
|
+
sheetThreshold,
|
|
472
|
+
create: Boolean(create || creatable),
|
|
473
|
+
remote
|
|
474
|
+
});
|
|
475
|
+
const fieldRef = useRef(null);
|
|
476
|
+
const triggerRef = useRef(null);
|
|
477
|
+
const searchInputRef = useRef(null);
|
|
478
|
+
const [uncontrolledPhoneOpen, setUncontrolledPhoneOpen] = useState(Boolean(rootProps.defaultOpen));
|
|
479
|
+
const phoneOpen = rootProps.open ?? uncontrolledPhoneOpen;
|
|
480
|
+
const [listSide, setListSide] = useState("bottom");
|
|
481
|
+
const [fieldLabel, setFieldLabel] = useState("");
|
|
482
|
+
const sheetNamed = sheetTitle != null || Boolean(inputLabel);
|
|
483
|
+
useLayoutEffect(() => {
|
|
484
|
+
if (mode !== "sheet" || !phoneOpen || sheetNamed) return;
|
|
485
|
+
setFieldLabel(readFieldLabel(triggerRef.current, inputLabelledBy));
|
|
486
|
+
}, [
|
|
487
|
+
mode,
|
|
488
|
+
phoneOpen,
|
|
489
|
+
sheetNamed,
|
|
490
|
+
inputLabelledBy
|
|
491
|
+
]);
|
|
492
|
+
const setSearchInput = useCallback((element) => {
|
|
493
|
+
searchInputRef.current = element;
|
|
494
|
+
assignRef(inputRef, element);
|
|
495
|
+
}, [inputRef]);
|
|
496
|
+
const onOpenChangeProp = rootProps.onOpenChange;
|
|
497
|
+
const openProp = rootProps.open;
|
|
498
|
+
const groups = normalized.groups;
|
|
499
|
+
const handlePhoneOpenChange = useCallback((next, details) => {
|
|
500
|
+
onOpenChangeProp?.(next, details);
|
|
501
|
+
if (details.isCanceled) return;
|
|
502
|
+
if (next && mode === "list") setListSide(measureListSide(fieldRef.current, groups));
|
|
503
|
+
if (!next && mode === "sheet") handleInputValueChange("", syntheticDetails("input-clear", details.event));
|
|
504
|
+
if (openProp === void 0) setUncontrolledPhoneOpen(next);
|
|
505
|
+
}, [
|
|
506
|
+
onOpenChangeProp,
|
|
507
|
+
openProp,
|
|
508
|
+
mode,
|
|
509
|
+
groups,
|
|
510
|
+
handleInputValueChange
|
|
511
|
+
]);
|
|
512
|
+
const handleSheetValueChange = useCallback((next, details) => {
|
|
513
|
+
handleValueChange(next, details);
|
|
514
|
+
if (!multiple && !details.isCanceled) handlePhoneOpenChange(false, details);
|
|
515
|
+
}, [
|
|
516
|
+
handleValueChange,
|
|
517
|
+
multiple,
|
|
518
|
+
handlePhoneOpenChange
|
|
519
|
+
]);
|
|
520
|
+
const handlePhoneClear = useCallback((event) => {
|
|
521
|
+
handleValueChange(multiple ? [] : null, syntheticDetails("clear-press", event.nativeEvent));
|
|
522
|
+
}, [handleValueChange, multiple]);
|
|
523
|
+
const focusSearchInput = useCallback(() => searchInputRef.current, []);
|
|
524
|
+
if (mode !== "field") {
|
|
525
|
+
const placeholderText = /* @__PURE__ */ jsx("span", {
|
|
526
|
+
className: "r-combobox-select__placeholder",
|
|
527
|
+
children: placeholder
|
|
528
|
+
});
|
|
529
|
+
const phoneValue = multiple ? selected.length ? /* @__PURE__ */ jsxs("span", {
|
|
530
|
+
className: "r-combobox-select__chips",
|
|
531
|
+
children: [selected.slice(0, tagLimit).map((record) => /* @__PURE__ */ jsx("span", {
|
|
532
|
+
className: "r-combobox-select__chip",
|
|
533
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
534
|
+
className: "r-combobox-select__text",
|
|
535
|
+
children: record.option === void 0 ? record.label : renderTag?.(record.option) ?? recordLabel(record, renderLabel)
|
|
536
|
+
})
|
|
537
|
+
}, optionKey(record.value))), overflow]
|
|
538
|
+
}) : placeholderText : selected[0] ? /* @__PURE__ */ jsx("span", {
|
|
539
|
+
className: "r-combobox-select__value",
|
|
540
|
+
children: selected[0].label
|
|
541
|
+
}) : placeholderText;
|
|
542
|
+
const phoneClear = clearable && selected.length > 0 && !disabled && !readOnly ? /* @__PURE__ */ jsx("button", {
|
|
543
|
+
type: "button",
|
|
544
|
+
className: "r-combobox-select__action r-combobox-select__clear",
|
|
545
|
+
"aria-label": strings.clear,
|
|
546
|
+
onClick: handlePhoneClear,
|
|
547
|
+
children: /* @__PURE__ */ jsx(X, { "aria-hidden": true })
|
|
548
|
+
}) : null;
|
|
549
|
+
const phoneActions = /* @__PURE__ */ jsxs("span", {
|
|
550
|
+
className: "r-combobox-select__actions",
|
|
551
|
+
children: [
|
|
552
|
+
loadingIndicator,
|
|
553
|
+
phoneClear,
|
|
554
|
+
showArrow ? /* @__PURE__ */ jsx("span", {
|
|
555
|
+
className: "r-combobox-select__action r-combobox-select__arrow",
|
|
556
|
+
"aria-hidden": true,
|
|
557
|
+
children: /* @__PURE__ */ jsx(ChevronDown, {})
|
|
558
|
+
}) : null
|
|
559
|
+
]
|
|
560
|
+
});
|
|
561
|
+
const phoneControlClassName = [
|
|
562
|
+
css,
|
|
563
|
+
"r-combobox-select__control r-combobox-select__control--trigger has-focus-visible:shadow-focus",
|
|
564
|
+
className
|
|
565
|
+
].filter(Boolean).join(" ");
|
|
566
|
+
const triggerProps = {
|
|
567
|
+
ref: triggerRef,
|
|
568
|
+
id,
|
|
569
|
+
"data-slot": "combobox-select-trigger",
|
|
570
|
+
className: "r-combobox-select__trigger",
|
|
571
|
+
"aria-label": inputLabel,
|
|
572
|
+
"aria-labelledby": inputLabelledBy,
|
|
573
|
+
"aria-describedby": inputDescribedBy,
|
|
574
|
+
"aria-invalid": inputInvalid
|
|
575
|
+
};
|
|
576
|
+
const field = (trigger) => /* @__PURE__ */ jsxs("div", {
|
|
577
|
+
ref: fieldRef,
|
|
578
|
+
"data-slot": "combobox-select",
|
|
579
|
+
"data-status": status,
|
|
580
|
+
"data-disabled": disabled || void 0,
|
|
581
|
+
"data-readonly": readOnly || void 0,
|
|
582
|
+
"data-popup-open": phoneOpen || void 0,
|
|
583
|
+
className: phoneControlClassName,
|
|
584
|
+
style,
|
|
585
|
+
children: [trigger, phoneActions]
|
|
586
|
+
});
|
|
587
|
+
if (mode === "list") {
|
|
588
|
+
const anchoredPopupClassName = `${popupClassName} r-combobox-select__popup--anchored`;
|
|
589
|
+
const hasOptions = normalized.groups.some((group) => group.items.length > 0);
|
|
590
|
+
return /* @__PURE__ */ jsxs(Select.Root, {
|
|
591
|
+
multiple,
|
|
592
|
+
value: controlValue,
|
|
593
|
+
onValueChange: valueChange,
|
|
594
|
+
open: phoneOpen,
|
|
595
|
+
onOpenChange: handlePhoneOpenChange,
|
|
596
|
+
onOpenChangeComplete: rootProps.onOpenChangeComplete,
|
|
597
|
+
modal: false,
|
|
598
|
+
name: rootProps.name,
|
|
599
|
+
form: rootProps.form,
|
|
600
|
+
required: rootProps.required,
|
|
601
|
+
autoComplete: rootProps.autoComplete,
|
|
602
|
+
disabled,
|
|
603
|
+
readOnly,
|
|
604
|
+
itemToStringLabel,
|
|
605
|
+
itemToStringValue,
|
|
606
|
+
isItemEqualToValue,
|
|
607
|
+
children: [field(/* @__PURE__ */ jsx(Select.Trigger, {
|
|
608
|
+
...triggerProps,
|
|
609
|
+
children: phoneValue
|
|
610
|
+
})), /* @__PURE__ */ jsx(Select.Portal, {
|
|
611
|
+
...scopedPortalProps,
|
|
612
|
+
children: /* @__PURE__ */ jsx(Select.Positioner, {
|
|
613
|
+
anchor: fieldRef,
|
|
614
|
+
side: listSide,
|
|
615
|
+
align: "start",
|
|
616
|
+
sideOffset: 4,
|
|
617
|
+
alignItemWithTrigger: false,
|
|
618
|
+
collisionAvoidance: PHONE_LIST_COLLISION,
|
|
619
|
+
className: positionerClassName,
|
|
620
|
+
children: /* @__PURE__ */ jsxs(Select.Popup, {
|
|
621
|
+
"data-slot": "combobox-select-popup",
|
|
622
|
+
className: anchoredPopupClassName,
|
|
623
|
+
children: [
|
|
624
|
+
loading ? /* @__PURE__ */ jsx("div", {
|
|
625
|
+
role: "status",
|
|
626
|
+
className: "r-combobox-select__message",
|
|
627
|
+
children: loadingContent
|
|
628
|
+
}) : null,
|
|
629
|
+
/* @__PURE__ */ jsx(Select.List, {
|
|
630
|
+
className: "r-combobox-select__list",
|
|
631
|
+
"aria-busy": loading,
|
|
632
|
+
children: normalized.groups.map((group) => /* @__PURE__ */ jsxs(Select.Group, { children: [group.label == null ? null : /* @__PURE__ */ jsx(Select.GroupLabel, {
|
|
633
|
+
className: "r-combobox-select__group-label",
|
|
634
|
+
children: group.label
|
|
635
|
+
}), group.items.map((record) => renderSelectOption({
|
|
636
|
+
record,
|
|
637
|
+
selected: selected.some((item) => item.value === record.value),
|
|
638
|
+
loading,
|
|
639
|
+
renderLabel,
|
|
640
|
+
renderOption
|
|
641
|
+
}))] }, group.key))
|
|
642
|
+
}),
|
|
643
|
+
!hasOptions ? emptyMessage : null
|
|
644
|
+
]
|
|
645
|
+
})
|
|
646
|
+
})
|
|
647
|
+
})]
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
const titleId = `${id}-sheet-title`;
|
|
651
|
+
const sheetCss = `r-combobox-select r-combobox-select--md${resolvedMode === "dark" ? " r-combobox-select--dark" : ""} r-combobox-select__sheet`;
|
|
652
|
+
const hiddenInputs = rootProps.name ? selectedKeys.map((key) => /* @__PURE__ */ jsx("input", {
|
|
653
|
+
type: "hidden",
|
|
654
|
+
name: rootProps.name,
|
|
655
|
+
form: rootProps.form,
|
|
656
|
+
value: String(key),
|
|
657
|
+
disabled
|
|
658
|
+
}, optionKey(key))) : null;
|
|
659
|
+
return /* @__PURE__ */ jsxs(Drawer, {
|
|
660
|
+
open: phoneOpen,
|
|
661
|
+
onOpenChange: handlePhoneOpenChange,
|
|
662
|
+
onOpenChangeComplete: rootProps.onOpenChangeComplete,
|
|
663
|
+
children: [
|
|
664
|
+
field(/* @__PURE__ */ jsx(DrawerTrigger, {
|
|
665
|
+
...triggerProps,
|
|
666
|
+
disabled,
|
|
667
|
+
role: "combobox",
|
|
668
|
+
"aria-haspopup": "dialog",
|
|
669
|
+
"aria-expanded": phoneOpen,
|
|
670
|
+
"aria-readonly": readOnly || void 0,
|
|
671
|
+
"aria-required": rootProps.required || void 0,
|
|
672
|
+
children: phoneValue
|
|
673
|
+
})),
|
|
674
|
+
hiddenInputs,
|
|
675
|
+
/* @__PURE__ */ jsx(DrawerContent, {
|
|
676
|
+
portalProps,
|
|
677
|
+
"data-slot": "combobox-select-sheet",
|
|
678
|
+
className: "r-combobox-select__sheet-popup",
|
|
679
|
+
initialFocus: create || creatable ? focusSearchInput : void 0,
|
|
680
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
681
|
+
className: sheetCss,
|
|
682
|
+
children: [
|
|
683
|
+
/* @__PURE__ */ jsxs(DrawerHeader, {
|
|
684
|
+
className: "r-combobox-select__sheet-header",
|
|
685
|
+
children: [/* @__PURE__ */ jsx(DrawerTitle, {
|
|
686
|
+
id: titleId,
|
|
687
|
+
className: "r-combobox-select__sheet-title",
|
|
688
|
+
children: sheetTitle ?? inputLabel ?? (fieldLabel || placeholder)
|
|
689
|
+
}), /* @__PURE__ */ jsx(DrawerClose, { "aria-label": strings.close })]
|
|
690
|
+
}),
|
|
691
|
+
/* @__PURE__ */ jsxs(Combobox.Root, {
|
|
692
|
+
inline: true,
|
|
693
|
+
multiple,
|
|
694
|
+
disabled,
|
|
695
|
+
readOnly,
|
|
696
|
+
items,
|
|
697
|
+
value: controlValue,
|
|
698
|
+
onValueChange: handleSheetValueChange,
|
|
699
|
+
inputValue,
|
|
700
|
+
defaultInputValue,
|
|
701
|
+
onInputValueChange: handleInputValueChange,
|
|
702
|
+
filter: filterOptions,
|
|
703
|
+
itemToStringLabel,
|
|
704
|
+
itemToStringValue,
|
|
705
|
+
isItemEqualToValue,
|
|
706
|
+
autoHighlight,
|
|
707
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
708
|
+
className: "r-combobox-select__sheet-search",
|
|
709
|
+
children: /* @__PURE__ */ jsxs(Combobox.InputGroup, {
|
|
710
|
+
className: "r-combobox-select__control has-focus-visible:shadow-focus",
|
|
711
|
+
children: [
|
|
712
|
+
/* @__PURE__ */ jsx(Search, {
|
|
713
|
+
className: "r-combobox-select__search-icon",
|
|
714
|
+
"aria-hidden": true
|
|
715
|
+
}),
|
|
716
|
+
/* @__PURE__ */ jsx(Combobox.Input, {
|
|
717
|
+
...inputProps,
|
|
718
|
+
ref: setSearchInput,
|
|
719
|
+
id: `${id}-search`,
|
|
720
|
+
"data-slot": "combobox-select-input",
|
|
721
|
+
className: inputClassName,
|
|
722
|
+
"aria-labelledby": titleId,
|
|
723
|
+
placeholder: strings.search,
|
|
724
|
+
readOnly: inputReadOnly,
|
|
725
|
+
enterKeyHint: "search"
|
|
726
|
+
}),
|
|
727
|
+
loadingIndicator
|
|
728
|
+
]
|
|
729
|
+
})
|
|
730
|
+
}), /* @__PURE__ */ jsxs(DrawerBody, {
|
|
731
|
+
className: "r-combobox-select__sheet-body",
|
|
732
|
+
children: [
|
|
733
|
+
/* @__PURE__ */ jsx(Combobox.Status, { children: loadingMessage }),
|
|
734
|
+
/* @__PURE__ */ jsx(Combobox.List, {
|
|
735
|
+
className: "r-combobox-select__list",
|
|
736
|
+
"aria-busy": loading,
|
|
737
|
+
children: renderGroup
|
|
738
|
+
}),
|
|
739
|
+
/* @__PURE__ */ jsx(Combobox.Empty, { children: emptyMessage })
|
|
740
|
+
]
|
|
741
|
+
})]
|
|
742
|
+
}),
|
|
743
|
+
multiple ? /* @__PURE__ */ jsx(DrawerFooter, { children: /* @__PURE__ */ jsx(DrawerActions, { children: /* @__PURE__ */ jsx(DrawerClose, {
|
|
744
|
+
render: /* @__PURE__ */ jsx(Button, { size: "lg" }),
|
|
745
|
+
children: strings.done
|
|
746
|
+
}) }) }) : null
|
|
747
|
+
]
|
|
748
|
+
})
|
|
749
|
+
})
|
|
750
|
+
]
|
|
751
|
+
});
|
|
752
|
+
}
|
|
351
753
|
return /* @__PURE__ */ jsxs(Combobox.Root, {
|
|
352
754
|
...rootProps,
|
|
353
755
|
id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: raft-ui-critique
|
|
3
|
-
description: "Critique raft-ui components and product surfaces from screenshots, live renders, or code evidence in Light and Dark modes. Use when reviewing, auditing, polishing, comparing, or validating visual hierarchy, Raft fidelity, interaction feedback, accessibility, consistency, and user experience in packages/ui
|
|
3
|
+
description: "Critique raft-ui components and product surfaces from screenshots, live renders, or code evidence in Light and Dark modes. Use when reviewing, auditing, polishing, comparing, or validating visual hierarchy, Raft fidelity, interaction feedback, accessibility, consistency, and user experience in packages/ui or docs compositions."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Raft UI Critique
|
|
@@ -63,7 +63,7 @@ Compare like for like:
|
|
|
63
63
|
- Use the same component, content, state, viewport dimensions, zoom, and motion frame.
|
|
64
64
|
- Keep surrounding context equivalent. A fragment without its normal shell cannot prove page hierarchy.
|
|
65
65
|
- Record which interaction states are shown: default, hover, pressed, focus-visible, disabled, selected, open, checked, invalid, loading, or empty, as applicable.
|
|
66
|
-
- Separate the live primitive
|
|
66
|
+
- Separate the live primitive and the docs composition. Never treat one as proof for the other.
|
|
67
67
|
|
|
68
68
|
Record an evidence manifest with the route or artifact, preset, exact viewport, browser/platform when known, zoom, font-loading status, UI state, and reduced-motion setting when motion is relevant. For a full product-surface review, include a representative wide and narrow viewport when the layout is responsive; a single isolated primitive may need only its relevant size.
|
|
69
69
|
|
|
@@ -118,7 +118,7 @@ When code is available, connect each visual issue to the most likely owner witho
|
|
|
118
118
|
|
|
119
119
|
- Reusable appearance or interaction styling: `packages/ui` component recipe or semantic tokens.
|
|
120
120
|
- Canonical example structure: docs module composition.
|
|
121
|
-
- Product state or workflow coordination:
|
|
121
|
+
- Product state or workflow coordination: the docs composition or the application consumer.
|
|
122
122
|
- Global token and font material: `foundation.css` / `fonts.css` (see Package evidence lookup).
|
|
123
123
|
|
|
124
124
|
State whether the cause is observed, code-verified, or inferred. Do not prescribe a new token for a one-off correction, and do not patch a consumer to compensate for a broken reusable component contract.
|