@ai-matrx/alchemy 0.8.13 → 0.8.17
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/CHANGELOG.md +32 -1
- package/dist/actions.cjs +1 -373
- package/dist/actions.cjs.map +1 -1
- package/dist/actions.js +1 -350
- package/dist/actions.js.map +1 -1
- package/dist/checks.cjs +9 -314
- package/dist/checks.cjs.map +1 -1
- package/dist/checks.js +8 -290
- package/dist/checks.js.map +1 -1
- package/dist/core.cjs +9 -1728
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +2 -0
- package/dist/core.d.ts +2 -0
- package/dist/core.js +9 -1702
- package/dist/core.js.map +1 -1
- package/dist/declare-host.cjs +1 -18
- package/dist/declare-host.cjs.map +1 -1
- package/dist/declare.cjs +1 -638
- package/dist/declare.cjs.map +1 -1
- package/dist/declare.js +1 -615
- package/dist/declare.js.map +1 -1
- package/dist/index.cjs +9 -1728
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -1702
- package/dist/index.js.map +1 -1
- package/dist/menu.cjs +1 -529
- package/dist/menu.cjs.map +1 -1
- package/dist/menu.js +1 -506
- package/dist/menu.js.map +1 -1
- package/dist/operate-document.cjs +4 -162
- package/dist/operate-document.cjs.map +1 -1
- package/dist/operate-document.js +4 -129
- package/dist/operate-document.js.map +1 -1
- package/dist/operate-rich-html.cjs +1 -128
- package/dist/operate-rich-html.cjs.map +1 -1
- package/dist/operate-rich-html.js +1 -95
- package/dist/operate-rich-html.js.map +1 -1
- package/dist/operate-server.cjs +1 -156
- package/dist/operate-server.cjs.map +1 -1
- package/dist/operate-server.js +1 -133
- package/dist/operate-server.js.map +1 -1
- package/dist/operate-xlsx.cjs +143 -34230
- package/dist/operate-xlsx.cjs.map +1 -1
- package/dist/operate-xlsx.js +143 -34214
- package/dist/operate-xlsx.js.map +1 -1
- package/dist/operate.cjs +9 -1726
- package/dist/operate.cjs.map +1 -1
- package/dist/operate.d.cts +2 -0
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +9 -1702
- package/dist/operate.js.map +1 -1
- package/dist/ports.cjs +1 -18
- package/dist/ports.cjs.map +1 -1
- package/dist/react-bar.cjs +2 -795
- package/dist/react-bar.cjs.map +1 -1
- package/dist/react-bar.js +2 -774
- package/dist/react-bar.js.map +1 -1
- package/dist/react-host.cjs +1 -299
- package/dist/react-host.cjs.map +1 -1
- package/dist/react-host.js +1 -267
- package/dist/react-host.js.map +1 -1
- package/dist/react-menu.cjs +2 -848
- package/dist/react-menu.cjs.map +1 -1
- package/dist/react-menu.js +2 -835
- package/dist/react-menu.js.map +1 -1
- package/dist/react-overflow.cjs +2 -695
- package/dist/react-overflow.cjs.map +1 -1
- package/dist/react-overflow.js +2 -674
- package/dist/react-overflow.js.map +1 -1
- package/dist/react-palette.cjs +2 -586
- package/dist/react-palette.cjs.map +1 -1
- package/dist/react-palette.js +2 -554
- package/dist/react-palette.js.map +1 -1
- package/dist/react-selection.cjs +2 -726
- package/dist/react-selection.cjs.map +1 -1
- package/dist/react-selection.js +2 -705
- package/dist/react-selection.js.map +1 -1
- package/dist/react-sheet.cjs +2 -734
- package/dist/react-sheet.cjs.map +1 -1
- package/dist/react-sheet.js +2 -713
- package/dist/react-sheet.js.map +1 -1
- package/dist/react.cjs +1 -29
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +1 -8
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
package/dist/react-host.js
CHANGED
|
@@ -1,269 +1,3 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
|
|
3
|
-
// src/react/host.tsx
|
|
4
|
-
import * as React2 from "react";
|
|
5
|
-
|
|
6
|
-
// src/actions/click-target.ts
|
|
7
|
-
var UNIVERSAL_VERBS = ["copy", "cut", "paste", "undo", "redo", "find"];
|
|
8
|
-
|
|
9
|
-
// src/actions/registry.ts
|
|
10
|
-
var DuplicateActionError = class extends Error {
|
|
11
|
-
actionId;
|
|
12
|
-
providers;
|
|
13
|
-
constructor(actionId, first, second) {
|
|
14
|
-
super(
|
|
15
|
-
first === second ? `Action "${actionId}" is yielded twice by provider "${first}". Ids are unique; the second was refused.` : `Action "${actionId}" is registered by provider "${first}" and again by "${second}". Ids are unique; "${second}"'s copy was refused.`
|
|
16
|
-
);
|
|
17
|
-
this.name = "DuplicateActionError";
|
|
18
|
-
this.actionId = actionId;
|
|
19
|
-
this.providers = [first, second];
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
var DuplicateProviderError = class extends Error {
|
|
23
|
-
providerId;
|
|
24
|
-
constructor(providerId) {
|
|
25
|
-
super(`Action provider "${providerId}" is already registered. Unregister it first; nothing was overwritten.`);
|
|
26
|
-
this.name = "DuplicateProviderError";
|
|
27
|
-
this.providerId = providerId;
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
var VERBS = new Set(UNIVERSAL_VERBS);
|
|
31
|
-
function capture(ports, error, detail) {
|
|
32
|
-
try {
|
|
33
|
-
ports.diagnostics.capture(error, { area: "actions", detail });
|
|
34
|
-
} catch {
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
function computeEligibility(action, target, options) {
|
|
38
|
-
const restrict = options.restrict;
|
|
39
|
-
if (target.excludedActionIds.has(action.id)) return { status: "absent" };
|
|
40
|
-
if (restrict?.exclude?.includes(action.id)) return { status: "absent" };
|
|
41
|
-
if (restrict?.categories && !restrict.categories.includes(action.category)) return { status: "absent" };
|
|
42
|
-
if (action.writes && action.writes.length > 0) {
|
|
43
|
-
if (target.readOnly) return { status: "absent" };
|
|
44
|
-
const writable = new Set(target.writable.map((w) => w.name));
|
|
45
|
-
if (!action.writes.some((name) => writable.has(name))) return { status: "absent" };
|
|
46
|
-
}
|
|
47
|
-
if (action.requiresAuth && !target.auth.authenticated && !options.authResumable) {
|
|
48
|
-
return { status: "absent" };
|
|
49
|
-
}
|
|
50
|
-
const own = action.eligible(target);
|
|
51
|
-
if (own.status !== "unavailable-verb") return own;
|
|
52
|
-
if (action.category !== "clipboard" || !VERBS.has(own.verb)) {
|
|
53
|
-
return {
|
|
54
|
-
status: "contract-break",
|
|
55
|
-
reason: `Action "${action.id}" answered unavailable-verb but only the six universal verbs (category "clipboard") may grey; it is absent instead.`
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
if (!own.sentence.trim()) {
|
|
59
|
-
return {
|
|
60
|
-
status: "contract-break",
|
|
61
|
-
reason: `Universal verb "${action.id}" greyed without a sentence; a greyed verb must say why.`
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
return own;
|
|
65
|
-
}
|
|
66
|
-
function createActionRegistry(options) {
|
|
67
|
-
const ports = options.ports;
|
|
68
|
-
const entries = /* @__PURE__ */ new Map();
|
|
69
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
70
|
-
const notify = () => {
|
|
71
|
-
for (const l of [...listeners]) l();
|
|
72
|
-
};
|
|
73
|
-
return {
|
|
74
|
-
register(provider) {
|
|
75
|
-
if (entries.has(provider.id)) throw new DuplicateProviderError(provider.id);
|
|
76
|
-
const declared = provider.declaredIds?.() ?? [];
|
|
77
|
-
const own = /* @__PURE__ */ new Set();
|
|
78
|
-
for (const id of declared) {
|
|
79
|
-
if (own.has(id)) throw new DuplicateActionError(id, provider.id, provider.id);
|
|
80
|
-
own.add(id);
|
|
81
|
-
for (const [otherId, other] of entries) {
|
|
82
|
-
if (other.declared.includes(id)) throw new DuplicateActionError(id, otherId, provider.id);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const entry = { provider, declared };
|
|
86
|
-
entries.set(provider.id, entry);
|
|
87
|
-
notify();
|
|
88
|
-
return () => {
|
|
89
|
-
if (entries.get(provider.id) === entry) {
|
|
90
|
-
entries.delete(provider.id);
|
|
91
|
-
notify();
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
},
|
|
95
|
-
async resolve(target, restrict) {
|
|
96
|
-
const authResumable = Boolean(ports.auth);
|
|
97
|
-
const seen = /* @__PURE__ */ new Map();
|
|
98
|
-
const out = [];
|
|
99
|
-
for (const [providerId, { provider }] of [...entries]) {
|
|
100
|
-
let actions;
|
|
101
|
-
try {
|
|
102
|
-
actions = await provider.actions(target);
|
|
103
|
-
} catch (error) {
|
|
104
|
-
capture(ports, error, { providerId, stage: "actions" });
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
for (const raw of actions) {
|
|
108
|
-
const first = seen.get(raw.id);
|
|
109
|
-
if (first !== void 0) {
|
|
110
|
-
capture(ports, new DuplicateActionError(raw.id, first, providerId), { actionId: raw.id });
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
seen.set(raw.id, providerId);
|
|
114
|
-
const action = raw.provider === providerId ? raw : { ...raw, provider: providerId };
|
|
115
|
-
let eligibility;
|
|
116
|
-
try {
|
|
117
|
-
eligibility = computeEligibility(action, target, { restrict: restrict ?? {}, authResumable });
|
|
118
|
-
} catch (error) {
|
|
119
|
-
capture(ports, error, { actionId: action.id, stage: "eligible" });
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (eligibility.status === "contract-break") {
|
|
123
|
-
capture(ports, new Error(eligibility.reason), { actionId: action.id, stage: "eligible" });
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (eligibility.status === "absent") continue;
|
|
127
|
-
out.push({ action, eligibility });
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return out;
|
|
131
|
-
},
|
|
132
|
-
providers() {
|
|
133
|
-
return [...entries.keys()];
|
|
134
|
-
},
|
|
135
|
-
subscribe(listener) {
|
|
136
|
-
listeners.add(listener);
|
|
137
|
-
return () => {
|
|
138
|
-
listeners.delete(listener);
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// src/react/context.ts
|
|
145
|
-
import * as React from "react";
|
|
146
|
-
var KEY = /* @__PURE__ */ Symbol.for("ai-matrx.alchemy.actions-context");
|
|
147
|
-
var AlchemyActionsContext = globalThis[KEY] ??= React.createContext(null);
|
|
148
|
-
function useAlchemyActions() {
|
|
149
|
-
const value = React.useContext(AlchemyActionsContext);
|
|
150
|
-
if (!value) throw new Error("Alchemy actions: render inside <AlchemyActionsProvider> (@ai-matrx/alchemy/react/host).");
|
|
151
|
-
return value;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// src/react/host.tsx
|
|
155
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
156
|
-
function AlchemyActionsProvider({ ports, registry, children }) {
|
|
157
|
-
const own = React2.useMemo(() => registry ?? createActionRegistry({ ports }), [registry, ports]);
|
|
158
|
-
const value = React2.useMemo(() => ({ registry: own, ports }), [own, ports]);
|
|
159
|
-
return /* @__PURE__ */ jsx(AlchemyActionsContext.Provider, { value, children });
|
|
160
|
-
}
|
|
161
|
-
function useRegisterActionProvider(provider) {
|
|
162
|
-
const { registry } = useAlchemyActions();
|
|
163
|
-
React2.useEffect(() => provider ? registry.register(provider) : void 0, [registry, provider]);
|
|
164
|
-
}
|
|
165
|
-
var LONG_PRESS_MS = 480;
|
|
166
|
-
function isEditable(el) {
|
|
167
|
-
if (!el) return false;
|
|
168
|
-
if (el instanceof HTMLTextAreaElement) return true;
|
|
169
|
-
if (el instanceof HTMLInputElement) return !["checkbox", "radio", "button", "submit", "range", "color", "file"].includes(el.type);
|
|
170
|
-
return el.isContentEditable === true;
|
|
171
|
-
}
|
|
172
|
-
function mobileViewport() {
|
|
173
|
-
if (typeof window === "undefined") return false;
|
|
174
|
-
return window.matchMedia?.("(max-width: 767px)").matches ?? false;
|
|
175
|
-
}
|
|
176
|
-
function ActionMenuTrigger({
|
|
177
|
-
resolveTarget,
|
|
178
|
-
children,
|
|
179
|
-
arrangement,
|
|
180
|
-
density,
|
|
181
|
-
mobile,
|
|
182
|
-
editable = false,
|
|
183
|
-
loadMenu = () => import("@ai-matrx/alchemy/react/menu"),
|
|
184
|
-
loadSheet = () => import("@ai-matrx/alchemy/react/sheet"),
|
|
185
|
-
...options
|
|
186
|
-
}) {
|
|
187
|
-
const [state, setState] = React2.useState(null);
|
|
188
|
-
const [open, setOpen] = React2.useState(false);
|
|
189
|
-
const [Menu, setMenu] = React2.useState(null);
|
|
190
|
-
const [Sheet, setSheet] = React2.useState(null);
|
|
191
|
-
const press = React2.useRef(null);
|
|
192
|
-
const openAt = React2.useCallback(
|
|
193
|
-
(element, x, y, touch) => {
|
|
194
|
-
const editable2 = isEditable(element) || isEditable(document.activeElement);
|
|
195
|
-
const selectionText = window.getSelection?.()?.toString() ?? "";
|
|
196
|
-
const target = resolveTarget({ element, selectionText, editable: editable2 });
|
|
197
|
-
if (!target) return false;
|
|
198
|
-
const sheet = mobile ?? (touch || mobileViewport());
|
|
199
|
-
setState({ target, point: { x, y }, sheet });
|
|
200
|
-
setOpen(true);
|
|
201
|
-
if (sheet && !Sheet) void loadSheet().then((m) => setSheet(() => m.ActionSheet));
|
|
202
|
-
if (!sheet && !Menu) void loadMenu().then((m) => setMenu(() => m.ContextMenuPanel));
|
|
203
|
-
return true;
|
|
204
|
-
},
|
|
205
|
-
[resolveTarget, mobile, Sheet, Menu, loadSheet, loadMenu]
|
|
206
|
-
);
|
|
207
|
-
const child = React2.Children.only(children);
|
|
208
|
-
const props = child.props;
|
|
209
|
-
const compose = (own, theirs) => (e) => {
|
|
210
|
-
if (typeof theirs === "function") theirs(e);
|
|
211
|
-
own(e);
|
|
212
|
-
};
|
|
213
|
-
const trigger = React2.cloneElement(child, {
|
|
214
|
-
"data-alchemy-trigger": "context",
|
|
215
|
-
onContextMenu: compose((e) => {
|
|
216
|
-
if (e.defaultPrevented) return;
|
|
217
|
-
const el = e.target;
|
|
218
|
-
if (isEditable(el) && !editable) return;
|
|
219
|
-
if (openAt(el, e.clientX, e.clientY, false)) e.preventDefault();
|
|
220
|
-
}, props.onContextMenu),
|
|
221
|
-
onTouchStart: compose((e) => {
|
|
222
|
-
const t = e.touches[0];
|
|
223
|
-
if (!t) return;
|
|
224
|
-
const el = e.target;
|
|
225
|
-
if (isEditable(el) && !editable) return;
|
|
226
|
-
const timer = setTimeout(() => {
|
|
227
|
-
press.current = null;
|
|
228
|
-
openAt(el, t.clientX, t.clientY, true);
|
|
229
|
-
}, LONG_PRESS_MS);
|
|
230
|
-
press.current = { timer, x: t.clientX, y: t.clientY };
|
|
231
|
-
e.stopPropagation();
|
|
232
|
-
}, props.onTouchStart),
|
|
233
|
-
onTouchMove: compose((e) => {
|
|
234
|
-
const t = e.touches[0];
|
|
235
|
-
const p = press.current;
|
|
236
|
-
if (p && t && Math.hypot(t.clientX - p.x, t.clientY - p.y) > 8) {
|
|
237
|
-
clearTimeout(p.timer);
|
|
238
|
-
press.current = null;
|
|
239
|
-
}
|
|
240
|
-
}, props.onTouchMove),
|
|
241
|
-
onTouchEnd: compose(() => {
|
|
242
|
-
if (press.current) clearTimeout(press.current.timer);
|
|
243
|
-
press.current = null;
|
|
244
|
-
}, props.onTouchEnd)
|
|
245
|
-
});
|
|
246
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
247
|
-
trigger,
|
|
248
|
-
state && !state.sheet && Menu ? /* @__PURE__ */ jsx(
|
|
249
|
-
Menu,
|
|
250
|
-
{
|
|
251
|
-
...options,
|
|
252
|
-
target: state.target,
|
|
253
|
-
point: state.point,
|
|
254
|
-
open,
|
|
255
|
-
onOpenChange: setOpen,
|
|
256
|
-
...arrangement ? { arrangement } : {},
|
|
257
|
-
...density ? { density } : {}
|
|
258
|
-
}
|
|
259
|
-
) : null,
|
|
260
|
-
state && state.sheet && Sheet ? /* @__PURE__ */ jsx(Sheet, { ...options, target: state.target, open, onOpenChange: setOpen }) : null
|
|
261
|
-
] });
|
|
262
|
-
}
|
|
263
|
-
export {
|
|
264
|
-
ActionMenuTrigger,
|
|
265
|
-
AlchemyActionsProvider,
|
|
266
|
-
useAlchemyActions,
|
|
267
|
-
useRegisterActionProvider
|
|
268
|
-
};
|
|
2
|
+
import*as React2 from"react";var UNIVERSAL_VERBS=["copy","cut","paste","undo","redo","find"];var DuplicateActionError=class extends Error{actionId;providers;constructor(actionId,first,second){super(first===second?`Action "${actionId}" is yielded twice by provider "${first}". Ids are unique; the second was refused.`:`Action "${actionId}" is registered by provider "${first}" and again by "${second}". Ids are unique; "${second}"'s copy was refused.`),this.name="DuplicateActionError",this.actionId=actionId,this.providers=[first,second]}},DuplicateProviderError=class extends Error{providerId;constructor(providerId){super(`Action provider "${providerId}" is already registered. Unregister it first; nothing was overwritten.`),this.name="DuplicateProviderError",this.providerId=providerId}},VERBS=new Set(UNIVERSAL_VERBS);function capture(ports,error,detail){try{ports.diagnostics.capture(error,{area:"actions",detail})}catch{}}function computeEligibility(action,target,options){let restrict=options.restrict;if(target.excludedActionIds.has(action.id))return{status:"absent"};if(restrict?.exclude?.includes(action.id))return{status:"absent"};if(restrict?.categories&&!restrict.categories.includes(action.category))return{status:"absent"};if(action.writes&&action.writes.length>0){if(target.readOnly)return{status:"absent"};let writable=new Set(target.writable.map(w=>w.name));if(!action.writes.some(name=>writable.has(name)))return{status:"absent"}}if(action.requiresAuth&&!target.auth.authenticated&&!options.authResumable)return{status:"absent"};let own=action.eligible(target);return own.status!=="unavailable-verb"?own:action.category!=="clipboard"||!VERBS.has(own.verb)?{status:"contract-break",reason:`Action "${action.id}" answered unavailable-verb but only the six universal verbs (category "clipboard") may grey; it is absent instead.`}:own.sentence.trim()?own:{status:"contract-break",reason:`Universal verb "${action.id}" greyed without a sentence; a greyed verb must say why.`}}function createActionRegistry(options){let ports=options.ports,entries=new Map,listeners=new Set,notify=()=>{for(let l of[...listeners])l()};return{register(provider){if(entries.has(provider.id))throw new DuplicateProviderError(provider.id);let declared=provider.declaredIds?.()??[],own=new Set;for(let id of declared){if(own.has(id))throw new DuplicateActionError(id,provider.id,provider.id);own.add(id);for(let[otherId,other]of entries)if(other.declared.includes(id))throw new DuplicateActionError(id,otherId,provider.id)}let entry={provider,declared};return entries.set(provider.id,entry),notify(),()=>{entries.get(provider.id)===entry&&(entries.delete(provider.id),notify())}},async resolve(target,restrict){let authResumable=!!ports.auth,seen=new Map,out=[];for(let[providerId,{provider}]of[...entries]){let actions;try{actions=await provider.actions(target)}catch(error){capture(ports,error,{providerId,stage:"actions"});continue}for(let raw of actions){let first=seen.get(raw.id);if(first!==void 0){capture(ports,new DuplicateActionError(raw.id,first,providerId),{actionId:raw.id});continue}seen.set(raw.id,providerId);let action=raw.provider===providerId?raw:{...raw,provider:providerId},eligibility;try{eligibility=computeEligibility(action,target,{restrict:restrict??{},authResumable})}catch(error){capture(ports,error,{actionId:action.id,stage:"eligible"});continue}if(eligibility.status==="contract-break"){capture(ports,new Error(eligibility.reason),{actionId:action.id,stage:"eligible"});continue}eligibility.status!=="absent"&&out.push({action,eligibility})}}return out},providers(){return[...entries.keys()]},subscribe(listener){return listeners.add(listener),()=>{listeners.delete(listener)}}}}import*as React from"react";var KEY=Symbol.for("ai-matrx.alchemy.actions-context"),AlchemyActionsContext=globalThis[KEY]??=React.createContext(null);function useAlchemyActions(){let value=React.useContext(AlchemyActionsContext);if(!value)throw new Error("Alchemy actions: render inside <AlchemyActionsProvider> (@ai-matrx/alchemy/react/host).");return value}import{Fragment,jsx,jsxs}from"react/jsx-runtime";function AlchemyActionsProvider({ports,registry,children}){let own=React2.useMemo(()=>registry??createActionRegistry({ports}),[registry,ports]),value=React2.useMemo(()=>({registry:own,ports}),[own,ports]);return jsx(AlchemyActionsContext.Provider,{value,children})}function useRegisterActionProvider(provider){let{registry}=useAlchemyActions();React2.useEffect(()=>provider?registry.register(provider):void 0,[registry,provider])}var LONG_PRESS_MS=480;function isEditable(el){return el?el instanceof HTMLTextAreaElement?!0:el instanceof HTMLInputElement?!["checkbox","radio","button","submit","range","color","file"].includes(el.type):el.isContentEditable===!0:!1}function mobileViewport(){return typeof window>"u"?!1:window.matchMedia?.("(max-width: 767px)").matches??!1}function ActionMenuTrigger({resolveTarget,children,arrangement,density,mobile,editable=!1,loadMenu=()=>import("@ai-matrx/alchemy/react/menu"),loadSheet=()=>import("@ai-matrx/alchemy/react/sheet"),...options}){let[state,setState]=React2.useState(null),[open,setOpen]=React2.useState(!1),[Menu,setMenu]=React2.useState(null),[Sheet,setSheet]=React2.useState(null),press=React2.useRef(null),openAt=React2.useCallback((element,x,y,touch)=>{let editable2=isEditable(element)||isEditable(document.activeElement),selectionText=window.getSelection?.()?.toString()??"",target=resolveTarget({element,selectionText,editable:editable2});if(!target)return!1;let sheet=mobile??(touch||mobileViewport());return setState({target,point:{x,y},sheet}),setOpen(!0),sheet&&!Sheet&&loadSheet().then(m=>setSheet(()=>m.ActionSheet)),!sheet&&!Menu&&loadMenu().then(m=>setMenu(()=>m.ContextMenuPanel)),!0},[resolveTarget,mobile,Sheet,Menu,loadSheet,loadMenu]),child=React2.Children.only(children),props=child.props,compose=(own,theirs)=>e=>{typeof theirs=="function"&&theirs(e),own(e)},trigger=React2.cloneElement(child,{"data-alchemy-trigger":"context",onContextMenu:compose(e=>{if(e.defaultPrevented)return;let el=e.target;isEditable(el)&&!editable||openAt(el,e.clientX,e.clientY,!1)&&e.preventDefault()},props.onContextMenu),onTouchStart:compose(e=>{let t=e.touches[0];if(!t)return;let el=e.target;if(isEditable(el)&&!editable)return;let timer=setTimeout(()=>{press.current=null,openAt(el,t.clientX,t.clientY,!0)},LONG_PRESS_MS);press.current={timer,x:t.clientX,y:t.clientY},e.stopPropagation()},props.onTouchStart),onTouchMove:compose(e=>{let t=e.touches[0],p=press.current;p&&t&&Math.hypot(t.clientX-p.x,t.clientY-p.y)>8&&(clearTimeout(p.timer),press.current=null)},props.onTouchMove),onTouchEnd:compose(()=>{press.current&&clearTimeout(press.current.timer),press.current=null},props.onTouchEnd)});return jsxs(Fragment,{children:[trigger,state&&!state.sheet&&Menu?jsx(Menu,{...options,target:state.target,point:state.point,open,onOpenChange:setOpen,...arrangement?{arrangement}:{},...density?{density}:{}}):null,state&&state.sheet&&Sheet?jsx(Sheet,{...options,target:state.target,open,onOpenChange:setOpen}):null]})}export{ActionMenuTrigger,AlchemyActionsProvider,useAlchemyActions,useRegisterActionProvider};
|
|
269
3
|
//# sourceMappingURL=react-host.js.map
|
package/dist/react-host.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/react/host.tsx","../src/actions/click-target.ts","../src/actions/registry.ts","../src/react/context.ts"],"sourcesContent":["\"use client\";\n/**\n * `@ai-matrx/alchemy/react/host` — T0, the shell every mount pays for.\n *\n * Provider (registry + ports), provider registration, and the gesture shell\n * (right-click on desktop, long-press on touch). It imports NO model, NO\n * layout, NO renderer: the context menu (`/react/menu`, T1) and the mobile\n * sheet (`/react/sheet`, T1m) load by dynamic import on first open only.\n */\nimport * as React from \"react\";\nimport { createActionRegistry } from \"../actions/registry\";\nimport type { ActionProvider, ActionRegistry, ClickTarget } from \"../actions/types\";\nimport type { AlchemyHostPorts } from \"../ports\";\nimport { AlchemyActionsContext, useAlchemyActions, type AlchemyActionsValue } from \"./context\";\nimport type { MenuEngineOptions } from \"./engine\";\n\nexport { useAlchemyActions, type AlchemyActionsValue };\n\nexport interface AlchemyActionsProviderProps {\n ports: AlchemyHostPorts;\n /** Share one registry across providers; default: one per provider mount. */\n registry?: ActionRegistry;\n children: React.ReactNode;\n}\n\nexport function AlchemyActionsProvider({ ports, registry, children }: AlchemyActionsProviderProps): React.ReactElement {\n const own = React.useMemo(() => registry ?? createActionRegistry({ ports }), [registry, ports]);\n const value = React.useMemo(() => ({ registry: own, ports }), [own, ports]);\n return <AlchemyActionsContext.Provider value={value}>{children}</AlchemyActionsContext.Provider>;\n}\n\n/** Register a provider for the lifetime of the calling component. Duplicate ids throw (refused). */\nexport function useRegisterActionProvider(provider: ActionProvider | null): void {\n const { registry } = useAlchemyActions();\n React.useEffect(() => (provider ? registry.register(provider) : undefined), [registry, provider]);\n}\n\ntype MenuModule = typeof import(\"./menu\");\ntype SheetModule = typeof import(\"./sheet\");\n\nexport interface ActionMenuTriggerProps extends MenuEngineOptions {\n /** Build the target at gesture time (selection captured then). */\n resolveTarget(gesture: { element: Element; selectionText: string; editable: boolean }): ClickTarget | null;\n children: React.ReactElement;\n arrangement?: \"classic\" | \"tiered\" | \"command\";\n density?: \"comfortable\" | \"compact\";\n /** Force a renderer; default: sheet below 768px or on coarse pointers. */\n mobile?: boolean;\n /**\n * The wrapped surface is an editor that wires text mutation into the menu,\n * so the menu takes its right-click. Otherwise a live text field keeps its\n * native menu (Paste, spellcheck, autofill).\n */\n editable?: boolean;\n /** Override the lazy loaders (tests, custom bundlers). */\n loadMenu?: () => Promise<Pick<MenuModule, \"ContextMenuPanel\">>;\n loadSheet?: () => Promise<Pick<SheetModule, \"ActionSheet\">>;\n}\n\nconst LONG_PRESS_MS = 480;\n\nfunction isEditable(el: Element | null): boolean {\n if (!el) return false;\n if (el instanceof HTMLTextAreaElement) return true;\n if (el instanceof HTMLInputElement) return ![\"checkbox\", \"radio\", \"button\", \"submit\", \"range\", \"color\", \"file\"].includes(el.type);\n return (el as HTMLElement).isContentEditable === true;\n}\n\nfunction mobileViewport(): boolean {\n if (typeof window === \"undefined\") return false;\n return window.matchMedia?.(\"(max-width: 767px)\").matches ?? false;\n}\n\nexport function ActionMenuTrigger({\n resolveTarget,\n children,\n arrangement,\n density,\n mobile,\n editable = false,\n loadMenu = () => import(\"@ai-matrx/alchemy/react/menu\"),\n loadSheet = () => import(\"@ai-matrx/alchemy/react/sheet\"),\n ...options\n}: ActionMenuTriggerProps): React.ReactElement {\n const [state, setState] = React.useState<{ target: ClickTarget; point: { x: number; y: number }; sheet: boolean } | null>(null);\n const [open, setOpen] = React.useState(false);\n const [Menu, setMenu] = React.useState<MenuModule[\"ContextMenuPanel\"] | null>(null);\n const [Sheet, setSheet] = React.useState<SheetModule[\"ActionSheet\"] | null>(null);\n const press = React.useRef<{ timer: ReturnType<typeof setTimeout>; x: number; y: number } | null>(null);\n\n const openAt = React.useCallback(\n (element: Element, x: number, y: number, touch: boolean) => {\n const editable = isEditable(element) || isEditable(document.activeElement);\n const selectionText = window.getSelection?.()?.toString() ?? \"\";\n const target = resolveTarget({ element, selectionText, editable });\n if (!target) return false;\n const sheet = mobile ?? (touch || mobileViewport());\n setState({ target, point: { x, y }, sheet });\n setOpen(true);\n if (sheet && !Sheet) void loadSheet().then((m) => setSheet(() => m.ActionSheet));\n if (!sheet && !Menu) void loadMenu().then((m) => setMenu(() => m.ContextMenuPanel));\n return true;\n },\n [resolveTarget, mobile, Sheet, Menu, loadSheet, loadMenu],\n );\n\n const child = React.Children.only(children) as React.ReactElement<Record<string, unknown>>;\n const props = child.props;\n const compose =\n <E,>(own: (e: E) => void, theirs: unknown) =>\n (e: E) => {\n if (typeof theirs === \"function\") (theirs as (e: E) => void)(e);\n own(e);\n };\n\n const trigger = React.cloneElement(child, {\n \"data-alchemy-trigger\": \"context\",\n onContextMenu: compose((e: React.MouseEvent) => {\n if (e.defaultPrevented) return; // an inner menu owns the gesture\n const el = e.target as Element;\n if (isEditable(el) && !editable) return; // never steal a live text field's native menu\n if (openAt(el, e.clientX, e.clientY, false)) e.preventDefault();\n }, props.onContextMenu),\n onTouchStart: compose((e: React.TouchEvent) => {\n const t = e.touches[0];\n if (!t) return;\n const el = e.target as Element;\n if (isEditable(el) && !editable) return;\n const timer = setTimeout(() => {\n press.current = null;\n openAt(el, t.clientX, t.clientY, true);\n }, LONG_PRESS_MS);\n press.current = { timer, x: t.clientX, y: t.clientY };\n e.stopPropagation();\n }, props.onTouchStart),\n onTouchMove: compose((e: React.TouchEvent) => {\n const t = e.touches[0];\n const p = press.current;\n if (p && t && Math.hypot(t.clientX - p.x, t.clientY - p.y) > 8) {\n clearTimeout(p.timer);\n press.current = null;\n }\n }, props.onTouchMove),\n onTouchEnd: compose(() => {\n if (press.current) clearTimeout(press.current.timer);\n press.current = null;\n }, props.onTouchEnd),\n });\n\n return (\n <>\n {trigger}\n {state && !state.sheet && Menu ? (\n <Menu\n {...options}\n target={state.target}\n point={state.point}\n open={open}\n onOpenChange={setOpen}\n {...(arrangement ? { arrangement } : {})}\n {...(density ? { density } : {})}\n />\n ) : null}\n {state && state.sheet && Sheet ? <Sheet {...options} target={state.target} open={open} onOpenChange={setOpen} /> : null}\n </>\n );\n}\n","/**\n * The click target (CONTRACT §2.4) in its own module: `/ports` references it,\n * and `/ports` must stay free of every other package (types only).\n */\nimport type { QualifiedValueKey, ResolvedItem, ResolvedSurfaceDeclaration, WriteTarget } from \"../declare/types\";\n\n/** The six universal verbs — the ONLY rows that may grey (R1 c). */\nexport const UNIVERSAL_VERBS = [\"copy\", \"cut\", \"paste\", \"undo\", \"redo\", \"find\"] as const;\nexport type UniversalVerb = (typeof UNIVERSAL_VERBS)[number];\n\n/** The transfer Payload's kinds (`actions/types.ts` asserts equality at compile time). */\nexport type PayloadKind = \"text\" | \"markdown\" | \"json\" | \"rows\" | \"registered\";\n\n/** What eligibility is computed from: everything declared at the click target. */\nexport interface ClickTarget {\n surface: { instanceId: string; surfaceName: string } | null;\n /** The resolved declaration (inheritance applied). Eligibility decides by KEY existence over it. */\n declaration: ResolvedSurfaceDeclaration | null;\n /** Declared value keys with a read path here (screen names + qualified item keys). Key existence, not population. */\n valueKeys: ReadonlySet<QualifiedValueKey>;\n /** Scope chain, most general first: [\"global\", \"<domain>\", \"<surfaceName>\"]. */\n scopeChain: readonly string[];\n /** The surface's explicit exclusions (the one override). */\n excludedActionIds: ReadonlySet<string>;\n item: ResolvedItem | null;\n level: string | null;\n /** Offsets + the live range handle, editable or not. */\n selection: {\n text: string;\n type: \"editable\" | \"non-editable\";\n start: number;\n end: number;\n /** Opaque host handle (DOM Range / element) used to restore or stream into the selection. */\n handle?: unknown;\n } | null;\n /** Payload kinds a capture of this target would produce (from declared value types/kinds). */\n payloadKinds: readonly PayloadKind[];\n /** Declared targets writable on this target right now. */\n writable: readonly WriteTarget[];\n readOnly: boolean;\n /** True when any declared value here is secret/credential or non-exportable. */\n restricted: boolean;\n organizationId: string | null;\n auth: { authenticated: boolean; admin: boolean; creator: boolean };\n /**\n * Host-owned context a provider reads to act (the rich-document source, the\n * editable field's callbacks). Opaque to the package; never crosses into the\n * model. Providers that need it narrow it themselves.\n */\n host?: unknown;\n}\n\n","/**\n * The one action registry (CONTRACT §2.4).\n *\n * - Duplicate ids are REFUSED, never overwritten (rich-document's registry\n * silently overwrote — R12). A duplicate known at registration throws; one\n * that only appears when a provider yields it is dropped and captured.\n * - Eligibility is one pure pass over the resolved click target, under\n * \"restrict, never relax\": a host may remove ids or categories, never add.\n * - R1: an action that does not apply is ABSENT. Only the six universal verbs\n * (category \"clipboard\") may answer `unavailable-verb`, with a sentence; any\n * other action answering it is a contract break → absent + diagnostics.\n */\nimport type { AlchemyHostPorts, Json } from \"../ports\";\nimport {\n UNIVERSAL_VERBS,\n type Action,\n type ActionProvider,\n type ActionRegistry,\n type ClickTarget,\n type Eligibility,\n type ResolveRestriction,\n type ResolvedAction,\n} from \"./types\";\n\nexport class DuplicateActionError extends Error {\n readonly actionId: string;\n readonly providers: readonly [string, string];\n constructor(actionId: string, first: string, second: string) {\n super(\n first === second\n ? `Action \"${actionId}\" is yielded twice by provider \"${first}\". Ids are unique; the second was refused.`\n : `Action \"${actionId}\" is registered by provider \"${first}\" and again by \"${second}\". Ids are unique; \"${second}\"'s copy was refused.`,\n );\n this.name = \"DuplicateActionError\";\n this.actionId = actionId;\n this.providers = [first, second];\n }\n}\n\nexport class DuplicateProviderError extends Error {\n readonly providerId: string;\n constructor(providerId: string) {\n super(`Action provider \"${providerId}\" is already registered. Unregister it first; nothing was overwritten.`);\n this.name = \"DuplicateProviderError\";\n this.providerId = providerId;\n }\n}\n\nexport interface ActionRegistryOptions {\n /** `diagnostics` is required (nothing fails silently); `auth` decides whether requiresAuth actions reach a signed-out person. */\n ports: Pick<AlchemyHostPorts, \"diagnostics\" | \"auth\">;\n}\n\nconst VERBS = new Set<string>(UNIVERSAL_VERBS);\n\nfunction capture(ports: ActionRegistryOptions[\"ports\"], error: unknown, detail: Json): void {\n try {\n ports.diagnostics.capture(error, { area: \"actions\", detail });\n } catch {\n // A broken diagnostics port must never take the menu down with it.\n }\n}\n\n/**\n * Eligibility of one action at one target — the ONE pure pass (CONTRACT §1.6).\n * Exported for providers' tests; hosts call `registry.resolve`.\n */\nexport function computeEligibility(\n action: Action,\n target: ClickTarget,\n options: { restrict?: ResolveRestriction; authResumable: boolean },\n): Eligibility | { status: \"contract-break\"; reason: string } {\n const restrict = options.restrict;\n if (target.excludedActionIds.has(action.id)) return { status: \"absent\" };\n if (restrict?.exclude?.includes(action.id)) return { status: \"absent\" };\n if (restrict?.categories && !restrict.categories.includes(action.category)) return { status: \"absent\" };\n if (action.writes && action.writes.length > 0) {\n // A read-only target offers nothing that would change the record.\n if (target.readOnly) return { status: \"absent\" };\n const writable = new Set(target.writable.map((w) => w.name));\n if (!action.writes.some((name) => writable.has(name))) return { status: \"absent\" };\n }\n if (action.requiresAuth && !target.auth.authenticated && !options.authResumable) {\n // Sign-in-then-resume is the capability; without the auth port it is absent (N8).\n return { status: \"absent\" };\n }\n const own = action.eligible(target);\n if (own.status !== \"unavailable-verb\") return own;\n if (action.category !== \"clipboard\" || !VERBS.has(own.verb)) {\n return {\n status: \"contract-break\",\n reason: `Action \"${action.id}\" answered unavailable-verb but only the six universal verbs (category \"clipboard\") may grey; it is absent instead.`,\n };\n }\n if (!own.sentence.trim()) {\n return {\n status: \"contract-break\",\n reason: `Universal verb \"${action.id}\" greyed without a sentence; a greyed verb must say why.`,\n };\n }\n return own;\n}\n\ninterface Entry {\n provider: ActionProvider;\n declared: readonly string[];\n}\n\nexport function createActionRegistry(options: ActionRegistryOptions): ActionRegistry {\n const ports = options.ports;\n const entries = new Map<string, Entry>();\n const listeners = new Set<() => void>();\n const notify = () => {\n for (const l of [...listeners]) l();\n };\n\n return {\n register(provider) {\n if (entries.has(provider.id)) throw new DuplicateProviderError(provider.id);\n const declared = provider.declaredIds?.() ?? [];\n const own = new Set<string>();\n for (const id of declared) {\n if (own.has(id)) throw new DuplicateActionError(id, provider.id, provider.id);\n own.add(id);\n for (const [otherId, other] of entries) {\n if (other.declared.includes(id)) throw new DuplicateActionError(id, otherId, provider.id);\n }\n }\n const entry: Entry = { provider, declared };\n entries.set(provider.id, entry);\n notify();\n return () => {\n if (entries.get(provider.id) === entry) {\n entries.delete(provider.id);\n notify();\n }\n };\n },\n\n async resolve(target, restrict) {\n const authResumable = Boolean(ports.auth);\n const seen = new Map<string, string>();\n const out: ResolvedAction[] = [];\n for (const [providerId, { provider }] of [...entries]) {\n let actions: readonly Action[];\n try {\n actions = await provider.actions(target);\n } catch (error) {\n capture(ports, error, { providerId, stage: \"actions\" });\n continue;\n }\n for (const raw of actions) {\n const first = seen.get(raw.id);\n if (first !== undefined) {\n capture(ports, new DuplicateActionError(raw.id, first, providerId), { actionId: raw.id });\n continue;\n }\n seen.set(raw.id, providerId);\n const action: Action = raw.provider === providerId ? raw : { ...raw, provider: providerId };\n let eligibility: ReturnType<typeof computeEligibility>;\n try {\n eligibility = computeEligibility(action, target, { restrict: restrict ?? {}, authResumable });\n } catch (error) {\n capture(ports, error, { actionId: action.id, stage: \"eligible\" });\n continue;\n }\n if (eligibility.status === \"contract-break\") {\n capture(ports, new Error(eligibility.reason), { actionId: action.id, stage: \"eligible\" });\n continue;\n }\n if (eligibility.status === \"absent\") continue;\n out.push({ action, eligibility });\n }\n }\n return out;\n },\n\n providers() {\n return [...entries.keys()];\n },\n\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n\n/** A provider over a fixed list; its ids are declared up front so duplicates fail at registration. */\nexport function staticActionProvider(\n id: string,\n actions: readonly Action[],\n tier: ActionProvider[\"tier\"] = \"T0\",\n): ActionProvider {\n const list = actions.map((a) => (a.provider === id ? a : { ...a, provider: id }));\n return {\n id,\n tier,\n declaredIds: () => list.map((a) => a.id),\n actions: () => list,\n };\n}\n\n/** Always-available eligibility, for actions with no rule of their own. */\nexport const alwaysEligible = (): Eligibility => ({ status: \"available\" });\n","/**\n * The shared React context (T0-safe: imports only React and types). Shared\n * through globalThis so every per-layout bundle reads the same provider.\n */\nimport * as React from \"react\";\nimport type { ActionRegistry } from \"../actions/types\";\nimport type { AlchemyHostPorts } from \"../ports\";\n\nexport interface AlchemyActionsValue {\n registry: ActionRegistry;\n ports: AlchemyHostPorts;\n}\n\nconst KEY = Symbol.for(\"ai-matrx.alchemy.actions-context\");\ntype Holder = { [KEY]?: React.Context<AlchemyActionsValue | null> };\nexport const AlchemyActionsContext: React.Context<AlchemyActionsValue | null> =\n ((globalThis as Holder)[KEY] ??= React.createContext<AlchemyActionsValue | null>(null));\n\nexport function useAlchemyActions(): AlchemyActionsValue {\n const value = React.useContext(AlchemyActionsContext);\n if (!value) throw new Error(\"Alchemy actions: render inside <AlchemyActionsProvider> (@ai-matrx/alchemy/react/host).\");\n return value;\n}\n"],"mappings":"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","names":["React","editable"]}
|
|
1
|
+
{"version":3,"sources":["../src/react/host.tsx","../src/actions/click-target.ts","../src/actions/registry.ts","../src/react/context.ts"],"sourcesContent":["\"use client\";\n/**\n * `@ai-matrx/alchemy/react/host` — T0, the shell every mount pays for.\n *\n * Provider (registry + ports), provider registration, and the gesture shell\n * (right-click on desktop, long-press on touch). It imports NO model, NO\n * layout, NO renderer: the context menu (`/react/menu`, T1) and the mobile\n * sheet (`/react/sheet`, T1m) load by dynamic import on first open only.\n */\nimport * as React from \"react\";\nimport { createActionRegistry } from \"../actions/registry\";\nimport type { ActionProvider, ActionRegistry, ClickTarget } from \"../actions/types\";\nimport type { AlchemyHostPorts } from \"../ports\";\nimport { AlchemyActionsContext, useAlchemyActions, type AlchemyActionsValue } from \"./context\";\nimport type { MenuEngineOptions } from \"./engine\";\n\nexport { useAlchemyActions, type AlchemyActionsValue };\n\nexport interface AlchemyActionsProviderProps {\n ports: AlchemyHostPorts;\n /** Share one registry across providers; default: one per provider mount. */\n registry?: ActionRegistry;\n children: React.ReactNode;\n}\n\nexport function AlchemyActionsProvider({ ports, registry, children }: AlchemyActionsProviderProps): React.ReactElement {\n const own = React.useMemo(() => registry ?? createActionRegistry({ ports }), [registry, ports]);\n const value = React.useMemo(() => ({ registry: own, ports }), [own, ports]);\n return <AlchemyActionsContext.Provider value={value}>{children}</AlchemyActionsContext.Provider>;\n}\n\n/** Register a provider for the lifetime of the calling component. Duplicate ids throw (refused). */\nexport function useRegisterActionProvider(provider: ActionProvider | null): void {\n const { registry } = useAlchemyActions();\n React.useEffect(() => (provider ? registry.register(provider) : undefined), [registry, provider]);\n}\n\ntype MenuModule = typeof import(\"./menu\");\ntype SheetModule = typeof import(\"./sheet\");\n\nexport interface ActionMenuTriggerProps extends MenuEngineOptions {\n /** Build the target at gesture time (selection captured then). */\n resolveTarget(gesture: { element: Element; selectionText: string; editable: boolean }): ClickTarget | null;\n children: React.ReactElement;\n arrangement?: \"classic\" | \"tiered\" | \"command\";\n density?: \"comfortable\" | \"compact\";\n /** Force a renderer; default: sheet below 768px or on coarse pointers. */\n mobile?: boolean;\n /**\n * The wrapped surface is an editor that wires text mutation into the menu,\n * so the menu takes its right-click. Otherwise a live text field keeps its\n * native menu (Paste, spellcheck, autofill).\n */\n editable?: boolean;\n /** Override the lazy loaders (tests, custom bundlers). */\n loadMenu?: () => Promise<Pick<MenuModule, \"ContextMenuPanel\">>;\n loadSheet?: () => Promise<Pick<SheetModule, \"ActionSheet\">>;\n}\n\nconst LONG_PRESS_MS = 480;\n\nfunction isEditable(el: Element | null): boolean {\n if (!el) return false;\n if (el instanceof HTMLTextAreaElement) return true;\n if (el instanceof HTMLInputElement) return ![\"checkbox\", \"radio\", \"button\", \"submit\", \"range\", \"color\", \"file\"].includes(el.type);\n return (el as HTMLElement).isContentEditable === true;\n}\n\nfunction mobileViewport(): boolean {\n if (typeof window === \"undefined\") return false;\n return window.matchMedia?.(\"(max-width: 767px)\").matches ?? false;\n}\n\nexport function ActionMenuTrigger({\n resolveTarget,\n children,\n arrangement,\n density,\n mobile,\n editable = false,\n loadMenu = () => import(\"@ai-matrx/alchemy/react/menu\"),\n loadSheet = () => import(\"@ai-matrx/alchemy/react/sheet\"),\n ...options\n}: ActionMenuTriggerProps): React.ReactElement {\n const [state, setState] = React.useState<{ target: ClickTarget; point: { x: number; y: number }; sheet: boolean } | null>(null);\n const [open, setOpen] = React.useState(false);\n const [Menu, setMenu] = React.useState<MenuModule[\"ContextMenuPanel\"] | null>(null);\n const [Sheet, setSheet] = React.useState<SheetModule[\"ActionSheet\"] | null>(null);\n const press = React.useRef<{ timer: ReturnType<typeof setTimeout>; x: number; y: number } | null>(null);\n\n const openAt = React.useCallback(\n (element: Element, x: number, y: number, touch: boolean) => {\n const editable = isEditable(element) || isEditable(document.activeElement);\n const selectionText = window.getSelection?.()?.toString() ?? \"\";\n const target = resolveTarget({ element, selectionText, editable });\n if (!target) return false;\n const sheet = mobile ?? (touch || mobileViewport());\n setState({ target, point: { x, y }, sheet });\n setOpen(true);\n if (sheet && !Sheet) void loadSheet().then((m) => setSheet(() => m.ActionSheet));\n if (!sheet && !Menu) void loadMenu().then((m) => setMenu(() => m.ContextMenuPanel));\n return true;\n },\n [resolveTarget, mobile, Sheet, Menu, loadSheet, loadMenu],\n );\n\n const child = React.Children.only(children) as React.ReactElement<Record<string, unknown>>;\n const props = child.props;\n const compose =\n <E,>(own: (e: E) => void, theirs: unknown) =>\n (e: E) => {\n if (typeof theirs === \"function\") (theirs as (e: E) => void)(e);\n own(e);\n };\n\n const trigger = React.cloneElement(child, {\n \"data-alchemy-trigger\": \"context\",\n onContextMenu: compose((e: React.MouseEvent) => {\n if (e.defaultPrevented) return; // an inner menu owns the gesture\n const el = e.target as Element;\n if (isEditable(el) && !editable) return; // never steal a live text field's native menu\n if (openAt(el, e.clientX, e.clientY, false)) e.preventDefault();\n }, props.onContextMenu),\n onTouchStart: compose((e: React.TouchEvent) => {\n const t = e.touches[0];\n if (!t) return;\n const el = e.target as Element;\n if (isEditable(el) && !editable) return;\n const timer = setTimeout(() => {\n press.current = null;\n openAt(el, t.clientX, t.clientY, true);\n }, LONG_PRESS_MS);\n press.current = { timer, x: t.clientX, y: t.clientY };\n e.stopPropagation();\n }, props.onTouchStart),\n onTouchMove: compose((e: React.TouchEvent) => {\n const t = e.touches[0];\n const p = press.current;\n if (p && t && Math.hypot(t.clientX - p.x, t.clientY - p.y) > 8) {\n clearTimeout(p.timer);\n press.current = null;\n }\n }, props.onTouchMove),\n onTouchEnd: compose(() => {\n if (press.current) clearTimeout(press.current.timer);\n press.current = null;\n }, props.onTouchEnd),\n });\n\n return (\n <>\n {trigger}\n {state && !state.sheet && Menu ? (\n <Menu\n {...options}\n target={state.target}\n point={state.point}\n open={open}\n onOpenChange={setOpen}\n {...(arrangement ? { arrangement } : {})}\n {...(density ? { density } : {})}\n />\n ) : null}\n {state && state.sheet && Sheet ? <Sheet {...options} target={state.target} open={open} onOpenChange={setOpen} /> : null}\n </>\n );\n}\n","/**\n * The click target (CONTRACT §2.4) in its own module: `/ports` references it,\n * and `/ports` must stay free of every other package (types only).\n */\nimport type { QualifiedValueKey, ResolvedItem, ResolvedSurfaceDeclaration, WriteTarget } from \"../declare/types\";\n\n/** The six universal verbs — the ONLY rows that may grey (R1 c). */\nexport const UNIVERSAL_VERBS = [\"copy\", \"cut\", \"paste\", \"undo\", \"redo\", \"find\"] as const;\nexport type UniversalVerb = (typeof UNIVERSAL_VERBS)[number];\n\n/** The transfer Payload's kinds (`actions/types.ts` asserts equality at compile time). */\nexport type PayloadKind = \"text\" | \"markdown\" | \"json\" | \"rows\" | \"registered\";\n\n/** What eligibility is computed from: everything declared at the click target. */\nexport interface ClickTarget {\n surface: { instanceId: string; surfaceName: string } | null;\n /** The resolved declaration (inheritance applied). Eligibility decides by KEY existence over it. */\n declaration: ResolvedSurfaceDeclaration | null;\n /** Declared value keys with a read path here (screen names + qualified item keys). Key existence, not population. */\n valueKeys: ReadonlySet<QualifiedValueKey>;\n /** Scope chain, most general first: [\"global\", \"<domain>\", \"<surfaceName>\"]. */\n scopeChain: readonly string[];\n /** The surface's explicit exclusions (the one override). */\n excludedActionIds: ReadonlySet<string>;\n item: ResolvedItem | null;\n level: string | null;\n /** Offsets + the live range handle, editable or not. */\n selection: {\n text: string;\n type: \"editable\" | \"non-editable\";\n start: number;\n end: number;\n /** Opaque host handle (DOM Range / element) used to restore or stream into the selection. */\n handle?: unknown;\n } | null;\n /** Payload kinds a capture of this target would produce (from declared value types/kinds). */\n payloadKinds: readonly PayloadKind[];\n /** Declared targets writable on this target right now. */\n writable: readonly WriteTarget[];\n readOnly: boolean;\n /** True when any declared value here is secret/credential or non-exportable. */\n restricted: boolean;\n organizationId: string | null;\n auth: { authenticated: boolean; admin: boolean; creator: boolean };\n /**\n * Host-owned context a provider reads to act (the rich-document source, the\n * editable field's callbacks). Opaque to the package; never crosses into the\n * model. Providers that need it narrow it themselves.\n */\n host?: unknown;\n}\n\n","/**\n * The one action registry (CONTRACT §2.4).\n *\n * - Duplicate ids are REFUSED, never overwritten (rich-document's registry\n * silently overwrote — R12). A duplicate known at registration throws; one\n * that only appears when a provider yields it is dropped and captured.\n * - Eligibility is one pure pass over the resolved click target, under\n * \"restrict, never relax\": a host may remove ids or categories, never add.\n * - R1: an action that does not apply is ABSENT. Only the six universal verbs\n * (category \"clipboard\") may answer `unavailable-verb`, with a sentence; any\n * other action answering it is a contract break → absent + diagnostics.\n */\nimport type { AlchemyHostPorts, Json } from \"../ports\";\nimport {\n UNIVERSAL_VERBS,\n type Action,\n type ActionProvider,\n type ActionRegistry,\n type ClickTarget,\n type Eligibility,\n type ResolveRestriction,\n type ResolvedAction,\n} from \"./types\";\n\nexport class DuplicateActionError extends Error {\n readonly actionId: string;\n readonly providers: readonly [string, string];\n constructor(actionId: string, first: string, second: string) {\n super(\n first === second\n ? `Action \"${actionId}\" is yielded twice by provider \"${first}\". Ids are unique; the second was refused.`\n : `Action \"${actionId}\" is registered by provider \"${first}\" and again by \"${second}\". Ids are unique; \"${second}\"'s copy was refused.`,\n );\n this.name = \"DuplicateActionError\";\n this.actionId = actionId;\n this.providers = [first, second];\n }\n}\n\nexport class DuplicateProviderError extends Error {\n readonly providerId: string;\n constructor(providerId: string) {\n super(`Action provider \"${providerId}\" is already registered. Unregister it first; nothing was overwritten.`);\n this.name = \"DuplicateProviderError\";\n this.providerId = providerId;\n }\n}\n\nexport interface ActionRegistryOptions {\n /** `diagnostics` is required (nothing fails silently); `auth` decides whether requiresAuth actions reach a signed-out person. */\n ports: Pick<AlchemyHostPorts, \"diagnostics\" | \"auth\">;\n}\n\nconst VERBS = new Set<string>(UNIVERSAL_VERBS);\n\nfunction capture(ports: ActionRegistryOptions[\"ports\"], error: unknown, detail: Json): void {\n try {\n ports.diagnostics.capture(error, { area: \"actions\", detail });\n } catch {\n // A broken diagnostics port must never take the menu down with it.\n }\n}\n\n/**\n * Eligibility of one action at one target — the ONE pure pass (CONTRACT §1.6).\n * Exported for providers' tests; hosts call `registry.resolve`.\n */\nexport function computeEligibility(\n action: Action,\n target: ClickTarget,\n options: { restrict?: ResolveRestriction; authResumable: boolean },\n): Eligibility | { status: \"contract-break\"; reason: string } {\n const restrict = options.restrict;\n if (target.excludedActionIds.has(action.id)) return { status: \"absent\" };\n if (restrict?.exclude?.includes(action.id)) return { status: \"absent\" };\n if (restrict?.categories && !restrict.categories.includes(action.category)) return { status: \"absent\" };\n if (action.writes && action.writes.length > 0) {\n // A read-only target offers nothing that would change the record.\n if (target.readOnly) return { status: \"absent\" };\n const writable = new Set(target.writable.map((w) => w.name));\n if (!action.writes.some((name) => writable.has(name))) return { status: \"absent\" };\n }\n if (action.requiresAuth && !target.auth.authenticated && !options.authResumable) {\n // Sign-in-then-resume is the capability; without the auth port it is absent (N8).\n return { status: \"absent\" };\n }\n const own = action.eligible(target);\n if (own.status !== \"unavailable-verb\") return own;\n if (action.category !== \"clipboard\" || !VERBS.has(own.verb)) {\n return {\n status: \"contract-break\",\n reason: `Action \"${action.id}\" answered unavailable-verb but only the six universal verbs (category \"clipboard\") may grey; it is absent instead.`,\n };\n }\n if (!own.sentence.trim()) {\n return {\n status: \"contract-break\",\n reason: `Universal verb \"${action.id}\" greyed without a sentence; a greyed verb must say why.`,\n };\n }\n return own;\n}\n\ninterface Entry {\n provider: ActionProvider;\n declared: readonly string[];\n}\n\nexport function createActionRegistry(options: ActionRegistryOptions): ActionRegistry {\n const ports = options.ports;\n const entries = new Map<string, Entry>();\n const listeners = new Set<() => void>();\n const notify = () => {\n for (const l of [...listeners]) l();\n };\n\n return {\n register(provider) {\n if (entries.has(provider.id)) throw new DuplicateProviderError(provider.id);\n const declared = provider.declaredIds?.() ?? [];\n const own = new Set<string>();\n for (const id of declared) {\n if (own.has(id)) throw new DuplicateActionError(id, provider.id, provider.id);\n own.add(id);\n for (const [otherId, other] of entries) {\n if (other.declared.includes(id)) throw new DuplicateActionError(id, otherId, provider.id);\n }\n }\n const entry: Entry = { provider, declared };\n entries.set(provider.id, entry);\n notify();\n return () => {\n if (entries.get(provider.id) === entry) {\n entries.delete(provider.id);\n notify();\n }\n };\n },\n\n async resolve(target, restrict) {\n const authResumable = Boolean(ports.auth);\n const seen = new Map<string, string>();\n const out: ResolvedAction[] = [];\n for (const [providerId, { provider }] of [...entries]) {\n let actions: readonly Action[];\n try {\n actions = await provider.actions(target);\n } catch (error) {\n capture(ports, error, { providerId, stage: \"actions\" });\n continue;\n }\n for (const raw of actions) {\n const first = seen.get(raw.id);\n if (first !== undefined) {\n capture(ports, new DuplicateActionError(raw.id, first, providerId), { actionId: raw.id });\n continue;\n }\n seen.set(raw.id, providerId);\n const action: Action = raw.provider === providerId ? raw : { ...raw, provider: providerId };\n let eligibility: ReturnType<typeof computeEligibility>;\n try {\n eligibility = computeEligibility(action, target, { restrict: restrict ?? {}, authResumable });\n } catch (error) {\n capture(ports, error, { actionId: action.id, stage: \"eligible\" });\n continue;\n }\n if (eligibility.status === \"contract-break\") {\n capture(ports, new Error(eligibility.reason), { actionId: action.id, stage: \"eligible\" });\n continue;\n }\n if (eligibility.status === \"absent\") continue;\n out.push({ action, eligibility });\n }\n }\n return out;\n },\n\n providers() {\n return [...entries.keys()];\n },\n\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n\n/** A provider over a fixed list; its ids are declared up front so duplicates fail at registration. */\nexport function staticActionProvider(\n id: string,\n actions: readonly Action[],\n tier: ActionProvider[\"tier\"] = \"T0\",\n): ActionProvider {\n const list = actions.map((a) => (a.provider === id ? a : { ...a, provider: id }));\n return {\n id,\n tier,\n declaredIds: () => list.map((a) => a.id),\n actions: () => list,\n };\n}\n\n/** Always-available eligibility, for actions with no rule of their own. */\nexport const alwaysEligible = (): Eligibility => ({ status: \"available\" });\n","/**\n * The shared React context (T0-safe: imports only React and types). Shared\n * through globalThis so every per-layout bundle reads the same provider.\n */\nimport * as React from \"react\";\nimport type { ActionRegistry } from \"../actions/types\";\nimport type { AlchemyHostPorts } from \"../ports\";\n\nexport interface AlchemyActionsValue {\n registry: ActionRegistry;\n ports: AlchemyHostPorts;\n}\n\nconst KEY = Symbol.for(\"ai-matrx.alchemy.actions-context\");\ntype Holder = { [KEY]?: React.Context<AlchemyActionsValue | null> };\nexport const AlchemyActionsContext: React.Context<AlchemyActionsValue | null> =\n ((globalThis as Holder)[KEY] ??= React.createContext<AlchemyActionsValue | null>(null));\n\nexport function useAlchemyActions(): AlchemyActionsValue {\n const value = React.useContext(AlchemyActionsContext);\n if (!value) throw new Error(\"Alchemy actions: render inside <AlchemyActionsProvider> (@ai-matrx/alchemy/react/host).\");\n return value;\n}\n"],"mappings":"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","names":["React","editable"]}
|