@cyprnet/node-red-contrib-uibuilder-formgen 0.5.36 → 0.5.37

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/CHANGELOG.md CHANGED
@@ -80,6 +80,10 @@ All notable changes to this package will be documented in this file.
80
80
 
81
81
  - Checkbox defaults: Vue 2 portals now normalize checkbox <code>defaultValue</code> (accepts <code>true</code>/<code>false</code>, <code>1</code>/<code>0</code>, and common strings). Fixes cases where a checkbox appears checked but <code>showIf</code> dependent fields do not render until toggled.
82
82
 
83
+ ## 0.5.37
84
+
85
+ - Portal UX: added a live Theme selector (Auto/Light/Dark) in the portal header. Selection applies immediately and is persisted in <code>localStorage</code> (<code>portalsmith:theme</code>) for both Vue 2 and Vue 3 portals (including legacy uib2).
86
+
83
87
  ## 0.5.21
84
88
 
85
89
  - Legacy uibuilder 2.x: fixed vendor library paths (<code>../vendor/...</code>) in the uib2 portal template and uib2 Schema Builder so uibuilder comms work and <code>lookup:get</code> messages emit correctly.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyprnet/node-red-contrib-uibuilder-formgen",
3
- "version": "0.5.36",
3
+ "version": "0.5.37",
4
4
  "description": "PortalSmith: Generate schema-driven uibuilder form portals from JSON",
5
5
  "keywords": [
6
6
  "node-red",
@@ -148,6 +148,22 @@
148
148
  gap: 1rem;
149
149
  margin-bottom: 0.5rem;
150
150
  }
151
+ .ps-header-right {
152
+ display: flex;
153
+ align-items: center;
154
+ justify-content: flex-end;
155
+ gap: 0.75rem;
156
+ flex-wrap: wrap;
157
+ }
158
+ .ps-theme-label {
159
+ font-size: 12px;
160
+ color: var(--ps-muted);
161
+ margin: 0;
162
+ }
163
+ .ps-theme-select {
164
+ width: auto;
165
+ min-width: 120px;
166
+ }
151
167
  .ps-logo {
152
168
  max-height: 48px;
153
169
  max-width: 220px;
@@ -229,18 +245,29 @@
229
245
  <div class="container form-container">
230
246
  <div class="ps-header" style="--ps-logo-max-height: [[logoMaxHeightPx]]px; --ps-logo-max-width: [[logoMaxWidthPx]]px;">
231
247
  <h1 class="mb-0">[[title]]</h1>
232
- [[#logoUrl]]
233
- <img class="ps-custom-logo" src="[[logoUrl]]" alt="[[logoAlt]]">
234
- [[/logoUrl]]
235
- [[^logoUrl]]
236
- [[^licensed]]
237
- <svg class="ps-logo" viewBox="0 0 420 96" role="img" aria-label="PortalSmith">
238
- <rect x="0" y="0" width="420" height="96" rx="14" fill="var(--ps-surface)" stroke="var(--ps-border)"/>
239
- <text x="24" y="58" font-size="34" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-text)">PortalSmith</text>
240
- <text x="24" y="78" font-size="14" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-muted)">FormGen</text>
241
- </svg>
242
- [[/licensed]]
243
- [[/logoUrl]]
248
+ <div class="ps-header-right">
249
+ <div>
250
+ <div class="ps-theme-label">Theme</div>
251
+ <select class="custom-select custom-select-sm ps-theme-select" v-model="themeMode" @change="setTheme(themeMode)">
252
+ <option value="auto">Auto</option>
253
+ <option value="light">Light</option>
254
+ <option value="dark">Dark</option>
255
+ </select>
256
+ </div>
257
+
258
+ [[#logoUrl]]
259
+ <img class="ps-custom-logo" src="[[logoUrl]]" alt="[[logoAlt]]">
260
+ [[/logoUrl]]
261
+ [[^logoUrl]]
262
+ [[^licensed]]
263
+ <svg class="ps-logo" viewBox="0 0 420 96" role="img" aria-label="PortalSmith">
264
+ <rect x="0" y="0" width="420" height="96" rx="14" fill="var(--ps-surface)" stroke="var(--ps-border)"/>
265
+ <text x="24" y="58" font-size="34" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-text)">PortalSmith</text>
266
+ <text x="24" y="78" font-size="14" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-muted)">FormGen</text>
267
+ </svg>
268
+ [[/licensed]]
269
+ [[/logoUrl]]
270
+ </div>
244
271
  </div>
245
272
  <p v-if="description" class="text-muted mb-4">[[description]]</p>
246
273
 
@@ -495,18 +495,31 @@
495
495
  .replace(/'/g, '&#039;');
496
496
  }
497
497
 
498
- function applyTheme() {
498
+ function normalizeThemeMode(mode, fallback) {
499
+ const m = String(mode || '').trim().toLowerCase();
500
+ if (m === 'light' || m === 'dark' || m === 'auto') return m;
501
+ const fb = String(fallback || '').trim().toLowerCase();
502
+ return (fb === 'light' || fb === 'dark' || fb === 'auto') ? fb : 'auto';
503
+ }
504
+
505
+ function getThemeMode() {
499
506
  try {
500
- // Optional override via localStorage (lets you tweak without regenerating)
501
507
  const stored = localStorage.getItem('portalsmith:theme');
502
- const desired = (stored && stored.trim()) ? stored.trim().toLowerCase() : (CONFIG.themeMode || 'auto');
508
+ if (stored && stored.trim()) return normalizeThemeMode(stored, CONFIG.themeMode);
509
+ } catch (e) { /* ignore */ }
510
+ return normalizeThemeMode(CONFIG.themeMode, 'auto');
511
+ }
503
512
 
504
- if (desired === 'light' || desired === 'dark' || desired === 'auto') {
505
- document.documentElement.setAttribute('data-theme', desired);
506
- }
507
- } catch (e) {
508
- // ignore (storage blocked, etc.)
509
- }
513
+ function applyTheme(mode) {
514
+ const desired = normalizeThemeMode(mode, getThemeMode());
515
+ document.documentElement.setAttribute('data-theme', desired);
516
+ return desired;
517
+ }
518
+
519
+ function setThemeMode(mode) {
520
+ const desired = normalizeThemeMode(mode, getThemeMode());
521
+ try { localStorage.setItem('portalsmith:theme', desired); } catch (e) { /* ignore */ }
522
+ return applyTheme(desired);
510
523
  }
511
524
 
512
525
  // Vue app instance
@@ -778,7 +791,8 @@
778
791
  resultSortKey: '',
779
792
  resultSortDir: 'asc',
780
793
  copyBlockActions: schema.actions ? schema.actions.filter(a => a.type === 'copyBlock') : [],
781
- exportFormats: CONFIG.exportFormats
794
+ exportFormats: CONFIG.exportFormats,
795
+ themeMode: getThemeMode(),
782
796
  },
783
797
  computed: {
784
798
  description() {
@@ -965,6 +979,9 @@
965
979
  }
966
980
  },
967
981
  methods: {
982
+ setTheme(mode) {
983
+ this.themeMode = setThemeMode(mode);
984
+ },
968
985
  toggleResultSort(key) {
969
986
  const k = String(key || '').trim();
970
987
  if (!k) return;
@@ -146,6 +146,22 @@
146
146
  gap: 1rem;
147
147
  margin-bottom: 0.5rem;
148
148
  }
149
+ .ps-header-right {
150
+ display: flex;
151
+ align-items: center;
152
+ justify-content: flex-end;
153
+ gap: 0.75rem;
154
+ flex-wrap: wrap;
155
+ }
156
+ .ps-theme-label {
157
+ font-size: 12px;
158
+ color: var(--ps-muted);
159
+ margin: 0;
160
+ }
161
+ .ps-theme-select {
162
+ width: auto;
163
+ min-width: 120px;
164
+ }
149
165
  .ps-logo {
150
166
  max-height: 48px;
151
167
  max-width: 220px;
@@ -227,18 +243,29 @@
227
243
  <div class="container form-container">
228
244
  <div class="ps-header" style="--ps-logo-max-height: [[logoMaxHeightPx]]px; --ps-logo-max-width: [[logoMaxWidthPx]]px;">
229
245
  <h1 class="mb-0">[[title]]</h1>
230
- [[#logoUrl]]
231
- <img class="ps-custom-logo" src="[[logoUrl]]" alt="[[logoAlt]]">
232
- [[/logoUrl]]
233
- [[^logoUrl]]
234
- [[^licensed]]
235
- <svg class="ps-logo" viewBox="0 0 420 96" role="img" aria-label="PortalSmith">
236
- <rect x="0" y="0" width="420" height="96" rx="14" fill="var(--ps-surface)" stroke="var(--ps-border)"/>
237
- <text x="24" y="58" font-size="34" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-text)">PortalSmith</text>
238
- <text x="24" y="78" font-size="14" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-muted)">FormGen</text>
239
- </svg>
240
- [[/licensed]]
241
- [[/logoUrl]]
246
+ <div class="ps-header-right">
247
+ <div>
248
+ <div class="ps-theme-label">Theme</div>
249
+ <select class="custom-select custom-select-sm ps-theme-select" v-model="themeMode" @change="setTheme(themeMode)">
250
+ <option value="auto">Auto</option>
251
+ <option value="light">Light</option>
252
+ <option value="dark">Dark</option>
253
+ </select>
254
+ </div>
255
+
256
+ [[#logoUrl]]
257
+ <img class="ps-custom-logo" src="[[logoUrl]]" alt="[[logoAlt]]">
258
+ [[/logoUrl]]
259
+ [[^logoUrl]]
260
+ [[^licensed]]
261
+ <svg class="ps-logo" viewBox="0 0 420 96" role="img" aria-label="PortalSmith">
262
+ <rect x="0" y="0" width="420" height="96" rx="14" fill="var(--ps-surface)" stroke="var(--ps-border)"/>
263
+ <text x="24" y="58" font-size="34" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-text)">PortalSmith</text>
264
+ <text x="24" y="78" font-size="14" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-muted)">FormGen</text>
265
+ </svg>
266
+ [[/licensed]]
267
+ [[/logoUrl]]
268
+ </div>
242
269
  </div>
243
270
  <p v-if="description" class="text-muted mb-4">[[description]]</p>
244
271
 
@@ -131,6 +131,22 @@
131
131
  gap: 1rem;
132
132
  margin-bottom: 0.5rem;
133
133
  }
134
+ .ps-header-right {
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: flex-end;
138
+ gap: 0.75rem;
139
+ flex-wrap: wrap;
140
+ }
141
+ .ps-theme-label {
142
+ font-size: 12px;
143
+ color: var(--ps-muted);
144
+ margin: 0;
145
+ }
146
+ .ps-theme-select {
147
+ width: auto;
148
+ min-width: 120px;
149
+ }
134
150
  .ps-logo {
135
151
  max-height: 48px;
136
152
  max-width: 220px;
@@ -212,18 +228,29 @@
212
228
  <div class="container form-container">
213
229
  <div class="ps-header" style="--ps-logo-max-height: [[logoMaxHeightPx]]px; --ps-logo-max-width: [[logoMaxWidthPx]]px;">
214
230
  <h1 class="mb-0">[[title]]</h1>
215
- [[#logoUrl]]
216
- <img class="ps-custom-logo" src="[[logoUrl]]" alt="[[logoAlt]]">
217
- [[/logoUrl]]
218
- [[^logoUrl]]
219
- [[^licensed]]
220
- <svg class="ps-logo" viewBox="0 0 420 96" role="img" aria-label="PortalSmith">
221
- <rect x="0" y="0" width="420" height="96" rx="14" fill="var(--ps-surface)" stroke="var(--ps-border)"/>
222
- <text x="24" y="58" font-size="34" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-text)">PortalSmith</text>
223
- <text x="24" y="78" font-size="14" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-muted)">FormGen</text>
224
- </svg>
225
- [[/licensed]]
226
- [[/logoUrl]]
231
+ <div class="ps-header-right">
232
+ <div>
233
+ <div class="ps-theme-label">Theme</div>
234
+ <select class="form-select form-select-sm ps-theme-select" v-model="themeMode" @change="setTheme(themeMode)">
235
+ <option value="auto">Auto</option>
236
+ <option value="light">Light</option>
237
+ <option value="dark">Dark</option>
238
+ </select>
239
+ </div>
240
+
241
+ [[#logoUrl]]
242
+ <img class="ps-custom-logo" src="[[logoUrl]]" alt="[[logoAlt]]">
243
+ [[/logoUrl]]
244
+ [[^logoUrl]]
245
+ [[^licensed]]
246
+ <svg class="ps-logo" viewBox="0 0 420 96" role="img" aria-label="PortalSmith">
247
+ <rect x="0" y="0" width="420" height="96" rx="14" fill="var(--ps-surface)" stroke="var(--ps-border)"/>
248
+ <text x="24" y="58" font-size="34" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-text)">PortalSmith</text>
249
+ <text x="24" y="78" font-size="14" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-muted)">FormGen</text>
250
+ </svg>
251
+ [[/licensed]]
252
+ [[/logoUrl]]
253
+ </div>
227
254
  </div>
228
255
  <p v-if="description" class="text-muted mb-4">[[description]]</p>
229
256
 
@@ -352,9 +352,31 @@
352
352
  });
353
353
  }
354
354
 
355
- function applyTheme() {
356
- const m = String(CONFIG.themeMode || 'auto').toLowerCase();
357
- document.documentElement.setAttribute('data-theme', m);
355
+ function normalizeThemeMode(mode, fallback) {
356
+ const m = String(mode || '').trim().toLowerCase();
357
+ if (m === 'light' || m === 'dark' || m === 'auto') return m;
358
+ const fb = String(fallback || '').trim().toLowerCase();
359
+ return (fb === 'light' || fb === 'dark' || fb === 'auto') ? fb : 'auto';
360
+ }
361
+
362
+ function getThemeMode() {
363
+ try {
364
+ const stored = localStorage.getItem('portalsmith:theme');
365
+ if (stored && stored.trim()) return normalizeThemeMode(stored, CONFIG.themeMode);
366
+ } catch (e) { /* ignore */ }
367
+ return normalizeThemeMode(CONFIG.themeMode, 'auto');
368
+ }
369
+
370
+ function applyTheme(mode) {
371
+ const desired = normalizeThemeMode(mode, getThemeMode());
372
+ document.documentElement.setAttribute('data-theme', desired);
373
+ return desired;
374
+ }
375
+
376
+ function setThemeMode(mode) {
377
+ const desired = normalizeThemeMode(mode, getThemeMode());
378
+ try { localStorage.setItem('portalsmith:theme', desired); } catch (e) { /* ignore */ }
379
+ return applyTheme(desired);
358
380
  }
359
381
 
360
382
  function tryParseHeadersJson(s) {
@@ -516,7 +538,8 @@
516
538
  resultSortKey: '',
517
539
  resultSortDir: 'asc',
518
540
  copyBlockActions: schema.actions ? schema.actions.filter(a => a.type === 'copyBlock') : [],
519
- exportFormats: CONFIG.exportFormats
541
+ exportFormats: CONFIG.exportFormats,
542
+ themeMode: getThemeMode(),
520
543
  };
521
544
  },
522
545
  computed: {
@@ -657,6 +680,9 @@
657
680
  }
658
681
  },
659
682
  methods: {
683
+ setTheme(mode) {
684
+ this.themeMode = setThemeMode(mode);
685
+ },
660
686
  toggleResultSort(key) {
661
687
  const k = String(key || '').trim();
662
688
  if (!k) return;