@embedpdf/plugin-interaction 3.0.0-next.14 → 3.0.0-next.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/index.js CHANGED
@@ -1,104 +1,255 @@
1
- import { n as InteractionToken, r as samplePointOn, t as FeedbackToken } from "./contract-CrM0i9pQ.js";
2
- import { definePlugin } from "@embedpdf/core";
3
- //#region src/capability.ts
1
+ import { n as InteractionToken, r as FeedbackToken, t as INTERACTION_DEFAULTS } from "./contract-CmEMPz9L.js";
2
+ import { InteractionToken as InteractionToken$1, samplePointOn } from "./contract/host.js";
3
+ import { PluginError, definePlugin, defineState, memo } from "@embedpdf/core";
4
+ //#region src/model.ts
5
+ /** A document starts on `pointer`; the controller then activates the `defaultTool` setting. */
6
+ const initialInteractionState = () => ({
7
+ activeToolId: "pointer",
8
+ toolStack: [],
9
+ cursor: "default"
10
+ });
11
+ /** Activate a tool and forget every pushed one. */
12
+ function activateTool(state, toolId) {
13
+ if (state.activeToolId === toolId && state.toolStack.length === 0) return state;
14
+ return {
15
+ ...state,
16
+ activeToolId: toolId,
17
+ toolStack: []
18
+ };
19
+ }
20
+ /** Activate a tool for a moment, saving the active one for {@link popTool}. */
21
+ function pushTool(state, toolId) {
22
+ return {
23
+ ...state,
24
+ toolStack: [...state.toolStack, state.activeToolId],
25
+ activeToolId: toolId
26
+ };
27
+ }
28
+ /** Activate the tool saved last; an empty stack changes nothing. */
29
+ function popTool(state) {
30
+ if (state.toolStack.length === 0) return state;
31
+ return {
32
+ ...state,
33
+ toolStack: state.toolStack.slice(0, -1),
34
+ activeToolId: state.toolStack[state.toolStack.length - 1]
35
+ };
36
+ }
37
+ function setCursor(state, cursor) {
38
+ return state.cursor === cursor ? state : {
39
+ ...state,
40
+ cursor
41
+ };
42
+ }
43
+ /**
44
+ * The two built-in tools. Features add tools via `registerTool`. `enables`
45
+ * is the composition seam:
46
+ * pointer → text selection + annotation editing + marquee selection
47
+ * pan → scrolling (contributed by Stage) + annotation editing, no text select
48
+ * Both carry `form-fill` and `link-nav`: filling forms and following links is
49
+ * the resting state of a viewer (Acrobat's hand tool does both).
50
+ */
51
+ const builtinTools = () => [{
52
+ id: "pointer",
53
+ cursor: "default",
54
+ enables: /* @__PURE__ */ new Set([
55
+ "text-select",
56
+ "annotation-edit",
57
+ "annotation-marquee",
58
+ "form-fill",
59
+ "link-nav"
60
+ ])
61
+ }, {
62
+ id: "pan",
63
+ cursor: "grab",
64
+ gapCursor: "grab",
65
+ enables: /* @__PURE__ */ new Set([
66
+ "scroll",
67
+ "annotation-edit",
68
+ "form-fill",
69
+ "link-nav"
70
+ ])
71
+ }];
72
+ //#endregion
73
+ //#region src/controller.ts
74
+ /** A tool's own pointer methods take a press before every handler a plugin registers. */
75
+ const OWN_POINTER_PRIORITY = 1e3;
76
+ const NO_TAGS = /* @__PURE__ */ new Set();
4
77
  /**
5
- * The interaction hub. Tools + handlers + the captured-gesture owner + cursor
6
- * claims live in this closure (runtime registries, not serializable state); the
7
- * reducer state holds only the active tool id and the resolved cursor, so the UI
8
- * can react to them. The router (`dispatch`) is the heart: on down, the first
9
- * eligible handler (by priority) to return true OWNS the gesture; move/up route
10
- * to it; with no owner, move is hover (cursor feedback).
78
+ * The interaction hub. Tools, handlers, the captured-gesture owner and cursor
79
+ * claims are registries in this closure; the state holds only the active
80
+ * tool, the tool stack and the resolved cursor, so UI can react. Registering
81
+ * or removing a tool wakes readers with `ctx.notify()`.
11
82
  *
12
- * Cursor arbitration, top to bottom: the highest-priority CLAIM (hover feedback
13
- * — 'move' over an annotation, 'text' over text) → over a page, the tool's
14
- * declared cursor → over a gap, the tool's `gapCursor` (default the neutral
15
- * arrow). The winning KEYWORD (claim or base, never gap) is then restyled
16
- * through the armed tool's skin when it maps that keyword. One resolver, so an
17
- * image cursor can never shadow hover feedback or outlive the page under the
18
- * pointer.
83
+ * Cursor arbitration, top to bottom: the highest-priority claim (hover
84
+ * feedback) → over a page, the tool's declared cursor → over a gap, the
85
+ * tool's `gapCursor`. The winning keyword is replaced through the active
86
+ * tool's cursors (`setToolCursor`) when they name that keyword.
19
87
  */
20
- function createInteractionCapability(ctx, builtinTools) {
88
+ function createInteractionController(ctx, builtinTools) {
89
+ const settings = ctx.settings();
21
90
  const tools = /* @__PURE__ */ new Map();
22
- for (const t of builtinTools) tools.set(t.id, t);
91
+ /** The handler each tool with its own pointer methods brings, registered beside the plugins' handlers. */
92
+ const ownHandlers = /* @__PURE__ */ new Map();
93
+ /** Bumped on every tool registry change: the input of the tool list. */
94
+ let toolsVersion = 0;
23
95
  const handlers = [];
96
+ const handlerSources = /* @__PURE__ */ new Map();
24
97
  const claims = /* @__PURE__ */ new Map();
25
- const cursorSkins = /* @__PURE__ */ new Map();
26
- const toolCbs = /* @__PURE__ */ new Set();
98
+ const toolCursors = /* @__PURE__ */ new Map();
27
99
  let owner = null;
100
+ /** The gesture `owner` took, as its events report it. */
101
+ let gesture = null;
102
+ /** Whether the last dispatched sample hit a page: gaps fall back to `gapCursor`. */
28
103
  let overPage = false;
104
+ const toolChanged = ctx.events.source();
105
+ const gestureStarted = ctx.events.source();
106
+ const gestureEnded = ctx.events.source();
107
+ const gestureCancelled = ctx.events.source();
108
+ const cursorChanged = ctx.events.source();
109
+ ctx.state.onChange(({ previous, next }) => {
110
+ if (previous.cursor !== next.cursor) cursorChanged.emit({ cursor: next.cursor });
111
+ });
112
+ const state = () => ctx.state.get();
29
113
  const toolOf = (id) => tools.get(id) ?? {
30
114
  id,
31
115
  cursor: "default",
32
- enables: /* @__PURE__ */ new Set()
116
+ enables: NO_TAGS
33
117
  };
34
- const active = () => toolOf(ctx.getState().activeToolId);
118
+ const active = () => toolOf(state().activeToolId);
119
+ const listTools = memo(() => [toolsVersion], (_version) => [...tools.values()]);
35
120
  const resolveCursor = () => {
36
121
  let top = null;
37
- for (const c of claims.values()) if (!top || c.priority > top.priority) top = c;
122
+ for (const claim of claims.values()) if (!top || claim.priority > top.priority) top = claim;
38
123
  const tool = active();
39
- const skin = cursorSkins.get(tool.id);
40
- if (top) return skin?.[top.cursor] ?? top.cursor;
124
+ const cursors = toolCursors.get(tool.id);
125
+ if (top) return cursors?.[top.cursor] ?? top.cursor;
41
126
  if (!overPage) return tool.gapCursor ?? "default";
42
- return skin?.[tool.cursor] ?? tool.cursor;
127
+ return cursors?.[tool.cursor] ?? tool.cursor;
43
128
  };
44
- const syncCursor = () => {
45
- const next = resolveCursor();
46
- if (next !== ctx.getState().cursor) ctx.dispatch({
47
- type: "SET_CURSOR",
48
- cursor: next
49
- });
129
+ const syncCursor = () => ctx.state.update(setCursor, resolveCursor());
130
+ const removeHandler = (handler) => {
131
+ const index = handlers.indexOf(handler);
132
+ if (index >= 0) handlers.splice(index, 1);
133
+ handlerSources.delete(handler);
134
+ if (owner === handler) {
135
+ owner = null;
136
+ gesture = null;
137
+ }
50
138
  };
51
- const handlerSources = /* @__PURE__ */ new Map();
139
+ const addHandler = (handler, source) => {
140
+ handlers.push(handler);
141
+ if (source !== void 0) handlerSources.set(handler, source);
142
+ return () => removeHandler(handler);
143
+ };
144
+ /** Take a tool out of the registry, with the handler its pointer methods brought. */
145
+ const removeTool = (tool) => {
146
+ tools.delete(tool.id);
147
+ const handler = ownHandlers.get(tool);
148
+ ownHandlers.delete(tool);
149
+ if (handler) removeHandler(handler);
150
+ };
151
+ /**
152
+ * Put a tool in the registry without waking readers, and return the stored
153
+ * tool (tags filled in). A taken id is refused unless `replace`.
154
+ */
155
+ const addTool = (input, replace) => {
156
+ const taken = tools.get(input.id);
157
+ if (taken && !replace) throw new PluginError("conflict", "interaction", `tool '${input.id}' is already registered; pass { replace: true } to replace it`);
158
+ if (taken) removeTool(taken);
159
+ const tool = input.enables ? input : {
160
+ ...input,
161
+ enables: NO_TAGS
162
+ };
163
+ tools.set(tool.id, tool);
164
+ const handler = ownPointerHandler(tool);
165
+ if (handler) {
166
+ ownHandlers.set(tool, handler);
167
+ addHandler(handler);
168
+ }
169
+ return tool;
170
+ };
171
+ const toolsChanged = () => {
172
+ toolsVersion += 1;
173
+ ctx.notify();
174
+ };
175
+ /** The registration's remover: it removes this registration only, never a later one with its id. */
176
+ const removerOf = (tool) => () => {
177
+ if (tools.get(tool.id) !== tool) return;
178
+ removeTool(tool);
179
+ toolCursors.delete(tool.id);
180
+ toolsChanged();
181
+ };
182
+ for (const tool of builtinTools) addTool(tool, true);
183
+ let removeSettingTools = settings.get().tools.map((tool) => removerOf(addTool(tool, true)));
184
+ settings.api.onSettingsChanged(({ changed }) => {
185
+ if (!changed.includes("tools")) return;
186
+ for (const remove of removeSettingTools) remove();
187
+ removeSettingTools = settings.get().tools.map((tool) => removerOf(addTool(tool, true)));
188
+ toolsChanged();
189
+ });
190
+ ctx.state.update(activateTool, settings.get().defaultTool);
191
+ syncCursor();
192
+ /** Lens scoping: a handler registered with a `source` only sees samples stamped with it. */
52
193
  const eligible = (source) => {
53
194
  const tool = active();
54
- return handlers.filter((h) => {
55
- const hs = handlerSources.get(h);
56
- return hs === void 0 || source === void 0 || hs === source;
57
- }).filter((h) => h.enabledFor(tool)).sort((a, b) => b.priority - a.priority);
195
+ return handlers.filter((handler) => {
196
+ const handlerSource = handlerSources.get(handler);
197
+ return handlerSource === void 0 || source === void 0 || handlerSource === source;
198
+ }).filter((handler) => handler.enabledFor(tool)).sort((left, right) => right.priority - left.priority);
58
199
  };
59
- return {
60
- activeTool: active,
61
- activeToolId: () => ctx.getState().activeToolId,
62
- cursor: () => ctx.getState().cursor,
63
- tools: () => [...tools.values()],
64
- activateTool: (id) => {
65
- if (!tools.has(id)) throw new Error(`[interaction] unknown tool '${id}'`);
66
- owner = null;
67
- claims.clear();
68
- ctx.dispatch({
69
- type: "SET_TOOL",
70
- toolId: id
71
- });
72
- syncCursor();
73
- toolCbs.forEach((cb) => cb());
74
- },
75
- onToolChange: (cb) => {
76
- toolCbs.add(cb);
77
- return () => toolCbs.delete(cb);
200
+ /**
201
+ * Activate a tool through `transition`. Every activation is announced,
202
+ * including activating the active tool again.
203
+ */
204
+ function arm(id, transition) {
205
+ if (!tools.has(id)) throw new PluginError("not-found", "interaction", `unknown tool '${id}'`);
206
+ const previousToolId = state().activeToolId;
207
+ owner = null;
208
+ gesture = null;
209
+ claims.clear();
210
+ ctx.state.update(transition);
211
+ syncCursor();
212
+ toolChanged.emit({
213
+ toolId: id,
214
+ previousToolId
215
+ });
216
+ }
217
+ return { api: {
218
+ ...settings.api,
219
+ getActiveTool: active,
220
+ getActiveToolId: () => state().activeToolId,
221
+ getDefaultToolId: () => settings.get().defaultTool,
222
+ listTools,
223
+ getTool: (id) => tools.get(id) ?? null,
224
+ hasTool: (id) => tools.has(id),
225
+ activateTool: (id) => arm(id, (current) => activateTool(current, id)),
226
+ activateDefaultTool: () => {
227
+ const id = settings.get().defaultTool;
228
+ arm(id, (current) => activateTool(current, id));
78
229
  },
79
- registerTool: (tool) => {
80
- tools.set(tool.id, tool);
81
- return () => {
82
- tools.delete(tool.id);
83
- cursorSkins.delete(tool.id);
84
- };
230
+ pushTool: (id) => arm(id, (current) => pushTool(current, id)),
231
+ popTool: () => {
232
+ const stack = state().toolStack;
233
+ if (stack.length === 0) return;
234
+ arm(stack[stack.length - 1], popTool);
85
235
  },
86
- setToolCursor: (id, skin) => {
87
- if (skin === null) cursorSkins.delete(id);
88
- else cursorSkins.set(id, skin);
236
+ setToolCursor: (id, cursors) => {
237
+ if (cursors === null) toolCursors.delete(id);
238
+ else toolCursors.set(id, cursors);
89
239
  syncCursor();
90
240
  },
91
- registerHandler: (handler, options) => {
92
- handlers.push(handler);
93
- if (options?.source !== void 0) handlerSources.set(handler, options.source);
94
- return () => {
95
- const i = handlers.indexOf(handler);
96
- if (i >= 0) handlers.splice(i, 1);
97
- handlerSources.delete(handler);
98
- if (owner === handler) owner = null;
99
- };
241
+ registerTool: (tool, options) => {
242
+ const stored = addTool(tool, options?.replace ?? false);
243
+ toolsChanged();
244
+ return removerOf(stored);
100
245
  },
101
- setCursor: (token, cursor, priority = 0) => {
246
+ onToolChanged: toolChanged.on,
247
+ onGestureStarted: gestureStarted.on,
248
+ onGestureEnded: gestureEnded.on,
249
+ onGestureCancelled: gestureCancelled.on,
250
+ activeToolEnables: (behavior) => active().enables.has(behavior),
251
+ registerHandler: (handler, options) => addHandler(handler, options?.source),
252
+ claimCursor: (token, cursor, priority = 0) => {
102
253
  if (cursor === null) claims.delete(token);
103
254
  else claims.set(token, {
104
255
  cursor,
@@ -106,11 +257,14 @@ function createInteractionCapability(ctx, builtinTools) {
106
257
  });
107
258
  syncCursor();
108
259
  },
260
+ getCursor: () => state().cursor,
261
+ hasCursorClaim: () => claims.size > 0,
262
+ onCursorChanged: cursorChanged.on,
109
263
  wouldClaimTouch: (sample) => {
110
- for (const h of eligible(sample.source)) if (h.claimsTouch?.(sample)) return true;
264
+ for (const handler of eligible(sample.source)) if (handler.claimsTouch?.(sample)) return true;
111
265
  return false;
112
266
  },
113
- dispatch: (sample) => {
267
+ dispatchPointer: (sample) => {
114
268
  const nowOverPage = sample.page != null;
115
269
  if (nowOverPage !== overPage) {
116
270
  overPage = nowOverPage;
@@ -118,108 +272,140 @@ function createInteractionCapability(ctx, builtinTools) {
118
272
  }
119
273
  if (sample.phase === "down") {
120
274
  owner = null;
121
- for (const h of eligible(sample.source)) if (h.onDown(sample)) {
122
- owner = h;
275
+ gesture = null;
276
+ for (const handler of eligible(sample.source)) if (handler.onDown(sample)) {
277
+ owner = handler;
278
+ gesture = {
279
+ toolId: state().activeToolId,
280
+ page: sample.page?.ref ?? null,
281
+ pointerType: sample.pointerType ?? "mouse"
282
+ };
283
+ gestureStarted.emit(gesture);
123
284
  break;
124
285
  }
125
286
  } else if (sample.phase === "move") if (owner) owner.onMove?.(sample);
126
- else for (const h of eligible(sample.source)) h.onHover?.(sample);
127
- else if (sample.phase === "cancel") {
128
- const o = owner;
129
- owner = null;
130
- if (o) (o.onCancel ?? o.onUp)?.call(o, sample);
131
- } else {
132
- owner?.onUp?.(sample);
287
+ else for (const handler of eligible(sample.source)) handler.onHover?.(sample);
288
+ else {
289
+ const captured = owner;
290
+ const taken = gesture;
133
291
  owner = null;
292
+ gesture = null;
293
+ if (!captured || !taken) return;
294
+ if (sample.phase === "cancel") {
295
+ (captured.onCancel ?? captured.onUp)?.call(captured, sample);
296
+ gestureCancelled.emit(taken);
297
+ } else {
298
+ captured.onUp?.(sample);
299
+ gestureEnded.emit(taken);
300
+ }
134
301
  }
135
302
  }
303
+ } };
304
+ }
305
+ /**
306
+ * The handler a tool's own pointer methods bring, or null for a tool without
307
+ * them: it is live only while that tool is active, and turns samples into
308
+ * {@link ToolPointerEvent}s in page coordinates. A gesture keeps the page it
309
+ * started on, so a drag that leaves the page goes on in that page's
310
+ * coordinates.
311
+ */
312
+ function ownPointerHandler(tool) {
313
+ if (!tool.onPointerDown && !tool.onHover) return null;
314
+ let home = null;
315
+ let last = null;
316
+ const eventOf = (page, point, sample) => ({
317
+ page,
318
+ point,
319
+ modifiers: sample.modifiers,
320
+ pointerType: sample.pointerType ?? "mouse"
321
+ });
322
+ /** The sample on the gesture's page, or the last event when the source can't project it there. */
323
+ const follow = (sample) => {
324
+ if (!home) return null;
325
+ const point = samplePointOn(sample, home);
326
+ if (point) last = eventOf(home, point, sample);
327
+ return last;
328
+ };
329
+ const end = (sample) => {
330
+ const event = follow(sample);
331
+ home = null;
332
+ last = null;
333
+ if (event) tool.onPointerUp?.(event);
334
+ };
335
+ return {
336
+ id: `tool:${tool.id}`,
337
+ priority: OWN_POINTER_PRIORITY,
338
+ enabledFor: (active) => active === tool,
339
+ onDown: (sample) => {
340
+ if (!sample.page || !tool.onPointerDown) return false;
341
+ const event = eventOf(sample.page.ref, sample.page.point, sample);
342
+ if (tool.onPointerDown(event) !== true) return false;
343
+ home = sample.page.ref;
344
+ last = event;
345
+ return true;
346
+ },
347
+ onMove: (sample) => {
348
+ const event = follow(sample);
349
+ if (event) tool.onPointerMove?.(event);
350
+ },
351
+ onUp: end,
352
+ onCancel: end,
353
+ onHover: (sample) => {
354
+ if (sample.page) tool.onHover?.(eventOf(sample.page.ref, sample.page.point, sample));
355
+ }
136
356
  };
137
357
  }
138
358
  //#endregion
139
- //#region src/reducer.ts
140
- const initialInteractionState = (config) => ({
141
- activeToolId: config.defaultTool ?? "pointer",
142
- cursor: "default"
143
- });
144
- const interactionReducer = (state, action) => {
145
- switch (action.type) {
146
- case "SET_TOOL": return state.activeToolId === action.toolId ? state : {
147
- ...state,
148
- activeToolId: action.toolId
149
- };
150
- case "SET_CURSOR": return state.cursor === action.cursor ? state : {
151
- ...state,
152
- cursor: action.cursor
153
- };
154
- default: return state;
155
- }
156
- };
157
- //#endregion
158
359
  //#region src/interaction.plugin.ts
159
360
  /**
160
- * The two built-in tools. Features ADD tools via `registerTool` (a draw tool, a
161
- * redact tool…). `enables` is the composition seam:
162
- * pointer → text selection + annotation editing + annotation marquee selection
163
- * pan → scrolling (contributed by Stage) + annotation editing, NO text select
164
- *
165
- * Both carry `form-fill`: filling a form is the RESTING state of a viewer
166
- * (Acrobat's hand tool fills forms too), so widgets are fill-controls under the
167
- * default tools and only become geometry-editable under a form-design tool.
168
- * Both carry `link-nav` for the same reason: links navigate under the default
169
- * tools (Acrobat behaviour) and only become geometry-editable under the link
170
- * tool. Tags are opaque to the hub — without the form/link plugins they're inert.
361
+ * The interaction hub: document-scoped, depends on nothing. Feature plugins
362
+ * `require` this token; the Stage `optional`-ly contributes a scroll handler.
363
+ * `config` is the settings the app registers, over {@link INTERACTION_DEFAULTS}.
171
364
  */
172
- const builtinTools = () => [{
173
- id: "pointer",
174
- cursor: "default",
175
- enables: /* @__PURE__ */ new Set([
176
- "text-select",
177
- "annotation-edit",
178
- "annotation-marquee",
179
- "form-fill",
180
- "link-nav"
181
- ])
182
- }, {
183
- id: "pan",
184
- cursor: "grab",
185
- gapCursor: "grab",
186
- enables: /* @__PURE__ */ new Set([
187
- "scroll",
188
- "annotation-edit",
189
- "form-fill",
190
- "link-nav"
191
- ])
192
- }];
193
- /**
194
- * The interaction hub plugin — document-scoped, depends on nothing. Every feature
195
- * plugin (selection, annotation, forms, redaction) `requires` this token; Stage
196
- * `optional`-ly contributes a scroll handler. Works with `<Stage>` or a standalone
197
- * `<PageView>` — it only routes page-space pointer samples.
198
- */
199
- const interactionPlugin = (config = {}) => definePlugin({
365
+ const interactionPlugin = (config) => definePlugin({
200
366
  id: "interaction",
201
- token: InteractionToken,
367
+ token: InteractionToken$1,
202
368
  scope: "document",
203
- initialState: () => initialInteractionState(config),
204
- reduce: interactionReducer,
205
- capability: (ctx) => createInteractionCapability(ctx, builtinTools())
369
+ state: initialInteractionState,
370
+ settings: {
371
+ defaults: INTERACTION_DEFAULTS,
372
+ registered: config
373
+ },
374
+ create: (ctx) => createInteractionController(ctx, builtinTools())
375
+ });
376
+ //#endregion
377
+ //#region src/state.ts
378
+ /**
379
+ * The tools page's State table as code: what `useInteractionState()` returns,
380
+ * and the same fields in every other framework. With no document there is no
381
+ * active tool and no tool to switch to.
382
+ */
383
+ const interactionState = defineState(InteractionToken, {
384
+ read: (interaction) => ({
385
+ activeToolId: interaction.getActiveToolId(),
386
+ tools: interaction.listTools()
387
+ }),
388
+ empty: {
389
+ activeToolId: null,
390
+ tools: []
391
+ }
206
392
  });
207
393
  //#endregion
208
394
  //#region src/feedback.ts
209
395
  /**
210
- * Platform feedback (haptics) — the OUTPUT half of the interaction seam:
396
+ * Platform feedback (haptics) — the output half of the interaction seam:
211
397
  * pointers come in, feedback goes back out to the device.
212
398
  *
213
- * ONE workspace-scoped provider, MANY consumers: any plugin lists
399
+ * One workspace-scoped provider, many consumers: any plugin lists
214
400
  * {@link FeedbackToken} as an optional dependency and calls the capability at
215
- * its semantic moments (the selection handler on a long-press word-select; an
216
- * annotation pickup or a snap later). The plugin itself stays DOM-free — the
217
- * PROVIDER is injected from outside (`@embedpdf/web` ships `vibrationFeedback`
218
- * and a WKWebView bridge; native shells bring their own), exactly like the
219
- * stage's Scheduler seam: host dependencies enter through explicit injection,
220
- * never a hidden global.
401
+ * its semantic moments (the selection handler on a long-press word-select).
402
+ * The plugin itself stays DOM-free — the provider is injected from outside
403
+ * (`@embedpdf/web` ships `vibrationFeedback` and a WKWebView bridge; native
404
+ * shells bring their own), as the host's clock enters the kernel
405
+ * (`createKernel({ clock })`): host dependencies enter through explicit
406
+ * injection, never a hidden global.
221
407
  *
222
- * The vocabulary is deliberately the PLATFORM's haptic taxonomy — iOS's three
408
+ * The vocabulary is deliberately the platform's haptic taxonomy — iOS's three
223
409
  * generator families, which Android mirrors in `HapticFeedbackConstants` and
224
410
  * the web approximates with vibration patterns — not app vocabulary. A verb
225
411
  * belongs here only if a provider would map it to a physically different
@@ -233,18 +419,16 @@ const NOOP = {
233
419
  notify() {}
234
420
  };
235
421
  /**
236
- * Register the workspace's ONE feedback provider. Stateless: the capability IS
237
- * the provider (or the no-op).
422
+ * Register the workspace's one feedback provider. Stateless: the capability
423
+ * is the provider (or the no-op).
238
424
  */
239
425
  const feedbackPlugin = (options = {}) => definePlugin({
240
426
  id: "feedback",
241
427
  scope: "workspace",
242
428
  token: FeedbackToken,
243
- initialState: () => ({}),
244
- reduce: (state) => state,
245
- capability: () => options.provider ?? NOOP
429
+ create: () => ({ api: options.provider ?? NOOP })
246
430
  });
247
431
  //#endregion
248
- export { FeedbackToken, InteractionToken, builtinTools, createInteractionCapability, feedbackPlugin, initialInteractionState, interactionPlugin, interactionReducer, samplePointOn };
432
+ export { FeedbackToken, INTERACTION_DEFAULTS, InteractionToken, feedbackPlugin, interactionPlugin, interactionState };
249
433
 
250
434
  //# sourceMappingURL=index.js.map