@loom-forge/editor 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 +91 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +134 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @loom-forge/editor
|
|
2
|
+
|
|
3
|
+
O **controller de edição**: a definição num `TreeState` do ApexStore, com ops atômicas em
|
|
4
|
+
transação, undo/redo por snapshot e o patch da mudança a cada commit. Não reimplementa estado,
|
|
5
|
+
transação nem undo — compõe o [ApexStore](https://github.com/andersondrosa).
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @loom-forge/editor @statedelta-apex/tree-state
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## `createEditor(definition, opts?)`
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createEditor } from "@loom-forge/editor";
|
|
15
|
+
|
|
16
|
+
const ed = createEditor(definition, {
|
|
17
|
+
address: createMyersAddress(), // emite o patch da mudança
|
|
18
|
+
onChange: (c) => persist(c.patch),
|
|
19
|
+
});
|
|
20
|
+
ed.insert("section-1", { id: "f1", type: "Field" }, 0);
|
|
21
|
+
ed.move("f1", "section-2");
|
|
22
|
+
ed.undo();
|
|
23
|
+
render(ed.getDefinition());
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
function createEditor(definition: DefNode, opts?: CreateEditorOptions): Editor;
|
|
28
|
+
|
|
29
|
+
interface CreateEditorOptions {
|
|
30
|
+
treeId?: string;
|
|
31
|
+
address?: AddressPort; // json-myers — patch por commit
|
|
32
|
+
onChange?(change: { def: DefNode; patch?: unknown }): void;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| Método | Descrição |
|
|
37
|
+
| ---------------------------------- | --------------------------------------------------------------- |
|
|
38
|
+
| `getDefinition()` | definição atual (aninhada), derivada do TreeState |
|
|
39
|
+
| `insert(parentId, node, atIndex?)` | insere subárvore sob `parentId` (default: fim) |
|
|
40
|
+
| `remove(id)` | remove o nó + subárvore (**cascade**) |
|
|
41
|
+
| `move(id, toParentId, atIndex?)` | re-parenta + posiciona |
|
|
42
|
+
| `reorder(id, atIndex)` | reordena dentro do pai atual |
|
|
43
|
+
| `update(id, patch)` | atualiza props/placement/type/events/layout (não move/renomeia) |
|
|
44
|
+
| `undo()` / `redo()` | `boolean` — pilha de snapshots |
|
|
45
|
+
| `canUndo()` / `canRedo()` | `boolean` |
|
|
46
|
+
| `subscribe(listener)` | notifica a definição a cada mudança; retorna unsubscribe |
|
|
47
|
+
|
|
48
|
+
Cada op é uma **transação atômica** (rollback no throw → sem entrada de undo) e emite o patch
|
|
49
|
+
identity-aware no `onChange`. **Invariante (conformance):** `address.patch(antes, change.patch)` ===
|
|
50
|
+
`ed.getDefinition()`.
|
|
51
|
+
|
|
52
|
+
### bridge (flat ↔ nested)
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { fromNested, toNested, type FlatNode } from "@loom-forge/editor";
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`fromNested(def): FlatNode[]` (aninhado → flat, atribui `name=id`/`parentId`/`__order`) e
|
|
59
|
+
`toNested(records): DefNode` (flat → aninhado, ordena por `__order`, remove os campos internos).
|
|
60
|
+
Round-trip garantido: `toNested(fromNested(def)) === def`.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Os vizinhos
|
|
65
|
+
|
|
66
|
+
O patch vem do [`@loom-forge/address`](../address/README.md) (o `AddressPort`, DI), e é ele que
|
|
67
|
+
faz o `onChange` carregar a mudança em formato de fio. A camada de tipos é o
|
|
68
|
+
[`@loom-forge/check`](../check/README.md). O invariante que prende os três está na suíte de
|
|
69
|
+
conformance: `address.patch(antes, change.patch) === ed.getDefinition()`.
|
|
70
|
+
|
|
71
|
+
## Um editor por COMPONENTE
|
|
72
|
+
|
|
73
|
+
O editor segura **uma árvore** — a de um `ComponentDef` — e o `id` é único dentro dela (ADR-205:
|
|
74
|
+
`name = id` no `TreeState`). Num sistema com vários componentes os ids se repetem entre eles,
|
|
75
|
+
então quem edita o documento inteiro mantém **um editor por componente**, e não um do documento.
|
|
76
|
+
Foi assim que a bancada montou o inspetor: cada componente em edição tem o seu `createEditor`,
|
|
77
|
+
o campo do inspetor grava no commit (blur ou `Enter`) — um passo de desfazer por **edição**, não
|
|
78
|
+
por tecla — e `update(id, { props })` / `update(id, { placement })` é a op de cada campo.
|
|
79
|
+
|
|
80
|
+
Duas consequências que a bancada achou e valem para qualquer consumidor:
|
|
81
|
+
|
|
82
|
+
- **um id repetido dentro da definição bloqueia a edição** dela até ser corrigido: o editor não
|
|
83
|
+
tem como apontar para um nó só. Quem acusa é o `checkIdentity` do `@loom-forge/forge`, antes
|
|
84
|
+
de qualquer op;
|
|
85
|
+
- **trocar a definição por outra autoria** (o texto editado à mão, outra sessão) é outra árvore,
|
|
86
|
+
que o editor não viu: o histórico recomeça com um `createEditor` novo. Um `undo` que
|
|
87
|
+
atravessasse a troca desfaria uma mudança que este editor não fez.
|
|
88
|
+
|
|
89
|
+
O `update(id, patch)` escreve o campo inteiro — em `events`, é a intenção com a cadeia dela
|
|
90
|
+
(`params`, `then`, `catch`; ADR-018 do repo) —, e é o `patch` do `AddressPort` que diz ao fio
|
|
91
|
+
o que mudou por dentro.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { AddressPort, DefNode } from '@loom-forge/core';
|
|
2
|
+
|
|
3
|
+
interface EditorChange {
|
|
4
|
+
/** definição (aninhada) após a mudança. */
|
|
5
|
+
def: DefNode;
|
|
6
|
+
/** patch identity-aware da mudança (se um AddressPort foi provido). */
|
|
7
|
+
patch?: unknown;
|
|
8
|
+
}
|
|
9
|
+
interface CreateEditorOptions {
|
|
10
|
+
treeId?: string;
|
|
11
|
+
/** AddressPort (json-myers) — emite o patch da mudança no `onChange`. */
|
|
12
|
+
address?: AddressPort;
|
|
13
|
+
onChange?(change: EditorChange): void;
|
|
14
|
+
}
|
|
15
|
+
interface Editor {
|
|
16
|
+
/** definição atual (aninhada), derivada do TreeState. */
|
|
17
|
+
getDefinition(): DefNode;
|
|
18
|
+
/** insere uma subárvore sob `parentId`, na posição `atIndex` (default: fim). */
|
|
19
|
+
insert(parentId: string, node: DefNode, atIndex?: number): void;
|
|
20
|
+
/** remove o nó e toda a subárvore (cascade). */
|
|
21
|
+
remove(id: string): void;
|
|
22
|
+
/** re-parenta para `toParentId` e posiciona em `atIndex`. */
|
|
23
|
+
move(id: string, toParentId: string, atIndex?: number): void;
|
|
24
|
+
/** reordena dentro do pai atual. */
|
|
25
|
+
reorder(id: string, atIndex: number): void;
|
|
26
|
+
/** atualiza campos do nó (props/placement/type/events/layout…); não move nem renomeia. */
|
|
27
|
+
update(id: string, patch: Partial<Omit<DefNode, "id" | "children">>): void;
|
|
28
|
+
undo(): boolean;
|
|
29
|
+
redo(): boolean;
|
|
30
|
+
canUndo(): boolean;
|
|
31
|
+
canRedo(): boolean;
|
|
32
|
+
/** notifica a definição (aninhada) a cada mudança. */
|
|
33
|
+
subscribe(listener: (def: DefNode) => void): () => void;
|
|
34
|
+
}
|
|
35
|
+
declare function createEditor(definition: DefNode, opts?: CreateEditorOptions): Editor;
|
|
36
|
+
|
|
37
|
+
/** Record flat de um nó no TreeState. `name` (= id) satisfaz a unicidade de irmãos do
|
|
38
|
+
* Apex; `__order` é a ordem entre irmãos (interno). O index signature satisfaz o
|
|
39
|
+
* `T extends Record<string, unknown>` do TreeState. */
|
|
40
|
+
interface FlatNode {
|
|
41
|
+
id: string;
|
|
42
|
+
parentId: string | null;
|
|
43
|
+
name: string;
|
|
44
|
+
type?: string;
|
|
45
|
+
__order: number;
|
|
46
|
+
props?: Record<string, unknown>;
|
|
47
|
+
placement?: unknown;
|
|
48
|
+
events?: DefNode["events"];
|
|
49
|
+
layout?: string;
|
|
50
|
+
fallback?: DefNode["fallback"];
|
|
51
|
+
requires?: Record<string, string>;
|
|
52
|
+
version?: string;
|
|
53
|
+
[key: string]: unknown;
|
|
54
|
+
}
|
|
55
|
+
/** Aninhado → flat (pré-ordem). Atribui `name = id`, `parentId` e `__order` (posição). */
|
|
56
|
+
declare function fromNested(root: DefNode): FlatNode[];
|
|
57
|
+
/** Flat → aninhado. Agrupa por `parentId`, ordena cada grupo por `__order`, e remove os
|
|
58
|
+
* campos internos — o `DefNode` resultante é limpo. Assume raiz única. */
|
|
59
|
+
declare function toNested(records: FlatNode[]): DefNode;
|
|
60
|
+
|
|
61
|
+
export { type CreateEditorOptions, type Editor, type EditorChange, type FlatNode, createEditor, fromNested, toNested };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createTree } from '@statedelta-apex/tree-state';
|
|
2
|
+
|
|
3
|
+
// src/controller.ts
|
|
4
|
+
|
|
5
|
+
// src/bridge.ts
|
|
6
|
+
var INTERNAL = /* @__PURE__ */ new Set(["parentId", "__order", "name"]);
|
|
7
|
+
function fromNested(root) {
|
|
8
|
+
const out = [];
|
|
9
|
+
const walk = (node, parentId, order) => {
|
|
10
|
+
const { children, ...rest } = node;
|
|
11
|
+
out.push({ ...rest, name: node.id, parentId, __order: order });
|
|
12
|
+
(children ?? []).forEach((child, i) => walk(child, node.id, i));
|
|
13
|
+
};
|
|
14
|
+
walk(root, null, 0);
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
function toNested(records) {
|
|
18
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
19
|
+
for (const r of records) {
|
|
20
|
+
const key = r.parentId ?? null;
|
|
21
|
+
const group = byParent.get(key);
|
|
22
|
+
if (group) group.push(r);
|
|
23
|
+
else byParent.set(key, [r]);
|
|
24
|
+
}
|
|
25
|
+
const strip = (r) => {
|
|
26
|
+
const node = {};
|
|
27
|
+
for (const k of Object.keys(r)) if (!INTERNAL.has(k)) node[k] = r[k];
|
|
28
|
+
return node;
|
|
29
|
+
};
|
|
30
|
+
const build = (r) => {
|
|
31
|
+
const node = strip(r);
|
|
32
|
+
const kids = (byParent.get(r.id) ?? []).slice().sort((a, b) => a.__order - b.__order).map(build);
|
|
33
|
+
if (kids.length) node.children = kids;
|
|
34
|
+
return node;
|
|
35
|
+
};
|
|
36
|
+
const roots = (byParent.get(null) ?? []).slice().sort((a, b) => a.__order - b.__order);
|
|
37
|
+
if (!roots[0]) throw new Error("toNested: defini\xE7\xE3o sem raiz.");
|
|
38
|
+
return build(roots[0]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/controller.ts
|
|
42
|
+
function createEditor(definition, opts = {}) {
|
|
43
|
+
const tree = createTree(
|
|
44
|
+
opts.treeId ?? "loom-editor",
|
|
45
|
+
fromNested(definition)
|
|
46
|
+
);
|
|
47
|
+
const undoStack = [];
|
|
48
|
+
const redoStack = [];
|
|
49
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
50
|
+
const current = () => toNested(tree.getState());
|
|
51
|
+
const parentOf = (id) => tree.getState().find((r) => r.id === id)?.parentId ?? null;
|
|
52
|
+
const siblingIds = (parentId) => tree.getState().filter((r) => (r.parentId ?? null) === parentId).sort((a, b) => a.__order - b.__order).map((r) => r.id);
|
|
53
|
+
const place = (id, parentId, atIndex) => {
|
|
54
|
+
const sibs = siblingIds(parentId).filter((s) => s !== id);
|
|
55
|
+
const i = atIndex == null ? sibs.length : Math.max(0, Math.min(atIndex, sibs.length));
|
|
56
|
+
sibs.splice(i, 0, id);
|
|
57
|
+
sibs.forEach((sid, k) => tree.update(sid, { __order: k }));
|
|
58
|
+
};
|
|
59
|
+
const emit = (before) => {
|
|
60
|
+
const def = current();
|
|
61
|
+
opts.onChange?.({ def, patch: opts.address?.diff(before, def) });
|
|
62
|
+
for (const l of listeners) l(def);
|
|
63
|
+
};
|
|
64
|
+
const commit = (fn) => {
|
|
65
|
+
const before = current();
|
|
66
|
+
const snap = tree.snapshot();
|
|
67
|
+
tree.transaction(fn);
|
|
68
|
+
undoStack.push(snap);
|
|
69
|
+
redoStack.length = 0;
|
|
70
|
+
emit(before);
|
|
71
|
+
};
|
|
72
|
+
const restoreTo = (snap, counterStack) => {
|
|
73
|
+
const before = current();
|
|
74
|
+
counterStack.push(tree.snapshot());
|
|
75
|
+
tree.restore(snap);
|
|
76
|
+
emit(before);
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
getDefinition: current,
|
|
80
|
+
insert(parentId, node, atIndex) {
|
|
81
|
+
commit(() => {
|
|
82
|
+
for (const r of fromNested(node))
|
|
83
|
+
tree.add(r.id === node.id ? { ...r, parentId } : r);
|
|
84
|
+
place(node.id, parentId, atIndex);
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
remove(id) {
|
|
88
|
+
commit(() => {
|
|
89
|
+
tree.remove(id);
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
move(id, toParentId, atIndex) {
|
|
93
|
+
commit(() => {
|
|
94
|
+
tree.move(id, toParentId);
|
|
95
|
+
place(id, toParentId, atIndex);
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
reorder(id, atIndex) {
|
|
99
|
+
commit(() => place(id, parentOf(id), atIndex));
|
|
100
|
+
},
|
|
101
|
+
update(id, patch) {
|
|
102
|
+
commit(() => {
|
|
103
|
+
const safe = { ...patch };
|
|
104
|
+
for (const k of ["id", "parentId", "__order", "name", "children"])
|
|
105
|
+
delete safe[k];
|
|
106
|
+
tree.update(id, safe);
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
undo() {
|
|
110
|
+
const snap = undoStack.pop();
|
|
111
|
+
if (!snap) return false;
|
|
112
|
+
restoreTo(snap, redoStack);
|
|
113
|
+
return true;
|
|
114
|
+
},
|
|
115
|
+
redo() {
|
|
116
|
+
const snap = redoStack.pop();
|
|
117
|
+
if (!snap) return false;
|
|
118
|
+
restoreTo(snap, undoStack);
|
|
119
|
+
return true;
|
|
120
|
+
},
|
|
121
|
+
canUndo: () => undoStack.length > 0,
|
|
122
|
+
canRedo: () => redoStack.length > 0,
|
|
123
|
+
subscribe(listener) {
|
|
124
|
+
listeners.add(listener);
|
|
125
|
+
return () => {
|
|
126
|
+
listeners.delete(listener);
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export { createEditor, fromNested, toNested };
|
|
133
|
+
//# sourceMappingURL=index.js.map
|
|
134
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bridge.ts","../src/controller.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { DefNode } from \"@loom-forge/core\";\n\n/* ──────────────────────────────────────────────────────────────────────────────\n Bridge flat ↔ nested. O `TreeState` do ApexStore é FLAT (`{id, parentId, name}`,\n filhos derivados do parentId — modelo filesystem); o `DefNode` é ANINHADO\n (`children: DefNode[]`). O renderer quer aninhado; o editor trabalha flat (move O(1),\n cascade, transações). A ordem entre irmãos vira `__order` (bookkeeping do editor),\n removido na volta — o aninhado fica limpo (ordem = posição no array).\n ────────────────────────────────────────────────────────────────────────────── */\n\n/** Record flat de um nó no TreeState. `name` (= id) satisfaz a unicidade de irmãos do\n * Apex; `__order` é a ordem entre irmãos (interno). O index signature satisfaz o\n * `T extends Record<string, unknown>` do TreeState. */\nexport interface FlatNode {\n id: string;\n parentId: string | null;\n name: string;\n type?: string;\n __order: number;\n props?: Record<string, unknown>;\n placement?: unknown;\n events?: DefNode[\"events\"];\n layout?: string;\n fallback?: DefNode[\"fallback\"];\n requires?: Record<string, string>;\n version?: string;\n [key: string]: unknown;\n}\n\nconst INTERNAL = new Set([\"parentId\", \"__order\", \"name\"]);\n\n/** Aninhado → flat (pré-ordem). Atribui `name = id`, `parentId` e `__order` (posição). */\nexport function fromNested(root: DefNode): FlatNode[] {\n const out: FlatNode[] = [];\n const walk = (\n node: DefNode,\n parentId: string | null,\n order: number,\n ): void => {\n const { children, ...rest } = node;\n out.push({ ...rest, name: node.id, parentId, __order: order });\n (children ?? []).forEach((child, i) => walk(child, node.id, i));\n };\n walk(root, null, 0);\n return out;\n}\n\n/** Flat → aninhado. Agrupa por `parentId`, ordena cada grupo por `__order`, e remove os\n * campos internos — o `DefNode` resultante é limpo. Assume raiz única. */\nexport function toNested(records: FlatNode[]): DefNode {\n const byParent = new Map<string | null, FlatNode[]>();\n for (const r of records) {\n const key = r.parentId ?? null;\n const group = byParent.get(key);\n if (group) group.push(r);\n else byParent.set(key, [r]);\n }\n\n const strip = (r: FlatNode): DefNode => {\n const node: Record<string, unknown> = {};\n for (const k of Object.keys(r)) if (!INTERNAL.has(k)) node[k] = r[k];\n return node as unknown as DefNode;\n };\n const build = (r: FlatNode): DefNode => {\n const node = strip(r);\n const kids = (byParent.get(r.id) ?? [])\n .slice()\n .sort((a, b) => a.__order - b.__order)\n .map(build);\n if (kids.length) node.children = kids;\n return node;\n };\n\n const roots = (byParent.get(null) ?? [])\n .slice()\n .sort((a, b) => a.__order - b.__order);\n if (!roots[0]) throw new Error(\"toNested: definição sem raiz.\");\n return build(roots[0]);\n}\n","import { createTree } from \"@statedelta-apex/tree-state\";\nimport type { AddressPort, DefNode } from \"@loom-forge/core\";\nimport { fromNested, toNested, type FlatNode } from \"./bridge\";\n\n/* ──────────────────────────────────────────────────────────────────────────────\n Editor controller sobre o ApexStore — DI/opt-in. A definição vive num `TreeState`\n (move O(1) / cascade / queries); cada op é uma `transaction` ATÔMICA (rollback no\n throw); undo/redo via snapshot/restore (pilha); cada commit emite o patch da mudança\n (json-myers, via AddressPort). Não reimplementa estado/transação/undo — compõe o Apex.\n A bridge flat↔nested mantém o renderer (que quer aninhado) feliz.\n ────────────────────────────────────────────────────────────────────────────── */\n\nexport interface EditorChange {\n /** definição (aninhada) após a mudança. */\n def: DefNode;\n /** patch identity-aware da mudança (se um AddressPort foi provido). */\n patch?: unknown;\n}\n\nexport interface CreateEditorOptions {\n treeId?: string;\n /** AddressPort (json-myers) — emite o patch da mudança no `onChange`. */\n address?: AddressPort;\n onChange?(change: EditorChange): void;\n}\n\nexport interface Editor {\n /** definição atual (aninhada), derivada do TreeState. */\n getDefinition(): DefNode;\n /** insere uma subárvore sob `parentId`, na posição `atIndex` (default: fim). */\n insert(parentId: string, node: DefNode, atIndex?: number): void;\n /** remove o nó e toda a subárvore (cascade). */\n remove(id: string): void;\n /** re-parenta para `toParentId` e posiciona em `atIndex`. */\n move(id: string, toParentId: string, atIndex?: number): void;\n /** reordena dentro do pai atual. */\n reorder(id: string, atIndex: number): void;\n /** atualiza campos do nó (props/placement/type/events/layout…); não move nem renomeia. */\n update(id: string, patch: Partial<Omit<DefNode, \"id\" | \"children\">>): void;\n undo(): boolean;\n redo(): boolean;\n canUndo(): boolean;\n canRedo(): boolean;\n /** notifica a definição (aninhada) a cada mudança. */\n subscribe(listener: (def: DefNode) => void): () => void;\n}\n\nexport function createEditor(\n definition: DefNode,\n opts: CreateEditorOptions = {},\n): Editor {\n const tree = createTree<FlatNode>(\n opts.treeId ?? \"loom-editor\",\n fromNested(definition),\n );\n type Snap = ReturnType<typeof tree.snapshot>;\n const undoStack: Snap[] = [];\n const redoStack: Snap[] = [];\n const listeners = new Set<(def: DefNode) => void>();\n\n const current = (): DefNode => toNested(tree.getState());\n const parentOf = (id: string): string | null =>\n tree.getState().find((r) => r.id === id)?.parentId ?? null;\n const siblingIds = (parentId: string | null): string[] =>\n tree\n .getState()\n .filter((r) => (r.parentId ?? null) === parentId)\n .sort((a, b) => a.__order - b.__order)\n .map((r) => r.id);\n\n /** posiciona `id` entre os irmãos de `parentId` em `atIndex`, renumerando `__order`. */\n const place = (\n id: string,\n parentId: string | null,\n atIndex?: number,\n ): void => {\n const sibs = siblingIds(parentId).filter((s) => s !== id);\n const i =\n atIndex == null\n ? sibs.length\n : Math.max(0, Math.min(atIndex, sibs.length));\n sibs.splice(i, 0, id);\n sibs.forEach((sid, k) => tree.update(sid, { __order: k }));\n };\n\n const emit = (before: DefNode): void => {\n const def = current();\n opts.onChange?.({ def, patch: opts.address?.diff(before, def) });\n for (const l of listeners) l(def);\n };\n /** transação atômica + entrada de undo + emissão. Throw → rollback, sem undo. */\n const commit = (fn: () => void): void => {\n const before = current();\n const snap = tree.snapshot();\n tree.transaction(fn);\n undoStack.push(snap);\n redoStack.length = 0;\n emit(before);\n };\n const restoreTo = (snap: Snap, counterStack: Snap[]): void => {\n const before = current();\n counterStack.push(tree.snapshot());\n tree.restore(snap);\n emit(before);\n };\n\n return {\n getDefinition: current,\n insert(parentId, node, atIndex) {\n commit(() => {\n for (const r of fromNested(node))\n tree.add(r.id === node.id ? { ...r, parentId } : r);\n place(node.id, parentId, atIndex);\n });\n },\n remove(id) {\n commit(() => {\n tree.remove(id);\n });\n },\n move(id, toParentId, atIndex) {\n commit(() => {\n tree.move(id, toParentId);\n place(id, toParentId, atIndex);\n });\n },\n reorder(id, atIndex) {\n commit(() => place(id, parentOf(id), atIndex));\n },\n update(id, patch) {\n commit(() => {\n const safe: Record<string, unknown> = { ...patch };\n for (const k of [\"id\", \"parentId\", \"__order\", \"name\", \"children\"])\n delete safe[k];\n tree.update(id, safe);\n });\n },\n undo() {\n const snap = undoStack.pop();\n if (!snap) return false;\n restoreTo(snap, redoStack);\n return true;\n },\n redo() {\n const snap = redoStack.pop();\n if (!snap) return false;\n restoreTo(snap, undoStack);\n return true;\n },\n canUndo: () => undoStack.length > 0,\n canRedo: () => redoStack.length > 0,\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@loom-forge/editor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "loom · controller de editor sobre o ApexStore: a definição num TreeState, ops atômicas em transação, undo/redo por snapshot, patch identity-aware por commit. Bridge flat ↔ nested.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"editor",
|
|
8
|
+
"undo",
|
|
9
|
+
"redo",
|
|
10
|
+
"patch",
|
|
11
|
+
"json"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Anderson D. Rosa",
|
|
16
|
+
"url": "https://github.com/andersondrosa"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/andersondrosa/loom-forge.git",
|
|
21
|
+
"directory": "packages/editor"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/andersondrosa/loom-forge/tree/main/packages/editor#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/andersondrosa/loom-forge/issues"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist"
|
|
39
|
+
],
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@loom-forge/core": "0.1.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@statedelta-apex/tree-state": "^0.3.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@statedelta-apex/tree-state": "^0.3.0",
|
|
48
|
+
"json-myers": "^3.2.0",
|
|
49
|
+
"tsup": "^8.0.0",
|
|
50
|
+
"typescript": "^5.9.3",
|
|
51
|
+
"@loom-forge/address": "0.1.0"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsup",
|
|
55
|
+
"dev": "tsup --watch",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"test:watch": "vitest",
|
|
58
|
+
"typecheck": "tsc --noEmit",
|
|
59
|
+
"clean": "rm -rf dist"
|
|
60
|
+
}
|
|
61
|
+
}
|