@fieldnotes/svelte 0.0.0-stage → 0.1.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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +215 -2
  3. package/dist/CanvasElement.svelte +122 -0
  4. package/dist/CanvasElement.svelte.d.ts +25 -0
  5. package/dist/CanvasElement.svelte.d.ts.map +1 -0
  6. package/dist/FieldNotesCanvas.svelte +161 -0
  7. package/dist/FieldNotesCanvas.svelte.d.ts +42 -0
  8. package/dist/FieldNotesCanvas.svelte.d.ts.map +1 -0
  9. package/dist/Minimap.svelte +85 -0
  10. package/dist/Minimap.svelte.d.ts +25 -0
  11. package/dist/Minimap.svelte.d.ts.map +1 -0
  12. package/dist/ViewportProvider.svelte +13 -0
  13. package/dist/ViewportProvider.svelte.d.ts +10 -0
  14. package/dist/ViewportProvider.svelte.d.ts.map +1 -0
  15. package/dist/context.d.ts +6 -0
  16. package/dist/context.d.ts.map +1 -0
  17. package/dist/context.js +14 -0
  18. package/dist/helpers/from-source.svelte.d.ts +5 -0
  19. package/dist/helpers/from-source.svelte.d.ts.map +1 -0
  20. package/dist/helpers/from-source.svelte.js +25 -0
  21. package/dist/helpers/use-active-tool.svelte.d.ts +4 -0
  22. package/dist/helpers/use-active-tool.svelte.d.ts.map +1 -0
  23. package/dist/helpers/use-active-tool.svelte.js +8 -0
  24. package/dist/helpers/use-camera.svelte.d.ts +5 -0
  25. package/dist/helpers/use-camera.svelte.d.ts.map +1 -0
  26. package/dist/helpers/use-camera.svelte.js +7 -0
  27. package/dist/helpers/use-element-rects.svelte.d.ts +9 -0
  28. package/dist/helpers/use-element-rects.svelte.d.ts.map +1 -0
  29. package/dist/helpers/use-element-rects.svelte.js +17 -0
  30. package/dist/helpers/use-elements.svelte.d.ts +16 -0
  31. package/dist/helpers/use-elements.svelte.d.ts.map +1 -0
  32. package/dist/helpers/use-elements.svelte.js +10 -0
  33. package/dist/helpers/use-history.svelte.d.ts +10 -0
  34. package/dist/helpers/use-history.svelte.d.ts.map +1 -0
  35. package/dist/helpers/use-history.svelte.js +13 -0
  36. package/dist/helpers/use-layers.svelte.d.ts +21 -0
  37. package/dist/helpers/use-layers.svelte.d.ts.map +1 -0
  38. package/dist/helpers/use-layers.svelte.js +25 -0
  39. package/dist/helpers/use-selection-ops.svelte.d.ts +22 -0
  40. package/dist/helpers/use-selection-ops.svelte.d.ts.map +1 -0
  41. package/dist/helpers/use-selection-ops.svelte.js +27 -0
  42. package/dist/helpers/use-selection-style-details.svelte.d.ts +12 -0
  43. package/dist/helpers/use-selection-style-details.svelte.d.ts.map +1 -0
  44. package/dist/helpers/use-selection-style-details.svelte.js +15 -0
  45. package/dist/helpers/use-selection-style.svelte.d.ts +12 -0
  46. package/dist/helpers/use-selection-style.svelte.d.ts.map +1 -0
  47. package/dist/helpers/use-selection-style.svelte.js +15 -0
  48. package/dist/helpers/use-selection.svelte.d.ts +4 -0
  49. package/dist/helpers/use-selection.svelte.d.ts.map +1 -0
  50. package/dist/helpers/use-selection.svelte.js +7 -0
  51. package/dist/helpers/use-tool-options.svelte.d.ts +8 -0
  52. package/dist/helpers/use-tool-options.svelte.d.ts.map +1 -0
  53. package/dist/helpers/use-tool-options.svelte.js +28 -0
  54. package/dist/index.d.ts +23 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +15 -0
  57. package/package.json +58 -3
package/LICENSE ADDED
@@ -0,0 +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.
package/README.md CHANGED
@@ -1,3 +1,216 @@
1
- # Temporary Holding Version
1
+ # @fieldnotes/svelte
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Svelte bindings for the [Field Notes](https://github.com/IrakliDevelop/fieldnotes) infinite canvas SDK. Embed Svelte components directly onto an infinite, pannable, zoomable canvas.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @fieldnotes/core @fieldnotes/svelte
9
+ ```
10
+
11
+ Requires Svelte 5 (`svelte >=5 <6`, runes) and `@fieldnotes/core >=0.96.0`. The package ships
12
+ `.svelte` sources, so your bundler compiles them with your Svelte version (Vite with
13
+ `@sveltejs/vite-plugin-svelte`, or SvelteKit).
14
+
15
+ ## Quick Start
16
+
17
+ Full runnable example: `examples/svelte` — `pnpm --filter fieldnotes-svelte-example dev`
18
+
19
+ The canvas needs a container with a defined size — it fills whatever it's given. With no `tools`
20
+ prop and no tools registered in `onready`, it registers the default tool bundle
21
+ (`createDefaultTools()`: hand, select, pencil, eraser, arrow, note, text, shape) with `select`
22
+ active, so a persisting board is one component:
23
+
24
+ ```svelte
25
+ <script lang="ts">
26
+ import { FieldNotesCanvas } from '@fieldnotes/svelte';
27
+ import type { CanvasState } from '@fieldnotes/core';
28
+
29
+ const saved = localStorage.getItem('board') ?? undefined;
30
+ const save = (state: CanvasState) => localStorage.setItem('board', JSON.stringify(state));
31
+ </script>
32
+
33
+ <div style="width: 100vw; height: 100vh">
34
+ <FieldNotesCanvas initialData={saved} onchange={save} style="width: 100%; height: 100%" />
35
+ </div>
36
+ ```
37
+
38
+ `initialData` takes a saved state object or JSON string. `onchange` receives `viewport.exportState()`,
39
+ trailing-debounced by `changeDebounceMs` (default 250); a pending call is flushed on unmount, and
40
+ the initial load is not reported as a change.
41
+
42
+ For your own toolbar, pass an explicit tool array and control the active tool with `tool` and
43
+ `ontoolchange`. Keep `tools` a module constant or `$state.raw([...])`, never a deep `$state` proxy:
44
+
45
+ ```svelte
46
+ <script module lang="ts">
47
+ import { HandTool, SelectTool, PencilTool, EraserTool, NoteTool, ShapeTool } from '@fieldnotes/core';
48
+
49
+ const TOOLS = [
50
+ new HandTool(),
51
+ new SelectTool(),
52
+ new PencilTool(),
53
+ new EraserTool(),
54
+ new NoteTool(),
55
+ new ShapeTool(),
56
+ ];
57
+ </script>
58
+
59
+ <script lang="ts">
60
+ import { FieldNotesCanvas } from '@fieldnotes/svelte';
61
+ import Toolbar from './Toolbar.svelte';
62
+
63
+ let tool = $state('select');
64
+ const setTool = (name: string) => (tool = name);
65
+ </script>
66
+
67
+ <FieldNotesCanvas tools={TOOLS} {tool} ontoolchange={setTool} style="width: 100vw; height: 100vh">
68
+ <Toolbar {tool} onselect={setTool} />
69
+ </FieldNotesCanvas>
70
+ ```
71
+
72
+ ## Props
73
+
74
+ Not all props are reactive — the canvas is stateful. Changing a mount-only prop after mount has no effect:
75
+
76
+ | Prop | Reactivity |
77
+ | ------------------------------- | ---------------------------------------------------------------------------- |
78
+ | `options` | Mount-only — the canvas is stateful; construct with the right options |
79
+ | `tools` | Array: reactive, append-only. Omitted or `'default'`: mount-only (see below) |
80
+ | `initialData` | Mount-only — loaded once before `onready` |
81
+ | `onchange` / `changeDebounceMs` | Reactive — the latest callback is used without resubscribing |
82
+ | `tool` / `ontoolchange` | Reactive (controlled) |
83
+ | `defaultTool` | Mount-only (uncontrolled initial tool); ignored when `tool` is set |
84
+ | `snapToGrid` (deprecated) | Reactive compatibility prop; prefer a domain constraint service |
85
+ | `class` / `style` / `children` | Reactive; `class` and `style` apply to the root `<div>` |
86
+
87
+ `tools` as an array registers each tool once (tools cannot be unregistered). `tools="default"`
88
+ registers `createDefaultTools()` at mount, before `onready` (so you can customise them there).
89
+ Omitted registers the same bundle after `onready` only when neither `options.tools` is set nor any
90
+ tool was registered by then, then activates `tool`, `defaultTool` or `select`; hosts that register
91
+ tools in `onready` get no defaults. `tools={[]}` registers nothing.
92
+
93
+ `tool` is a plain prop, not `$bindable`: a core tool switch (keyboard or API) never reverts or
94
+ overwrites it. Update your state from `ontoolchange`, as in the example above.
95
+
96
+ - **`onready(viewport)`** fires once after the `Viewport` is created, tools are registered and
97
+ `initialData` is loaded. If it throws, the viewport is destroyed and the error is rethrown.
98
+ - **`onchange(state)`** — see Quick Start. If the flush on unmount throws, the viewport is still
99
+ destroyed and the error is passed to `reportError`.
100
+ - **`ontoolchange(name)`** fires whenever the active tool changes (keyboard, API, or the `tool` prop).
101
+
102
+ `children` render only once the viewport exists, so components inside can call `useViewport()` and
103
+ the helpers during init.
104
+
105
+ ## Embedding Svelte Components
106
+
107
+ The headline feature — render any Svelte subtree as a canvas element that pans, zooms, and
108
+ resizes with the canvas:
109
+
110
+ ```svelte
111
+ <FieldNotesCanvas style="width: 100vw; height: 100vh">
112
+ <CanvasElement position={{ x: 100, y: 200 }} size={{ w: 300, h: 200 }}>
113
+ <MyCard />
114
+ </CanvasElement>
115
+ <CanvasElement position={{ x: 500, y: 100 }}>
116
+ <button onclick={clicked}>Interactive button on the canvas</button>
117
+ </CanvasElement>
118
+ </FieldNotesCanvas>
119
+ ```
120
+
121
+ Embedded components use a **two-mode interaction model**: by default they can be selected, dragged, and resized. **Double-click** to enter interact mode (clicks, inputs, forms work). **Escape** or click outside to exit.
122
+
123
+ `position` is required; `size` is optional — omit it to let the element size to its content. Both
124
+ props are reactive and compared by value: new objects with equal coordinates do not touch the
125
+ store. The children keep their component context (`useViewport()` works inside), and keyed
126
+ reorders of `CanvasElement`s leave their content on the canvas.
127
+
128
+ `<CanvasElement>` is **host-owned**: Svelte is the source of truth for its element. Its store mutations carry `origin: 'host'`, so mounting, moving, resizing and unmounting never create undo steps and are never sent to a sync relay (the DOM subtree exists only in this document). The element is **transient**: `exportState()` / `exportJSON()` omit it, so a saved canvas never contains a ghost of a Svelte embed. After a store `clear` (a user's clear-canvas gesture or a remote one) or a `loadState()` the component re-registers itself, so the embed survives. To persist an embed across reloads, use the viewport's `registerHtmlRenderer` with an `htmlType` and serializable `data` instead.
129
+
130
+ ## Minimap
131
+
132
+ ```svelte
133
+ <Minimap class="minimap" width={200} height={140} />
134
+ ```
135
+
136
+ A collapsible thumbnail navigator. Props: `width` (default `200`), `height` (default `140`),
137
+ `defaultCollapsed` (uncontrolled, default `false`), `background`, `viewportStroke`, and `class` /
138
+ `style` for positioning, which is yours. While collapsed no controller exists: zero listeners,
139
+ zero rendering. Works inside `<FieldNotesCanvas>` or under `setViewportContext(viewport)`.
140
+
141
+ ## Helpers
142
+
143
+ Each helper has the same name as its `@fieldnotes/react` hook and returns Svelte stores. Call it
144
+ during component init (the top level of `<script>`) inside `<FieldNotesCanvas>` or under
145
+ `setViewportContext`; a getter `useToolOptions` name is tracked with `$effect.pre`, which only
146
+ works there. Stores subscribe to the viewport while they have subscribers, so `$store` in the
147
+ template is all you need. Single values are `Readable`s; tuples hold a store and a setter; result
148
+ objects are a `Readable` of the state that also carries the action functions (`$history.canUndo`,
149
+ `history.undo()`).
150
+
151
+ | Helper | Returns |
152
+ | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
153
+ | `useViewport()` | The `Viewport`; throws outside a canvas |
154
+ | `setViewportContext(viewport)` | Provides a host-created viewport to descendants (call during init) |
155
+ | `useCamera()` | `Readable<{ x, y, zoom }>` |
156
+ | `useElements(type?)` | `Readable<CanvasElement[]>`, all or one type; `type` is fixed at init |
157
+ | `useElements(selector, isEqual?)` | `Readable<R>`; updates only when the selected value changes (default: one-level shallow compare) |
158
+ | `useSelection()` | `Readable<string[]>`, a stable array that changes only with the selection |
159
+ | `useSelectionStyle()` | `[style, applyStyle]` — `style` is `Readable<ElementStyle \| null>`; `applyStyle(patch)` is one undo step |
160
+ | `useSelectionStyleDetails()` | `[details, applyStyle]` — per-field applicability, mixed state and common style |
161
+ | `useSelectionOps()` | `$ops`: `selectedIds`, `selectedCount`, `canGroup`, `canUngroup`, `canAlign`, `canDistribute`, `isLocked`; `ops.group`, `ungroup`, `toggleLock`, `align`, `distribute` |
162
+ | `useHistory()` | `$history`: `canUndo`, `canRedo`; `history.undo`, `redo` |
163
+ | `useLayers()` | `$layers`: `layers`, `activeLayerId`; `layers.createLayer`, `removeLayer`, `renameLayer`, `reorderLayer`, `setVisible`, `setLocked`, `setOpacity`, `setActiveLayer`, `moveElement` |
164
+ | `useActiveTool()` | `[name, setTool]`; `$name` is `''` with no active tool |
165
+ | `useToolOptions(name)` | `[options, setOptions]`; `$options` is `null` for an unknown tool or one without options. A getter `() => name` is tracked |
166
+ | `useElementRects(match)` | `Readable<readonly ElementRect[]>`; ignores camera motion. `match` is fixed at init |
167
+
168
+ ```svelte
169
+ <script lang="ts">
170
+ import { useElements, useHistory } from '@fieldnotes/svelte';
171
+ import type { CanvasElement } from '@fieldnotes/core';
172
+
173
+ // A selector updates only when its result changes; plain useElements() updates on every mutation.
174
+ const count = useElements((els: CanvasElement[]) => els.length);
175
+ const notes = useElements('note');
176
+ const history = useHistory();
177
+ </script>
178
+
179
+ <p>{$count} elements, {$notes.length} notes</p>
180
+ <button onclick={history.undo} disabled={!$history.canUndo}>Undo</button>
181
+ <button onclick={history.redo} disabled={!$history.canRedo}>Redo</button>
182
+ ```
183
+
184
+ Selectors returning nested fresh objects need a custom `isEqual`, as in React.
185
+
186
+ ## Escape Hatch
187
+
188
+ `useViewport()` returns the raw `Viewport` for anything the helpers do not cover. Outside the
189
+ canvas, keep the instance `onready` hands you in `$state.raw` (never deep `$state`); it is
190
+ destroyed when the canvas unmounts. See the [core README](../core/README.md) for the full
191
+ `Viewport` API.
192
+
193
+ ## Server-Side Rendering
194
+
195
+ Importing the package and server-rendering a page that contains `<FieldNotesCanvas>`
196
+ (`svelte/server` `render`, SvelteKit SSR) does not throw: the server renders an empty container
197
+ and the canvas mounts on the client. `children`, `onready` and the helpers run only on the client.
198
+
199
+ ## Lifecycle
200
+
201
+ The viewport is created in `onMount` and destroyed exactly once when the component is destroyed.
202
+ Svelte tears down the children first — `CanvasElement`s remove their elements and helper stores
203
+ unsubscribe while the viewport is still alive — and then the canvas flushes a pending `onchange`
204
+ and destroys the viewport. `@fieldnotes/react` and `@fieldnotes/vue` destroy the viewport before
205
+ their children unmount.
206
+
207
+ ## Versioning
208
+
209
+ `@fieldnotes/core` and `@fieldnotes/svelte` are versioned independently. The package's
210
+ `peerDependencies` declare the compatible core range (`>=0.96.0 <1.0.0`). Pre-1.0, minor versions
211
+ may contain breaking changes; if a core minor ever breaks the wrapper, a coordinated release raises
212
+ the lower bound.
213
+
214
+ ## License
215
+
216
+ MIT
@@ -0,0 +1,122 @@
1
+ <script module lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ /** Props for {@link CanvasElement}. */
5
+ export interface CanvasElementProps {
6
+ /** Canvas-space position of the element's top-left corner. */
7
+ position: { x: number; y: number };
8
+ /** Optional fixed size; omit to let the element size to its content. */
9
+ size?: { w: number; h: number };
10
+ children: Snippet;
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
+ </script>
19
+
20
+ <!--
21
+ @component
22
+ Renders its children as an HTML element embedded in the canvas coordinate space.
23
+ The element is host-owned and transient: it is driven entirely by props, never appears
24
+ in exported state, and never records an undo step.
25
+ Must be used inside a `<FieldNotesCanvas>`.
26
+ -->
27
+ <script lang="ts">
28
+ import { onMount } from 'svelte';
29
+ import { useViewport } from './context.js';
30
+
31
+ const { position, size, children }: CanvasElementProps = $props();
32
+ const viewport = useViewport();
33
+
34
+ // Derived per value, so new props with equal geometry do not update the store.
35
+ const x = $derived(position.x);
36
+ const y = $derived(position.y);
37
+ const w = $derived(size?.w);
38
+ const h = $derived(size?.h);
39
+ const geometry = () => ({
40
+ position: { x, y },
41
+ size: w === undefined || h === undefined ? undefined : { w, h },
42
+ });
43
+
44
+ let container: HTMLDivElement;
45
+ let elementId: string | null = null;
46
+
47
+ 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
+ };
98
+ });
99
+
100
+ let registered = false;
101
+ $effect(() => {
102
+ 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();
116
+ });
117
+ </script>
118
+
119
+ <!-- Only the inner div moves into the canvas: Svelte moves its own root on keyed reorders. -->
120
+ <div style="display: contents">
121
+ <div bind:this={container} style="width: 100%; height: 100%">{@render children()}</div>
122
+ </div>
@@ -0,0 +1,25 @@
1
+ import type { Snippet } from 'svelte';
2
+ /** Props for {@link CanvasElement}. */
3
+ export interface CanvasElementProps {
4
+ /** Canvas-space position of the element's top-left corner. */
5
+ position: {
6
+ x: number;
7
+ y: number;
8
+ };
9
+ /** Optional fixed size; omit to let the element size to its content. */
10
+ size?: {
11
+ w: number;
12
+ h: number;
13
+ };
14
+ children: Snippet;
15
+ }
16
+ /**
17
+ * Renders its children as an HTML element embedded in the canvas coordinate space.
18
+ * The element is host-owned and transient: it is driven entirely by props, never appears
19
+ * in exported state, and never records an undo step.
20
+ * Must be used inside a `<FieldNotesCanvas>`.
21
+ */
22
+ declare const CanvasElement: import("svelte").Component<CanvasElementProps, {}, "">;
23
+ type CanvasElement = ReturnType<typeof CanvasElement>;
24
+ export default CanvasElement;
25
+ //# sourceMappingURL=CanvasElement.svelte.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,161 @@
1
+ <script module lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { ViewportOptions, Tool, CanvasState, ImportableCanvasState } from '@fieldnotes/core';
4
+ import { Viewport, createDefaultTools } from '@fieldnotes/core';
5
+
6
+ export interface FieldNotesCanvasProps {
7
+ /** Constructor-only: changes after mount are ignored (the canvas is stateful). */
8
+ options?: ViewportOptions;
9
+ /**
10
+ * `'default'` (mount-only): `createDefaultTools()` registered before `onready`, `select` active.
11
+ * Omitted: the same bundle after `onready`, only if no tools are registered by then.
12
+ * An array is reactive (append-only): tools present here are registered; removal is not
13
+ * supported, and `[]` registers nothing.
14
+ */
15
+ tools?: Tool[] | 'default';
16
+ /** Mount-only: a saved state (object or JSON string) loaded before `onready`. */
17
+ initialData?: ImportableCanvasState | string;
18
+ /** Debounce window for `onchange` in milliseconds (default 250). */
19
+ changeDebounceMs?: number;
20
+ /** Uncontrolled initial tool. Ignored when `tool` is set. */
21
+ defaultTool?: string;
22
+ /** Controlled active tool. Pair with `ontoolchange`. */
23
+ tool?: string;
24
+ /**
25
+ * Reactive compatibility toggle for the core snap flag.
26
+ * @deprecated Grid snapping is domain behavior. Configure it through the
27
+ * VTT grid/constraint service instead.
28
+ */
29
+ snapToGrid?: boolean;
30
+ /** Called once the viewport exists; a throw destroys the viewport and is rethrown. */
31
+ onready?: (viewport: Viewport) => void;
32
+ /** Trailing-debounced with the exported state after any change; flushed on unmount. */
33
+ onchange?: (state: CanvasState) => void;
34
+ /** Fires whenever the active tool changes (keyboard, API, or `tool` prop). */
35
+ ontoolchange?: (name: string) => void;
36
+ class?: string;
37
+ style?: string;
38
+ /** Rendered inside the canvas once the viewport exists; `useViewport()` works there. */
39
+ children?: Snippet;
40
+ }
41
+ </script>
42
+
43
+ <script lang="ts">
44
+ import { onMount } from 'svelte';
45
+ import ViewportProvider from './ViewportProvider.svelte';
46
+
47
+ const {
48
+ options,
49
+ tools,
50
+ initialData,
51
+ changeDebounceMs = 250,
52
+ defaultTool,
53
+ tool,
54
+ snapToGrid,
55
+ onready,
56
+ onchange,
57
+ ontoolchange,
58
+ class: className,
59
+ style,
60
+ children,
61
+ }: FieldNotesCanvasProps = $props();
62
+
63
+ let root: HTMLDivElement;
64
+ let viewport = $state.raw<Viewport | null>(null);
65
+
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);
80
+
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);
98
+ });
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;
120
+
121
+ 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();
135
+ viewport = null;
136
+ };
137
+ });
138
+
139
+ $effect(() => {
140
+ if (!viewport || tool === undefined) return;
141
+ if (viewport.toolManager.activeTool?.name !== tool) viewport.setTool(tool);
142
+ });
143
+
144
+ $effect(() => {
145
+ if (!viewport || snapToGrid === undefined) return;
146
+ viewport.setSnapToGrid(snapToGrid);
147
+ });
148
+
149
+ $effect(() => {
150
+ if (!viewport || !Array.isArray(tools)) return;
151
+ for (const t of tools) {
152
+ if (!viewport.toolManager.getTool(t.name)) viewport.toolManager.register(t);
153
+ }
154
+ });
155
+ </script>
156
+
157
+ <div bind:this={root} class={className} {style}>
158
+ {#if viewport}
159
+ <ViewportProvider {viewport}>{@render children?.()}</ViewportProvider>
160
+ {/if}
161
+ </div>
@@ -0,0 +1,42 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { ViewportOptions, Tool, CanvasState, ImportableCanvasState } from '@fieldnotes/core';
3
+ import { Viewport } from '@fieldnotes/core';
4
+ export interface FieldNotesCanvasProps {
5
+ /** Constructor-only: changes after mount are ignored (the canvas is stateful). */
6
+ options?: ViewportOptions;
7
+ /**
8
+ * `'default'` (mount-only): `createDefaultTools()` registered before `onready`, `select` active.
9
+ * Omitted: the same bundle after `onready`, only if no tools are registered by then.
10
+ * An array is reactive (append-only): tools present here are registered; removal is not
11
+ * supported, and `[]` registers nothing.
12
+ */
13
+ tools?: Tool[] | 'default';
14
+ /** Mount-only: a saved state (object or JSON string) loaded before `onready`. */
15
+ initialData?: ImportableCanvasState | string;
16
+ /** Debounce window for `onchange` in milliseconds (default 250). */
17
+ changeDebounceMs?: number;
18
+ /** Uncontrolled initial tool. Ignored when `tool` is set. */
19
+ defaultTool?: string;
20
+ /** Controlled active tool. Pair with `ontoolchange`. */
21
+ tool?: string;
22
+ /**
23
+ * Reactive compatibility toggle for the core snap flag.
24
+ * @deprecated Grid snapping is domain behavior. Configure it through the
25
+ * VTT grid/constraint service instead.
26
+ */
27
+ snapToGrid?: boolean;
28
+ /** Called once the viewport exists; a throw destroys the viewport and is rethrown. */
29
+ onready?: (viewport: Viewport) => void;
30
+ /** Trailing-debounced with the exported state after any change; flushed on unmount. */
31
+ onchange?: (state: CanvasState) => void;
32
+ /** Fires whenever the active tool changes (keyboard, API, or `tool` prop). */
33
+ ontoolchange?: (name: string) => void;
34
+ class?: string;
35
+ style?: string;
36
+ /** Rendered inside the canvas once the viewport exists; `useViewport()` works there. */
37
+ children?: Snippet;
38
+ }
39
+ declare const FieldNotesCanvas: import("svelte").Component<FieldNotesCanvasProps, {}, "">;
40
+ type FieldNotesCanvas = ReturnType<typeof FieldNotesCanvas>;
41
+ export default FieldNotesCanvas;
42
+ //# sourceMappingURL=FieldNotesCanvas.svelte.d.ts.map
@@ -0,0 +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"}