@conduction/nextcloud-vue 0.1.0-beta.14 → 0.1.0-beta.16

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 (46) hide show
  1. package/dist/nextcloud-vue.cjs.js +7282 -3443
  2. package/dist/nextcloud-vue.cjs.js.map +1 -1
  3. package/dist/nextcloud-vue.css +719 -100
  4. package/dist/nextcloud-vue.esm.js +7120 -3300
  5. package/dist/nextcloud-vue.esm.js.map +1 -1
  6. package/package.json +3 -2
  7. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +36 -3
  8. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +34 -19
  9. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +312 -36
  10. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +983 -64
  11. package/src/components/CnAdvancedFormDialog/index.js +3 -0
  12. package/src/components/CnAppLoading/CnAppLoading.vue +93 -0
  13. package/src/components/CnAppLoading/index.js +3 -0
  14. package/src/components/CnAppNav/CnAppNav.vue +269 -0
  15. package/src/components/CnAppNav/index.js +3 -0
  16. package/src/components/CnAppRoot/CnAppRoot.vue +201 -0
  17. package/src/components/CnAppRoot/index.js +3 -0
  18. package/src/components/CnColorPicker/CnColorPicker.vue +251 -0
  19. package/src/components/CnColorPicker/index.js +1 -0
  20. package/src/components/CnContextMenu/CnContextMenu.vue +41 -4
  21. package/src/components/CnDashboardPage/CnDashboardPage.vue +8 -0
  22. package/src/components/CnDependencyMissing/CnDependencyMissing.vue +152 -0
  23. package/src/components/CnDependencyMissing/index.js +3 -0
  24. package/src/components/CnDetailPage/CnDetailPage.vue +27 -16
  25. package/src/components/CnIndexPage/CnIndexPage.vue +36 -6
  26. package/src/components/CnPageRenderer/CnPageRenderer.vue +278 -0
  27. package/src/components/CnPageRenderer/index.js +4 -0
  28. package/src/components/CnPageRenderer/pageTypes.js +37 -0
  29. package/src/components/CnRowActions/CnRowActions.vue +44 -3
  30. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +4 -0
  31. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +103 -74
  32. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +30 -2
  33. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +16 -12
  34. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +9 -4
  35. package/src/components/index.js +7 -1
  36. package/src/composables/index.js +2 -0
  37. package/src/composables/useAppManifest.js +115 -0
  38. package/src/composables/useAppStatus.js +107 -0
  39. package/src/css/CnSchemaFormDialog.css +22 -0
  40. package/src/index.js +24 -2
  41. package/src/schemas/app-manifest.schema.json +153 -0
  42. package/src/types/index.d.ts +9 -0
  43. package/src/types/manifest.d.ts +88 -0
  44. package/src/utils/index.js +1 -1
  45. package/src/utils/schema.js +157 -2
  46. package/src/utils/validateManifest.js +113 -0
@@ -1,67 +1,213 @@
1
1
  <template>
2
- <!-- Edit mode -->
3
- <div
4
- v-if="isEditable && (inputComponent === 'NcCheckboxRadioSwitch' || isEditing)"
5
- class="cn-advanced-form-dialog__value-input-container"
6
- @click.stop>
2
+ <div class="cn-advanced-form-dialog__value-cell-wrapper">
3
+ <!-- Edit mode -->
7
4
  <div
8
- v-if="inputComponent === 'NcCheckboxRadioSwitch'"
9
- class="cn-advanced-form-dialog__boolean-input-row">
10
- <NcCheckboxRadioSwitch
11
- :checked="value"
12
- type="switch"
13
- class="cn-advanced-form-dialog__boolean-input-row__input"
14
- @update:checked="emit($event)">
15
- {{ displayName }}
16
- </NcCheckboxRadioSwitch>
17
- <InformationOutline
18
- v-if="schemaProp && schemaProp.description"
19
- v-tooltip="schemaProp.description"
20
- class="cn-advanced-form-dialog__info-icon"
21
- :size="16" />
5
+ v-if="isEditable && (resolvedWidget === 'boolean' || isEditing)"
6
+ class="cn-advanced-form-dialog__value-input-container"
7
+ @click.stop>
8
+ <div
9
+ v-if="resolvedWidget === 'boolean'"
10
+ class="cn-advanced-form-dialog__boolean-input-row">
11
+ <NcCheckboxRadioSwitch
12
+ :checked="!!value"
13
+ type="switch"
14
+ class="cn-advanced-form-dialog__boolean-input-row__input"
15
+ @update:checked="emit($event)">
16
+ {{ displayName }}
17
+ </NcCheckboxRadioSwitch>
18
+ <InformationOutline
19
+ v-if="schemaProp && schemaProp.description"
20
+ v-tooltip="schemaProp.description"
21
+ class="cn-advanced-form-dialog__info-icon"
22
+ :size="16" />
23
+ </div>
24
+ <div
25
+ v-else-if="resolvedWidget === 'color'"
26
+ class="cn-advanced-form-dialog__color-input-row">
27
+ <CnColorPicker
28
+ :value="chromePickerValue"
29
+ :disable-alpha="!hasAlpha"
30
+ :mode="colorPickerMode"
31
+ @input="onChromeColorInput" />
32
+ <NcTextField
33
+ ref="inputRef"
34
+ :value="colorTextValue"
35
+ :placeholder="colorPlaceholder"
36
+ @update:value="onColorTextInput($event)" />
37
+ </div>
38
+ <NcDateTimePicker
39
+ v-else-if="resolvedWidget === 'datetime'"
40
+ :value="datetimeValue"
41
+ :type="datetimePickerType"
42
+ :placeholder="displayName"
43
+ :input-label="displayName"
44
+ @input="emitDatetime($event)" />
45
+ <NcTextArea
46
+ v-else-if="resolvedWidget === 'textarea'"
47
+ ref="inputRef"
48
+ :value="stringValue"
49
+ :placeholder="displayName"
50
+ :rows="textareaRows"
51
+ :maxlength="maxLengthAttr"
52
+ class="cn-advanced-form-dialog__textarea"
53
+ @update:value="emit($event)" />
54
+ <NcSelect
55
+ v-else-if="resolvedWidget === 'select'"
56
+ :value="effectiveSelectValue"
57
+ :options="effectiveSelectOptions"
58
+ :multiple="effectiveSelectMultiple"
59
+ :taggable="effectiveSelectTaggable"
60
+ :push-tags="effectiveSelectTaggable"
61
+ :close-on-select="!effectiveSelectMultiple"
62
+ :input-label="displayName"
63
+ :placeholder="displayName"
64
+ @input="emitSelect($event)" />
65
+ <CnJsonViewer
66
+ v-else-if="resolvedWidget === 'object'"
67
+ :value="objectJsonString"
68
+ :height="objectEditorHeight"
69
+ language="json"
70
+ @update:value="emitObject($event)" />
71
+ <div
72
+ v-else-if="resolvedWidget === 'objectArray'"
73
+ class="cn-advanced-form-dialog__object-array">
74
+ <div class="cn-advanced-form-dialog__object-array-chips">
75
+ <button
76
+ v-for="(item, idx) in objectArrayItems"
77
+ :key="idx"
78
+ type="button"
79
+ class="cn-advanced-form-dialog__object-array-chip"
80
+ :title="t('nextcloud-vue', 'Edit item')"
81
+ @click.stop="openObjectArrayItem(idx)">
82
+ <span class="cn-advanced-form-dialog__object-array-chip-label">{{ objectArrayItemLabel(item, idx) }}</span>
83
+ <NcButton
84
+ type="tertiary-no-background"
85
+ :aria-label="t('nextcloud-vue', 'Remove item')"
86
+ :title="t('nextcloud-vue', 'Remove item')"
87
+ class="cn-advanced-form-dialog__object-array-chip-remove"
88
+ @click.stop="removeObjectArrayItem(idx)">
89
+ <template #icon>
90
+ <Close :size="14" />
91
+ </template>
92
+ </NcButton>
93
+ </button>
94
+ </div>
95
+ <NcButton
96
+ type="secondary"
97
+ class="cn-advanced-form-dialog__object-array-add"
98
+ @click.stop="openObjectArrayItem(null)">
99
+ <template #icon>
100
+ <Plus :size="16" />
101
+ </template>
102
+ {{ t('nextcloud-vue', 'Add item') }}
103
+ </NcButton>
104
+ <CnAdvancedFormDialog
105
+ v-if="objectArrayDialogOpen"
106
+ :schema="schemaProp.items"
107
+ :item="objectArrayDialogItem"
108
+ :dialog-title="objectArrayDialogTitle"
109
+ :show-metadata-tab="false"
110
+ @confirm="onObjectArrayConfirm"
111
+ @close="closeObjectArrayDialog" />
112
+ </div>
113
+ <NcTextField
114
+ v-else
115
+ ref="inputRef"
116
+ :value="stringValue"
117
+ :type="inputType"
118
+ :placeholder="displayName"
119
+ :min="minimum"
120
+ :max="maximum"
121
+ :step="step"
122
+ :pattern="pattern"
123
+ :minlength="minLengthAttr"
124
+ :maxlength="maxLengthAttr"
125
+ @update:value="emitConverted($event)" />
22
126
  </div>
23
- <NcDateTimePickerNative
24
- v-else-if="inputComponent === 'NcDateTimePickerNative'"
25
- :value="value"
26
- :type="inputType"
27
- :label="displayName"
28
- @update:value="emit($event)" />
29
- <NcTextField
127
+
128
+ <!-- Display mode -->
129
+ <div
30
130
  v-else
31
- ref="inputRef"
32
- :value="stringValue"
33
- :type="inputType"
34
- :placeholder="displayName"
35
- :min="minimum"
36
- :max="maximum"
37
- :step="step"
38
- @update:value="emitConverted($event)" />
39
- </div>
131
+ :title="editabilityWarning">
132
+ <pre
133
+ v-if="typeof value === 'object' && value !== null"
134
+ class="cn-advanced-form-dialog__json-value">{{ formattedObjectValue }}</pre>
135
+ <span
136
+ v-else-if="resolvedWidget === 'datetime' && isValidDate(value)">{{
137
+ formattedDateValue
138
+ }}</span>
139
+ <span
140
+ v-else-if="resolvedWidget === 'color' && value"
141
+ class="cn-advanced-form-dialog__color-display">
142
+ <span
143
+ class="cn-advanced-form-dialog__color-swatch cn-advanced-form-dialog__color-swatch--readonly"
144
+ :style="colorSwatchStyle" />
145
+ <span>{{ displayValue }}</span>
146
+ </span>
147
+ <span v-else>{{ displayValue }}</span>
148
+ </div>
149
+
150
+ <!-- Help text: description + example -->
151
+ <div
152
+ v-if="showHelpText && (helpDescription || helpExample)"
153
+ class="cn-advanced-form-dialog__field-help">
154
+ <span
155
+ v-if="helpDescription"
156
+ class="cn-advanced-form-dialog__field-description">{{ helpDescription }}</span>
157
+ <span
158
+ v-if="helpExample"
159
+ class="cn-advanced-form-dialog__field-example">{{ t('nextcloud-vue', 'e.g.') }} {{ helpExample }}</span>
160
+ </div>
40
161
 
41
- <!-- Display mode -->
42
- <div
43
- v-else
44
- :title="editabilityWarning">
45
- <pre
46
- v-if="typeof value === 'object' && value !== null"
47
- class="cn-advanced-form-dialog__json-value">{{ formattedObjectValue }}</pre>
48
- <span
49
- v-else-if="inputComponent === 'NcDateTimePickerNative' && isValidDate(value)">{{
50
- new Date(value).toLocaleString()
51
- }}</span>
52
- <span v-else>{{ displayValue }}</span>
162
+ <!-- Inline validation error -->
163
+ <div
164
+ v-if="showHelpText && fieldError"
165
+ class="cn-advanced-form-dialog__field-error"
166
+ role="alert">
167
+ {{ fieldError }}
168
+ </div>
53
169
  </div>
54
170
  </template>
55
171
 
56
172
  <script>
173
+ import { translate as t } from '@nextcloud/l10n'
57
174
  import {
58
175
  NcTextField,
176
+ NcTextArea,
59
177
  NcCheckboxRadioSwitch,
60
- NcDateTimePickerNative,
178
+ NcSelect,
179
+ NcButton,
180
+ NcDateTimePicker,
61
181
  } from '@nextcloud/vue'
62
182
  import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
183
+ import Plus from 'vue-material-design-icons/Plus.vue'
184
+ import Close from 'vue-material-design-icons/Close.vue'
63
185
  import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip.js'
64
- import { formatValue } from '../../utils/schema.js'
186
+ import { formatValue, validateValue } from '../../utils/schema.js'
187
+ import CnJsonViewer from '../CnJsonViewer/CnJsonViewer.vue'
188
+ import CnColorPicker from '../CnColorPicker/CnColorPicker.vue'
189
+
190
+ const SUPPORTED_WIDGETS = ['text', 'number', 'boolean', 'datetime', 'textarea', 'array', 'select', 'object', 'objectArray', 'color']
191
+
192
+ /** String formats that map to HTML5 `<input type="url">`. */
193
+ const URL_FORMATS = new Set([
194
+ 'url', 'uri', 'uri-reference', 'iri', 'iri-reference', 'uri-template',
195
+ 'accessUrl', 'shareUrl', 'downloadUrl',
196
+ ])
197
+
198
+ /** All color-related string formats — rendered with the `color` widget (swatch + text input). */
199
+ const COLOR_FORMATS = new Set([
200
+ 'color',
201
+ 'color-hex',
202
+ 'color-hex-alpha',
203
+ 'color-rgb',
204
+ 'color-rgba',
205
+ 'color-hsl',
206
+ 'color-hsla',
207
+ ])
208
+
209
+ /** String formats that render as a multi-line textarea instead of a single-line input. */
210
+ const TEXTAREA_FORMATS = new Set(['html', 'markdown'])
65
211
 
66
212
  export default {
67
213
  name: 'CnPropertyValueCell',
@@ -70,9 +216,18 @@ export default {
70
216
 
71
217
  components: {
72
218
  NcTextField,
219
+ NcTextArea,
73
220
  NcCheckboxRadioSwitch,
74
- NcDateTimePickerNative,
221
+ NcSelect,
222
+ NcButton,
223
+ NcDateTimePicker,
75
224
  InformationOutline,
225
+ Plus,
226
+ Close,
227
+ CnJsonViewer,
228
+ CnColorPicker,
229
+ // Lazy-required to break the circular dep with CnAdvancedFormDialog.
230
+ CnAdvancedFormDialog: () => import('./CnAdvancedFormDialog.vue'),
76
231
  },
77
232
 
78
233
  props: {
@@ -90,6 +245,43 @@ export default {
90
245
  displayName: { type: String, default: '' },
91
246
  /** Editability warning message shown as title when not editable */
92
247
  editabilityWarning: { type: String, default: null },
248
+ /**
249
+ * Override the auto-detected widget for this cell. When null (default),
250
+ * the widget is derived from the schema (boolean/datetime/text/number),
251
+ * with auto-detection for `format: 'text'` (textarea) and `type: 'array'`.
252
+ * Accepted: 'text', 'number', 'boolean', 'datetime', 'textarea', 'array', 'select'.
253
+ */
254
+ widget: {
255
+ type: String,
256
+ default: null,
257
+ validator: (v) => v === null || SUPPORTED_WIDGETS.includes(v),
258
+ },
259
+ /** Options for the `select` widget. Each option may be a string, or `{ id, label }`. */
260
+ selectOptions: { type: Array, default: null },
261
+ /** Whether the `select` widget allows multiple values. */
262
+ selectMultiple: { type: Boolean, default: true },
263
+ /** Number of rows for the `textarea` widget. */
264
+ textareaRows: { type: Number, default: 4 },
265
+ /** CSS height for the `object` widget's CodeMirror editor. */
266
+ objectEditorHeight: { type: String, default: '300px' },
267
+ },
268
+
269
+ data() {
270
+ return {
271
+ /**
272
+ * Optimistic color value while the user is dragging the native picker.
273
+ * Used for the swatch preview to feel instant; the upstream
274
+ * `update:value` emit is debounced so validation/re-renders don't
275
+ * fire on every drag tick.
276
+ */
277
+ pendingColor: null,
278
+ pendingColorTimer: null,
279
+ colorPickerOpen: false,
280
+ /** State for the object-array sub-dialog. `null` index means "add new". */
281
+ objectArrayDialogOpen: false,
282
+ objectArrayDialogIndex: null,
283
+ objectArrayDialogItem: null,
284
+ }
93
285
  },
94
286
 
95
287
  computed: {
@@ -97,14 +289,32 @@ export default {
97
289
  return this.schema?.properties?.[this.propertyKey] || null
98
290
  },
99
291
 
100
- inputComponent() {
292
+ /**
293
+ * Resolved widget after applying explicit override + schema auto-detection.
294
+ * Arrays and string-with-enum become a `select` widget; the array/enum
295
+ * shape is inferred from the schema by the `effectiveSelect*` computeds.
296
+ * @return {string} one of SUPPORTED_WIDGETS
297
+ */
298
+ resolvedWidget() {
299
+ if (this.widget === 'array') return 'select'
300
+ if (this.widget) return this.widget
101
301
  const prop = this.schemaProp
102
- if (!prop) return 'NcTextField'
103
- if (prop.type === 'boolean') return 'NcCheckboxRadioSwitch'
104
- if (prop.type === 'string' && ['date', 'time', 'date-time'].includes(prop.format)) {
105
- return 'NcDateTimePickerNative'
302
+ if (!prop) return 'text'
303
+ if (prop.type === 'boolean') return 'boolean'
304
+ if (prop.type === 'array') {
305
+ if (prop.items?.type === 'object') return 'objectArray'
306
+ return 'select'
106
307
  }
107
- return 'NcTextField'
308
+ if (prop.type === 'object') return 'object'
309
+ if (prop.type === 'string') {
310
+ if (Array.isArray(prop.enum) && prop.enum.length > 0) return 'select'
311
+ const fmt = prop.format || ''
312
+ if (['date', 'time', 'date-time'].includes(fmt)) return 'datetime'
313
+ if (TEXTAREA_FORMATS.has(fmt)) return 'textarea'
314
+ if (COLOR_FORMATS.has(fmt)) return 'color'
315
+ }
316
+ if (prop.type === 'number' || prop.type === 'integer') return 'number'
317
+ return 'text'
108
318
  },
109
319
 
110
320
  inputType() {
@@ -112,16 +322,109 @@ export default {
112
322
  if (!prop) return 'text'
113
323
  const fmt = prop.format || ''
114
324
  if (prop.type === 'string') {
115
- if (fmt === 'date') return 'date'
116
- if (fmt === 'time') return 'time'
117
- if (fmt === 'date-time') return 'datetime-local'
118
- if (fmt === 'email') return 'email'
119
- if (fmt === 'url' || fmt === 'uri') return 'url'
325
+ if (fmt === 'email' || fmt === 'idn-email') return 'email'
326
+ if (URL_FORMATS.has(fmt)) return 'url'
327
+ if (fmt === 'password') return 'password'
328
+ if (fmt === 'telephone' || fmt === 'phone') return 'tel'
120
329
  }
121
330
  if (prop.type === 'number' || prop.type === 'integer') return 'number'
122
331
  return 'text'
123
332
  },
124
333
 
334
+ pattern() {
335
+ const prop = this.schemaProp
336
+ if (!prop || prop.type !== 'string') return undefined
337
+ if (prop.pattern) return prop.pattern
338
+ return undefined
339
+ },
340
+
341
+ colorPlaceholder() {
342
+ const fmt = this.schemaProp?.format
343
+ switch (fmt) {
344
+ case 'color-hex': return '#rrggbb'
345
+ case 'color-hex-alpha': return '#rrggbbaa'
346
+ case 'color-rgb': return 'rgb(0, 0, 0)'
347
+ case 'color-rgba': return 'rgba(0, 0, 0, 1)'
348
+ case 'color-hsl': return 'hsl(0, 0%, 0%)'
349
+ case 'color-hsla': return 'hsla(0, 0%, 0%, 1)'
350
+ default: return this.displayName || '#rrggbb'
351
+ }
352
+ },
353
+
354
+ /** CSS-renderable representation of the current color value (raw value works for all standard formats). */
355
+ colorPreviewValue() {
356
+ if (this.pendingColor) return this.pendingColor
357
+ const v = this.stringValue
358
+ if (!v) return ''
359
+ return v
360
+ },
361
+
362
+ /** Text-field value: shows the optimistic pendingColor while the picker is dragging. */
363
+ colorTextValue() {
364
+ if (this.pendingColor) return this.pendingColor
365
+ return this.stringValue
366
+ },
367
+
368
+ /** True when the schema-declared format includes an alpha channel. */
369
+ hasAlpha() {
370
+ const fmt = this.schemaProp?.format
371
+ return fmt === 'color-hex-alpha' || fmt === 'color-rgba' || fmt === 'color-hsla'
372
+ },
373
+
374
+ /**
375
+ * Lock the picker's numeric-field mode to match the schema format so
376
+ * the user can't edit, say, an `rgba()` value via hex inputs.
377
+ */
378
+ colorPickerMode() {
379
+ const fmt = this.schemaProp?.format
380
+ if (fmt === 'color-rgb' || fmt === 'color-rgba') return 'rgb'
381
+ if (fmt === 'color-hsl' || fmt === 'color-hsla') return 'hsl'
382
+ return 'hex'
383
+ },
384
+
385
+ /**
386
+ * Value passed to vue-color's `Chrome` picker. We feed the picker the
387
+ * latest displayable value (pending or committed) so dragging the
388
+ * picker stays smooth even while the upstream emit is debounced.
389
+ */
390
+ chromePickerValue() {
391
+ const v = this.pendingColor || this.stringValue
392
+ if (v) return v
393
+ return { hex: this.hexColorValue, a: 1 }
394
+ },
395
+
396
+ /**
397
+ * Inline style for the color swatch: layers a solid color over the checker
398
+ * background-image so alpha colors render against the checker pattern.
399
+ */
400
+ colorSwatchStyle() {
401
+ const c = this.colorPreviewValue
402
+ if (!c) return {}
403
+ const fill = `linear-gradient(${c}, ${c})`
404
+ return {
405
+ backgroundImage: `${fill}, var(--cn-color-swatch-checker)`,
406
+ backgroundSize: '100% 100%, 8px 8px',
407
+ backgroundPosition: '0 0, 0 0',
408
+ }
409
+ },
410
+
411
+ /** Hex string used as the value of the native `<input type="color">`. */
412
+ hexColorValue() {
413
+ const v = this.stringValue
414
+ if (!v) return '#000000'
415
+ const trimmed = v.trim()
416
+ const hexMatch = trimmed.match(/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i)
417
+ if (hexMatch) {
418
+ const h = hexMatch[1]
419
+ if (h.length === 3 || h.length === 4) {
420
+ return '#' + h.slice(0, 3).split('').map((c) => c + c).join('')
421
+ }
422
+ return '#' + h.slice(0, 6)
423
+ }
424
+ const computed = this.cssColorToHex(trimmed)
425
+ return computed || '#000000'
426
+ },
427
+
125
428
  minimum() {
126
429
  return this.schemaProp?.minimum
127
430
  },
@@ -137,6 +440,121 @@ export default {
137
440
  return undefined
138
441
  },
139
442
 
443
+ minLengthAttr() {
444
+ const v = this.schemaProp?.minLength
445
+ return typeof v === 'number' ? v : undefined
446
+ },
447
+
448
+ maxLengthAttr() {
449
+ const v = this.schemaProp?.maxLength
450
+ return typeof v === 'number' ? v : undefined
451
+ },
452
+
453
+ showHelpText() {
454
+ return this.isEditable && (this.resolvedWidget === 'boolean' || this.isEditing)
455
+ },
456
+
457
+ helpDescription() {
458
+ const prop = this.schemaProp
459
+ if (!prop) return ''
460
+ return prop.userDescription || prop.description || ''
461
+ },
462
+
463
+ helpExample() {
464
+ const ex = this.schemaProp?.example
465
+ if (ex === undefined || ex === null || ex === '') return ''
466
+ if (typeof ex === 'object') {
467
+ try { return JSON.stringify(ex) } catch { return '' }
468
+ }
469
+ return String(ex)
470
+ },
471
+
472
+ /**
473
+ * Inline validation message for the current value, or null when valid.
474
+ * Required-ness is owned by the parent form so it isn't surfaced here.
475
+ */
476
+ fieldError() {
477
+ const raw = validateValue(this.value, this.schemaProp || {})
478
+ return raw ? t('nextcloud-vue', raw) : null
479
+ },
480
+
481
+ /** Resolved option list for the `select` widget (explicit prop, schema enum, or items.enum). */
482
+ effectiveSelectOptions() {
483
+ if (this.selectOptions) return this.selectOptions
484
+ const prop = this.schemaProp
485
+ if (!prop) return []
486
+ if (Array.isArray(prop.enum) && prop.enum.length > 0) return prop.enum
487
+ if (prop.type === 'array' && Array.isArray(prop.items?.enum) && prop.items.enum.length > 0) {
488
+ return prop.items.enum
489
+ }
490
+ return []
491
+ },
492
+
493
+ /** Whether the `select` widget allows multiple values. */
494
+ effectiveSelectMultiple() {
495
+ if (this.widget === 'select') return this.selectMultiple
496
+ if (this.widget === 'array') return true
497
+ const prop = this.schemaProp
498
+ if (prop?.type === 'array') return true
499
+ return false
500
+ },
501
+
502
+ /** Whether the `select` widget accepts free-form tags (no fixed enum). */
503
+ effectiveSelectTaggable() {
504
+ if (this.widget === 'select') return false
505
+ const prop = this.schemaProp
506
+ const isArray = this.widget === 'array' || prop?.type === 'array'
507
+ return isArray && this.effectiveSelectOptions.length === 0
508
+ },
509
+
510
+ /** Selected-value object(s) for NcSelect, mapping ids back to option objects when needed. */
511
+ effectiveSelectValue() {
512
+ const v = this.value
513
+ const opts = this.effectiveSelectOptions
514
+ const lookup = (id) => {
515
+ const match = opts.find((o) => (typeof o === 'object' ? o.id : o) === id)
516
+ return match !== undefined ? match : id
517
+ }
518
+ if (this.effectiveSelectMultiple) {
519
+ if (!Array.isArray(v)) return []
520
+ return v.map(lookup)
521
+ }
522
+ if (v == null || v === '') return null
523
+ return lookup(v)
524
+ },
525
+
526
+ /** Items array for the `objectArray` widget. Always an array. */
527
+ objectArrayItems() {
528
+ return Array.isArray(this.value) ? this.value : []
529
+ },
530
+
531
+ /** Title shown in the sub-dialog when adding/editing an object item. */
532
+ objectArrayDialogTitle() {
533
+ const itemTitle = this.schemaProp?.items?.title || this.displayName || t('nextcloud-vue', 'Item')
534
+ return this.objectArrayDialogIndex === null
535
+ ? t('nextcloud-vue', 'Add {title}', { title: itemTitle })
536
+ : t('nextcloud-vue', 'Edit {title}', { title: itemTitle })
537
+ },
538
+
539
+ /**
540
+ * NcDateTimePicker `type` mapped from the schema's string `format`.
541
+ * Falls back to `datetime` for unknown date-ish formats.
542
+ */
543
+ datetimePickerType() {
544
+ const fmt = this.schemaProp?.format
545
+ if (fmt === 'date') return 'date'
546
+ if (fmt === 'time') return 'time'
547
+ return 'datetime'
548
+ },
549
+
550
+ /** Current value as a `Date` instance for NcDateTimePicker, or null. */
551
+ datetimeValue() {
552
+ const v = this.value
553
+ if (!v) return null
554
+ const d = new Date(v)
555
+ return Number.isNaN(d.getTime()) ? null : d
556
+ },
557
+
140
558
  stringValue() {
141
559
  const v = this.value
142
560
  if (v == null) return ''
@@ -145,8 +563,23 @@ export default {
145
563
  return String(v)
146
564
  },
147
565
 
566
+ objectJsonString() {
567
+ const v = this.value
568
+ if (v == null) return ''
569
+ if (typeof v === 'string') return v
570
+ try {
571
+ return JSON.stringify(v, null, 2)
572
+ } catch {
573
+ return ''
574
+ }
575
+ },
576
+
148
577
  formattedObjectValue() {
149
- return formatValue(this.value, this.schemaProp || {})
578
+ try {
579
+ return JSON.stringify(this.value, null, 2)
580
+ } catch {
581
+ return formatValue(this.value, this.schemaProp || {})
582
+ }
150
583
  },
151
584
 
152
585
  displayValue() {
@@ -156,17 +589,38 @@ export default {
156
589
  if (v === null || v === undefined || v === '') return '—'
157
590
  return formatValue(v, prop || {})
158
591
  },
592
+
593
+ formattedDateValue() {
594
+ const v = this.value
595
+ if (!v) return ''
596
+ const fmt = this.schemaProp?.format
597
+ const d = new Date(v)
598
+ if (Number.isNaN(d.getTime())) return String(v)
599
+ if (fmt === 'date') return d.toLocaleDateString()
600
+ if (fmt === 'time') return d.toLocaleTimeString()
601
+ return d.toLocaleString()
602
+ },
603
+ },
604
+
605
+ beforeDestroy() {
606
+ if (this.pendingColorTimer) {
607
+ clearTimeout(this.pendingColorTimer)
608
+ this.pendingColorTimer = null
609
+ }
159
610
  },
160
611
 
161
612
  methods: {
613
+ t,
614
+
162
615
  /** Focus the underlying text input (called by parent after row click) */
163
616
  focus() {
164
617
  const ref = this.$refs.inputRef
165
618
  if (!ref) return
166
- const input = ref.$el?.querySelector('input')
619
+ const el = ref.$el || ref
620
+ const input = el?.querySelector?.('input,textarea')
167
621
  if (input) {
168
622
  input.focus()
169
- input.select()
623
+ if (typeof input.select === 'function') input.select()
170
624
  }
171
625
  },
172
626
 
@@ -197,16 +651,323 @@ export default {
197
651
  this.$emit('update:value', converted)
198
652
  },
199
653
 
654
+ /**
655
+ * Emit a `Date` from NcDateTimePicker as the schema-appropriate string:
656
+ * `date` → `YYYY-MM-DD`, `time` → `HH:MM:SS`, `date-time` → ISO 8601.
657
+ * @param {Date|null} date - Date emitted by the picker.
658
+ */
659
+ emitDatetime(date) {
660
+ if (!date) {
661
+ this.$emit('update:value', null)
662
+ return
663
+ }
664
+ const fmt = this.schemaProp?.format
665
+ if (fmt === 'date') {
666
+ this.$emit('update:value', date.toISOString().slice(0, 10))
667
+ return
668
+ }
669
+ if (fmt === 'time') {
670
+ this.$emit('update:value', date.toTimeString().slice(0, 8))
671
+ return
672
+ }
673
+ this.$emit('update:value', date.toISOString())
674
+ },
675
+
676
+ emitObject(jsonString) {
677
+ if (typeof jsonString !== 'string') {
678
+ this.$emit('update:value', jsonString)
679
+ return
680
+ }
681
+ const trimmed = jsonString.trim()
682
+ if (trimmed === '') {
683
+ this.$emit('update:value', null)
684
+ return
685
+ }
686
+ try {
687
+ this.$emit('update:value', JSON.parse(trimmed))
688
+ } catch {
689
+ // Invalid JSON: keep the raw string so the user sees what they typed.
690
+ // CnJsonViewer surfaces a parse error inline; the parent's save path
691
+ // can refuse non-object values if it requires structured data.
692
+ this.$emit('update:value', jsonString)
693
+ }
694
+ },
695
+
696
+ emitSelect(selected) {
697
+ const toId = (item) => (item && typeof item === 'object' ? item.id : item)
698
+ if (this.effectiveSelectMultiple) {
699
+ const arr = Array.isArray(selected) ? selected : []
700
+ const itemType = this.schemaProp?.items?.type
701
+ // Coerce taggable input (always emitted as strings) to the
702
+ // declared `items.type`. Drop entries that fail to coerce.
703
+ const coerced = arr
704
+ .map(toId)
705
+ .map((v) => this.coerceItem(v, itemType))
706
+ .filter((v) => v !== undefined)
707
+ this.$emit('update:value', coerced)
708
+ return
709
+ }
710
+ this.$emit('update:value', selected == null ? null : toId(selected))
711
+ },
712
+
713
+ /**
714
+ * Coerce a raw select-emitted value (typically a string from a
715
+ * taggable NcSelect) into the array's declared `items.type`. Returns
716
+ * `undefined` for entries that can't be coerced so the caller can drop
717
+ * them from the array.
718
+ * @param {*} v - The raw value.
719
+ * @param {string} [itemType] - Schema `items.type` (string, number, integer, boolean).
720
+ * @return {*}
721
+ */
722
+ coerceItem(v, itemType) {
723
+ if (v === null || v === undefined) return v
724
+ // No declared item type — pass through untouched (preserves the
725
+ // shape consumers may have set up via `selectOptions` etc).
726
+ if (!itemType) return v
727
+ // Already the right shape — pass through.
728
+ if (itemType === 'number' && typeof v === 'number') return v
729
+ if (itemType === 'integer' && typeof v === 'number' && Number.isInteger(v)) return v
730
+ if (itemType === 'boolean' && typeof v === 'boolean') return v
731
+ if (itemType === 'string' && typeof v === 'string') return v
732
+ const s = String(v).trim()
733
+ if (s === '') return undefined
734
+ if (itemType === 'number') {
735
+ const n = Number(s)
736
+ return Number.isFinite(n) ? n : undefined
737
+ }
738
+ if (itemType === 'integer') {
739
+ const n = Number(s)
740
+ return Number.isFinite(n) && Number.isInteger(n) ? n : undefined
741
+ }
742
+ if (itemType === 'boolean') {
743
+ if (/^(true|1|yes|on)$/i.test(s)) return true
744
+ if (/^(false|0|no|off)$/i.test(s)) return false
745
+ return undefined
746
+ }
747
+ return s
748
+ },
749
+
750
+ /**
751
+ * Open the sub-dialog to add a new object item or edit an existing
752
+ * one. `idx === null` means add.
753
+ * @param {number|null} idx - Index of the item to edit, or `null`.
754
+ */
755
+ openObjectArrayItem(idx) {
756
+ this.objectArrayDialogIndex = idx
757
+ this.objectArrayDialogItem = idx === null
758
+ ? null
759
+ : JSON.parse(JSON.stringify(this.objectArrayItems[idx] || {}))
760
+ this.objectArrayDialogOpen = true
761
+ },
762
+
763
+ closeObjectArrayDialog() {
764
+ this.objectArrayDialogOpen = false
765
+ this.objectArrayDialogIndex = null
766
+ this.objectArrayDialogItem = null
767
+ },
768
+
769
+ /**
770
+ * Confirmed object from the sub-dialog. Replace the existing item or
771
+ * append a new one, then emit the updated array.
772
+ * @param {object} formData - Form data emitted by CnAdvancedFormDialog.
773
+ */
774
+ onObjectArrayConfirm(formData) {
775
+ const next = [...this.objectArrayItems]
776
+ if (this.objectArrayDialogIndex === null) {
777
+ next.push(formData)
778
+ } else {
779
+ next.splice(this.objectArrayDialogIndex, 1, formData)
780
+ }
781
+ this.$emit('update:value', next)
782
+ this.closeObjectArrayDialog()
783
+ },
784
+
785
+ /**
786
+ * Remove an item from the object array.
787
+ * @param {number} idx - Index of the item to remove.
788
+ */
789
+ removeObjectArrayItem(idx) {
790
+ const next = [...this.objectArrayItems]
791
+ next.splice(idx, 1)
792
+ this.$emit('update:value', next)
793
+ },
794
+
795
+ /**
796
+ * Pick a human-readable label for an item chip. Tries the schema-
797
+ * declared name field first, then the first non-empty primitive
798
+ * property, then falls back to "Item N".
799
+ * @param {object} item - The array item.
800
+ * @param {number} idx - Index of the item (used for fallback label).
801
+ * @return {string}
802
+ */
803
+ objectArrayItemLabel(item, idx) {
804
+ const items = this.schemaProp?.items
805
+ const nameField = items?.objectConfiguration?.objectNameField
806
+ || items?.configuration?.objectNameField
807
+ if (nameField && item && item[nameField] != null && item[nameField] !== '') {
808
+ return String(item[nameField])
809
+ }
810
+ if (item && typeof item === 'object') {
811
+ for (const v of Object.values(item)) {
812
+ if (v == null || v === '') continue
813
+ if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
814
+ return String(v)
815
+ }
816
+ }
817
+ }
818
+ return t('nextcloud-vue', 'Item {n}', { n: idx + 1 })
819
+ },
820
+
200
821
  isValidDate(v) {
201
822
  if (!v) return false
202
823
  const d = new Date(v)
203
824
  return d instanceof Date && !Number.isNaN(d.getTime())
204
825
  },
826
+
827
+ /**
828
+ * Convert any CSS-recognized color string to a 6-digit hex string by
829
+ * round-tripping through a detached DOM node. Returns null when the
830
+ * browser cannot parse the input.
831
+ * @param {string} cssValue - The CSS color value to convert.
832
+ * @return {string|null}
833
+ */
834
+ cssColorToHex(cssValue) {
835
+ try {
836
+ const el = document.createElement('div')
837
+ el.style.color = ''
838
+ el.style.color = cssValue
839
+ if (!el.style.color) return null
840
+ document.body.appendChild(el)
841
+ const rgb = getComputedStyle(el).color
842
+ document.body.removeChild(el)
843
+ const m = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/)
844
+ if (!m) return null
845
+ const toHex = (n) => parseInt(n, 10).toString(16).padStart(2, '0')
846
+ return `#${toHex(m[1])}${toHex(m[2])}${toHex(m[3])}`
847
+ } catch {
848
+ return null
849
+ }
850
+ },
851
+
852
+ /**
853
+ * Continuous handler for vue-color's `Chrome` picker. Updates the
854
+ * swatch + text input synchronously via `pendingColor`, but debounces
855
+ * the upstream `update:value` emit so validation and parent re-renders
856
+ * don't fire on every drag tick.
857
+ * @param {object} color - vue-color emitted color object.
858
+ * @param {string} color.hex - `#rrggbb`.
859
+ * @param {string} [color.hex8] - `#rrggbbaa`, when alpha is enabled.
860
+ * @param {object} color.rgba - RGBA components (`r`, `g`, `b`, `a`).
861
+ * @param {object} color.hsl - HSL components (`h`, `s`, `l`, `a`).
862
+ * @param {number} [color.a] - Alpha 0–1.
863
+ */
864
+ onChromeColorInput(color) {
865
+ this.pendingColor = this.chromeColorToFormatValue(color)
866
+ if (this.pendingColorTimer) clearTimeout(this.pendingColorTimer)
867
+ this.pendingColorTimer = setTimeout(() => this.flushPendingColor(), 120)
868
+ },
869
+
870
+ /**
871
+ * Translate vue-color's emitted color object into the schema-declared
872
+ * color format string.
873
+ * @param {object} color - vue-color color object.
874
+ * @return {string}
875
+ */
876
+ chromeColorToFormatValue(color) {
877
+ const fmt = this.schemaProp?.format || 'color-hex'
878
+ const { rgba, hex, hex8 } = color || {}
879
+ const r = rgba?.r ?? 0
880
+ const g = rgba?.g ?? 0
881
+ const b = rgba?.b ?? 0
882
+ const a = rgba?.a ?? color?.a ?? 1
883
+ if (fmt === 'color-hex' || fmt === 'color') return (hex || '#000000').toLowerCase()
884
+ if (fmt === 'color-hex-alpha') {
885
+ if (hex8) return hex8.toLowerCase()
886
+ const aHex = Math.round(a * 255).toString(16).padStart(2, '0')
887
+ return ((hex || '#000000') + aHex).toLowerCase()
888
+ }
889
+ if (fmt === 'color-rgb') return `rgb(${r}, ${g}, ${b})`
890
+ if (fmt === 'color-rgba') return `rgba(${r}, ${g}, ${b}, ${this.formatAlpha(a)})`
891
+ if (fmt === 'color-hsl' || fmt === 'color-hsla') {
892
+ const { h, s, l } = this.rgbToHsl(r, g, b)
893
+ if (fmt === 'color-hsla') {
894
+ return `hsla(${h}, ${s}%, ${l}%, ${this.formatAlpha(a)})`
895
+ }
896
+ return `hsl(${h}, ${s}%, ${l}%)`
897
+ }
898
+ return hex || '#000000'
899
+ },
900
+
901
+ /** Emit the pending color upstream and clear local state. */
902
+ flushPendingColor() {
903
+ if (this.pendingColorTimer) {
904
+ clearTimeout(this.pendingColorTimer)
905
+ this.pendingColorTimer = null
906
+ }
907
+ if (this.pendingColor === null) return
908
+ const out = this.pendingColor
909
+ this.pendingColor = null
910
+ this.$emit('update:value', out)
911
+ },
912
+
913
+ /**
914
+ * Manual text-field edit for a color value. Cancels any in-flight
915
+ * picker debounce so the typed value isn't immediately overwritten.
916
+ * @param {string} v - The new text value.
917
+ */
918
+ onColorTextInput(v) {
919
+ if (this.pendingColorTimer) {
920
+ clearTimeout(this.pendingColorTimer)
921
+ this.pendingColorTimer = null
922
+ }
923
+ this.pendingColor = null
924
+ this.$emit('update:value', v)
925
+ },
926
+
927
+ /**
928
+ * Format an alpha 0–1 for CSS output (max 2 decimals, drops trailing zeros).
929
+ * @param {number} a - Alpha in 0–1 range.
930
+ * @return {string}
931
+ */
932
+ formatAlpha(a) {
933
+ return Number.isInteger(a) ? String(a) : a.toFixed(2).replace(/\.?0+$/, '') || '0'
934
+ },
935
+
936
+ rgbToHsl(r, g, b) {
937
+ const rN = r / 255
938
+ const gN = g / 255
939
+ const bN = b / 255
940
+ const max = Math.max(rN, gN, bN)
941
+ const min = Math.min(rN, gN, bN)
942
+ const l = (max + min) / 2
943
+ let h = 0
944
+ let s = 0
945
+ if (max !== min) {
946
+ const d = max - min
947
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
948
+ switch (max) {
949
+ case rN: h = (gN - bN) / d + (gN < bN ? 6 : 0); break
950
+ case gN: h = (bN - rN) / d + 2; break
951
+ case bN: h = (rN - gN) / d + 4; break
952
+ }
953
+ h /= 6
954
+ }
955
+ return {
956
+ h: Math.round(h * 360),
957
+ s: Math.round(s * 100),
958
+ l: Math.round(l * 100),
959
+ }
960
+ },
205
961
  },
206
962
  }
207
963
  </script>
208
964
 
209
965
  <style scoped>
966
+ .cn-advanced-form-dialog__value-input-container {
967
+ width: 100%;
968
+ min-width: 0;
969
+ }
970
+
210
971
  .cn-advanced-form-dialog__value-input-container :deep(.text-field) {
211
972
  margin: 0;
212
973
  padding: 0;
@@ -218,6 +979,16 @@ export default {
218
979
  gap: 6px;
219
980
  }
220
981
 
982
+ .cn-advanced-form-dialog__array-input-row {
983
+ display: flex;
984
+ align-items: center;
985
+ gap: 6px;
986
+ }
987
+
988
+ .cn-advanced-form-dialog__array-input-row > :first-child {
989
+ flex: 1;
990
+ }
991
+
221
992
  /* patch extreme size in field */
222
993
  .cn-advanced-form-dialog__boolean-input-row__input > span {
223
994
  padding-left: 0;
@@ -244,4 +1015,152 @@ export default {
244
1015
  border-radius: 4px;
245
1016
  margin: 0;
246
1017
  }
1018
+
1019
+ /* Textarea: keep it inside the table cell instead of overflowing.
1020
+ NcTextArea's wrapper has a fixed height (`calc(var(--default-clickable-area) * 2)`)
1021
+ that ignores the `rows` attribute, which causes the textarea to render with a
1022
+ too-small viewport. Let the wrapper grow with its content instead. */
1023
+ .cn-advanced-form-dialog__textarea {
1024
+ width: 100%;
1025
+ box-sizing: border-box;
1026
+ }
1027
+
1028
+ .cn-advanced-form-dialog__textarea :deep(.textarea__main-wrapper) {
1029
+ height: auto;
1030
+ min-height: var(--default-clickable-area);
1031
+ }
1032
+
1033
+ .cn-advanced-form-dialog__textarea :deep(textarea) {
1034
+ width: 100%;
1035
+ max-width: 100%;
1036
+ box-sizing: border-box;
1037
+ resize: vertical;
1038
+ min-height: 80px;
1039
+ max-height: 240px;
1040
+ display: block;
1041
+ }
1042
+
1043
+ /* Color widget: clickable swatch (native picker) + visible text input */
1044
+ .cn-advanced-form-dialog__color-input-row {
1045
+ display: flex;
1046
+ align-items: center;
1047
+ gap: 8px;
1048
+ width: 100%;
1049
+ }
1050
+
1051
+ .cn-advanced-form-dialog__color-input-row > :last-child {
1052
+ flex: 1;
1053
+ min-width: 0;
1054
+ }
1055
+
1056
+ .cn-advanced-form-dialog__color-swatch {
1057
+ --cn-color-swatch-checker:
1058
+ linear-gradient(45deg, var(--color-background-dark) 25%, transparent 25%),
1059
+ linear-gradient(-45deg, var(--color-background-dark) 25%, transparent 25%),
1060
+ linear-gradient(45deg, transparent 75%, var(--color-background-dark) 75%),
1061
+ linear-gradient(-45deg, transparent 75%, var(--color-background-dark) 75%);
1062
+ display: inline-block;
1063
+ width: 32px;
1064
+ height: 32px;
1065
+ flex-shrink: 0;
1066
+ padding: 0;
1067
+ border: 1px solid var(--color-border);
1068
+ border-radius: var(--border-radius);
1069
+ cursor: pointer;
1070
+ background-image: var(--cn-color-swatch-checker);
1071
+ background-size: 8px 8px;
1072
+ background-position: 0 0, 0 4px, 4px -4px, -4px 0;
1073
+ overflow: hidden;
1074
+ position: relative;
1075
+ }
1076
+
1077
+ .cn-advanced-form-dialog__color-swatch:focus-visible {
1078
+ outline: 2px solid var(--color-primary-element);
1079
+ outline-offset: 2px;
1080
+ }
1081
+
1082
+ .cn-advanced-form-dialog__color-swatch--readonly {
1083
+ cursor: default;
1084
+ width: 20px;
1085
+ height: 20px;
1086
+ vertical-align: middle;
1087
+ margin-inline-end: 6px;
1088
+ }
1089
+
1090
+ .cn-advanced-form-dialog__color-display {
1091
+ display: inline-flex;
1092
+ align-items: center;
1093
+ }
1094
+
1095
+ /* Help text & inline validation under the input */
1096
+ .cn-advanced-form-dialog__field-help {
1097
+ display: flex;
1098
+ flex-direction: column;
1099
+ gap: 2px;
1100
+ margin-top: 4px;
1101
+ font-size: 0.85em;
1102
+ color: var(--color-text-maxcontrast);
1103
+ }
1104
+
1105
+ .cn-advanced-form-dialog__field-example {
1106
+ font-style: italic;
1107
+ }
1108
+
1109
+ .cn-advanced-form-dialog__field-error {
1110
+ margin-top: 4px;
1111
+ font-size: 0.85em;
1112
+ color: var(--color-error-text);
1113
+ }
1114
+
1115
+ .cn-advanced-form-dialog__value-cell-wrapper {
1116
+ display: flex;
1117
+ flex-direction: column;
1118
+ width: 100%;
1119
+ }
1120
+
1121
+ /* Object-array widget: chip list + add button */
1122
+ .cn-advanced-form-dialog__object-array {
1123
+ display: flex;
1124
+ flex-direction: column;
1125
+ gap: 8px;
1126
+ width: 100%;
1127
+ }
1128
+
1129
+ .cn-advanced-form-dialog__object-array-chips {
1130
+ display: flex;
1131
+ flex-wrap: wrap;
1132
+ gap: 6px;
1133
+ }
1134
+
1135
+ .cn-advanced-form-dialog__object-array-chip {
1136
+ display: inline-flex;
1137
+ align-items: center;
1138
+ gap: 4px;
1139
+ padding: 2px 4px 2px 10px;
1140
+ background: var(--color-background-dark);
1141
+ border: 1px solid var(--color-border);
1142
+ border-radius: var(--border-radius-pill, 14px);
1143
+ color: var(--color-main-text);
1144
+ cursor: pointer;
1145
+ font: inherit;
1146
+ max-width: 240px;
1147
+ }
1148
+
1149
+ .cn-advanced-form-dialog__object-array-chip:hover {
1150
+ background: var(--color-background-hover);
1151
+ }
1152
+
1153
+ .cn-advanced-form-dialog__object-array-chip-label {
1154
+ overflow: hidden;
1155
+ text-overflow: ellipsis;
1156
+ white-space: nowrap;
1157
+ }
1158
+
1159
+ .cn-advanced-form-dialog__object-array-chip-remove {
1160
+ flex-shrink: 0;
1161
+ }
1162
+
1163
+ .cn-advanced-form-dialog__object-array-add {
1164
+ align-self: flex-start;
1165
+ }
247
1166
  </style>