clarity-mind 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.
package/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # clarity-mind
2
+
3
+ [![npm](https://img.shields.io/npm/v/clarity-mind?color=6366F1)](https://www.npmjs.com/package/clarity-mind)
4
+ [![MIT](https://img.shields.io/npm/l/clarity-mind?color=64748B)](https://github.com/udielenberg/clarity/blob/main/LICENSE)
5
+
6
+ The **headless engine** behind [clarity](https://github.com/udielenberg/clarity) — a better-UX, open-source mind-mapping library.
7
+
8
+ Framework-agnostic and **DOM-free**: an immutable tree model, a tidy-tree layout, undo/redo, and Markdown/JSON I/O. Bring your own renderer — or use the React binding, [`@clarity-mind/react`](https://www.npmjs.com/package/@clarity-mind/react).
9
+
10
+ ```bash
11
+ npm install clarity-mind
12
+ ```
13
+
14
+ ## Example
15
+
16
+ ```ts
17
+ import { createStore, layout, toMarkdown, fromMarkdown } from "clarity-mind";
18
+
19
+ // A store wraps the tree with undo/redo + change subscriptions.
20
+ const store = createStore("Roadmap");
21
+ const q1 = store.addChild(store.rootId, "Q1");
22
+ store.addChild(q1, "Ship v1");
23
+ store.setText(q1, "Q1 — launch");
24
+
25
+ // Deterministic, two-sided "tidy tree" layout. You supply node sizes, so it
26
+ // works with any measuring strategy (DOM, canvas, a fixed grid…).
27
+ const { boxes, edges, bounds } = layout(store.map.root, {
28
+ measure: (node) => ({ width: node.text.length * 8 + 24, height: 32 }),
29
+ });
30
+
31
+ // Round-trips through plain Markdown outlines.
32
+ const md = toMarkdown(store.map.root);
33
+ const tree = fromMarkdown(md);
34
+
35
+ store.undo();
36
+ store.redo();
37
+ ```
38
+
39
+ ## What's in the box
40
+
41
+ | Area | Exports |
42
+ | ------------------------- | -------------------------------------------------------------------------------------------------------------------- |
43
+ | **Model** (pure tree ops) | `createMap`, `node`, `insertChild`, `removeNode`, `moveNode`, `updateNode`, `toggleCollapsed`, `findNode`, `walk`, … |
44
+ | **Store** (stateful) | `createStore`, `MindMapStore` — `addChild`, `addSibling`, `setText`, `move`, `remove`, `undo`, `redo`, `subscribe` |
45
+ | **Layout** | `layout(root, options)` → `{ boxes, edges, bounds }` |
46
+ | **History** | `History` — the undo/redo stack |
47
+ | **I/O** | `toMarkdown` / `fromMarkdown`, `toJSON` / `fromJSON` (versioned) |
48
+
49
+ Every model operation is **pure** — it returns a new tree and never mutates its input, which is what makes undo/redo and predictable rendering trivial. Moves are cycle-safe (you can't drop a node into its own descendant).
50
+
51
+ ## Types
52
+
53
+ Fully typed, `strict` + `noUncheckedIndexedAccess`. Key shapes:
54
+
55
+ ```ts
56
+ interface MindNode {
57
+ id: string;
58
+ text: string;
59
+ children: MindNode[];
60
+ collapsed?: boolean;
61
+ note?: string;
62
+ }
63
+
64
+ interface MindMap {
65
+ root: MindNode;
66
+ }
67
+ ```
68
+
69
+ ## License
70
+
71
+ [MIT](https://github.com/udielenberg/clarity/blob/main/LICENSE) © udielenberg
@@ -0,0 +1,22 @@
1
+ /**
2
+ * A tiny generic undo/redo stack.
3
+ *
4
+ * Because every model operation returns a new immutable tree, "undo" is just
5
+ * "point back at the previous snapshot" — no inverse commands needed.
6
+ */
7
+ export declare class History<T> {
8
+ private stack;
9
+ private index;
10
+ private readonly limit;
11
+ constructor(initial: T, limit?: number);
12
+ /** The current state. */
13
+ get current(): T;
14
+ /** Push a new state, discarding any redo history beyond the current point. */
15
+ push(state: T): void;
16
+ canUndo(): boolean;
17
+ canRedo(): boolean;
18
+ /** Step back one state (no-op if none). Returns the new current state. */
19
+ undo(): T;
20
+ /** Step forward one state (no-op if none). Returns the new current state. */
21
+ redo(): T;
22
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * A tiny generic undo/redo stack.
3
+ *
4
+ * Because every model operation returns a new immutable tree, "undo" is just
5
+ * "point back at the previous snapshot" — no inverse commands needed.
6
+ */
7
+ export class History {
8
+ constructor(initial, limit = 200) {
9
+ this.stack = [initial];
10
+ this.index = 0;
11
+ this.limit = Math.max(1, limit);
12
+ }
13
+ /** The current state. */
14
+ get current() {
15
+ return this.stack[this.index];
16
+ }
17
+ /** Push a new state, discarding any redo history beyond the current point. */
18
+ push(state) {
19
+ if (state === this.current)
20
+ return;
21
+ this.stack = this.stack.slice(0, this.index + 1);
22
+ this.stack.push(state);
23
+ this.index += 1;
24
+ // Trim from the front if we exceed the limit.
25
+ if (this.stack.length > this.limit) {
26
+ const overflow = this.stack.length - this.limit;
27
+ this.stack = this.stack.slice(overflow);
28
+ this.index -= overflow;
29
+ }
30
+ }
31
+ canUndo() {
32
+ return this.index > 0;
33
+ }
34
+ canRedo() {
35
+ return this.index < this.stack.length - 1;
36
+ }
37
+ /** Step back one state (no-op if none). Returns the new current state. */
38
+ undo() {
39
+ if (this.canUndo())
40
+ this.index -= 1;
41
+ return this.current;
42
+ }
43
+ /** Step forward one state (no-op if none). Returns the new current state. */
44
+ redo() {
45
+ if (this.canRedo())
46
+ this.index += 1;
47
+ return this.current;
48
+ }
49
+ }
50
+ //# sourceMappingURL=history.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history.js","sourceRoot":"","sources":["../src/history.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,OAAO,OAAO;IAKlB,YAAY,OAAU,EAAE,KAAK,GAAG,GAAG;QACjC,IAAI,CAAC,KAAK,GAAG,CAAC,OAAO,CAAC,CAAC;QACvB,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;QACf,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IAClC,CAAC;IAED,yBAAyB;IACzB,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAE,CAAC;IACjC,CAAC;IAED,8EAA8E;IAC9E,IAAI,CAAC,KAAQ;QACX,IAAI,KAAK,KAAK,IAAI,CAAC,OAAO;YAAE,OAAO;QACnC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QACjD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACvB,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC;QAChB,8CAA8C;QAC9C,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;YACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC;YAChD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACxC,IAAI,CAAC,KAAK,IAAI,QAAQ,CAAC;QACzB,CAAC;IACH,CAAC;IAED,OAAO;QACL,OAAO,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;IACxB,CAAC;IAED,OAAO;QACL,OAAO,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;IAC5C,CAAC;IAED,0EAA0E;IAC1E,IAAI;QACF,IAAI,IAAI,CAAC,OAAO,EAAE;YAAE,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC;QACpC,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,6EAA6E;IAC7E,IAAI;QACF,IAAI,IAAI,CAAC,OAAO,EAAE;YAAE,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC;QACpC,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;CACF"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * clarity-mind — headless mind-mapping engine.
3
+ *
4
+ * Framework-agnostic and DOM-free: data model, layout, undo/redo, and I/O.
5
+ * Pair it with a rendering binding (e.g. `@clarity-mind/react`) to draw a map.
6
+ */
7
+ export type { MindNode, MindMap } from "./model.js";
8
+ export { createId, node, createMap, cloneNode, findNode, findParent, isAncestor, updateNode, insertChild, removeNode, toggleCollapsed, moveNode, walk, countNodes, } from "./model.js";
9
+ export { History } from "./history.js";
10
+ export { MindMapStore, createStore, type Unsubscribe } from "./store.js";
11
+ export { layout, type Box, type Edge, type Size, type LayoutResult, type LayoutOptions, } from "./layout.js";
12
+ export { toMarkdown, fromMarkdown } from "./io/markdown.js";
13
+ export { toJSON, fromJSON, FORMAT_VERSION } from "./io/json.js";
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { createId, node, createMap, cloneNode, findNode, findParent, isAncestor, updateNode, insertChild, removeNode, toggleCollapsed, moveNode, walk, countNodes, } from "./model.js";
2
+ export { History } from "./history.js";
3
+ export { MindMapStore, createStore } from "./store.js";
4
+ export { layout, } from "./layout.js";
5
+ export { toMarkdown, fromMarkdown } from "./io/markdown.js";
6
+ export { toJSON, fromJSON, FORMAT_VERSION } from "./io/json.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,QAAQ,EACR,IAAI,EACJ,SAAS,EACT,SAAS,EACT,QAAQ,EACR,UAAU,EACV,UAAU,EACV,UAAU,EACV,WAAW,EACX,UAAU,EACV,eAAe,EACf,QAAQ,EACR,IAAI,EACJ,UAAU,GACX,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAoB,MAAM,YAAY,CAAC;AAEzE,OAAO,EACL,MAAM,GAMP,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * JSON import/export with validation.
3
+ *
4
+ * The on-disk shape is versioned so future changes can migrate old files.
5
+ */
6
+ import type { MindMap } from "../model.js";
7
+ export declare const FORMAT_VERSION: 1;
8
+ /** Serialize a map to a JSON string. */
9
+ export declare function toJSON(map: MindMap, pretty?: boolean): string;
10
+ /** Parse and validate a JSON string into a map. Throws on malformed input. */
11
+ export declare function fromJSON(text: string): MindMap;
@@ -0,0 +1,47 @@
1
+ export const FORMAT_VERSION = 1;
2
+ /** Serialize a map to a JSON string. */
3
+ export function toJSON(map, pretty = true) {
4
+ const payload = { version: FORMAT_VERSION, root: map.root };
5
+ return JSON.stringify(payload, null, pretty ? 2 : undefined);
6
+ }
7
+ function validateNode(value, path) {
8
+ if (typeof value !== "object" || value === null) {
9
+ throw new Error(`clarity: ${path} must be an object`);
10
+ }
11
+ const obj = value;
12
+ if (typeof obj.id !== "string" || obj.id.length === 0) {
13
+ throw new Error(`clarity: ${path}.id must be a non-empty string`);
14
+ }
15
+ if (typeof obj.text !== "string") {
16
+ throw new Error(`clarity: ${path}.text must be a string`);
17
+ }
18
+ if (!Array.isArray(obj.children)) {
19
+ throw new Error(`clarity: ${path}.children must be an array`);
20
+ }
21
+ const children = obj.children.map((c, i) => validateNode(c, `${path}.children[${i}]`));
22
+ const result = { id: obj.id, text: obj.text, children };
23
+ if (typeof obj.collapsed === "boolean")
24
+ result.collapsed = obj.collapsed;
25
+ if (typeof obj.note === "string")
26
+ result.note = obj.note;
27
+ return result;
28
+ }
29
+ /** Parse and validate a JSON string into a map. Throws on malformed input. */
30
+ export function fromJSON(text) {
31
+ let data;
32
+ try {
33
+ data = JSON.parse(text);
34
+ }
35
+ catch {
36
+ throw new Error("clarity: invalid JSON");
37
+ }
38
+ if (typeof data !== "object" || data === null) {
39
+ throw new Error("clarity: expected an object at the top level");
40
+ }
41
+ const obj = data;
42
+ if (obj.version !== FORMAT_VERSION) {
43
+ throw new Error(`clarity: unsupported format version ${String(obj.version)}`);
44
+ }
45
+ return { root: validateNode(obj.root, "root") };
46
+ }
47
+ //# sourceMappingURL=json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.js","sourceRoot":"","sources":["../../src/io/json.ts"],"names":[],"mappings":"AAOA,MAAM,CAAC,MAAM,cAAc,GAAG,CAAU,CAAC;AAOzC,wCAAwC;AACxC,MAAM,UAAU,MAAM,CAAC,GAAY,EAAE,MAAM,GAAG,IAAI;IAChD,MAAM,OAAO,GAAkB,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3E,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;AAC/D,CAAC;AAED,SAAS,YAAY,CAAC,KAAc,EAAE,IAAY;IAChD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CAAC,YAAY,IAAI,oBAAoB,CAAC,CAAC;IACxD,CAAC;IACD,MAAM,GAAG,GAAG,KAAgC,CAAC;IAC7C,IAAI,OAAO,GAAG,CAAC,EAAE,KAAK,QAAQ,IAAI,GAAG,CAAC,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,YAAY,IAAI,gCAAgC,CAAC,CAAC;IACpE,CAAC;IACD,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,YAAY,IAAI,wBAAwB,CAAC,CAAC;IAC5D,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,YAAY,IAAI,4BAA4B,CAAC,CAAC;IAChE,CAAC;IACD,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACzC,YAAY,CAAC,CAAC,EAAE,GAAG,IAAI,aAAa,CAAC,GAAG,CAAC,CAC1C,CAAC;IACF,MAAM,MAAM,GAAa,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC;IAClE,IAAI,OAAO,GAAG,CAAC,SAAS,KAAK,SAAS;QAAE,MAAM,CAAC,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;IACzE,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ;QAAE,MAAM,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;IACzD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,IAAI,IAAa,CAAC;IAClB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;IAC3C,CAAC;IACD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAClE,CAAC;IACD,MAAM,GAAG,GAAG,IAA+B,CAAC;IAC5C,IAAI,GAAG,CAAC,OAAO,KAAK,cAAc,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACb,uCAAuC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAC7D,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,YAAY,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC;AAClD,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Markdown import/export.
3
+ *
4
+ * A map serializes to an indented bullet list — the format people already use
5
+ * for outlines, so it round-trips cleanly and pastes into any editor. Two
6
+ * spaces of indentation per level; the first bullet is the root.
7
+ *
8
+ * Example:
9
+ * - My ideas
10
+ * - First branch
11
+ * - Detail
12
+ * - Second branch
13
+ */
14
+ import { type MindNode } from "../model.js";
15
+ /** Serialize a tree to an indented Markdown bullet list. */
16
+ export declare function toMarkdown(root: MindNode): string;
17
+ /**
18
+ * Parse an indented bullet list back into a tree.
19
+ *
20
+ * If the text has a single top-level bullet, it becomes the root. If it has
21
+ * several (or none), they are wrapped under a synthetic root titled `rootText`.
22
+ */
23
+ export declare function fromMarkdown(text: string, rootText?: string): MindNode;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Markdown import/export.
3
+ *
4
+ * A map serializes to an indented bullet list — the format people already use
5
+ * for outlines, so it round-trips cleanly and pastes into any editor. Two
6
+ * spaces of indentation per level; the first bullet is the root.
7
+ *
8
+ * Example:
9
+ * - My ideas
10
+ * - First branch
11
+ * - Detail
12
+ * - Second branch
13
+ */
14
+ import { node } from "../model.js";
15
+ const INDENT = " ";
16
+ /** Serialize a tree to an indented Markdown bullet list. */
17
+ export function toMarkdown(root) {
18
+ const lines = [];
19
+ const go = (n, depth) => {
20
+ const text = n.text.replace(/\r?\n/g, " ").trimEnd();
21
+ lines.push(`${INDENT.repeat(depth)}- ${text}`);
22
+ for (const c of n.children)
23
+ go(c, depth + 1);
24
+ };
25
+ go(root, 0);
26
+ return lines.join("\n");
27
+ }
28
+ function parseLine(line) {
29
+ // Accept "-", "*", or "+" bullets. Indentation may be spaces or tabs.
30
+ const match = /^([ \t]*)[-*+]\s+(.*)$/.exec(line);
31
+ if (!match)
32
+ return null;
33
+ const indent = match[1] ?? "";
34
+ const text = (match[2] ?? "").trim();
35
+ // A tab counts as two spaces; otherwise depth is spaces / 2 (rounded down).
36
+ const spaces = indent.replace(/\t/g, INDENT).length;
37
+ return { depth: Math.floor(spaces / 2), text };
38
+ }
39
+ /**
40
+ * Parse an indented bullet list back into a tree.
41
+ *
42
+ * If the text has a single top-level bullet, it becomes the root. If it has
43
+ * several (or none), they are wrapped under a synthetic root titled `rootText`.
44
+ */
45
+ export function fromMarkdown(text, rootText = "Map") {
46
+ const parsed = text
47
+ .split(/\r?\n/)
48
+ .map(parseLine)
49
+ .filter((p) => p !== null && p.text.length > 0);
50
+ if (parsed.length === 0)
51
+ return node(rootText);
52
+ // Normalize depths so the shallowest line is depth 0.
53
+ const minDepth = Math.min(...parsed.map((p) => p.depth));
54
+ for (const p of parsed)
55
+ p.depth -= minDepth;
56
+ const topLevel = parsed.filter((p) => p.depth === 0);
57
+ const singleRoot = topLevel.length === 1;
58
+ // Build using a stack of { depth, node }.
59
+ const roots = [];
60
+ const stack = [];
61
+ for (const p of parsed) {
62
+ const n = node(p.text);
63
+ while (stack.length > 0 && stack[stack.length - 1].depth >= p.depth) {
64
+ stack.pop();
65
+ }
66
+ const parent = stack[stack.length - 1];
67
+ if (parent) {
68
+ parent.node.children.push(n);
69
+ }
70
+ else {
71
+ roots.push(n);
72
+ }
73
+ stack.push({ depth: p.depth, node: n });
74
+ }
75
+ if (singleRoot && roots.length === 1)
76
+ return roots[0];
77
+ return node(rootText, roots);
78
+ }
79
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../../src/io/markdown.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The layout engine.
3
+ *
4
+ * Turns a {@link MindNode} tree into positioned boxes that a renderer can draw.
5
+ * It is **headless**: it does not measure text itself (that needs a DOM/canvas),
6
+ * so the caller supplies a `measure` function. The result is a flat map of
7
+ * `id -> Box` plus the parent→child edges.
8
+ *
9
+ * Algorithm: a variable-size "tidy tree" laid out on two sides of a central
10
+ * root (the classic mind-map look). First-level children are split between the
11
+ * right and left sides; each side grows outward horizontally and stacks
12
+ * vertically without overlap. Each side is vertically centered against the
13
+ * taller one so the root sits in the middle.
14
+ */
15
+ import type { MindNode } from "./model.js";
16
+ /** A positioned node. `x`/`y` is the top-left corner. */
17
+ export interface Box {
18
+ id: string;
19
+ x: number;
20
+ y: number;
21
+ width: number;
22
+ height: number;
23
+ /** Depth from the root (root = 0). */
24
+ depth: number;
25
+ /** Which side of the root this box sits on. The root itself is `"root"`. */
26
+ side: "left" | "right" | "root";
27
+ }
28
+ /** A connector from a parent box to a child box. */
29
+ export interface Edge {
30
+ from: string;
31
+ to: string;
32
+ }
33
+ export interface LayoutResult {
34
+ boxes: Map<string, Box>;
35
+ edges: Edge[];
36
+ /** Bounding box of the whole map. */
37
+ bounds: {
38
+ x: number;
39
+ y: number;
40
+ width: number;
41
+ height: number;
42
+ };
43
+ }
44
+ export interface Size {
45
+ width: number;
46
+ height: number;
47
+ }
48
+ export interface LayoutOptions {
49
+ /** Measure a node's rendered size. Defaults to a rough text estimate. */
50
+ measure?: (node: MindNode) => Size;
51
+ /** Horizontal gap between a parent and its children. Default 48. */
52
+ hGap?: number;
53
+ /** Vertical gap between sibling subtrees. Default 16. */
54
+ vGap?: number;
55
+ /** Lay children out on both sides of the root (true) or only the right. Default true. */
56
+ twoSided?: boolean;
57
+ }
58
+ export declare function layout(root: MindNode, options?: LayoutOptions): LayoutResult;
package/dist/layout.js ADDED
@@ -0,0 +1,154 @@
1
+ const defaultMeasure = (n) => ({
2
+ width: Math.max(48, n.text.length * 8 + 24),
3
+ height: 32,
4
+ });
5
+ /** Children that participate in layout (collapsed nodes hide theirs). */
6
+ function visibleChildren(n) {
7
+ return n.collapsed ? [] : n.children;
8
+ }
9
+ /** Build the working tree and measure every node. */
10
+ function build(node, measure) {
11
+ return {
12
+ node,
13
+ size: measure(node),
14
+ children: visibleChildren(node).map((c) => build(c, measure)),
15
+ cy: 0,
16
+ extent: 0,
17
+ };
18
+ }
19
+ /**
20
+ * Pass 1 (vertical): assign each subtree a vertical center relative to a local
21
+ * cursor starting at 0. A leaf reserves its own height; a parent centers on the
22
+ * span of its children. Returns the total height consumed.
23
+ */
24
+ function layoutVertical(work, vGap, cursor) {
25
+ if (work.children.length === 0) {
26
+ work.cy = cursor.y + work.size.height / 2;
27
+ work.extent = work.size.height;
28
+ cursor.y += work.size.height + vGap;
29
+ return;
30
+ }
31
+ for (const child of work.children) {
32
+ layoutVertical(child, vGap, cursor);
33
+ }
34
+ const first = work.children[0];
35
+ const last = work.children[work.children.length - 1];
36
+ // Guarantee the parent's own height fits even if its children span less.
37
+ const childSpan = last.cy + last.size.height / 2 - (first.cy - first.size.height / 2);
38
+ work.extent = Math.max(childSpan, work.size.height);
39
+ work.cy = (first.cy + last.cy) / 2;
40
+ }
41
+ /** Shift a whole subtree vertically by `dy`. */
42
+ function shiftVertical(work, dy) {
43
+ work.cy += dy;
44
+ for (const child of work.children)
45
+ shiftVertical(child, dy);
46
+ }
47
+ /**
48
+ * Pass 2 (horizontal): walk outward from the root assigning x. For the right
49
+ * side, a child sits to the right of its parent; for the left side, to the left.
50
+ * Emits boxes and edges.
51
+ */
52
+ function emit(work, parentLeft, parentWidth, depth, side, hGap, out) {
53
+ let x;
54
+ if (side === "left") {
55
+ // Child's right edge sits `hGap` to the left of the parent's left edge.
56
+ x = parentLeft - hGap - work.size.width;
57
+ }
58
+ else if (side === "right") {
59
+ x = parentLeft + parentWidth + hGap;
60
+ }
61
+ else {
62
+ // Root: centered on x = 0.
63
+ x = -work.size.width / 2;
64
+ }
65
+ out.boxes.set(work.node.id, {
66
+ id: work.node.id,
67
+ x,
68
+ y: work.cy - work.size.height / 2,
69
+ width: work.size.width,
70
+ height: work.size.height,
71
+ depth,
72
+ side,
73
+ });
74
+ // Below the root, children always inherit their parent's side.
75
+ for (const child of work.children) {
76
+ out.edges.push({ from: work.node.id, to: child.node.id });
77
+ emit(child, x, work.size.width, depth + 1, side, hGap, out);
78
+ }
79
+ }
80
+ export function layout(root, options = {}) {
81
+ const measure = options.measure ?? defaultMeasure;
82
+ const hGap = options.hGap ?? 48;
83
+ const vGap = options.vGap ?? 16;
84
+ const twoSided = options.twoSided ?? true;
85
+ const rootWork = build(root, measure);
86
+ const rootSize = rootWork.size;
87
+ // Split first-level children between the two sides.
88
+ const firstLevel = rootWork.children;
89
+ let rightWorks;
90
+ let leftWorks;
91
+ if (twoSided && firstLevel.length > 1) {
92
+ const half = Math.ceil(firstLevel.length / 2);
93
+ rightWorks = firstLevel.slice(0, half);
94
+ leftWorks = firstLevel.slice(half);
95
+ }
96
+ else {
97
+ rightWorks = firstLevel;
98
+ leftWorks = [];
99
+ }
100
+ // Vertical pass, independently per side.
101
+ const measureSide = (works) => {
102
+ const cursor = { y: 0 };
103
+ for (const w of works)
104
+ layoutVertical(w, vGap, cursor);
105
+ return works.length === 0 ? 0 : cursor.y - vGap;
106
+ };
107
+ const rightHeight = measureSide(rightWorks);
108
+ const leftHeight = measureSide(leftWorks);
109
+ const sidesHeight = Math.max(rightHeight, leftHeight);
110
+ const overallHeight = Math.max(sidesHeight, rootSize.height);
111
+ // Center each side vertically within the overall height.
112
+ for (const w of rightWorks)
113
+ shiftVertical(w, (overallHeight - rightHeight) / 2);
114
+ for (const w of leftWorks)
115
+ shiftVertical(w, (overallHeight - leftHeight) / 2);
116
+ rootWork.cy = overallHeight / 2;
117
+ const out = { boxes: new Map(), edges: [] };
118
+ // Emit root box (centered on origin).
119
+ out.boxes.set(root.id, {
120
+ id: root.id,
121
+ x: -rootSize.width / 2,
122
+ y: rootWork.cy - rootSize.height / 2,
123
+ width: rootSize.width,
124
+ height: rootSize.height,
125
+ depth: 0,
126
+ side: "root",
127
+ });
128
+ const rootLeft = -rootSize.width / 2;
129
+ for (const w of rightWorks) {
130
+ out.edges.push({ from: root.id, to: w.node.id });
131
+ emit(w, rootLeft, rootSize.width, 1, "right", hGap, out);
132
+ }
133
+ for (const w of leftWorks) {
134
+ out.edges.push({ from: root.id, to: w.node.id });
135
+ emit(w, rootLeft, rootSize.width, 1, "left", hGap, out);
136
+ }
137
+ // Compute bounds.
138
+ let minX = Infinity;
139
+ let minY = Infinity;
140
+ let maxX = -Infinity;
141
+ let maxY = -Infinity;
142
+ for (const b of out.boxes.values()) {
143
+ minX = Math.min(minX, b.x);
144
+ minY = Math.min(minY, b.y);
145
+ maxX = Math.max(maxX, b.x + b.width);
146
+ maxY = Math.max(maxY, b.y + b.height);
147
+ }
148
+ return {
149
+ boxes: out.boxes,
150
+ edges: out.edges,
151
+ bounds: { x: minX, y: minY, width: maxX - minX, height: maxY - minY },
152
+ };
153
+ }
154
+ //# sourceMappingURL=layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.js","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"AA0DA,MAAM,cAAc,GAAG,CAAC,CAAW,EAAQ,EAAE,CAAC,CAAC;IAC7C,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,EAAE,CAAC;IAC3C,MAAM,EAAE,EAAE;CACX,CAAC,CAAC;AAEH,yEAAyE;AACzE,SAAS,eAAe,CAAC,CAAW;IAClC,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;AACvC,CAAC;AAYD,qDAAqD;AACrD,SAAS,KAAK,CAAC,IAAc,EAAE,OAA8B;IAC3D,OAAO;QACL,IAAI;QACJ,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC;QACnB,QAAQ,EAAE,eAAe,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;QAC7D,EAAE,EAAE,CAAC;QACL,MAAM,EAAE,CAAC;KACV,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAS,cAAc,CAAC,IAAU,EAAE,IAAY,EAAE,MAAqB;IACrE,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,IAAI,CAAC,EAAE,GAAG,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;QAC1C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;QAC/B,MAAM,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACpC,OAAO;IACT,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClC,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IACtC,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAE,CAAC;IAChC,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;IACtD,yEAAyE;IACzE,MAAM,SAAS,GACb,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACtE,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACpD,IAAI,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;AACrC,CAAC;AAED,gDAAgD;AAChD,SAAS,aAAa,CAAC,IAAU,EAAE,EAAU;IAC3C,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC;IACd,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ;QAAE,aAAa,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAC9D,CAAC;AAED;;;;GAIG;AACH,SAAS,IAAI,CACX,IAAU,EACV,UAAkB,EAClB,WAAmB,EACnB,KAAa,EACb,IAA+B,EAC/B,IAAY,EACZ,GAA+C;IAE/C,IAAI,CAAS,CAAC;IACd,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,wEAAwE;QACxE,CAAC,GAAG,UAAU,GAAG,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;IAC1C,CAAC;SAAM,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QAC5B,CAAC,GAAG,UAAU,GAAG,WAAW,GAAG,IAAI,CAAC;IACtC,CAAC;SAAM,CAAC;QACN,2BAA2B;QAC3B,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;IAC3B,CAAC;IAED,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE;QAC1B,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE;QAChB,CAAC;QACD,CAAC,EAAE,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;QACjC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK;QACtB,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM;QACxB,KAAK;QACL,IAAI;KACL,CAAC,CAAC;IAEH,+DAA+D;IAC/D,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QAC1D,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC;AAED,MAAM,UAAU,MAAM,CACpB,IAAc,EACd,UAAyB,EAAE;IAE3B,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,cAAc,CAAC;IAClD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC;IAChC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC;IAChC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,IAAI,CAAC;IAE1C,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACtC,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC;IAE/B,oDAAoD;IACpD,MAAM,UAAU,GAAG,QAAQ,CAAC,QAAQ,CAAC;IACrC,IAAI,UAAkB,CAAC;IACvB,IAAI,SAAiB,CAAC;IACtB,IAAI,QAAQ,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC9C,UAAU,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;QACvC,SAAS,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC;SAAM,CAAC;QACN,UAAU,GAAG,UAAU,CAAC;QACxB,SAAS,GAAG,EAAE,CAAC;IACjB,CAAC;IAED,yCAAyC;IACzC,MAAM,WAAW,GAAG,CAAC,KAAa,EAAU,EAAE;QAC5C,MAAM,MAAM,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;QACxB,KAAK,MAAM,CAAC,IAAI,KAAK;YAAE,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QACvD,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC;IAClD,CAAC,CAAC;IACF,MAAM,WAAW,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;IAC5C,MAAM,UAAU,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IACtD,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAE7D,yDAAyD;IACzD,KAAK,MAAM,CAAC,IAAI,UAAU;QACxB,aAAa,CAAC,CAAC,EAAE,CAAC,aAAa,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;IACtD,KAAK,MAAM,CAAC,IAAI,SAAS;QAAE,aAAa,CAAC,CAAC,EAAE,CAAC,aAAa,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IAE9E,QAAQ,CAAC,EAAE,GAAG,aAAa,GAAG,CAAC,CAAC;IAEhC,MAAM,GAAG,GAAG,EAAE,KAAK,EAAE,IAAI,GAAG,EAAe,EAAE,KAAK,EAAE,EAAY,EAAE,CAAC;IAEnE,sCAAsC;IACtC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE;QACrB,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAC;QACtB,CAAC,EAAE,QAAQ,CAAC,EAAE,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC;QACpC,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,KAAK,EAAE,CAAC;QACR,IAAI,EAAE,MAAM;KACb,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAC,CAAC;IACrC,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QACjD,IAAI,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;IAC3D,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;QAC1B,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QACjD,IAAI,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;IAC1D,CAAC;IAED,kBAAkB;IAClB,IAAI,IAAI,GAAG,QAAQ,CAAC;IACpB,IAAI,IAAI,GAAG,QAAQ,CAAC;IACpB,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC;IACrB,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC;IACrB,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;QACnC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;QACrC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;IACxC,CAAC;IAED,OAAO;QACL,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,MAAM,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,GAAG,IAAI,EAAE,MAAM,EAAE,IAAI,GAAG,IAAI,EAAE;KACtE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The clarity data model.
3
+ *
4
+ * A mind map is a single tree of {@link MindNode}s. Every operation in this
5
+ * module is **pure**: it returns a new tree and never mutates its input. That
6
+ * makes undo/redo trivial (keep old references) and rendering predictable.
7
+ */
8
+ /** A single topic in the map. */
9
+ export interface MindNode {
10
+ /** Stable unique id. */
11
+ id: string;
12
+ /** The topic text. */
13
+ text: string;
14
+ /** Child topics, in display order. */
15
+ children: MindNode[];
16
+ /** When true, children are hidden (and treated as absent by layout). */
17
+ collapsed?: boolean;
18
+ /** Optional longer note attached to the topic. */
19
+ note?: string;
20
+ }
21
+ /** A whole map. Thin wrapper so the shape can grow (themes, meta) without churn. */
22
+ export interface MindMap {
23
+ root: MindNode;
24
+ }
25
+ /** Generate a collision-resistant id. Deterministic-enough for a session, unique across maps. */
26
+ export declare function createId(): string;
27
+ /** Create a node. Pass an explicit `id` (e.g. in tests) or let one be generated. */
28
+ export declare function node(text: string, children?: MindNode[], opts?: {
29
+ id?: string;
30
+ collapsed?: boolean;
31
+ note?: string;
32
+ }): MindNode;
33
+ /** Create a map from a root node (or root text). */
34
+ export declare function createMap(root: MindNode | string): MindMap;
35
+ /** Deep clone a subtree. */
36
+ export declare function cloneNode(n: MindNode): MindNode;
37
+ /** Depth-first find. Returns the node with `id`, or `undefined`. */
38
+ export declare function findNode(root: MindNode, id: string): MindNode | undefined;
39
+ /** Find the parent of `id`. Returns `undefined` for the root or a missing id. */
40
+ export declare function findParent(root: MindNode, id: string): MindNode | undefined;
41
+ /** True if `ancestorId` is `id` or an ancestor of `id`. Used to block illegal moves. */
42
+ export declare function isAncestor(root: MindNode, ancestorId: string, id: string): boolean;
43
+ /** Update a node's fields (text/note/collapsed). Returns a new tree. */
44
+ export declare function updateNode(root: MindNode, id: string, patch: Partial<Omit<MindNode, "id" | "children">>): MindNode;
45
+ /** Insert `child` under `parentId` at `index` (clamped; defaults to the end). */
46
+ export declare function insertChild(root: MindNode, parentId: string, child: MindNode, index?: number): MindNode;
47
+ /** Remove the subtree rooted at `id`. Throws if `id` is the root. Returns a new tree. */
48
+ export declare function removeNode(root: MindNode, id: string): MindNode;
49
+ /** Toggle (or set) the collapsed flag on a node. Returns a new tree. */
50
+ export declare function toggleCollapsed(root: MindNode, id: string, value?: boolean): MindNode;
51
+ /**
52
+ * Move the subtree `id` under `newParentId` at `index`.
53
+ * Throws if the move would create a cycle (moving a node into its own descendant)
54
+ * or if either node is missing. Returns a new tree.
55
+ */
56
+ export declare function moveNode(root: MindNode, id: string, newParentId: string, index?: number): MindNode;
57
+ /** Visit every node depth-first (parent before children). */
58
+ export declare function walk(root: MindNode, visit: (n: MindNode, depth: number) => void): void;
59
+ /** Count all nodes in the tree. */
60
+ export declare function countNodes(root: MindNode): number;
package/dist/model.js ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * The clarity data model.
3
+ *
4
+ * A mind map is a single tree of {@link MindNode}s. Every operation in this
5
+ * module is **pure**: it returns a new tree and never mutates its input. That
6
+ * makes undo/redo trivial (keep old references) and rendering predictable.
7
+ */
8
+ let idCounter = 0;
9
+ /** Generate a collision-resistant id. Deterministic-enough for a session, unique across maps. */
10
+ export function createId() {
11
+ idCounter += 1;
12
+ const rand = Math.random().toString(36).slice(2, 8);
13
+ return `n${idCounter.toString(36)}-${rand}`;
14
+ }
15
+ /** Create a node. Pass an explicit `id` (e.g. in tests) or let one be generated. */
16
+ export function node(text, children = [], opts = {}) {
17
+ return {
18
+ id: opts.id ?? createId(),
19
+ text,
20
+ children,
21
+ ...(opts.collapsed ? { collapsed: true } : {}),
22
+ ...(opts.note !== undefined ? { note: opts.note } : {}),
23
+ };
24
+ }
25
+ /** Create a map from a root node (or root text). */
26
+ export function createMap(root) {
27
+ return { root: typeof root === "string" ? node(root) : root };
28
+ }
29
+ /** Deep clone a subtree. */
30
+ export function cloneNode(n) {
31
+ return { ...n, children: n.children.map(cloneNode) };
32
+ }
33
+ /** Depth-first find. Returns the node with `id`, or `undefined`. */
34
+ export function findNode(root, id) {
35
+ if (root.id === id)
36
+ return root;
37
+ for (const child of root.children) {
38
+ const hit = findNode(child, id);
39
+ if (hit)
40
+ return hit;
41
+ }
42
+ return undefined;
43
+ }
44
+ /** Find the parent of `id`. Returns `undefined` for the root or a missing id. */
45
+ export function findParent(root, id) {
46
+ for (const child of root.children) {
47
+ if (child.id === id)
48
+ return root;
49
+ const hit = findParent(child, id);
50
+ if (hit)
51
+ return hit;
52
+ }
53
+ return undefined;
54
+ }
55
+ /** True if `ancestorId` is `id` or an ancestor of `id`. Used to block illegal moves. */
56
+ export function isAncestor(root, ancestorId, id) {
57
+ const ancestor = findNode(root, ancestorId);
58
+ if (!ancestor)
59
+ return false;
60
+ return !!findNode(ancestor, id);
61
+ }
62
+ /**
63
+ * Return a new tree where the node matching `id` is replaced by `fn(node)`.
64
+ * Only the path to that node is rebuilt; untouched subtrees are shared.
65
+ */
66
+ function transform(root, id, fn) {
67
+ if (root.id === id)
68
+ return fn(root);
69
+ let changed = false;
70
+ const children = root.children.map((child) => {
71
+ const next = transform(child, id, fn);
72
+ if (next !== child)
73
+ changed = true;
74
+ return next;
75
+ });
76
+ return changed ? { ...root, children } : root;
77
+ }
78
+ /** Update a node's fields (text/note/collapsed). Returns a new tree. */
79
+ export function updateNode(root, id, patch) {
80
+ return transform(root, id, (n) => ({ ...n, ...patch }));
81
+ }
82
+ /** Insert `child` under `parentId` at `index` (clamped; defaults to the end). */
83
+ export function insertChild(root, parentId, child, index = Number.POSITIVE_INFINITY) {
84
+ return transform(root, parentId, (p) => {
85
+ const children = [...p.children];
86
+ const i = Math.max(0, Math.min(index, children.length));
87
+ children.splice(i, 0, child);
88
+ return { ...p, children };
89
+ });
90
+ }
91
+ /** Remove the subtree rooted at `id`. Throws if `id` is the root. Returns a new tree. */
92
+ export function removeNode(root, id) {
93
+ if (root.id === id) {
94
+ throw new Error("clarity: cannot remove the root node");
95
+ }
96
+ const recur = (n) => {
97
+ let changed = false;
98
+ const kept = n.children.filter((c) => c.id !== id);
99
+ if (kept.length !== n.children.length)
100
+ changed = true;
101
+ const children = kept.map((c) => {
102
+ const next = recur(c);
103
+ if (next !== c)
104
+ changed = true;
105
+ return next;
106
+ });
107
+ return changed ? { ...n, children } : n;
108
+ };
109
+ return recur(root);
110
+ }
111
+ /** Toggle (or set) the collapsed flag on a node. Returns a new tree. */
112
+ export function toggleCollapsed(root, id, value) {
113
+ return transform(root, id, (n) => ({
114
+ ...n,
115
+ collapsed: value ?? !n.collapsed,
116
+ }));
117
+ }
118
+ /**
119
+ * Move the subtree `id` under `newParentId` at `index`.
120
+ * Throws if the move would create a cycle (moving a node into its own descendant)
121
+ * or if either node is missing. Returns a new tree.
122
+ */
123
+ export function moveNode(root, id, newParentId, index = Number.POSITIVE_INFINITY) {
124
+ if (id === root.id)
125
+ throw new Error("clarity: cannot move the root node");
126
+ if (id === newParentId)
127
+ throw new Error("clarity: cannot move a node into itself");
128
+ if (isAncestor(root, id, newParentId)) {
129
+ throw new Error("clarity: cannot move a node into its own descendant");
130
+ }
131
+ const subtree = findNode(root, id);
132
+ if (!subtree)
133
+ throw new Error(`clarity: node "${id}" not found`);
134
+ if (!findNode(root, newParentId))
135
+ throw new Error(`clarity: node "${newParentId}" not found`);
136
+ const detached = removeNode(root, id);
137
+ return insertChild(detached, newParentId, cloneNode(subtree), index);
138
+ }
139
+ /** Visit every node depth-first (parent before children). */
140
+ export function walk(root, visit) {
141
+ const go = (n, depth) => {
142
+ visit(n, depth);
143
+ for (const c of n.children)
144
+ go(c, depth + 1);
145
+ };
146
+ go(root, 0);
147
+ }
148
+ /** Count all nodes in the tree. */
149
+ export function countNodes(root) {
150
+ let count = 0;
151
+ walk(root, () => (count += 1));
152
+ return count;
153
+ }
154
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import { type MindMap, type MindNode } from "./model.js";
2
+ export type Unsubscribe = () => void;
3
+ export declare class MindMapStore {
4
+ private history;
5
+ private listeners;
6
+ constructor(root: MindNode | string, historyLimit?: number);
7
+ /** The current map (immutable snapshot). */
8
+ get map(): MindMap;
9
+ /** The root node's id — handy since callers often add to the root. */
10
+ get rootId(): string;
11
+ /** Subscribe to changes. Returns an unsubscribe function. */
12
+ subscribe(listener: () => void): Unsubscribe;
13
+ private commit;
14
+ private emit;
15
+ /** Add a child topic under `parentId`. Returns the new node's id. */
16
+ addChild(parentId: string, text?: string, index?: number): string;
17
+ /** Add a sibling after `siblingId`. Throws if `siblingId` is the root. Returns the new id. */
18
+ addSibling(siblingId: string, text?: string): string;
19
+ /** Change a node's text. */
20
+ setText(id: string, text: string): void;
21
+ /** Change a node's note. */
22
+ setNote(id: string, note: string): void;
23
+ /** Remove a subtree. */
24
+ remove(id: string): void;
25
+ /** Toggle (or set) a node's collapsed flag. */
26
+ toggleCollapse(id: string, value?: boolean): void;
27
+ /** Move a subtree under a new parent. */
28
+ move(id: string, newParentId: string, index?: number): void;
29
+ /** Replace the whole map (e.g. after an import). Resets nothing but the current state. */
30
+ replace(map: MindMap): void;
31
+ canUndo(): boolean;
32
+ canRedo(): boolean;
33
+ undo(): void;
34
+ redo(): void;
35
+ }
36
+ /** Convenience factory. */
37
+ export declare function createStore(root: MindNode | string): MindMapStore;
package/dist/store.js ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * An ergonomic, mutable-feeling wrapper over the pure model + history.
3
+ *
4
+ * The store holds the current {@link MindMap} and an undo stack. Every mutating
5
+ * method applies a pure operation, records the new snapshot, and notifies
6
+ * subscribers. This is what a UI binding (e.g. the React package) drives.
7
+ */
8
+ import { History } from "./history.js";
9
+ import { createMap, insertChild, moveNode, node, removeNode, toggleCollapsed, updateNode, } from "./model.js";
10
+ export class MindMapStore {
11
+ constructor(root, historyLimit = 200) {
12
+ this.listeners = new Set();
13
+ this.history = new History(createMap(root), historyLimit);
14
+ }
15
+ /** The current map (immutable snapshot). */
16
+ get map() {
17
+ return this.history.current;
18
+ }
19
+ /** The root node's id — handy since callers often add to the root. */
20
+ get rootId() {
21
+ return this.map.root.id;
22
+ }
23
+ /** Subscribe to changes. Returns an unsubscribe function. */
24
+ subscribe(listener) {
25
+ this.listeners.add(listener);
26
+ return () => this.listeners.delete(listener);
27
+ }
28
+ commit(nextRoot) {
29
+ if (nextRoot === this.map.root)
30
+ return;
31
+ this.history.push({ root: nextRoot });
32
+ this.emit();
33
+ }
34
+ emit() {
35
+ for (const l of this.listeners)
36
+ l();
37
+ }
38
+ /** Add a child topic under `parentId`. Returns the new node's id. */
39
+ addChild(parentId, text = "", index) {
40
+ const child = node(text);
41
+ this.commit(insertChild(this.map.root, parentId, child, index));
42
+ return child.id;
43
+ }
44
+ /** Add a sibling after `siblingId`. Throws if `siblingId` is the root. Returns the new id. */
45
+ addSibling(siblingId, text = "") {
46
+ const parent = findParentId(this.map.root, siblingId);
47
+ if (!parent)
48
+ throw new Error("clarity: cannot add a sibling to the root");
49
+ const idx = childIndex(this.map.root, parent, siblingId);
50
+ const child = node(text);
51
+ this.commit(insertChild(this.map.root, parent, child, idx + 1));
52
+ return child.id;
53
+ }
54
+ /** Change a node's text. */
55
+ setText(id, text) {
56
+ this.commit(updateNode(this.map.root, id, { text }));
57
+ }
58
+ /** Change a node's note. */
59
+ setNote(id, note) {
60
+ this.commit(updateNode(this.map.root, id, { note }));
61
+ }
62
+ /** Remove a subtree. */
63
+ remove(id) {
64
+ this.commit(removeNode(this.map.root, id));
65
+ }
66
+ /** Toggle (or set) a node's collapsed flag. */
67
+ toggleCollapse(id, value) {
68
+ this.commit(toggleCollapsed(this.map.root, id, value));
69
+ }
70
+ /** Move a subtree under a new parent. */
71
+ move(id, newParentId, index) {
72
+ this.commit(moveNode(this.map.root, id, newParentId, index));
73
+ }
74
+ /** Replace the whole map (e.g. after an import). Resets nothing but the current state. */
75
+ replace(map) {
76
+ this.history.push(map);
77
+ this.emit();
78
+ }
79
+ canUndo() {
80
+ return this.history.canUndo();
81
+ }
82
+ canRedo() {
83
+ return this.history.canRedo();
84
+ }
85
+ undo() {
86
+ if (!this.canUndo())
87
+ return;
88
+ this.history.undo();
89
+ this.emit();
90
+ }
91
+ redo() {
92
+ if (!this.canRedo())
93
+ return;
94
+ this.history.redo();
95
+ this.emit();
96
+ }
97
+ }
98
+ function findParentId(root, id) {
99
+ for (const child of root.children) {
100
+ if (child.id === id)
101
+ return root.id;
102
+ const hit = findParentId(child, id);
103
+ if (hit)
104
+ return hit;
105
+ }
106
+ return undefined;
107
+ }
108
+ function childIndex(root, parentId, childId) {
109
+ const find = (n) => {
110
+ if (n.id === parentId)
111
+ return n.children.findIndex((c) => c.id === childId);
112
+ for (const c of n.children) {
113
+ const i = find(c);
114
+ if (i !== -1)
115
+ return i;
116
+ }
117
+ return -1;
118
+ };
119
+ return find(root);
120
+ }
121
+ /** Convenience factory. */
122
+ export function createStore(root) {
123
+ return new MindMapStore(root);
124
+ }
125
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EACL,SAAS,EACT,WAAW,EACX,QAAQ,EACR,IAAI,EACJ,UAAU,EACV,eAAe,EACf,UAAU,GAGX,MAAM,YAAY,CAAC;AAIpB,MAAM,OAAO,YAAY;IAIvB,YAAY,IAAuB,EAAE,YAAY,GAAG,GAAG;QAF/C,cAAS,GAAG,IAAI,GAAG,EAAc,CAAC;QAGxC,IAAI,CAAC,OAAO,GAAG,IAAI,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC,CAAC;IAC5D,CAAC;IAED,4CAA4C;IAC5C,IAAI,GAAG;QACL,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;IAC9B,CAAC;IAED,sEAAsE;IACtE,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;IAC1B,CAAC;IAED,6DAA6D;IAC7D,SAAS,CAAC,QAAoB;QAC5B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC/C,CAAC;IAEO,MAAM,CAAC,QAAkB;QAC/B,IAAI,QAAQ,KAAK,IAAI,CAAC,GAAG,CAAC,IAAI;YAAE,OAAO;QACvC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;QACtC,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IAEO,IAAI;QACV,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,SAAS;YAAE,CAAC,EAAE,CAAC;IACtC,CAAC;IAED,qEAAqE;IACrE,QAAQ,CAAC,QAAgB,EAAE,IAAI,GAAG,EAAE,EAAE,KAAc;QAClD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;QACzB,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;QAChE,OAAO,KAAK,CAAC,EAAE,CAAC;IAClB,CAAC;IAED,8FAA8F;IAC9F,UAAU,CAAC,SAAiB,EAAE,IAAI,GAAG,EAAE;QACrC,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QACtD,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;QAC1E,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;QACzD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;QACzB,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QAChE,OAAO,KAAK,CAAC,EAAE,CAAC;IAClB,CAAC;IAED,4BAA4B;IAC5B,OAAO,CAAC,EAAU,EAAE,IAAY;QAC9B,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACvD,CAAC;IAED,4BAA4B;IAC5B,OAAO,CAAC,EAAU,EAAE,IAAY;QAC9B,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACvD,CAAC;IAED,wBAAwB;IACxB,MAAM,CAAC,EAAU;QACf,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;IAC7C,CAAC;IAED,+CAA+C;IAC/C,cAAc,CAAC,EAAU,EAAE,KAAe;QACxC,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC;IACzD,CAAC;IAED,yCAAyC;IACzC,IAAI,CAAC,EAAU,EAAE,WAAmB,EAAE,KAAc;QAClD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC;IAC/D,CAAC;IAED,0FAA0F;IAC1F,OAAO,CAAC,GAAY;QAClB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACvB,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IAED,OAAO;QACL,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;IAChC,CAAC;IAED,OAAO;QACL,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;IAChC,CAAC;IAED,IAAI;QACF,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;YAAE,OAAO;QAC5B,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QACpB,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IAED,IAAI;QACF,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;YAAE,OAAO;QAC5B,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QACpB,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;CACF;AAED,SAAS,YAAY,CAAC,IAAc,EAAE,EAAU;IAC9C,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClC,IAAI,KAAK,CAAC,EAAE,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QACpC,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACpC,IAAI,GAAG;YAAE,OAAO,GAAG,CAAC;IACtB,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,UAAU,CAAC,IAAc,EAAE,QAAgB,EAAE,OAAe;IACnE,MAAM,IAAI,GAAG,CAAC,CAAW,EAAU,EAAE;QACnC,IAAI,CAAC,CAAC,EAAE,KAAK,QAAQ;YAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,OAAO,CAAC,CAAC;QAC5E,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;YAC3B,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;YAClB,IAAI,CAAC,KAAK,CAAC,CAAC;gBAAE,OAAO,CAAC,CAAC;QACzB,CAAC;QACD,OAAO,CAAC,CAAC,CAAC;IACZ,CAAC,CAAC;IACF,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC;AACpB,CAAC;AAED,2BAA2B;AAC3B,MAAM,UAAU,WAAW,CAAC,IAAuB;IACjD,OAAO,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;AAChC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "clarity-mind",
3
+ "version": "0.1.0",
4
+ "description": "Headless, framework-agnostic mind-mapping engine: data model, tidy-tree layout, undo/redo, Markdown/JSON I/O.",
5
+ "keywords": [
6
+ "mind-map",
7
+ "mindmap",
8
+ "brainstorming",
9
+ "layout",
10
+ "tidy-tree",
11
+ "tree",
12
+ "diagram",
13
+ "headless"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "udielenberg <udielenberg@users.noreply.github.com>",
17
+ "homepage": "https://github.com/udielenberg/clarity#readme",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/udielenberg/clarity.git",
21
+ "directory": "packages/core"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/udielenberg/clarity/issues"
25
+ },
26
+ "type": "module",
27
+ "main": "./dist/index.js",
28
+ "types": "./dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.js"
33
+ }
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "README.md"
38
+ ],
39
+ "sideEffects": false,
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.build.json",
42
+ "dev": "tsc -p tsconfig.build.json --watch",
43
+ "prepublishOnly": "npm run build"
44
+ }
45
+ }