@fieldnotes/svelte 0.1.0 → 0.1.1

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.
@@ -9,12 +9,6 @@
9
9
  size?: { w: number; h: number };
10
10
  children: Snippet;
11
11
  }
12
-
13
- /**
14
- * Marks every store mutation this component makes as host-owned: external origin, so it
15
- * neither creates an undo step nor is re-broadcast to the sync hub.
16
- */
17
- const HOST_ORIGIN = 'host';
18
12
  </script>
19
13
 
20
14
  <!--
@@ -26,6 +20,8 @@
26
20
  -->
27
21
  <script lang="ts">
28
22
  import { onMount } from 'svelte';
23
+ import { attachHostElement } from '@fieldnotes/core';
24
+ import type { HostElementHandle } from '@fieldnotes/core';
29
25
  import { useViewport } from './context.js';
30
26
 
31
27
  const { position, size, children }: CanvasElementProps = $props();
@@ -42,77 +38,18 @@
42
38
  });
43
39
 
44
40
  let container: HTMLDivElement;
45
- let elementId: string | null = null;
41
+ let host: HostElementHandle | undefined;
46
42
 
47
43
  onMount(() => {
48
- const register = () => {
49
- // Append to domLayer immediately so the children are queryable in the document
50
- // before the viewport render loop fires via requestAnimationFrame.
51
- viewport.domLayer.appendChild(container);
52
- const { position: current, size: currentSize } = geometry();
53
- return viewport.addHtmlElement(container, current, currentSize, {
54
- transient: true,
55
- origin: HOST_ORIGIN,
56
- });
57
- };
58
-
59
- elementId = register();
60
-
61
- // A store clear detaches every canvas-owned DOM node and forgets the content registered
62
- // for it, so this component has to hand its container back afterwards. The decision is
63
- // deferred to a microtask because a remote clear is followed — synchronously, by the sync
64
- // client — by re-adding the transient elements: reading the store inside the event would
65
- // always report this element gone and add a duplicate.
66
- const handleStoreReplacement = () => {
67
- const clearedId = elementId;
68
- if (clearedId === null) return;
69
- queueMicrotask(() => {
70
- if (elementId !== clearedId) return;
71
- if (viewport.store.getById(clearedId)?.type === 'html') {
72
- // Survived the clear (the sync client re-adds transient elements after a remote
73
- // clear): restore the container as that element's content instead of adding a second.
74
- viewport.updateHtmlElement(clearedId, container);
75
- } else {
76
- elementId = register();
77
- }
78
- viewport.requestRender();
79
- });
80
- };
81
- // `clear` covers a clear-canvas gesture and a remote clear. A wholesale state replacement
82
- // (`loadState`/`loadJSON`) runs inside `suspendNotifications`, so the store coalesces its
83
- // events into a single `batch` instead: the same recovery applies.
84
- const unsubscribeClear = viewport.store.on('clear', handleStoreReplacement);
85
- const unsubscribeBatch = viewport.store.on('batch', handleStoreReplacement);
86
-
87
- return () => {
88
- unsubscribeClear();
89
- unsubscribeBatch();
90
- if (elementId) {
91
- viewport.store.remove(elementId, { origin: HOST_ORIGIN });
92
- viewport.requestRender();
93
- elementId = null;
94
- }
95
- // Removing the element usually detaches the container; never leave it orphaned in the layer.
96
- if (container.parentNode === viewport.domLayer) container.remove();
97
- };
44
+ const handle = attachHostElement(viewport, container, geometry());
45
+ host = handle;
46
+ return () => handle.destroy();
98
47
  });
99
48
 
100
- let registered = false;
101
49
  $effect(() => {
50
+ // Read before the optional call so tracking never depends on `host`.
102
51
  const next = geometry();
103
- // The first run sees the geometry onMount just registered.
104
- if (!registered) {
105
- registered = true;
106
- return;
107
- }
108
- if (!elementId) return;
109
- const { position: nextPosition, size: nextSize } = next;
110
- viewport.store.update(
111
- elementId,
112
- { position: nextPosition, ...(nextSize ? { size: nextSize } : {}) },
113
- { origin: HOST_ORIGIN },
114
- );
115
- viewport.requestRender();
52
+ host?.setGeometry(next);
116
53
  });
117
54
  </script>
118
55
 
@@ -1 +1 @@
1
- {"version":3,"file":"CanvasElement.svelte.d.ts","sourceRoot":"","sources":["../src/CanvasElement.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,uCAAuC;AACvC,MAAM,WAAW,kBAAkB;IACjC,8DAA8D;IAC9D,QAAQ,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnC,wEAAwE;IACxE,IAAI,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChC,QAAQ,EAAE,OAAO,CAAC;CACnB;AAkHH;;;;;GAKG;AACH,QAAA,MAAM,aAAa,wDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"CanvasElement.svelte.d.ts","sourceRoot":"","sources":["../src/CanvasElement.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,uCAAuC;AACvC,MAAM,WAAW,kBAAkB;IACjC,8DAA8D;IAC9D,QAAQ,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnC,wEAAwE;IACxE,IAAI,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChC,QAAQ,EAAE,OAAO,CAAC;CACnB;AAqDH;;;;;GAKG;AACH,QAAA,MAAM,aAAa,wDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -1,7 +1,12 @@
1
1
  <script module lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
- import type { ViewportOptions, Tool, CanvasState, ImportableCanvasState } from '@fieldnotes/core';
4
- import { Viewport, createDefaultTools } from '@fieldnotes/core';
3
+ import type {
4
+ ViewportOptions,
5
+ Tool,
6
+ CanvasState,
7
+ ImportableCanvasState,
8
+ Viewport,
9
+ } from '@fieldnotes/core';
5
10
 
6
11
  export interface FieldNotesCanvasProps {
7
12
  /** Constructor-only: changes after mount are ignored (the canvas is stateful). */
@@ -42,6 +47,8 @@
42
47
 
43
48
  <script lang="ts">
44
49
  import { onMount } from 'svelte';
50
+ import { mountViewport } from '@fieldnotes/core';
51
+ import type { MountedViewport } from '@fieldnotes/core';
45
52
  import ViewportProvider from './ViewportProvider.svelte';
46
53
 
47
54
  const {
@@ -63,82 +70,44 @@
63
70
  let root: HTMLDivElement;
64
71
  let viewport = $state.raw<Viewport | null>(null);
65
72
 
66
- // Props are read inside callbacks, so the debounce and handlers always see the latest values.
67
- onMount(() => {
68
- const vp = new Viewport(root, options);
69
-
70
- const activate = (fallback?: string) => {
71
- const name = tool ?? defaultTool ?? fallback;
72
- if (name) vp.setTool(name);
73
- };
74
- // An explicit `options.tools` is the host's choice; core registers it and activates select.
75
- const deferDefaults = tools === undefined && options?.tools === undefined;
76
- for (const t of tools === 'default' ? createDefaultTools() : (tools ?? [])) {
77
- vp.toolManager.register(t);
78
- }
79
- if (!deferDefaults) activate(tools === 'default' ? 'select' : undefined);
73
+ let mounted: MountedViewport | undefined;
80
74
 
81
- try {
82
- if (typeof initialData === 'string') vp.loadJSON(initialData);
83
- else if (initialData) vp.loadState(initialData);
84
- } catch (error) {
85
- vp.destroy();
86
- throw error;
87
- }
88
-
89
- let timer: ReturnType<typeof setTimeout> | undefined;
90
- const emitChange = () => {
91
- timer = undefined;
92
- onchange?.(vp.exportState());
93
- };
94
- // Subscribed after initialData so the initial load is not reported as a change.
95
- const unsubscribe = vp.onStateChange(() => {
96
- clearTimeout(timer);
97
- timer = setTimeout(emitChange, changeDebounceMs);
75
+ // Getters keep the destructured props live for the helper's deferred reads.
76
+ onMount(() => {
77
+ const handle = mountViewport(root, {
78
+ options,
79
+ tools,
80
+ initialData,
81
+ get tool() {
82
+ return tool;
83
+ },
84
+ get defaultTool() {
85
+ return defaultTool;
86
+ },
87
+ onReady: onready,
88
+ get onChange() {
89
+ return onchange;
90
+ },
91
+ get changeDebounceMs() {
92
+ return changeDebounceMs;
93
+ },
94
+ get onToolChange() {
95
+ return ontoolchange;
96
+ },
98
97
  });
99
-
100
- try {
101
- onready?.(vp);
102
-
103
- // Hosts that register their own tools in onready keep exactly those.
104
- if (deferDefaults) {
105
- const registerDefaults = vp.toolManager.toolNames.length === 0;
106
- if (registerDefaults) {
107
- for (const t of createDefaultTools()) vp.toolManager.register(t);
108
- }
109
- activate(registerDefaults ? 'select' : undefined);
110
- }
111
- } catch (error) {
112
- clearTimeout(timer);
113
- unsubscribe();
114
- vp.destroy();
115
- throw error;
116
- }
117
-
118
- const unsubscribeTool = vp.toolManager.onChange((name) => ontoolchange?.(name));
119
- viewport = vp;
98
+ mounted = handle;
99
+ viewport = handle.viewport;
120
100
 
121
101
  return () => {
122
- if (timer !== undefined) {
123
- clearTimeout(timer);
124
- try {
125
- emitChange();
126
- } catch (error) {
127
- // A failing host save must not leak the viewport or throw out of unmount.
128
- if (typeof reportError === 'function') reportError(error);
129
- else console.error('[fieldnotes] onChange failed during unmount', error);
130
- }
131
- }
132
- unsubscribeTool();
133
- unsubscribe();
134
- vp.destroy();
102
+ handle.destroy();
103
+ mounted = undefined;
135
104
  viewport = null;
136
105
  };
137
106
  });
138
107
 
139
108
  $effect(() => {
140
109
  if (!viewport || tool === undefined) return;
141
- if (viewport.toolManager.activeTool?.name !== tool) viewport.setTool(tool);
110
+ mounted?.setTool(tool);
142
111
  });
143
112
 
144
113
  $effect(() => {
@@ -148,9 +117,7 @@
148
117
 
149
118
  $effect(() => {
150
119
  if (!viewport || !Array.isArray(tools)) return;
151
- for (const t of tools) {
152
- if (!viewport.toolManager.getTool(t.name)) viewport.toolManager.register(t);
153
- }
120
+ mounted?.addTools(tools);
154
121
  });
155
122
  </script>
156
123
 
@@ -1,6 +1,5 @@
1
1
  import type { Snippet } from 'svelte';
2
- import type { ViewportOptions, Tool, CanvasState, ImportableCanvasState } from '@fieldnotes/core';
3
- import { Viewport } from '@fieldnotes/core';
2
+ import type { ViewportOptions, Tool, CanvasState, ImportableCanvasState, Viewport } from '@fieldnotes/core';
4
3
  export interface FieldNotesCanvasProps {
5
4
  /** Constructor-only: changes after mount are ignored (the canvas is stateful). */
6
5
  options?: ViewportOptions;
@@ -1 +1 @@
1
- {"version":3,"file":"FieldNotesCanvas.svelte.d.ts","sourceRoot":"","sources":["../src/FieldNotesCanvas.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAClG,OAAO,EAAE,QAAQ,EAAsB,MAAM,kBAAkB,CAAC;AAEhE,MAAM,WAAW,qBAAqB;IACpC,kFAAkF;IAClF,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC;IAC3B,iFAAiF;IACjF,WAAW,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAAC;IAC7C,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,sFAAsF;IACtF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAC;IACvC,uFAAuF;IACvF,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACxC,8EAA8E;IAC9E,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wFAAwF;IACxF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAkIH,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"FieldNotesCanvas.svelte.d.ts","sourceRoot":"","sources":["../src/FieldNotesCanvas.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EACV,eAAe,EACf,IAAI,EACJ,WAAW,EACX,qBAAqB,EACrB,QAAQ,EACT,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,qBAAqB;IACpC,kFAAkF;IAClF,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC;IAC3B,iFAAiF;IACjF,WAAW,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAAC;IAC7C,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,sFAAsF;IACtF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAC;IACvC,uFAAuF;IACvF,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACxC,8EAA8E;IAC9E,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wFAAwF;IACxF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA8FH,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fieldnotes/svelte",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Svelte wrapper for Field Notes canvas SDK",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -37,7 +37,7 @@
37
37
  "fieldnotes"
38
38
  ],
39
39
  "peerDependencies": {
40
- "@fieldnotes/core": ">=0.96.0 <1.0.0",
40
+ "@fieldnotes/core": ">=0.98.0 <1.0.0",
41
41
  "svelte": ">=5 <6"
42
42
  },
43
43
  "devDependencies": {
@@ -49,7 +49,7 @@
49
49
  "svelte": "^5.57.2",
50
50
  "svelte-check": "^4.7.7",
51
51
  "vitest": "^5.0.5",
52
- "@fieldnotes/core": "0.96.0"
52
+ "@fieldnotes/core": "0.98.0"
53
53
  },
54
54
  "scripts": {
55
55
  "build": "svelte-package -i src -o dist",