@intentface/ui 0.0.0-stage → 0.7.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 (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -2
  3. package/dist/ask-user/ask-user.d.ts +104 -0
  4. package/dist/ask-user/ask-user.js +245 -0
  5. package/dist/ask-user/index.d.ts +3 -0
  6. package/dist/ask-user/index.js +3 -0
  7. package/dist/ask-user/index.parts.d.ts +1 -0
  8. package/dist/ask-user/index.parts.js +5 -0
  9. package/dist/attachments/attachments.d.ts +73 -0
  10. package/dist/attachments/attachments.js +121 -0
  11. package/dist/attachments/index.d.ts +3 -0
  12. package/dist/attachments/index.js +3 -0
  13. package/dist/attachments/index.parts.d.ts +1 -0
  14. package/dist/attachments/index.parts.js +5 -0
  15. package/dist/chip/chip.d.ts +45 -0
  16. package/dist/chip/chip.js +43 -0
  17. package/dist/chip/index.d.ts +2 -0
  18. package/dist/chip/index.js +2 -0
  19. package/dist/chip/index.parts.d.ts +1 -0
  20. package/dist/chip/index.parts.js +5 -0
  21. package/dist/chip-markdown.d.ts +25 -0
  22. package/dist/chip-markdown.js +54 -0
  23. package/dist/composer/actions.d.ts +35 -0
  24. package/dist/composer/actions.js +76 -0
  25. package/dist/composer/ask-user-machine.d.ts +52 -0
  26. package/dist/composer/ask-user-machine.js +164 -0
  27. package/dist/composer/attachments-machine.d.ts +34 -0
  28. package/dist/composer/attachments-machine.js +91 -0
  29. package/dist/composer/attachments.d.ts +16 -0
  30. package/dist/composer/attachments.js +32 -0
  31. package/dist/composer/command-list.d.ts +49 -0
  32. package/dist/composer/command-list.js +360 -0
  33. package/dist/composer/commands.d.ts +28 -0
  34. package/dist/composer/commands.js +56 -0
  35. package/dist/composer/container.d.ts +3 -0
  36. package/dist/composer/container.js +32 -0
  37. package/dist/composer/controller.d.ts +3 -0
  38. package/dist/composer/controller.js +6 -0
  39. package/dist/composer/editor-dom.d.ts +57 -0
  40. package/dist/composer/editor-dom.js +517 -0
  41. package/dist/composer/fuzzy.d.ts +4 -0
  42. package/dist/composer/fuzzy.js +49 -0
  43. package/dist/composer/index.d.ts +25 -0
  44. package/dist/composer/index.js +13 -0
  45. package/dist/composer/index.parts.d.ts +11 -0
  46. package/dist/composer/index.parts.js +15 -0
  47. package/dist/composer/internals.d.ts +30 -0
  48. package/dist/composer/internals.js +102 -0
  49. package/dist/composer/keyboard.d.ts +61 -0
  50. package/dist/composer/keyboard.js +75 -0
  51. package/dist/composer/panel.d.ts +28 -0
  52. package/dist/composer/panel.js +76 -0
  53. package/dist/composer/placeholder.d.ts +10 -0
  54. package/dist/composer/placeholder.js +8 -0
  55. package/dist/composer/popover.d.ts +12 -0
  56. package/dist/composer/popover.js +62 -0
  57. package/dist/composer/prefix-detection.d.ts +23 -0
  58. package/dist/composer/prefix-detection.js +67 -0
  59. package/dist/composer/root.d.ts +21 -0
  60. package/dist/composer/root.js +124 -0
  61. package/dist/composer/segments.d.ts +108 -0
  62. package/dist/composer/segments.js +366 -0
  63. package/dist/composer/store.d.ts +98 -0
  64. package/dist/composer/store.js +457 -0
  65. package/dist/composer/textarea.d.ts +39 -0
  66. package/dist/composer/textarea.js +64 -0
  67. package/dist/composer/trigger-tracker.d.ts +12 -0
  68. package/dist/composer/trigger-tracker.js +90 -0
  69. package/dist/composer/types.d.ts +127 -0
  70. package/dist/composer/types.js +4 -0
  71. package/dist/composer/use-composer-editor.d.ts +43 -0
  72. package/dist/composer/use-composer-editor.js +653 -0
  73. package/dist/generative/catalog.d.ts +177 -0
  74. package/dist/generative/catalog.js +20 -0
  75. package/dist/generative/index.d.ts +26 -0
  76. package/dist/generative/index.js +20 -0
  77. package/dist/generative/json-schema.d.ts +45 -0
  78. package/dist/generative/json-schema.js +216 -0
  79. package/dist/generative/markdown.d.ts +21 -0
  80. package/dist/generative/markdown.js +76 -0
  81. package/dist/generative/tree.d.ts +56 -0
  82. package/dist/generative/tree.js +365 -0
  83. package/dist/generative/use-tree.d.ts +12 -0
  84. package/dist/generative/use-tree.js +64 -0
  85. package/dist/internal/change-event-details.d.ts +12 -0
  86. package/dist/internal/change-event-details.js +16 -0
  87. package/dist/internal/collapsible.d.ts +22 -0
  88. package/dist/internal/collapsible.js +94 -0
  89. package/dist/internal/collection.d.ts +32 -0
  90. package/dist/internal/collection.js +72 -0
  91. package/dist/internal/iso-layout-effect.d.ts +2 -0
  92. package/dist/internal/iso-layout-effect.js +6 -0
  93. package/dist/internal/portal.d.ts +6 -0
  94. package/dist/internal/portal.js +18 -0
  95. package/dist/internal/primitive-props.d.ts +4 -0
  96. package/dist/internal/primitive-props.js +3 -0
  97. package/dist/internal/render/baseTypes.d.ts +18 -0
  98. package/dist/internal/render/baseTypes.js +4 -0
  99. package/dist/internal/render/empty.d.ts +3 -0
  100. package/dist/internal/render/empty.js +5 -0
  101. package/dist/internal/render/getReactElementRef.d.ts +5 -0
  102. package/dist/internal/render/getReactElementRef.js +15 -0
  103. package/dist/internal/render/getStateAttributesProps.d.ts +4 -0
  104. package/dist/internal/render/getStateAttributesProps.js +23 -0
  105. package/dist/internal/render/mergeObjects.d.ts +1 -0
  106. package/dist/internal/render/mergeObjects.js +14 -0
  107. package/dist/internal/render/mergeProps.d.ts +55 -0
  108. package/dist/internal/render/mergeProps.js +183 -0
  109. package/dist/internal/render/positioning.d.ts +33 -0
  110. package/dist/internal/render/positioning.js +112 -0
  111. package/dist/internal/render/reactVersion.d.ts +3 -0
  112. package/dist/internal/render/reactVersion.js +7 -0
  113. package/dist/internal/render/resolveClassName.d.ts +8 -0
  114. package/dist/internal/render/resolveClassName.js +12 -0
  115. package/dist/internal/render/resolveStyle.d.ts +8 -0
  116. package/dist/internal/render/resolveStyle.js +12 -0
  117. package/dist/internal/render/transition.d.ts +47 -0
  118. package/dist/internal/render/transition.js +259 -0
  119. package/dist/internal/render/types.d.ts +74 -0
  120. package/dist/internal/render/types.js +3 -0
  121. package/dist/internal/render/useMergedRefs.d.ts +21 -0
  122. package/dist/internal/render/useMergedRefs.js +102 -0
  123. package/dist/internal/render/useRefWithInit.d.ts +10 -0
  124. package/dist/internal/render/useRefWithInit.js +12 -0
  125. package/dist/internal/render/useRender.d.ts +71 -0
  126. package/dist/internal/render/useRender.js +12 -0
  127. package/dist/internal/render/useRenderElement.d.ts +62 -0
  128. package/dist/internal/render/useRenderElement.js +157 -0
  129. package/dist/internal/render/warn.d.ts +1 -0
  130. package/dist/internal/render/warn.js +15 -0
  131. package/dist/internal/safe-polygon.d.ts +21 -0
  132. package/dist/internal/safe-polygon.js +179 -0
  133. package/dist/internal/scroll-into-view-within.d.ts +1 -0
  134. package/dist/internal/scroll-into-view-within.js +26 -0
  135. package/dist/internal/standard-schema.d.ts +38 -0
  136. package/dist/internal/standard-schema.js +4 -0
  137. package/dist/internal/state-mappings.d.ts +8 -0
  138. package/dist/internal/state-mappings.js +17 -0
  139. package/dist/internal/use-controlled.d.ts +19 -0
  140. package/dist/internal/use-controlled.js +33 -0
  141. package/dist/message/index.d.ts +3 -0
  142. package/dist/message/index.js +3 -0
  143. package/dist/message/index.parts.d.ts +1 -0
  144. package/dist/message/index.parts.js +5 -0
  145. package/dist/message/message.d.ts +58 -0
  146. package/dist/message/message.js +93 -0
  147. package/dist/message-utils.d.ts +55 -0
  148. package/dist/message-utils.js +79 -0
  149. package/dist/nav/index.d.ts +4 -0
  150. package/dist/nav/index.js +5 -0
  151. package/dist/nav/index.parts.d.ts +3 -0
  152. package/dist/nav/index.parts.js +7 -0
  153. package/dist/nav/nav.d.ts +152 -0
  154. package/dist/nav/nav.js +660 -0
  155. package/dist/nav/store.d.ts +50 -0
  156. package/dist/nav/store.js +95 -0
  157. package/dist/reasoning/index.d.ts +3 -0
  158. package/dist/reasoning/index.js +3 -0
  159. package/dist/reasoning/index.parts.d.ts +1 -0
  160. package/dist/reasoning/index.parts.js +5 -0
  161. package/dist/reasoning/reasoning.d.ts +38 -0
  162. package/dist/reasoning/reasoning.js +67 -0
  163. package/dist/shell/index.d.ts +5 -0
  164. package/dist/shell/index.js +5 -0
  165. package/dist/shell/index.parts.d.ts +3 -0
  166. package/dist/shell/index.parts.js +7 -0
  167. package/dist/shell/shell.d.ts +108 -0
  168. package/dist/shell/shell.js +390 -0
  169. package/dist/shell/store.d.ts +112 -0
  170. package/dist/shell/store.js +134 -0
  171. package/dist/steps/index.d.ts +2 -0
  172. package/dist/steps/index.js +2 -0
  173. package/dist/steps/index.parts.d.ts +1 -0
  174. package/dist/steps/index.parts.js +5 -0
  175. package/dist/steps/steps.d.ts +90 -0
  176. package/dist/steps/steps.js +116 -0
  177. package/dist/tabs/anchored.d.ts +59 -0
  178. package/dist/tabs/anchored.js +109 -0
  179. package/dist/tabs/hover.d.ts +33 -0
  180. package/dist/tabs/hover.js +169 -0
  181. package/dist/tabs/index.d.ts +5 -0
  182. package/dist/tabs/index.js +4 -0
  183. package/dist/tabs/index.parts.d.ts +4 -0
  184. package/dist/tabs/index.parts.js +8 -0
  185. package/dist/tabs/store.d.ts +131 -0
  186. package/dist/tabs/store.js +248 -0
  187. package/dist/tabs/tabs.d.ts +217 -0
  188. package/dist/tabs/tabs.js +458 -0
  189. package/dist/thread/geometry.d.ts +23 -0
  190. package/dist/thread/geometry.js +48 -0
  191. package/dist/thread/index.d.ts +4 -0
  192. package/dist/thread/index.js +3 -0
  193. package/dist/thread/index.parts.d.ts +1 -0
  194. package/dist/thread/index.parts.js +5 -0
  195. package/dist/thread/stores.d.ts +21 -0
  196. package/dist/thread/stores.js +63 -0
  197. package/dist/thread/thread.d.ts +57 -0
  198. package/dist/thread/thread.js +663 -0
  199. package/dist/types.d.ts +58 -0
  200. package/dist/types.js +17 -0
  201. package/package.json +125 -4
@@ -0,0 +1,34 @@
1
+ import { type AttachmentErrorCode, type AttachmentItem } from "../attachments/index.js";
2
+ export type AttachmentStoreState = {
3
+ items: AttachmentItem[];
4
+ error: AttachmentErrorCode | null;
5
+ };
6
+ export type AttachmentStoreAction = {
7
+ type: "add";
8
+ files: File[] | FileList;
9
+ } | {
10
+ type: "remove";
11
+ id: string;
12
+ } | {
13
+ type: "reset";
14
+ };
15
+ export type AttachmentStoreConfig = {
16
+ accept: string;
17
+ maxFiles: number;
18
+ maxFileSize: number;
19
+ convert: (file: File) => AttachmentItem;
20
+ destroy: (item: AttachmentItem) => void;
21
+ };
22
+ export declare const INITIAL_ATTACHMENT_STATE: AttachmentStoreState;
23
+ export declare const attachmentReducer: (state: AttachmentStoreState, action: AttachmentStoreAction, config: AttachmentStoreConfig) => AttachmentStoreState;
24
+ export type DragHandlerCallbacks = {
25
+ isInScope: (event: DragEvent) => boolean;
26
+ onFiles: (files: FileList) => void;
27
+ setDragging: (active: boolean) => void;
28
+ };
29
+ export declare const createDragHandlers: (callbacks: DragHandlerCallbacks) => {
30
+ onDragOver: (event: Event) => void;
31
+ onDragEnter: (event: Event) => void;
32
+ onDragLeave: (event: Event) => void;
33
+ onDrop: (event: Event) => void;
34
+ };
@@ -0,0 +1,91 @@
1
+ // Attachments — pure store reducer that validates and accumulates files, plus
2
+ // the drag-and-drop handler factory. The store owns the state cell; this owns
3
+ // the rules.
4
+ import { matchesAccept } from "../attachments/index.js";
5
+ export const INITIAL_ATTACHMENT_STATE = {
6
+ items: [],
7
+ error: null,
8
+ };
9
+ export const attachmentReducer = (state, action, config) => {
10
+ switch (action.type) {
11
+ case "add": {
12
+ const incoming = [...action.files];
13
+ if (!incoming.length)
14
+ return state;
15
+ const accepted = incoming.filter((file) => matchesAccept(file, config.accept));
16
+ if (!accepted.length) {
17
+ return { ...state, error: "accept" };
18
+ }
19
+ const sized = accepted.filter((file) => file.size <= config.maxFileSize);
20
+ if (!sized.length) {
21
+ return { ...state, error: "max_file_size" };
22
+ }
23
+ const capacity = Math.max(0, config.maxFiles - state.items.length);
24
+ const capped = sized.slice(0, capacity);
25
+ const overCapacityError = sized.length > capacity ? "max_files" : null;
26
+ if (!capped.length) {
27
+ return { ...state, error: overCapacityError ?? state.error };
28
+ }
29
+ return {
30
+ items: [...state.items, ...capped.map(config.convert)],
31
+ error: overCapacityError,
32
+ };
33
+ }
34
+ case "remove": {
35
+ const found = state.items.find((item) => item.id === action.id);
36
+ if (found)
37
+ config.destroy(found);
38
+ return {
39
+ items: state.items.filter((item) => item.id !== action.id),
40
+ error: null,
41
+ };
42
+ }
43
+ case "reset": {
44
+ for (const item of state.items)
45
+ config.destroy(item);
46
+ return INITIAL_ATTACHMENT_STATE;
47
+ }
48
+ }
49
+ };
50
+ export const createDragHandlers = (callbacks) => {
51
+ const counter = { current: 0 };
52
+ const carriesFiles = (event) => event.dataTransfer?.types?.includes("Files") ?? false;
53
+ return {
54
+ onDragOver: (event) => {
55
+ const dragEvent = event;
56
+ if (!callbacks.isInScope(dragEvent))
57
+ return;
58
+ if (carriesFiles(dragEvent))
59
+ dragEvent.preventDefault();
60
+ },
61
+ onDragEnter: (event) => {
62
+ const dragEvent = event;
63
+ if (!callbacks.isInScope(dragEvent))
64
+ return;
65
+ if (carriesFiles(dragEvent)) {
66
+ counter.current++;
67
+ callbacks.setDragging(true);
68
+ }
69
+ },
70
+ onDragLeave: (event) => {
71
+ const dragEvent = event;
72
+ if (!callbacks.isInScope(dragEvent))
73
+ return;
74
+ counter.current--;
75
+ if (counter.current === 0)
76
+ callbacks.setDragging(false);
77
+ },
78
+ onDrop: (event) => {
79
+ const dragEvent = event;
80
+ if (!callbacks.isInScope(dragEvent))
81
+ return;
82
+ if (carriesFiles(dragEvent))
83
+ dragEvent.preventDefault();
84
+ counter.current = 0;
85
+ callbacks.setDragging(false);
86
+ const files = dragEvent.dataTransfer?.files;
87
+ if (files && files.length > 0)
88
+ callbacks.onFiles(files);
89
+ },
90
+ };
91
+ };
@@ -0,0 +1,16 @@
1
+ import type { ReactNode } from "react";
2
+ import { type AttachmentItem } from "../attachments/index.js";
3
+ import type { PrimitiveProps } from "../internal/primitive-props.js";
4
+ export type ComposerAttachmentsProps = {
5
+ convert?: (file: File) => AttachmentItem;
6
+ destroy?: (item: AttachmentItem) => void;
7
+ accept?: string;
8
+ maxFiles?: number;
9
+ maxFileSize?: number;
10
+ multiple?: boolean;
11
+ globalDrop?: boolean;
12
+ children?: ReactNode;
13
+ };
14
+ export declare const ComposerAttachments: ({ convert, destroy, accept, maxFiles, maxFileSize, multiple, globalDrop, children, }: ComposerAttachmentsProps) => import("react").JSX.Element;
15
+ export type ComposerAttachmentTriggerProps = PrimitiveProps<"button">;
16
+ export declare const ComposerAttachmentTrigger: ({ className, render, style, ...elementProps }: ComposerAttachmentTriggerProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { revokeAttachmentUrl, toAttachmentItem } from "../attachments/index.js";
4
+ import { useRenderElement } from "../internal/render/useRenderElement.js";
5
+ import { useComposer, useComposerContextStore } from "./store.js";
6
+ export const ComposerAttachments = ({ convert = toAttachmentItem, destroy = revokeAttachmentUrl, accept = "", maxFiles = Number.POSITIVE_INFINITY, maxFileSize = Number.POSITIVE_INFINITY, multiple = true, globalDrop = false, children, }) => {
7
+ const store = useComposerContextStore();
8
+ const attachments = useComposer((composer) => composer.attachments);
9
+ store.attachmentConfigRef.current = { accept, maxFiles, maxFileSize, convert, destroy };
10
+ attachments.globalDropRef.current = globalDrop;
11
+ const handleFileChange = (event) => {
12
+ if (event.currentTarget.files) {
13
+ attachments.add(event.currentTarget.files);
14
+ }
15
+ event.currentTarget.value = "";
16
+ };
17
+ return (_jsxs(_Fragment, { children: [_jsx("input", { accept: accept, style: { display: "none" }, multiple: multiple, onChange: handleFileChange, ref: attachments.fileInputRef, type: "file" }), children] }));
18
+ };
19
+ export const ComposerAttachmentTrigger = ({ className, render, style, ...elementProps }) => {
20
+ const attachments = useComposer((composer) => composer.attachments);
21
+ return useRenderElement("button", { className, render, style }, {
22
+ props: [
23
+ {
24
+ // Default overridable name — the trigger is typically icon-only.
25
+ "aria-label": "Add attachment",
26
+ "data-composer-attachment-trigger": "",
27
+ onClick: () => attachments.openFileDialog(),
28
+ },
29
+ elementProps,
30
+ ],
31
+ });
32
+ };
@@ -0,0 +1,49 @@
1
+ import { type ComponentProps, type ReactNode } from "react";
2
+ import type { PrimitiveProps } from "../internal/primitive-props.js";
3
+ import { Commands } from "./commands.js";
4
+ import type { CommandItemData } from "./types.js";
5
+ export type CommandListState = "loading" | "empty" | "ready";
6
+ export declare const optionDomId: (listboxId: string, value: string) => string;
7
+ export type ComposerCommandState = {
8
+ /** Present as data-loading while an async items callback is in flight. */
9
+ loading: boolean;
10
+ /** Present as data-empty when nothing matches the query. */
11
+ empty: boolean;
12
+ };
13
+ export type ComposerCommandProps = PrimitiveProps<"div", ComposerCommandState> & {
14
+ prefix: string;
15
+ };
16
+ export declare const ComposerCommand: ({ prefix, className, render, style, children, ...elementProps }: ComposerCommandProps) => import("react").JSX.Element | null;
17
+ export declare const useCommandListItems: <Item extends CommandItemData = CommandItemData>() => {
18
+ items: Item[];
19
+ state: CommandListState;
20
+ };
21
+ export type ComposerCommandListProps<Item extends CommandItemData> = Omit<PrimitiveProps<"div">, "children"> & {
22
+ children: (item: Item) => ReactNode;
23
+ };
24
+ export declare const ComposerCommandList: <Item extends CommandItemData>({ className, render, style, children: renderItem, ...elementProps }: ComposerCommandListProps<Item>) => ReactNode;
25
+ export type ComposerCommandLoadingProps = PrimitiveProps<"div">;
26
+ export declare const ComposerCommandLoading: ({ children, className, render, style, ...elementProps }: ComposerCommandLoadingProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
27
+ export type ComposerCommandEmptyProps = PrimitiveProps<"div">;
28
+ export declare const ComposerCommandEmpty: ({ children, className, render, style, ...elementProps }: ComposerCommandEmptyProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
29
+ export type ComposerCommandDismissProps = PrimitiveProps<"button">;
30
+ export declare const ComposerCommandDismiss: ({ children, className, render, style, ...elementProps }: ComposerCommandDismissProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
31
+ export type ComposerCommandItemProps = Omit<ComponentProps<typeof Commands.Item>, "highlighted" | "disabled"> & {
32
+ value: string;
33
+ };
34
+ export declare const ComposerCommandItem: ({ value, ...props }: ComposerCommandItemProps) => import("react").JSX.Element;
35
+ export type ComposerCommandItemIconProps = PrimitiveProps<"span">;
36
+ export declare const ComposerCommandItemIcon: ({ className, render, style, ...elementProps }: ComposerCommandItemIconProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
37
+ export type ComposerCommandItemLabelProps = PrimitiveProps<"span">;
38
+ export declare const ComposerCommandItemLabel: ({ className, render, style, ...elementProps }: ComposerCommandItemLabelProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
39
+ export type ComposerCommandItemDescriptionProps = PrimitiveProps<"span">;
40
+ export declare const ComposerCommandItemDescription: ({ className, render, style, ...elementProps }: ComposerCommandItemDescriptionProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
41
+ export type ComposerCommandGroupProps<Item extends CommandItemData = CommandItemData> = {
42
+ /** Bucket the resolved items by this key (defines the app's grouping taxonomy). */
43
+ groupBy: (item: Item) => string;
44
+ /** Render one group: its key + that group's already-filtered items. */
45
+ children: (group: string, items: Item[]) => ReactNode;
46
+ };
47
+ export declare const ComposerCommandGroup: <Item extends CommandItemData = CommandItemData>({ groupBy, children: renderGroup, }: ComposerCommandGroupProps<Item>) => ReactNode;
48
+ export type ComposerCommandGroupLabelProps = PrimitiveProps<"div">;
49
+ export declare const ComposerCommandGroupLabel: ({ className, render, style, ...elementProps }: ComposerCommandGroupLabelProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
@@ -0,0 +1,360 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
+ // Composer.Command family — resolution (sync/async items, fuzzy filter),
4
+ // keyboard/mouse selection, chip insertion, dismissal, and the nav/items
5
+ // context split (highlight changes don't re-render the items map, and items
6
+ // mutations don't re-render every row). All unstyled; state panels as
7
+ // data-state / data-highlighted.
8
+ import { createContext, Fragment, use, useCallback, useEffect, useMemo, useRef, useState, } from "react";
9
+ import { useRenderElement } from "../internal/render/useRenderElement.js";
10
+ import { Commands } from "./commands.js";
11
+ import { filterArrayItems, suggestionRemainder } from "./fuzzy.js";
12
+ import { useAsRef, useComposerInternals, useIsomorphicLayoutEffect } from "./internals.js";
13
+ import { useComposer, useComposerContextStore } from "./store.js";
14
+ const CommandListNavContext = createContext(null);
15
+ const CommandListItemsContext = createContext(null);
16
+ // ---------------------------------------------------------------------------
17
+ // Item resolution — turn the prefix's items config (array or async callback)
18
+ // into the current filtered list + loading/empty/ready state.
19
+ // ---------------------------------------------------------------------------
20
+ const useResolvedItems = (itemsProp, query, isActive) => {
21
+ const isCallback = typeof itemsProp === "function";
22
+ const [asyncState, setAsyncState] = useState(() => ({ items: [], loading: isCallback }));
23
+ useEffect(() => {
24
+ if (typeof itemsProp !== "function")
25
+ return;
26
+ if (!isActive)
27
+ return;
28
+ const controller = new AbortController();
29
+ let cancelled = false;
30
+ let result;
31
+ try {
32
+ result = itemsProp(query, { signal: controller.signal });
33
+ }
34
+ catch (error) {
35
+ console.warn("Composer.commands items callback threw:", error);
36
+ setAsyncState({ items: [], loading: false });
37
+ return () => {
38
+ cancelled = true;
39
+ controller.abort();
40
+ };
41
+ }
42
+ if (result instanceof Promise) {
43
+ setAsyncState((previous) => ({ ...previous, loading: true }));
44
+ result.then((resolved) => {
45
+ if (cancelled)
46
+ return;
47
+ setAsyncState({ items: resolved, loading: false });
48
+ }, (error) => {
49
+ if (cancelled)
50
+ return;
51
+ if (error?.name === "AbortError")
52
+ return;
53
+ console.warn("Composer.commands items callback rejected:", error);
54
+ setAsyncState((previous) => ({ ...previous, loading: false }));
55
+ });
56
+ }
57
+ else {
58
+ setAsyncState({ items: result, loading: false });
59
+ }
60
+ return () => {
61
+ cancelled = true;
62
+ controller.abort();
63
+ };
64
+ }, [itemsProp, query, isActive]);
65
+ if (Array.isArray(itemsProp)) {
66
+ const items = filterArrayItems(itemsProp, query);
67
+ return { items, state: items.length === 0 ? "empty" : "ready" };
68
+ }
69
+ if (asyncState.loading) {
70
+ // Locally filter the most-recent resolved set so typing feels instant
71
+ // while the new fetch is in flight. The server result replaces this once
72
+ // it lands.
73
+ return {
74
+ items: filterArrayItems(asyncState.items, query),
75
+ state: "loading",
76
+ };
77
+ }
78
+ return {
79
+ items: asyncState.items,
80
+ state: asyncState.items.length === 0 ? "empty" : "ready",
81
+ };
82
+ };
83
+ // ---------------------------------------------------------------------------
84
+ // Composer.Command — the orchestrator: resolves items for the active
85
+ // prefix, owns the highlight/selection/dismiss logic, and provides the two
86
+ // contexts. Renders null unless this prefix is the active one.
87
+ // ---------------------------------------------------------------------------
88
+ const EMPTY_ITEMS = [];
89
+ // Deterministic option-row id shared by the row itself and the editor's
90
+ // aria-activedescendant. Values sanitize to valid id tokens.
91
+ export const optionDomId = (listboxId, value) => `${listboxId}-option-${value.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
92
+ // The badge's hint chrome: non-editable so the caret can't enter it, no chip
93
+ // id so the reader/mappers treat it as zero-width presentation, aria-hidden
94
+ // so screen readers skip the ghost. Styled via its part attribute like any other part.
95
+ const createHintElement = (badge) => {
96
+ const hint = badge.ownerDocument.createElement("span");
97
+ hint.setAttribute("data-command-hint", "");
98
+ hint.setAttribute("contenteditable", "false");
99
+ hint.setAttribute("aria-hidden", "true");
100
+ badge.appendChild(hint);
101
+ return hint;
102
+ };
103
+ export const ComposerCommand = ({ prefix, className, render, style, children, ...elementProps }) => {
104
+ const store = useComposerContextStore();
105
+ const internals = useComposerInternals();
106
+ // Renders while this prefix is the active one AND through its close animation. `active`
107
+ // flips false the instant the trigger closes, but the store keeps `present` sticky until
108
+ // the Panel's exit animation finishes (finalizePanelClose). `wasActive` marks that this
109
+ // prefix — not a sibling — is the one closing, so only it keeps rendering during the window.
110
+ const isActive = useComposer((composer) => composer.commands.active && composer.commands.trigger === prefix);
111
+ const present = useComposer((composer) => composer.commands.present);
112
+ const query = useComposer((composer) => composer.commands.query);
113
+ // State adjusted during render (not a ref React might keep from a discarded
114
+ // render); it only flips at open and at the end of the close, so it costs a
115
+ // re-render at those two moments and nowhere else.
116
+ const [wasActive, setWasActive] = useState(false);
117
+ if (isActive && !wasActive)
118
+ setWasActive(true);
119
+ else if (!isActive && !present && wasActive)
120
+ setWasActive(false);
121
+ const isPresent = isActive || (present && wasActive);
122
+ const config = internals.commands[prefix];
123
+ const itemsProp = config?.items ?? EMPTY_ITEMS;
124
+ const kind = config?.kind ?? "execute";
125
+ // Freeze the last resolved items while closing so the exiting panel shows what it had,
126
+ // not a re-filter/re-fetch against the now-empty query.
127
+ const resolved = useResolvedItems(itemsProp, query, isActive);
128
+ const lastResolvedRef = useRef(resolved);
129
+ if (isActive)
130
+ lastResolvedRef.current = resolved;
131
+ const { items, state } = isActive ? resolved : lastResolvedRef.current;
132
+ // The highlight index lives in the store (so the editor's keydown handler can
133
+ // move it). It's raw/unbounded; wrap it by the current item count here. The
134
+ // wrap runs over the enabled subset only, so disabled rows render but never
135
+ // take the highlight — arrow keys hop over them for free. An empty list has
136
+ // no highlighted row — the "No results" row stands in as the sole selectable.
137
+ const enabledItems = useMemo(() => items.filter((item) => !item.disabled), [items]);
138
+ const highlightIndex = useComposer((composer) => composer.commands.highlightIndex);
139
+ const activeIndex = enabledItems.length > 0
140
+ ? ((highlightIndex % enabledItems.length) + enabledItems.length) % enabledItems.length
141
+ : -1;
142
+ const highlightedItem = enabledItems[activeIndex] ?? null;
143
+ const effectiveHighlight = highlightedItem?.value ?? null;
144
+ // Badge hint — one real element (span[data-command-hint], appended
145
+ // inside the badge) carrying either the ghost-text completion of the
146
+ // highlighted item or the per-prefix empty-query placeholder. One slot, one
147
+ // value: the suggestion wins by a plain ?? chain, so exclusivity is code,
148
+ // not CSS specificity. The reader and position mappers treat non-editable,
149
+ // non-chip elements as zero-width presentation, so the hint never reaches
150
+ // the model. The badge is engine-owned DOM out of JSX reach — same access
151
+ // pattern as the popover's anchor query — so this is a true DOM-integration
152
+ // effect. No deps: badge identity changes on token rewraps, which always
153
+ // coincide with a re-render here (query/highlight subscribed).
154
+ const suggestion = isActive && highlightedItem && (config?.suggestion ?? true)
155
+ ? suggestionRemainder(query, highlightedItem.label)
156
+ : null;
157
+ const hintText = suggestion ?? (isActive && query === "" ? (config?.placeholder ?? "Type to filter") : null);
158
+ useIsomorphicLayoutEffect(() => {
159
+ const badge = store.editorRef.current?.getRootElement()?.querySelector("[data-command-badge]");
160
+ const existingHint = badge?.querySelector("[data-command-hint]") ?? null;
161
+ if (!badge || !hintText) {
162
+ existingHint?.remove();
163
+ return;
164
+ }
165
+ const hint = existingHint ?? createHintElement(badge);
166
+ if (hint.textContent !== hintText)
167
+ hint.textContent = hintText;
168
+ });
169
+ // Mirror the highlighted row's DOM id into the store — the editor renders
170
+ // it as aria-activedescendant. This is where the raw highlight index
171
+ // resolves against the actual item list, so the id can only be derived here.
172
+ // Only the ACTIVE command writes: with several Commands mounted, an inactive
173
+ // sibling writing null would ping-pong against the active one's id forever
174
+ // (max-update-depth). Clearing on close is setCommands' job.
175
+ useIsomorphicLayoutEffect(() => {
176
+ if (!isActive)
177
+ return;
178
+ store.setActiveOptionId(highlightedItem ? optionDomId(store.listboxId, highlightedItem.value) : null);
179
+ });
180
+ const selectByValue = useCallback((value) => {
181
+ const editor = store.editorRef.current;
182
+ if (!editor)
183
+ return;
184
+ const item = items.find((candidate) => candidate.value === value);
185
+ if (!item || item.disabled)
186
+ return;
187
+ if (kind === "insert") {
188
+ editor.insertChipAtTrigger({
189
+ prefix,
190
+ value: item.value,
191
+ label: item.label ?? item.value,
192
+ icon: item.icon,
193
+ });
194
+ }
195
+ else {
196
+ editor.deleteTrigger();
197
+ // Read the attachment actions lazily at selection time — their
198
+ // identities are store-stable, so subscribing would only re-render the
199
+ // list on unrelated attachment changes.
200
+ const { add, remove, openFileDialog } = store.getSnapshot().attachments;
201
+ const onSelectContext = {
202
+ editor: store.controller,
203
+ attachments: { add, remove, openFileDialog },
204
+ };
205
+ item.onSelect?.(onSelectContext);
206
+ }
207
+ editor.closeCommands();
208
+ }, [store, items, kind, prefix]);
209
+ const dismiss = useCallback(() => {
210
+ store.editorRef.current?.dismissCommands();
211
+ }, [store]);
212
+ // The editor's keydown handler invokes the current selection through
213
+ // commandSelectRef. Keep the latest values in a ref so the callback stays
214
+ // stable, and register it via the root's callback ref (commit-time) — not an
215
+ // effect, not a render-phase assignment.
216
+ const selectState = useAsRef({ effectiveHighlight, selectByValue, dismiss, state });
217
+ const runSelect = useCallback(() => {
218
+ const { effectiveHighlight, selectByValue, dismiss, state } = selectState.current;
219
+ // With a match, select it. When empty, the "No results" row is the sole
220
+ // option and selecting it dismisses — so Tab/Enter aren't dead (Linear-style).
221
+ if (effectiveHighlight)
222
+ selectByValue(effectiveHighlight);
223
+ else if (state === "empty")
224
+ dismiss();
225
+ }, []);
226
+ const registerSelect = useCallback((node) => {
227
+ store.commandSelectRef.current = node ? runSelect : null;
228
+ }, [store, runSelect]);
229
+ // Scroll the highlighted row into view as it becomes the highlight. Stable, so
230
+ // React calls it only on highlight change — no per-render scroll, no effect.
231
+ //
232
+ // container:"nearest" hard-scopes the scroll to the list on Chromium.
233
+ // Safari/Firefox silently ignore the member (unknown dictionary keys) and
234
+ // run an UNSCOPED block:"nearest" — scrollIntoView then scrolls every
235
+ // ancestor scrolling box, the window included, whenever the row isn't
236
+ // viewport-visible.
237
+ //
238
+ // KNOWN BUG (accepted 2026-07-10): on Safari, arrowing through the list
239
+ // while the popup opens can scroll the page behind it (reproduced; the
240
+ // positioner's viewport containment is not sufficient there). Accepted in
241
+ // exchange for native scroll-padding support and −24 lines; goes away when
242
+ // WebKit ships `container`. If it starts to hurt sooner, the scoped manual
243
+ // implementation lives in this function's git history (PR #39, first
244
+ // commit).
245
+ const scrollHighlightedIntoView = useCallback((node) => {
246
+ // Cast: lib.dom doesn't know the `container` member yet (Chromium-only).
247
+ node?.scrollIntoView({ block: "nearest", container: "nearest" });
248
+ }, []);
249
+ const navContext = useMemo(() => ({
250
+ highlightedValue: effectiveHighlight,
251
+ setHighlightedValue: (value) => {
252
+ const index = value === null ? -1 : enabledItems.findIndex((item) => item.value === value);
253
+ if (index >= 0)
254
+ store.setHighlight(index);
255
+ },
256
+ selectByValue,
257
+ dismiss,
258
+ scrollHighlightedIntoView,
259
+ }), [effectiveHighlight, enabledItems, selectByValue, dismiss, scrollHighlightedIntoView, store]);
260
+ const itemsContext = useMemo(() => ({ items, state }), [items, state]);
261
+ const element = useRenderElement("div", { className, render, style }, {
262
+ enabled: isPresent,
263
+ state: { loading: state === "loading", empty: state === "empty" },
264
+ ref: registerSelect,
265
+ props: [{ "data-composer-command-list": "", children }, elementProps],
266
+ });
267
+ if (!isPresent)
268
+ return null;
269
+ return (_jsx(CommandListItemsContext, { value: itemsContext, children: _jsx(CommandListNavContext, { value: navContext, children: element }) }));
270
+ };
271
+ // ---------------------------------------------------------------------------
272
+ // List hooks — the consumer-facing items hook and the internal nav accessor
273
+ // the slot primitives read.
274
+ // ---------------------------------------------------------------------------
275
+ export const useCommandListItems = () => {
276
+ const context = use(CommandListItemsContext);
277
+ if (!context) {
278
+ throw new Error("<Composer.CommandList> must be rendered inside <Composer.Command>.");
279
+ }
280
+ return context;
281
+ };
282
+ const useCommandListNav = (componentName) => {
283
+ const context = use(CommandListNavContext);
284
+ if (!context) {
285
+ throw new Error(`<Composer.${componentName}> must be rendered inside <Composer.Command>.`);
286
+ }
287
+ return context;
288
+ };
289
+ export const ComposerCommandList = ({ className, render, style, children: renderItem, ...elementProps }) => {
290
+ const { items, state } = useCommandListItems();
291
+ const store = useComposerContextStore();
292
+ return useRenderElement("div", { className, render, style }, {
293
+ props: [
294
+ {
295
+ // The combobox popup: the editor references this element via
296
+ // aria-controls and points aria-activedescendant at its rows.
297
+ role: "listbox",
298
+ id: store.listboxId,
299
+ "aria-label": "Suggestions",
300
+ "aria-busy": state === "loading" || undefined,
301
+ "data-composer-command-items": "",
302
+ children: items.map((item, index) => (_jsx(Fragment, { children: renderItem(item) }, item.value ?? `__cmd_${index}`))),
303
+ },
304
+ elementProps,
305
+ ],
306
+ });
307
+ };
308
+ export const ComposerCommandLoading = ({ children, className, render, style, ...elementProps }) => useRenderElement("div", { className, render, style }, {
309
+ props: [{ "data-composer-command-loading": "", children }, elementProps],
310
+ });
311
+ export const ComposerCommandEmpty = ({ children, className, render, style, ...elementProps }) => useRenderElement("div", { className, render, style }, {
312
+ props: [{ role: "status", "data-composer-command-empty": "", children }, elementProps],
313
+ });
314
+ export const ComposerCommandDismiss = ({ children, className, render, style, ...elementProps }) => {
315
+ const navContext = useCommandListNav("CommandDismiss");
316
+ return useRenderElement("button", { className, render, style }, {
317
+ props: [
318
+ {
319
+ tabIndex: -1,
320
+ "data-composer-command-dismiss": "",
321
+ onMouseDown: (event) => {
322
+ event.preventDefault();
323
+ navContext.dismiss();
324
+ },
325
+ children,
326
+ },
327
+ elementProps,
328
+ ],
329
+ });
330
+ };
331
+ export const ComposerCommandItem = ({ value, ...props }) => {
332
+ const navContext = useCommandListNav("CommandItem");
333
+ const itemsContext = use(CommandListItemsContext);
334
+ const store = useComposerContextStore();
335
+ const isHighlighted = navContext.highlightedValue === value;
336
+ // Disabled comes from the item data (CommandItemData.disabled) — the same
337
+ // source the nav wrap skips — so the row can never disagree with the
338
+ // keyboard behavior. Not a prop: two writers would let them drift.
339
+ const isDisabled = itemsContext?.items.some((item) => item.value === value && item.disabled) ?? false;
340
+ return (_jsx(Commands.Item, { ref: isHighlighted ? navContext.scrollHighlightedIntoView : undefined, id: optionDomId(store.listboxId, value), "aria-selected": isHighlighted, "data-composer-command-item": "", highlighted: isHighlighted, disabled: isDisabled, onMouseDown: (event) => {
341
+ // preventDefault even when disabled — the click must never steal
342
+ // focus from the editor.
343
+ event.preventDefault();
344
+ if (!isDisabled)
345
+ navContext.selectByValue(value);
346
+ }, onMouseEnter: isDisabled ? undefined : () => navContext.setHighlightedValue(value), ...props }));
347
+ };
348
+ export const ComposerCommandItemIcon = ({ className, render, style, ...elementProps }) => useRenderElement("span", { className, render, style }, { props: [{ "data-composer-command-item-icon": "" }, elementProps] });
349
+ export const ComposerCommandItemLabel = ({ className, render, style, ...elementProps }) => useRenderElement("span", { className, render, style }, { props: [{ "data-composer-command-item-label": "" }, elementProps] });
350
+ export const ComposerCommandItemDescription = ({ className, render, style, ...elementProps }) => useRenderElement("span", { className, render, style }, { props: [{ "data-composer-command-item-description": "" }, elementProps] });
351
+ // Grouping is a render concern: this reads the resolved items, buckets them by `groupBy`
352
+ // with `Map.groupBy` (which preserves first-appearance order — Object.groupBy would reorder
353
+ // integer-like keys), and renders the child once per group inside a `command-group`
354
+ // container. Nav order stays store-driven, so a group whose items all filter out just
355
+ // doesn't render. Annotate `groupBy`'s param to type the items (no inline JSX generic).
356
+ export const ComposerCommandGroup = ({ groupBy, children: renderGroup, }) => {
357
+ const { items } = useCommandListItems();
358
+ return (_jsx(_Fragment, { children: [...Map.groupBy(items, groupBy)].map(([group, groupItems]) => (_jsx(Commands.Group, { children: renderGroup(group, groupItems) }, group))) }));
359
+ };
360
+ export const ComposerCommandGroupLabel = ({ className, render, style, ...elementProps }) => useRenderElement("div", { className, render, style }, { props: [{ "data-composer-command-group-label": "" }, elementProps] });
@@ -0,0 +1,28 @@
1
+ import type { PrimitiveProps } from "../internal/primitive-props.js";
2
+ type CommandsRootProps = PrimitiveProps<"div">;
3
+ type CommandsGroupProps = PrimitiveProps<"div">;
4
+ type CommandsGroupLabelProps = PrimitiveProps<"div">;
5
+ export type CommandsItemState = {
6
+ /** Present as data-highlighted while keyboard/hover highlighted. */
7
+ highlighted: boolean;
8
+ /** Present as data-disabled while the row is inert. */
9
+ disabled: boolean;
10
+ };
11
+ export type CommandsItemProps = PrimitiveProps<"button", CommandsItemState> & {
12
+ highlighted?: boolean;
13
+ disabled?: boolean;
14
+ };
15
+ type CommandsItemLabelProps = PrimitiveProps<"span">;
16
+ type CommandsItemDescriptionProps = PrimitiveProps<"span">;
17
+ type CommandsEmptyProps = PrimitiveProps<"div">;
18
+ type CommandsFooterProps = PrimitiveProps<"div">;
19
+ export declare const Commands: (({ className, render, style, ...elementProps }: CommandsRootProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>) & {
20
+ Group: ({ className, render, style, ...elementProps }: CommandsGroupProps) => import("react").JSX.Element;
21
+ GroupLabel: ({ className, render, style, ...elementProps }: CommandsGroupLabelProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
22
+ Item: ({ highlighted, disabled, className, render, style, ...elementProps }: CommandsItemProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
23
+ ItemLabel: ({ className, render, style, ...elementProps }: CommandsItemLabelProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
24
+ ItemDescription: ({ className, render, style, ...elementProps }: CommandsItemDescriptionProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
25
+ Empty: ({ children, className, render, style, ...elementProps }: CommandsEmptyProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
26
+ Footer: ({ className, render, style, ...elementProps }: CommandsFooterProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
27
+ };
28
+ export {};