@ctrl-ui/react 0.8.5 → 0.8.7

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 (152) hide show
  1. package/dist/action-bar.js +1 -1
  2. package/dist/activity.js +1 -1
  3. package/dist/blocks/coding-agent.js +16 -19
  4. package/dist/blocks/design-canvas-data.d.ts +32 -0
  5. package/dist/blocks/design-canvas-data.js +100 -0
  6. package/dist/blocks/design-canvas-inspector.d.ts +8 -0
  7. package/dist/blocks/design-canvas-inspector.js +238 -0
  8. package/dist/blocks/design-canvas.d.ts +17 -0
  9. package/dist/blocks/design-canvas.js +346 -0
  10. package/dist/blocks/file-explorer.js +2 -2
  11. package/dist/blocks/settings.js +2 -2
  12. package/dist/chat-composer-attachment.d.ts +9 -3
  13. package/dist/chat-composer-attachment.js +27 -10
  14. package/dist/chat-composer-editor.js +20 -4
  15. package/dist/chat-composer.d.ts +1 -1
  16. package/dist/chat-composer.js +4 -3
  17. package/dist/chat-layout.js +32 -19
  18. package/dist/context.js +1 -1
  19. package/dist/dynamic-notification.js +1 -1
  20. package/dist/environment-variables.js +1 -1
  21. package/dist/filter-bar/context.d.ts +32 -0
  22. package/dist/filter-bar/context.js +11 -0
  23. package/dist/filter-bar/editor.d.ts +14 -0
  24. package/dist/filter-bar/editor.js +204 -0
  25. package/dist/filter-bar/model.d.ts +9 -0
  26. package/dist/filter-bar/model.js +48 -0
  27. package/dist/filter-bar/motion.d.ts +10 -0
  28. package/dist/filter-bar/motion.js +84 -0
  29. package/dist/filter-bar/parts.d.ts +19 -0
  30. package/dist/filter-bar/parts.js +233 -0
  31. package/dist/filter-bar/types.d.ts +53 -0
  32. package/dist/filter-bar/types.js +0 -0
  33. package/dist/filter-bar.d.ts +27 -0
  34. package/dist/filter-bar.js +304 -0
  35. package/dist/hooks/use-chat-composer.d.ts +3 -1
  36. package/dist/hooks/use-chat-composer.js +2 -2
  37. package/dist/hooks/use-dropzone.d.ts +7 -4
  38. package/dist/hooks/use-dropzone.js +34 -3
  39. package/dist/inline-attachment.js +8 -14
  40. package/dist/inline-citation.js +1 -1
  41. package/dist/knob-contracts/chat-layout-knobs.d.ts +1 -1
  42. package/dist/knob-contracts/chat-layout-knobs.js +2 -1
  43. package/dist/knob-contracts/choice-knobs.d.ts +1 -1
  44. package/dist/knob-contracts/choice-knobs.js +1 -0
  45. package/dist/knob-contracts/collapsible-knobs.d.ts +1 -1
  46. package/dist/knob-contracts/collapsible-knobs.js +1 -1
  47. package/dist/knob-contracts/filter-bar-knobs.d.ts +4 -0
  48. package/dist/knob-contracts/filter-bar-knobs.js +16 -0
  49. package/dist/knob-contracts/infinite-canvas-knobs.d.ts +1 -1
  50. package/dist/knob-contracts/infinite-canvas-knobs.js +3 -0
  51. package/dist/knob-contracts/page-layout-knobs.d.ts +1 -1
  52. package/dist/knob-contracts/page-layout-knobs.js +4 -1
  53. package/dist/knob-contracts/popup-knobs.d.ts +1 -1
  54. package/dist/knob-contracts/popup-knobs.js +1 -0
  55. package/dist/knob-contracts/table-of-contents-knobs.d.ts +1 -1
  56. package/dist/knob-contracts/table-of-contents-knobs.js +3 -5
  57. package/dist/lib/phone-input-format.js +1 -1
  58. package/dist/markdown-block.js +11 -7
  59. package/dist/scripts/contrast-eval.js +5 -4
  60. package/dist/styles/index.css +5 -4
  61. package/dist/styles/recipes/button-group.css +1 -1
  62. package/dist/styles/recipes/button.css +2 -2
  63. package/dist/styles/recipes/chat-composer.css +14 -6
  64. package/dist/styles/recipes/chat-layout.css +29 -2
  65. package/dist/styles/recipes/choice.css +10 -3
  66. package/dist/styles/recipes/code-diff.css +16 -4
  67. package/dist/styles/recipes/code.css +11 -5
  68. package/dist/styles/recipes/collapsible.css +37 -0
  69. package/dist/styles/recipes/color-picker.css +12 -2
  70. package/dist/styles/recipes/dockable-panel.css +11 -3
  71. package/dist/styles/recipes/field-parts.css +62 -0
  72. package/dist/styles/recipes/field.css +28 -6
  73. package/dist/styles/recipes/filter-bar.css +174 -0
  74. package/dist/styles/recipes/infinite-canvas.css +51 -3
  75. package/dist/styles/recipes/inline-attachment.css +6 -1
  76. package/dist/styles/recipes/page-layout.css +36 -2
  77. package/dist/styles/recipes/pagination.css +1 -1
  78. package/dist/styles/recipes/popup-controls.css +16 -0
  79. package/dist/styles/recipes/popup-drawer.css +34 -32
  80. package/dist/styles/recipes/popup-lists.css +11 -2
  81. package/dist/styles/recipes/popup.css +10 -12
  82. package/dist/styles/recipes/range.css +1 -1
  83. package/dist/styles/recipes/resizable.css +34 -0
  84. package/dist/styles/recipes/spinner.css +4 -0
  85. package/dist/styles/recipes/table-of-contents.css +42 -42
  86. package/dist/styles/recipes/tabs.css +21 -0
  87. package/dist/styles/recipes/track-highlight.css +2 -1
  88. package/dist/styles/recipes/transcript-divider.css +2 -2
  89. package/dist/styles/theme.css +250 -91
  90. package/dist/ui/alert-dialog.js +1 -1
  91. package/dist/ui/autocomplete.js +5 -1
  92. package/dist/ui/button.js +2 -2
  93. package/dist/ui/checkbox-group.js +1 -1
  94. package/dist/ui/code-diff.js +1 -1
  95. package/dist/ui/code.js +3 -3
  96. package/dist/ui/collapsible.js +1 -1
  97. package/dist/ui/color-picker.js +2 -2
  98. package/dist/ui/combobox-disabled-selection.d.ts +1 -1
  99. package/dist/ui/combobox-disabled-selection.js +2 -1
  100. package/dist/ui/combobox.d.ts +5 -23
  101. package/dist/ui/combobox.js +26 -14
  102. package/dist/ui/command.js +9 -1
  103. package/dist/ui/context-menu.js +1 -1
  104. package/dist/ui/dialog.js +1 -1
  105. package/dist/ui/dockable-panel.js +18 -10
  106. package/dist/ui/drawer.d.ts +7 -4
  107. package/dist/ui/drawer.js +36 -23
  108. package/dist/ui/dropdown-menu.js +1 -1
  109. package/dist/ui/dropzone.d.ts +3 -1
  110. package/dist/ui/dropzone.js +3 -1
  111. package/dist/ui/field.js +1 -1
  112. package/dist/ui/hover-card.js +1 -1
  113. package/dist/ui/infinite-canvas.d.ts +11 -0
  114. package/dist/ui/infinite-canvas.js +82 -7
  115. package/dist/ui/input-group.d.ts +2 -1
  116. package/dist/ui/input-group.js +12 -3
  117. package/dist/ui/input.js +9 -0
  118. package/dist/ui/item.js +1 -1
  119. package/dist/ui/markdown-elements.js +11 -8
  120. package/dist/ui/menubar.js +1 -1
  121. package/dist/ui/morphing-panel.js +1 -1
  122. package/dist/ui/navigation-menu.js +1 -1
  123. package/dist/ui/number-field.d.ts +1 -1
  124. package/dist/ui/number-field.js +13 -2
  125. package/dist/ui/phone-input.js +2 -2
  126. package/dist/ui/popover.js +1 -1
  127. package/dist/ui/radio-group.d.ts +1 -0
  128. package/dist/ui/radio-group.js +11 -1
  129. package/dist/ui/resizable.d.ts +18 -1
  130. package/dist/ui/resizable.js +127 -7
  131. package/dist/ui/responsive-dialog.js +6 -3
  132. package/dist/ui/rich-tooltip.js +1 -1
  133. package/dist/ui/scroll-area.d.ts +4 -1
  134. package/dist/ui/scroll-area.js +2 -1
  135. package/dist/ui/select.d.ts +2 -1
  136. package/dist/ui/select.js +58 -33
  137. package/dist/ui/sheet.js +1 -1
  138. package/dist/ui/sidebar-menu.js +1 -1
  139. package/dist/ui/sidebar-provider.js +2 -2
  140. package/dist/ui/sidebar-resize-rail.js +1 -1
  141. package/dist/ui/sidebar.js +1 -1
  142. package/dist/ui/table-of-contents.d.ts +4 -3
  143. package/dist/ui/table-of-contents.js +126 -47
  144. package/dist/ui/table.js +11 -7
  145. package/dist/ui/toast.js +1 -1
  146. package/dist/ui/toggle.d.ts +1 -1
  147. package/dist/ui/toggle.js +2 -1
  148. package/dist/ui/tooltip.js +1 -1
  149. package/dist/ui/tree.js +1 -1
  150. package/dist/ui/trigger-menu.js +7 -3
  151. package/dist/user-ask.js +1 -1
  152. package/package.json +1 -1
@@ -0,0 +1,233 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.js";
3
+ import { Button } from "../ui/button.js";
4
+ import { useFilterBarContext } from "./context.js";
5
+ import { formatFilterValue } from "./model.js";
6
+ import { FilterBarEditor, FilterBarInlineInput } from "./editor.js";
7
+ import { useEffect, useId, useRef } from "react";
8
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
9
+ import { PlusIcon, XIcon } from "lucide-react";
10
+ import { useRender } from "@base-ui/react/use-render";
11
+ //#region src/filter-bar/parts.tsx
12
+ function chipSegments(item, field, operator, draft) {
13
+ const segments = [{
14
+ stage: "field",
15
+ label: field?.label ?? item.fieldId,
16
+ icon: field?.icon
17
+ }];
18
+ if (operator || draft) segments.push({
19
+ stage: "operator",
20
+ label: operator?.label ?? "Operator…"
21
+ });
22
+ if (item.value !== null || draft && operator) segments.push({
23
+ stage: "value",
24
+ label: item.value === null ? "Value…" : formatFilterValue(item, field)
25
+ });
26
+ return segments;
27
+ }
28
+ function FilterBarChipContent({ item, readOnly = false, className, ref, ...props }) {
29
+ const context = useFilterBarContext();
30
+ const field = context.fields.find((candidate) => candidate.id === item.fieldId);
31
+ const operator = context.operators.find((candidate) => candidate.id === item.operatorId);
32
+ const draft = Boolean(context.draft?.id === item.id && !context.draft.editing);
33
+ const locked = readOnly || context.readOnly;
34
+ const segments = chipSegments(item, field, operator, draft);
35
+ return useRender({
36
+ ref: [ref ?? null, locked ? null : (element) => context.register(`chip:${item.id}`, element)],
37
+ render: /* @__PURE__ */ jsxs("div", {
38
+ "data-control-ui": "filter-bar",
39
+ "data-control-family": "filter-bar",
40
+ "data-slot": "chip",
41
+ "data-draft": draft || void 0,
42
+ className: cn("inline-flex max-w-full min-w-0 items-stretch overflow-hidden", className),
43
+ ...props,
44
+ children: [segments.map((segment) => /* @__PURE__ */ jsx(FilterBarSegment, {
45
+ item,
46
+ segment,
47
+ locked,
48
+ draft
49
+ }, segment.stage)), !locked && !draft && /* @__PURE__ */ jsx(Button, {
50
+ ref: (element) => context.register(`${item.id}:remove`, element),
51
+ variant: "ghost",
52
+ size: "sm",
53
+ iconOnly: true,
54
+ "data-control-ui": "filter-bar",
55
+ "data-control-family": "filter-bar",
56
+ "data-slot": "remove",
57
+ "aria-label": `Remove ${field?.label ?? item.fieldId} filter`,
58
+ disabled: context.disabled,
59
+ onClick: () => context.remove(item),
60
+ onKeyDown: (event) => context.navigate(event, item.id, "remove"),
61
+ children: /* @__PURE__ */ jsx(XIcon, {
62
+ "aria-hidden": "true",
63
+ className: "size-3"
64
+ })
65
+ })]
66
+ })
67
+ });
68
+ }
69
+ function FilterBarSegment({ item, segment, locked, draft }) {
70
+ const context = useFilterBarContext();
71
+ if (locked) return /* @__PURE__ */ jsx("span", {
72
+ "data-control-ui": "filter-bar",
73
+ "data-control-family": "filter-bar",
74
+ "data-slot": "segment",
75
+ className: "inline-flex min-w-0 items-center",
76
+ children: /* @__PURE__ */ jsxs("span", {
77
+ className: "flex min-w-0 items-center gap-1.5",
78
+ children: [segment.icon, /* @__PURE__ */ jsx("span", {
79
+ className: "truncate",
80
+ children: segment.label
81
+ })]
82
+ })
83
+ });
84
+ return /* @__PURE__ */ jsxs(Button, {
85
+ ref: (element) => context.register(`${item.id}:${segment.stage}`, element),
86
+ variant: "ghost",
87
+ size: "sm",
88
+ "data-control-ui": "filter-bar",
89
+ "data-control-family": "filter-bar",
90
+ "data-slot": "segment",
91
+ className: "shrink",
92
+ "aria-label": `Edit ${segment.stage}: ${segment.label}`,
93
+ "aria-haspopup": "listbox",
94
+ "aria-expanded": context.draft?.owner === item.id && context.draft.stage === segment.stage,
95
+ disabled: context.disabled || draft,
96
+ onClick: (event) => context.start(item.id, event.currentTarget, segment.stage, item),
97
+ onKeyDown: (event) => context.navigate(event, item.id, segment.stage),
98
+ children: [segment.icon, /* @__PURE__ */ jsx("span", {
99
+ className: "truncate",
100
+ children: segment.label
101
+ })]
102
+ });
103
+ }
104
+ function FilterBarChip(props) {
105
+ return /* @__PURE__ */ jsx(FilterBarEditor, {
106
+ owner: props.item.id,
107
+ mode: "button",
108
+ children: /* @__PURE__ */ jsx(FilterBarChipContent, { ...props })
109
+ });
110
+ }
111
+ function ExitingChip({ item, style }) {
112
+ const context = useFilterBarContext();
113
+ const ref = useRef(null);
114
+ const releaseExit = context.releaseExit;
115
+ useEffect(() => {
116
+ const animations = ref.current?.getAnimations({ subtree: true }) ?? [];
117
+ let mounted = true;
118
+ const release = () => {
119
+ if (mounted) releaseExit(item.id);
120
+ };
121
+ if (animations.length === 0) release();
122
+ else Promise.all(animations.map((animation) => animation.finished)).then(release, release);
123
+ return () => {
124
+ mounted = false;
125
+ };
126
+ }, [item.id, releaseExit]);
127
+ return /* @__PURE__ */ jsx("div", {
128
+ ref,
129
+ style,
130
+ "data-control-ui": "filter-bar",
131
+ "data-control-family": "filter-bar",
132
+ "data-slot": "exit",
133
+ "aria-hidden": "true",
134
+ inert: true,
135
+ children: /* @__PURE__ */ jsx(FilterBarChipContent, {
136
+ item,
137
+ readOnly: true
138
+ })
139
+ });
140
+ }
141
+ function FilterBarChips({ renderChip }) {
142
+ const context = useFilterBarContext();
143
+ const items = [...context.items];
144
+ const draft = context.draft;
145
+ if (draft?.fieldId && !draft.editing) items.push({
146
+ id: draft.id,
147
+ fieldId: draft.fieldId,
148
+ operatorId: draft.operatorId ?? "",
149
+ value: draft.values.length > 0 ? draft.values : null
150
+ });
151
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [items.map((item) => /* @__PURE__ */ jsx(FilterBarChipContentWithEditor, {
152
+ item,
153
+ renderChip
154
+ }, item.id)), context.exits.map((exit) => /* @__PURE__ */ jsx(ExitingChip, { ...exit }, exit.item.id))] });
155
+ }
156
+ function FilterBarChipContentWithEditor({ item, renderChip }) {
157
+ const context = useFilterBarContext();
158
+ return renderChip && item.id !== context.draft?.id ? renderChip(item) : /* @__PURE__ */ jsx(FilterBarChip, { item });
159
+ }
160
+ function FilterBarAddButton({ className, children = "Add filter", onClick, onKeyDown, ref, ...props }) {
161
+ const context = useFilterBarContext();
162
+ const owner = useId();
163
+ const button = useRender({
164
+ ref: [ref ?? null, (element) => context.register(`entry:${owner}`, element)],
165
+ render: /* @__PURE__ */ jsxs(Button, {
166
+ variant: "ghost",
167
+ size: "sm",
168
+ className,
169
+ "aria-haspopup": "listbox",
170
+ "aria-expanded": context.draft?.owner === owner,
171
+ disabled: context.disabled || context.readOnly,
172
+ ...props,
173
+ onClick: (event) => {
174
+ onClick?.(event);
175
+ if (!event.defaultPrevented) context.start(owner, event.currentTarget);
176
+ },
177
+ onKeyDown: (event) => {
178
+ onKeyDown?.(event);
179
+ context.navigate(event);
180
+ },
181
+ children: [/* @__PURE__ */ jsx(PlusIcon, {
182
+ "aria-hidden": "true",
183
+ className: "size-3.5"
184
+ }), children]
185
+ })
186
+ });
187
+ return /* @__PURE__ */ jsx(FilterBarEditor, {
188
+ owner,
189
+ mode: "button",
190
+ children: button
191
+ });
192
+ }
193
+ function FilterBarInput({ className, ...props }) {
194
+ const context = useFilterBarContext();
195
+ const owner = useId();
196
+ const inputRef = useRef(null);
197
+ return /* @__PURE__ */ jsx(FilterBarEditor, {
198
+ owner,
199
+ mode: "input",
200
+ anchorRef: inputRef,
201
+ children: /* @__PURE__ */ jsx("div", {
202
+ ref: (element) => context.register(`entry:${owner}`, element ? inputRef.current : null),
203
+ className: cn("min-w-0 max-w-full flex-[1_1_10rem]", className),
204
+ children: /* @__PURE__ */ jsx(FilterBarInlineInput, {
205
+ owner,
206
+ inputRef,
207
+ "aria-label": "Add filter",
208
+ placeholder: "Filter…",
209
+ disabled: context.disabled,
210
+ readOnly: context.readOnly,
211
+ ...props
212
+ })
213
+ })
214
+ });
215
+ }
216
+ function FilterBarClear({ children = "Clear", onClick, ...props }) {
217
+ const context = useFilterBarContext();
218
+ if (context.items.length === 0 && !context.draft) return null;
219
+ return /* @__PURE__ */ jsx(Button, {
220
+ variant: "ghost",
221
+ size: "sm",
222
+ "aria-label": "Clear filters",
223
+ disabled: context.disabled || context.readOnly,
224
+ ...props,
225
+ onClick: (event) => {
226
+ onClick?.(event);
227
+ if (!event.defaultPrevented) context.clear();
228
+ },
229
+ children
230
+ });
231
+ }
232
+ //#endregion
233
+ export { FilterBarAddButton, FilterBarChip, FilterBarChips, FilterBarClear, FilterBarInput };
@@ -0,0 +1,53 @@
1
+ import { ReactNode } from "react";
2
+ //#region src/filter-bar/types.d.ts
3
+ export type FilterBarScalar = string | number | boolean;
4
+ export type FilterBarValue = FilterBarScalar | FilterBarScalar[] | null;
5
+ export type FilterBarOperator = {
6
+ id: string;
7
+ label: string;
8
+ arity?: "none" | "one" | "many";
9
+ };
10
+ export type FilterBarOption = {
11
+ value: FilterBarScalar;
12
+ label: string;
13
+ icon?: ReactNode;
14
+ disabled?: boolean;
15
+ };
16
+ export type FilterBarQuery = {
17
+ fieldId: string;
18
+ operatorId: string;
19
+ query: string;
20
+ };
21
+ export type FilterBarField = {
22
+ id: string;
23
+ label: string;
24
+ icon?: ReactNode;
25
+ type?: "text" | "number" | "boolean";
26
+ operators?: readonly string[];
27
+ options?: readonly FilterBarOption[];
28
+ allowCustomValue?: boolean;
29
+ optionsMode?: "local" | "remote";
30
+ loading?: boolean;
31
+ error?: ReactNode;
32
+ onQueryChange?: (query: FilterBarQuery) => void;
33
+ formatValue?: (value: FilterBarScalar) => string;
34
+ };
35
+ export type FilterBarItem = {
36
+ id: string;
37
+ fieldId: string;
38
+ operatorId: string;
39
+ value: FilterBarValue;
40
+ };
41
+ export type FilterBarStage = "field" | "operator" | "value";
42
+ export type FilterBarDraft = {
43
+ id: string;
44
+ owner: string;
45
+ anchor: HTMLElement;
46
+ stage: FilterBarStage;
47
+ fieldId?: string;
48
+ operatorId?: string;
49
+ values: FilterBarScalar[];
50
+ query: string;
51
+ editing: boolean;
52
+ };
53
+ //#endregion
File without changes
@@ -0,0 +1,27 @@
1
+ import { FilterBarKnobStyle } from "./knob-contracts/filter-bar-knobs.js";
2
+ import { FilterBarField, FilterBarItem, FilterBarOperator, FilterBarOption, FilterBarQuery, FilterBarScalar, FilterBarValue } from "./filter-bar/types.js";
3
+ import { FilterBarAddButton, FilterBarChip, FilterBarChipProps, FilterBarChips, FilterBarClear, FilterBarInput } from "./filter-bar/parts.js";
4
+ import { CSSProperties, ComponentProps } from "react";
5
+ //#region src/filter-bar.d.ts
6
+ export type FilterBarRootProps = Omit<ComponentProps<"fieldset">, "onChange" | "style"> & {
7
+ fields: readonly FilterBarField[];
8
+ operators: readonly FilterBarOperator[];
9
+ value: readonly FilterBarItem[];
10
+ onValueChange: (value: FilterBarItem[]) => void;
11
+ createItemId?: () => string;
12
+ disabled?: boolean;
13
+ readOnly?: boolean;
14
+ style?: CSSProperties & FilterBarKnobStyle;
15
+ };
16
+ declare function FilterBarRoot({ fields, operators, value, onValueChange, createItemId, disabled, readOnly, className, children, ref, "aria-label": ariaLabel, ...props }: FilterBarRootProps): import("react").JSX.Element;
17
+ export declare const FilterBar: {
18
+ Root: typeof FilterBarRoot;
19
+ Chips: typeof FilterBarChips;
20
+ Chip: typeof FilterBarChip;
21
+ AddButton: typeof FilterBarAddButton;
22
+ Input: typeof FilterBarInput;
23
+ Clear: typeof FilterBarClear;
24
+ };
25
+ export declare const DEFAULT_FILTER_OPERATORS: readonly FilterBarOperator[];
26
+ //#endregion
27
+ export type { FilterBarChipProps, FilterBarField, FilterBarItem, FilterBarOperator, FilterBarOption, FilterBarQuery, FilterBarScalar, FilterBarValue };
@@ -0,0 +1,304 @@
1
+ "use client";
2
+ import { cn } from "./lib/cn.js";
3
+ import { FilterBarContext } from "./filter-bar/context.js";
4
+ import { fieldOperators, filterNavigationKeys } from "./filter-bar/model.js";
5
+ import { filterMotionEnabled, useFilterBarMotion } from "./filter-bar/motion.js";
6
+ import { FilterBarAddButton, FilterBarChip, FilterBarChips, FilterBarClear, FilterBarInput } from "./filter-bar/parts.js";
7
+ import { useState } from "react";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ import { useRender } from "@base-ui/react/use-render";
10
+ //#region src/filter-bar.tsx
11
+ function FilterBarRoot({ fields, operators, value, onValueChange, createItemId = () => crypto.randomUUID(), disabled = false, readOnly = false, className, children, ref, "aria-label": ariaLabel = "Filters", ...props }) {
12
+ const [draft, setDraft] = useState(null);
13
+ const [exits, setExits] = useState([]);
14
+ const [announcement, setAnnouncement] = useState("");
15
+ const [focusNodes] = useState(() => /* @__PURE__ */ new Map());
16
+ const { rootRef, ...motion } = useFilterBarMotion();
17
+ function register(key, element) {
18
+ if (key.startsWith("chip:") || key.startsWith("entry:")) motion.register(key, element);
19
+ if (element) focusNodes.set(key, element);
20
+ else focusNodes.delete(key);
21
+ }
22
+ function transition(next) {
23
+ motion.capture();
24
+ setDraft(next);
25
+ if (next.stage === "value" && next.fieldId && next.operatorId) fields.find((field) => field.id === next.fieldId)?.onQueryChange?.({
26
+ fieldId: next.fieldId,
27
+ operatorId: next.operatorId,
28
+ query: next.query
29
+ });
30
+ }
31
+ function start(owner, anchor, stage = "field", item, initialQuery) {
32
+ if (disabled || readOnly) return;
33
+ if (draft?.owner === owner && draft.stage === stage) return;
34
+ const values = item?.value === null || item?.value === void 0 ? [] : [item.value].flat();
35
+ const query = initialQuery ?? "";
36
+ transition({
37
+ id: item?.id ?? createItemId(),
38
+ owner,
39
+ anchor,
40
+ stage,
41
+ fieldId: item?.fieldId,
42
+ operatorId: item?.operatorId,
43
+ values,
44
+ query,
45
+ editing: Boolean(item)
46
+ });
47
+ }
48
+ function close(restoreFocus = true) {
49
+ motion.capture();
50
+ if (restoreFocus && draft?.anchor.isConnected) draft.anchor.focus();
51
+ setDraft(null);
52
+ }
53
+ function back() {
54
+ if (!draft) return;
55
+ if (draft.stage === "field") return close();
56
+ if (draft.stage === "operator") return transition({
57
+ ...draft,
58
+ stage: "field",
59
+ operatorId: void 0,
60
+ values: [],
61
+ query: ""
62
+ });
63
+ const field = fields.find((candidate) => candidate.id === draft.fieldId);
64
+ const stage = field && fieldOperators(field, operators).length === 1 ? "field" : "operator";
65
+ transition({
66
+ ...draft,
67
+ stage,
68
+ values: [],
69
+ query: ""
70
+ });
71
+ }
72
+ function setQuery(query) {
73
+ if (!draft) return;
74
+ setDraft({
75
+ ...draft,
76
+ query
77
+ });
78
+ if (draft.stage === "value" && draft.fieldId && draft.operatorId) fields.find((field) => field.id === draft.fieldId)?.onQueryChange?.({
79
+ fieldId: draft.fieldId,
80
+ operatorId: draft.operatorId,
81
+ query
82
+ });
83
+ }
84
+ function commitDraft(next, nextValue) {
85
+ if (disabled || readOnly) return;
86
+ if (!next.fieldId || !next.operatorId) return;
87
+ const item = {
88
+ id: next.id,
89
+ fieldId: next.fieldId,
90
+ operatorId: next.operatorId,
91
+ value: nextValue
92
+ };
93
+ motion.capture();
94
+ onValueChange(next.editing ? value.map((current) => current.id === item.id ? item : current) : [...value, item]);
95
+ setAnnouncement(next.editing ? "Filter updated." : "Filter added.");
96
+ if (next.anchor.isConnected) next.anchor.focus();
97
+ setDraft(null);
98
+ }
99
+ function selectOperator(operatorId, next = draft) {
100
+ if (!next) return;
101
+ const operator = operators.find((candidate) => candidate.id === operatorId);
102
+ if (!operator) return;
103
+ const nextDraft = {
104
+ ...next,
105
+ operatorId,
106
+ stage: "value",
107
+ values: [],
108
+ query: ""
109
+ };
110
+ if (operator.arity === "none") commitDraft(nextDraft, null);
111
+ else transition(nextDraft);
112
+ }
113
+ function selectField(fieldId) {
114
+ if (!draft) return;
115
+ const field = fields.find((candidate) => candidate.id === fieldId);
116
+ if (!field) return;
117
+ const available = fieldOperators(field, operators);
118
+ const next = {
119
+ ...draft,
120
+ fieldId,
121
+ operatorId: void 0,
122
+ stage: "operator",
123
+ values: [],
124
+ query: ""
125
+ };
126
+ if (available.length === 1 && available[0]) selectOperator(available[0].id, next);
127
+ else transition(next);
128
+ }
129
+ function addExits(items) {
130
+ const root = rootRef.current;
131
+ if (!root || !filterMotionEnabled(root)) return;
132
+ const rootBounds = root.getBoundingClientRect();
133
+ const leaving = items.flatMap((item) => {
134
+ const node = focusNodes.get(`chip:${item.id}`);
135
+ if (!node) return [];
136
+ const bounds = node.getBoundingClientRect();
137
+ return [{
138
+ item,
139
+ style: {
140
+ position: "absolute",
141
+ left: bounds.left - rootBounds.left,
142
+ top: bounds.top - rootBounds.top,
143
+ width: bounds.width,
144
+ height: bounds.height
145
+ }
146
+ }];
147
+ });
148
+ setExits((current) => [...current, ...leaving]);
149
+ }
150
+ function focusEntry() {
151
+ [...focusNodes].find(([key]) => key.startsWith("entry:"))?.[1].focus();
152
+ }
153
+ function remove(item) {
154
+ if (disabled || readOnly) return;
155
+ motion.capture();
156
+ addExits([item]);
157
+ const index = value.findIndex((candidate) => candidate.id === item.id);
158
+ const neighbour = value[index + 1] ?? value[index - 1];
159
+ if (neighbour) focusNodes.get(`${neighbour.id}:field`)?.focus();
160
+ else focusEntry();
161
+ if (draft?.id === item.id) setDraft(null);
162
+ onValueChange(value.filter((candidate) => candidate.id !== item.id));
163
+ setAnnouncement("Filter removed.");
164
+ }
165
+ function clear() {
166
+ if (disabled || readOnly) return;
167
+ motion.capture();
168
+ addExits(value);
169
+ setDraft(null);
170
+ onValueChange([]);
171
+ focusEntry();
172
+ setAnnouncement("Filters cleared.");
173
+ }
174
+ function deleteFocusedFilter(event, itemId) {
175
+ const item = value.find((candidate) => candidate.id === itemId);
176
+ if (!item) return;
177
+ event.preventDefault();
178
+ remove(item);
179
+ }
180
+ function focusAdjacentFilter(event, itemId, part) {
181
+ const keys = filterNavigationKeys(value);
182
+ const target = keys[(itemId ? keys.indexOf(`${itemId}:${part}`) : keys.length) + (event.key === "ArrowLeft" ? -1 : 1)];
183
+ event.preventDefault();
184
+ if (target) focusNodes.get(target)?.focus();
185
+ else if (itemId) focusEntry();
186
+ }
187
+ function navigate(event, itemId, part) {
188
+ if (event.defaultPrevented) return;
189
+ const removing = event.key === "Delete" || event.key === "Backspace";
190
+ if (itemId && removing) return deleteFocusedFilter(event, itemId);
191
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") focusAdjacentFilter(event, itemId, part);
192
+ }
193
+ const context = {
194
+ fields,
195
+ operators,
196
+ items: value,
197
+ draft,
198
+ exits,
199
+ disabled,
200
+ readOnly,
201
+ register,
202
+ start,
203
+ close,
204
+ back,
205
+ setQuery,
206
+ selectField,
207
+ selectOperator,
208
+ selectValues: (values) => {
209
+ if (draft) transition({
210
+ ...draft,
211
+ values,
212
+ query: ""
213
+ });
214
+ },
215
+ commit: (nextValue) => {
216
+ if (draft) commitDraft(draft, nextValue);
217
+ },
218
+ remove,
219
+ clear,
220
+ releaseExit: (id) => setExits((current) => current.filter((exit) => exit.item.id !== id)),
221
+ navigate
222
+ };
223
+ const rootElement = useRender({
224
+ ref: [rootRef, ref ?? null],
225
+ render: /* @__PURE__ */ jsxs("fieldset", {
226
+ "aria-label": ariaLabel,
227
+ "data-control-ui": "filter-bar",
228
+ "data-control-family": "filter-bar",
229
+ "data-slot": "root",
230
+ "data-disabled": disabled || void 0,
231
+ "data-readonly": readOnly || void 0,
232
+ className: cn("relative flex min-w-0 flex-wrap items-center", className),
233
+ ...props,
234
+ children: [children, /* @__PURE__ */ jsx("span", {
235
+ className: "sr-only",
236
+ "aria-live": "polite",
237
+ children: announcement
238
+ })]
239
+ })
240
+ });
241
+ return /* @__PURE__ */ jsx(FilterBarContext.Provider, {
242
+ value: context,
243
+ children: rootElement
244
+ });
245
+ }
246
+ const FilterBar = {
247
+ Root: FilterBarRoot,
248
+ Chips: FilterBarChips,
249
+ Chip: FilterBarChip,
250
+ AddButton: FilterBarAddButton,
251
+ Input: FilterBarInput,
252
+ Clear: FilterBarClear
253
+ };
254
+ const DEFAULT_FILTER_OPERATORS = [
255
+ {
256
+ id: "is",
257
+ label: "is"
258
+ },
259
+ {
260
+ id: "is-not",
261
+ label: "is not"
262
+ },
263
+ {
264
+ id: "contains",
265
+ label: "contains"
266
+ },
267
+ {
268
+ id: "starts-with",
269
+ label: "starts with"
270
+ },
271
+ {
272
+ id: "gt",
273
+ label: ">"
274
+ },
275
+ {
276
+ id: "gte",
277
+ label: "≥"
278
+ },
279
+ {
280
+ id: "lt",
281
+ label: "<"
282
+ },
283
+ {
284
+ id: "lte",
285
+ label: "≤"
286
+ },
287
+ {
288
+ id: "in",
289
+ label: "is any of",
290
+ arity: "many"
291
+ },
292
+ {
293
+ id: "is-empty",
294
+ label: "is empty",
295
+ arity: "none"
296
+ },
297
+ {
298
+ id: "is-not-empty",
299
+ label: "is not empty",
300
+ arity: "none"
301
+ }
302
+ ];
303
+ //#endregion
304
+ export { DEFAULT_FILTER_OPERATORS, FilterBar };
@@ -17,6 +17,8 @@ export type ChatComposerProps = Omit<ComponentProps<"form">, "onSubmit" | "style
17
17
  state?: "idle" | "submitting" | "disabled";
18
18
  density?: ChatDensity;
19
19
  disabled?: boolean;
20
+ /** Let an empty message send, for composers where an attachment or recording carries the turn. */
21
+ allowEmptySubmit?: boolean;
20
22
  style?: CSSProperties & ChatComposerKnobStyle;
21
23
  };
22
24
  export type MentionItem = {
@@ -33,7 +35,7 @@ type ComposerKeyEvent = {
33
35
  };
34
36
  preventDefault: () => void;
35
37
  };
36
- export declare function useChatComposer({ value, defaultValue, onValueChange, onSubmit, state, density, disabled, trackSends }: Pick<ChatComposerProps, "value" | "defaultValue" | "onValueChange" | "onSubmit" | "state" | "density" | "disabled"> & {
38
+ export declare function useChatComposer({ value, defaultValue, onValueChange, onSubmit, state, density, disabled, allowEmptySubmit, trackSends }: Pick<ChatComposerProps, "value" | "defaultValue" | "onValueChange" | "onSubmit" | "state" | "density" | "disabled" | "allowEmptySubmit"> & {
37
39
  /** Count successful submits — only enabled when something reads counter (send-layer anchor), so idle apps pay no extra state update. */
38
40
  trackSends?: boolean;
39
41
  }): {
@@ -10,7 +10,7 @@ function useControllableText({ value, defaultValue = "", onValueChange }) {
10
10
  }
11
11
  return [currentValue, setValue];
12
12
  }
13
- function useChatComposer({ value, defaultValue, onValueChange, onSubmit, state = "idle", density = "comfortable", disabled = false, trackSends = false }) {
13
+ function useChatComposer({ value, defaultValue, onValueChange, onSubmit, state = "idle", density = "comfortable", disabled = false, allowEmptySubmit = false, trackSends = false }) {
14
14
  const [inputValue, setInputValue] = useControllableText({
15
15
  value,
16
16
  defaultValue,
@@ -19,7 +19,7 @@ function useChatComposer({ value, defaultValue, onValueChange, onSubmit, state =
19
19
  const [sendCount, setSendCount] = useState(0);
20
20
  const normalizedValue = inputValue.trim();
21
21
  const isDisabled = disabled || state === "disabled" || state === "submitting";
22
- const canSubmit = normalizedValue.length > 0 && !isDisabled;
22
+ const canSubmit = (normalizedValue.length > 0 || allowEmptySubmit) && !isDisabled;
23
23
  const isCompact = density === "compact";
24
24
  function clear() {
25
25
  setInputValue("");