@cyprnet/node-red-contrib-uibuilder-formgen 0.5.36 → 0.5.38
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 +9 -0
- package/docs/user-guide.html +24 -0
- package/examples/formgen-builder/src/index.html +24 -1
- package/examples/formgen-builder/src/index.js +29 -3
- package/examples/formgen-builder-uib2/src/index.html +24 -1
- package/examples/formgen-builder-uib2/src/index.js +29 -3
- package/package.json +1 -1
- package/templates/index.html.mustache +64 -13
- package/templates/index.js.mustache +49 -10
- package/templates/index.uib2.html.mustache +64 -13
- package/templates/index.v3.html.mustache +66 -15
- package/templates/index.v3.js.mustache +52 -4
package/CHANGELOG.md
CHANGED
|
@@ -80,6 +80,15 @@ 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
|
+
|
|
87
|
+
## 0.5.38
|
|
88
|
+
|
|
89
|
+
- Schema Builder: sections can be marked collapsible (and optionally collapsed by default) for the generated portal.
|
|
90
|
+
- Portal UX: generated portals can now collapse/expand sections when <code>section.collapsible</code> is enabled.
|
|
91
|
+
|
|
83
92
|
## 0.5.21
|
|
84
93
|
|
|
85
94
|
- 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/docs/user-guide.html
CHANGED
|
@@ -608,6 +608,30 @@ return msg;</pre>
|
|
|
608
608
|
<li><code>actions[]</code> (optional)</li>
|
|
609
609
|
</ul>
|
|
610
610
|
|
|
611
|
+
<h3>Section properties</h3>
|
|
612
|
+
<p>Each section supports:</p>
|
|
613
|
+
<ul>
|
|
614
|
+
<li><code>id</code> (required)</li>
|
|
615
|
+
<li><code>title</code> (required)</li>
|
|
616
|
+
<li><code>description</code> (optional)</li>
|
|
617
|
+
<li><code>fields</code> (required array)</li>
|
|
618
|
+
<li><code>collapsible</code> (optional boolean): if true, the generated portal shows a Show/Hide toggle for that section</li>
|
|
619
|
+
<li><code>collapsedByDefault</code> (optional boolean): if true (and <code>collapsible</code> is true), the section starts collapsed</li>
|
|
620
|
+
</ul>
|
|
621
|
+
<pre>{
|
|
622
|
+
"id": "advanced",
|
|
623
|
+
"title": "Advanced Settings",
|
|
624
|
+
"description": "Optional settings most users can ignore",
|
|
625
|
+
"collapsible": true,
|
|
626
|
+
"collapsedByDefault": true,
|
|
627
|
+
"fields": [
|
|
628
|
+
{ "id": "notes", "type": "textarea", "label": "Notes" }
|
|
629
|
+
]
|
|
630
|
+
}</pre>
|
|
631
|
+
<div class="note">
|
|
632
|
+
<strong>Schema Builder:</strong> Edit a section → enable <strong>Collapsible (generated portal)</strong> and optionally <strong>Start collapsed by default</strong>.
|
|
633
|
+
</div>
|
|
634
|
+
|
|
611
635
|
<h3>Supported field types</h3>
|
|
612
636
|
<table>
|
|
613
637
|
<thead><tr><th>Type</th><th>Description</th></tr></thead>
|
|
@@ -189,8 +189,18 @@
|
|
|
189
189
|
<div>
|
|
190
190
|
<strong>{{section.title || 'Untitled Section'}}</strong>
|
|
191
191
|
<small class="text-muted ml-2">({{section.fields.length}} fields)</small>
|
|
192
|
+
<b-badge v-if="section.collapsible" variant="info" class="ml-2">Collapsible</b-badge>
|
|
192
193
|
</div>
|
|
193
194
|
<div>
|
|
195
|
+
<b-button
|
|
196
|
+
variant="outline-secondary"
|
|
197
|
+
size="sm"
|
|
198
|
+
class="mr-2"
|
|
199
|
+
@click="toggleBuilderSectionCollapse(section.id)"
|
|
200
|
+
:title="isBuilderSectionCollapsed(section.id) ? 'Expand section' : 'Collapse section'"
|
|
201
|
+
>
|
|
202
|
+
<span aria-hidden="true">{{ isBuilderSectionCollapsed(section.id) ? '▸' : '▾' }}</span>
|
|
203
|
+
</b-button>
|
|
194
204
|
<b-button variant="outline-primary" size="sm" @click="editSection(sectionIdx)" class="mr-2">
|
|
195
205
|
<i class="fa fa-edit"></i> Edit
|
|
196
206
|
</b-button>
|
|
@@ -202,7 +212,7 @@
|
|
|
202
212
|
</b-button>
|
|
203
213
|
</div>
|
|
204
214
|
</div>
|
|
205
|
-
<div class="section-body">
|
|
215
|
+
<div class="section-body" v-show="!isBuilderSectionCollapsed(section.id)">
|
|
206
216
|
<p v-if="section.description" class="text-muted mb-3">{{section.description}}</p>
|
|
207
217
|
|
|
208
218
|
<div v-if="section.fields.length === 0" class="empty-state">
|
|
@@ -328,6 +338,19 @@
|
|
|
328
338
|
rows="2"
|
|
329
339
|
/>
|
|
330
340
|
</b-form-group>
|
|
341
|
+
|
|
342
|
+
<b-form-group label="Collapsible (generated portal)" label-for="section-collapsible">
|
|
343
|
+
<b-form-checkbox id="section-collapsible" v-model="currentSection.collapsible">
|
|
344
|
+
Allow end users to collapse/expand this section in the generated portal
|
|
345
|
+
</b-form-checkbox>
|
|
346
|
+
<b-form-checkbox
|
|
347
|
+
class="mt-2"
|
|
348
|
+
v-model="currentSection.collapsedByDefault"
|
|
349
|
+
:disabled="!currentSection.collapsible"
|
|
350
|
+
>
|
|
351
|
+
Start collapsed by default
|
|
352
|
+
</b-form-checkbox>
|
|
353
|
+
</b-form-group>
|
|
331
354
|
</b-modal>
|
|
332
355
|
|
|
333
356
|
<!-- Field Editor Modal -->
|
|
@@ -86,8 +86,13 @@
|
|
|
86
86
|
currentSection: {
|
|
87
87
|
id: '',
|
|
88
88
|
title: '',
|
|
89
|
-
description: ''
|
|
89
|
+
description: '',
|
|
90
|
+
collapsible: false,
|
|
91
|
+
collapsedByDefault: false
|
|
90
92
|
},
|
|
93
|
+
|
|
94
|
+
// Builder-only UI state: collapse section cards while editing (not stored in schema)
|
|
95
|
+
builderCollapsedSections: {},
|
|
91
96
|
|
|
92
97
|
// Field editor
|
|
93
98
|
editingField: null,
|
|
@@ -791,7 +796,9 @@
|
|
|
791
796
|
this.currentSection = {
|
|
792
797
|
id: '',
|
|
793
798
|
title: '',
|
|
794
|
-
description: ''
|
|
799
|
+
description: '',
|
|
800
|
+
collapsible: false,
|
|
801
|
+
collapsedByDefault: false
|
|
795
802
|
};
|
|
796
803
|
this.$bvModal.show('section-modal');
|
|
797
804
|
},
|
|
@@ -799,6 +806,9 @@
|
|
|
799
806
|
editSection(sectionIdx) {
|
|
800
807
|
this.editingSection = sectionIdx;
|
|
801
808
|
this.currentSection = JSON.parse(JSON.stringify(this.schema.sections[sectionIdx]));
|
|
809
|
+
// Defaults for older schemas
|
|
810
|
+
if (this.currentSection.collapsible === undefined) this.$set(this.currentSection, 'collapsible', false);
|
|
811
|
+
if (this.currentSection.collapsedByDefault === undefined) this.$set(this.currentSection, 'collapsedByDefault', false);
|
|
802
812
|
this.$bvModal.show('section-modal');
|
|
803
813
|
},
|
|
804
814
|
|
|
@@ -826,6 +836,8 @@
|
|
|
826
836
|
id: this.currentSection.id,
|
|
827
837
|
title: this.currentSection.title,
|
|
828
838
|
description: this.currentSection.description || '',
|
|
839
|
+
collapsible: !!this.currentSection.collapsible,
|
|
840
|
+
collapsedByDefault: !!this.currentSection.collapsedByDefault,
|
|
829
841
|
fields: []
|
|
830
842
|
});
|
|
831
843
|
} else {
|
|
@@ -835,6 +847,8 @@
|
|
|
835
847
|
id: this.currentSection.id,
|
|
836
848
|
title: this.currentSection.title,
|
|
837
849
|
description: this.currentSection.description || '',
|
|
850
|
+
collapsible: !!this.currentSection.collapsible,
|
|
851
|
+
collapsedByDefault: !!this.currentSection.collapsedByDefault,
|
|
838
852
|
fields: this.schema.sections[this.editingSection].fields
|
|
839
853
|
};
|
|
840
854
|
|
|
@@ -850,7 +864,19 @@
|
|
|
850
864
|
|
|
851
865
|
cancelEditSection() {
|
|
852
866
|
this.editingSection = null;
|
|
853
|
-
this.currentSection = { id: '', title: '', description: '' };
|
|
867
|
+
this.currentSection = { id: '', title: '', description: '', collapsible: false, collapsedByDefault: false };
|
|
868
|
+
},
|
|
869
|
+
|
|
870
|
+
toggleBuilderSectionCollapse(sectionId) {
|
|
871
|
+
const id = String(sectionId || '').trim();
|
|
872
|
+
if (!id) return;
|
|
873
|
+
const cur = !!this.builderCollapsedSections[id];
|
|
874
|
+
this.$set(this.builderCollapsedSections, id, !cur);
|
|
875
|
+
},
|
|
876
|
+
|
|
877
|
+
isBuilderSectionCollapsed(sectionId) {
|
|
878
|
+
const id = String(sectionId || '').trim();
|
|
879
|
+
return !!this.builderCollapsedSections[id];
|
|
854
880
|
},
|
|
855
881
|
|
|
856
882
|
deleteSection(sectionIdx) {
|
|
@@ -188,8 +188,18 @@
|
|
|
188
188
|
<div>
|
|
189
189
|
<strong>{{section.title || 'Untitled Section'}}</strong>
|
|
190
190
|
<small class="text-muted ml-2">({{section.fields.length}} fields)</small>
|
|
191
|
+
<b-badge v-if="section.collapsible" variant="info" class="ml-2">Collapsible</b-badge>
|
|
191
192
|
</div>
|
|
192
193
|
<div>
|
|
194
|
+
<b-button
|
|
195
|
+
variant="outline-secondary"
|
|
196
|
+
size="sm"
|
|
197
|
+
class="mr-2"
|
|
198
|
+
@click="toggleBuilderSectionCollapse(section.id)"
|
|
199
|
+
:title="isBuilderSectionCollapsed(section.id) ? 'Expand section' : 'Collapse section'"
|
|
200
|
+
>
|
|
201
|
+
<span aria-hidden="true">{{ isBuilderSectionCollapsed(section.id) ? '▸' : '▾' }}</span>
|
|
202
|
+
</b-button>
|
|
193
203
|
<b-button variant="outline-primary" size="sm" @click="editSection(sectionIdx)" class="mr-2">
|
|
194
204
|
<i class="fa fa-edit"></i> Edit
|
|
195
205
|
</b-button>
|
|
@@ -201,7 +211,7 @@
|
|
|
201
211
|
</b-button>
|
|
202
212
|
</div>
|
|
203
213
|
</div>
|
|
204
|
-
<div class="section-body">
|
|
214
|
+
<div class="section-body" v-show="!isBuilderSectionCollapsed(section.id)">
|
|
205
215
|
<p v-if="section.description" class="text-muted mb-3">{{section.description}}</p>
|
|
206
216
|
|
|
207
217
|
<div v-if="section.fields.length === 0" class="empty-state">
|
|
@@ -327,6 +337,19 @@
|
|
|
327
337
|
rows="2"
|
|
328
338
|
/>
|
|
329
339
|
</b-form-group>
|
|
340
|
+
|
|
341
|
+
<b-form-group label="Collapsible (generated portal)" label-for="section-collapsible">
|
|
342
|
+
<b-form-checkbox id="section-collapsible" v-model="currentSection.collapsible">
|
|
343
|
+
Allow end users to collapse/expand this section in the generated portal
|
|
344
|
+
</b-form-checkbox>
|
|
345
|
+
<b-form-checkbox
|
|
346
|
+
class="mt-2"
|
|
347
|
+
v-model="currentSection.collapsedByDefault"
|
|
348
|
+
:disabled="!currentSection.collapsible"
|
|
349
|
+
>
|
|
350
|
+
Start collapsed by default
|
|
351
|
+
</b-form-checkbox>
|
|
352
|
+
</b-form-group>
|
|
330
353
|
</b-modal>
|
|
331
354
|
|
|
332
355
|
<!-- Field Editor Modal -->
|
|
@@ -86,8 +86,13 @@
|
|
|
86
86
|
currentSection: {
|
|
87
87
|
id: '',
|
|
88
88
|
title: '',
|
|
89
|
-
description: ''
|
|
89
|
+
description: '',
|
|
90
|
+
collapsible: false,
|
|
91
|
+
collapsedByDefault: false
|
|
90
92
|
},
|
|
93
|
+
|
|
94
|
+
// Builder-only UI state: collapse section cards while editing (not stored in schema)
|
|
95
|
+
builderCollapsedSections: {},
|
|
91
96
|
|
|
92
97
|
// Field editor
|
|
93
98
|
editingField: null,
|
|
@@ -791,7 +796,9 @@
|
|
|
791
796
|
this.currentSection = {
|
|
792
797
|
id: '',
|
|
793
798
|
title: '',
|
|
794
|
-
description: ''
|
|
799
|
+
description: '',
|
|
800
|
+
collapsible: false,
|
|
801
|
+
collapsedByDefault: false
|
|
795
802
|
};
|
|
796
803
|
this.$bvModal.show('section-modal');
|
|
797
804
|
},
|
|
@@ -799,6 +806,9 @@
|
|
|
799
806
|
editSection(sectionIdx) {
|
|
800
807
|
this.editingSection = sectionIdx;
|
|
801
808
|
this.currentSection = JSON.parse(JSON.stringify(this.schema.sections[sectionIdx]));
|
|
809
|
+
// Defaults for older schemas
|
|
810
|
+
if (this.currentSection.collapsible === undefined) this.$set(this.currentSection, 'collapsible', false);
|
|
811
|
+
if (this.currentSection.collapsedByDefault === undefined) this.$set(this.currentSection, 'collapsedByDefault', false);
|
|
802
812
|
this.$bvModal.show('section-modal');
|
|
803
813
|
},
|
|
804
814
|
|
|
@@ -826,6 +836,8 @@
|
|
|
826
836
|
id: this.currentSection.id,
|
|
827
837
|
title: this.currentSection.title,
|
|
828
838
|
description: this.currentSection.description || '',
|
|
839
|
+
collapsible: !!this.currentSection.collapsible,
|
|
840
|
+
collapsedByDefault: !!this.currentSection.collapsedByDefault,
|
|
829
841
|
fields: []
|
|
830
842
|
});
|
|
831
843
|
} else {
|
|
@@ -835,6 +847,8 @@
|
|
|
835
847
|
id: this.currentSection.id,
|
|
836
848
|
title: this.currentSection.title,
|
|
837
849
|
description: this.currentSection.description || '',
|
|
850
|
+
collapsible: !!this.currentSection.collapsible,
|
|
851
|
+
collapsedByDefault: !!this.currentSection.collapsedByDefault,
|
|
838
852
|
fields: this.schema.sections[this.editingSection].fields
|
|
839
853
|
};
|
|
840
854
|
|
|
@@ -850,7 +864,19 @@
|
|
|
850
864
|
|
|
851
865
|
cancelEditSection() {
|
|
852
866
|
this.editingSection = null;
|
|
853
|
-
this.currentSection = { id: '', title: '', description: '' };
|
|
867
|
+
this.currentSection = { id: '', title: '', description: '', collapsible: false, collapsedByDefault: false };
|
|
868
|
+
},
|
|
869
|
+
|
|
870
|
+
toggleBuilderSectionCollapse(sectionId) {
|
|
871
|
+
const id = String(sectionId || '').trim();
|
|
872
|
+
if (!id) return;
|
|
873
|
+
const cur = !!this.builderCollapsedSections[id];
|
|
874
|
+
this.$set(this.builderCollapsedSections, id, !cur);
|
|
875
|
+
},
|
|
876
|
+
|
|
877
|
+
isBuilderSectionCollapsed(sectionId) {
|
|
878
|
+
const id = String(sectionId || '').trim();
|
|
879
|
+
return !!this.builderCollapsedSections[id];
|
|
854
880
|
},
|
|
855
881
|
|
|
856
882
|
deleteSection(sectionIdx) {
|
package/package.json
CHANGED
|
@@ -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;
|
|
@@ -210,6 +226,15 @@
|
|
|
210
226
|
margin-bottom: 1.5rem;
|
|
211
227
|
margin-top: 2rem;
|
|
212
228
|
}
|
|
229
|
+
.ps-section-head {
|
|
230
|
+
display: flex;
|
|
231
|
+
align-items: flex-end;
|
|
232
|
+
justify-content: space-between;
|
|
233
|
+
gap: 0.75rem;
|
|
234
|
+
}
|
|
235
|
+
.ps-section-toggle {
|
|
236
|
+
white-space: nowrap;
|
|
237
|
+
}
|
|
213
238
|
.section-header:first-child {
|
|
214
239
|
margin-top: 0;
|
|
215
240
|
}
|
|
@@ -229,18 +254,29 @@
|
|
|
229
254
|
<div class="container form-container">
|
|
230
255
|
<div class="ps-header" style="--ps-logo-max-height: [[logoMaxHeightPx]]px; --ps-logo-max-width: [[logoMaxWidthPx]]px;">
|
|
231
256
|
<h1 class="mb-0">[[title]]</h1>
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
257
|
+
<div class="ps-header-right">
|
|
258
|
+
<div>
|
|
259
|
+
<div class="ps-theme-label">Theme</div>
|
|
260
|
+
<select class="custom-select custom-select-sm ps-theme-select" v-model="themeMode" @change="setTheme(themeMode)">
|
|
261
|
+
<option value="auto">Auto</option>
|
|
262
|
+
<option value="light">Light</option>
|
|
263
|
+
<option value="dark">Dark</option>
|
|
264
|
+
</select>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
[[#logoUrl]]
|
|
268
|
+
<img class="ps-custom-logo" src="[[logoUrl]]" alt="[[logoAlt]]">
|
|
269
|
+
[[/logoUrl]]
|
|
270
|
+
[[^logoUrl]]
|
|
271
|
+
[[^licensed]]
|
|
272
|
+
<svg class="ps-logo" viewBox="0 0 420 96" role="img" aria-label="PortalSmith">
|
|
273
|
+
<rect x="0" y="0" width="420" height="96" rx="14" fill="var(--ps-surface)" stroke="var(--ps-border)"/>
|
|
274
|
+
<text x="24" y="58" font-size="34" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-text)">PortalSmith</text>
|
|
275
|
+
<text x="24" y="78" font-size="14" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-muted)">FormGen</text>
|
|
276
|
+
</svg>
|
|
277
|
+
[[/licensed]]
|
|
278
|
+
[[/logoUrl]]
|
|
279
|
+
</div>
|
|
244
280
|
</div>
|
|
245
281
|
<p v-if="description" class="text-muted mb-4">[[description]]</p>
|
|
246
282
|
|
|
@@ -430,8 +466,22 @@
|
|
|
430
466
|
<form @submit.prevent="handleSubmit">
|
|
431
467
|
<!-- Dynamic Sections -->
|
|
432
468
|
<div v-for="section in schema.sections" :key="section.id" class="form-section">
|
|
433
|
-
<
|
|
469
|
+
<div class="ps-section-head section-header" v-if="section.title || section.collapsible">
|
|
470
|
+
<h3 v-if="section.title" class="mb-0">{{section.title}}</h3>
|
|
471
|
+
<button
|
|
472
|
+
v-if="section.collapsible"
|
|
473
|
+
type="button"
|
|
474
|
+
class="btn btn-sm btn-outline-secondary ps-section-toggle"
|
|
475
|
+
@click="toggleSection(section.id)"
|
|
476
|
+
:aria-expanded="(!isSectionCollapsed(section.id)).toString()"
|
|
477
|
+
:aria-controls="'ps-sec-' + section.id"
|
|
478
|
+
>
|
|
479
|
+
{{ isSectionCollapsed(section.id) ? 'Show' : 'Hide' }}
|
|
480
|
+
</button>
|
|
481
|
+
</div>
|
|
434
482
|
<p v-if="section.description" class="text-muted mb-3">{{section.description}}</p>
|
|
483
|
+
|
|
484
|
+
<div :id="'ps-sec-' + section.id" v-show="!section.collapsible || !isSectionCollapsed(section.id)">
|
|
435
485
|
|
|
436
486
|
<!-- Dynamic Fields -->
|
|
437
487
|
<div v-for="field in visibleFieldsBySection[section.id]" :key="field.id" class="form-group">
|
|
@@ -593,6 +643,7 @@
|
|
|
593
643
|
{{field.help}}
|
|
594
644
|
</small>
|
|
595
645
|
</div>
|
|
646
|
+
</div>
|
|
596
647
|
</div>
|
|
597
648
|
|
|
598
649
|
<!-- Action Buttons -->
|
|
@@ -495,18 +495,31 @@
|
|
|
495
495
|
.replace(/'/g, ''');
|
|
496
496
|
}
|
|
497
497
|
|
|
498
|
-
function
|
|
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
|
-
|
|
508
|
+
if (stored && stored.trim()) return normalizeThemeMode(stored, CONFIG.themeMode);
|
|
509
|
+
} catch (e) { /* ignore */ }
|
|
510
|
+
return normalizeThemeMode(CONFIG.themeMode, 'auto');
|
|
511
|
+
}
|
|
503
512
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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,20 @@
|
|
|
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(),
|
|
796
|
+
|
|
797
|
+
// Section collapse state (keyed by section.id)
|
|
798
|
+
collapsedSections: (function initCollapsed() {
|
|
799
|
+
const m = {};
|
|
800
|
+
try {
|
|
801
|
+
(schema.sections || []).forEach(s => {
|
|
802
|
+
if (!s || !s.id) return;
|
|
803
|
+
if (s.collapsible) m[s.id] = !!s.collapsedByDefault;
|
|
804
|
+
});
|
|
805
|
+
} catch (e) { /* ignore */ }
|
|
806
|
+
return m;
|
|
807
|
+
})(),
|
|
782
808
|
},
|
|
783
809
|
computed: {
|
|
784
810
|
description() {
|
|
@@ -965,6 +991,19 @@
|
|
|
965
991
|
}
|
|
966
992
|
},
|
|
967
993
|
methods: {
|
|
994
|
+
toggleSection(sectionId) {
|
|
995
|
+
const id = String(sectionId || '').trim();
|
|
996
|
+
if (!id) return;
|
|
997
|
+
const cur = !!this.collapsedSections[id];
|
|
998
|
+
this.$set(this.collapsedSections, id, !cur);
|
|
999
|
+
},
|
|
1000
|
+
isSectionCollapsed(sectionId) {
|
|
1001
|
+
const id = String(sectionId || '').trim();
|
|
1002
|
+
return !!this.collapsedSections[id];
|
|
1003
|
+
},
|
|
1004
|
+
setTheme(mode) {
|
|
1005
|
+
this.themeMode = setThemeMode(mode);
|
|
1006
|
+
},
|
|
968
1007
|
toggleResultSort(key) {
|
|
969
1008
|
const k = String(key || '').trim();
|
|
970
1009
|
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;
|
|
@@ -208,6 +224,15 @@
|
|
|
208
224
|
margin-bottom: 1.5rem;
|
|
209
225
|
margin-top: 2rem;
|
|
210
226
|
}
|
|
227
|
+
.ps-section-head {
|
|
228
|
+
display: flex;
|
|
229
|
+
align-items: flex-end;
|
|
230
|
+
justify-content: space-between;
|
|
231
|
+
gap: 0.75rem;
|
|
232
|
+
}
|
|
233
|
+
.ps-section-toggle {
|
|
234
|
+
white-space: nowrap;
|
|
235
|
+
}
|
|
211
236
|
.section-header:first-child {
|
|
212
237
|
margin-top: 0;
|
|
213
238
|
}
|
|
@@ -227,18 +252,29 @@
|
|
|
227
252
|
<div class="container form-container">
|
|
228
253
|
<div class="ps-header" style="--ps-logo-max-height: [[logoMaxHeightPx]]px; --ps-logo-max-width: [[logoMaxWidthPx]]px;">
|
|
229
254
|
<h1 class="mb-0">[[title]]</h1>
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
255
|
+
<div class="ps-header-right">
|
|
256
|
+
<div>
|
|
257
|
+
<div class="ps-theme-label">Theme</div>
|
|
258
|
+
<select class="custom-select custom-select-sm ps-theme-select" v-model="themeMode" @change="setTheme(themeMode)">
|
|
259
|
+
<option value="auto">Auto</option>
|
|
260
|
+
<option value="light">Light</option>
|
|
261
|
+
<option value="dark">Dark</option>
|
|
262
|
+
</select>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
[[#logoUrl]]
|
|
266
|
+
<img class="ps-custom-logo" src="[[logoUrl]]" alt="[[logoAlt]]">
|
|
267
|
+
[[/logoUrl]]
|
|
268
|
+
[[^logoUrl]]
|
|
269
|
+
[[^licensed]]
|
|
270
|
+
<svg class="ps-logo" viewBox="0 0 420 96" role="img" aria-label="PortalSmith">
|
|
271
|
+
<rect x="0" y="0" width="420" height="96" rx="14" fill="var(--ps-surface)" stroke="var(--ps-border)"/>
|
|
272
|
+
<text x="24" y="58" font-size="34" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-text)">PortalSmith</text>
|
|
273
|
+
<text x="24" y="78" font-size="14" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-muted)">FormGen</text>
|
|
274
|
+
</svg>
|
|
275
|
+
[[/licensed]]
|
|
276
|
+
[[/logoUrl]]
|
|
277
|
+
</div>
|
|
242
278
|
</div>
|
|
243
279
|
<p v-if="description" class="text-muted mb-4">[[description]]</p>
|
|
244
280
|
|
|
@@ -428,8 +464,22 @@
|
|
|
428
464
|
<form @submit.prevent="handleSubmit">
|
|
429
465
|
<!-- Dynamic Sections -->
|
|
430
466
|
<div v-for="section in schema.sections" :key="section.id" class="form-section">
|
|
431
|
-
<
|
|
467
|
+
<div class="ps-section-head section-header" v-if="section.title || section.collapsible">
|
|
468
|
+
<h3 v-if="section.title" class="mb-0">{{section.title}}</h3>
|
|
469
|
+
<button
|
|
470
|
+
v-if="section.collapsible"
|
|
471
|
+
type="button"
|
|
472
|
+
class="btn btn-sm btn-outline-secondary ps-section-toggle"
|
|
473
|
+
@click="toggleSection(section.id)"
|
|
474
|
+
:aria-expanded="(!isSectionCollapsed(section.id)).toString()"
|
|
475
|
+
:aria-controls="'ps-sec-' + section.id"
|
|
476
|
+
>
|
|
477
|
+
{{ isSectionCollapsed(section.id) ? 'Show' : 'Hide' }}
|
|
478
|
+
</button>
|
|
479
|
+
</div>
|
|
432
480
|
<p v-if="section.description" class="text-muted mb-3">{{section.description}}</p>
|
|
481
|
+
|
|
482
|
+
<div :id="'ps-sec-' + section.id" v-show="!section.collapsible || !isSectionCollapsed(section.id)">
|
|
433
483
|
|
|
434
484
|
<!-- Dynamic Fields -->
|
|
435
485
|
<div v-for="field in visibleFieldsBySection[section.id]" :key="field.id" class="form-group">
|
|
@@ -591,6 +641,7 @@
|
|
|
591
641
|
{{field.help}}
|
|
592
642
|
</small>
|
|
593
643
|
</div>
|
|
644
|
+
</div>
|
|
594
645
|
</div>
|
|
595
646
|
|
|
596
647
|
<!-- Action Buttons -->
|
|
@@ -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;
|
|
@@ -193,6 +209,15 @@
|
|
|
193
209
|
margin-bottom: 1.5rem;
|
|
194
210
|
margin-top: 2rem;
|
|
195
211
|
}
|
|
212
|
+
.ps-section-head {
|
|
213
|
+
display: flex;
|
|
214
|
+
align-items: flex-end;
|
|
215
|
+
justify-content: space-between;
|
|
216
|
+
gap: 0.75rem;
|
|
217
|
+
}
|
|
218
|
+
.ps-section-toggle {
|
|
219
|
+
white-space: nowrap;
|
|
220
|
+
}
|
|
196
221
|
.section-header:first-child {
|
|
197
222
|
margin-top: 0;
|
|
198
223
|
}
|
|
@@ -212,18 +237,29 @@
|
|
|
212
237
|
<div class="container form-container">
|
|
213
238
|
<div class="ps-header" style="--ps-logo-max-height: [[logoMaxHeightPx]]px; --ps-logo-max-width: [[logoMaxWidthPx]]px;">
|
|
214
239
|
<h1 class="mb-0">[[title]]</h1>
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
240
|
+
<div class="ps-header-right">
|
|
241
|
+
<div>
|
|
242
|
+
<div class="ps-theme-label">Theme</div>
|
|
243
|
+
<select class="form-select form-select-sm ps-theme-select" v-model="themeMode" @change="setTheme(themeMode)">
|
|
244
|
+
<option value="auto">Auto</option>
|
|
245
|
+
<option value="light">Light</option>
|
|
246
|
+
<option value="dark">Dark</option>
|
|
247
|
+
</select>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
[[#logoUrl]]
|
|
251
|
+
<img class="ps-custom-logo" src="[[logoUrl]]" alt="[[logoAlt]]">
|
|
252
|
+
[[/logoUrl]]
|
|
253
|
+
[[^logoUrl]]
|
|
254
|
+
[[^licensed]]
|
|
255
|
+
<svg class="ps-logo" viewBox="0 0 420 96" role="img" aria-label="PortalSmith">
|
|
256
|
+
<rect x="0" y="0" width="420" height="96" rx="14" fill="var(--ps-surface)" stroke="var(--ps-border)"/>
|
|
257
|
+
<text x="24" y="58" font-size="34" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-text)">PortalSmith</text>
|
|
258
|
+
<text x="24" y="78" font-size="14" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-muted)">FormGen</text>
|
|
259
|
+
</svg>
|
|
260
|
+
[[/licensed]]
|
|
261
|
+
[[/logoUrl]]
|
|
262
|
+
</div>
|
|
227
263
|
</div>
|
|
228
264
|
<p v-if="description" class="text-muted mb-4">[[description]]</p>
|
|
229
265
|
|
|
@@ -383,10 +419,24 @@
|
|
|
383
419
|
|
|
384
420
|
<!-- Sections -->
|
|
385
421
|
<div v-for="section in schema.sections" :key="section.id" class="mb-4">
|
|
386
|
-
<div class="section-header">
|
|
387
|
-
<
|
|
388
|
-
|
|
422
|
+
<div class="section-header ps-section-head">
|
|
423
|
+
<div>
|
|
424
|
+
<h2 class="h4 mb-1">{{section.title}}</h2>
|
|
425
|
+
<p v-if="section.description" class="text-muted mb-0">{{section.description}}</p>
|
|
426
|
+
</div>
|
|
427
|
+
<button
|
|
428
|
+
v-if="section.collapsible"
|
|
429
|
+
type="button"
|
|
430
|
+
class="btn btn-sm btn-outline-secondary ps-section-toggle"
|
|
431
|
+
@click="toggleSection(section.id)"
|
|
432
|
+
:aria-expanded="(!isSectionCollapsed(section.id)).toString()"
|
|
433
|
+
:aria-controls="'ps-sec-' + section.id"
|
|
434
|
+
>
|
|
435
|
+
{{ isSectionCollapsed(section.id) ? 'Show' : 'Hide' }}
|
|
436
|
+
</button>
|
|
389
437
|
</div>
|
|
438
|
+
|
|
439
|
+
<div :id="'ps-sec-' + section.id" v-show="!section.collapsible || !isSectionCollapsed(section.id)">
|
|
390
440
|
|
|
391
441
|
<!-- Fields -->
|
|
392
442
|
<div v-for="field in visibleFieldsBySection[section.id]" :key="field.id" class="mb-3">
|
|
@@ -552,6 +602,7 @@
|
|
|
552
602
|
{{fieldErrors[field.id]}}
|
|
553
603
|
</div>
|
|
554
604
|
</div>
|
|
605
|
+
</div>
|
|
555
606
|
</div>
|
|
556
607
|
|
|
557
608
|
<!-- Actions -->
|
|
@@ -352,9 +352,31 @@
|
|
|
352
352
|
});
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
-
function
|
|
356
|
-
const m = String(
|
|
357
|
-
|
|
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,20 @@
|
|
|
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(),
|
|
543
|
+
|
|
544
|
+
// Section collapse state (keyed by section.id)
|
|
545
|
+
collapsedSections: (function initCollapsed() {
|
|
546
|
+
const m = {};
|
|
547
|
+
try {
|
|
548
|
+
(schema.sections || []).forEach(s => {
|
|
549
|
+
if (!s || !s.id) return;
|
|
550
|
+
if (s.collapsible) m[s.id] = !!s.collapsedByDefault;
|
|
551
|
+
});
|
|
552
|
+
} catch (e) { /* ignore */ }
|
|
553
|
+
return m;
|
|
554
|
+
})(),
|
|
520
555
|
};
|
|
521
556
|
},
|
|
522
557
|
computed: {
|
|
@@ -657,6 +692,19 @@
|
|
|
657
692
|
}
|
|
658
693
|
},
|
|
659
694
|
methods: {
|
|
695
|
+
setTheme(mode) {
|
|
696
|
+
this.themeMode = setThemeMode(mode);
|
|
697
|
+
},
|
|
698
|
+
toggleSection(sectionId) {
|
|
699
|
+
const id = String(sectionId || '').trim();
|
|
700
|
+
if (!id) return;
|
|
701
|
+
const cur = !!this.collapsedSections[id];
|
|
702
|
+
this.collapsedSections[id] = !cur;
|
|
703
|
+
},
|
|
704
|
+
isSectionCollapsed(sectionId) {
|
|
705
|
+
const id = String(sectionId || '').trim();
|
|
706
|
+
return !!this.collapsedSections[id];
|
|
707
|
+
},
|
|
660
708
|
toggleResultSort(key) {
|
|
661
709
|
const k = String(key || '').trim();
|
|
662
710
|
if (!k) return;
|