@atom.io/template-preact-realtime-vector-editor 0.0.0-stage → 0.0.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/.oxlintrc.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3
+ "extends": ["./node_modules/atom.io/oxlint.json"],
4
+ "plugins": ["typescript", "react"],
5
+ "rules": {
6
+ "react/jsx-key": "error",
7
+ "react/jsx-no-duplicate-props": "error",
8
+ "react/no-children-prop": "error",
9
+ "react/no-danger-with-children": "error",
10
+ "react/rules-of-hooks": "error"
11
+ }
12
+ }
package/INNOVATIONS.md ADDED
@@ -0,0 +1,43 @@
1
+ # Innovations and boundaries
2
+
3
+ ## Integrity is the model boundary
4
+
5
+ Plane's durable object is one closed contour value. It contains an ordered list of three to sixty-four uniquely identified finite points. The renderer joins adjacent points and closes the final point to the first. There is no durable edge registry, path order, subpath order, curve control, or close command.
6
+
7
+ That representation removes an entire class of reconciliation bugs. Every node has exactly two implicit neighbors. Branching, dangling edges, missing close operations, cross-member identity mismatches, and partially undone structural batches have no representable object state.
8
+
9
+ The schema validates proposals, checkpoint hydration, and direct reducer use. The reducer parses before accumulation, so application code cannot bypass integrity by calling it locally.
10
+
11
+ ## One durable member and complete history states
12
+
13
+ The `contour` member is a deterministic register of complete validated contour snapshots. A pointer-up, insertion, deletion, or replacement contributes one operation to that member. Undo and redo compensate only the authenticated actor's operations, then materialize another complete validated snapshot. Foreign snapshots remain active.
14
+
15
+ The collaboration regression exercises the failure that motivated this design: one actor inserts a node, another actor moves that node, and the creator undoes the insertion. The participating actor's complete contour remains visible and valid. Undoing that participation returns to the valid default contour. The test checks the server and both clients and proves every visible node has degree two.
16
+
17
+ The deliberate tradeoff is coarse contention. Simultaneous gestures based on the same contour are complete alternatives, and deterministic register order chooses one. This small example prefers an obvious integrity boundary over teaching an application-specific operation algebra. A product that needs disjoint contour edits to merge should define a single contour model whose operations replay into a validated contour; it should not split topology across independently undoable members.
18
+
19
+ ## The richer SVG tutorial stays separate
20
+
21
+ The local `preact-svg-editor` still demonstrates paths, subpaths, line and cubic edges, ordering, and local transactions. Plane uses the constrained `./contour` export from that package. This keeps the reusable contour schema, reducer, Domain, and editor in one module without pretending that the Bezier tutorial's representational freedom is safe for this collaborative product.
22
+
23
+ The legacy multi-member editor also adopts the complete subpath when a participant reorders it. That compatibility hardening prevents a foreign order receipt from outliving the creator's subpath, node, and edge receipts. Plane does not rely on that repair; its normal path has no independent order or edge members.
24
+
25
+ ## Presence is lossy and session-scoped
26
+
27
+ Collaborator presence carries simulated identity, logical-coordinate pointer, and selected node ID. Drag presence carries one node ID and preview point. Both are ephemeral, authenticated by actor and session, absent from durable history, and cleared by disconnect or expiry. DOM elements, pointer capture, zoom, pan, and workspace selection remain local.
28
+
29
+ Here, “authenticated” means bound to the actor and session accepted by the Domain controllers. The template handshake only allowlists simulated actors and can be impersonated. A deployed application must derive those values from verified credentials and provide its own batch authorization policy.
30
+
31
+ The UI publishes pointer movement without waiting for acknowledgement. Pointer-up is the durable boundary.
32
+
33
+ ## Recovery and restart
34
+
35
+ The Node service accepts a `MosaicDomainCheckpointStorageAdapter`. It serializes proposals with checkpoint publication so member reads belong to the accepted revision being published. Checkpoint compaction keeps the visible contour and protected receipts, and a fresh server generation reconstructs the same validated contour before serving recovery.
36
+
37
+ This template keeps one small contour resident and uses the reference in-memory checkpoint adapter. Large design applications should provide durable storage and application-defined residency cuts. Scale policy belongs outside the contour reducer.
38
+
39
+ ## Renderer and transport boundaries
40
+
41
+ The shipped application renders with Preact. Its `multiClient` fixture uses Testing Library's React renderer to drive the same Socket.IO client runtime, so React and ReactDOM remain test-only dependencies.
42
+
43
+ The browser connects to its current origin. Vite proxies `/socket.io` and `/health` to the Node process in development and preview. Event names are application protocol; validation, atomic settlement, recovery, presence expiry, history, and actor authorization remain public Atom.io controllers.
package/README.md CHANGED
@@ -1,3 +1,17 @@
1
- # Temporary Holding Version
1
+ # Plane: Preact realtime contour editor
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
+ Plane is a deliberately small collaborative editor for one closed, straight-line contour. It demonstrates a design application built on an Atom.io Mosaic Domain without asking a tutorial model to carry paths, subpaths, curves, edge handles, and independent topology registries.
4
+
5
+ Run `npm run dev`, then open two tabs and choose different simulated identities from the header. Vite proxies both health and Socket.IO traffic to the Node service, so the browser connects through `window.location.origin`.
6
+
7
+ The identity picker is an exploration aid, not authentication. Before deployment, derive actor and session identity from verified credentials and supply an application authorization policy to the collaboration service.
8
+
9
+ The canvas supports node dragging, node insertion and deletion, private zoom, advisory focus, lossy remote pointers and drag previews, actor-selective undo and redo, offline replay, and visible pending or rejected work. A contour always has three to sixty-four uniquely identified finite points. Straight edges and closure are implicit, so a dangling edge, branch, curve, or node with degree other than two cannot be represented.
10
+
11
+ The separate `preact-svg-editor` application remains the richer local Bezier tutorial. Plane imports its constrained contour module instead of putting that richer topology into a collaborative Domain.
12
+
13
+ Read `INNOVATIONS.md` before adapting the example. It explains why the integrity boundary is one durable aggregate, what selective history can expose, and the deliberate concurrency tradeoff of this spartan model.
14
+
15
+ ## Quality Checks
16
+
17
+ Run `npm run lint` before committing. It enforces ESLint, type-aware Oxlint, React hook call-order correctness, TypeScript, and CSS-module reachability with zero warnings. Run `npm run fmt:check` in CI and `npm run fmt` to apply the pinned dprint configuration locally.
package/_gitignore ADDED
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ dist
3
+ .turbo
4
+ *.local
5
+ *.log
6
+ *.tsbuildinfo
package/dprint.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://dprint.dev/schemas/v0.json",
3
+ "lineWidth": 81,
4
+ "useTabs": true,
5
+ "indentWidth": 2,
6
+ "includes": ["**/*.{ts,tsx,js,jsx,mjs,cjs,json,jsonc,md,mdx,css}"],
7
+ "excludes": [
8
+ "**/dist/**",
9
+ "**/node_modules/**",
10
+ "**/playwright-report/**",
11
+ "**/test-results/**"
12
+ ],
13
+ "oxc": {
14
+ "associations": ["**/*.{ts,tsx,js,jsx,mjs,cjs}"],
15
+ "semicolons": "asNeeded",
16
+ "trailingCommas": "all"
17
+ },
18
+ "json": {
19
+ "associations": ["**/*.{json,jsonc}"]
20
+ },
21
+ "markdown": {
22
+ "associations": ["**/*.{md,mdx}"]
23
+ },
24
+ "malva": {
25
+ "associations": ["**/*.css"]
26
+ },
27
+ "plugins": [
28
+ "./node_modules/@dprint/oxc/plugin.wasm",
29
+ "./node_modules/@dprint/json/plugin.wasm",
30
+ "./node_modules/@dprint/markdown/plugin.wasm",
31
+ "./node_modules/dprint-plugin-malva/plugin.wasm"
32
+ ]
33
+ }
@@ -0,0 +1,37 @@
1
+ import * as tsParser from "@typescript-eslint/parser"
2
+ import AtomIOPlugin from "@atom.io/eslint-plugin"
3
+ import type { Linter } from "eslint"
4
+ import LasertagPlugin from "lasertag/eslint-plugin"
5
+
6
+ const ERROR = 2
7
+
8
+ export default [
9
+ { ignores: [`**/dist/**`, `**/node_modules/**`] },
10
+ {
11
+ files: [`**/*.ts`, `**/*.tsx`],
12
+ languageOptions: {
13
+ parser: tsParser,
14
+ parserOptions: { projectService: true, sourceType: `module` },
15
+ },
16
+ plugins: { "atom.io": AtomIOPlugin },
17
+ rules: {
18
+ "atom.io/exact-catch-types": ERROR,
19
+ "atom.io/explicit-state-types": [ERROR, { permitAnnotation: true }],
20
+ "atom.io/naming-convention": ERROR,
21
+ quotes: [ERROR, `backtick`],
22
+ },
23
+ },
24
+ {
25
+ files: [`src/**/*.tsx`],
26
+ plugins: { lasertag: LasertagPlugin },
27
+ rules: {
28
+ "lasertag/access-css-module-class-only": ERROR,
29
+ "lasertag/ban-div": ERROR,
30
+ "lasertag/export-own-component-only": ERROR,
31
+ "lasertag/header-main-footer-as-group": ERROR,
32
+ "lasertag/import-own-css-module-only": ERROR,
33
+ "lasertag/name-imported-css-module-as-css": ERROR,
34
+ "lasertag/render-tag-with-own-name": ERROR,
35
+ },
36
+ },
37
+ ] satisfies Linter.Config[]
package/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="description" content="Plane, a realtime vector editor built with Atom.io Mosaic Domains." />
7
+ <title>Plane · Shared vectors</title>
8
+ </head>
9
+ <body>
10
+ <main id="root"></main>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
package/mise.toml ADDED
@@ -0,0 +1 @@
1
+ [tools]
package/node/server.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { createServer } from "node:http"
2
+
3
+ import { Server } from "socket.io"
4
+
5
+ import { identityById } from "../src/identities.ts"
6
+ import { createVectorCollaborationService } from "./service.ts"
7
+
8
+ const PORT = 3000
9
+ const collaboration = await createVectorCollaborationService()
10
+ const httpServer = createServer((request, response) => {
11
+ if (request.url === `/health`) {
12
+ response.writeHead(200, { "content-type": `application/json` })
13
+ response.end(JSON.stringify({ ok: true, revision: collaboration.revision }))
14
+ return
15
+ }
16
+ response.writeHead(404)
17
+ response.end()
18
+ })
19
+ const socketServer = new Server(httpServer)
20
+
21
+ socketServer.on(`connection`, (socket) => {
22
+ const requestedActor =
23
+ typeof socket.handshake.auth.actor === `string`
24
+ ? socket.handshake.auth.actor
25
+ : undefined
26
+ const session =
27
+ typeof socket.handshake.auth.session === `string`
28
+ ? socket.handshake.auth.session
29
+ : ``
30
+ const identity = identityById(requestedActor)
31
+ if (
32
+ identity.id !== requestedActor ||
33
+ session.length === 0 ||
34
+ session.includes(`::`)
35
+ ) {
36
+ socket.disconnect(true)
37
+ return
38
+ }
39
+ void collaboration.bindSocket({ actor: identity.id, session, socket })
40
+ })
41
+
42
+ httpServer.listen(PORT, () => {
43
+ console.log(
44
+ `Vector collaboration server listening on http://localhost:${PORT}`,
45
+ )
46
+ })
47
+
48
+ const shutdown = (): void => {
49
+ collaboration[Symbol.dispose]()
50
+ void socketServer.close()
51
+ httpServer.close()
52
+ }
53
+ process.once(`SIGINT`, shutdown)
54
+ process.once(`SIGTERM`, shutdown)
@@ -0,0 +1,280 @@
1
+ import { Silo } from "atom.io"
2
+ import type { Json } from "atom.io/foundations/json"
3
+ import type {
4
+ MosaicDomainBatchProposal,
5
+ MosaicDomainResidencyTransport,
6
+ } from "@atom.io/realtime"
7
+ import {
8
+ bindMosaicDomainHistoryServerSocket,
9
+ bindMosaicDomainPresenceServerSocket,
10
+ createMosaicDomainBatchServer,
11
+ createMosaicDomainCheckpointCoordinator,
12
+ createMosaicDomainHistoryCoordinator,
13
+ createMosaicDomainPresenceServer,
14
+ createMosaicDomainResidencyServer,
15
+ InMemoryMosaicDomainCheckpointStorage,
16
+ type MosaicDomainBatchAuthorizationContext,
17
+ type MosaicDomainBatchConnection,
18
+ type MosaicDomainCheckpointCoordinator,
19
+ type MosaicDomainCheckpointStorageAdapter,
20
+ type MosaicDomainHistoryCoordinator,
21
+ } from "@atom.io/realtime/server"
22
+ import type { Socket as ServerSocket } from "socket.io"
23
+
24
+ import {
25
+ activateSvgContourDomain,
26
+ type SvgContourDomain,
27
+ type SvgContourStateAccess,
28
+ } from "../src/design-model.ts"
29
+ import {
30
+ VECTOR_BATCH_EVENTS,
31
+ VECTOR_RESIDENCY_EVENTS,
32
+ type VectorAcknowledgement,
33
+ } from "../src/protocol.ts"
34
+
35
+ export type VectorCollaborationService = Disposable & {
36
+ bindSocket(options: {
37
+ readonly actor: string
38
+ readonly session: string
39
+ readonly socket: ServerSocket
40
+ }): Promise<() => Promise<void>>
41
+ readonly checkpoints: MosaicDomainCheckpointCoordinator<
42
+ SvgContourDomain[`identity`]
43
+ >
44
+ readonly domain: SvgContourDomain
45
+ readonly history: MosaicDomainHistoryCoordinator<SvgContourDomain[`identity`]>
46
+ readonly revision: number
47
+ }
48
+
49
+ export async function createVectorCollaborationService(
50
+ options: {
51
+ readonly authorize?: (
52
+ context: MosaicDomainBatchAuthorizationContext,
53
+ ) => boolean | Promise<boolean>
54
+ readonly silo?: Silo
55
+ readonly storage?: MosaicDomainCheckpointStorageAdapter
56
+ } = {},
57
+ ): Promise<VectorCollaborationService> {
58
+ const silo =
59
+ options.silo ??
60
+ new Silo({
61
+ name: `vector-collaboration-server`,
62
+ // Domain families acquire physical members from authenticated addresses.
63
+ lifespan: `ephemeral`,
64
+ isProduction: process.env.NODE_ENV === `production`,
65
+ })
66
+ const storage = options.storage ?? new InMemoryMosaicDomainCheckpointStorage()
67
+ const domain = await activateSvgContourDomain({
68
+ instance: `shared-drawing`,
69
+ silo,
70
+ })
71
+ const readState = silo.getState as SvgContourStateAccess[`getState`]
72
+ const batchServer = createMosaicDomainBatchServer({
73
+ ...(options.authorize === undefined ? {} : { authorize: options.authorize }),
74
+ domain,
75
+ storage,
76
+ })
77
+ let history!: MosaicDomainHistoryCoordinator<SvgContourDomain[`identity`]>
78
+ const checkpoints = createMosaicDomainCheckpointCoordinator({
79
+ domain: domain.identity,
80
+ indexes: (context) => history.checkpoint.indexes(context),
81
+ readMember: async ({ address, revision }) => {
82
+ if (revision !== batchServer.revision) {
83
+ throw new Error(
84
+ `Plane checkpoints require a quiescent accepted revision.`,
85
+ )
86
+ }
87
+ const parsed = await domain.parseAddress(address)
88
+ const acquired = await domain.acquire(parsed)
89
+ return history.checkpoint.compactMember({
90
+ address,
91
+ revision,
92
+ value: readState(acquired.token) as Json.Serializable,
93
+ })
94
+ },
95
+ storage,
96
+ })
97
+ history = createMosaicDomainHistoryCoordinator({
98
+ batches: batchServer,
99
+ checkpoint: checkpoints,
100
+ domain,
101
+ storage,
102
+ })
103
+ const presenceServer = createMosaicDomainPresenceServer({ domain })
104
+ const residencyServer = createMosaicDomainResidencyServer({
105
+ batches: batchServer,
106
+ checkpoint: checkpoints,
107
+ domain,
108
+ maxResidentMembers: 512,
109
+ })
110
+ type VectorResidencyTransport = MosaicDomainResidencyTransport<
111
+ SvgContourDomain[`identity`]
112
+ >
113
+ const cleanups = new Set<() => Promise<void>>()
114
+ let disposed = false
115
+ let serial = Promise.resolve()
116
+ const exclusively = <Value>(work: () => Promise<Value>): Promise<Value> => {
117
+ const result = serial.then(work)
118
+ serial = result.then(
119
+ () => undefined,
120
+ () => undefined,
121
+ )
122
+ return result
123
+ }
124
+ const acknowledge = <Value>(
125
+ work: Promise<Value>,
126
+ respond: (acknowledgement: VectorAcknowledgement<Value>) => void,
127
+ ): void => {
128
+ void work.then(
129
+ (value) => {
130
+ respond({ ok: true, value })
131
+ },
132
+ (error: unknown) => {
133
+ respond({
134
+ ok: false,
135
+ reason: error instanceof Error ? error.message : String(error),
136
+ })
137
+ },
138
+ )
139
+ }
140
+
141
+ return {
142
+ bindSocket({ actor, session, socket }) {
143
+ if (disposed) {
144
+ return Promise.reject(new Error(`The vector service is disposed.`))
145
+ }
146
+ const batch = batchServer.connect({ actor, session })
147
+ const presence = presenceServer.connect({ actor, session })
148
+ const residency = residencyServer.connect({ actor, session })
149
+ const unbindHistory = bindMosaicDomainHistoryServerSocket(
150
+ history.connect({ actor, session }),
151
+ socket,
152
+ { session },
153
+ )
154
+ const unsubscribeBatch = batch.subscribe((accepted) => {
155
+ socket.emit(VECTOR_BATCH_EVENTS.accepted, accepted)
156
+ })
157
+ const residencySubscriptions = new Map<string, () => void>()
158
+ const unbindPresence = bindMosaicDomainPresenceServerSocket(
159
+ presence,
160
+ socket,
161
+ )
162
+ const onPropose = (
163
+ proposal: MosaicDomainBatchProposal,
164
+ respond: (
165
+ acknowledgement: VectorAcknowledgement<
166
+ Awaited<ReturnType<MosaicDomainBatchConnection[`propose`]>>
167
+ >,
168
+ ) => void,
169
+ ): void => {
170
+ acknowledge(
171
+ exclusively(() => batch.propose(proposal)),
172
+ respond,
173
+ )
174
+ }
175
+ const onRecover = (
176
+ afterRevision: number,
177
+ respond: (
178
+ acknowledgement: VectorAcknowledgement<
179
+ Awaited<ReturnType<MosaicDomainBatchConnection[`recover`]>>
180
+ >,
181
+ ) => void,
182
+ ): void => {
183
+ acknowledge(batch.recover(afterRevision), respond)
184
+ }
185
+ const onResidencyHydrate = (
186
+ requests: unknown,
187
+ respond: (
188
+ acknowledgement: VectorAcknowledgement<
189
+ Awaited<ReturnType<VectorResidencyTransport[`hydrate`]>>
190
+ >,
191
+ ) => void,
192
+ ): void => {
193
+ acknowledge(residency.hydrate(requests as never), respond)
194
+ }
195
+ const onResidencyPropose = (
196
+ proposal: unknown,
197
+ respond: (
198
+ acknowledgement: VectorAcknowledgement<
199
+ Awaited<ReturnType<VectorResidencyTransport[`propose`]>>
200
+ >,
201
+ ) => void,
202
+ ): void => {
203
+ acknowledge(residency.propose(proposal as never), respond)
204
+ }
205
+ const onResidencySubscribe = (
206
+ id: string,
207
+ requests: unknown,
208
+ respond: (acknowledgement: VectorAcknowledgement<void>) => void,
209
+ ): void => {
210
+ acknowledge(
211
+ Promise.resolve(
212
+ residency.subscribe(requests as never, (accepted) => {
213
+ socket.emit(VECTOR_RESIDENCY_EVENTS.accepted, id, accepted)
214
+ }),
215
+ ).then((stop) => {
216
+ residencySubscriptions.get(id)?.()
217
+ residencySubscriptions.set(id, stop)
218
+ }),
219
+ respond,
220
+ )
221
+ }
222
+ const onResidencyUnsubscribe = (id: string): void => {
223
+ residencySubscriptions.get(id)?.()
224
+ residencySubscriptions.delete(id)
225
+ }
226
+ socket.on(VECTOR_BATCH_EVENTS.propose, onPropose)
227
+ socket.on(VECTOR_BATCH_EVENTS.recover, onRecover)
228
+ socket.on(VECTOR_RESIDENCY_EVENTS.hydrate, onResidencyHydrate)
229
+ socket.on(VECTOR_RESIDENCY_EVENTS.propose, onResidencyPropose)
230
+ socket.on(VECTOR_RESIDENCY_EVENTS.subscribe, onResidencySubscribe)
231
+ socket.on(VECTOR_RESIDENCY_EVENTS.unsubscribe, onResidencyUnsubscribe)
232
+ let cleaned = false
233
+ const cleanup = async (): Promise<void> => {
234
+ if (cleaned) return
235
+ cleaned = true
236
+ socket.off(VECTOR_BATCH_EVENTS.propose, onPropose)
237
+ socket.off(VECTOR_BATCH_EVENTS.recover, onRecover)
238
+ socket.off(`disconnect`, onDisconnect)
239
+ unsubscribeBatch()
240
+ for (const stop of residencySubscriptions.values()) stop()
241
+ residencySubscriptions.clear()
242
+ socket.off(VECTOR_RESIDENCY_EVENTS.hydrate, onResidencyHydrate)
243
+ socket.off(VECTOR_RESIDENCY_EVENTS.propose, onResidencyPropose)
244
+ socket.off(VECTOR_RESIDENCY_EVENTS.subscribe, onResidencySubscribe)
245
+ socket.off(VECTOR_RESIDENCY_EVENTS.unsubscribe, onResidencyUnsubscribe)
246
+ await Promise.resolve(residency.dispose?.()).catch(() => undefined)
247
+ unbindHistory()
248
+ await unbindPresence()
249
+ cleanups.delete(cleanup)
250
+ }
251
+ const onDisconnect = (): void => {
252
+ void cleanup().catch(() => undefined)
253
+ }
254
+ socket.on(`disconnect`, onDisconnect)
255
+ cleanups.add(cleanup)
256
+ return Promise.resolve(cleanup)
257
+ },
258
+ checkpoints: {
259
+ checkpoint: () => exclusively(() => checkpoints.checkpoint()),
260
+ readIndex: checkpoints.readIndex,
261
+ recover: checkpoints.recover,
262
+ },
263
+ domain,
264
+ history,
265
+ get revision() {
266
+ return batchServer.revision
267
+ },
268
+ [Symbol.dispose]() {
269
+ if (disposed) return
270
+ disposed = true
271
+ for (const cleanup of cleanups) void cleanup().catch(() => undefined)
272
+ cleanups.clear()
273
+ presenceServer[Symbol.dispose]()
274
+ residencyServer[Symbol.dispose]()
275
+ history[Symbol.dispose]()
276
+ batchServer.dispose()
277
+ domain[Symbol.dispose]()
278
+ },
279
+ }
280
+ }
package/package.json CHANGED
@@ -1,6 +1,68 @@
1
1
  {
2
- "name": "@atom.io/template-preact-realtime-vector-editor",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@atom.io/template-preact-realtime-vector-editor",
3
+ "version": "0.0.0",
4
+ "description": "A realtime collaborative vector editor built with Atom.io Mosaic Domains and Preact.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/jeremybanka/atom.io.git",
8
+ "directory": "templates/preact-realtime-vector-editor"
9
+ },
10
+ "license": "Apache-2.0",
11
+ "type": "module",
12
+ "scripts": {
13
+ "dev": "concurrently vite \"node --watch node/server.ts\"",
14
+ "build": "tsc -b && vite build",
15
+ "fmt": "dprint fmt .",
16
+ "fmt:check": "dprint check .",
17
+ "lint": "npm run lint:eslint && npm run lint:oxlint && npm run lint:lasertag && npm run lint:types",
18
+ "lint:eslint": "eslint --max-warnings 0 .",
19
+ "lint:lasertag": "lasertag check \"src/**/*.module.css\"",
20
+ "lint:oxlint": "oxlint --deny-warnings .",
21
+ "lint:types": "tsc -b",
22
+ "preview": "concurrently \"vite preview\" \"node node/server.ts\"",
23
+ "test": "vitest run",
24
+ "test:once": "vitest run"
25
+ },
26
+ "dependencies": {
27
+ "@atom.io/react": "workspace:*",
28
+ "@atom.io/realtime": "workspace:*",
29
+ "@atom.io/template-preact-svg-editor": "workspace:*",
30
+ "atom.io": "workspace:*",
31
+ "preact": "11.0.1",
32
+ "socket.io": "4.8.4",
33
+ "socket.io-client": "4.8.4"
34
+ },
35
+ "devDependencies": {
36
+ "@atom.io/eslint-plugin": "workspace:*",
37
+ "@dprint/json": "0.25.2",
38
+ "@dprint/markdown": "0.27.1",
39
+ "@dprint/oxc": "0.47.0",
40
+ "@preact/preset-vite": "2.10.6",
41
+ "@testing-library/dom": "10.4.2",
42
+ "@testing-library/react": "16.3.3",
43
+ "@types/node": "26.6.5",
44
+ "@types/react": "19.3.0",
45
+ "@types/react-dom": "19.3.0",
46
+ "@typescript-eslint/parser": "8.72.0",
47
+ "@typescript/native": "npm:typescript@7.0.2",
48
+ "concurrently": "10.0.6",
49
+ "dprint": "0.64.1",
50
+ "dprint-plugin-malva": "0.16.0",
51
+ "eslint": "10.13.0",
52
+ "happy-dom": "21.0.4",
53
+ "lasertag": "0.8.8",
54
+ "oxlint": "1.89.0",
55
+ "oxlint-tsgolint": "7.0.2004",
56
+ "react": "19.3.1",
57
+ "react-dom": "19.3.1",
58
+ "typescript": "npm:@typescript/typescript6@6.0.2",
59
+ "vite": "8.3.5",
60
+ "vitest": "5.0.5"
61
+ },
62
+ "engines": {
63
+ "node": ">=24.12.0"
64
+ },
65
+ "publishConfig": {
66
+ "access": "public"
67
+ }
68
+ }