@aaqu/fromcubes-react 0.2.0 → 0.2.2
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 +11 -1
- package/THIRD-PARTY-NOTICES.md +27 -0
- package/nodes/editor/class-completion.js +1116 -0
- package/nodes/editor/file-tree.js +1 -1
- package/nodes/editor/format.js +1 -1
- package/nodes/editor/provider-settings.js +8 -8
- package/nodes/editor/workspace.js +5 -3
- package/nodes/lib/admin-api.js +64 -23
- package/nodes/lib/providers.js +272 -30
- package/nodes/portal-react.html +517 -261
- package/nodes/portal-react.js +6 -1
- package/package.json +1 -2
package/nodes/portal-react.html
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
<!-- ============================================================
|
|
2
2
|
Portal Code Workspace — sidebar tab + full-screen editor overlay.
|
|
3
|
-
Served from nodes/editor/ via the /fromcubes-react
|
|
3
|
+
Served from nodes/editor/ via the /fromcubes-react/<version>/editor admin route.
|
|
4
4
|
============================================================ -->
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
<script type="text/javascript">
|
|
6
|
+
// Loaded from the versioned asset path the runtime exports (ASSET_BASE in
|
|
7
|
+
// lib/admin-api.js), so no cache — browser, CDN or proxy — can serve this
|
|
8
|
+
// template an older file, or a 404 it remembered. Execution order follows
|
|
9
|
+
// insertion order (async = false); every script waits for what it needs.
|
|
10
|
+
(function () {
|
|
11
|
+
const base = (RED.settings && RED.settings.portalReactAssetBase) || "fromcubes-react/";
|
|
12
|
+
window.__fcAssetBase = base;
|
|
13
|
+
const link = document.createElement("link");
|
|
14
|
+
link.rel = "stylesheet";
|
|
15
|
+
link.href = base + "editor/workspace.css";
|
|
16
|
+
document.head.appendChild(link);
|
|
17
|
+
["file-tree.js", "class-completion.js", "workspace.js", "format.js", "provider-settings.js"].forEach(function (file) {
|
|
18
|
+
const s = document.createElement("script");
|
|
19
|
+
s.src = base + "editor/" + file;
|
|
20
|
+
s.async = false;
|
|
21
|
+
document.head.appendChild(s);
|
|
22
|
+
});
|
|
23
|
+
})();
|
|
24
|
+
</script>
|
|
10
25
|
|
|
11
26
|
<!-- ============================================================
|
|
12
27
|
Shared: Monaco loader (used by both node editors)
|
|
@@ -29,12 +44,13 @@
|
|
|
29
44
|
quickSuggestions: { strings: true },
|
|
30
45
|
};
|
|
31
46
|
|
|
32
|
-
// className="…" completion
|
|
33
|
-
// Fromcubes)
|
|
47
|
+
// className="…" completion data, from the CSS engine in force (Settings →
|
|
48
|
+
// Fromcubes): classes with their swatch colours and modifiers, and the
|
|
49
|
+
// variants. Refetched every time a dialog or the React Editor opens and
|
|
34
50
|
// after the engine changes — the engine can change while the editor is
|
|
35
51
|
// open, so "load once" would keep offering the old engine's classes.
|
|
36
52
|
window.__fcCssClasses = null;
|
|
37
|
-
window.
|
|
53
|
+
window.__fcCssData = null;
|
|
38
54
|
window.__fcCssEngine = null;
|
|
39
55
|
let cssClassesPending = false;
|
|
40
56
|
window.__fcRefreshCssClasses = function () {
|
|
@@ -42,9 +58,26 @@
|
|
|
42
58
|
cssClassesPending = true;
|
|
43
59
|
$.getJSON("fromcubes-react/css-classes", function (data) {
|
|
44
60
|
if (!data || !Array.isArray(data.classes)) return;
|
|
61
|
+
const prev = window.__fcCssData;
|
|
45
62
|
window.__fcCssEngine = data.engine;
|
|
46
63
|
window.__fcCssClasses = data.classes;
|
|
47
|
-
window.
|
|
64
|
+
window.__fcCssData = {
|
|
65
|
+
engine: data.engine,
|
|
66
|
+
classes: data.classes,
|
|
67
|
+
colors: data.colors || [],
|
|
68
|
+
modifiers: data.modifiers || [],
|
|
69
|
+
classModifiers: data.classModifiers || [],
|
|
70
|
+
variants: data.variants || [],
|
|
71
|
+
describe: data.describe === true,
|
|
72
|
+
canonical: data.canonical === true,
|
|
73
|
+
};
|
|
74
|
+
// What the engine said about classes stays true while it is the same
|
|
75
|
+
// engine; a refetch on every dialog open must not ask again.
|
|
76
|
+
if (prev && prev.engine === data.engine && prev._described) {
|
|
77
|
+
Object.defineProperty(window.__fcCssData, "_described", { value: prev._described });
|
|
78
|
+
Object.defineProperty(window.__fcCssData, "_canonical", { value: prev._canonical });
|
|
79
|
+
}
|
|
80
|
+
if (window.__fcRevalidateCss) window.__fcRevalidateCss();
|
|
48
81
|
})
|
|
49
82
|
.fail(function (xhr) {
|
|
50
83
|
console.error("[FC-Monaco] css-classes FAILED:", xhr.status, xhr.statusText);
|
|
@@ -102,102 +135,398 @@
|
|
|
102
135
|
console.log(PREFIX, "addExtraLib globals.d.ts");
|
|
103
136
|
jsDef.addExtraLib(libContent, "file:///globals.d.ts");
|
|
104
137
|
|
|
105
|
-
// className completion
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
138
|
+
// className completion, hover and CSS preview — Tailwind CSS
|
|
139
|
+
// IntelliSense's logic (editor/class-completion.js) over the data of the
|
|
140
|
+
// CSS engine in force. Inside className="…" / className={…} it offers
|
|
141
|
+
// what IntelliSense offers: variants first (`hover:` reopens the list),
|
|
142
|
+
// then every class in the engine's order with colour swatches, and after
|
|
143
|
+
// `/` the class's modifiers. Strings elsewhere complete too once they
|
|
144
|
+
// hold a known class — this editor's addition.
|
|
145
|
+
console.log(PREFIX, "registering CSS class completion provider");
|
|
146
|
+
const cssKinds = {
|
|
147
|
+
variant: monaco.languages.CompletionItemKind.Module,
|
|
148
|
+
color: monaco.languages.CompletionItemKind.Color,
|
|
149
|
+
constant: monaco.languages.CompletionItemKind.Constant,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
function cssData() {
|
|
153
|
+
const cc = window.__fcClassCompletion;
|
|
154
|
+
const data = window.__fcCssData;
|
|
155
|
+
if (!cc || !data || !data.classes.length) return null;
|
|
156
|
+
return cc.prepare(data);
|
|
157
|
+
}
|
|
114
158
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
159
|
+
// What the engine says about each candidate (CSS, summary, colour,
|
|
160
|
+
// conflict rules), or null — one request per batch, answers kept for
|
|
161
|
+
// the data's lifetime. Fast: hovers and the detail pane wait for it.
|
|
162
|
+
function describeCss(data, candidates) {
|
|
163
|
+
return askCssEngine(data, data.describe, "css-describe", data._described, candidates, 5000);
|
|
164
|
+
}
|
|
119
165
|
|
|
120
|
-
|
|
121
|
-
|
|
166
|
+
// Canonical spellings, for the diagnostic only: the engine may take its
|
|
167
|
+
// time (the Tailwind add-on's worker builds an index on its first
|
|
168
|
+
// answer), so nothing else waits for these.
|
|
169
|
+
function canonicalCss(data, candidates) {
|
|
170
|
+
return askCssEngine(data, data.canonical, "css-canonical", data._canonical, candidates, 30000);
|
|
171
|
+
}
|
|
122
172
|
|
|
123
|
-
|
|
173
|
+
const cssInflight = new WeakMap(); // answer cache → candidate → pending request
|
|
124
174
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
175
|
+
function askCssEngine(data, supported, route, cache, candidates, timeout) {
|
|
176
|
+
const cc = window.__fcClassCompletion;
|
|
177
|
+
const nulls = candidates.map(function () {
|
|
178
|
+
return null;
|
|
179
|
+
});
|
|
180
|
+
if (!supported) return Promise.resolve(nulls);
|
|
181
|
+
// Classes already asked about and not answered yet are waited for,
|
|
182
|
+
// not asked again — typing while the first answer is slow would
|
|
183
|
+
// otherwise queue the same classes over and over.
|
|
184
|
+
let inflight = cssInflight.get(cache);
|
|
185
|
+
if (!inflight) cssInflight.set(cache, (inflight = new Map()));
|
|
186
|
+
const waiting = [];
|
|
187
|
+
const missing = candidates.filter(function (c, i) {
|
|
188
|
+
if (cache.has(c) || candidates.indexOf(c) !== i) return false;
|
|
189
|
+
if (inflight.has(c)) {
|
|
190
|
+
waiting.push(inflight.get(c));
|
|
191
|
+
return false;
|
|
132
192
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
break;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
// If typing the very first word, accept if it looks like a prefix
|
|
156
|
-
if (!hasTwToken && parts.length === 1 && word.length >= 2) {
|
|
157
|
-
for (let j = 0; j < classes.length; j++) {
|
|
158
|
-
if (classes[j].indexOf(word) === 0) {
|
|
159
|
-
hasTwToken = true;
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
193
|
+
return true;
|
|
194
|
+
});
|
|
195
|
+
const batches = [];
|
|
196
|
+
for (let i = 0; i < missing.length; i += cc.MAX_DESCRIBE) {
|
|
197
|
+
batches.push(missing.slice(i, i + cc.MAX_DESCRIBE));
|
|
198
|
+
}
|
|
199
|
+
const asked = batches.map(function (batch) {
|
|
200
|
+
const request = new Promise(function (resolve) {
|
|
201
|
+
$.ajax({
|
|
202
|
+
url: "fromcubes-react/" + route,
|
|
203
|
+
data: { c: batch.join("\n") },
|
|
204
|
+
dataType: "json",
|
|
205
|
+
timeout: timeout,
|
|
206
|
+
})
|
|
207
|
+
.done(function (res) {
|
|
208
|
+
if (res && Array.isArray(res.results) && res.engine === data.engine) {
|
|
209
|
+
batch.forEach(function (c, i) {
|
|
210
|
+
cache.set(c, res.results[i] || null);
|
|
211
|
+
});
|
|
162
212
|
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
213
|
+
})
|
|
214
|
+
.fail(function (xhr) {
|
|
215
|
+
console.error(PREFIX, route + " FAILED:", xhr.status, xhr.statusText);
|
|
216
|
+
})
|
|
217
|
+
.always(function () {
|
|
218
|
+
batch.forEach(function (c) {
|
|
219
|
+
inflight.delete(c);
|
|
220
|
+
});
|
|
221
|
+
resolve();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
batch.forEach(function (c) {
|
|
225
|
+
inflight.set(c, request);
|
|
226
|
+
});
|
|
227
|
+
return request;
|
|
228
|
+
});
|
|
229
|
+
return Promise.all(asked.concat(waiting)).then(function () {
|
|
230
|
+
return candidates.map(function (c) {
|
|
231
|
+
return cache.has(c) ? cache.get(c) : null;
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
167
235
|
|
|
168
|
-
|
|
169
|
-
|
|
236
|
+
// A variant the data does not list by name (`group-hover/item`,
|
|
237
|
+
// `data-[open]`) is valid when the engine compiles it — IntelliSense's
|
|
238
|
+
// test, `<part>:[color:red]`.
|
|
239
|
+
function isValidVariant(data, part) {
|
|
240
|
+
if (!window.__fcClassCompletion.mayBeVariant(data, part)) return false;
|
|
241
|
+
return describeCss(data, [part + ":[color:red]"]).then(function (r) {
|
|
242
|
+
return !!r[0];
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Text the class-list scan needs: up to SEARCH_RANGE before the cursor,
|
|
247
|
+
// and the cursor's line split at the cursor.
|
|
248
|
+
function cssScanText(model, position) {
|
|
249
|
+
const cc = window.__fcClassCompletion;
|
|
250
|
+
const offset = model.getOffsetAt(position);
|
|
251
|
+
const from = model.getPositionAt(Math.max(0, offset - cc.SEARCH_RANGE));
|
|
252
|
+
const line = model.getLineContent(position.lineNumber);
|
|
253
|
+
return {
|
|
254
|
+
before: model.getValueInRange({
|
|
255
|
+
startLineNumber: from.lineNumber,
|
|
256
|
+
startColumn: from.column,
|
|
170
257
|
endLineNumber: position.lineNumber,
|
|
171
|
-
startColumn: position.column - word.length,
|
|
172
258
|
endColumn: position.column,
|
|
173
|
-
}
|
|
259
|
+
}),
|
|
260
|
+
lineBefore: line.substring(0, position.column - 1),
|
|
261
|
+
lineAfter: line.substring(position.column - 1),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
174
264
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
265
|
+
function cssFence(css) {
|
|
266
|
+
return { value: "```css\n" + css + "\n```" };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
monaco.languages.registerCompletionItemProvider("javascript", {
|
|
270
|
+
triggerCharacters: window.__fcClassCompletion
|
|
271
|
+
? window.__fcClassCompletion.TRIGGER_CHARACTERS
|
|
272
|
+
: ['"', "'", "`", " ", ".", "(", "[", "]", "!", "/", "-", ":"],
|
|
273
|
+
provideCompletionItems: function (model, position, context) {
|
|
274
|
+
const data = cssData();
|
|
275
|
+
if (!data) return { suggestions: [] };
|
|
276
|
+
const cc = window.__fcClassCompletion;
|
|
277
|
+
const text = cssScanText(model, position);
|
|
278
|
+
const at = cc.classListAt(data, text.before, text.lineBefore);
|
|
279
|
+
if (!at) return { suggestions: [] };
|
|
280
|
+
const kinds = monaco.languages.CompletionTriggerKind;
|
|
281
|
+
const ctx = {
|
|
282
|
+
triggerKind:
|
|
283
|
+
context.triggerKind === kinds.Invoke
|
|
284
|
+
? "invoke"
|
|
285
|
+
: context.triggerKind === kinds.TriggerCharacter
|
|
286
|
+
? "character"
|
|
287
|
+
: "incomplete",
|
|
288
|
+
triggerCharacter: context.triggerCharacter,
|
|
289
|
+
};
|
|
290
|
+
return cc
|
|
291
|
+
.completionsFromClassList(data, at.classList, ctx, function (part) {
|
|
292
|
+
return isValidVariant(data, part);
|
|
293
|
+
})
|
|
294
|
+
.then(function (list) {
|
|
295
|
+
if (!list.modifiers) return list;
|
|
296
|
+
return describeCss(
|
|
297
|
+
data,
|
|
298
|
+
list.items.map(function (item) {
|
|
299
|
+
return item.label;
|
|
300
|
+
}),
|
|
301
|
+
).then(function (described) {
|
|
302
|
+
cc.applyModifierColors(list.items, described);
|
|
303
|
+
return list;
|
|
304
|
+
});
|
|
305
|
+
})
|
|
306
|
+
.then(function (list) {
|
|
307
|
+
const range = {
|
|
308
|
+
startLineNumber: position.lineNumber,
|
|
309
|
+
endLineNumber: position.lineNumber,
|
|
310
|
+
startColumn: position.column + list.range.start,
|
|
311
|
+
endColumn: position.column + list.range.end,
|
|
312
|
+
};
|
|
313
|
+
const owner = { variants: list.variants, important: list.important };
|
|
314
|
+
const suggestions = list.items.map(function (item) {
|
|
315
|
+
const s = {
|
|
316
|
+
label: item.label,
|
|
317
|
+
kind: cssKinds[item.kind],
|
|
318
|
+
insertText: item.insertText || item.label,
|
|
319
|
+
sortText: item.sortText,
|
|
320
|
+
range: range,
|
|
321
|
+
};
|
|
322
|
+
if (item.filterText) s.filterText = item.filterText;
|
|
323
|
+
if (item.detail) s.detail = item.detail;
|
|
324
|
+
if (item.documentation) s.documentation = item.documentation;
|
|
325
|
+
if (item.snippet) {
|
|
326
|
+
s.insertTextRules = monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet;
|
|
327
|
+
}
|
|
328
|
+
if (item.retrigger) s.command = { id: "editor.action.triggerSuggest", title: "" };
|
|
329
|
+
if (item.kind !== "variant") s.fcClassOwner = owner;
|
|
330
|
+
return s;
|
|
186
331
|
});
|
|
332
|
+
console.log(
|
|
333
|
+
PREFIX,
|
|
334
|
+
"CSS class completion: ctx=" +
|
|
335
|
+
(at.attribute ? "attribute" : "string") +
|
|
336
|
+
" items=" +
|
|
337
|
+
suggestions.length,
|
|
338
|
+
);
|
|
339
|
+
return { suggestions: suggestions };
|
|
340
|
+
});
|
|
341
|
+
},
|
|
342
|
+
// The detail pane: the declarations on one line, and the CSS the
|
|
343
|
+
// class generates under the variants typed before it.
|
|
344
|
+
resolveCompletionItem: function (item) {
|
|
345
|
+
const data = cssData();
|
|
346
|
+
if (!data || !item.fcClassOwner || (item.detail && item.documentation)) return item;
|
|
347
|
+
const className = item.fcClassOwner.important ? "!" + item.label : item.label;
|
|
348
|
+
const full = item.fcClassOwner.variants.concat([className]).join(":");
|
|
349
|
+
return describeCss(data, [full, className]).then(function (r) {
|
|
350
|
+
const whole = r[0];
|
|
351
|
+
const base = r[1];
|
|
352
|
+
if (!whole) return item;
|
|
353
|
+
if (!item.detail) {
|
|
354
|
+
item.detail = /^\d+ rules$/.test(whole.detail || "") ? whole.detail : (base || whole).detail || undefined;
|
|
187
355
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
);
|
|
200
|
-
|
|
356
|
+
if (!item.documentation && whole.documentation) item.documentation = cssFence(whole.documentation);
|
|
357
|
+
return item;
|
|
358
|
+
});
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
monaco.languages.registerHoverProvider("javascript", {
|
|
363
|
+
provideHover: function (model, position) {
|
|
364
|
+
const data = cssData();
|
|
365
|
+
if (!data || !data.describe) return null;
|
|
366
|
+
const text = cssScanText(model, position);
|
|
367
|
+
const hit = window.__fcClassCompletion.classNameAt(data, text.before, text.lineBefore, text.lineAfter);
|
|
368
|
+
if (!hit) return null;
|
|
369
|
+
return describeCss(data, [hit.className]).then(function (r) {
|
|
370
|
+
if (!r[0] || !r[0].hover) return null;
|
|
371
|
+
return {
|
|
372
|
+
range: {
|
|
373
|
+
startLineNumber: position.lineNumber,
|
|
374
|
+
endLineNumber: position.lineNumber,
|
|
375
|
+
startColumn: position.column + hit.start,
|
|
376
|
+
endColumn: position.column + hit.end,
|
|
377
|
+
},
|
|
378
|
+
contents: [cssFence(r[0].hover)],
|
|
379
|
+
};
|
|
380
|
+
});
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Diagnostics, quick fixes and colour squares — IntelliSense's
|
|
385
|
+
// cssConflict and suggestCanonicalClasses warnings and its document
|
|
386
|
+
// colours, over every class attribute of every javascript model (node
|
|
387
|
+
// dialogs and the React Editor alike), rechecked as the code changes.
|
|
388
|
+
function cssRange(model, start, end) {
|
|
389
|
+
const a = model.getPositionAt(start);
|
|
390
|
+
const b = model.getPositionAt(end);
|
|
391
|
+
return { startLineNumber: a.lineNumber, startColumn: a.column, endLineNumber: b.lineNumber, endColumn: b.column };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function cssClassLists(model) {
|
|
395
|
+
const cc = window.__fcClassCompletion;
|
|
396
|
+
return cc.findClassListsInText(cc.jsWithoutComments(model.getValue()));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const CSS_MARKER_OWNER = "tailwindcss";
|
|
400
|
+
const cssDiagnostics = new WeakMap(); // model → diagnostics, for their quick fixes
|
|
401
|
+
const cssTimers = new WeakMap();
|
|
402
|
+
|
|
403
|
+
function validateCss(model) {
|
|
404
|
+
if (model.isDisposed() || model.getLanguageId() !== "javascript") return;
|
|
405
|
+
const data = cssData();
|
|
406
|
+
if (!data || !data.describe) {
|
|
407
|
+
cssDiagnostics.delete(model);
|
|
408
|
+
monaco.editor.setModelMarkers(model, CSS_MARKER_OWNER, []);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const cc = window.__fcClassCompletion;
|
|
412
|
+
const version = model.getVersionId();
|
|
413
|
+
const lists = cssClassLists(model);
|
|
414
|
+
const names = cc.uniqueClassNames(lists);
|
|
415
|
+
// Conflicts as soon as the classes are described; canonical
|
|
416
|
+
// suggestions join when the engine has them — a slow first answer
|
|
417
|
+
// never holds back the rest.
|
|
418
|
+
describeCss(data, names).then(function () {
|
|
419
|
+
publishCssMarkers(model, version, data, lists);
|
|
420
|
+
canonicalCss(data, names).then(function () {
|
|
421
|
+
publishCssMarkers(model, version, data, lists);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function publishCssMarkers(model, version, data, lists) {
|
|
427
|
+
if (model.isDisposed() || model.getVersionId() !== version || cssData() !== data) return;
|
|
428
|
+
const diagnostics = window.__fcClassCompletion.diagnose(lists, data._described, data._canonical);
|
|
429
|
+
cssDiagnostics.set(model, diagnostics);
|
|
430
|
+
monaco.editor.setModelMarkers(
|
|
431
|
+
model,
|
|
432
|
+
CSS_MARKER_OWNER,
|
|
433
|
+
diagnostics.map(function (d) {
|
|
434
|
+
return Object.assign(cssRange(model, d.start, d.end), {
|
|
435
|
+
severity: d.severity === "error" ? monaco.MarkerSeverity.Error : monaco.MarkerSeverity.Warning,
|
|
436
|
+
message: d.message,
|
|
437
|
+
source: "tailwindcss",
|
|
438
|
+
code: d.code,
|
|
439
|
+
relatedInformation: d.related.map(function (r) {
|
|
440
|
+
return Object.assign(cssRange(model, r.start, r.end), { resource: model.uri, message: r.message });
|
|
441
|
+
}),
|
|
442
|
+
});
|
|
443
|
+
}),
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function scheduleCssValidation(model) {
|
|
448
|
+
clearTimeout(cssTimers.get(model));
|
|
449
|
+
cssTimers.set(
|
|
450
|
+
model,
|
|
451
|
+
setTimeout(function () {
|
|
452
|
+
validateCss(model);
|
|
453
|
+
}, 400),
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function watchCssModel(model) {
|
|
458
|
+
const changed = model.onDidChangeContent(function () {
|
|
459
|
+
scheduleCssValidation(model);
|
|
460
|
+
});
|
|
461
|
+
const language = model.onDidChangeLanguage(function () {
|
|
462
|
+
scheduleCssValidation(model);
|
|
463
|
+
});
|
|
464
|
+
model.onWillDispose(function () {
|
|
465
|
+
changed.dispose();
|
|
466
|
+
language.dispose();
|
|
467
|
+
clearTimeout(cssTimers.get(model));
|
|
468
|
+
});
|
|
469
|
+
scheduleCssValidation(model);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
monaco.editor.getModels().forEach(watchCssModel);
|
|
473
|
+
monaco.editor.onDidCreateModel(watchCssModel);
|
|
474
|
+
// New engine data (a dialog opened, the engine switched): check again.
|
|
475
|
+
window.__fcRevalidateCss = function () {
|
|
476
|
+
monaco.editor.getModels().forEach(scheduleCssValidation);
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
monaco.languages.registerCodeActionProvider("javascript", {
|
|
480
|
+
provideCodeActions: function (model, _range, context) {
|
|
481
|
+
const diagnostics = cssDiagnostics.get(model) || [];
|
|
482
|
+
const actions = [];
|
|
483
|
+
context.markers.forEach(function (marker) {
|
|
484
|
+
if (marker.source !== "tailwindcss") return;
|
|
485
|
+
const start = model.getOffsetAt({ lineNumber: marker.startLineNumber, column: marker.startColumn });
|
|
486
|
+
const end = model.getOffsetAt({ lineNumber: marker.endLineNumber, column: marker.endColumn });
|
|
487
|
+
diagnostics.forEach(function (d) {
|
|
488
|
+
if (d.start !== start || d.end !== end || d.code !== marker.code || d.message !== marker.message) return;
|
|
489
|
+
d.fixes.forEach(function (fix) {
|
|
490
|
+
actions.push({
|
|
491
|
+
title: fix.title,
|
|
492
|
+
kind: "quickfix",
|
|
493
|
+
diagnostics: [marker],
|
|
494
|
+
edit: {
|
|
495
|
+
edits: [
|
|
496
|
+
{
|
|
497
|
+
resource: model.uri,
|
|
498
|
+
versionId: model.getVersionId(),
|
|
499
|
+
textEdit: { range: cssRange(model, fix.start, fix.end), text: fix.text },
|
|
500
|
+
},
|
|
501
|
+
],
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
return { actions: actions, dispose: function () {} };
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
monaco.languages.registerColorProvider("javascript", {
|
|
512
|
+
provideDocumentColors: function (model) {
|
|
513
|
+
const data = cssData();
|
|
514
|
+
if (!data) return [];
|
|
515
|
+
const cc = window.__fcClassCompletion;
|
|
516
|
+
const lists = cssClassLists(model);
|
|
517
|
+
return describeCss(data, cc.classesNeedingColor(data, lists)).then(function () {
|
|
518
|
+
if (model.isDisposed()) return [];
|
|
519
|
+
return cc.documentColors(data, lists, data._described).map(function (c) {
|
|
520
|
+
return { color: c.color, range: cssRange(model, c.start, c.end) };
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
},
|
|
524
|
+
// Only an arbitrary colour (`bg-[#f00]`) takes one back from the picker.
|
|
525
|
+
provideColorPresentations: function (model, info) {
|
|
526
|
+
const className = model.getValueInRange(info.range);
|
|
527
|
+
return window.__fcClassCompletion.colorPresentations(className, info.color).map(function (label) {
|
|
528
|
+
return { label: label, textEdit: { range: info.range, text: label } };
|
|
529
|
+
});
|
|
201
530
|
},
|
|
202
531
|
});
|
|
203
532
|
|
|
@@ -1041,10 +1370,11 @@
|
|
|
1041
1370
|
return;
|
|
1042
1371
|
}
|
|
1043
1372
|
const s = document.createElement("script");
|
|
1044
|
-
|
|
1373
|
+
const base = window.__fcAssetBase || "fromcubes-react/";
|
|
1374
|
+
s.src = base + "vs/loader.js";
|
|
1045
1375
|
s.onload = function () {
|
|
1046
1376
|
console.log(PREFIX, "loader.js loaded, configuring require paths");
|
|
1047
|
-
require.config({ paths: { vs: "
|
|
1377
|
+
require.config({ paths: { vs: base + "vs" } });
|
|
1048
1378
|
require(["vs/editor/editor.main"], function () {
|
|
1049
1379
|
console.log(PREFIX, "editor.main loaded");
|
|
1050
1380
|
ensureJsxSetup();
|
|
@@ -1312,32 +1642,26 @@
|
|
|
1312
1642
|
</script>
|
|
1313
1643
|
|
|
1314
1644
|
<script type="text/html" data-help-name="fc-portal-component">
|
|
1645
|
+
<p>A React component every portal can use as a tag.</p>
|
|
1646
|
+
|
|
1647
|
+
<h3>Details</h3>
|
|
1315
1648
|
<p>
|
|
1316
|
-
|
|
1317
|
-
<
|
|
1649
|
+
Write the component in <b>JSX Code</b> and give its name in
|
|
1650
|
+
<b>JSX Tag</b>. After deploy, any <b>React Portal</b> uses it as
|
|
1651
|
+
<code><Name /></code>, without imports. Components can use each
|
|
1652
|
+
other and <code>useNodeRed()</code>.
|
|
1318
1653
|
</p>
|
|
1654
|
+
<pre>function Badge({ label }) {
|
|
1655
|
+
return <span className="badge">{label}</span>;
|
|
1656
|
+
}</pre>
|
|
1657
|
+
|
|
1319
1658
|
<h3>Properties</h3>
|
|
1320
1659
|
<dl>
|
|
1321
1660
|
<dt>JSX Tag</dt>
|
|
1322
|
-
<dd>
|
|
1323
|
-
The name used as a JSX tag in portal-react pages (e.g.
|
|
1324
|
-
<code><MyComponent /></code>).
|
|
1325
|
-
</dd>
|
|
1326
|
-
<dt>Input fields</dt>
|
|
1327
|
-
<dd>Comma-separated list of expected input props (documentation only).</dd>
|
|
1328
|
-
<dt>Output fields</dt>
|
|
1329
|
-
<dd>Comma-separated list of output props (documentation only).</dd>
|
|
1661
|
+
<dd>The component's name, starting with a capital letter (<code>Badge</code>).</dd>
|
|
1330
1662
|
<dt>JSX Code</dt>
|
|
1331
|
-
<dd>
|
|
1332
|
-
The component function. Use <code>useNodeRed()</code> for data binding.
|
|
1333
|
-
</dd>
|
|
1663
|
+
<dd>The component function.</dd>
|
|
1334
1664
|
</dl>
|
|
1335
|
-
<h3>Usage</h3>
|
|
1336
|
-
<p>
|
|
1337
|
-
Place this node on the canvas, set the JSX tag name and code, then deploy.
|
|
1338
|
-
All <strong>portal-react</strong> nodes will automatically have access to
|
|
1339
|
-
this component.
|
|
1340
|
-
</p>
|
|
1341
1665
|
</script>
|
|
1342
1666
|
|
|
1343
1667
|
<!-- ============================================================
|
|
@@ -1558,57 +1882,28 @@
|
|
|
1558
1882
|
</script>
|
|
1559
1883
|
|
|
1560
1884
|
<script type="text/html" data-help-name="fc-portal-utility">
|
|
1885
|
+
<p>Shared JavaScript — helpers, hooks, constants — for every portal.</p>
|
|
1886
|
+
|
|
1887
|
+
<h3>Details</h3>
|
|
1561
1888
|
<p>
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
top level (no IIFE wrapper) — a single utility node may declare many symbols.
|
|
1889
|
+
Declare any number of functions, constants or classes in <b>Code</b>.
|
|
1890
|
+
Portals and components use them by name, without imports. A portal only
|
|
1891
|
+
includes this node if it uses something from it.
|
|
1566
1892
|
</p>
|
|
1893
|
+
<pre>function clamp(n, min, max) {
|
|
1894
|
+
return Math.max(min, Math.min(max, n));
|
|
1895
|
+
}</pre>
|
|
1896
|
+
|
|
1567
1897
|
<h3>Properties</h3>
|
|
1568
1898
|
<dl>
|
|
1569
1899
|
<dt>Module name</dt>
|
|
1570
1900
|
<dd>
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
components and utilities.
|
|
1901
|
+
A name for this node; it must not repeat a component's or another
|
|
1902
|
+
utility's name.
|
|
1574
1903
|
</dd>
|
|
1575
1904
|
<dt>Code</dt>
|
|
1576
|
-
<dd>
|
|
1577
|
-
Top-level JavaScript: <code>const</code>, <code>let</code>,
|
|
1578
|
-
<code>function</code>, <code>class</code> declarations. <code>import</code>
|
|
1579
|
-
statements at the top are auto-hoisted into the bundle.
|
|
1580
|
-
</dd>
|
|
1905
|
+
<dd>Top-level declarations; <code>import</code> lines are allowed.</dd>
|
|
1581
1906
|
</dl>
|
|
1582
|
-
<h3>Selective inclusion</h3>
|
|
1583
|
-
<p>
|
|
1584
|
-
A utility node is bundled into a portal only when the portal's JSX (or any
|
|
1585
|
-
of its referenced library components) mentions at least one of the symbols
|
|
1586
|
-
declared in the utility. Unused utility nodes are skipped.
|
|
1587
|
-
</p>
|
|
1588
|
-
<p>
|
|
1589
|
-
Group related symbols in one node — referencing any one symbol pulls in the
|
|
1590
|
-
whole node's code. Split unrelated helpers across multiple nodes for finer
|
|
1591
|
-
granularity.
|
|
1592
|
-
</p>
|
|
1593
|
-
<h3>Example</h3>
|
|
1594
|
-
<pre>function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); }
|
|
1595
|
-
|
|
1596
|
-
function useDebounce(value, ms = 300) {
|
|
1597
|
-
const [v, setV] = React.useState(value);
|
|
1598
|
-
React.useEffect(() => {
|
|
1599
|
-
const t = setTimeout(() => setV(value), ms);
|
|
1600
|
-
return () => clearTimeout(t);
|
|
1601
|
-
}, [value, ms]);
|
|
1602
|
-
return v;
|
|
1603
|
-
}</pre>
|
|
1604
|
-
<p>
|
|
1605
|
-
Then in any <strong>portal-react</strong> node:
|
|
1606
|
-
</p>
|
|
1607
|
-
<pre>function App() {
|
|
1608
|
-
const { data } = useNodeRed();
|
|
1609
|
-
const slow = useDebounce(data?.value);
|
|
1610
|
-
return <div>{clamp(slow ?? 0, 0, 100)}</div>;
|
|
1611
|
-
}</pre>
|
|
1612
1907
|
</script>
|
|
1613
1908
|
|
|
1614
1909
|
<!-- ============================================================
|
|
@@ -2415,125 +2710,86 @@ function useDebounce(value, ms = 300) {
|
|
|
2415
2710
|
Help
|
|
2416
2711
|
============================================================ -->
|
|
2417
2712
|
<script type="text/html" data-help-name="portal-react">
|
|
2418
|
-
<h3>Quick Reference</h3>
|
|
2419
|
-
<ul>
|
|
2420
|
-
<li><code>useNodeRed()</code> → <code>{ data, send, user, portalClient }</code></li>
|
|
2421
|
-
<li>Components from <code>fc-portal-component</code> nodes are auto-imported</li>
|
|
2422
|
-
<li>Must export <code><App /></code></li>
|
|
2423
|
-
<li>JSX is transpiled server-side at deploy</li>
|
|
2424
|
-
</ul>
|
|
2425
|
-
|
|
2426
2713
|
<p>
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
time</strong> using esbuild — browsers receive pre-compiled JS with zero
|
|
2430
|
-
runtime compilation. Tailwind CSS 4 utility classes are generated
|
|
2431
|
-
server-side and served as static CSS.
|
|
2714
|
+
A React page served at <code>/fromcubes/<sub-path></code>, kept live
|
|
2715
|
+
with data from the flow.
|
|
2432
2716
|
</p>
|
|
2433
2717
|
|
|
2434
2718
|
<h3>Inputs</h3>
|
|
2435
|
-
<
|
|
2436
|
-
<
|
|
2437
|
-
to
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
>
|
|
2719
|
+
<dl class="message-properties">
|
|
2720
|
+
<dt>payload <span class="property-type">any</span></dt>
|
|
2721
|
+
<dd>Sent to the open pages; read it with <code>useNodeRed().data</code>.</dd>
|
|
2722
|
+
<dt class="optional">_client <span class="property-type">object</span></dt>
|
|
2723
|
+
<dd>
|
|
2724
|
+
Who gets it: <code>{ portalClient }</code> one browser tab,
|
|
2725
|
+
<code>{ userId }</code> every tab of one user,
|
|
2726
|
+
<code>{ authenticated: true }</code> every logged-in visitor.
|
|
2727
|
+
Without it, every open page.
|
|
2728
|
+
</dd>
|
|
2729
|
+
</dl>
|
|
2446
2730
|
|
|
2447
2731
|
<h3>Outputs</h3>
|
|
2448
|
-
<
|
|
2449
|
-
<
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
<p>
|
|
2455
|
-
Add npm packages in the <strong>Modules</strong> list (e.g.
|
|
2456
|
-
<code>chart.js/auto@^4.4.0</code>, <code>d3</code>, <code>three</code>).
|
|
2457
|
-
Node-RED auto-installs them at deploy time. All packages are bundled with
|
|
2458
|
-
React into a single vendor IIFE via esbuild, cached by hash of installed
|
|
2459
|
-
versions. Use them in JSX via standard imports.
|
|
2460
|
-
</p>
|
|
2732
|
+
<dl class="message-properties">
|
|
2733
|
+
<dt>payload <span class="property-type">any</span></dt>
|
|
2734
|
+
<dd>What the page passed to <code>send(payload, topic)</code>.</dd>
|
|
2735
|
+
<dt>_client <span class="property-type">object</span></dt>
|
|
2736
|
+
<dd>The tab and user it came from — send a reply back with it.</dd>
|
|
2737
|
+
</dl>
|
|
2461
2738
|
|
|
2462
|
-
<h3>
|
|
2463
|
-
<ul>
|
|
2464
|
-
<li>
|
|
2465
|
-
Each deploy re-transpiles JSX via esbuild and rebuilds Tailwind CSS
|
|
2466
|
-
(both cached by content hash — unchanged code is instant)
|
|
2467
|
-
</li>
|
|
2468
|
-
<li>
|
|
2469
|
-
Vendor bundle (React + npm packages) rebuilt only when package versions
|
|
2470
|
-
change
|
|
2471
|
-
</li>
|
|
2472
|
-
<li>
|
|
2473
|
-
Active WebSocket clients receive close code 1001 and auto-reconnect
|
|
2474
|
-
(exponential backoff)
|
|
2475
|
-
</li>
|
|
2476
|
-
<li>
|
|
2477
|
-
Stale HTTP routes and WS handlers are cleaned up before new ones register
|
|
2478
|
-
</li>
|
|
2479
|
-
<li>
|
|
2480
|
-
Transpile errors show in node status and as an error page on the endpoint
|
|
2481
|
-
</li>
|
|
2482
|
-
</ul>
|
|
2483
|
-
|
|
2484
|
-
<h3>Shared Components</h3>
|
|
2739
|
+
<h3>Details</h3>
|
|
2485
2740
|
<p>
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
by their component name — no imports needed.
|
|
2741
|
+
Write a <code>function App()</code> in the <b>JSX</b> tab. It is compiled
|
|
2742
|
+
when you deploy, so the browser gets ready-made JavaScript.
|
|
2489
2743
|
</p>
|
|
2490
|
-
|
|
2491
|
-
|
|
2744
|
+
<pre>function App() {
|
|
2745
|
+
const { data, send } = useNodeRed();
|
|
2746
|
+
return <button onClick={() => send("hi")}>{data}</button>;
|
|
2747
|
+
}</pre>
|
|
2492
2748
|
<p>
|
|
2493
|
-
|
|
2494
|
-
|
|
2749
|
+
<code>useNodeRed()</code> gives <code>data</code> (the last payload,
|
|
2750
|
+
<code>null</code> until one arrives), <code>send</code>, <code>user</code>
|
|
2751
|
+
(the visitor, or <code>null</code>) and <code>portalClient</code> (this
|
|
2752
|
+
tab's id).
|
|
2495
2753
|
</p>
|
|
2496
|
-
<ul>
|
|
2497
|
-
<li><code>useNodeRed()</code> returns <code>{ data, send, user, portalClient }</code></li>
|
|
2498
|
-
<li>
|
|
2499
|
-
All WebSocket messages include <code>msg._client</code> with
|
|
2500
|
-
<code>{ portalClient, ...userFields }</code>
|
|
2501
|
-
</li>
|
|
2502
|
-
<li>
|
|
2503
|
-
To target a specific tab: keep <code>msg._client</code> (or set
|
|
2504
|
-
<code>msg._client = { portalClient: "..." }</code>)
|
|
2505
|
-
</li>
|
|
2506
|
-
<li>
|
|
2507
|
-
To target all tabs of a user: set
|
|
2508
|
-
<code>msg._client = { userId: "..." }</code> (no portalClient)
|
|
2509
|
-
</li>
|
|
2510
|
-
<li>
|
|
2511
|
-
To target every authenticated session (skips anonymous):
|
|
2512
|
-
<code>msg._client = { authenticated: true }</code>
|
|
2513
|
-
</li>
|
|
2514
|
-
<li>
|
|
2515
|
-
To broadcast to all: remove <code>msg._client</code>. With
|
|
2516
|
-
<b>Authenticated-only delivery</b> enabled this defaults to the
|
|
2517
|
-
authenticated sessions instead; force a true broadcast with
|
|
2518
|
-
<code>msg._client = { authenticated: false }</code>
|
|
2519
|
-
</li>
|
|
2520
|
-
</ul>
|
|
2521
|
-
|
|
2522
|
-
<h3>Custom Head HTML</h3>
|
|
2523
2754
|
<p>
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
event handlers run in the public portal page. Example:
|
|
2755
|
+
<b>React Component</b> and <b>React Utility</b> nodes can be used here by
|
|
2756
|
+
name, without imports.
|
|
2527
2757
|
</p>
|
|
2528
|
-
<pre>
|
|
2529
|
-
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet"></pre
|
|
2530
|
-
>
|
|
2531
2758
|
|
|
2532
|
-
<h3>
|
|
2759
|
+
<h3>Properties</h3>
|
|
2760
|
+
<dl>
|
|
2761
|
+
<dt>Sub-path</dt>
|
|
2762
|
+
<dd>Where the page lives: <code>sensors</code> → <code>/fromcubes/sensors</code>.</dd>
|
|
2763
|
+
<dt>Title</dt>
|
|
2764
|
+
<dd>The browser tab's title.</dd>
|
|
2765
|
+
<dt>Modules</dt>
|
|
2766
|
+
<dd>
|
|
2767
|
+
npm packages to use in the code (<code>chart.js/auto</code>,
|
|
2768
|
+
<code>d3</code>, <code>three</code>); installed on deploy, then
|
|
2769
|
+
<code>import</code> them.
|
|
2770
|
+
</dd>
|
|
2771
|
+
<dt>Enable Portal Auth headers</dt>
|
|
2772
|
+
<dd>
|
|
2773
|
+
Identify visitors with the identity provider chosen in
|
|
2774
|
+
<b>Settings → Fromcubes</b>. Off: everyone is anonymous.
|
|
2775
|
+
</dd>
|
|
2776
|
+
<dt>Authenticated-only delivery</dt>
|
|
2777
|
+
<dd>
|
|
2778
|
+
Messages without <code>_client</code> go to logged-in visitors only;
|
|
2779
|
+
<code>_client = { authenticated: false }</code> still reaches everyone.
|
|
2780
|
+
</dd>
|
|
2781
|
+
<dt>Show WebSocket status indicator</dt>
|
|
2782
|
+
<dd>A small connected/disconnected badge in the page's corner.</dd>
|
|
2783
|
+
<dt>Head HTML</dt>
|
|
2784
|
+
<dd>Extra tags for the page's <code><head></code>, such as fonts or stylesheets.</dd>
|
|
2785
|
+
</dl>
|
|
2786
|
+
|
|
2787
|
+
<h3>Styling</h3>
|
|
2533
2788
|
<p>
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2789
|
+
The page's stylesheet comes from the CSS engine chosen in
|
|
2790
|
+
<b>Settings → Fromcubes</b>. Install <code>@aaqu/fromcubes-tailwind</code>
|
|
2791
|
+
to style with Tailwind CSS classes; without a CSS engine the page has no
|
|
2792
|
+
generated stylesheet.
|
|
2537
2793
|
</p>
|
|
2538
2794
|
</script>
|
|
2539
2795
|
|