@commonpub/layer 0.24.0 → 0.25.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.
- package/README.md +41 -12
- package/components/LayoutRow.vue +944 -0
- package/components/LayoutSection.vue +1028 -0
- package/components/LayoutSlot.vue +104 -162
- package/components/PageFrame.vue +116 -0
- package/components/admin/layouts/AdminLayoutsAnnouncer.vue +53 -0
- package/components/admin/layouts/AdminLayoutsAutoForm.vue +419 -0
- package/components/admin/layouts/AdminLayoutsCanvas.vue +332 -0
- package/components/admin/layouts/AdminLayoutsConflictModal.vue +266 -0
- package/components/admin/layouts/AdminLayoutsHelpOverlay.vue +346 -0
- package/components/admin/layouts/AdminLayoutsInspector.vue +157 -0
- package/components/admin/layouts/AdminLayoutsInspectorPage.vue +266 -0
- package/components/admin/layouts/AdminLayoutsInspectorRow.vue +80 -0
- package/components/admin/layouts/AdminLayoutsInspectorSection.vue +175 -0
- package/components/admin/layouts/AdminLayoutsPalette.vue +117 -0
- package/components/admin/layouts/AdminLayoutsPaletteTile.vue +149 -0
- package/components/admin/layouts/AdminLayoutsToolbar.vue +483 -0
- package/components/blocks/BlockDividerView.vue +52 -2
- package/components/homepage/ContentGridSection.vue +23 -1
- package/components/homepage/HeroSection.vue +69 -8
- package/components/sections/SectionCta.vue +175 -0
- package/composables/autoFormSchema.ts +319 -0
- package/composables/useAdminSidebar.ts +116 -0
- package/composables/useEditorChrome.ts +56 -0
- package/composables/useLayout.ts +34 -41
- package/composables/useLayoutAnnouncer.ts +332 -0
- package/composables/useLayoutAutoSave.ts +117 -0
- package/composables/useLayoutDrag.ts +290 -0
- package/composables/useLayoutEditor.ts +593 -0
- package/composables/useLayoutHistory.ts +583 -0
- package/composables/useLayoutHotkeys.ts +366 -0
- package/composables/useLayoutResize.ts +783 -0
- package/layouts/admin.vue +137 -24
- package/middleware/admin-layouts.ts +29 -0
- package/package.json +10 -7
- package/pages/[...customPath].vue +154 -0
- package/pages/admin/homepage.vue +46 -0
- package/pages/admin/index.vue +16 -0
- package/pages/admin/layouts/[id].vue +1110 -0
- package/pages/admin/layouts/index.vue +356 -0
- package/pages/explore.vue +16 -6
- package/sections/builtin/content-feed.ts +18 -29
- package/sections/builtin/contests.ts +11 -19
- package/sections/builtin/cta.ts +46 -0
- package/sections/builtin/custom-html.ts +16 -30
- package/sections/builtin/divider.ts +15 -17
- package/sections/builtin/editorial.ts +11 -21
- package/sections/builtin/embed.ts +31 -0
- package/sections/builtin/gallery.ts +29 -0
- package/sections/builtin/heading.ts +14 -19
- package/sections/builtin/hero.ts +16 -51
- package/sections/builtin/hubs.ts +11 -26
- package/sections/builtin/image.ts +12 -49
- package/sections/builtin/learning.ts +5 -13
- package/sections/builtin/markdown.ts +29 -0
- package/sections/builtin/paragraph.ts +14 -17
- package/sections/builtin/stats.ts +17 -18
- package/sections/builtin/video.ts +30 -0
- package/sections/registry.ts +11 -0
- package/server/api/admin/homepage/sections.put.ts +52 -1
- package/server/api/admin/layouts/[id]/publish.post.ts +12 -0
- package/server/api/admin/layouts/[id]/versions/[versionId]/revert.post.ts +11 -0
- package/server/api/admin/layouts/[id].delete.ts +33 -1
- package/server/api/admin/layouts/[id].put.ts +78 -0
- package/server/api/admin/layouts/index.post.ts +60 -4
- package/server/api/admin/layouts/migrate-homepage.post.ts +12 -0
- package/server/api/admin/layouts/seed-homepage.post.ts +9 -0
- package/server/api/layouts/by-route.get.ts +64 -12
- package/server/utils/layoutCache.ts +37 -1
- package/server/utils/validateSectionConfigs.ts +123 -0
- package/theme/base.css +1 -0
- package/components/sections/SectionContentFeed.vue +0 -160
- package/components/sections/SectionContests.vue +0 -193
- package/components/sections/SectionCustomHtml.vue +0 -70
- package/components/sections/SectionDivider.vue +0 -55
- package/components/sections/SectionEditorial.vue +0 -138
- package/components/sections/SectionHeading.vue +0 -78
- package/components/sections/SectionHero.vue +0 -164
- package/components/sections/SectionHubs.vue +0 -247
- package/components/sections/SectionImage.vue +0 -104
- package/components/sections/SectionParagraph.vue +0 -55
- package/components/sections/SectionStats.vue +0 -151
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* AdminLayoutsAutoForm — the recursive native-input renderer for a
|
|
4
|
+
* normalized `AutoFormField[]` (Phase 3e). Controlled component: takes
|
|
5
|
+
* `modelValue` (the config object), renders one control per field, emits
|
|
6
|
+
* `update:modelValue` with a fresh object on every edit. Recurses into
|
|
7
|
+
* itself for `group` (nested object) + `array` (repeater) controls.
|
|
8
|
+
*
|
|
9
|
+
* Shared by `<AdminLayoutsInspectorSection>` (section config) +
|
|
10
|
+
* `<AdminLayoutsInspectorRow>` (row config) — one code path serves every
|
|
11
|
+
* registered section's auto-form. Reuses the `cpub-inspector-page-*`
|
|
12
|
+
* design language (mono uppercase labels, 2px borders, sharp corners) via
|
|
13
|
+
* its own `cpub-autoform-*` classes so it composes the same way without
|
|
14
|
+
* structurally coupling to the page-meta form (feedback-view-identity-classes).
|
|
15
|
+
*
|
|
16
|
+
* Errors: `errors` is a flat map keyed by DOT-joined config path
|
|
17
|
+
* (`ctas.0.href`). Each field looks itself up via the accumulated
|
|
18
|
+
* `pathPrefix`. Surfaced inline; the section validates against the Zod
|
|
19
|
+
* schema and passes the map down.
|
|
20
|
+
*
|
|
21
|
+
* Pure-presentation: zero hardcoded colors/fonts (CLAUDE.md #3). No
|
|
22
|
+
* pointer-events / visibility cascades on interactive children — the
|
|
23
|
+
* controls are plain native inputs (feedback-css-cascade-unit-test-blind-spot).
|
|
24
|
+
*/
|
|
25
|
+
import type { AutoFormField } from '../../../composables/autoFormSchema';
|
|
26
|
+
|
|
27
|
+
defineOptions({ name: 'AdminLayoutsAutoForm' });
|
|
28
|
+
|
|
29
|
+
const props = withDefaults(
|
|
30
|
+
defineProps<{
|
|
31
|
+
fields: AutoFormField[];
|
|
32
|
+
modelValue: Record<string, unknown>;
|
|
33
|
+
/** Flat error map keyed by dot-joined config path. */
|
|
34
|
+
errors?: Record<string, string>;
|
|
35
|
+
/** Accumulated path prefix for nested groups/arrays. Root = ''. */
|
|
36
|
+
pathPrefix?: string;
|
|
37
|
+
/** Stable id seed so generated control ids don't collide across forms. */
|
|
38
|
+
idSeed?: string;
|
|
39
|
+
}>(),
|
|
40
|
+
{ errors: () => ({}), pathPrefix: '', idSeed: 'autoform' },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const emit = defineEmits<{
|
|
44
|
+
(e: 'update:modelValue', value: Record<string, unknown>): void;
|
|
45
|
+
}>();
|
|
46
|
+
|
|
47
|
+
function pathOf(key: string): string {
|
|
48
|
+
return props.pathPrefix ? `${props.pathPrefix}.${key}` : key;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function controlId(key: string): string {
|
|
52
|
+
return `cpub-${props.idSeed}-${pathOf(key).replace(/\./g, '-')}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function errorFor(key: string): string | undefined {
|
|
56
|
+
return props.errors[pathOf(key)];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Emit a new config object with one key replaced. */
|
|
60
|
+
function setKey(key: string, value: unknown): void {
|
|
61
|
+
emit('update:modelValue', { ...props.modelValue, [key]: value });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function valueOf(field: AutoFormField): unknown {
|
|
65
|
+
const v = props.modelValue[field.key];
|
|
66
|
+
return v === undefined ? field.defaultValue : v;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- scalar handlers (coerce native string/checkbox values per control) ---
|
|
70
|
+
|
|
71
|
+
function onText(field: AutoFormField, e: Event): void {
|
|
72
|
+
setKey(field.key, (e.target as HTMLInputElement | HTMLTextAreaElement).value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function onNumber(field: AutoFormField, e: Event): void {
|
|
76
|
+
const raw = (e.target as HTMLInputElement).value;
|
|
77
|
+
// Empty input → clear back to the default (or undefined) rather than NaN.
|
|
78
|
+
setKey(field.key, raw === '' ? field.defaultValue : Number(raw));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function onToggle(field: AutoFormField, e: Event): void {
|
|
82
|
+
setKey(field.key, (e.target as HTMLInputElement).checked);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function onSelect(field: AutoFormField, e: Event): void {
|
|
86
|
+
const raw = (e.target as HTMLSelectElement).value;
|
|
87
|
+
// The leading "— Default —" option (optional fields) clears the key so
|
|
88
|
+
// the renderer falls back to its own default instead of a forced choice.
|
|
89
|
+
if (raw === '' && field.optional) {
|
|
90
|
+
setKey(field.key, undefined);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Numeric-const selects (heading.level, columns) must store numbers —
|
|
94
|
+
// <select> always yields strings. Coerce when every option is numeric.
|
|
95
|
+
const numeric = field.options?.every((o) => typeof o.value === 'number');
|
|
96
|
+
setKey(field.key, numeric ? Number(raw) : raw);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- array (repeater) handlers ---
|
|
100
|
+
|
|
101
|
+
function arrayValue(field: AutoFormField): Record<string, unknown>[] {
|
|
102
|
+
const v = props.modelValue[field.key];
|
|
103
|
+
return Array.isArray(v) ? (v as Record<string, unknown>[]) : [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function addItem(field: AutoFormField): void {
|
|
107
|
+
const items = arrayValue(field);
|
|
108
|
+
if (field.maxItems !== undefined && items.length >= field.maxItems) return;
|
|
109
|
+
const blank = JSON.parse(JSON.stringify(field.itemDefault ?? {}));
|
|
110
|
+
setKey(field.key, [...items, blank]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function removeItem(field: AutoFormField, index: number): void {
|
|
114
|
+
const items = arrayValue(field);
|
|
115
|
+
setKey(field.key, items.filter((_, i) => i !== index));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function updateItem(field: AutoFormField, index: number, value: Record<string, unknown>): void {
|
|
119
|
+
const items = arrayValue(field);
|
|
120
|
+
setKey(field.key, items.map((it, i) => (i === index ? value : it)));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function groupValue(field: AutoFormField): Record<string, unknown> {
|
|
124
|
+
const v = props.modelValue[field.key];
|
|
125
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : {};
|
|
126
|
+
}
|
|
127
|
+
</script>
|
|
128
|
+
|
|
129
|
+
<template>
|
|
130
|
+
<div class="cpub-autoform">
|
|
131
|
+
<template v-for="f in fields" :key="f.key">
|
|
132
|
+
<!-- TOGGLE (boolean) -->
|
|
133
|
+
<div v-if="f.control === 'toggle'" class="cpub-autoform-checkbox">
|
|
134
|
+
<input
|
|
135
|
+
:id="controlId(f.key)"
|
|
136
|
+
type="checkbox"
|
|
137
|
+
:checked="!!valueOf(f)"
|
|
138
|
+
@change="onToggle(f, $event)"
|
|
139
|
+
/>
|
|
140
|
+
<label :for="controlId(f.key)">{{ f.label }}</label>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<!-- ARRAY (repeater of object items) -->
|
|
144
|
+
<fieldset v-else-if="f.control === 'array'" class="cpub-autoform-array">
|
|
145
|
+
<legend class="cpub-autoform-label">
|
|
146
|
+
{{ f.label }}
|
|
147
|
+
<span v-if="f.maxItems !== undefined" class="cpub-autoform-count">
|
|
148
|
+
{{ arrayValue(f).length }}/{{ f.maxItems }}
|
|
149
|
+
</span>
|
|
150
|
+
</legend>
|
|
151
|
+
|
|
152
|
+
<p v-if="arrayValue(f).length === 0" class="cpub-autoform-empty">
|
|
153
|
+
No {{ f.label.toLowerCase() }} yet.
|
|
154
|
+
</p>
|
|
155
|
+
|
|
156
|
+
<div
|
|
157
|
+
v-for="(item, i) in arrayValue(f)"
|
|
158
|
+
:key="i"
|
|
159
|
+
class="cpub-autoform-array-item"
|
|
160
|
+
>
|
|
161
|
+
<div class="cpub-autoform-array-item-head">
|
|
162
|
+
<span class="cpub-autoform-array-item-index">{{ i + 1 }}</span>
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
class="cpub-autoform-array-remove"
|
|
166
|
+
:aria-label="`Remove ${f.label.toLowerCase()} ${i + 1}`"
|
|
167
|
+
@click="removeItem(f, i)"
|
|
168
|
+
>
|
|
169
|
+
<i class="fa-solid fa-trash-can" aria-hidden="true"></i>
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
172
|
+
<AdminLayoutsAutoForm
|
|
173
|
+
:fields="f.itemFields ?? []"
|
|
174
|
+
:model-value="item"
|
|
175
|
+
:errors="errors"
|
|
176
|
+
:path-prefix="`${pathOf(f.key)}.${i}`"
|
|
177
|
+
:id-seed="idSeed"
|
|
178
|
+
@update:model-value="updateItem(f, i, $event)"
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
class="cpub-autoform-array-add"
|
|
185
|
+
:disabled="f.maxItems !== undefined && arrayValue(f).length >= f.maxItems"
|
|
186
|
+
@click="addItem(f)"
|
|
187
|
+
>
|
|
188
|
+
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
|
189
|
+
Add {{ f.label.toLowerCase().replace(/s$/, '') }}
|
|
190
|
+
</button>
|
|
191
|
+
</fieldset>
|
|
192
|
+
|
|
193
|
+
<!-- GROUP (nested object) -->
|
|
194
|
+
<fieldset v-else-if="f.control === 'group'" class="cpub-autoform-group">
|
|
195
|
+
<legend class="cpub-autoform-label">{{ f.label }}</legend>
|
|
196
|
+
<AdminLayoutsAutoForm
|
|
197
|
+
:fields="f.fields ?? []"
|
|
198
|
+
:model-value="groupValue(f)"
|
|
199
|
+
:errors="errors"
|
|
200
|
+
:path-prefix="pathOf(f.key)"
|
|
201
|
+
:id-seed="idSeed"
|
|
202
|
+
@update:model-value="setKey(f.key, $event)"
|
|
203
|
+
/>
|
|
204
|
+
</fieldset>
|
|
205
|
+
|
|
206
|
+
<!-- SCALAR FIELDS (text / textarea / number / select / unsupported) -->
|
|
207
|
+
<div v-else class="cpub-autoform-field">
|
|
208
|
+
<label :for="controlId(f.key)" class="cpub-autoform-label">
|
|
209
|
+
{{ f.label }}
|
|
210
|
+
<span v-if="f.required" class="cpub-autoform-required" aria-hidden="true">*</span>
|
|
211
|
+
</label>
|
|
212
|
+
|
|
213
|
+
<select
|
|
214
|
+
v-if="f.control === 'select'"
|
|
215
|
+
:id="controlId(f.key)"
|
|
216
|
+
:value="valueOf(f) ?? ''"
|
|
217
|
+
:aria-invalid="!!errorFor(f.key)"
|
|
218
|
+
:aria-describedby="errorFor(f.key) ? `${controlId(f.key)}-err` : undefined"
|
|
219
|
+
@change="onSelect(f, $event)"
|
|
220
|
+
>
|
|
221
|
+
<!-- Optional fields (no default) get a leading unset option so an
|
|
222
|
+
undefined value reads as "default", not the first real choice. -->
|
|
223
|
+
<option v-if="f.optional" value="">— Default —</option>
|
|
224
|
+
<option v-for="opt in f.options" :key="String(opt.value)" :value="opt.value">
|
|
225
|
+
{{ opt.label }}
|
|
226
|
+
</option>
|
|
227
|
+
</select>
|
|
228
|
+
|
|
229
|
+
<textarea
|
|
230
|
+
v-else-if="f.control === 'textarea'"
|
|
231
|
+
:id="controlId(f.key)"
|
|
232
|
+
:value="String(valueOf(f) ?? '')"
|
|
233
|
+
:maxlength="f.maxLength"
|
|
234
|
+
rows="4"
|
|
235
|
+
:aria-invalid="!!errorFor(f.key)"
|
|
236
|
+
:aria-describedby="errorFor(f.key) ? `${controlId(f.key)}-err` : undefined"
|
|
237
|
+
@input="onText(f, $event)"
|
|
238
|
+
></textarea>
|
|
239
|
+
|
|
240
|
+
<input
|
|
241
|
+
v-else-if="f.control === 'number'"
|
|
242
|
+
:id="controlId(f.key)"
|
|
243
|
+
type="number"
|
|
244
|
+
:value="valueOf(f)"
|
|
245
|
+
:min="f.min"
|
|
246
|
+
:max="f.max"
|
|
247
|
+
:step="f.step"
|
|
248
|
+
:aria-invalid="!!errorFor(f.key)"
|
|
249
|
+
:aria-describedby="errorFor(f.key) ? `${controlId(f.key)}-err` : undefined"
|
|
250
|
+
@input="onNumber(f, $event)"
|
|
251
|
+
/>
|
|
252
|
+
|
|
253
|
+
<input
|
|
254
|
+
v-else-if="f.control === 'text'"
|
|
255
|
+
:id="controlId(f.key)"
|
|
256
|
+
type="text"
|
|
257
|
+
:value="String(valueOf(f) ?? '')"
|
|
258
|
+
:maxlength="f.maxLength"
|
|
259
|
+
autocomplete="off"
|
|
260
|
+
:aria-invalid="!!errorFor(f.key)"
|
|
261
|
+
:aria-describedby="errorFor(f.key) ? `${controlId(f.key)}-err` : undefined"
|
|
262
|
+
@input="onText(f, $event)"
|
|
263
|
+
/>
|
|
264
|
+
|
|
265
|
+
<!-- unsupported control (forward-compat for new Zod kinds) -->
|
|
266
|
+
<p v-else class="cpub-autoform-unsupported">
|
|
267
|
+
This field type isn’t editable here yet.
|
|
268
|
+
</p>
|
|
269
|
+
|
|
270
|
+
<p v-if="errorFor(f.key)" :id="`${controlId(f.key)}-err`" class="cpub-autoform-error" role="alert">
|
|
271
|
+
{{ errorFor(f.key) }}
|
|
272
|
+
</p>
|
|
273
|
+
</div>
|
|
274
|
+
</template>
|
|
275
|
+
</div>
|
|
276
|
+
</template>
|
|
277
|
+
|
|
278
|
+
<style scoped>
|
|
279
|
+
.cpub-autoform {
|
|
280
|
+
display: flex;
|
|
281
|
+
flex-direction: column;
|
|
282
|
+
gap: var(--space-4);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.cpub-autoform-field { display: flex; flex-direction: column; gap: var(--space-1); }
|
|
286
|
+
|
|
287
|
+
.cpub-autoform-label {
|
|
288
|
+
font-family: var(--font-mono);
|
|
289
|
+
font-size: 10px;
|
|
290
|
+
text-transform: uppercase;
|
|
291
|
+
letter-spacing: var(--tracking-wide);
|
|
292
|
+
color: var(--text-dim);
|
|
293
|
+
font-weight: var(--font-weight-semibold);
|
|
294
|
+
}
|
|
295
|
+
.cpub-autoform-required { color: var(--red); margin-left: 2px; }
|
|
296
|
+
.cpub-autoform-count {
|
|
297
|
+
color: var(--text-faint);
|
|
298
|
+
font-weight: var(--font-weight-normal);
|
|
299
|
+
margin-left: var(--space-1);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.cpub-autoform-field input,
|
|
303
|
+
.cpub-autoform-field textarea,
|
|
304
|
+
.cpub-autoform-field select {
|
|
305
|
+
padding: var(--space-2) var(--space-3);
|
|
306
|
+
background: var(--surface);
|
|
307
|
+
border: var(--border-width-default) solid var(--border);
|
|
308
|
+
color: var(--text);
|
|
309
|
+
font-family: var(--font-body);
|
|
310
|
+
font-size: var(--text-sm);
|
|
311
|
+
border-radius: 0;
|
|
312
|
+
}
|
|
313
|
+
.cpub-autoform-field textarea { resize: vertical; }
|
|
314
|
+
.cpub-autoform-field input:focus-visible,
|
|
315
|
+
.cpub-autoform-field textarea:focus-visible,
|
|
316
|
+
.cpub-autoform-field select:focus-visible {
|
|
317
|
+
outline: 2px solid var(--accent);
|
|
318
|
+
outline-offset: 1px;
|
|
319
|
+
border-color: var(--accent);
|
|
320
|
+
}
|
|
321
|
+
/* Invalid state — red border + the inline message below carries the why. */
|
|
322
|
+
.cpub-autoform-field input[aria-invalid='true'],
|
|
323
|
+
.cpub-autoform-field textarea[aria-invalid='true'],
|
|
324
|
+
.cpub-autoform-field select[aria-invalid='true'] {
|
|
325
|
+
border-color: var(--red);
|
|
326
|
+
}
|
|
327
|
+
.cpub-autoform-error {
|
|
328
|
+
font-size: var(--text-xs);
|
|
329
|
+
color: var(--red);
|
|
330
|
+
margin: 0;
|
|
331
|
+
}
|
|
332
|
+
.cpub-autoform-unsupported {
|
|
333
|
+
font-size: var(--text-xs);
|
|
334
|
+
color: var(--text-faint);
|
|
335
|
+
font-style: italic;
|
|
336
|
+
margin: 0;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.cpub-autoform-checkbox { display: flex; align-items: center; gap: var(--space-2); }
|
|
340
|
+
.cpub-autoform-checkbox label { font-size: var(--text-sm); color: var(--text); }
|
|
341
|
+
.cpub-autoform-checkbox input { cursor: pointer; }
|
|
342
|
+
|
|
343
|
+
/* Array repeater + nested group share a bordered card so the nesting is
|
|
344
|
+
visually obvious without indentation drift. */
|
|
345
|
+
.cpub-autoform-array,
|
|
346
|
+
.cpub-autoform-group {
|
|
347
|
+
display: flex;
|
|
348
|
+
flex-direction: column;
|
|
349
|
+
gap: var(--space-3);
|
|
350
|
+
margin: 0;
|
|
351
|
+
/* fieldset defaults to min-inline-size: min-content, which refuses to
|
|
352
|
+
shrink below its content + overflows the 320px inspector. Force it to
|
|
353
|
+
shrink with its container. */
|
|
354
|
+
min-width: 0;
|
|
355
|
+
padding: var(--space-3);
|
|
356
|
+
border: var(--border-width-default) solid var(--border2);
|
|
357
|
+
border-radius: 0;
|
|
358
|
+
}
|
|
359
|
+
.cpub-autoform-array legend,
|
|
360
|
+
.cpub-autoform-group legend { padding: 0 var(--space-1); }
|
|
361
|
+
|
|
362
|
+
.cpub-autoform-empty {
|
|
363
|
+
font-size: var(--text-xs);
|
|
364
|
+
color: var(--text-faint);
|
|
365
|
+
margin: 0;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.cpub-autoform-array-item {
|
|
369
|
+
display: flex;
|
|
370
|
+
flex-direction: column;
|
|
371
|
+
gap: var(--space-2);
|
|
372
|
+
padding: var(--space-3);
|
|
373
|
+
background: var(--surface2);
|
|
374
|
+
border: 1px solid var(--border2);
|
|
375
|
+
}
|
|
376
|
+
.cpub-autoform-array-item-head {
|
|
377
|
+
display: flex;
|
|
378
|
+
align-items: center;
|
|
379
|
+
justify-content: space-between;
|
|
380
|
+
}
|
|
381
|
+
.cpub-autoform-array-item-index {
|
|
382
|
+
font-family: var(--font-mono);
|
|
383
|
+
font-size: var(--text-xs);
|
|
384
|
+
color: var(--text-dim);
|
|
385
|
+
}
|
|
386
|
+
.cpub-autoform-array-remove {
|
|
387
|
+
width: 28px;
|
|
388
|
+
height: 28px;
|
|
389
|
+
display: inline-flex;
|
|
390
|
+
align-items: center;
|
|
391
|
+
justify-content: center;
|
|
392
|
+
background: transparent;
|
|
393
|
+
border: 1px solid var(--border2);
|
|
394
|
+
color: var(--text-dim);
|
|
395
|
+
cursor: pointer;
|
|
396
|
+
font-size: var(--text-xs);
|
|
397
|
+
}
|
|
398
|
+
.cpub-autoform-array-remove:hover { color: var(--red); border-color: var(--red); }
|
|
399
|
+
.cpub-autoform-array-remove:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
400
|
+
|
|
401
|
+
.cpub-autoform-array-add {
|
|
402
|
+
align-self: flex-start;
|
|
403
|
+
display: inline-flex;
|
|
404
|
+
align-items: center;
|
|
405
|
+
gap: var(--space-1);
|
|
406
|
+
padding: var(--space-1) var(--space-3);
|
|
407
|
+
background: var(--surface);
|
|
408
|
+
border: var(--border-width-default) solid var(--border);
|
|
409
|
+
color: var(--text);
|
|
410
|
+
font-family: var(--font-mono);
|
|
411
|
+
font-size: var(--text-xs);
|
|
412
|
+
text-transform: uppercase;
|
|
413
|
+
letter-spacing: var(--tracking-wide);
|
|
414
|
+
cursor: pointer;
|
|
415
|
+
}
|
|
416
|
+
.cpub-autoform-array-add:hover:not(:disabled) { background: var(--surface2); border-color: var(--accent); color: var(--accent); }
|
|
417
|
+
.cpub-autoform-array-add:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
418
|
+
.cpub-autoform-array-add:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
419
|
+
</style>
|