@aaqu/fromcubes-react 0.2.1 → 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/lib/admin-api.js +31 -1
- package/nodes/lib/providers.js +272 -30
- package/nodes/portal-react.html +494 -254
- package/package.json +1 -2
package/nodes/portal-react.html
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
link.rel = "stylesheet";
|
|
15
15
|
link.href = base + "editor/workspace.css";
|
|
16
16
|
document.head.appendChild(link);
|
|
17
|
-
["file-tree.js", "workspace.js", "format.js", "provider-settings.js"].forEach(function (file) {
|
|
17
|
+
["file-tree.js", "class-completion.js", "workspace.js", "format.js", "provider-settings.js"].forEach(function (file) {
|
|
18
18
|
const s = document.createElement("script");
|
|
19
19
|
s.src = base + "editor/" + file;
|
|
20
20
|
s.async = false;
|
|
@@ -44,12 +44,13 @@
|
|
|
44
44
|
quickSuggestions: { strings: true },
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
// className="…" completion
|
|
48
|
-
// 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
|
|
49
50
|
// after the engine changes — the engine can change while the editor is
|
|
50
51
|
// open, so "load once" would keep offering the old engine's classes.
|
|
51
52
|
window.__fcCssClasses = null;
|
|
52
|
-
window.
|
|
53
|
+
window.__fcCssData = null;
|
|
53
54
|
window.__fcCssEngine = null;
|
|
54
55
|
let cssClassesPending = false;
|
|
55
56
|
window.__fcRefreshCssClasses = function () {
|
|
@@ -57,9 +58,26 @@
|
|
|
57
58
|
cssClassesPending = true;
|
|
58
59
|
$.getJSON("fromcubes-react/css-classes", function (data) {
|
|
59
60
|
if (!data || !Array.isArray(data.classes)) return;
|
|
61
|
+
const prev = window.__fcCssData;
|
|
60
62
|
window.__fcCssEngine = data.engine;
|
|
61
63
|
window.__fcCssClasses = data.classes;
|
|
62
|
-
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();
|
|
63
81
|
})
|
|
64
82
|
.fail(function (xhr) {
|
|
65
83
|
console.error("[FC-Monaco] css-classes FAILED:", xhr.status, xhr.statusText);
|
|
@@ -117,102 +135,398 @@
|
|
|
117
135
|
console.log(PREFIX, "addExtraLib globals.d.ts");
|
|
118
136
|
jsDef.addExtraLib(libContent, "file:///globals.d.ts");
|
|
119
137
|
|
|
120
|
-
// className completion
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
};
|
|
129
151
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
}
|
|
134
158
|
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
}
|
|
137
165
|
|
|
138
|
-
|
|
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
|
+
}
|
|
139
172
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
173
|
+
const cssInflight = new WeakMap(); // answer cache → candidate → pending request
|
|
174
|
+
|
|
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;
|
|
147
192
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
// If typing the very first word, accept if it looks like a prefix
|
|
171
|
-
if (!hasTwToken && parts.length === 1 && word.length >= 2) {
|
|
172
|
-
for (let j = 0; j < classes.length; j++) {
|
|
173
|
-
if (classes[j].indexOf(word) === 0) {
|
|
174
|
-
hasTwToken = true;
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
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
|
+
});
|
|
177
212
|
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
+
}
|
|
182
235
|
|
|
183
|
-
|
|
184
|
-
|
|
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,
|
|
185
257
|
endLineNumber: position.lineNumber,
|
|
186
|
-
startColumn: position.column - word.length,
|
|
187
258
|
endColumn: position.column,
|
|
188
|
-
}
|
|
259
|
+
}),
|
|
260
|
+
lineBefore: line.substring(0, position.column - 1),
|
|
261
|
+
lineAfter: line.substring(position.column - 1),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
189
264
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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;
|
|
201
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;
|
|
202
355
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
);
|
|
215
|
-
|
|
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
|
+
});
|
|
216
530
|
},
|
|
217
531
|
});
|
|
218
532
|
|
|
@@ -1328,32 +1642,26 @@
|
|
|
1328
1642
|
</script>
|
|
1329
1643
|
|
|
1330
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>
|
|
1331
1648
|
<p>
|
|
1332
|
-
|
|
1333
|
-
<
|
|
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>.
|
|
1334
1653
|
</p>
|
|
1654
|
+
<pre>function Badge({ label }) {
|
|
1655
|
+
return <span className="badge">{label}</span>;
|
|
1656
|
+
}</pre>
|
|
1657
|
+
|
|
1335
1658
|
<h3>Properties</h3>
|
|
1336
1659
|
<dl>
|
|
1337
1660
|
<dt>JSX Tag</dt>
|
|
1338
|
-
<dd>
|
|
1339
|
-
The name used as a JSX tag in portal-react pages (e.g.
|
|
1340
|
-
<code><MyComponent /></code>).
|
|
1341
|
-
</dd>
|
|
1342
|
-
<dt>Input fields</dt>
|
|
1343
|
-
<dd>Comma-separated list of expected input props (documentation only).</dd>
|
|
1344
|
-
<dt>Output fields</dt>
|
|
1345
|
-
<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>
|
|
1346
1662
|
<dt>JSX Code</dt>
|
|
1347
|
-
<dd>
|
|
1348
|
-
The component function. Use <code>useNodeRed()</code> for data binding.
|
|
1349
|
-
</dd>
|
|
1663
|
+
<dd>The component function.</dd>
|
|
1350
1664
|
</dl>
|
|
1351
|
-
<h3>Usage</h3>
|
|
1352
|
-
<p>
|
|
1353
|
-
Place this node on the canvas, set the JSX tag name and code, then deploy.
|
|
1354
|
-
All <strong>portal-react</strong> nodes will automatically have access to
|
|
1355
|
-
this component.
|
|
1356
|
-
</p>
|
|
1357
1665
|
</script>
|
|
1358
1666
|
|
|
1359
1667
|
<!-- ============================================================
|
|
@@ -1574,57 +1882,28 @@
|
|
|
1574
1882
|
</script>
|
|
1575
1883
|
|
|
1576
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>
|
|
1577
1888
|
<p>
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
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.
|
|
1582
1892
|
</p>
|
|
1893
|
+
<pre>function clamp(n, min, max) {
|
|
1894
|
+
return Math.max(min, Math.min(max, n));
|
|
1895
|
+
}</pre>
|
|
1896
|
+
|
|
1583
1897
|
<h3>Properties</h3>
|
|
1584
1898
|
<dl>
|
|
1585
1899
|
<dt>Module name</dt>
|
|
1586
1900
|
<dd>
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
components and utilities.
|
|
1901
|
+
A name for this node; it must not repeat a component's or another
|
|
1902
|
+
utility's name.
|
|
1590
1903
|
</dd>
|
|
1591
1904
|
<dt>Code</dt>
|
|
1592
|
-
<dd>
|
|
1593
|
-
Top-level JavaScript: <code>const</code>, <code>let</code>,
|
|
1594
|
-
<code>function</code>, <code>class</code> declarations. <code>import</code>
|
|
1595
|
-
statements at the top are auto-hoisted into the bundle.
|
|
1596
|
-
</dd>
|
|
1905
|
+
<dd>Top-level declarations; <code>import</code> lines are allowed.</dd>
|
|
1597
1906
|
</dl>
|
|
1598
|
-
<h3>Selective inclusion</h3>
|
|
1599
|
-
<p>
|
|
1600
|
-
A utility node is bundled into a portal only when the portal's JSX (or any
|
|
1601
|
-
of its referenced library components) mentions at least one of the symbols
|
|
1602
|
-
declared in the utility. Unused utility nodes are skipped.
|
|
1603
|
-
</p>
|
|
1604
|
-
<p>
|
|
1605
|
-
Group related symbols in one node — referencing any one symbol pulls in the
|
|
1606
|
-
whole node's code. Split unrelated helpers across multiple nodes for finer
|
|
1607
|
-
granularity.
|
|
1608
|
-
</p>
|
|
1609
|
-
<h3>Example</h3>
|
|
1610
|
-
<pre>function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); }
|
|
1611
|
-
|
|
1612
|
-
function useDebounce(value, ms = 300) {
|
|
1613
|
-
const [v, setV] = React.useState(value);
|
|
1614
|
-
React.useEffect(() => {
|
|
1615
|
-
const t = setTimeout(() => setV(value), ms);
|
|
1616
|
-
return () => clearTimeout(t);
|
|
1617
|
-
}, [value, ms]);
|
|
1618
|
-
return v;
|
|
1619
|
-
}</pre>
|
|
1620
|
-
<p>
|
|
1621
|
-
Then in any <strong>portal-react</strong> node:
|
|
1622
|
-
</p>
|
|
1623
|
-
<pre>function App() {
|
|
1624
|
-
const { data } = useNodeRed();
|
|
1625
|
-
const slow = useDebounce(data?.value);
|
|
1626
|
-
return <div>{clamp(slow ?? 0, 0, 100)}</div>;
|
|
1627
|
-
}</pre>
|
|
1628
1907
|
</script>
|
|
1629
1908
|
|
|
1630
1909
|
<!-- ============================================================
|
|
@@ -2431,125 +2710,86 @@ function useDebounce(value, ms = 300) {
|
|
|
2431
2710
|
Help
|
|
2432
2711
|
============================================================ -->
|
|
2433
2712
|
<script type="text/html" data-help-name="portal-react">
|
|
2434
|
-
<h3>Quick Reference</h3>
|
|
2435
|
-
<ul>
|
|
2436
|
-
<li><code>useNodeRed()</code> → <code>{ data, send, user, portalClient }</code></li>
|
|
2437
|
-
<li>Components from <code>fc-portal-component</code> nodes are auto-imported</li>
|
|
2438
|
-
<li>Must export <code><App /></code></li>
|
|
2439
|
-
<li>JSX is transpiled server-side at deploy</li>
|
|
2440
|
-
</ul>
|
|
2441
|
-
|
|
2442
2713
|
<p>
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
time</strong> using esbuild — browsers receive pre-compiled JS with zero
|
|
2446
|
-
runtime compilation. Tailwind CSS 4 utility classes are generated
|
|
2447
|
-
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.
|
|
2448
2716
|
</p>
|
|
2449
2717
|
|
|
2450
2718
|
<h3>Inputs</h3>
|
|
2451
|
-
<
|
|
2452
|
-
<
|
|
2453
|
-
to
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
>
|
|
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>
|
|
2462
2730
|
|
|
2463
2731
|
<h3>Outputs</h3>
|
|
2464
|
-
<
|
|
2465
|
-
<
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
<p>
|
|
2471
|
-
Add npm packages in the <strong>Modules</strong> list (e.g.
|
|
2472
|
-
<code>chart.js/auto@^4.4.0</code>, <code>d3</code>, <code>three</code>).
|
|
2473
|
-
Node-RED auto-installs them at deploy time. All packages are bundled with
|
|
2474
|
-
React into a single vendor IIFE via esbuild, cached by hash of installed
|
|
2475
|
-
versions. Use them in JSX via standard imports.
|
|
2476
|
-
</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>
|
|
2477
2738
|
|
|
2478
|
-
<h3>
|
|
2479
|
-
<ul>
|
|
2480
|
-
<li>
|
|
2481
|
-
Each deploy re-transpiles JSX via esbuild and rebuilds Tailwind CSS
|
|
2482
|
-
(both cached by content hash — unchanged code is instant)
|
|
2483
|
-
</li>
|
|
2484
|
-
<li>
|
|
2485
|
-
Vendor bundle (React + npm packages) rebuilt only when package versions
|
|
2486
|
-
change
|
|
2487
|
-
</li>
|
|
2488
|
-
<li>
|
|
2489
|
-
Active WebSocket clients receive close code 1001 and auto-reconnect
|
|
2490
|
-
(exponential backoff)
|
|
2491
|
-
</li>
|
|
2492
|
-
<li>
|
|
2493
|
-
Stale HTTP routes and WS handlers are cleaned up before new ones register
|
|
2494
|
-
</li>
|
|
2495
|
-
<li>
|
|
2496
|
-
Transpile errors show in node status and as an error page on the endpoint
|
|
2497
|
-
</li>
|
|
2498
|
-
</ul>
|
|
2499
|
-
|
|
2500
|
-
<h3>Shared Components</h3>
|
|
2739
|
+
<h3>Details</h3>
|
|
2501
2740
|
<p>
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
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.
|
|
2505
2743
|
</p>
|
|
2506
|
-
|
|
2507
|
-
|
|
2744
|
+
<pre>function App() {
|
|
2745
|
+
const { data, send } = useNodeRed();
|
|
2746
|
+
return <button onClick={() => send("hi")}>{data}</button>;
|
|
2747
|
+
}</pre>
|
|
2508
2748
|
<p>
|
|
2509
|
-
|
|
2510
|
-
|
|
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).
|
|
2511
2753
|
</p>
|
|
2512
|
-
<ul>
|
|
2513
|
-
<li><code>useNodeRed()</code> returns <code>{ data, send, user, portalClient }</code></li>
|
|
2514
|
-
<li>
|
|
2515
|
-
All WebSocket messages include <code>msg._client</code> with
|
|
2516
|
-
<code>{ portalClient, ...userFields }</code>
|
|
2517
|
-
</li>
|
|
2518
|
-
<li>
|
|
2519
|
-
To target a specific tab: keep <code>msg._client</code> (or set
|
|
2520
|
-
<code>msg._client = { portalClient: "..." }</code>)
|
|
2521
|
-
</li>
|
|
2522
|
-
<li>
|
|
2523
|
-
To target all tabs of a user: set
|
|
2524
|
-
<code>msg._client = { userId: "..." }</code> (no portalClient)
|
|
2525
|
-
</li>
|
|
2526
|
-
<li>
|
|
2527
|
-
To target every authenticated session (skips anonymous):
|
|
2528
|
-
<code>msg._client = { authenticated: true }</code>
|
|
2529
|
-
</li>
|
|
2530
|
-
<li>
|
|
2531
|
-
To broadcast to all: remove <code>msg._client</code>. With
|
|
2532
|
-
<b>Authenticated-only delivery</b> enabled this defaults to the
|
|
2533
|
-
authenticated sessions instead; force a true broadcast with
|
|
2534
|
-
<code>msg._client = { authenticated: false }</code>
|
|
2535
|
-
</li>
|
|
2536
|
-
</ul>
|
|
2537
|
-
|
|
2538
|
-
<h3>Custom Head HTML</h3>
|
|
2539
2754
|
<p>
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
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.
|
|
2543
2757
|
</p>
|
|
2544
|
-
<pre>
|
|
2545
|
-
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet"></pre
|
|
2546
|
-
>
|
|
2547
2758
|
|
|
2548
|
-
<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>
|
|
2549
2788
|
<p>
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
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.
|
|
2553
2793
|
</p>
|
|
2554
2794
|
</script>
|
|
2555
2795
|
|