@floegence/redevplugin-ui 3.0.21 → 3.0.23

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.
@@ -33,6 +33,7 @@ export declare const opaqueSurfaceRenderLimits: {
33
33
  readonly max_text_length: 65536;
34
34
  readonly max_attribute_value_length: 4096;
35
35
  readonly max_form_fields: 128;
36
+ readonly max_keyboard_bindings: 128;
36
37
  readonly max_canvas_count: 4;
37
38
  readonly max_canvas_dimension: 4096;
38
39
  readonly max_canvas_total_pixels: 16777216;
@@ -215,6 +215,7 @@ export const opaqueSurfaceRenderLimits = {
215
215
  "max_text_length": 65536,
216
216
  "max_attribute_value_length": 4096,
217
217
  "max_form_fields": 128,
218
+ "max_keyboard_bindings": 128,
218
219
  "max_canvas_count": 4,
219
220
  "max_canvas_dimension": 4096,
220
221
  "max_canvas_total_pixels": 16777216,
package/dist/plugin.d.ts CHANGED
@@ -2,5 +2,5 @@ export { PluginBridgeClient } from "./surface.js";
2
2
  export { PluginBridgeError } from "./errors.js";
3
3
  export { pluginAPIMajor } from "./contracts.gen.js";
4
4
  export { callCapabilityOperation, callCapabilityStream, callCapabilitySync, isCapabilityBusinessError, } from "./capability-client.js";
5
- export type { BridgeLifecycleEvent, MessagePortLike, PluginBridgeClientOptions, PluginBridgeRequestOptions, PluginCanvasAccessibilityState, PluginCanvasInputEvent, PluginCanvasKeyEvent, PluginCanvasPointerEvent, PluginCanvasResizeEvent, PluginCanvasSurface, PluginJSONObject, PluginJSONValue, PluginMethodResult, PluginExecutionFailureCode, PluginExecutionSnapshot, PluginExecutionProgress, PluginExecutionEvent, PluginExecutionEventList, PluginSurfaceContext, PluginUIActionEvent, PluginUIAttributeValue, PluginUIElementVNode, PluginUIPatchOperation, PluginUIVNode, } from "./surface.js";
5
+ export type { BridgeLifecycleEvent, MessagePortLike, PluginBridgeClientOptions, PluginBridgeRequestOptions, PluginCanvasAccessibilityState, PluginCanvasInputEvent, PluginCanvasKeyEvent, PluginCanvasPointerEvent, PluginCanvasWheelEvent, PluginCanvasResizeEvent, PluginCanvasSurface, PluginSurfaceKeyboardBinding, PluginSurfaceKeyboardEvent, PluginSurfaceKeyboardTargetKind, PluginJSONObject, PluginJSONValue, PluginMethodResult, PluginExecutionFailureCode, PluginExecutionSnapshot, PluginExecutionProgress, PluginExecutionEvent, PluginExecutionEventList, PluginSurfaceContext, PluginUIActionEvent, PluginUIAttributeValue, PluginUIElementVNode, PluginUIPatchOperation, PluginUIVNode, } from "./surface.js";
6
6
  export type { PluginCapabilitySchema, PluginCapabilityEffect, PluginCapabilityOperationContract, PluginCapabilityStreamContract, PluginCapabilitySyncContract, PluginCapabilityStreamEvent, PluginCapabilityStreamReadResult, PluginExecution, PluginExecutionTerminalStatus, PluginExecutionWaitOptions, PluginStream, } from "./capability-client.js";
package/dist/surface.d.ts CHANGED
@@ -136,7 +136,47 @@ export type PluginCanvasPointerEvent = {
136
136
  y: number;
137
137
  pressure: number;
138
138
  };
139
- export type PluginCanvasInputEvent = PluginCanvasFocusEvent | PluginCanvasResizeEvent | PluginCanvasKeyEvent | PluginCanvasPointerEvent;
139
+ export type PluginCanvasWheelEvent = {
140
+ type: "wheel";
141
+ x: number;
142
+ y: number;
143
+ deltaX: number;
144
+ deltaY: number;
145
+ deltaMode: 0 | 1 | 2;
146
+ altKey: boolean;
147
+ ctrlKey: boolean;
148
+ metaKey: boolean;
149
+ shiftKey: boolean;
150
+ };
151
+ export type PluginCanvasInputEvent = PluginCanvasFocusEvent | PluginCanvasResizeEvent | PluginCanvasKeyEvent | PluginCanvasPointerEvent | PluginCanvasWheelEvent;
152
+ export type PluginSurfaceKeyboardTargetKind = "canvas" | "editable" | "control" | "surface";
153
+ export type PluginSurfaceKeyboardBinding = {
154
+ id: string;
155
+ event: "keydown" | "keyup";
156
+ code: string;
157
+ repeat: boolean;
158
+ altKey: boolean;
159
+ ctrlKey: boolean;
160
+ metaKey: boolean;
161
+ shiftKey: boolean;
162
+ targetKey: string | null;
163
+ targetKind: PluginSurfaceKeyboardTargetKind;
164
+ };
165
+ export type PluginSurfaceKeyboardEvent = {
166
+ event: "keydown" | "keyup";
167
+ code: string;
168
+ key: string;
169
+ repeat: boolean;
170
+ altKey: boolean;
171
+ ctrlKey: boolean;
172
+ metaKey: boolean;
173
+ shiftKey: boolean;
174
+ isComposing: boolean;
175
+ targetKey: string | null;
176
+ targetKind: PluginSurfaceKeyboardTargetKind;
177
+ defaultPrevented: boolean;
178
+ bindingId: string | null;
179
+ };
140
180
  export type PluginMethodResult<T = unknown> = {
141
181
  data: T;
142
182
  execution_id?: string;
@@ -293,6 +333,8 @@ export declare class PluginBridgeClient {
293
333
  render(tree: PluginUIVNode): Promise<void>;
294
334
  openCanvas(canvasId: string): Promise<PluginCanvasSurface>;
295
335
  updateCanvasAccessibility(canvasId: string, state: PluginCanvasAccessibilityState): Promise<void>;
336
+ setKeyboardBindings(bindings: readonly PluginSurfaceKeyboardBinding[]): Promise<void>;
337
+ onKeyboardInput(handler: (event: PluginSurfaceKeyboardEvent) => void): () => void;
296
338
  loadImageAsset(assetId: string): Promise<ImageBitmap>;
297
339
  onAction(action: string, handler: (event: PluginUIActionEvent) => void): () => void;
298
340
  onCanvasInput(canvasId: string, handler: (event: PluginCanvasInputEvent) => void): () => void;
package/dist/surface.js CHANGED
@@ -28,6 +28,7 @@ export class PluginBridgeClient {
28
28
  #pending = new Map();
29
29
  #actionHandlers = new Map();
30
30
  #canvasInputHandlers = new Map();
31
+ #keyboardInputHandlers = new Set();
31
32
  #lifecycleHandlers = new Set();
32
33
  #contextHandlers = new Set();
33
34
  #context;
@@ -194,6 +195,37 @@ export class PluginBridgeClient {
194
195
  description: state.description,
195
196
  });
196
197
  }
198
+ setKeyboardBindings(bindings) {
199
+ this.#assertActive();
200
+ if (!validKeyboardBindings(bindings)) {
201
+ throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin surface keyboard bindings are invalid");
202
+ }
203
+ const id = this.#requestID("keyboard");
204
+ return this.#request(id, {
205
+ type: "redevplugin.ui.keyboard.bindings",
206
+ id,
207
+ bindings: bindings.map((binding) => ({
208
+ binding_id: binding.id,
209
+ event: binding.event,
210
+ code: binding.code,
211
+ repeat: binding.repeat,
212
+ alt_key: binding.altKey,
213
+ ctrl_key: binding.ctrlKey,
214
+ meta_key: binding.metaKey,
215
+ shift_key: binding.shiftKey,
216
+ target_key: binding.targetKey,
217
+ target_kind: binding.targetKind,
218
+ })),
219
+ });
220
+ }
221
+ onKeyboardInput(handler) {
222
+ this.#assertActive();
223
+ if (typeof handler !== "function") {
224
+ throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin surface keyboard input subscription is invalid");
225
+ }
226
+ this.#keyboardInputHandlers.add(handler);
227
+ return () => this.#keyboardInputHandlers.delete(handler);
228
+ }
197
229
  loadImageAsset(assetId) {
198
230
  this.#assertActive();
199
231
  if (!validUIIdentifier(assetId)) {
@@ -257,6 +289,7 @@ export class PluginBridgeClient {
257
289
  this.#pending.clear();
258
290
  this.#actionHandlers.clear();
259
291
  this.#canvasInputHandlers.clear();
292
+ this.#keyboardInputHandlers.clear();
260
293
  this.#lifecycleHandlers.clear();
261
294
  this.#contextHandlers.clear();
262
295
  this.#context = undefined;
@@ -517,6 +550,12 @@ export class PluginBridgeClient {
517
550
  if (canvasInput) {
518
551
  for (const handler of this.#canvasInputHandlers.get(canvasInput.canvasId) ?? [])
519
552
  handler(canvasInput.event);
553
+ return;
554
+ }
555
+ const keyboardInput = publicKeyboardInputMessage(data);
556
+ if (keyboardInput) {
557
+ for (const handler of this.#keyboardInputHandlers)
558
+ handler(keyboardInput);
520
559
  }
521
560
  }
522
561
  #requestID(prefix) {
@@ -742,6 +781,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
742
781
  const maxTextLength = ${opaqueSurfaceRenderLimits.max_text_length};
743
782
  const maxAttributeValueLength = ${opaqueSurfaceRenderLimits.max_attribute_value_length};
744
783
  const maxFormFields = ${opaqueSurfaceRenderLimits.max_form_fields};
784
+ const maxKeyboardBindings = ${opaqueSurfaceRenderLimits.max_keyboard_bindings};
745
785
  const maxCanvasCount = ${opaqueSurfaceRenderLimits.max_canvas_count};
746
786
  const maxCanvasDimension = ${opaqueSurfaceRenderLimits.max_canvas_dimension};
747
787
  const maxCanvasTotalPixels = ${opaqueSurfaceRenderLimits.max_canvas_total_pixels};
@@ -867,7 +907,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
867
907
  let runtimeDisposed = false;
868
908
  const pendingWorkerRequests = new Set();
869
909
  const pendingRendererCommits = new Set();
870
- const requestSequence = { rpc: 0, execution: 0, render: 0, canvas: 0, asset: 0 };
910
+ const requestSequence = { rpc: 0, execution: 0, render: 0, canvas: 0, asset: 0, keyboard: 0 };
871
911
  let executionQuerySurfaceTokens = 8;
872
912
  let executionQuerySurfaceRefillAt = performance.now();
873
913
  const executionQueryStateIdleMS = 120000;
@@ -881,6 +921,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
881
921
  let uiGraph = new Map();
882
922
  let uiNodes = new Map();
883
923
  const controlEdits = new Map();
924
+ let keyboardBindings = [];
884
925
  let lastAutofocusIdentity = "";
885
926
  const pendingAssets = new Map();
886
927
  const queuedAssets = [];
@@ -898,6 +939,55 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
898
939
  let retainedImagePixels = 0;
899
940
  const canvasRuntimes = new Map();
900
941
 
942
+ const keyboardTarget = (target) => {
943
+ const origin = target instanceof Element ? target : document.activeElement instanceof Element ? document.activeElement : null;
944
+ const keyed = origin?.closest("[data-redevplugin-key]");
945
+ const key = keyed?.getAttribute("data-redevplugin-key");
946
+ const targetKey = validResourceIdentifier(key) ? key : null;
947
+ const control = origin?.closest("canvas, input, textarea, select, button");
948
+ if (!control) return { targetKey, targetKind: "surface" };
949
+ const tag = control.tagName.toLowerCase();
950
+ if (tag === "canvas") return { targetKey, targetKind: "canvas" };
951
+ if (tag === "textarea") return { targetKey, targetKind: "editable" };
952
+ if (tag === "input" && ["text", "search", "number"].includes((control.getAttribute("type") || "text").toLowerCase())) {
953
+ return { targetKey, targetKind: "editable" };
954
+ }
955
+ return { targetKey, targetKind: "control" };
956
+ };
957
+ const handleSurfaceKeyboard = (event) => {
958
+ if (!initialized || runtimeDisposed || pendingQuiesceID || !workerPort || !["keydown", "keyup"].includes(event.type)) return;
959
+ const target = keyboardTarget(event.target);
960
+ const edit = target.targetKey === null ? undefined : controlEdits.get(target.targetKey);
961
+ const isComposing = Boolean(event.isComposing || edit?.isComposing);
962
+ const binding = isComposing ? undefined : keyboardBindings.find((candidate) =>
963
+ candidate.event === event.type && candidate.code === String(event.code || "") &&
964
+ candidate.repeat === Boolean(event.repeat) && candidate.alt_key === Boolean(event.altKey) &&
965
+ candidate.ctrl_key === Boolean(event.ctrlKey) && candidate.meta_key === Boolean(event.metaKey) &&
966
+ candidate.shift_key === Boolean(event.shiftKey) && candidate.target_key === target.targetKey &&
967
+ candidate.target_kind === target.targetKind
968
+ );
969
+ if (binding) event.preventDefault();
970
+ sendWorker({
971
+ type: "redevplugin.ui.keyboard.input",
972
+ event: {
973
+ event: event.type,
974
+ code: String(event.code || "").slice(0, 64),
975
+ key: String(event.key || "").slice(0, 64),
976
+ repeat: Boolean(event.repeat),
977
+ alt_key: Boolean(event.altKey),
978
+ ctrl_key: Boolean(event.ctrlKey),
979
+ meta_key: Boolean(event.metaKey),
980
+ shift_key: Boolean(event.shiftKey),
981
+ is_composing: isComposing,
982
+ target_key: target.targetKey,
983
+ target_kind: target.targetKind,
984
+ default_prevented: Boolean(event.defaultPrevented),
985
+ binding_id: binding?.binding_id || null,
986
+ },
987
+ });
988
+ };
989
+ for (const eventType of ["keydown", "keyup"]) document.addEventListener(eventType, handleSurfaceKeyboard, true);
990
+
901
991
  const sendParent = (message) => {
902
992
  if (parentPort && withinLimit(message)) parentPort.postMessage(message);
903
993
  };
@@ -937,7 +1027,12 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
937
1027
  };
938
1028
  document.addEventListener("pointerdown", (event) => sendInteraction("activation", event.target), true);
939
1029
  document.addEventListener("focusin", (event) => sendInteraction("focus", event.target), true);
940
- document.addEventListener("wheel", (event) => sendInteraction("wheel", event.target, { localScroll: ownsLocalScroll(event) }), { capture: true, passive: true });
1030
+ const ownsCanvasWheel = (target) => {
1031
+ const element = target instanceof Element ? target.closest("canvas[data-redevplugin-canvas]") : null;
1032
+ const canvasID = element?.getAttribute("data-redevplugin-canvas");
1033
+ return validResourceIdentifier(canvasID) && canvasRuntimes.has(canvasID);
1034
+ };
1035
+ document.addEventListener("wheel", (event) => sendInteraction("wheel", event.target, { localScroll: ownsCanvasWheel(event.target) || ownsLocalScroll(event) }), { capture: true, passive: true });
941
1036
  document.addEventListener("selectionchange", () => {
942
1037
  const selection = document.getSelection();
943
1038
  const target = selection?.anchorNode instanceof Element ? selection.anchorNode : selection?.anchorNode?.parentElement;
@@ -972,6 +1067,8 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
972
1067
  workerHeartbeatTimeout = undefined;
973
1068
  workerHeartbeatPendingID = undefined;
974
1069
  workerReady = false;
1070
+ keyboardBindings = [];
1071
+ for (const eventType of ["keydown", "keyup"]) document.removeEventListener(eventType, handleSurfaceKeyboard, true);
975
1072
  for (const runtime of canvasRuntimes.values()) runtime.dispose();
976
1073
  canvasRuntimes.clear();
977
1074
  if (workerControlPort) {
@@ -1983,7 +2080,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
1983
2080
  const validCall = (value) => exactKeys(value, ["type", "request"]) && value.type === "redevplugin.bridge.call" && isRecord(value.request) && Object.keys(value.request).every((key) => ["id", "method", "params"].includes(key)) && typeof value.request.id === "string" && value.request.id.length <= 128 && typeof value.request.method === "string" && /^[A-Za-z0-9._:-]{1,256}$/.test(value.request.method) && (value.request.params === undefined || isRecord(value.request.params));
1984
2081
  const requestID = (value, expectedKind) => {
1985
2082
  if (typeof value !== "string") return undefined;
1986
- const match = /^(rpc|execution|render|canvas|asset)_([1-9][0-9]{0,15})$/.exec(value);
2083
+ const match = /^(rpc|execution|render|canvas|asset|keyboard)_([1-9][0-9]{0,15})$/.exec(value);
1987
2084
  if (!match || match[1] !== expectedKind) return undefined;
1988
2085
  const sequence = Number(match[2]);
1989
2086
  return Number.isSafeInteger(sequence) ? { kind: match[1], sequence } : undefined;
@@ -1996,6 +2093,26 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
1996
2093
  return true;
1997
2094
  };
1998
2095
  const completeWorkerRequest = (id) => pendingWorkerRequests.delete(id);
2096
+ const validKeyboardBinding = (value) => exactKeys(value, ["binding_id", "event", "code", "repeat", "alt_key", "ctrl_key", "meta_key", "shift_key", "target_key", "target_kind"]) &&
2097
+ validIdentifier(value.binding_id) && ["keydown", "keyup"].includes(value.event) &&
2098
+ typeof value.code === "string" && value.code.length > 0 && value.code.length <= 64 &&
2099
+ typeof value.repeat === "boolean" && typeof value.alt_key === "boolean" && typeof value.ctrl_key === "boolean" &&
2100
+ typeof value.meta_key === "boolean" && typeof value.shift_key === "boolean" &&
2101
+ (value.target_key === null || validResourceIdentifier(value.target_key)) &&
2102
+ ["canvas", "editable", "control", "surface"].includes(value.target_kind);
2103
+ const validKeyboardBindings = (value) => {
2104
+ if (!Array.isArray(value) || value.length > maxKeyboardBindings || !value.every(validKeyboardBinding)) return false;
2105
+ const ids = new Set();
2106
+ const matchers = new Set();
2107
+ for (const binding of value) {
2108
+ if (ids.has(binding.binding_id)) return false;
2109
+ ids.add(binding.binding_id);
2110
+ const matcher = JSON.stringify([binding.event, binding.code, binding.repeat, binding.alt_key, binding.ctrl_key, binding.meta_key, binding.shift_key, binding.target_key, binding.target_kind]);
2111
+ if (matchers.has(matcher)) return false;
2112
+ matchers.add(matcher);
2113
+ }
2114
+ return true;
2115
+ };
1999
2116
  const completeExecutionQueryRequest = (id) => {
2000
2117
  const executionID = executionQueryRequests.get(id);
2001
2118
  executionQueryRequests.delete(id);
@@ -2159,8 +2276,25 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
2159
2276
  shift_key: Boolean(event.shiftKey),
2160
2277
  });
2161
2278
  };
2279
+ const handleWheel = (event) => {
2280
+ event.preventDefault();
2281
+ const rect = canvas.getBoundingClientRect();
2282
+ sendInput({
2283
+ type: "wheel",
2284
+ x: Math.min(32768, Math.max(-16384, event.clientX - rect.left)),
2285
+ y: Math.min(32768, Math.max(-16384, event.clientY - rect.top)),
2286
+ delta_x: Math.min(32768, Math.max(-32768, Number.isFinite(event.deltaX) ? event.deltaX : 0)),
2287
+ delta_y: Math.min(32768, Math.max(-32768, Number.isFinite(event.deltaY) ? event.deltaY : 0)),
2288
+ delta_mode: [0, 1, 2].includes(event.deltaMode) ? event.deltaMode : 0,
2289
+ alt_key: Boolean(event.altKey),
2290
+ ctrl_key: Boolean(event.ctrlKey),
2291
+ meta_key: Boolean(event.metaKey),
2292
+ shift_key: Boolean(event.shiftKey),
2293
+ });
2294
+ };
2162
2295
  for (const type of ["pointerdown", "pointermove", "pointerup", "pointercancel"]) listen(type, handlePointer);
2163
2296
  for (const type of ["keydown", "keyup"]) listen(type, handleKey);
2297
+ listen("wheel", handleWheel);
2164
2298
  listen("focus", () => sendInput({ type: "focus" }));
2165
2299
  listen("blur", () => sendInput({ type: "blur" }));
2166
2300
  const observer = new ResizeObserver(() => {
@@ -2285,6 +2419,17 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
2285
2419
  updateCanvasAccessibility(message.id, message.canvas_id, message.label, message.description);
2286
2420
  return;
2287
2421
  }
2422
+ if (exactKeys(message, ["type", "id", "bindings"]) && message.type === "redevplugin.ui.keyboard.bindings" && typeof message.id === "string") {
2423
+ if (!acceptWorkerRequest(message.id, "keyboard")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
2424
+ if (!validKeyboardBindings(message.bindings)) {
2425
+ completeWorkerRequest(message.id);
2426
+ return rejectWorkerRequest(message.id, "plugin surface keyboard bindings are invalid");
2427
+ }
2428
+ keyboardBindings = message.bindings.map((binding) => ({ ...binding }));
2429
+ completeWorkerRequest(message.id);
2430
+ sendWorker({ type: "redevplugin.bridge.response", id: message.id, ok: true });
2431
+ return;
2432
+ }
2288
2433
  if (exactKeys(message, ["type", "id", "asset_id"]) && message.type === "redevplugin.ui.asset.image.open" && typeof message.id === "string" && validResourceIdentifier(message.asset_id)) {
2289
2434
  if (!acceptWorkerRequest(message.id, "asset")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
2290
2435
  const asset = assetByLogicalID.get(message.asset_id);
@@ -4221,6 +4366,35 @@ function publicActionEvent(value) {
4221
4366
  form_data: value.form_data,
4222
4367
  });
4223
4368
  }
4369
+ function publicKeyboardInputMessage(value) {
4370
+ if (!isRecord(value) || !hasExactKeys(value, ["type", "event"]) || value.type !== "redevplugin.ui.keyboard.input" || !isRecord(value.event))
4371
+ return undefined;
4372
+ const event = value.event;
4373
+ if (!hasExactKeys(event, ["event", "code", "key", "repeat", "alt_key", "ctrl_key", "meta_key", "shift_key", "is_composing", "target_key", "target_kind", "default_prevented", "binding_id"]) ||
4374
+ (event.event !== "keydown" && event.event !== "keyup") || typeof event.code !== "string" || event.code.length > 64 ||
4375
+ typeof event.key !== "string" || event.key.length > 64 || typeof event.repeat !== "boolean" ||
4376
+ typeof event.alt_key !== "boolean" || typeof event.ctrl_key !== "boolean" || typeof event.meta_key !== "boolean" ||
4377
+ typeof event.shift_key !== "boolean" || typeof event.is_composing !== "boolean" ||
4378
+ (event.target_key !== null && !validUIIdentifier(event.target_key)) ||
4379
+ (event.target_kind !== "canvas" && event.target_kind !== "editable" && event.target_kind !== "control" && event.target_kind !== "surface") ||
4380
+ typeof event.default_prevented !== "boolean" || (event.binding_id !== null && !validActionID(event.binding_id)))
4381
+ return undefined;
4382
+ return {
4383
+ event: event.event,
4384
+ code: event.code,
4385
+ key: event.key,
4386
+ repeat: event.repeat,
4387
+ altKey: event.alt_key,
4388
+ ctrlKey: event.ctrl_key,
4389
+ metaKey: event.meta_key,
4390
+ shiftKey: event.shift_key,
4391
+ isComposing: event.is_composing,
4392
+ targetKey: event.target_key,
4393
+ targetKind: event.target_kind,
4394
+ defaultPrevented: event.default_prevented,
4395
+ bindingId: event.binding_id,
4396
+ };
4397
+ }
4224
4398
  function isCanvasReadyCandidate(value) {
4225
4399
  return isRecord(value) && value.type === "redevplugin.ui.canvas.ready" && typeof value.id === "string" && typeof value.canvas_id === "string";
4226
4400
  }
@@ -4298,6 +4472,28 @@ function publicCanvasInputMessage(value) {
4298
4472
  },
4299
4473
  };
4300
4474
  }
4475
+ if (hasExactKeys(event, ["type", "x", "y", "delta_x", "delta_y", "delta_mode", "alt_key", "ctrl_key", "meta_key", "shift_key"]) &&
4476
+ event.type === "wheel" && validCanvasCoordinate(event.x) && validCanvasCoordinate(event.y) &&
4477
+ validCanvasWheelDelta(event.delta_x) && validCanvasWheelDelta(event.delta_y) &&
4478
+ (event.delta_mode === 0 || event.delta_mode === 1 || event.delta_mode === 2) &&
4479
+ typeof event.alt_key === "boolean" && typeof event.ctrl_key === "boolean" &&
4480
+ typeof event.meta_key === "boolean" && typeof event.shift_key === "boolean") {
4481
+ return {
4482
+ canvasId: value.canvas_id,
4483
+ event: {
4484
+ type: "wheel",
4485
+ x: event.x,
4486
+ y: event.y,
4487
+ deltaX: event.delta_x,
4488
+ deltaY: event.delta_y,
4489
+ deltaMode: event.delta_mode,
4490
+ altKey: event.alt_key,
4491
+ ctrlKey: event.ctrl_key,
4492
+ metaKey: event.meta_key,
4493
+ shiftKey: event.shift_key,
4494
+ },
4495
+ };
4496
+ }
4301
4497
  return undefined;
4302
4498
  }
4303
4499
  function isOffscreenCanvasLike(value) {
@@ -4317,6 +4513,9 @@ function validDevicePixelRatio(value) {
4317
4513
  function validCanvasCoordinate(value) {
4318
4514
  return typeof value === "number" && Number.isFinite(value) && value >= -16384 && value <= 32768;
4319
4515
  }
4516
+ function validCanvasWheelDelta(value) {
4517
+ return typeof value === "number" && Number.isFinite(value) && value >= -32768 && value <= 32768;
4518
+ }
4320
4519
  function isMessagePortLike(value) {
4321
4520
  return isRecord(value) &&
4322
4521
  typeof value.postMessage === "function" &&
@@ -4354,7 +4553,7 @@ const pluginMethodPattern = new RegExp("^[-A-Za-z0-9._:]{1,256}$");
4354
4553
  const pluginActionPattern = new RegExp("^[-A-Za-z0-9._:]{1,128}$");
4355
4554
  const pluginUIIdentifierPattern = new RegExp("^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$");
4356
4555
  const opaqueHandlePattern = new RegExp("^[-A-Za-z0-9_]{8,160}$");
4357
- const bridgeRequestIDPattern = /^(rpc|execution|render|canvas|asset)_([1-9][0-9]{0,15})$/;
4556
+ const bridgeRequestIDPattern = /^(rpc|execution|render|canvas|asset|keyboard)_([1-9][0-9]{0,15})$/;
4358
4557
  function validBridgeRequestID(value, expectedKind) {
4359
4558
  if (typeof value !== "string")
4360
4559
  return false;
@@ -4363,6 +4562,34 @@ function validBridgeRequestID(value, expectedKind) {
4363
4562
  return false;
4364
4563
  return Number.isSafeInteger(Number(match[2]));
4365
4564
  }
4565
+ function validKeyboardBinding(value) {
4566
+ return isRecord(value) && hasExactKeys(value, ["id", "event", "code", "repeat", "altKey", "ctrlKey", "metaKey", "shiftKey", "targetKey", "targetKind"]) &&
4567
+ validActionID(value.id) && (value.event === "keydown" || value.event === "keyup") &&
4568
+ typeof value.code === "string" && value.code.length > 0 && value.code.length <= 64 &&
4569
+ typeof value.repeat === "boolean" && typeof value.altKey === "boolean" && typeof value.ctrlKey === "boolean" &&
4570
+ typeof value.metaKey === "boolean" && typeof value.shiftKey === "boolean" &&
4571
+ (value.targetKey === null || validUIIdentifier(value.targetKey)) &&
4572
+ (value.targetKind === "canvas" || value.targetKind === "editable" || value.targetKind === "control" || value.targetKind === "surface");
4573
+ }
4574
+ function validKeyboardBindings(value) {
4575
+ if (!Array.isArray(value) || value.length > 128 || !value.every(validKeyboardBinding))
4576
+ return false;
4577
+ const ids = new Set();
4578
+ const matchers = new Set();
4579
+ for (const binding of value) {
4580
+ if (ids.has(binding.id))
4581
+ return false;
4582
+ ids.add(binding.id);
4583
+ const matcher = JSON.stringify([
4584
+ binding.event, binding.code, binding.repeat, binding.altKey, binding.ctrlKey,
4585
+ binding.metaKey, binding.shiftKey, binding.targetKey, binding.targetKind,
4586
+ ]);
4587
+ if (matchers.has(matcher))
4588
+ return false;
4589
+ matchers.add(matcher);
4590
+ }
4591
+ return true;
4592
+ }
4366
4593
  function validMethod(value) {
4367
4594
  return typeof value === "string" && pluginMethodPattern.test(value);
4368
4595
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/redevplugin-ui",
3
- "version": "3.0.21",
3
+ "version": "3.0.23",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -40,7 +40,7 @@
40
40
  "dist"
41
41
  ],
42
42
  "dependencies": {
43
- "@floegence/redevplugin-contracts": "3.0.21"
43
+ "@floegence/redevplugin-contracts": "3.0.23"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"