@ai-matrx/rich-content 0.2.10 → 0.2.16

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 (114) hide show
  1. package/dist/copy/copy-commands.d.ts +42 -0
  2. package/dist/copy/copy-commands.js +119 -0
  3. package/dist/copy/markdown-copy-html.d.ts +3 -0
  4. package/dist/copy/markdown-copy-html.js +8 -0
  5. package/dist/copy/markdown-readable-text.d.ts +4 -0
  6. package/dist/copy/markdown-readable-text.js +62 -0
  7. package/dist/display/chat-markdown/block-registry/BlockComponentRegistry.js +2 -0
  8. package/dist/display/chat-markdown/block-registry/block-dispatch.js +2 -0
  9. package/dist/host/app-bindings.d.ts +30 -0
  10. package/dist/host/app-bindings.js +38 -0
  11. package/dist/host/feedback-subject.d.ts +16 -0
  12. package/dist/host/feedback-subject.js +0 -0
  13. package/dist/host/manual-copy.d.ts +7 -0
  14. package/dist/host/manual-copy.js +11 -0
  15. package/dist/host/registry.d.ts +2 -0
  16. package/dist/host/registry.js +10 -0
  17. package/dist/host/session-knob.d.ts +5 -0
  18. package/dist/host/session-knob.js +7 -0
  19. package/dist/host/types.d.ts +24 -0
  20. package/dist/rich-document/RegistryContextMenu.d.ts +29 -0
  21. package/dist/rich-document/RegistryContextMenu.js +40 -0
  22. package/dist/rich-document/RichDocument.d.ts +78 -0
  23. package/dist/rich-document/RichDocument.js +218 -0
  24. package/dist/rich-document/RichDocumentActionProvider.d.ts +18 -0
  25. package/dist/rich-document/RichDocumentActionProvider.js +18 -0
  26. package/dist/rich-document/RichDocumentActionSurface.d.ts +11 -0
  27. package/dist/rich-document/RichDocumentActionSurface.js +59 -0
  28. package/dist/rich-document/RichDocumentActions.d.ts +12 -0
  29. package/dist/rich-document/RichDocumentActions.js +29 -0
  30. package/dist/rich-document/actions/hostActions.d.ts +5 -0
  31. package/dist/rich-document/actions/hostActions.js +31 -0
  32. package/dist/rich-document/actions/labels.d.ts +9 -0
  33. package/dist/rich-document/actions/labels.js +8 -0
  34. package/dist/rich-document/actions/markdownTable.d.ts +9 -0
  35. package/dist/rich-document/actions/markdownTable.js +39 -0
  36. package/dist/rich-document/actions/provider.d.ts +58 -0
  37. package/dist/rich-document/actions/provider.js +245 -0
  38. package/dist/rich-document/actions/resumePendingAuthAction.d.ts +3 -0
  39. package/dist/rich-document/actions/resumePendingAuthAction.js +31 -0
  40. package/dist/rich-document/actions/sources/index.d.ts +9 -0
  41. package/dist/rich-document/actions/sources/index.js +22 -0
  42. package/dist/rich-document/actions/sources/raw.d.ts +4 -0
  43. package/dist/rich-document/actions/sources/raw.js +35 -0
  44. package/dist/rich-document/actions/useRichDocumentProvider.d.ts +1 -0
  45. package/dist/rich-document/actions/useRichDocumentProvider.js +13 -0
  46. package/dist/rich-document/host-types.d.ts +51 -0
  47. package/dist/rich-document/host-types.js +0 -0
  48. package/dist/rich-document/registrations.d.ts +1 -0
  49. package/dist/rich-document/registrations.js +13 -0
  50. package/dist/rich-document/review/applyTargets.d.ts +16 -0
  51. package/dist/rich-document/review/applyTargets.js +32 -0
  52. package/dist/rich-document/review/proposedEdit.d.ts +22 -0
  53. package/dist/rich-document/review/proposedEdit.js +48 -0
  54. package/dist/rich-document/review/protectedSpans.d.ts +14 -0
  55. package/dist/rich-document/review/protectedSpans.js +61 -0
  56. package/dist/rich-document/runtime/actionSurfacesStore.d.ts +30 -0
  57. package/dist/rich-document/runtime/actionSurfacesStore.js +79 -0
  58. package/dist/rich-document/runtime/contextMenuFor.d.ts +6 -0
  59. package/dist/rich-document/runtime/contextMenuFor.js +7 -0
  60. package/dist/rich-document/runtime/providerBridge.d.ts +20 -0
  61. package/dist/rich-document/runtime/providerBridge.js +23 -0
  62. package/dist/rich-document/runtime/useActionSurfaceProvider.d.ts +33 -0
  63. package/dist/rich-document/runtime/useActionSurfaceProvider.js +214 -0
  64. package/dist/rich-document/search/ContentFindControl.d.ts +8 -0
  65. package/dist/rich-document/search/ContentFindControl.js +46 -0
  66. package/dist/rich-document/search/RenderedFindBar.d.ts +8 -0
  67. package/dist/rich-document/search/RenderedFindBar.js +110 -0
  68. package/dist/rich-document/search/renderedFind.d.ts +1 -0
  69. package/dist/rich-document/search/renderedFind.js +1 -0
  70. package/dist/rich-document/types.d.ts +384 -0
  71. package/dist/rich-document/types.js +0 -0
  72. package/dist/rich-document/variants/ActionBar.d.ts +17 -0
  73. package/dist/rich-document/variants/ActionBar.js +48 -0
  74. package/dist/rich-document/variants/MenuVariant.d.ts +15 -0
  75. package/dist/rich-document/variants/MenuVariant.js +17 -0
  76. package/dist/rich-document/variants/MiniActionBar.d.ts +5 -0
  77. package/dist/rich-document/variants/MiniActionBar.js +11 -0
  78. package/dist/rich-document/variants/RegistryActionList.d.ts +17 -0
  79. package/dist/rich-document/variants/RegistryActionList.js +94 -0
  80. package/dist/rich-document/variants/RegistryActionMenu.d.ts +24 -0
  81. package/dist/rich-document/variants/RegistryActionMenu.js +74 -0
  82. package/dist/rich-document/variants/shared/AlchemyDocumentMenu.d.ts +7 -0
  83. package/dist/rich-document/variants/shared/AlchemyDocumentMenu.js +83 -0
  84. package/dist/rich-document/variants/shared/OpenOneMenuButton.d.ts +9 -0
  85. package/dist/rich-document/variants/shared/OpenOneMenuButton.js +70 -0
  86. package/dist/rich-document/variants/shared/StandaloneOneMenu.d.ts +8 -0
  87. package/dist/rich-document/variants/shared/StandaloneOneMenu.js +32 -0
  88. package/dist/rich-document/variants/shared/categories.d.ts +4 -0
  89. package/dist/rich-document/variants/shared/categories.js +31 -0
  90. package/dist/rich-document/variants/shared/iconMap.d.ts +2 -0
  91. package/dist/rich-document/variants/shared/iconMap.js +73 -0
  92. package/dist/rich-document/variants/shared/menuStructure.d.ts +81 -0
  93. package/dist/rich-document/variants/shared/menuStructure.js +271 -0
  94. package/dist/rich-document/variants/shared/runAction.d.ts +17 -0
  95. package/dist/rich-document/variants/shared/runAction.js +24 -0
  96. package/dist/selection-toolbar/SelectionToolbarFrame.d.ts +64 -0
  97. package/dist/selection-toolbar/SelectionToolbarFrame.js +191 -0
  98. package/dist/selection-toolbar/common-actions.d.ts +2 -0
  99. package/dist/selection-toolbar/common-actions.js +103 -0
  100. package/dist/selection-toolbar/selection-actions.d.ts +126 -0
  101. package/dist/selection-toolbar/selection-actions.js +240 -0
  102. package/dist/selection-toolbar/selection-copy.d.ts +19 -0
  103. package/dist/selection-toolbar/selection-copy.js +98 -0
  104. package/dist/selection-toolbar/selection-shape.d.ts +1 -0
  105. package/dist/selection-toolbar/selection-shape.js +15 -0
  106. package/dist/selection-toolbar/selection-zones.d.ts +90 -0
  107. package/dist/selection-toolbar/selection-zones.js +85 -0
  108. package/dist/selection-toolbar/textarea-caret.d.ts +8 -0
  109. package/dist/selection-toolbar/textarea-caret.js +69 -0
  110. package/dist/utils/alchemy-icon-keys.d.ts +18 -0
  111. package/dist/utils/alchemy-icon-keys.js +29 -0
  112. package/dist/utils/rendered-find.d.ts +9 -0
  113. package/dist/utils/rendered-find.js +68 -0
  114. package/package.json +23 -7
@@ -0,0 +1,79 @@
1
+ import { useSyncExternalStore } from "react";
2
+ const KEY = /* @__PURE__ */ Symbol.for("ai-matrx.rich-content.rich-document.action-surfaces");
3
+ function store() {
4
+ const g = globalThis;
5
+ return g[KEY] ??= { state: { bySurfaceId: {} }, listeners: /* @__PURE__ */ new Set() };
6
+ }
7
+ function commit(next) {
8
+ const s = store();
9
+ s.state = { bySurfaceId: next };
10
+ for (const listener of s.listeners) listener();
11
+ }
12
+ function registerProvider(payload) {
13
+ const { surfaceId, registration } = payload;
14
+ const stack = [...store().state.bySurfaceId[surfaceId] ?? []];
15
+ const idx = stack.findIndex((entry) => entry.providerId === registration.providerId);
16
+ if (idx >= 0) stack[idx] = registration;
17
+ else stack.push(registration);
18
+ commit({ ...store().state.bySurfaceId, [surfaceId]: stack });
19
+ }
20
+ function updateProviderSpecs(payload) {
21
+ const { surfaceId, providerId, computedActionSpecs, contentSourceId } = payload;
22
+ const stack = store().state.bySurfaceId[surfaceId];
23
+ if (!stack) return;
24
+ const idx = stack.findIndex((entry) => entry.providerId === providerId);
25
+ if (idx < 0) return;
26
+ const next = [...stack];
27
+ next[idx] = { ...stack[idx], computedActionSpecs, contentSourceId };
28
+ commit({ ...store().state.bySurfaceId, [surfaceId]: next });
29
+ }
30
+ function unregisterProvider(payload) {
31
+ const { surfaceId, providerId } = payload;
32
+ const stack = store().state.bySurfaceId[surfaceId];
33
+ if (!stack) return;
34
+ const next = stack.filter((entry) => entry.providerId !== providerId);
35
+ const all = { ...store().state.bySurfaceId };
36
+ if (next.length === 0) delete all[surfaceId];
37
+ else all[surfaceId] = next;
38
+ commit(all);
39
+ }
40
+ function clearAll() {
41
+ commit({});
42
+ }
43
+ function getActionSurfacesState() {
44
+ return store().state;
45
+ }
46
+ function subscribe(listener) {
47
+ store().listeners.add(listener);
48
+ return () => store().listeners.delete(listener);
49
+ }
50
+ const EMPTY_PROVIDER_STACK = [];
51
+ const EMPTY_ACTION_SPECS = [];
52
+ function selectTopProvider(state, surfaceId) {
53
+ const stack = state.bySurfaceId[surfaceId];
54
+ return stack && stack.length > 0 ? stack[stack.length - 1] : null;
55
+ }
56
+ function selectProviderStack(state, surfaceId) {
57
+ return state.bySurfaceId[surfaceId] ?? EMPTY_PROVIDER_STACK;
58
+ }
59
+ function selectSurfaceActionSpecs(state, surfaceId) {
60
+ return selectTopProvider(state, surfaceId)?.computedActionSpecs ?? EMPTY_ACTION_SPECS;
61
+ }
62
+ function useTopProvider(surfaceId) {
63
+ return useSyncExternalStore(
64
+ subscribe,
65
+ () => selectTopProvider(store().state, surfaceId),
66
+ () => null
67
+ );
68
+ }
69
+ export {
70
+ clearAll,
71
+ getActionSurfacesState,
72
+ registerProvider,
73
+ selectProviderStack,
74
+ selectSurfaceActionSpecs,
75
+ selectTopProvider,
76
+ unregisterProvider,
77
+ updateProviderSpecs,
78
+ useTopProvider
79
+ };
@@ -0,0 +1,6 @@
1
+ import type { RichDocumentAction, RichDocumentActionId, RichDocumentActionsVariant } from "../types.js";
2
+ export type ContextMenuOption = boolean | {
3
+ extra?: RichDocumentAction[];
4
+ exclude?: (RichDocumentActionId | string)[];
5
+ };
6
+ export declare function contextMenuFor(prop: ContextMenuOption | undefined, variant: RichDocumentActionsVariant): ContextMenuOption | false;
@@ -0,0 +1,7 @@
1
+ function contextMenuFor(prop, variant) {
2
+ if (prop !== void 0) return prop;
3
+ return variant === "bar" || variant === "mini-bar" || variant === "menu" || variant === "icon-only";
4
+ }
5
+ export {
6
+ contextMenuFor
7
+ };
@@ -0,0 +1,20 @@
1
+ import type { ClickTarget } from "@ai-matrx/alchemy/actions";
2
+ import type { RichDocumentAction, RichDocumentActionContext } from "../types.js";
3
+ export interface ProviderBridge {
4
+ /** Builds a fresh action context from the host's live state/refs. */
5
+ getCtx: () => RichDocumentActionContext;
6
+ /** The actions visible for this provider (already filtered + sorted). */
7
+ resolvedActions: RichDocumentAction[];
8
+ /** The Alchemy click target the package layouts resolve against (ALC-15). */
9
+ target: ClickTarget;
10
+ }
11
+ export declare function registerBridge(providerId: string, bridge: ProviderBridge): void;
12
+ /**
13
+ * Refresh an existing bridge (no semantic difference from register today,
14
+ * but kept separate so call sites are self-documenting).
15
+ */
16
+ export declare function updateBridge(providerId: string, bridge: ProviderBridge): void;
17
+ export declare function unregisterBridge(providerId: string): void;
18
+ export declare function getBridge(providerId: string): ProviderBridge | undefined;
19
+ /** Diagnostics only. */
20
+ export declare function _getAllBridges(): ReadonlyMap<string, ProviderBridge>;
@@ -0,0 +1,23 @@
1
+ const BRIDGES = /* @__PURE__ */ new Map();
2
+ function registerBridge(providerId, bridge) {
3
+ BRIDGES.set(providerId, bridge);
4
+ }
5
+ function updateBridge(providerId, bridge) {
6
+ BRIDGES.set(providerId, bridge);
7
+ }
8
+ function unregisterBridge(providerId) {
9
+ BRIDGES.delete(providerId);
10
+ }
11
+ function getBridge(providerId) {
12
+ return BRIDGES.get(providerId);
13
+ }
14
+ function _getAllBridges() {
15
+ return BRIDGES;
16
+ }
17
+ export {
18
+ _getAllBridges,
19
+ getBridge,
20
+ registerBridge,
21
+ unregisterBridge,
22
+ updateBridge
23
+ };
@@ -0,0 +1,33 @@
1
+ import type { ClickTarget } from "@ai-matrx/alchemy/actions";
2
+ import type { ContentSource, RichDocumentAction, RichDocumentActionContext, RichDocumentActionsProp, RichDocumentActionsVariant } from "../types.js";
3
+ export interface UseActionSurfaceProviderArgs {
4
+ content: string | undefined;
5
+ source: ContentSource;
6
+ actions?: RichDocumentActionsProp;
7
+ /** Only "remote" registers to the Redux surface; all variants register the bridge. */
8
+ actionsVariant: RichDocumentActionsVariant;
9
+ /** Required when actionsVariant === "remote". */
10
+ actionsSurfaceId?: string;
11
+ }
12
+ export interface UseActionSurfaceProviderResult {
13
+ /** Render-time context (from props). Safe to read during render — used to
14
+ * resolve auxiliary action sets (e.g. context-menu actions). */
15
+ ctx: RichDocumentActionContext;
16
+ /** Ref-based context factory. Read inside event handlers only. */
17
+ getCtx: () => RichDocumentActionContext;
18
+ /** The actions visible for this source + this consumer's exclude/extra. */
19
+ resolvedActions: RichDocumentAction[];
20
+ /**
21
+ * The Alchemy click target for this content (ALC-15): what the package's
22
+ * layouts (bar, ⋯, sheet) resolve the one registry against. Stable until
23
+ * something that changes eligibility changes (content, source, auth, org,
24
+ * exclude/extra).
25
+ */
26
+ target: ClickTarget;
27
+ }
28
+ /**
29
+ * Register a RichDocument action provider (specs + bridge) for a surface and
30
+ * expose the live context + resolved action list. See the file header for the
31
+ * no-functions-in-Redux invariant.
32
+ */
33
+ export declare function useActionSurfaceProvider(args: UseActionSurfaceProviderArgs): UseActionSurfaceProviderResult;
@@ -0,0 +1,214 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import {
4
+ useHostDispatch,
5
+ useHostGetState,
6
+ useIsAuthenticated,
7
+ useIsSuperAdmin,
8
+ useActiveOrganizationId
9
+ } from "../../host/app-bindings.js";
10
+ import {
11
+ registerProvider,
12
+ unregisterProvider,
13
+ updateProviderSpecs
14
+ } from "./actionSurfacesStore.js";
15
+ import { getSourceAdapter } from "../actions/sources/index.js";
16
+ import { shortHash } from "../actions/sources/raw.js";
17
+ import { resolveActions, richDocumentClickTarget } from "../actions/provider.js";
18
+ import { resolveActionLabel } from "../actions/labels.js";
19
+ import {
20
+ registerBridge,
21
+ unregisterBridge,
22
+ updateBridge
23
+ } from "./providerBridge.js";
24
+ function actionsToSpecs(actions, ctx) {
25
+ return actions.map((action) => {
26
+ const disabledResult = action.disabled?.(ctx);
27
+ const isDisabled = disabledResult === true || typeof disabledResult === "object" && disabledResult !== null;
28
+ const disabledReason = typeof disabledResult === "object" && disabledResult !== null ? disabledResult.reason : void 0;
29
+ return {
30
+ id: action.id,
31
+ label: resolveActionLabel(action.label, ctx),
32
+ iconName: action.icon.displayName ?? action.icon.name ?? "Circle",
33
+ category: action.category,
34
+ renderSlot: action.renderSlot ?? "overflow",
35
+ order: action.order ?? 0,
36
+ disabled: isDisabled,
37
+ disabledReason
38
+ };
39
+ });
40
+ }
41
+ function specsKey(specs, contentSourceId) {
42
+ return `${contentSourceId}::${specs.map(
43
+ (s) => `${s.id}|${s.label}|${s.disabled ? 1 : 0}|${s.disabledReason ?? ""}`
44
+ ).join(";")}`;
45
+ }
46
+ function buildContext(args) {
47
+ const adapter = getSourceAdapter(args.source.type);
48
+ const prefix = adapter.instanceKeyPrefix(args.source);
49
+ const instancePrefix = args.source.type === "raw" ? `${prefix}-${shortHash(args.content).slice(0, 8)}` : prefix;
50
+ return {
51
+ content: args.content,
52
+ source: args.source,
53
+ metadata: args.host?.metadata ?? null,
54
+ dispatch: args.dispatch,
55
+ getState: args.getState,
56
+ organizationId: args.organizationId,
57
+ isAuthenticated: args.isAuthenticated,
58
+ isAdmin: args.isAdmin,
59
+ isCreator: args.host?.isCreator ?? false,
60
+ surfaceKey: args.host?.surfaceKey ?? null,
61
+ onClose: () => {
62
+ },
63
+ instanceKey: (suffix) => `${instancePrefix}-${suffix}`,
64
+ sourceAdapter: adapter,
65
+ callbacks: args.callbacks,
66
+ extensions: args.extensions
67
+ };
68
+ }
69
+ function useActionSurfaceProvider(args) {
70
+ const {
71
+ content,
72
+ source,
73
+ actions: actionsProp,
74
+ actionsVariant,
75
+ actionsSurfaceId
76
+ } = args;
77
+ const dispatch = useHostDispatch();
78
+ const getState = useHostGetState();
79
+ const isAuthenticated = Boolean(useIsAuthenticated());
80
+ const isAdmin = Boolean(useIsSuperAdmin());
81
+ const organizationId = useActiveOrganizationId();
82
+ const providerId = React.useId();
83
+ const contentRef = React.useRef(content ?? "");
84
+ const sourceRef = React.useRef(source);
85
+ const callbacksRef = React.useRef(actionsProp?.callbacks);
86
+ const extensionsRef = React.useRef(
87
+ actionsProp?.extensions
88
+ );
89
+ const hostRef = React.useRef(actionsProp);
90
+ const dispatchRef = React.useRef(dispatch);
91
+ const isAuthRef = React.useRef(isAuthenticated);
92
+ const isAdminRef = React.useRef(isAdmin);
93
+ React.useLayoutEffect(() => {
94
+ contentRef.current = content ?? "";
95
+ sourceRef.current = source;
96
+ callbacksRef.current = actionsProp?.callbacks;
97
+ extensionsRef.current = actionsProp?.extensions;
98
+ hostRef.current = actionsProp;
99
+ dispatchRef.current = dispatch;
100
+ isAuthRef.current = isAuthenticated;
101
+ isAdminRef.current = isAdmin;
102
+ });
103
+ const getCtx = () => buildContext({
104
+ content: contentRef.current,
105
+ source: sourceRef.current,
106
+ callbacks: callbacksRef.current,
107
+ extensions: extensionsRef.current,
108
+ host: hostRef.current,
109
+ dispatch: dispatchRef.current,
110
+ getState,
111
+ isAuthenticated: isAuthRef.current,
112
+ isAdmin: isAdminRef.current,
113
+ organizationId
114
+ });
115
+ const ctx = buildContext({
116
+ content: content ?? "",
117
+ source,
118
+ callbacks: actionsProp?.callbacks,
119
+ extensions: actionsProp?.extensions,
120
+ host: actionsProp,
121
+ dispatch,
122
+ getState,
123
+ isAuthenticated,
124
+ isAdmin,
125
+ organizationId
126
+ });
127
+ const resolvedActions = resolveActions(ctx, {
128
+ exclude: actionsProp?.exclude,
129
+ extra: actionsProp?.extra
130
+ });
131
+ const specs = actionsToSpecs(resolvedActions, ctx);
132
+ const targetKey = [
133
+ ctx.instanceKey("target"),
134
+ source.readOnly ? 1 : 0,
135
+ isAuthenticated ? 1 : 0,
136
+ isAdmin ? 1 : 0,
137
+ organizationId ?? "",
138
+ actionsProp?.isCreator ? 1 : 0,
139
+ (actionsProp?.exclude ?? []).join(","),
140
+ (actionsProp?.extra ?? []).map((a) => a.id).join(","),
141
+ shortHash(content ?? ""),
142
+ resolvedActions.map((a) => a.id).join(",")
143
+ ].join("|");
144
+ const [targetState, setTargetState] = React.useState(() => ({
145
+ key: targetKey,
146
+ target: richDocumentClickTarget(ctx, {
147
+ getCtx,
148
+ exclude: actionsProp?.exclude ?? [],
149
+ extra: actionsProp?.extra ?? []
150
+ })
151
+ }));
152
+ if (targetState.key !== targetKey) {
153
+ setTargetState({
154
+ key: targetKey,
155
+ target: richDocumentClickTarget(ctx, {
156
+ getCtx,
157
+ exclude: actionsProp?.exclude ?? [],
158
+ extra: actionsProp?.extra ?? []
159
+ })
160
+ });
161
+ }
162
+ const target = targetState.target;
163
+ const contentSourceId = ctx.instanceKey("alchemy");
164
+ const lastSpecsKey = React.useRef("");
165
+ React.useEffect(() => {
166
+ if (actionsVariant !== "remote" || !actionsSurfaceId) return void 0;
167
+ registerProvider({
168
+ surfaceId: actionsSurfaceId,
169
+ registration: {
170
+ providerId,
171
+ computedActionSpecs: specs,
172
+ contentSourceId,
173
+ sourceType: source.type
174
+ }
175
+ });
176
+ lastSpecsKey.current = specsKey(specs, contentSourceId);
177
+ return () => {
178
+ unregisterProvider({
179
+ surfaceId: actionsSurfaceId,
180
+ providerId
181
+ });
182
+ };
183
+ }, [actionsVariant, actionsSurfaceId, providerId, dispatch, source.type]);
184
+ React.useEffect(() => {
185
+ if (actionsVariant !== "remote" || !actionsSurfaceId) return;
186
+ const key = specsKey(specs, contentSourceId);
187
+ if (key === lastSpecsKey.current) return;
188
+ lastSpecsKey.current = key;
189
+ updateProviderSpecs({
190
+ surfaceId: actionsSurfaceId,
191
+ providerId,
192
+ computedActionSpecs: specs,
193
+ contentSourceId
194
+ });
195
+ }, [
196
+ actionsVariant,
197
+ actionsSurfaceId,
198
+ providerId,
199
+ dispatch,
200
+ specs,
201
+ contentSourceId
202
+ ]);
203
+ React.useEffect(() => {
204
+ registerBridge(providerId, { getCtx, resolvedActions, target });
205
+ return () => unregisterBridge(providerId);
206
+ }, [providerId]);
207
+ React.useEffect(() => {
208
+ updateBridge(providerId, { getCtx, resolvedActions, target });
209
+ });
210
+ return { ctx, getCtx, resolvedActions, target };
211
+ }
212
+ export {
213
+ useActionSurfaceProvider
214
+ };
@@ -0,0 +1,8 @@
1
+ import { type RefObject } from "react";
2
+ /** Places the shared rendered-content finder in a reader's existing header. */
3
+ export declare function ContentFindControl({ rootRef, label, inline, onOpenChange, }: {
4
+ rootRef: RefObject<HTMLElement | null>;
5
+ label?: string;
6
+ inline?: boolean;
7
+ onOpenChange?: (open: boolean) => void;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,46 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
4
+ import { Search } from "lucide-react";
5
+ import { Button } from "@ai-matrx/design-system/controls";
6
+ import { RenderedFindBar } from "./RenderedFindBar.js";
7
+ import { surfaceOwnsKey } from "@ai-matrx/kit/keyboard-scope";
8
+ function ContentFindControl({
9
+ rootRef,
10
+ label = "Find in this content",
11
+ inline = false,
12
+ onOpenChange
13
+ }) {
14
+ const [open, setOpen] = useState(false);
15
+ const [focusRequest, setFocusRequest] = useState(0);
16
+ const openFind = () => {
17
+ setOpen(true);
18
+ onOpenChange?.(true);
19
+ setFocusRequest((value) => value + 1);
20
+ };
21
+ const closeFind = () => {
22
+ setOpen(false);
23
+ onOpenChange?.(false);
24
+ };
25
+ useEffect(() => {
26
+ const onKeyDown = (event) => {
27
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "f" && !event.shiftKey && !event.altKey) {
28
+ if (!surfaceOwnsKey(event, rootRef.current)) return;
29
+ event.preventDefault();
30
+ setOpen(true);
31
+ onOpenChange?.(true);
32
+ setFocusRequest((value) => value + 1);
33
+ }
34
+ };
35
+ window.addEventListener("keydown", onKeyDown, true);
36
+ return () => window.removeEventListener("keydown", onKeyDown, true);
37
+ }, [onOpenChange, rootRef]);
38
+ if (inline && open) return /* @__PURE__ */ jsx("div", { className: "min-w-0 w-full sm:w-[360px]", children: /* @__PURE__ */ jsx(RenderedFindBar, { rootRef, label, focusRequest, onClose: closeFind, compact: true }) });
39
+ return /* @__PURE__ */ jsxs("div", { className: "relative shrink-0", children: [
40
+ /* @__PURE__ */ jsx(Button, { icon: /* @__PURE__ */ jsx(Search, { "aria-hidden": true }), type: "button", variant: "quiet", "aria-label": label, title: label, onClick: openFind }),
41
+ open && /* @__PURE__ */ jsx("div", { className: "absolute right-0 top-full z-30 mt-1 w-[min(88vw,520px)]", children: /* @__PURE__ */ jsx(RenderedFindBar, { rootRef, label, focusRequest, onClose: closeFind }) })
42
+ ] });
43
+ }
44
+ export {
45
+ ContentFindControl
46
+ };
@@ -0,0 +1,8 @@
1
+ /** A read-only find bar for any rendered RichDocument. Never mutates content or selection. */
2
+ export declare function RenderedFindBar({ rootRef, onClose, label, focusRequest, compact }: {
3
+ rootRef: React.RefObject<HTMLElement | null>;
4
+ onClose: () => void;
5
+ label?: string;
6
+ focusRequest?: number;
7
+ compact?: boolean;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,110 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef, useState } from "react";
4
+ import { CaseSensitive, ChevronDown, ChevronUp, Regex, Search, WholeWord, X } from "lucide-react";
5
+ import { collectRenderedFindRanges } from "./renderedFind.js";
6
+ function highlightApi() {
7
+ const css = window.CSS;
8
+ const Highlight = window.Highlight;
9
+ return css?.highlights && Highlight ? { registry: css.highlights, Highlight } : null;
10
+ }
11
+ function clearHighlights(all, active) {
12
+ const api = highlightApi();
13
+ api?.registry.delete(all);
14
+ api?.registry.delete(active);
15
+ }
16
+ function RenderedFindBar({ rootRef, onClose, label = "Find in document", focusRequest = 0, compact = false }) {
17
+ const inputRef = useRef(null);
18
+ const [query, setQuery] = useState("");
19
+ const [options, setOptions] = useState({});
20
+ const [ranges, setRanges] = useState([]);
21
+ const [current, setCurrent] = useState(0);
22
+ const [version, setVersion] = useState(0);
23
+ const instance = useId().replace(/[^a-zA-Z0-9_-]/g, "_");
24
+ const allHighlight = `matrx-rendered-find-${instance}`;
25
+ const activeHighlight = `${allHighlight}-active`;
26
+ const scope = `${allHighlight}-scope`;
27
+ useEffect(() => {
28
+ const root = rootRef.current;
29
+ if (!root) return;
30
+ root.classList.add(scope);
31
+ return () => root.classList.remove(scope);
32
+ }, [rootRef, scope]);
33
+ useEffect(() => {
34
+ inputRef.current?.focus();
35
+ inputRef.current?.select();
36
+ }, [focusRequest]);
37
+ useEffect(() => () => clearHighlights(allHighlight, activeHighlight), [allHighlight, activeHighlight]);
38
+ useEffect(() => {
39
+ const root = rootRef.current;
40
+ if (!root || !query.trim()) return;
41
+ let timer;
42
+ const observer = new MutationObserver(() => {
43
+ if (timer) clearTimeout(timer);
44
+ timer = setTimeout(() => setVersion((value) => value + 1), 150);
45
+ });
46
+ observer.observe(root, { childList: true, subtree: true, characterData: true });
47
+ return () => {
48
+ observer.disconnect();
49
+ if (timer) clearTimeout(timer);
50
+ };
51
+ }, [rootRef, query]);
52
+ useEffect(() => {
53
+ const root = rootRef.current;
54
+ const found = root ? collectRenderedFindRanges(root, query, options) : [];
55
+ setRanges(found);
56
+ setCurrent(0);
57
+ }, [rootRef, query, options, version]);
58
+ useEffect(() => {
59
+ const api = highlightApi();
60
+ if (api) {
61
+ if (ranges.length) api.registry.set(allHighlight, new api.Highlight(...ranges));
62
+ else api.registry.delete(allHighlight);
63
+ const active = ranges[current];
64
+ if (active) api.registry.set(activeHighlight, new api.Highlight(active));
65
+ else api.registry.delete(activeHighlight);
66
+ }
67
+ ranges[current]?.startContainer.parentElement?.scrollIntoView({ block: "center", behavior: "smooth" });
68
+ }, [ranges, current, allHighlight, activeHighlight]);
69
+ const step = (direction) => {
70
+ if (ranges.length) setCurrent((value) => (value + direction + ranges.length) % ranges.length);
71
+ };
72
+ const toggle = (key) => setOptions((value) => ({ ...value, [key]: !value[key] }));
73
+ const validRegex = !options.regex || (() => {
74
+ try {
75
+ new RegExp(query);
76
+ return true;
77
+ } catch {
78
+ return false;
79
+ }
80
+ })();
81
+ return /* @__PURE__ */ jsxs("div", { role: "search", "aria-label": label, "data-find-ignore": "", className: `flex min-w-0 items-center gap-1 rounded-lg border border-border bg-card px-2 py-1 shadow-sm ${compact ? "flex-nowrap" : "flex-wrap"}`, children: [
82
+ /* @__PURE__ */ jsx("style", { children: `.${scope}::highlight(${allHighlight}), .${scope} ::highlight(${allHighlight}) { background-color: rgb(250 204 21 / .5); color: inherit; } .${scope}::highlight(${activeHighlight}), .${scope} ::highlight(${activeHighlight}) { background-color: rgb(249 115 22 / .85); color: white; }` }),
83
+ /* @__PURE__ */ jsx(Search, { className: "h-4 w-4 shrink-0 text-muted-foreground", "aria-hidden": true }),
84
+ /* @__PURE__ */ jsx("input", { ref: inputRef, type: "search", value: query, onChange: (event) => setQuery(event.target.value), onKeyDown: (event) => {
85
+ if (event.key === "Enter") {
86
+ event.preventDefault();
87
+ step(event.shiftKey ? -1 : 1);
88
+ }
89
+ if (event.key === "Escape") {
90
+ event.preventDefault();
91
+ onClose();
92
+ }
93
+ }, placeholder: label, "aria-label": label, className: "min-w-0 flex-1 bg-transparent text-base outline-none md:text-sm" }),
94
+ /* @__PURE__ */ jsx("span", { className: "min-w-12 text-right text-xs tabular-nums text-muted-foreground", "aria-live": "polite", children: !validRegex ? "Invalid pattern" : !query.trim() ? "" : ranges.length ? `${current + 1} of ${ranges.length}` : "No matches" }),
95
+ !compact && /* @__PURE__ */ jsxs(Fragment, { children: [
96
+ /* @__PURE__ */ jsx(OptionButton, { label: "Match case", pressed: !!options.caseSensitive, onClick: () => toggle("caseSensitive"), children: /* @__PURE__ */ jsx(CaseSensitive, { className: "h-4 w-4" }) }),
97
+ /* @__PURE__ */ jsx(OptionButton, { label: "Whole word", pressed: !!options.wholeWord, onClick: () => toggle("wholeWord"), children: /* @__PURE__ */ jsx(WholeWord, { className: "h-4 w-4" }) }),
98
+ /* @__PURE__ */ jsx(OptionButton, { label: "Regular expression", pressed: !!options.regex, onClick: () => toggle("regex"), children: /* @__PURE__ */ jsx(Regex, { className: "h-4 w-4" }) })
99
+ ] }),
100
+ /* @__PURE__ */ jsx(OptionButton, { label: "Previous match", disabled: !ranges.length, onClick: () => step(-1), children: /* @__PURE__ */ jsx(ChevronUp, { className: "h-4 w-4" }) }),
101
+ /* @__PURE__ */ jsx(OptionButton, { label: "Next match", disabled: !ranges.length, onClick: () => step(1), children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4" }) }),
102
+ /* @__PURE__ */ jsx(OptionButton, { label: "Close search", onClick: onClose, children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }) })
103
+ ] });
104
+ }
105
+ function OptionButton({ label, pressed, disabled, onClick, children }) {
106
+ return /* @__PURE__ */ jsx("button", { type: "button", "aria-label": label, title: label, "aria-pressed": pressed, disabled, onClick, className: "rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground aria-pressed:bg-primary/10 aria-pressed:text-primary-ink disabled:opacity-40", children });
107
+ }
108
+ export {
109
+ RenderedFindBar
110
+ };
@@ -0,0 +1 @@
1
+ export * from "../../utils/rendered-find.js";
@@ -0,0 +1 @@
1
+ export * from "../../utils/rendered-find.js";