@fieldnotes/core 0.18.0 → 0.19.0

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Irakli Iremashvili
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Irakli Iremashvili
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -269,6 +269,35 @@ viewport.toolManager.onChange((toolName) => {
269
269
  });
270
270
  ```
271
271
 
272
+ ## Keyboard shortcuts
273
+
274
+ Defaults (remappable): `Delete`/`Backspace` delete · `Escape` deselect · `mod+Z` undo ·
275
+ `mod+Y`/`mod+Shift+Z` redo · `mod+A` select all · `mod+C/V/D` copy/paste/duplicate ·
276
+ `[`/`]` z-order (with `mod` = to back/front) · `Shift+1` zoom-to-fit · arrows nudge
277
+ (`Shift` = one grid cell) · tool keys `V` select, `H` hand, `P` pencil, `E` eraser,
278
+ `A` arrow, `N` note, `T` text, `S` shape, `M` measure, `G` template.
279
+
280
+ `mod` = Ctrl or Cmd. Shortcuts fire only while the canvas has focus (click it once);
281
+ pass `shortcuts: { scope: 'window' }` for page-wide handling.
282
+
283
+ ```ts
284
+ const viewport = new Viewport(el, {
285
+ shortcuts: {
286
+ bindings: {
287
+ duplicate: 'mod+shift+d', // remap
288
+ 'tool:pencil': ['p', 'b'], // multiple bindings
289
+ copy: null, // disable
290
+ 'tool:my-custom-tool': 'f', // any registered tool works
291
+ },
292
+ },
293
+ });
294
+
295
+ viewport.shortcuts.rebind('undo', 'mod+u');
296
+ viewport.shortcuts.disable('select-all');
297
+ viewport.shortcuts.reset(); // back to defaults
298
+ viewport.shortcuts.getBindings(); // current table — render a settings UI
299
+ ```
300
+
272
301
  ## Changing Tool Options at Runtime
273
302
 
274
303
  All drawing tools support `setOptions()` for changing color, width, and other settings without re-creating the tool:
@@ -435,7 +464,8 @@ Works in all modern browsers supporting Pointer Events API and HTML5 Canvas.
435
464
 
436
465
  `@fieldnotes/core` and `@fieldnotes/react` are versioned independently. The react
437
466
  package's `peerDependencies` declare the compatible core range. Pre-1.0, minor
438
- versions may contain breaking changes.
467
+ versions may contain breaking changes. The core peer range is bounded at the next major rather than per-minor; if a core minor
468
+ ever breaks the wrapper, a coordinated react release raises the lower bound.
439
469
 
440
470
  ## License
441
471
 
package/dist/index.cjs CHANGED
@@ -1150,14 +1150,158 @@ var KeyboardActions = class {
1150
1150
  }
1151
1151
  };
1152
1152
 
1153
+ // src/canvas/shortcut-map.ts
1154
+ var DEFAULT_BINDINGS = [
1155
+ ["delete", ["delete", "backspace"]],
1156
+ ["deselect", ["escape"]],
1157
+ ["undo", ["mod+z"]],
1158
+ ["redo", ["mod+y", "mod+shift+z"]],
1159
+ ["select-all", ["mod+a"]],
1160
+ ["copy", ["mod+c"]],
1161
+ ["paste", ["mod+v"]],
1162
+ ["duplicate", ["mod+d"]],
1163
+ ["z-forward", ["]"]],
1164
+ ["z-backward", ["["]],
1165
+ ["z-front", ["mod+]"]],
1166
+ ["z-back", ["mod+["]],
1167
+ ["zoom-fit", ["shift+1"]],
1168
+ ["nudge-left", ["arrowleft"]],
1169
+ ["nudge-right", ["arrowright"]],
1170
+ ["nudge-up", ["arrowup"]],
1171
+ ["nudge-down", ["arrowdown"]],
1172
+ ["tool:select", ["v"]],
1173
+ ["tool:hand", ["h"]],
1174
+ ["tool:pencil", ["p"]],
1175
+ ["tool:eraser", ["e"]],
1176
+ ["tool:arrow", ["a"]],
1177
+ ["tool:note", ["n"]],
1178
+ ["tool:text", ["t"]],
1179
+ ["tool:shape", ["s"]],
1180
+ ["tool:measure", ["m"]],
1181
+ ["tool:template", ["g"]]
1182
+ ];
1183
+ var ALLOW_SHIFT = /* @__PURE__ */ new Set(["nudge-left", "nudge-right", "nudge-up", "nudge-down"]);
1184
+ var MODIFIERS = /* @__PURE__ */ new Set(["mod", "ctrl", "meta", "shift", "alt"]);
1185
+ function parseBinding(binding) {
1186
+ const parts = binding.toLowerCase().split("+");
1187
+ const key = parts.pop();
1188
+ if (key === void 0 || key.length === 0 || MODIFIERS.has(key)) {
1189
+ throw new Error(`Invalid shortcut binding "${binding}": missing key`);
1190
+ }
1191
+ const normalizedKey = key === "space" ? " " : key;
1192
+ const parsed = {
1193
+ mod: false,
1194
+ ctrl: false,
1195
+ meta: false,
1196
+ shift: false,
1197
+ alt: false,
1198
+ key: normalizedKey,
1199
+ digit: /^[0-9]$/.test(normalizedKey)
1200
+ };
1201
+ for (const part of parts) {
1202
+ switch (part) {
1203
+ case "mod":
1204
+ parsed.mod = true;
1205
+ break;
1206
+ case "ctrl":
1207
+ parsed.ctrl = true;
1208
+ break;
1209
+ case "meta":
1210
+ parsed.meta = true;
1211
+ break;
1212
+ case "shift":
1213
+ parsed.shift = true;
1214
+ break;
1215
+ case "alt":
1216
+ parsed.alt = true;
1217
+ break;
1218
+ default:
1219
+ throw new Error(`Invalid shortcut binding "${binding}": unknown modifier "${part}"`);
1220
+ }
1221
+ }
1222
+ return parsed;
1223
+ }
1224
+ function bindingMatches(p, e, allowShift) {
1225
+ if (p.mod) {
1226
+ if (!e.ctrlKey && !e.metaKey) return false;
1227
+ } else if (e.ctrlKey !== p.ctrl || e.metaKey !== p.meta) {
1228
+ return false;
1229
+ }
1230
+ if (!allowShift && e.shiftKey !== p.shift) return false;
1231
+ if (e.altKey !== p.alt) return false;
1232
+ return p.digit ? e.code === `Digit${p.key}` : e.key.toLowerCase() === p.key;
1233
+ }
1234
+ function toArray(bindings) {
1235
+ if (bindings === null) return [];
1236
+ return Array.isArray(bindings) ? [...bindings] : [bindings];
1237
+ }
1238
+ var ShortcutMap = class {
1239
+ raw = /* @__PURE__ */ new Map();
1240
+ parsed = /* @__PURE__ */ new Map();
1241
+ constructor(overrides) {
1242
+ this.applyDefaults();
1243
+ if (overrides) {
1244
+ for (const [action, bindings] of Object.entries(overrides)) {
1245
+ this.rebind(action, bindings);
1246
+ }
1247
+ }
1248
+ }
1249
+ /** First matching action in registration order wins when bindings conflict. */
1250
+ match(e) {
1251
+ for (const [action, parsedList] of this.parsed) {
1252
+ const allowShift = ALLOW_SHIFT.has(action);
1253
+ for (const p of parsedList) {
1254
+ if (bindingMatches(p, e, allowShift)) return action;
1255
+ }
1256
+ }
1257
+ return null;
1258
+ }
1259
+ rebind(action, bindings) {
1260
+ const list = toArray(bindings);
1261
+ const parsedList = list.map(parseBinding);
1262
+ this.raw.set(action, list);
1263
+ this.parsed.set(action, parsedList);
1264
+ }
1265
+ disable(action) {
1266
+ this.rebind(action, null);
1267
+ }
1268
+ reset(action) {
1269
+ if (action === void 0) {
1270
+ this.raw.clear();
1271
+ this.parsed.clear();
1272
+ this.applyDefaults();
1273
+ return;
1274
+ }
1275
+ const def = DEFAULT_BINDINGS.find(([name]) => name === action);
1276
+ if (def) {
1277
+ this.rebind(action, [...def[1]]);
1278
+ } else if (this.raw.has(action)) {
1279
+ this.raw.delete(action);
1280
+ this.parsed.delete(action);
1281
+ }
1282
+ }
1283
+ getBindings() {
1284
+ const out = {};
1285
+ for (const [action, list] of this.raw) {
1286
+ out[action] = [...list];
1287
+ }
1288
+ return out;
1289
+ }
1290
+ applyDefaults() {
1291
+ for (const [action, bindings] of DEFAULT_BINDINGS) {
1292
+ this.rebind(action, [...bindings]);
1293
+ }
1294
+ }
1295
+ };
1296
+
1153
1297
  // src/canvas/input-handler.ts
1154
1298
  var ZOOM_SENSITIVITY = 1e-3;
1155
1299
  var MIDDLE_BUTTON = 1;
1156
- var NUDGE_KEYS = {
1157
- ArrowLeft: [-1, 0],
1158
- ArrowRight: [1, 0],
1159
- ArrowUp: [0, -1],
1160
- ArrowDown: [0, 1]
1300
+ var NUDGE_DELTAS = {
1301
+ "nudge-left": [-1, 0],
1302
+ "nudge-right": [1, 0],
1303
+ "nudge-up": [0, -1],
1304
+ "nudge-down": [0, 1]
1161
1305
  };
1162
1306
  var InputHandler = class {
1163
1307
  constructor(element, camera, options = {}) {
@@ -1175,7 +1319,13 @@ var InputHandler = class {
1175
1319
  isToolActive: () => this.isToolActive,
1176
1320
  fitToContent: options.fitToContent
1177
1321
  });
1322
+ this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
1323
+ this.scope = options.shortcuts?.scope ?? "focus";
1178
1324
  this.element.style.touchAction = "none";
1325
+ if (this.scope === "focus") {
1326
+ this.element.tabIndex = 0;
1327
+ this.element.style.outline = "none";
1328
+ }
1179
1329
  this.bind();
1180
1330
  }
1181
1331
  isPanning = false;
@@ -1194,6 +1344,8 @@ var InputHandler = class {
1194
1344
  deferredDown = null;
1195
1345
  abortController = new AbortController();
1196
1346
  actions;
1347
+ shortcutMap;
1348
+ scope;
1197
1349
  setToolManager(toolManager, toolContext) {
1198
1350
  this.toolManager = toolManager;
1199
1351
  this.toolContext = toolContext;
@@ -1201,6 +1353,9 @@ var InputHandler = class {
1201
1353
  flushPendingHistory() {
1202
1354
  this.actions.flushPendingNudge();
1203
1355
  }
1356
+ get shortcuts() {
1357
+ return this.shortcutMap;
1358
+ }
1204
1359
  destroy() {
1205
1360
  this.actions.dispose();
1206
1361
  this.abortController.abort();
@@ -1230,6 +1385,7 @@ var InputHandler = class {
1230
1385
  });
1231
1386
  };
1232
1387
  onPointerDown = (e) => {
1388
+ this.focusSelf();
1233
1389
  this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
1234
1390
  this.element.setPointerCapture?.(e.pointerId);
1235
1391
  if (this.activePointers.size === 2) {
@@ -1312,57 +1468,13 @@ var InputHandler = class {
1312
1468
  if (target?.isContentEditable) return;
1313
1469
  const tag = target?.tagName;
1314
1470
  if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
1471
+ if (!this.isInScope()) return;
1315
1472
  if (e.key === " ") {
1316
1473
  this.spaceHeld = true;
1317
1474
  }
1318
- if (e.key === "Delete" || e.key === "Backspace") {
1319
- this.actions.deleteSelected();
1320
- }
1321
- if (e.key === "Escape") {
1322
- this.actions.deselect();
1323
- }
1324
- if ((e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey) {
1325
- e.preventDefault();
1326
- this.actions.undo();
1327
- }
1328
- if ((e.ctrlKey || e.metaKey) && (e.key === "y" || e.key === "z" && e.shiftKey)) {
1329
- e.preventDefault();
1330
- this.actions.redo();
1331
- }
1332
- if ((e.ctrlKey || e.metaKey) && e.key === "a") {
1333
- e.preventDefault();
1334
- this.actions.selectAll();
1335
- }
1336
- if ((e.ctrlKey || e.metaKey) && e.key === "c") {
1337
- e.preventDefault();
1338
- this.actions.copy();
1339
- }
1340
- if ((e.ctrlKey || e.metaKey) && e.key === "v") {
1341
- e.preventDefault();
1342
- this.actions.paste();
1343
- }
1344
- if ((e.ctrlKey || e.metaKey) && e.key === "d") {
1345
- e.preventDefault();
1346
- this.actions.duplicate();
1347
- }
1348
- if (e.key === "]") {
1349
- e.preventDefault();
1350
- this.actions.zOrder(e.ctrlKey || e.metaKey ? "front" : "forward");
1351
- }
1352
- if (e.key === "[") {
1353
- e.preventDefault();
1354
- this.actions.zOrder(e.ctrlKey || e.metaKey ? "back" : "backward");
1355
- }
1356
- if (e.shiftKey && e.code === "Digit1" && !e.ctrlKey && !e.metaKey && !e.altKey) {
1357
- e.preventDefault();
1358
- this.actions.zoomToFit();
1359
- }
1360
- const nudgeDelta = NUDGE_KEYS[e.key];
1361
- if (nudgeDelta) {
1362
- const [dx, dy] = nudgeDelta;
1363
- if (this.actions.nudge(dx, dy, e.shiftKey)) {
1364
- e.preventDefault();
1365
- }
1475
+ const action = this.shortcutMap.match(e);
1476
+ if (action !== null) {
1477
+ this.runAction(action, e);
1366
1478
  }
1367
1479
  };
1368
1480
  onKeyUp = (e) => {
@@ -1377,6 +1489,79 @@ var InputHandler = class {
1377
1489
  }
1378
1490
  }
1379
1491
  };
1492
+ runAction(action, e) {
1493
+ switch (action) {
1494
+ case "delete":
1495
+ e.preventDefault();
1496
+ this.actions.deleteSelected();
1497
+ return;
1498
+ case "deselect":
1499
+ this.actions.deselect();
1500
+ return;
1501
+ case "undo":
1502
+ e.preventDefault();
1503
+ this.actions.undo();
1504
+ return;
1505
+ case "redo":
1506
+ e.preventDefault();
1507
+ this.actions.redo();
1508
+ return;
1509
+ case "select-all":
1510
+ e.preventDefault();
1511
+ this.actions.selectAll();
1512
+ return;
1513
+ case "copy":
1514
+ e.preventDefault();
1515
+ this.actions.copy();
1516
+ return;
1517
+ case "paste":
1518
+ e.preventDefault();
1519
+ this.actions.paste();
1520
+ return;
1521
+ case "duplicate":
1522
+ e.preventDefault();
1523
+ this.actions.duplicate();
1524
+ return;
1525
+ case "z-forward":
1526
+ e.preventDefault();
1527
+ this.actions.zOrder("forward");
1528
+ return;
1529
+ case "z-backward":
1530
+ e.preventDefault();
1531
+ this.actions.zOrder("backward");
1532
+ return;
1533
+ case "z-front":
1534
+ e.preventDefault();
1535
+ this.actions.zOrder("front");
1536
+ return;
1537
+ case "z-back":
1538
+ e.preventDefault();
1539
+ this.actions.zOrder("back");
1540
+ return;
1541
+ case "zoom-fit":
1542
+ e.preventDefault();
1543
+ this.actions.zoomToFit();
1544
+ return;
1545
+ case "nudge-left":
1546
+ case "nudge-right":
1547
+ case "nudge-up":
1548
+ case "nudge-down": {
1549
+ const delta = NUDGE_DELTAS[action];
1550
+ if (delta && this.actions.nudge(delta[0], delta[1], e.shiftKey)) {
1551
+ e.preventDefault();
1552
+ }
1553
+ return;
1554
+ }
1555
+ default:
1556
+ if (action.startsWith("tool:")) {
1557
+ if (this.isToolActive) return;
1558
+ e.preventDefault();
1559
+ this.toolContext?.switchTool?.(action.slice("tool:".length));
1560
+ return;
1561
+ }
1562
+ console.warn(`[fieldnotes] unknown shortcut action "${action}"`);
1563
+ }
1564
+ }
1380
1565
  startPinch() {
1381
1566
  this.inputFilter.reset();
1382
1567
  this.deferredDown = null;
@@ -1447,6 +1632,15 @@ var InputHandler = class {
1447
1632
  this.toolManager.handlePointerUp(this.toPointerState(e), this.toolContext);
1448
1633
  this.historyRecorder?.commit();
1449
1634
  }
1635
+ isInScope() {
1636
+ if (this.scope === "window") return true;
1637
+ const active = document.activeElement;
1638
+ return active === this.element || this.element.contains(active);
1639
+ }
1640
+ focusSelf() {
1641
+ if (this.scope !== "focus" || this.isInScope()) return;
1642
+ this.element.focus({ preventScroll: true });
1643
+ }
1450
1644
  cancelToolIfActive(e) {
1451
1645
  if (this.isToolActive) {
1452
1646
  this.dispatchToolUp(e);
@@ -4841,7 +5035,8 @@ var Viewport = class {
4841
5035
  toolContext: this.toolContext,
4842
5036
  historyRecorder: this.historyRecorder,
4843
5037
  historyStack: this.history,
4844
- fitToContent: () => this.fitToContent()
5038
+ fitToContent: () => this.fitToContent(),
5039
+ shortcuts: options.shortcuts
4845
5040
  });
4846
5041
  this.domNodeManager = new DomNodeManager({
4847
5042
  domLayer: this.domLayer,
@@ -5015,6 +5210,9 @@ var Viewport = class {
5015
5210
  setTool(name) {
5016
5211
  this.toolManager.setTool(name, this.toolContext);
5017
5212
  }
5213
+ get shortcuts() {
5214
+ return this.inputHandler.shortcuts;
5215
+ }
5018
5216
  undo() {
5019
5217
  this.inputHandler.flushPendingHistory();
5020
5218
  this.historyRecorder.pause();
@@ -7053,7 +7251,7 @@ var TemplateTool = class {
7053
7251
  };
7054
7252
 
7055
7253
  // src/index.ts
7056
- var VERSION = "0.18.0";
7254
+ var VERSION = "0.19.0";
7057
7255
  // Annotate the CommonJS export names for ESM import in node:
7058
7256
  0 && (module.exports = {
7059
7257
  AddElementCommand,