@aaqu/fromcubes-react 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +405 -2
  3. package/THIRD-PARTY-NOTICES.md +164 -0
  4. package/examples/D3 Poland Map.json +90 -0
  5. package/examples/Live Chart.json +101 -0
  6. package/examples/PixiJS Sprites.json +114 -0
  7. package/examples/Sensor Portal.json +93 -0
  8. package/examples/Shared Components.json +68 -0
  9. package/examples/Three.js Scene.json +118 -0
  10. package/examples/Utility Hooks.json +94 -0
  11. package/examples/WebGPU Shader.json +110 -0
  12. package/nodes/editor/ark-pixel.LICENSE.txt +96 -0
  13. package/nodes/editor/ark-pixel.woff2 +0 -0
  14. package/nodes/editor/file-tree.js +403 -0
  15. package/nodes/editor/format.js +182 -0
  16. package/nodes/editor/icons.svg +46 -0
  17. package/nodes/editor/mark.svg +5 -0
  18. package/nodes/editor/provider-settings.js +192 -0
  19. package/nodes/editor/workspace.css +653 -0
  20. package/nodes/editor/workspace.js +1565 -0
  21. package/nodes/icons/fromcubes-component.svg +27 -0
  22. package/nodes/icons/fromcubes-react.svg +25 -0
  23. package/nodes/icons/fromcubes-utility.svg +21 -0
  24. package/nodes/lib/admin-api.js +238 -0
  25. package/nodes/lib/admin-security.js +161 -0
  26. package/nodes/lib/api.js +29 -0
  27. package/nodes/lib/helpers.js +990 -0
  28. package/nodes/lib/hooks.js +145 -0
  29. package/nodes/lib/page-builder.js +499 -0
  30. package/nodes/lib/portal-page-route.js +131 -0
  31. package/nodes/lib/providers.js +536 -0
  32. package/nodes/lib/registry-nodes.js +331 -0
  33. package/nodes/lib/router.js +103 -0
  34. package/nodes/lib/ws-heartbeat.js +58 -0
  35. package/nodes/portal-react.html +2539 -0
  36. package/nodes/portal-react.js +1652 -0
  37. package/package.json +76 -4
@@ -0,0 +1,403 @@
1
+ /**
2
+ * Portal Code Workspace — pure file-tree layer.
3
+ *
4
+ * Maps Node-RED editor node objects (`portal-react`, `fc-portal-component`,
5
+ * `fc-portal-utility`) onto a virtual file listing, then folds that flat
6
+ * listing into a nested tree. No DOM, no jQuery, no `RED` access — the whole
7
+ * module is a pair of pure functions so it can be unit-tested under vitest and
8
+ * still be loaded as a plain browser script by the Node-RED editor.
9
+ *
10
+ * Loaded in the browser via `<script src="fromcubes-react/editor/file-tree.js">`
11
+ * (exposes `window.__fcFileTree`) and in tests via `require()`.
12
+ */
13
+ (function (root, factory) {
14
+ const api = factory();
15
+ if (typeof module === "object" && module.exports) {
16
+ module.exports = api;
17
+ } else {
18
+ root.__fcFileTree = api;
19
+ }
20
+ })(typeof self !== "undefined" ? self : this, function () {
21
+ "use strict";
22
+
23
+ /** Matches lib/registry-nodes.js — oversized code is not scanned. */
24
+ const MAX_UTIL_CODE_BYTES = 1000000;
25
+
26
+ const GROUP_PORTALS = "Portals";
27
+ const GROUP_COMPONENTS = "Components";
28
+ const GROUP_UTILITIES = "Utilities";
29
+
30
+ /** Fixed group ordering — mirrors bundle order (utilities → components → portal). */
31
+ const GROUP_ORDER = [GROUP_PORTALS, GROUP_COMPONENTS, GROUP_UTILITIES];
32
+
33
+ /**
34
+ * Per-node-type description of which config fields are editable "files".
35
+ *
36
+ * `file` is a fixed name; without it the file is named after the node's
37
+ * registered name plus `ext`. `optional: true` fields are hidden while empty
38
+ * unless `showHead` is set. `under: true` nests the file beneath the node's
39
+ * primary file instead of beside it.
40
+ */
41
+ const FIELD_MAP = {
42
+ "portal-react": {
43
+ group: GROUP_PORTALS,
44
+ nameField: "subPath",
45
+ files: [
46
+ // The portal reads as one file named after its sub-path; its custom
47
+ // <head> hangs off that file rather than forcing a folder around a
48
+ // pair, so a portal costs one row until you ask for the second.
49
+ { field: "componentCode", ext: "jsx", lang: "javascript" },
50
+ {
51
+ field: "customHead",
52
+ file: "head.html",
53
+ lang: "html",
54
+ optional: true,
55
+ under: true,
56
+ },
57
+ ],
58
+ },
59
+ "fc-portal-component": {
60
+ group: GROUP_COMPONENTS,
61
+ nameField: "compName",
62
+ files: [{ field: "compCode", ext: "jsx", lang: "javascript" }],
63
+ },
64
+ "fc-portal-utility": {
65
+ group: GROUP_UTILITIES,
66
+ nameField: "utilName",
67
+ files: [{ field: "utilCode", ext: "js", lang: "javascript" }],
68
+ },
69
+ };
70
+
71
+ /**
72
+ * True when the node type owns portal-react source code.
73
+ * @param {string} type
74
+ * @returns {boolean}
75
+ */
76
+ function isPortalNodeType(type) {
77
+ return Object.prototype.hasOwnProperty.call(FIELD_MAP, type);
78
+ }
79
+
80
+ /**
81
+ * Display name for a node: its registered name (subPath / compName /
82
+ * utilName), falling back to the canvas label and finally the node id, so a
83
+ * half-configured node still shows up in the tree instead of vanishing.
84
+ *
85
+ * @param {Object} node
86
+ * @param {string} nameField
87
+ * @returns {string}
88
+ */
89
+ function displayName(node, nameField) {
90
+ const registered = (node[nameField] || "").trim();
91
+ if (registered) return registered;
92
+ const label = (node.name || "").trim();
93
+ if (label) return label;
94
+ return node.id;
95
+ }
96
+
97
+ /**
98
+ * Flatten editor nodes into virtual files.
99
+ *
100
+ * Two layouts, both keeping the flow level only where it disambiguates:
101
+ * - `groupBy: "kind"` → `Utilities/<flow>/<name>.js`. Kind first, so all
102
+ * utilities sit in one folder however many flow tabs there are.
103
+ * - `groupBy: "flow"` → `<flow>/<name>.js`, mirroring the canvas: one flat
104
+ * alphabetical list per tab. No kind level — inside a flow the extension
105
+ * already says what a file is, and a folder per kind holding one file is
106
+ * noise. Portals keep their own folder, since a portal owns two files.
107
+ *
108
+ * @param {Array<Object>} nodes Plain node objects from `RED.nodes.eachNode`.
109
+ * @param {Object} [opts]
110
+ * @param {(flowId: string) => string} [opts.flowLabel] Resolves a flow tab id
111
+ * to its label. Only consulted when a level actually spans several flows.
112
+ * @param {boolean} [opts.showHead] Include empty `head.html` files.
113
+ * @param {"kind"|"flow"} [opts.groupBy] Layout, default `"kind"`.
114
+ * @param {(flowId: string) => boolean} [opts.flowDisabled] Is that flow tab
115
+ * disabled? Files inherit it, since a disabled tab takes its nodes with it.
116
+ * @returns {Array<Object>} File descriptors, sorted by group then path.
117
+ */
118
+ function collectPortalFiles(nodes, opts) {
119
+ const options = opts || {};
120
+ const byFlow = options.groupBy === "flow";
121
+ const list = [];
122
+ const allFlows = new Set();
123
+ const flowsPerGroup = {};
124
+
125
+ (nodes || []).forEach(function (node) {
126
+ if (!node || !isPortalNodeType(node.type)) return;
127
+ const flowId = node.z || "";
128
+ allFlows.add(flowId);
129
+ const group = FIELD_MAP[node.type].group;
130
+ (flowsPerGroup[group] = flowsPerGroup[group] || new Set()).add(flowId);
131
+ });
132
+
133
+ const labelOf =
134
+ options.flowLabel ||
135
+ function (id) {
136
+ return id || "Flow";
137
+ };
138
+ const flowOff = options.flowDisabled || function () {
139
+ return false;
140
+ };
141
+
142
+ (nodes || []).forEach(function (node) {
143
+ if (!node || !isPortalNodeType(node.type)) return;
144
+ const spec = FIELD_MAP[node.type];
145
+ const name = displayName(node, spec.nameField);
146
+ // Only pay for a flow level when it separates something: the whole tree
147
+ // for flow-first, this one group for kind-first.
148
+ const scope = byFlow ? allFlows : flowsPerGroup[spec.group];
149
+ const flowLevel = scope.size > 1 ? labelOf(node.z || "") || "Flow" : null;
150
+ // A node can be disabled on its own, or ride a disabled flow tab down
151
+ // with it. Both mean "this code is not running"; the tree says which.
152
+ const flowIsOff = flowOff(node.z || "");
153
+ const disabled = node.d ? "node" : flowIsOff ? "flow" : "";
154
+
155
+ const primaryName = name + "." + (spec.files[0].ext || "jsx");
156
+
157
+ spec.files.forEach(function (f) {
158
+ const value = node[f.field];
159
+ if (f.optional && !options.showHead && !(value || "").trim()) return;
160
+
161
+ const fileName = f.file || name + "." + f.ext;
162
+ const segments = [];
163
+ if (byFlow) {
164
+ if (flowLevel) segments.push(flowLevel);
165
+ } else {
166
+ segments.push(spec.group);
167
+ if (flowLevel) segments.push(flowLevel);
168
+ }
169
+ if (f.under) segments.push(primaryName);
170
+ segments.push(fileName);
171
+
172
+ list.push({
173
+ key: node.id + ":" + f.field,
174
+ nodeId: node.id,
175
+ type: node.type,
176
+ field: f.field,
177
+ lang: f.lang,
178
+ name: fileName,
179
+ registryName: node.type === "portal-react" ? null : name,
180
+ group: spec.group,
181
+ groupIndex: GROUP_ORDER.indexOf(spec.group),
182
+ flowId: node.z || "",
183
+ flowDisabled: flowIsOff,
184
+ disabled: disabled,
185
+ // Which segment is the flow tab, so the tree can badge that folder
186
+ // as a flow rather than a plain directory.
187
+ flowSegment: flowLevel ? (byFlow ? 0 : 1) : -1,
188
+ segments: segments,
189
+ path: segments.join("/"),
190
+ });
191
+ });
192
+ });
193
+
194
+ list.sort(function (a, b) {
195
+ // Kind leads — portals, then components, then utilities: how the bundle
196
+ // is assembled, and how you read a portal (the page first, then what it
197
+ // is built from). Which level the kind sits at differs per layout, so the
198
+ // comparison follows the segment order the layout actually produced.
199
+ const dirOf = (f) => f.segments.slice(0, -1).join("/");
200
+ const byName = () => a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
201
+ const byDir = () => dirOf(a).localeCompare(dirOf(b), undefined, { sensitivity: "base" });
202
+ if (byFlow) {
203
+ if (dirOf(a) !== dirOf(b)) return byDir();
204
+ if (a.groupIndex !== b.groupIndex) return a.groupIndex - b.groupIndex;
205
+ return byName();
206
+ }
207
+ if (a.groupIndex !== b.groupIndex) return a.groupIndex - b.groupIndex;
208
+ if (dirOf(a) !== dirOf(b)) return byDir();
209
+ return byName();
210
+ });
211
+ return list;
212
+ }
213
+
214
+ /**
215
+ * Fold a flat file listing into a nested directory tree.
216
+ *
217
+ * @param {Array<Object>} files Output of {@link collectPortalFiles}.
218
+ * @returns {{name: string, path: string, type: string, children: Array}}
219
+ * Synthetic root node; directories sort before files, both alphabetically.
220
+ */
221
+ function buildTree(files) {
222
+ const root = { name: "", path: "", type: "dir", children: [], index: {} };
223
+
224
+ (files || []).forEach(function (file) {
225
+ let cursor = root;
226
+ const dirs = file.segments.slice(0, -1);
227
+ dirs.forEach(function (seg, i) {
228
+ let child = cursor.index[seg];
229
+ if (!child) {
230
+ const isFlowDir = i === file.flowSegment;
231
+ child = {
232
+ name: seg,
233
+ // Group folders sort by kind like their files do; flow folders
234
+ // have no kind of their own and fall back to their name.
235
+ order: isFlowDir ? null : GROUP_ORDER.indexOf(seg),
236
+ path: file.segments.slice(0, i + 1).join("/"),
237
+ type: "dir",
238
+ kind: isFlowDir ? "flow" : "group",
239
+ flowId: isFlowDir ? file.flowId : null,
240
+ disabled: isFlowDir && file.flowDisabled ? "flow" : "",
241
+ children: [],
242
+ index: {},
243
+ };
244
+ cursor.index[seg] = child;
245
+ cursor.children.push(child);
246
+ }
247
+ cursor = child;
248
+ });
249
+ // A path can be both a file and a parent — a portal's own `.jsx` holds
250
+ // its `head.html`. Whichever arrives second merges into the first.
251
+ const existing = cursor.index[file.name];
252
+ if (existing) {
253
+ existing.file = file;
254
+ return;
255
+ }
256
+ const leaf = {
257
+ name: file.name,
258
+ path: file.path,
259
+ order: file.groupIndex,
260
+ type: "file",
261
+ file: file,
262
+ children: [],
263
+ index: {},
264
+ };
265
+ cursor.index[file.name] = leaf;
266
+ cursor.children.push(leaf);
267
+ });
268
+
269
+ sortTree(root);
270
+ return root;
271
+ }
272
+
273
+ /**
274
+ * In-place recursive sort: directories first, then case-insensitive by name.
275
+ * Also drops the `index` lookup maps used while building.
276
+ *
277
+ * @param {Object} dir
278
+ * @returns {void}
279
+ */
280
+ function sortTree(dir) {
281
+ delete dir.index;
282
+ if (!dir.children) return;
283
+ dir.children.sort(function (a, b) {
284
+ // Kind order first where both siblings have one — that is what puts
285
+ // portals above components above utilities inside a flow.
286
+ const ka = typeof a.order === "number" && a.order >= 0 ? a.order : null;
287
+ const kb = typeof b.order === "number" && b.order >= 0 ? b.order : null;
288
+ if (ka !== null && kb !== null && ka !== kb) return ka - kb;
289
+ if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
290
+ return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
291
+ });
292
+ dir.children.forEach(sortTree);
293
+ }
294
+
295
+ /**
296
+ * A file that also holds files — a portal's `.jsx` owning its `head.html`.
297
+ * @param {Object} node A tree node from {@link buildTree}.
298
+ * @returns {boolean}
299
+ */
300
+ function isParentFile(node) {
301
+ return !!(node && node.file && node.children && node.children.length);
302
+ }
303
+
304
+ /**
305
+ * Does this node start closed?
306
+ *
307
+ * Folders open by default, because that is what a tree is for. Two things
308
+ * earn the opposite: a file holding files (you asked for the portal, not its
309
+ * `head.html`) and a flow Node-RED is not running (it is context, not work).
310
+ *
311
+ * @param {Object} node A tree node from {@link buildTree}.
312
+ * @returns {boolean}
313
+ */
314
+ function startsCollapsed(node) {
315
+ if (!node) return false;
316
+ return isParentFile(node) || node.disabled === "flow";
317
+ }
318
+
319
+ /**
320
+ * Which kind of thing a file is, for icon and colour purposes.
321
+ *
322
+ * Decided by node type and field, never by extension: a portal and a
323
+ * component are both `.jsx`, and they are exactly the two the tree most
324
+ * needs to tell apart.
325
+ *
326
+ * @param {Object} file A descriptor from {@link collectPortalFiles}.
327
+ * @returns {"portal"|"component"|"utility"|"head"|"file"}
328
+ */
329
+ function iconKindFor(file) {
330
+ if (!file) return "file";
331
+ if (file.type === "portal-react") {
332
+ return file.field === "customHead" ? "head" : "portal";
333
+ }
334
+ if (file.type === "fc-portal-component") return "component";
335
+ if (file.type === "fc-portal-utility") return "utility";
336
+ return "file";
337
+ }
338
+
339
+ /**
340
+ * Top-level `function`/`const`/`let`/`var`/`class` names in utility code.
341
+ *
342
+ * Client mirror of `extractUtilitySymbols` in lib/registry-nodes.js — same
343
+ * anchored regex, so what the editor treats as a utility's exported name
344
+ * matches what the bundler injects. Kept here (pure) so it is testable, and
345
+ * kept as a scan so it still answers for half-written code. Multi-declarator
346
+ * statements are not followed; the caller falls back to a plain reference
347
+ * search, which costs at worst a jump to the wrong line of the right file.
348
+ *
349
+ * @param {string} code
350
+ * @returns {Set<string>}
351
+ */
352
+ function topLevelSymbols(code) {
353
+ const names = new Set();
354
+ if (!code || code.length > MAX_UTIL_CODE_BYTES) return names;
355
+ const re = /^(?:export\s+)?(?:async\s+)?(?:function\s*\*?|const|let|var|class)\s+([A-Za-z_$][\w$]*)/gm;
356
+ let m;
357
+ while ((m = re.exec(code))) names.add(m[1]);
358
+ return names;
359
+ }
360
+
361
+ /**
362
+ * Does this utility code declare `name` at its top level?
363
+ * @param {string} code
364
+ * @param {string} name
365
+ * @returns {boolean}
366
+ */
367
+ function declaresSymbol(code, name) {
368
+ return topLevelSymbols(code).has(name);
369
+ }
370
+
371
+ /**
372
+ * Case-insensitive substring filter over full paths.
373
+ *
374
+ * @param {Array<Object>} files
375
+ * @param {string} query
376
+ * @returns {Array<Object>}
377
+ */
378
+ function filterFiles(files, query) {
379
+ const q = (query || "").trim().toLowerCase();
380
+ if (!q) return files || [];
381
+ return (files || []).filter(function (f) {
382
+ return f.path.toLowerCase().indexOf(q) !== -1;
383
+ });
384
+ }
385
+
386
+ return {
387
+ GROUP_PORTALS: GROUP_PORTALS,
388
+ GROUP_COMPONENTS: GROUP_COMPONENTS,
389
+ GROUP_UTILITIES: GROUP_UTILITIES,
390
+ GROUP_ORDER: GROUP_ORDER,
391
+ FIELD_MAP: FIELD_MAP,
392
+ isPortalNodeType: isPortalNodeType,
393
+ displayName: displayName,
394
+ isParentFile: isParentFile,
395
+ startsCollapsed: startsCollapsed,
396
+ iconKindFor: iconKindFor,
397
+ topLevelSymbols: topLevelSymbols,
398
+ declaresSymbol: declaresSymbol,
399
+ collectPortalFiles: collectPortalFiles,
400
+ buildTree: buildTree,
401
+ filterFiles: filterFiles,
402
+ };
403
+ });
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Formatting for the fromcubes editors — Prettier, loaded on first use.
3
+ *
4
+ * Registers Monaco formatting providers for `javascript` (portal JSX,
5
+ * components, utilities) and `html` (a portal's custom head). Providers are
6
+ * global per language, so this serves the React Editor overlay and the four
7
+ * Monaco instances inside node dialogs alike.
8
+ *
9
+ * Nothing is fetched until someone actually formats: Prettier's browser builds
10
+ * are ~772 KB, and most sessions never ask for them.
11
+ */
12
+ (function () {
13
+ "use strict";
14
+
15
+ const PREFIX = "[FC-Format]";
16
+ const BASE = "fromcubes-react/prettier/";
17
+ // UMD builds: each sets window.prettier / window.prettierPlugins.
18
+ const SCRIPTS = [
19
+ "standalone.js",
20
+ "plugins/babel.js",
21
+ "plugins/estree.js",
22
+ "plugins/html.js",
23
+ ];
24
+
25
+ /** @type {Promise<boolean>|null} in-flight or settled load */
26
+ let loading = null;
27
+ let registered = false;
28
+
29
+ /**
30
+ * Load one script tag.
31
+ * @param {string} src
32
+ * @returns {Promise<void>}
33
+ */
34
+ function loadScript(src) {
35
+ return new Promise(function (resolve, reject) {
36
+ const s = document.createElement("script");
37
+ s.src = src;
38
+ s.onload = function () {
39
+ resolve();
40
+ };
41
+ s.onerror = function () {
42
+ reject(new Error("failed to load " + src));
43
+ };
44
+ document.head.appendChild(s);
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Fetch Prettier once. Resolves false when it could not be loaded, so the
50
+ * caller can fall back rather than fail.
51
+ * @returns {Promise<boolean>}
52
+ */
53
+ function ensurePrettier() {
54
+ if (loading) return loading;
55
+ loading = SCRIPTS.reduce(function (chain, name) {
56
+ return chain.then(function () {
57
+ return loadScript(BASE + name);
58
+ });
59
+ }, Promise.resolve())
60
+ .then(function () {
61
+ if (!window.prettier || !window.prettierPlugins) {
62
+ throw new Error("prettier globals missing after load");
63
+ }
64
+ // Monaco already ships a TypeScript formatter on `javascript`. With two
65
+ // providers registered it picks one, and a silent loss would read as
66
+ // "formatting is broken" — so TS steps aside, but only once Prettier is
67
+ // definitely here. Until then its formatter stays as the fallback.
68
+ try {
69
+ const ts = monaco.languages.typescript.javascriptDefaults;
70
+ ts.setModeConfiguration(
71
+ Object.assign({}, ts.modeConfiguration, {
72
+ documentFormattingEdits: false,
73
+ documentRangeFormattingEdits: false,
74
+ }),
75
+ );
76
+ } catch (err) {
77
+ console.warn(PREFIX, "could not stand down the TS formatter", err);
78
+ }
79
+ console.log(PREFIX, "prettier", window.prettier.version, "ready");
80
+ return true;
81
+ })
82
+ .catch(function (err) {
83
+ console.warn(PREFIX, "prettier unavailable — Monaco's own formatter stays in charge", err);
84
+ return false;
85
+ });
86
+ return loading;
87
+ }
88
+
89
+ /**
90
+ * Run Prettier over a whole model.
91
+ *
92
+ * @param {Object} model Monaco text model
93
+ * @param {string} parser Prettier parser name
94
+ * @returns {Promise<Array>} Monaco text edits — empty when nothing can be done
95
+ */
96
+ function formatModel(model, parser) {
97
+ return ensurePrettier().then(function (ok) {
98
+ if (!ok) return [];
99
+ const text = model.getValue();
100
+ return window.prettier
101
+ .format(text, {
102
+ parser: parser,
103
+ plugins: [
104
+ window.prettierPlugins.babel,
105
+ window.prettierPlugins.estree,
106
+ window.prettierPlugins.html,
107
+ ],
108
+ // Follow the editor rather than impose a second opinion on indent.
109
+ tabWidth: model.getOptions().tabSize || 2,
110
+ })
111
+ .then(function (out) {
112
+ if (out === text) return [];
113
+ return [{ range: model.getFullModelRange(), text: out }];
114
+ })
115
+ .catch(function (err) {
116
+ // Prettier throws on code that does not parse — which is normal for
117
+ // a file still being written. Say so instead of doing nothing.
118
+ reportParseError(err);
119
+ return [];
120
+ });
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Surface a parse failure where the user is looking.
126
+ * @param {Error} err
127
+ * @returns {void}
128
+ */
129
+ function reportParseError(err) {
130
+ const loc = err && err.loc && err.loc.start;
131
+ const first = String((err && err.message) || err).split("\n")[0];
132
+ // Prettier usually puts "(line:column)" in the message already; only add
133
+ // one when it did not, so the notification says it once.
134
+ const where = loc && !/\(\d+:\d+\)/.test(first)
135
+ ? " (line " + loc.line + ", column " + loc.column + ")"
136
+ : "";
137
+ console.warn(PREFIX, "not formatted:", err);
138
+ if (typeof RED !== "undefined" && RED.notify) {
139
+ RED.notify("Cannot format — " + first + where, { type: "warning", timeout: 6000 });
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Register the providers once Monaco exists.
145
+ * @returns {void}
146
+ */
147
+ function register() {
148
+ if (registered) return;
149
+ if (typeof monaco === "undefined" || !monaco.languages) {
150
+ setTimeout(register, 300);
151
+ return;
152
+ }
153
+ registered = true;
154
+
155
+ [
156
+ ["javascript", "babel"],
157
+ ["html", "html"],
158
+ ].forEach(function (pair) {
159
+ const language = pair[0];
160
+ const parser = pair[1];
161
+ monaco.languages.registerDocumentFormattingEditProvider(language, {
162
+ displayName: "Prettier (fromcubes)",
163
+ provideDocumentFormattingEdits: function (model) {
164
+ return formatModel(model, parser);
165
+ },
166
+ });
167
+ // Format Selection falls out of the same call: Prettier has no partial
168
+ // mode, so the whole document is formatted and Monaco applies the edit.
169
+ monaco.languages.registerDocumentRangeFormattingEditProvider(language, {
170
+ displayName: "Prettier (fromcubes)",
171
+ provideDocumentRangeFormattingEdits: function (model) {
172
+ return formatModel(model, parser);
173
+ },
174
+ });
175
+ });
176
+
177
+ window.__fcFormat = { ensure: ensurePrettier, format: formatModel };
178
+ console.log(PREFIX, "formatting providers registered");
179
+ }
180
+
181
+ register();
182
+ })();
@@ -0,0 +1,46 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" style="display:none" aria-hidden="true">
2
+ <!-- Generated by tools/build-sprite.mjs — do not edit.
3
+ Icons from Lucide (https://lucide.dev), ISC — see THIRD-PARTY-NOTICES.md.
4
+ Stroke is currentColor and stroke-width is a presentation attribute, so
5
+ the page can recolour and thicken each use from CSS. -->
6
+ <symbol id="fc-icon-atom" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
7
+ <circle cx="12" cy="12" r="1"/>
8
+ <path d="M20.2 20.2c2.04-2.03.02-7.36-4.5-11.9-4.54-4.52-9.87-6.54-11.9-4.5-2.04 2.03-.02 7.36 4.5 11.9 4.54 4.52 9.87 6.54 11.9 4.5Z"/>
9
+ <path d="M15.7 15.7c4.52-4.54 6.54-9.87 4.5-11.9-2.03-2.04-7.36-.02-11.9 4.5-4.52 4.54-6.54 9.87-4.5 11.9 2.03 2.04 7.36.02 11.9-4.5Z"/>
10
+ </symbol>
11
+ <symbol id="fc-icon-component" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
12
+ <path d="M15.536 11.293a1 1 0 0 0 0 1.414l2.376 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z"/>
13
+ <path d="M2.297 11.293a1 1 0 0 0 0 1.414l2.377 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414L6.088 8.916a1 1 0 0 0-1.414 0z"/>
14
+ <path d="M8.916 17.912a1 1 0 0 0 0 1.415l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.415l-2.377-2.376a1 1 0 0 0-1.414 0z"/>
15
+ <path d="M8.916 4.674a1 1 0 0 0 0 1.414l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z"/>
16
+ </symbol>
17
+ <symbol id="fc-icon-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
18
+ <path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z"/>
19
+ </symbol>
20
+ <symbol id="fc-icon-code-xml" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
21
+ <path d="m18 16 4-4-4-4"/>
22
+ <path d="m6 8-4 4 4 4"/>
23
+ <path d="m14.5 4-5 16"/>
24
+ </symbol>
25
+ <symbol id="fc-icon-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
26
+ <path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>
27
+ </symbol>
28
+ <symbol id="fc-icon-folder-open" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
29
+ <path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/>
30
+ </symbol>
31
+ <symbol id="fc-icon-workflow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
32
+ <rect width="8" height="8" x="3" y="3" rx="2"/>
33
+ <path d="M7 11v4a2 2 0 0 0 2 2h4"/>
34
+ <rect width="8" height="8" x="13" y="13" rx="2"/>
35
+ </symbol>
36
+ <symbol id="fc-icon-ban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
37
+ <circle cx="12" cy="12" r="10"/>
38
+ <path d="M4.929 4.929 19.07 19.071"/>
39
+ </symbol>
40
+ <symbol id="fc-icon-chevron-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
41
+ <path d="m9 18 6-6-6-6"/>
42
+ </symbol>
43
+ <symbol id="fc-icon-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
44
+ <path d="m6 9 6 6 6-6"/>
45
+ </symbol>
46
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
2
+ <!-- The fromcubes mark, traced from the project artwork by tools/build-icon.mjs.
3
+ currentColor so the editor chrome can recolour it per surface. -->
4
+ <path fill="currentColor" fill-rule="evenodd" d="M31.03 1.00L32.97 1.00L34.91 1.97L49.11 10.69L57.83 15.21L59.13 16.50L59.13 47.50L57.83 48.79L49.11 53.31L34.91 62.03L32.97 63.00L30.39 62.68L17.15 54.60L8.43 50.08L4.88 47.50L4.88 16.50L12.95 11.33L31.03 1.32ZM31.68 3.91L9.07 16.82L9.40 17.47L10.69 17.47L15.21 14.56L20.70 13.27L23.28 13.27L33.61 19.08L37.17 21.34L37.49 23.93L36.20 25.22L35.23 27.48L31.35 29.42L31.68 30.39L54.93 17.15L53.64 16.50L48.79 19.73L41.04 21.02L30.71 15.21L28.77 13.59L26.83 12.95L26.51 10.36L29.09 6.49L32.97 4.55L32.00 3.91ZM35.55 6.17L31.35 8.43L29.74 11.33L41.69 18.11L47.18 17.15L51.05 14.56L35.88 6.17ZM21.02 16.18L16.18 17.15L12.95 19.41L22.96 24.90L24.90 26.51L27.48 27.48L27.80 28.13L33.29 25.22L34.26 22.96L25.22 17.47L22.64 16.18L21.34 16.18ZM7.78 19.73L7.46 21.02L12.63 23.60L17.79 29.42L18.11 46.21L16.82 47.50L11.98 47.50L8.43 45.24L7.46 45.24L7.46 46.21L13.27 49.11L30.39 59.45L30.39 57.51L25.54 55.25L20.70 50.08L20.05 32.65L21.34 31.35L26.51 31.35L30.06 33.61L30.71 33.61L30.71 32.65L8.10 19.73ZM56.22 19.73L54.93 20.38L54.93 27.16L52.67 33.61L39.43 41.69L37.81 42.01L36.20 41.36L34.26 37.81L33.94 32.65L33.29 32.97L33.29 59.13L34.58 58.80L34.26 52.99L36.52 45.89L50.73 37.17L53.31 38.14L55.25 41.69L55.25 46.53L56.54 46.21L56.54 19.73ZM52.02 21.99L36.84 30.71L37.17 37.17L37.81 38.46L38.78 38.78L41.69 36.52L50.41 31.68L52.02 26.83L52.02 22.31ZM7.78 24.25L7.46 41.69L11.98 43.95L12.30 44.59L15.21 44.59L14.89 30.06L11.98 27.48L11.33 26.19L7.78 24.57ZM23.28 34.26L23.28 48.47L27.48 52.99L30.39 54.60L30.71 37.17L25.86 34.26L23.60 34.26ZM50.73 40.72L38.78 47.82L37.17 52.99L37.17 57.19L52.34 48.47L52.34 42.33L51.05 40.72Z"/>
5
+ </svg>