@aaqu/fromcubes-react 0.0.0-stage → 0.2.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 +435 -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 +660 -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 +581 -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 +73 -4
@@ -0,0 +1,660 @@
1
+ /* ============================================================
2
+ Portal Code Workspace — sidebar tab + full-screen overlay.
3
+ Palette follows Monaco's vs-dark so the editor pane and the
4
+ surrounding chrome read as one surface.
5
+ ============================================================ */
6
+
7
+ :root {
8
+ --fc-ws-bg: #1e1e1e;
9
+ --fc-ws-panel: #252526;
10
+ --fc-ws-bar: #333333;
11
+ --fc-ws-border: #3c3c3c;
12
+ --fc-ws-fg: #cccccc;
13
+ --fc-ws-fg-dim: #8a8a8a;
14
+ --fc-ws-accent: #0e639c;
15
+ --fc-ws-hover: #2a2d2e;
16
+ --fc-ws-selected: #04395e;
17
+ --fc-ws-dirty: #e2c08d;
18
+ --fc-ws-error: #f14c4c;
19
+ }
20
+
21
+ /* ── Brand font ──────────────────────────────────────────── */
22
+ /* Ark Pixel 10px proportional, latin subset (SIL OFL 1.1, TakWolf) — see
23
+ THIRD-PARTY-NOTICES.md. A 10px design on a 1000-unit em, so it only lands on
24
+ whole pixels at multiples of 10px; anything else blurs the very pixel grid
25
+ the face is drawn on. */
26
+
27
+ @font-face {
28
+ font-family: "Ark Pixel";
29
+ src: url("ark-pixel.woff2") format("woff2");
30
+ font-weight: normal;
31
+ font-style: normal;
32
+ font-display: swap;
33
+ }
34
+
35
+ /* ── Status bar button (bottom bar, one click into the editor) ── */
36
+ /* Node-RED's own .red-ui-footer-button does the pill; only the icon needs us. */
37
+
38
+ /* Node-RED sizes footer buttons for an icon alone; this one carries a label,
39
+ and its height is left to Node-RED so it sits level with its neighbours. */
40
+ .fc-ws-statusbar-btn.red-ui-footer-button,
41
+ button.fc-ws-statusbar-btn.red-ui-footer-button {
42
+ display: inline-flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ gap: 5px;
46
+ width: auto;
47
+ }
48
+
49
+ /* `.red-ui-editor img` sets height:auto and outranks a bare class selector. */
50
+ .red-ui-editor img.fc-ws-statusbar-icon,
51
+ img.fc-ws-statusbar-icon {
52
+ height: 16px;
53
+ width: auto;
54
+ display: block;
55
+ }
56
+
57
+ /* ── Brand lockup (mark + fromcubes-react) ───────────────── */
58
+
59
+ .fc-ws-brand {
60
+ display: inline-flex;
61
+ align-items: center;
62
+ gap: 6px;
63
+ }
64
+
65
+ /* The mark keeps its own black on a white tile rather than being inverted into
66
+ the header — same lockup as everywhere else the logo appears. */
67
+ .red-ui-editor img.fc-ws-mark-logo,
68
+ img.fc-ws-mark-logo {
69
+ height: 22px;
70
+ width: 22px;
71
+ padding: 2px;
72
+ box-sizing: border-box;
73
+ background: #ffffff;
74
+ border-radius: 5px;
75
+ }
76
+
77
+ .fc-ws-brand-text {
78
+ display: inline-flex;
79
+ align-items: baseline;
80
+ /* 20px = two font pixels per screen pixel; sits right in a 38px header. */
81
+ font-family: "Ark Pixel", "Cascadia Code", Consolas, monospace;
82
+ font-size: 20px;
83
+ line-height: 1;
84
+ /* Pixel faces must not be smoothed — that is the whole point of them. */
85
+ -webkit-font-smoothing: none;
86
+ font-smooth: never;
87
+ }
88
+
89
+ .fc-ws-brand-name {
90
+ color: #f2f2f2;
91
+ }
92
+
93
+ .fc-ws-brand-accent {
94
+ color: #f5a623;
95
+ }
96
+
97
+ /* ── Node-RED's own toasts ───────────────────────────────── */
98
+ /* The overlay sits at 2000; #red-ui-notifications sits at 100 inside
99
+ #red-ui-editor, whose z-index is auto — so nothing between them makes a
100
+ stacking context and the toasts are simply buried. That silences Node-RED's
101
+ "Successfully deployed" and every warning this editor raises. Lifted only
102
+ while the overlay is open, so the rest of Node-RED — and other plugins'
103
+ overlays — keep their own stacking. */
104
+ body.fc-ws-open #red-ui-notifications {
105
+ z-index: 2001;
106
+ }
107
+
108
+ /* ── Overlay shell ───────────────────────────────────────── */
109
+
110
+ #fc-ws-overlay {
111
+ position: fixed;
112
+ inset: 0;
113
+ z-index: 2000;
114
+ display: flex;
115
+ flex-direction: column;
116
+ background: var(--fc-ws-bg);
117
+ color: var(--fc-ws-fg);
118
+ font-size: 13px;
119
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
120
+ }
121
+
122
+ #fc-ws-overlay[hidden] {
123
+ display: none;
124
+ }
125
+
126
+ .fc-ws-header {
127
+ display: flex;
128
+ align-items: center;
129
+ gap: 10px;
130
+ height: 38px;
131
+ flex: 0 0 38px;
132
+ padding: 0 10px;
133
+ background: var(--fc-ws-bar);
134
+ border-bottom: 1px solid var(--fc-ws-border);
135
+ }
136
+
137
+ /* Node-RED's own button markup sits on our dark header — Node-RED styles it
138
+ for the light header toolbar, so restate the parts that don't carry over. */
139
+ #fc-ws-overlay .red-ui-button {
140
+ display: inline-flex;
141
+ align-items: center;
142
+ gap: 6px;
143
+ height: 26px;
144
+ padding: 0 10px;
145
+ border: 1px solid var(--fc-ws-border);
146
+ border-radius: 4px;
147
+ background: transparent;
148
+ /* Node-RED sets this colour with !important, for its own light chrome — on
149
+ the dark header #646464 lands at ~1.9:1 against #333 and reads as a
150
+ disabled button, so the override has to shout back. */
151
+ color: var(--fc-ws-fg) !important;
152
+ font-size: 12px;
153
+ line-height: 24px;
154
+ text-decoration: none;
155
+ }
156
+
157
+ #fc-ws-overlay .red-ui-button:hover {
158
+ background: var(--fc-ws-hover);
159
+ border-color: #6e6e6e;
160
+ color: #ffffff !important;
161
+ }
162
+
163
+ #fc-ws-overlay .red-ui-deploy-button {
164
+ display: inline-flex;
165
+ align-items: center;
166
+ height: 26px;
167
+ padding: 0 12px;
168
+ border: none;
169
+ border-radius: 4px;
170
+ background: var(--red-ui-deploy-button-background, #ad1625);
171
+ color: #fff;
172
+ font-size: 12px;
173
+ text-decoration: none;
174
+ }
175
+
176
+ #fc-ws-overlay .red-ui-deploy-button:hover {
177
+ background: var(--red-ui-deploy-button-background-hover, #c62035);
178
+ color: #fff;
179
+ }
180
+
181
+ #fc-ws-overlay .red-ui-deploy-button.disabled {
182
+ opacity: 0.45;
183
+ pointer-events: none;
184
+ }
185
+
186
+ #fc-ws-overlay .red-ui-deploy-button-content {
187
+ display: inline-flex;
188
+ align-items: center;
189
+ gap: 6px;
190
+ }
191
+
192
+ #fc-ws-overlay .red-ui-deploy-button img {
193
+ height: 14px;
194
+ width: auto;
195
+ }
196
+
197
+ /* Icon-only, so it needs no text padding. */
198
+ #fc-ws-overlay .fc-ws-open-portal {
199
+ padding: 0 8px;
200
+ }
201
+
202
+ #fc-ws-overlay .fc-ws-kbd {
203
+ opacity: 0.55;
204
+ font-size: 10px;
205
+ margin-left: 3px;
206
+ }
207
+
208
+ .fc-ws-status {
209
+ color: var(--fc-ws-fg-dim);
210
+ font-size: 12px;
211
+ }
212
+
213
+ .fc-ws-status.fc-ws-has-changes {
214
+ color: var(--fc-ws-dirty);
215
+ }
216
+
217
+ .fc-ws-spacer {
218
+ flex: 1;
219
+ }
220
+
221
+ .fc-ws-btn {
222
+ background: transparent;
223
+ color: var(--fc-ws-fg);
224
+ border: 1px solid var(--fc-ws-border);
225
+ border-radius: 3px;
226
+ padding: 3px 10px;
227
+ cursor: pointer;
228
+ font-size: 12px;
229
+ }
230
+
231
+ .fc-ws-btn:hover {
232
+ background: var(--fc-ws-hover);
233
+ }
234
+
235
+ .fc-ws-btn-primary {
236
+ background: var(--fc-ws-accent);
237
+ border-color: var(--fc-ws-accent);
238
+ color: #fff;
239
+ }
240
+
241
+ .fc-ws-btn-primary:hover {
242
+ background: #1177bb;
243
+ }
244
+
245
+ .fc-ws-btn-primary:disabled {
246
+ opacity: 0.45;
247
+ cursor: default;
248
+ }
249
+
250
+ .fc-ws-body {
251
+ flex: 1;
252
+ display: flex;
253
+ min-height: 0;
254
+ }
255
+
256
+ /* ── Tree pane ───────────────────────────────────────────── */
257
+
258
+ /* The tree pane hosts the settings popover. */
259
+ .fc-ws-tree-pane {
260
+ position: relative;
261
+ width: 260px;
262
+ flex: 0 0 auto;
263
+ display: flex;
264
+ flex-direction: column;
265
+ min-width: 150px;
266
+ background: var(--fc-ws-panel);
267
+ border-right: 1px solid var(--fc-ws-border);
268
+ }
269
+
270
+ .fc-ws-filter-row {
271
+ padding: 6px;
272
+ border-bottom: 1px solid var(--fc-ws-border);
273
+ display: flex;
274
+ gap: 6px;
275
+ align-items: center;
276
+ }
277
+
278
+ .fc-ws-filter {
279
+ flex: 1;
280
+ min-width: 0;
281
+ background: #3c3c3c;
282
+ border: 1px solid #3c3c3c;
283
+ color: var(--fc-ws-fg);
284
+ border-radius: 2px;
285
+ padding: 3px 6px;
286
+ font-size: 12px;
287
+ }
288
+
289
+ .fc-ws-filter:focus {
290
+ outline: 1px solid var(--fc-ws-accent);
291
+ }
292
+
293
+ .fc-ws-toggle {
294
+ color: var(--fc-ws-fg-dim);
295
+ cursor: pointer;
296
+ padding: 2px 4px;
297
+ border-radius: 2px;
298
+ }
299
+
300
+ .fc-ws-toggle:hover {
301
+ color: var(--fc-ws-fg);
302
+ background: var(--fc-ws-hover);
303
+ }
304
+
305
+ .fc-ws-toggle.fc-ws-on {
306
+ color: #4ec9b0;
307
+ background: var(--fc-ws-hover);
308
+ }
309
+
310
+ /* ── Settings popover ────────────────────────────────────── */
311
+
312
+ .fc-ws-settings {
313
+ position: absolute;
314
+ top: 36px;
315
+ right: 6px;
316
+ z-index: 5;
317
+ min-width: 190px;
318
+ padding: 6px 0;
319
+ background: var(--fc-ws-bar);
320
+ border: 1px solid var(--fc-ws-border);
321
+ border-radius: 4px;
322
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
323
+ }
324
+
325
+ .fc-ws-settings-title {
326
+ padding: 4px 10px 2px;
327
+ font-size: 10px;
328
+ letter-spacing: 0.08em;
329
+ text-transform: uppercase;
330
+ color: var(--fc-ws-fg-dim);
331
+ }
332
+
333
+ .fc-ws-settings-title:not(:first-child) {
334
+ margin-top: 4px;
335
+ border-top: 1px solid var(--fc-ws-border);
336
+ padding-top: 8px;
337
+ }
338
+
339
+ .fc-ws-settings-row {
340
+ display: flex;
341
+ align-items: center;
342
+ gap: 7px;
343
+ padding: 3px 10px;
344
+ cursor: pointer;
345
+ font-size: 12px;
346
+ color: var(--fc-ws-fg);
347
+ margin: 0;
348
+ }
349
+
350
+ .fc-ws-settings-row:hover {
351
+ background: var(--fc-ws-hover);
352
+ }
353
+
354
+ .fc-ws-settings-row input {
355
+ margin: 0;
356
+ accent-color: #4ec9b0;
357
+ cursor: pointer;
358
+ }
359
+
360
+ .fc-ws-tree {
361
+ flex: 1;
362
+ overflow: auto;
363
+ padding: 4px 0 12px;
364
+ user-select: none;
365
+ }
366
+
367
+ .fc-ws-row {
368
+ display: flex;
369
+ align-items: center;
370
+ gap: 5px;
371
+ padding: 2px 8px;
372
+ cursor: pointer;
373
+ white-space: nowrap;
374
+ line-height: 20px;
375
+ }
376
+
377
+ .fc-ws-row:hover {
378
+ background: var(--fc-ws-hover);
379
+ }
380
+
381
+ .fc-ws-row.fc-ws-active {
382
+ background: var(--fc-ws-selected);
383
+ }
384
+
385
+ /* ── File icons (Lucide sprite) ──────────────────────────── */
386
+ /* Same shapes as the node badges on the canvas, so a portal reads as a portal
387
+ in both places. Colour marks the file's role — never its state: amber and
388
+ red already mean "changed" and "syntax error" on the markers to the right. */
389
+
390
+ .fc-ws-row .fc-ws-icon {
391
+ width: 14px;
392
+ height: 14px;
393
+ flex: 0 0 auto;
394
+ color: var(--fc-ws-fg-dim);
395
+ /* Lucide is drawn for 24px; at 14 the hairline needs a touch more weight.
396
+ CSS wins over the sprite's stroke-width presentation attribute. */
397
+ stroke-width: 2.25;
398
+ }
399
+
400
+ /* Scoped to the row so these beat the base `.fc-ws-row .fc-ws-icon` colour. */
401
+ .fc-ws-row .fc-ws-icon--portal {
402
+ color: #61dafb;
403
+ }
404
+
405
+ .fc-ws-row .fc-ws-icon--component {
406
+ color: #c586c0;
407
+ }
408
+
409
+ .fc-ws-row .fc-ws-icon--utility {
410
+ color: #4ec9b0;
411
+ }
412
+
413
+ .fc-ws-row .fc-ws-icon--head {
414
+ color: #e34c26;
415
+ }
416
+
417
+ /* Disabled in Node-RED — the node itself, or the flow tab carrying it. */
418
+ .fc-ws-row.fc-ws-off .fc-ws-name {
419
+ opacity: 0.45;
420
+ }
421
+
422
+ .fc-ws-row.fc-ws-off .fc-ws-icon:not(.fc-ws-off-mark) {
423
+ opacity: 0.4;
424
+ }
425
+
426
+ .fc-ws-row .fc-ws-off-mark {
427
+ margin-left: auto;
428
+ padding-left: 6px;
429
+ width: 11px;
430
+ height: 11px;
431
+ color: var(--fc-ws-fg-dim);
432
+ opacity: 0.8;
433
+ }
434
+
435
+ /* The portal twisty sits ahead of the file icon and takes its own clicks. */
436
+ .fc-ws-row .fc-ws-twisty {
437
+ width: 10px;
438
+ height: 10px;
439
+ margin-right: -1px;
440
+ font-size: 11px;
441
+ opacity: 0.7;
442
+ stroke-width: 3;
443
+ }
444
+
445
+ .fc-ws-row .fc-ws-twisty:hover {
446
+ opacity: 1;
447
+ color: var(--fc-ws-fg);
448
+ }
449
+
450
+ .fc-ws-row i {
451
+ width: 13px;
452
+ text-align: center;
453
+ font-size: 12px;
454
+ color: var(--fc-ws-fg-dim);
455
+ flex: 0 0 auto;
456
+ }
457
+
458
+ .fc-ws-row .fc-ws-name {
459
+ overflow: hidden;
460
+ text-overflow: ellipsis;
461
+ }
462
+
463
+ .fc-ws-row.fc-ws-dir > .fc-ws-name {
464
+ font-weight: 600;
465
+ color: #d7d7d7;
466
+ }
467
+
468
+ .fc-ws-mark {
469
+ margin-left: auto;
470
+ padding-left: 6px;
471
+ font-size: 10px;
472
+ flex: 0 0 auto;
473
+ }
474
+
475
+ .fc-ws-mark.fc-ws-dirty {
476
+ color: var(--fc-ws-dirty);
477
+ }
478
+
479
+ .fc-ws-mark.fc-ws-error {
480
+ color: var(--fc-ws-error);
481
+ }
482
+
483
+ .fc-ws-empty {
484
+ padding: 12px;
485
+ color: var(--fc-ws-fg-dim);
486
+ font-size: 12px;
487
+ line-height: 1.5;
488
+ }
489
+
490
+ .fc-ws-splitter {
491
+ flex: 0 0 4px;
492
+ cursor: col-resize;
493
+ background: var(--fc-ws-border);
494
+ }
495
+
496
+ .fc-ws-splitter:hover {
497
+ background: var(--fc-ws-accent);
498
+ }
499
+
500
+ /* ── Scrollbars ──────────────────────────────────────────── */
501
+ /* Node-RED's editor is light; these panes are not, and the platform scrollbar
502
+ would land as a bright slab in the middle of them. */
503
+
504
+ #fc-ws-overlay * {
505
+ scrollbar-width: thin;
506
+ scrollbar-color: #4b4b4b transparent;
507
+ }
508
+
509
+ #fc-ws-overlay ::-webkit-scrollbar {
510
+ width: 11px;
511
+ height: 11px;
512
+ }
513
+
514
+ #fc-ws-overlay ::-webkit-scrollbar-track {
515
+ background: transparent;
516
+ }
517
+
518
+ #fc-ws-overlay ::-webkit-scrollbar-thumb {
519
+ background: #4b4b4b;
520
+ border-radius: 6px;
521
+ /* Transparent border keeps the thumb off the pane edge without a track. */
522
+ border: 3px solid transparent;
523
+ background-clip: padding-box;
524
+ }
525
+
526
+ #fc-ws-overlay ::-webkit-scrollbar-thumb:hover {
527
+ background: #5f5f5f;
528
+ background-clip: padding-box;
529
+ }
530
+
531
+ #fc-ws-overlay ::-webkit-scrollbar-corner {
532
+ background: transparent;
533
+ }
534
+
535
+ /* ── Editor pane ─────────────────────────────────────────── */
536
+
537
+ .fc-ws-editor-pane {
538
+ flex: 1;
539
+ min-width: 0;
540
+ display: flex;
541
+ flex-direction: column;
542
+ }
543
+
544
+ .fc-ws-tabs {
545
+ display: flex;
546
+ flex: 0 0 auto;
547
+ overflow-x: auto;
548
+ background: var(--fc-ws-panel);
549
+ border-bottom: 1px solid var(--fc-ws-border);
550
+ }
551
+
552
+ .fc-ws-tab {
553
+ display: flex;
554
+ align-items: center;
555
+ gap: 6px;
556
+ padding: 6px 8px 6px 12px;
557
+ border-right: 1px solid var(--fc-ws-border);
558
+ cursor: pointer;
559
+ white-space: nowrap;
560
+ color: var(--fc-ws-fg-dim);
561
+ font-size: 12px;
562
+ }
563
+
564
+ .fc-ws-tab.fc-ws-active {
565
+ background: var(--fc-ws-bg);
566
+ color: #fff;
567
+ box-shadow: inset 0 1px 0 0 var(--fc-ws-accent);
568
+ }
569
+
570
+ .fc-ws-tab .fc-ws-close {
571
+ width: 14px;
572
+ text-align: center;
573
+ border-radius: 2px;
574
+ opacity: 0.6;
575
+ }
576
+
577
+ .fc-ws-tab .fc-ws-close:hover {
578
+ background: #4b4b4b;
579
+ opacity: 1;
580
+ }
581
+
582
+ .fc-ws-tab.fc-ws-tab-dirty .fc-ws-close::before {
583
+ content: "\25CF";
584
+ color: var(--fc-ws-dirty);
585
+ }
586
+
587
+ .fc-ws-tab.fc-ws-tab-dirty:hover .fc-ws-close::before {
588
+ content: "\00D7";
589
+ color: inherit;
590
+ }
591
+
592
+ .fc-ws-monaco {
593
+ flex: 1;
594
+ min-height: 0;
595
+ }
596
+
597
+ .fc-ws-fallback {
598
+ flex: 1;
599
+ min-height: 0;
600
+ width: 100%;
601
+ box-sizing: border-box;
602
+ background: var(--fc-ws-bg);
603
+ color: var(--fc-ws-fg);
604
+ border: 0;
605
+ padding: 8px;
606
+ font-family: "Cascadia Code", "Fira Code", Consolas, monospace;
607
+ font-size: 13px;
608
+ resize: none;
609
+ }
610
+
611
+ .fc-ws-placeholder {
612
+ flex: 1;
613
+ display: flex;
614
+ align-items: center;
615
+ justify-content: center;
616
+ color: var(--fc-ws-fg-dim);
617
+ font-size: 13px;
618
+ text-align: center;
619
+ line-height: 1.7;
620
+ }
621
+
622
+ .fc-ws-placeholder kbd {
623
+ background: var(--fc-ws-bar);
624
+ border: 1px solid var(--fc-ws-border);
625
+ border-radius: 3px;
626
+ padding: 1px 5px;
627
+ font-size: 11px;
628
+ }
629
+
630
+ /* ── Settings → Fromcubes (provider-settings.js) ─────────────────
631
+ Lives in Node-RED's own Settings tray, not in the overlay, so it uses
632
+ Node-RED's theme variables instead of the vs-dark palette above. */
633
+ /* Same box as Node-RED's own View pane (#red-ui-settings-tab-view): fills
634
+ the content area, scrolls on its own, 20px side padding. */
635
+ .fc-ps {
636
+ position: absolute; top: 0; right: 0; bottom: 0; left: 0;
637
+ padding: 8px 20px 20px;
638
+ overflow-y: auto;
639
+ }
640
+ .fc-ps .fc-ps-section { margin-bottom: 24px; }
641
+ .fc-ps .fc-ps-hint { color: var(--red-ui-secondary-text-color); margin: 0 0 8px; }
642
+ .fc-ps .fc-ps-note,
643
+ .fc-ps .fc-ps-warn { margin: 0 0 8px; }
644
+ .fc-ps .fc-ps-warn { color: var(--red-ui-text-color-error); }
645
+ .fc-ps .fc-ps-row { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 6px; }
646
+ .fc-ps .fc-ps-row input[type="radio"] { margin: 3px 0 0; flex: none; width: auto; }
647
+ .fc-ps .fc-ps-row label { width: auto; margin: 0; cursor: pointer; }
648
+ .fc-ps .fc-ps-row input:disabled + label { cursor: default; opacity: 0.6; }
649
+ .fc-ps .fc-ps-id { font-family: var(--red-ui-monospace-font); font-weight: bold; }
650
+ .fc-ps .fc-ps-module {
651
+ margin-left: 8px;
652
+ padding: 0 5px;
653
+ border: 1px solid var(--red-ui-secondary-border-color);
654
+ border-radius: 3px;
655
+ font-size: 11px;
656
+ color: var(--red-ui-secondary-text-color);
657
+ }
658
+ .fc-ps .fc-ps-label,
659
+ .fc-ps .fc-ps-problem { display: block; font-size: 12px; color: var(--red-ui-secondary-text-color); }
660
+ .fc-ps .fc-ps-problem { color: var(--red-ui-text-color-error); }