@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/dist/wip.mjs CHANGED
@@ -1,8 +1,54 @@
1
- import { S as useTheme, f as DialogTitle, i as DialogContent, m as Button, p as DialogTrigger, r as DialogClose, t as Dialog, w as tv, x as useThemeScopeProps } from "./dialog-CDPAT3DH.mjs";
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": "@botiverse/raft-ui",
3
- "version": "0.10.4",
3
+ "version": "0.10.5",
4
4
  "description": "React components and CSS foundations for the Raft Design System.",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -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, docs compositions, or Playground."
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, docs composition, and Playground wrapper. Never treat one as proof for another.
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: Playground or application consumer.
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.