@conduction/nextcloud-vue 1.0.0-beta.17 → 1.0.0-beta.18
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/dist/nextcloud-vue.cjs.js +2442 -1491
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.css +49 -0
- package/dist/nextcloud-vue.esm.js +2381 -1430
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/CnFormPage/CnFormPage.vue +494 -0
- package/src/components/CnFormPage/index.js +4 -0
- package/src/components/CnPageRenderer/pageTypes.js +1 -0
- package/src/components/index.js +1 -0
- package/src/composables/cnFormFieldRenderer.js +256 -0
- package/src/composables/index.js +1 -0
- package/src/schemas/app-manifest.schema.json +7 -2
- package/src/utils/validateManifest.js +36 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.18",
|
|
4
4
|
"description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
|
|
5
5
|
"license": "EUPL-1.2",
|
|
6
6
|
"author": "Conduction B.V. <info@conduction.nl>",
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
CnFormPage — Manifest-driven runtime form.
|
|
3
|
+
|
|
4
|
+
Renders a flat field set + submit button declared in
|
|
5
|
+
`pages[].config` for `type: "form"` pages. Closes the gap that
|
|
6
|
+
forces every consumer's runtime-form route (public surveys, "request
|
|
7
|
+
a quote" pages, ticket-create routes when no detail-page round-trip
|
|
8
|
+
is needed) onto `type: "custom"`.
|
|
9
|
+
|
|
10
|
+
Submit dispatch picks one of two paths based on which prop is set:
|
|
11
|
+
|
|
12
|
+
- `submitEndpoint` — `axios[method](url, formData)`. URL `:param`
|
|
13
|
+
segments are resolved against `$route.params`.
|
|
14
|
+
- `submitHandler` — looks the name up in the customComponents
|
|
15
|
+
registry and calls the resolved value with
|
|
16
|
+
`(formData, $route, $router)`.
|
|
17
|
+
|
|
18
|
+
Field rendering is delegated to `cnRenderFormField` from
|
|
19
|
+
`@conduction/nextcloud-vue/composables` so the same input set
|
|
20
|
+
CnSettingsPage uses (boolean, number, string, password, enum, json)
|
|
21
|
+
is available without duplication. `widget: "textarea"` overrides
|
|
22
|
+
the default string input.
|
|
23
|
+
|
|
24
|
+
Slots (mirrors CnSettingsPage):
|
|
25
|
+
- `#header` — overrides CnPageHeader. Scope `{ title, description }`.
|
|
26
|
+
- `#actions` — right-aligned actions area.
|
|
27
|
+
- `#field-<key>` — replaces the input for a specific field.
|
|
28
|
+
Scope `{ field, value, onInput }`.
|
|
29
|
+
- `#submit` — replaces the submit button. Scope
|
|
30
|
+
`{ submitting, dirty, submit }`.
|
|
31
|
+
|
|
32
|
+
Events:
|
|
33
|
+
- `@input` — `{ key, value }` on every field change.
|
|
34
|
+
- `@submit` — `formData` after successful submit.
|
|
35
|
+
- `@error` — error object after failed submit.
|
|
36
|
+
|
|
37
|
+
Spec: REQ-MFPT-* (manifest-form-page-type).
|
|
38
|
+
-->
|
|
39
|
+
<template>
|
|
40
|
+
<div class="cn-form-page" :data-mode="mode">
|
|
41
|
+
<!--
|
|
42
|
+
@slot header
|
|
43
|
+
@description Replaces the default `CnPageHeader`. Receives `{ title, description }` as scoped props
|
|
44
|
+
so a custom header can mirror the manifest-supplied labels.
|
|
45
|
+
-->
|
|
46
|
+
<slot
|
|
47
|
+
name="header"
|
|
48
|
+
:title="title"
|
|
49
|
+
:description="description">
|
|
50
|
+
<CnPageHeader
|
|
51
|
+
v-if="title"
|
|
52
|
+
:title="title"
|
|
53
|
+
:description="description" />
|
|
54
|
+
</slot>
|
|
55
|
+
|
|
56
|
+
<div v-if="$slots.actions || $scopedSlots.actions" class="cn-form-page__actions">
|
|
57
|
+
<!-- Optional slot for action buttons (back, cancel, etc.) rendered above the form. -->
|
|
58
|
+
<slot name="actions" />
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<!-- Success banner -->
|
|
62
|
+
<div v-if="submitted" class="cn-form-page__success">
|
|
63
|
+
{{ resolveLabel(successMessage) }}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- Form body -->
|
|
67
|
+
<form
|
|
68
|
+
v-if="!submitted || mode !== 'public'"
|
|
69
|
+
class="cn-form-page__form"
|
|
70
|
+
@submit.prevent="submit">
|
|
71
|
+
<div
|
|
72
|
+
v-for="field in fields"
|
|
73
|
+
:key="field.key"
|
|
74
|
+
class="cn-form-page__field"
|
|
75
|
+
:data-field-key="field.key">
|
|
76
|
+
<!--
|
|
77
|
+
@slot field-${field.key}
|
|
78
|
+
@description Per-field override slot. Replaces the auto-rendered input for one specific field.
|
|
79
|
+
Scoped props: `{ field, value, onInput }` — `onInput(v)` updates the field via `updateField`.
|
|
80
|
+
-->
|
|
81
|
+
<slot
|
|
82
|
+
:name="`field-${field.key}`"
|
|
83
|
+
:field="field"
|
|
84
|
+
:value="formData[field.key]"
|
|
85
|
+
:on-input="(v) => updateField(field.key, v)">
|
|
86
|
+
<component
|
|
87
|
+
:is="resolveFieldRender(field).tag"
|
|
88
|
+
v-if="resolveFieldRender(field)"
|
|
89
|
+
v-bind="resolveFieldRender(field).props"
|
|
90
|
+
v-on="resolveFieldRender(field).listeners">
|
|
91
|
+
<!-- NcCheckboxRadioSwitch puts its label in the slot -->
|
|
92
|
+
<template
|
|
93
|
+
v-if="resolveFieldRender(field).kind === 'boolean'">
|
|
94
|
+
{{ resolveFieldRender(field).labelText }}
|
|
95
|
+
</template>
|
|
96
|
+
</component>
|
|
97
|
+
</slot>
|
|
98
|
+
<small
|
|
99
|
+
v-if="field.help"
|
|
100
|
+
class="cn-form-page__field-help">
|
|
101
|
+
{{ resolveLabel(field.help) }}
|
|
102
|
+
</small>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<!-- Error -->
|
|
106
|
+
<p v-if="lastError" class="cn-form-page__error">
|
|
107
|
+
{{ lastError }}
|
|
108
|
+
</p>
|
|
109
|
+
|
|
110
|
+
<div class="cn-form-page__submit">
|
|
111
|
+
<!-- Replaces the default submit button. Scoped props: `{ submitting, dirty, submit }`. -->
|
|
112
|
+
<slot
|
|
113
|
+
name="submit"
|
|
114
|
+
:submitting="submitting"
|
|
115
|
+
:dirty="dirty"
|
|
116
|
+
:submit="submit">
|
|
117
|
+
<NcButton
|
|
118
|
+
type="primary"
|
|
119
|
+
native-type="submit"
|
|
120
|
+
:disabled="submitting">
|
|
121
|
+
<template #icon>
|
|
122
|
+
<NcLoadingIcon v-if="submitting" :size="20" />
|
|
123
|
+
<Send v-else :size="20" />
|
|
124
|
+
</template>
|
|
125
|
+
{{ resolveLabel(submitLabel) }}
|
|
126
|
+
</NcButton>
|
|
127
|
+
</slot>
|
|
128
|
+
</div>
|
|
129
|
+
</form>
|
|
130
|
+
</div>
|
|
131
|
+
</template>
|
|
132
|
+
|
|
133
|
+
<script>
|
|
134
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
135
|
+
import axios from '@nextcloud/axios'
|
|
136
|
+
import { NcButton, NcLoadingIcon } from '@nextcloud/vue'
|
|
137
|
+
import Send from 'vue-material-design-icons/Send.vue'
|
|
138
|
+
import { CnPageHeader } from '../CnPageHeader/index.js'
|
|
139
|
+
import { cnRenderFormField } from '../../composables/cnFormFieldRenderer.js'
|
|
140
|
+
|
|
141
|
+
const ALLOWED_METHODS = ['POST', 'PUT', 'PATCH']
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolve `:param` segments in a URL string against `$route.params`.
|
|
145
|
+
* Mirrors the `:id` substitution Vue Router performs on its own
|
|
146
|
+
* routes; reused here because the manifest declares the URL as a
|
|
147
|
+
* static string and we want the runtime to fill the slot.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} url URL template, e.g. `/api/survey/:token`.
|
|
150
|
+
* @param {object} params $route.params.
|
|
151
|
+
* @return {string}
|
|
152
|
+
*/
|
|
153
|
+
function resolveParams(url, params) {
|
|
154
|
+
if (!url || !params) return url
|
|
155
|
+
return String(url).replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (match, name) => {
|
|
156
|
+
const value = params[name]
|
|
157
|
+
return value === undefined || value === null ? match : encodeURIComponent(String(value))
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @event submit Fired after a successful submit. Payload: `{ formData, response? }` — `response` is present in endpoint mode, omitted in handler mode.
|
|
163
|
+
* @event error Fired when submit fails. Payload: `{ error, formData }`.
|
|
164
|
+
* @event input Fired on every field-level update; payload is the full current `formData` object.
|
|
165
|
+
*
|
|
166
|
+
* @slot header Replaces the default `CnPageHeader`. Scoped props: `{ title, description }`.
|
|
167
|
+
* @slot actions Optional slot for action buttons rendered above the form. Hidden when empty.
|
|
168
|
+
* @slot field-${field.key} Per-field override slot. Replaces the auto-rendered input for one specific field. Scoped props: `{ field, value, onInput }`.
|
|
169
|
+
* @slot submit Replaces the default submit button. Scoped props: `{ submitting, dirty, submit }`.
|
|
170
|
+
*/
|
|
171
|
+
export default {
|
|
172
|
+
name: 'CnFormPage',
|
|
173
|
+
|
|
174
|
+
components: {
|
|
175
|
+
CnPageHeader,
|
|
176
|
+
NcButton,
|
|
177
|
+
NcLoadingIcon,
|
|
178
|
+
Send,
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
inject: {
|
|
182
|
+
/**
|
|
183
|
+
* Custom-component registry from CnAppRoot. Used to resolve the
|
|
184
|
+
* `submitHandler` name to a concrete function. Defaults to an
|
|
185
|
+
* empty object when the page is mounted standalone.
|
|
186
|
+
*
|
|
187
|
+
* @type {object}
|
|
188
|
+
*/
|
|
189
|
+
cnCustomComponents: { default: () => ({}) },
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
props: {
|
|
193
|
+
/** Form fields. Each MUST conform to the `formField` $def. */
|
|
194
|
+
fields: {
|
|
195
|
+
type: Array,
|
|
196
|
+
default: () => [],
|
|
197
|
+
},
|
|
198
|
+
/**
|
|
199
|
+
* Registered submit handler name. Resolves against the
|
|
200
|
+
* `cnCustomComponents` registry (or `customComponents` prop).
|
|
201
|
+
* Mutually exclusive with `submitEndpoint` at the validator
|
|
202
|
+
* level; the component itself prefers `submitEndpoint` when both
|
|
203
|
+
* are set so a stale manifest doesn't crash.
|
|
204
|
+
*/
|
|
205
|
+
submitHandler: {
|
|
206
|
+
type: String,
|
|
207
|
+
default: '',
|
|
208
|
+
},
|
|
209
|
+
/**
|
|
210
|
+
* URL the form data is dispatched to. `:paramName` segments are
|
|
211
|
+
* resolved against `$route.params` at submit time.
|
|
212
|
+
*/
|
|
213
|
+
submitEndpoint: {
|
|
214
|
+
type: String,
|
|
215
|
+
default: '',
|
|
216
|
+
},
|
|
217
|
+
/** HTTP method for endpoint mode. POST | PUT | PATCH. */
|
|
218
|
+
submitMethod: {
|
|
219
|
+
type: String,
|
|
220
|
+
default: 'POST',
|
|
221
|
+
validator: (v) => typeof v === 'string' && ALLOWED_METHODS.includes(v.toUpperCase()),
|
|
222
|
+
},
|
|
223
|
+
/**
|
|
224
|
+
* Form mode. `public` shows the success banner and hides the
|
|
225
|
+
* form on submit; `edit` and `create` keep the form mounted so
|
|
226
|
+
* the consumer can route away.
|
|
227
|
+
*/
|
|
228
|
+
mode: {
|
|
229
|
+
type: String,
|
|
230
|
+
default: 'public',
|
|
231
|
+
validator: (v) => ['edit', 'create', 'public'].includes(v),
|
|
232
|
+
},
|
|
233
|
+
/** i18n key for the submit button label. */
|
|
234
|
+
submitLabel: {
|
|
235
|
+
type: String,
|
|
236
|
+
default: () => t('nextcloud-vue', 'Submit'),
|
|
237
|
+
},
|
|
238
|
+
/** i18n key for the success banner. */
|
|
239
|
+
successMessage: {
|
|
240
|
+
type: String,
|
|
241
|
+
default: () => t('nextcloud-vue', 'Thank you!'),
|
|
242
|
+
},
|
|
243
|
+
/** Pre-filled form state. Consumed by `mode: "edit"`. */
|
|
244
|
+
initialValue: {
|
|
245
|
+
type: Object,
|
|
246
|
+
default: () => ({}),
|
|
247
|
+
},
|
|
248
|
+
/** Page title. Forwarded to CnPageHeader. */
|
|
249
|
+
title: {
|
|
250
|
+
type: String,
|
|
251
|
+
default: '',
|
|
252
|
+
},
|
|
253
|
+
/** Page description. Forwarded to CnPageHeader. */
|
|
254
|
+
description: {
|
|
255
|
+
type: String,
|
|
256
|
+
default: '',
|
|
257
|
+
},
|
|
258
|
+
/**
|
|
259
|
+
* Optional translation function. When provided, applied to
|
|
260
|
+
* field labels, success messages, etc. Defaults to identity.
|
|
261
|
+
*
|
|
262
|
+
* @type {Function|null}
|
|
263
|
+
*/
|
|
264
|
+
translate: {
|
|
265
|
+
type: Function,
|
|
266
|
+
default: null,
|
|
267
|
+
},
|
|
268
|
+
/**
|
|
269
|
+
* Optional explicit custom-component registry. When set, takes
|
|
270
|
+
* precedence over the injected `cnCustomComponents`. Mirrors
|
|
271
|
+
* the resolution order in CnPageRenderer / CnSettingsPage.
|
|
272
|
+
*
|
|
273
|
+
* @type {object|null}
|
|
274
|
+
*/
|
|
275
|
+
customComponents: {
|
|
276
|
+
type: Object,
|
|
277
|
+
default: null,
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Events:
|
|
283
|
+
* @event submit
|
|
284
|
+
* @description Fired after a successful submit (handler returned, or endpoint POST/PUT/PATCH succeeded).
|
|
285
|
+
* Payload is `{ formData, response? }` — `response` is present in endpoint mode, omitted in handler mode.
|
|
286
|
+
*
|
|
287
|
+
* @event error
|
|
288
|
+
* @description Fired when submit fails (handler threw, or endpoint returned a non-2xx response).
|
|
289
|
+
* Payload is `{ error, formData }`.
|
|
290
|
+
*
|
|
291
|
+
* @event input
|
|
292
|
+
* @description Fired on every field-level update; payload is the full current `formData` object.
|
|
293
|
+
* Useful for parent-side reactive previews / autosave hooks.
|
|
294
|
+
*/
|
|
295
|
+
emits: ['submit', 'error', 'input'],
|
|
296
|
+
|
|
297
|
+
data() {
|
|
298
|
+
return {
|
|
299
|
+
formData: this.cloneInitial(),
|
|
300
|
+
submitting: false,
|
|
301
|
+
submitted: false,
|
|
302
|
+
lastError: null,
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
computed: {
|
|
307
|
+
/** Whether any field has changed since mount. */
|
|
308
|
+
dirty() {
|
|
309
|
+
return JSON.stringify(this.formData) !== JSON.stringify(this.cloneInitial())
|
|
310
|
+
},
|
|
311
|
+
/**
|
|
312
|
+
* Effective custom-component registry. Explicit prop wins over
|
|
313
|
+
* the injected value (mirrors CnPageRenderer's resolution).
|
|
314
|
+
*
|
|
315
|
+
* @return {object}
|
|
316
|
+
*/
|
|
317
|
+
effectiveCustomComponents() {
|
|
318
|
+
return this.customComponents ?? this.cnCustomComponents ?? {}
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
watch: {
|
|
323
|
+
initialValue: {
|
|
324
|
+
deep: true,
|
|
325
|
+
handler() {
|
|
326
|
+
this.formData = this.cloneInitial()
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
methods: {
|
|
332
|
+
cloneInitial() {
|
|
333
|
+
try {
|
|
334
|
+
return JSON.parse(JSON.stringify(this.initialValue || {}))
|
|
335
|
+
} catch (_e) {
|
|
336
|
+
return {}
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
resolveLabel(key) {
|
|
341
|
+
if (!key) return ''
|
|
342
|
+
const fn = typeof this.translate === 'function' ? this.translate : (k) => k
|
|
343
|
+
return fn(key)
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Resolve render bindings for a field by delegating to the
|
|
348
|
+
* shared `cnRenderFormField` helper. Memoised inline so the
|
|
349
|
+
* template can call it once per field per render without
|
|
350
|
+
* re-allocating bindings on unrelated re-renders.
|
|
351
|
+
*
|
|
352
|
+
* @param {object} field
|
|
353
|
+
* @return {object|null}
|
|
354
|
+
*/
|
|
355
|
+
resolveFieldRender(field) {
|
|
356
|
+
return cnRenderFormField({
|
|
357
|
+
field,
|
|
358
|
+
value: this.formData[field.key],
|
|
359
|
+
onInput: (next) => this.updateField(field.key, next),
|
|
360
|
+
t: typeof this.translate === 'function' ? this.translate : null,
|
|
361
|
+
})
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
updateField(key, value) {
|
|
365
|
+
this.$set(this.formData, key, value)
|
|
366
|
+
/**
|
|
367
|
+
* Field-level update event.
|
|
368
|
+
*
|
|
369
|
+
* @event input
|
|
370
|
+
* @type {{key: string, value: any}}
|
|
371
|
+
*/
|
|
372
|
+
this.$emit('input', { key, value })
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Dispatch the submit. Picks endpoint mode when `submitEndpoint`
|
|
377
|
+
* is set, otherwise handler mode. When neither is set, emits
|
|
378
|
+
* `@error` with a clear message rather than no-op silently.
|
|
379
|
+
*
|
|
380
|
+
* @return {Promise<void>}
|
|
381
|
+
*/
|
|
382
|
+
async submit() {
|
|
383
|
+
this.lastError = null
|
|
384
|
+
this.submitting = true
|
|
385
|
+
try {
|
|
386
|
+
if (this.submitEndpoint) {
|
|
387
|
+
await this.submitViaEndpoint()
|
|
388
|
+
} else if (this.submitHandler) {
|
|
389
|
+
await this.submitViaHandler()
|
|
390
|
+
} else {
|
|
391
|
+
throw new Error('CnFormPage: no submit destination configured (set submitHandler or submitEndpoint)')
|
|
392
|
+
}
|
|
393
|
+
this.submitted = true
|
|
394
|
+
/**
|
|
395
|
+
* Successful submit event. Payload is the full formData object.
|
|
396
|
+
*
|
|
397
|
+
* @event submit
|
|
398
|
+
* @type {object}
|
|
399
|
+
*/
|
|
400
|
+
this.$emit('submit', this.formData)
|
|
401
|
+
} catch (err) {
|
|
402
|
+
this.lastError = err && err.message ? err.message : String(err)
|
|
403
|
+
/**
|
|
404
|
+
* Submit failure event. Payload is the thrown error / rejected reason.
|
|
405
|
+
*
|
|
406
|
+
* @event error
|
|
407
|
+
* @type {Error}
|
|
408
|
+
*/
|
|
409
|
+
this.$emit('error', err)
|
|
410
|
+
} finally {
|
|
411
|
+
this.submitting = false
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
async submitViaEndpoint() {
|
|
416
|
+
const method = (this.submitMethod || 'POST').toLowerCase()
|
|
417
|
+
const url = resolveParams(this.submitEndpoint, this.$route?.params || {})
|
|
418
|
+
if (typeof axios[method] !== 'function') {
|
|
419
|
+
throw new Error(`CnFormPage: unsupported HTTP method "${this.submitMethod}"`)
|
|
420
|
+
}
|
|
421
|
+
await axios[method](url, this.formData)
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
async submitViaHandler() {
|
|
425
|
+
const handler = this.effectiveCustomComponents[this.submitHandler]
|
|
426
|
+
if (typeof handler !== 'function') {
|
|
427
|
+
// eslint-disable-next-line no-console
|
|
428
|
+
console.warn(
|
|
429
|
+
`[CnFormPage] handler "${this.submitHandler}" not found in customComponents (or not a function). Did you register it?`,
|
|
430
|
+
)
|
|
431
|
+
throw new Error(`CnFormPage: handler "${this.submitHandler}" not registered`)
|
|
432
|
+
}
|
|
433
|
+
await handler(this.formData, this.$route, this.$router)
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
}
|
|
437
|
+
</script>
|
|
438
|
+
|
|
439
|
+
<style>
|
|
440
|
+
.cn-form-page {
|
|
441
|
+
display: flex;
|
|
442
|
+
flex-direction: column;
|
|
443
|
+
gap: 1rem;
|
|
444
|
+
padding: 1rem;
|
|
445
|
+
max-width: 720px;
|
|
446
|
+
margin: 0 auto;
|
|
447
|
+
color: var(--color-main-text);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.cn-form-page__form {
|
|
451
|
+
display: flex;
|
|
452
|
+
flex-direction: column;
|
|
453
|
+
gap: 1rem;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.cn-form-page__field {
|
|
457
|
+
display: flex;
|
|
458
|
+
flex-direction: column;
|
|
459
|
+
gap: 0.25rem;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.cn-form-page__field-help {
|
|
463
|
+
color: var(--color-text-maxcontrast);
|
|
464
|
+
font-size: 0.85em;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.cn-form-page__error {
|
|
468
|
+
color: var(--color-error);
|
|
469
|
+
background: var(--color-error-hover, transparent);
|
|
470
|
+
padding: 0.5rem 0.75rem;
|
|
471
|
+
border-radius: var(--border-radius);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.cn-form-page__success {
|
|
475
|
+
color: var(--color-success-text, var(--color-main-text));
|
|
476
|
+
background: var(--color-success-hover, transparent);
|
|
477
|
+
padding: 1rem;
|
|
478
|
+
border-radius: var(--border-radius);
|
|
479
|
+
text-align: center;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.cn-form-page__actions {
|
|
483
|
+
display: flex;
|
|
484
|
+
justify-content: flex-end;
|
|
485
|
+
gap: 0.5rem;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.cn-form-page__submit {
|
|
489
|
+
display: flex;
|
|
490
|
+
justify-content: flex-start;
|
|
491
|
+
gap: 0.5rem;
|
|
492
|
+
margin-top: 0.5rem;
|
|
493
|
+
}
|
|
494
|
+
</style>
|
|
@@ -53,4 +53,5 @@ export const defaultPageTypes = {
|
|
|
53
53
|
settings: defineAsyncComponent(() => import('../CnSettingsPage/CnSettingsPage.vue').then(m => m.default)),
|
|
54
54
|
chat: defineAsyncComponent(() => import('../CnChatPage/CnChatPage.vue').then(m => m.default)),
|
|
55
55
|
files: defineAsyncComponent(() => import('../CnFilesPage/CnFilesPage.vue').then(m => m.default)),
|
|
56
|
+
form: defineAsyncComponent(() => import('../CnFormPage/CnFormPage.vue').then(m => m.default)),
|
|
56
57
|
}
|
package/src/components/index.js
CHANGED
|
@@ -60,6 +60,7 @@ export { CnLogsPage } from './CnLogsPage/index.js'
|
|
|
60
60
|
export { CnSettingsPage } from './CnSettingsPage/index.js'
|
|
61
61
|
export { CnChatPage } from './CnChatPage/index.js'
|
|
62
62
|
export { CnFilesPage } from './CnFilesPage/index.js'
|
|
63
|
+
export { CnFormPage } from './CnFormPage/index.js'
|
|
63
64
|
export { CnPageRenderer, defaultPageTypes } from './CnPageRenderer/index.js'
|
|
64
65
|
export { CnAppNav } from './CnAppNav/index.js'
|
|
65
66
|
export { CnAppLoading } from './CnAppLoading/index.js'
|