flexdesk 0.5.0 → 0.6.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 +621 -1
- package/css/base.css +2835 -0
- package/css/flexdesk.css +2835 -0
- package/css/tokens.css +72 -0
- package/dist/canvas.js +390 -0
- package/dist/canvas.js.map +7 -0
- package/dist/{chunk-24MS7W5Y.js → chunk-6NG7YVBG.js} +2 -470
- package/dist/chunk-6NG7YVBG.js.map +7 -0
- package/dist/chunk-H4LT43RS.js +475 -0
- package/dist/chunk-H4LT43RS.js.map +7 -0
- package/dist/chunk-SN4LSOAR.js +532 -0
- package/dist/chunk-SN4LSOAR.js.map +7 -0
- package/dist/flexdesk.css +2835 -0
- package/dist/flow.js +10030 -0
- package/dist/flow.js.map +7 -0
- package/dist/tiles.js +9 -9
- package/dist/tokens.css +72 -0
- package/dist/widgets.js +6 -4
- package/dist/widgets.js.map +1 -1
- package/dist/wm.js +4 -2
- package/dist/wm.js.map +1 -1
- package/package.json +9 -1
- package/src/canvas/canvas_adapter.js +523 -0
- package/src/canvas/edge_router.js +392 -0
- package/src/canvas/graph/connector_router.js +189 -0
- package/src/canvas/graph/history_service.js +170 -0
- package/src/canvas/graph/node_platform.js +116 -0
- package/src/canvas/index.js +16 -0
- package/src/flow/canvas_editor/editor.js +1081 -0
- package/src/flow/canvas_editor/index.js +16 -0
- package/src/flow/canvas_editor/layout.js +118 -0
- package/src/flow/canvas_editor/strings.js +37 -0
- package/src/flow/kit/always_before.js +325 -0
- package/src/flow/kit/catalogue.js +170 -0
- package/src/flow/kit/chip_input.js +461 -0
- package/src/flow/kit/dom.js +205 -0
- package/src/flow/kit/findings.js +129 -0
- package/src/flow/kit/history.js +173 -0
- package/src/flow/kit/index.js +38 -0
- package/src/flow/kit/keys.js +127 -0
- package/src/flow/kit/logic_graph.js +210 -0
- package/src/flow/kit/references.js +199 -0
- package/src/flow/kit/settings_panel.js +357 -0
- package/src/flow/kit/settings_schema.js +175 -0
- package/src/flow/kit/step_picker.js +209 -0
- package/src/flow/kit/strings.js +102 -0
- package/src/flow/kit/value_picker.js +238 -0
- package/src/flow/kit/widgets.js +763 -0
- package/src/flow/lanes/editor.js +1469 -0
- package/src/flow/lanes/index.js +26 -0
- package/src/flow/lanes/layout.js +525 -0
- package/src/flow/lanes/pipeline.js +327 -0
- package/src/flow/lanes/preview.js +109 -0
- package/src/flow/lanes/strings.js +98 -0
- package/src/flow/outline/editor.js +1511 -0
- package/src/flow/outline/index.js +20 -0
- package/src/flow/outline/mapping.js +286 -0
- package/src/flow/outline/menu.js +154 -0
- package/src/flow/outline/operations.js +738 -0
- package/src/flow/outline/recognise.js +755 -0
- package/src/flow/outline/strings.js +135 -0
- package/dist/chunk-24MS7W5Y.js.map +0 -7
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canvas kernel — GRAPH LAYER: undo and redo over recorded edits, behind a
|
|
3
|
+
* CLOSED action list (36 §4.3).
|
|
4
|
+
*
|
|
5
|
+
* GENERIC. An entry is `{action, …payload}` — typically `before` and `after` —
|
|
6
|
+
* and putting one back is the embedder's `apply(entry, direction)`: the
|
|
7
|
+
* service knows the stack, the batch, the hydration gate and the closed list,
|
|
8
|
+
* never what an entry means. (A whole-snapshot history, for a graph small
|
|
9
|
+
* enough to copy, is `@flexdesk/flow`'s `FlowHistory`; this one records
|
|
10
|
+
* DELTAS, for a diagram too large to snapshot on every move.)
|
|
11
|
+
*
|
|
12
|
+
* ══ THE CLOSED LIST ════════════════════════════════════════════════════
|
|
13
|
+
*
|
|
14
|
+
* The only actions that exist are the ones the embedder declares, and
|
|
15
|
+
* `record()` REJECTS anything else — loudly, with `refusal(action)`'s
|
|
16
|
+
* sentence. An embedder whose Ctrl+Z must never reach something (a change
|
|
17
|
+
* already applied to a server, say) enforces that structurally: the action
|
|
18
|
+
* that would touch it cannot be recorded by accident; it has to be added on
|
|
19
|
+
* purpose, to a list the embedder's own comment says not to add it to.
|
|
20
|
+
*
|
|
21
|
+
* ══ TWO TRAPS THIS IS BUILT AGAINST ════════════════════════════════════
|
|
22
|
+
*
|
|
23
|
+
* 1. A MISSING DEPENDENCY FAILS LOUDLY, AT CONSTRUCTION. An undo engine
|
|
24
|
+
* constructed with the wrong dependency set does nothing and reports
|
|
25
|
+
* nothing — every restore path quietly returns — and the symptom, "undo
|
|
26
|
+
* does nothing", says nothing about the cause. So `actions` and `apply`
|
|
27
|
+
* are required, and `requires` lets a subclass name its own
|
|
28
|
+
* (`{platform, router, plan}`): each must be present, or the constructor
|
|
29
|
+
* throws naming it.
|
|
30
|
+
* 2. THE HYDRATION GATE. Loading a drawing must not itself be undoable —
|
|
31
|
+
* otherwise the first Ctrl+Z after opening "undoes" the opening. Nothing is
|
|
32
|
+
* recorded until `hydrated()`.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const DEFAULT_LIMIT = 200;
|
|
36
|
+
|
|
37
|
+
const missing = (name) => new Error(
|
|
38
|
+
`HistoryService needs ${name}. Constructing it without the full dependency set is how an undo `
|
|
39
|
+
+ 'engine ends up silently doing nothing.');
|
|
40
|
+
|
|
41
|
+
export class HistoryService {
|
|
42
|
+
/**
|
|
43
|
+
* @param {object} o
|
|
44
|
+
* @param {readonly string[]} o.actions the only actions `record` accepts
|
|
45
|
+
* @param {(entry: object, direction: 'undo'|'redo') => void} o.apply puts one entry back (or forward)
|
|
46
|
+
* @param {(state: {canUndo, canRedo, depth}) => void} [o.onState]
|
|
47
|
+
* @param {number} [o.limit] entries kept; the oldest goes first
|
|
48
|
+
* @param {object} [o.requires] `{name: value}` a subclass cannot work without; each is checked
|
|
49
|
+
* @param {(action: string) => string} [o.refusal] the sentence a refused `record` throws
|
|
50
|
+
*/
|
|
51
|
+
constructor({ actions, apply, onState, limit = DEFAULT_LIMIT, requires = null, refusal = null } = {}) {
|
|
52
|
+
// Loudly, at construction — see the header.
|
|
53
|
+
for (const [name, value] of Object.entries(requires || {})) {
|
|
54
|
+
if (!value) throw missing(name);
|
|
55
|
+
}
|
|
56
|
+
if (!Array.isArray(actions) || !actions.length) throw missing('actions (its closed list)');
|
|
57
|
+
if (typeof apply !== 'function') throw missing('apply(entry, direction)');
|
|
58
|
+
this.actions = Object.freeze([...actions]);
|
|
59
|
+
this._applyEntry = apply;
|
|
60
|
+
this.onState = onState;
|
|
61
|
+
this.limit = limit;
|
|
62
|
+
this.refusal = typeof refusal === 'function' ? refusal : null;
|
|
63
|
+
this.undoStack = [];
|
|
64
|
+
this.redoStack = [];
|
|
65
|
+
// The hydration gate. Recording starts only once a drawing is loaded.
|
|
66
|
+
this.recording = false;
|
|
67
|
+
this.batch = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The drawing has been loaded: the stacks start empty, and recording starts. */
|
|
71
|
+
hydrated() {
|
|
72
|
+
this.undoStack = [];
|
|
73
|
+
this.redoStack = [];
|
|
74
|
+
this.recording = true;
|
|
75
|
+
this._publish();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Group what is recorded until `endBatch` into one entry. */
|
|
79
|
+
startBatch(label) {
|
|
80
|
+
if (!this.recording) return;
|
|
81
|
+
this.batch = { action: 'batch', label, entries: [] };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
endBatch() {
|
|
85
|
+
if (!this.batch) return;
|
|
86
|
+
const batch = this.batch;
|
|
87
|
+
this.batch = null;
|
|
88
|
+
if (batch.entries.length) this._push(batch);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
record(action, payload) {
|
|
92
|
+
if (!this.recording) return;
|
|
93
|
+
if (!this.actions.includes(action)) {
|
|
94
|
+
throw new Error(this.refusal
|
|
95
|
+
? this.refusal(action)
|
|
96
|
+
: `${action} is not an undoable action here; the ones that are: ${this.actions.join(', ')}.`);
|
|
97
|
+
}
|
|
98
|
+
const entry = { action, ...payload };
|
|
99
|
+
if (this.batch) { this.batch.entries.push(entry); return; }
|
|
100
|
+
this._push(entry);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
undo() {
|
|
104
|
+
const entry = this.undoStack.pop();
|
|
105
|
+
if (!entry) return false;
|
|
106
|
+
this._apply(entry, 'undo');
|
|
107
|
+
this.redoStack.push(entry);
|
|
108
|
+
this._publish();
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
redo() {
|
|
113
|
+
const entry = this.redoStack.pop();
|
|
114
|
+
if (!entry) return false;
|
|
115
|
+
this._apply(entry, 'redo');
|
|
116
|
+
this.undoStack.push(entry);
|
|
117
|
+
this._publish();
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
reset() {
|
|
122
|
+
this.undoStack = [];
|
|
123
|
+
this.redoStack = [];
|
|
124
|
+
this._publish();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Take over the undo and redo stacks of a torn-down view whose state this
|
|
129
|
+
* one has just taken on, entry for entry and in the same order. Nothing new
|
|
130
|
+
* becomes undoable: every entry was recorded through a `record` whose action
|
|
131
|
+
* list is closed, so a stack can only carry what that list allows. Call it
|
|
132
|
+
* after `hydrated()`.
|
|
133
|
+
*/
|
|
134
|
+
adopt({ undo = [], redo = [] } = {}) {
|
|
135
|
+
if (!this.recording) return;
|
|
136
|
+
this.undoStack = [...undo].slice(-this.limit);
|
|
137
|
+
this.redoStack = [...redo];
|
|
138
|
+
this._publish();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get canUndo() { return this.undoStack.length > 0; }
|
|
142
|
+
get canRedo() { return this.redoStack.length > 0; }
|
|
143
|
+
|
|
144
|
+
_push(entry) {
|
|
145
|
+
this.undoStack.push(entry);
|
|
146
|
+
// A new action invalidates the redo branch. Keeping it would let a user
|
|
147
|
+
// redo their way into a state that never existed.
|
|
148
|
+
this.redoStack = [];
|
|
149
|
+
if (this.undoStack.length > this.limit) this.undoStack.shift();
|
|
150
|
+
this._publish();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
_publish() {
|
|
154
|
+
this.onState?.({
|
|
155
|
+
canUndo: this.canUndo,
|
|
156
|
+
canRedo: this.canRedo,
|
|
157
|
+
depth: this.undoStack.length,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_apply(entry, direction) {
|
|
162
|
+
if (entry.action === 'batch') {
|
|
163
|
+
const entries = direction === 'undo'
|
|
164
|
+
? [...entry.entries].reverse() : entry.entries;
|
|
165
|
+
for (const child of entries) this._apply(child, direction);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this._applyEntry(entry, direction);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canvas kernel — GRAPH LAYER: the node and port model (36 §4.3).
|
|
3
|
+
*
|
|
4
|
+
* GENERIC. It holds nodes — each `{id, position, size, collapsed, ports}` plus
|
|
5
|
+
* whatever else the embedder hangs on it — and answers which ports a node
|
|
6
|
+
* offers. What a node IS (a table, a step, a form field) and which ports a
|
|
7
|
+
* collapsed one keeps are the embedder's: a subclass builds the nodes and
|
|
8
|
+
* calls `hydrate`, and the `collapsedPorts(node)` option decides what a folded
|
|
9
|
+
* node shows.
|
|
10
|
+
*
|
|
11
|
+
* The genuinely reusable idea is the shape of a port descriptor — `{id, role,
|
|
12
|
+
* direction, type, accepts, provides, allowMultiple, maxConnections, disabled,
|
|
13
|
+
* disabledReason, meta}` — which is what lets one legality guard
|
|
14
|
+
* (`ConnectorRouter`) serve a schema diagram, a flow graph and a form
|
|
15
|
+
* designer. Nothing here requires any of those keys but `id`; the router reads
|
|
16
|
+
* the ones it knows.
|
|
17
|
+
*
|
|
18
|
+
* DOM-free on purpose. It holds the graph; the embedder renders it. That split
|
|
19
|
+
* is what makes the legality guard and the history service testable under
|
|
20
|
+
* plain node.
|
|
21
|
+
*
|
|
22
|
+
* Events (`on(name, handler)` → an unsubscribe):
|
|
23
|
+
* `hydrated` `{count}` after `hydrate`
|
|
24
|
+
* `moved` `{id, before, after}` after a `setPosition` that is not `silent`
|
|
25
|
+
* `collapsed` `{id, before, after}` after a `setCollapsed` that is not `silent`
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export class NodePlatform {
|
|
29
|
+
/**
|
|
30
|
+
* @param {object} [options]
|
|
31
|
+
* @param {(node: object) => object[]} [options.collapsedPorts] the ports a COLLAPSED node
|
|
32
|
+
* still offers; by default none
|
|
33
|
+
*/
|
|
34
|
+
constructor({ collapsedPorts = null } = {}) {
|
|
35
|
+
/** @type {Map<string, object>} node id -> snapshot */
|
|
36
|
+
this.nodes = new Map();
|
|
37
|
+
this.listeners = new Map();
|
|
38
|
+
this.collapsedPorts = typeof collapsedPorts === 'function' ? collapsedPorts : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
on(event, handler) {
|
|
42
|
+
if (!this.listeners.has(event)) this.listeners.set(event, new Set());
|
|
43
|
+
this.listeners.get(event).add(handler);
|
|
44
|
+
return () => this.listeners.get(event).delete(handler);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
emit(event, payload) {
|
|
48
|
+
for (const handler of this.listeners.get(event) || []) handler(payload);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Replace the whole graph. Hydration, not an edit — see `HistoryService`.
|
|
53
|
+
*
|
|
54
|
+
* Each node is `{id, position?, size?, collapsed?, ports?, …}`: the position
|
|
55
|
+
* is copied (a missing one is (0, 0)), `size` is `{w, h}` or null, `ports`
|
|
56
|
+
* an array (none when absent), and every other key is kept as it is.
|
|
57
|
+
*/
|
|
58
|
+
hydrate(nodes) {
|
|
59
|
+
this.nodes.clear();
|
|
60
|
+
for (const raw of Array.isArray(nodes) ? nodes : []) {
|
|
61
|
+
if (!raw || raw.id === undefined || raw.id === null) continue;
|
|
62
|
+
const id = raw.id;
|
|
63
|
+
this.nodes.set(id, {
|
|
64
|
+
...raw,
|
|
65
|
+
id,
|
|
66
|
+
// `size` and `collapsed` are in the snapshot, not derived: at
|
|
67
|
+
// hundreds of nodes a node showing forty ports is unreadable, and
|
|
68
|
+
// a collapsed node's ports fold away.
|
|
69
|
+
position: raw.position ? { x: raw.position.x, y: raw.position.y } : { x: 0, y: 0 },
|
|
70
|
+
size: raw.size ? { w: raw.size.w, h: raw.size.h } : null,
|
|
71
|
+
collapsed: Boolean(raw.collapsed),
|
|
72
|
+
ports: Array.isArray(raw.ports) ? raw.ports : [],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
this.emit('hydrated', { count: this.nodes.size });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get(id) { return this.nodes.get(id); }
|
|
79
|
+
all() { return [...this.nodes.values()]; }
|
|
80
|
+
|
|
81
|
+
port(nodeId, portId) {
|
|
82
|
+
return this.nodes.get(nodeId)?.ports.find((p) => p.id === portId) || null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setPosition(id, position, { silent = false } = {}) {
|
|
86
|
+
const node = this.nodes.get(id);
|
|
87
|
+
if (!node) return null;
|
|
88
|
+
const before = { ...node.position };
|
|
89
|
+
node.position = { ...position };
|
|
90
|
+
if (!silent) this.emit('moved', { id, before, after: { ...position } });
|
|
91
|
+
return before;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setCollapsed(id, collapsed, { silent = false } = {}) {
|
|
95
|
+
const node = this.nodes.get(id);
|
|
96
|
+
if (!node) return null;
|
|
97
|
+
const before = node.collapsed;
|
|
98
|
+
node.collapsed = Boolean(collapsed);
|
|
99
|
+
if (!silent) this.emit('collapsed', { id, before, after: node.collapsed });
|
|
100
|
+
return before;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Ports that are visible right now.
|
|
105
|
+
*
|
|
106
|
+
* A collapsed node offers only what `collapsedPorts` says it keeps (by
|
|
107
|
+
* default nothing). Answering from one place is what stops a renderer and
|
|
108
|
+
* the legality guard disagreeing about what can be dropped on.
|
|
109
|
+
*/
|
|
110
|
+
visiblePorts(id) {
|
|
111
|
+
const node = this.nodes.get(id);
|
|
112
|
+
if (!node) return [];
|
|
113
|
+
if (node.collapsed) return this.collapsedPorts ? this.collapsedPorts(node) : [];
|
|
114
|
+
return node.ports;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `@flexdesk/canvas` exports — the canvas kernel (36 §4).
|
|
3
|
+
*
|
|
4
|
+
* OWNED BY the canvas agent (B, 36 §2): the base layer (`CanvasAdapter`) and
|
|
5
|
+
* the edge router, moved with their behaviour intact, and the GENERIC graph
|
|
6
|
+
* layer (`NodePlatform`, `ConnectorRouter`, `HistoryService`) that a
|
|
7
|
+
* consumer's own rules subclass. Nothing here knows what a node is.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { CanvasAdapter } from './canvas_adapter.js';
|
|
11
|
+
export {
|
|
12
|
+
OBSTACLE_ROUTING_NODE_LIMIT, routeEdge, footAnchor, crowsFoot, cardinality, selfLoop, returnEdge, arrange,
|
|
13
|
+
} from './edge_router.js';
|
|
14
|
+
export { NodePlatform } from './graph/node_platform.js';
|
|
15
|
+
export { ConnectorRouter, CONNECTOR_STRINGS } from './graph/connector_router.js';
|
|
16
|
+
export { HistoryService } from './graph/history_service.js';
|