@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.
- package/dist/copy/copy-commands.d.ts +42 -0
- package/dist/copy/copy-commands.js +119 -0
- package/dist/copy/markdown-copy-html.d.ts +3 -0
- package/dist/copy/markdown-copy-html.js +8 -0
- package/dist/copy/markdown-readable-text.d.ts +4 -0
- package/dist/copy/markdown-readable-text.js +62 -0
- package/dist/display/chat-markdown/block-registry/BlockComponentRegistry.js +2 -0
- package/dist/display/chat-markdown/block-registry/block-dispatch.js +2 -0
- package/dist/host/app-bindings.d.ts +30 -0
- package/dist/host/app-bindings.js +38 -0
- package/dist/host/feedback-subject.d.ts +16 -0
- package/dist/host/feedback-subject.js +0 -0
- package/dist/host/manual-copy.d.ts +7 -0
- package/dist/host/manual-copy.js +11 -0
- package/dist/host/registry.d.ts +2 -0
- package/dist/host/registry.js +10 -0
- package/dist/host/session-knob.d.ts +5 -0
- package/dist/host/session-knob.js +7 -0
- package/dist/host/types.d.ts +24 -0
- package/dist/rich-document/RegistryContextMenu.d.ts +29 -0
- package/dist/rich-document/RegistryContextMenu.js +40 -0
- package/dist/rich-document/RichDocument.d.ts +78 -0
- package/dist/rich-document/RichDocument.js +218 -0
- package/dist/rich-document/RichDocumentActionProvider.d.ts +18 -0
- package/dist/rich-document/RichDocumentActionProvider.js +18 -0
- package/dist/rich-document/RichDocumentActionSurface.d.ts +11 -0
- package/dist/rich-document/RichDocumentActionSurface.js +59 -0
- package/dist/rich-document/RichDocumentActions.d.ts +12 -0
- package/dist/rich-document/RichDocumentActions.js +29 -0
- package/dist/rich-document/actions/hostActions.d.ts +5 -0
- package/dist/rich-document/actions/hostActions.js +31 -0
- package/dist/rich-document/actions/labels.d.ts +9 -0
- package/dist/rich-document/actions/labels.js +8 -0
- package/dist/rich-document/actions/markdownTable.d.ts +9 -0
- package/dist/rich-document/actions/markdownTable.js +39 -0
- package/dist/rich-document/actions/provider.d.ts +58 -0
- package/dist/rich-document/actions/provider.js +245 -0
- package/dist/rich-document/actions/resumePendingAuthAction.d.ts +3 -0
- package/dist/rich-document/actions/resumePendingAuthAction.js +31 -0
- package/dist/rich-document/actions/sources/index.d.ts +9 -0
- package/dist/rich-document/actions/sources/index.js +22 -0
- package/dist/rich-document/actions/sources/raw.d.ts +4 -0
- package/dist/rich-document/actions/sources/raw.js +35 -0
- package/dist/rich-document/actions/useRichDocumentProvider.d.ts +1 -0
- package/dist/rich-document/actions/useRichDocumentProvider.js +13 -0
- package/dist/rich-document/host-types.d.ts +51 -0
- package/dist/rich-document/host-types.js +0 -0
- package/dist/rich-document/registrations.d.ts +1 -0
- package/dist/rich-document/registrations.js +13 -0
- package/dist/rich-document/review/applyTargets.d.ts +16 -0
- package/dist/rich-document/review/applyTargets.js +32 -0
- package/dist/rich-document/review/proposedEdit.d.ts +22 -0
- package/dist/rich-document/review/proposedEdit.js +48 -0
- package/dist/rich-document/review/protectedSpans.d.ts +14 -0
- package/dist/rich-document/review/protectedSpans.js +61 -0
- package/dist/rich-document/runtime/actionSurfacesStore.d.ts +30 -0
- package/dist/rich-document/runtime/actionSurfacesStore.js +79 -0
- package/dist/rich-document/runtime/contextMenuFor.d.ts +6 -0
- package/dist/rich-document/runtime/contextMenuFor.js +7 -0
- package/dist/rich-document/runtime/providerBridge.d.ts +20 -0
- package/dist/rich-document/runtime/providerBridge.js +23 -0
- package/dist/rich-document/runtime/useActionSurfaceProvider.d.ts +33 -0
- package/dist/rich-document/runtime/useActionSurfaceProvider.js +214 -0
- package/dist/rich-document/search/ContentFindControl.d.ts +8 -0
- package/dist/rich-document/search/ContentFindControl.js +46 -0
- package/dist/rich-document/search/RenderedFindBar.d.ts +8 -0
- package/dist/rich-document/search/RenderedFindBar.js +110 -0
- package/dist/rich-document/search/renderedFind.d.ts +1 -0
- package/dist/rich-document/search/renderedFind.js +1 -0
- package/dist/rich-document/types.d.ts +384 -0
- package/dist/rich-document/types.js +0 -0
- package/dist/rich-document/variants/ActionBar.d.ts +17 -0
- package/dist/rich-document/variants/ActionBar.js +48 -0
- package/dist/rich-document/variants/MenuVariant.d.ts +15 -0
- package/dist/rich-document/variants/MenuVariant.js +17 -0
- package/dist/rich-document/variants/MiniActionBar.d.ts +5 -0
- package/dist/rich-document/variants/MiniActionBar.js +11 -0
- package/dist/rich-document/variants/RegistryActionList.d.ts +17 -0
- package/dist/rich-document/variants/RegistryActionList.js +94 -0
- package/dist/rich-document/variants/RegistryActionMenu.d.ts +24 -0
- package/dist/rich-document/variants/RegistryActionMenu.js +74 -0
- package/dist/rich-document/variants/shared/AlchemyDocumentMenu.d.ts +7 -0
- package/dist/rich-document/variants/shared/AlchemyDocumentMenu.js +83 -0
- package/dist/rich-document/variants/shared/OpenOneMenuButton.d.ts +9 -0
- package/dist/rich-document/variants/shared/OpenOneMenuButton.js +70 -0
- package/dist/rich-document/variants/shared/StandaloneOneMenu.d.ts +8 -0
- package/dist/rich-document/variants/shared/StandaloneOneMenu.js +32 -0
- package/dist/rich-document/variants/shared/categories.d.ts +4 -0
- package/dist/rich-document/variants/shared/categories.js +31 -0
- package/dist/rich-document/variants/shared/iconMap.d.ts +2 -0
- package/dist/rich-document/variants/shared/iconMap.js +73 -0
- package/dist/rich-document/variants/shared/menuStructure.d.ts +81 -0
- package/dist/rich-document/variants/shared/menuStructure.js +271 -0
- package/dist/rich-document/variants/shared/runAction.d.ts +17 -0
- package/dist/rich-document/variants/shared/runAction.js +24 -0
- package/dist/selection-toolbar/SelectionToolbarFrame.d.ts +64 -0
- package/dist/selection-toolbar/SelectionToolbarFrame.js +191 -0
- package/dist/selection-toolbar/common-actions.d.ts +2 -0
- package/dist/selection-toolbar/common-actions.js +103 -0
- package/dist/selection-toolbar/selection-actions.d.ts +126 -0
- package/dist/selection-toolbar/selection-actions.js +240 -0
- package/dist/selection-toolbar/selection-copy.d.ts +19 -0
- package/dist/selection-toolbar/selection-copy.js +98 -0
- package/dist/selection-toolbar/selection-shape.d.ts +1 -0
- package/dist/selection-toolbar/selection-shape.js +15 -0
- package/dist/selection-toolbar/selection-zones.d.ts +90 -0
- package/dist/selection-toolbar/selection-zones.js +85 -0
- package/dist/selection-toolbar/textarea-caret.d.ts +8 -0
- package/dist/selection-toolbar/textarea-caret.js +69 -0
- package/dist/utils/alchemy-icon-keys.d.ts +18 -0
- package/dist/utils/alchemy-icon-keys.js +29 -0
- package/dist/utils/rendered-find.d.ts +9 -0
- package/dist/utils/rendered-find.js +68 -0
- 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,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";
|