@michaelthielemann/kestrel 2.1.0 → 3.0.1
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 +2 -1
- package/layers/admin/app/components/AdminAccount.vue +3 -3
- package/layers/admin/app/components/AdminNav.vue +1 -1
- package/layers/admin/app/components/BlockFields.vue +2 -2
- package/layers/admin/app/components/BlockPreview.vue +8 -8
- package/layers/admin/app/components/BlockTree.vue +10 -10
- package/layers/admin/app/components/BlocksBody.vue +4 -4
- package/layers/admin/app/components/CollectionDeleteDialog.vue +7 -7
- package/layers/admin/app/components/CollectionEditor.vue +3 -3
- package/layers/admin/app/components/CollectionList.vue +91 -792
- package/layers/admin/app/components/CollectionListBulkBar.vue +48 -0
- package/layers/admin/app/components/CollectionListColumnsPanel.vue +37 -0
- package/layers/admin/app/components/CollectionListFilterPanel.vue +145 -0
- package/layers/admin/app/components/CollectionListPager.vue +70 -0
- package/layers/admin/app/components/CollectionListTable.vue +308 -0
- package/layers/admin/app/components/EditorStatus.vue +4 -4
- package/layers/admin/app/components/FieldsBody.vue +1 -1
- package/layers/admin/app/components/LocaleBar.vue +4 -4
- package/layers/admin/app/components/PageFields.vue +12 -12
- package/layers/admin/app/components/PageFieldsPane.vue +2 -2
- package/layers/admin/app/components/SeoFields.vue +19 -19
- package/layers/admin/app/components/SingletonEditor.vue +7 -7
- package/layers/admin/app/composables/useListBatchActions.ts +61 -0
- package/layers/admin/app/composables/useListFilterDraft.ts +128 -0
- package/layers/admin/app/composables/useListRows.ts +70 -0
- package/layers/admin/app/composables/useListSelection.ts +28 -0
- package/layers/admin/app/composables/useToolbarPanel.ts +38 -0
- package/layers/admin/app/layouts/admin.vue +7 -7
- package/layers/admin/app/pages/admin/[collection]/[id].nuxt.test.ts +2 -2
- package/layers/admin/app/pages/admin/[collection]/[id].vue +11 -11
- package/layers/admin/app/pages/admin/[collection]/index.vue +2 -2
- package/layers/admin/app/pages/admin/[collection]/publish-preview.nuxt.test.ts +1 -1
- package/layers/admin/app/pages/admin/index.vue +3 -3
- package/layers/admin/app/pages/admin/login.vue +6 -6
- package/layers/admin/app/pages/admin/media.vue +1 -1
- package/layers/admin/app/pages/admin/references.vue +3 -3
- package/layers/admin/app/utils/list-cell.ts +46 -0
- package/layers/admin/nuxt.config.ts +3 -0
- package/layers/core/modules/component-namespace/index.ts +20 -0
- package/layers/core/modules/component-namespace/shared.ts +24 -0
- package/layers/core/nuxt.config.ts +1 -0
- package/layers/core/server/utils/defineCollection.ts +0 -1
- package/layers/core/server/utils/serialize-collection.ts +0 -2
- package/layers/fields/app/utils/field-factories.ts +1 -1
- package/layers/media/app/components/MediaConflictDialog.vue +7 -7
- package/layers/media/app/components/MediaDeleteDialog.vue +5 -5
- package/layers/media/app/components/MediaGrid.vue +3 -3
- package/layers/media/app/components/MediaLibrary.vue +20 -20
- package/layers/media/app/components/MediaNewFolderDialog.vue +7 -7
- package/layers/media/app/components/MediaPathBar.vue +1 -1
- package/layers/media/app/components/MediaPicker.vue +3 -3
- package/layers/media/app/components/MediaRenameDialog.vue +8 -8
- package/layers/media/app/components/MediaTable.vue +2 -2
- package/layers/media/app/components/MediaToolbar.vue +4 -4
- package/layers/media/app/components/MediaUploadDialog.vue +9 -9
- package/layers/media/app/components/MediaViewer.vue +14 -14
- package/layers/media/app/components/field/Media.vue +9 -9
- package/layers/media/app/plugins/field-media.client.ts +3 -3
- package/layers/media/nuxt.config.ts +5 -1
- package/layers/public/app/pages/[...slug].vue +2 -2
- package/layers/public/app/pages/__kestrel/preview.vue +1 -1
- package/layers/public/nuxt.config.ts +3 -0
- package/layers/ui/app/components/field/DeadRefNote.vue +1 -1
- package/layers/ui/app/components/field/Repeater.vue +5 -5
- package/layers/ui/app/components/field/Slug.vue +2 -2
- package/layers/ui/app/components/ui/Button.vue +3 -3
- package/layers/ui/app/components/ui/ButtonGroup.vue +2 -2
- package/layers/ui/app/components/ui/Checkbox.vue +2 -2
- package/layers/ui/app/components/ui/Combobox.vue +5 -5
- package/layers/ui/app/components/ui/DatePicker.vue +2 -2
- package/layers/ui/app/components/ui/DateRangePicker.vue +2 -2
- package/layers/ui/app/components/ui/EmptyState.vue +1 -1
- package/layers/ui/app/components/ui/NumberInput.vue +4 -4
- package/layers/ui/app/components/ui/Select.vue +2 -2
- package/layers/ui/app/components/ui/TextInput.vue +5 -5
- package/layers/ui/app/components/ui/Textarea.vue +2 -2
- package/layers/ui/app/components/ui/TimeInput.vue +2 -2
- package/layers/ui/app/components/ui/Toasts.vue +4 -4
- package/layers/ui/app/composables/useToast.ts +1 -1
- package/layers/ui/nuxt.config.ts +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -129,6 +129,7 @@ where to start reading, the cross-layer seams, the gotchas). Then the per-topic
|
|
|
129
129
|
| Doc | Covers |
|
|
130
130
|
|-----|--------|
|
|
131
131
|
| [architecture.md](docs/architecture.md) | **Start here** — the layer model, boot order, cross-cutting seams, per-layer guide |
|
|
132
|
+
| [field-types.md](docs/field-types.md) | Every built-in field type with its options, the column it becomes, what the server enforces vs. what only configures the widget |
|
|
132
133
|
| [consuming-kestrel.md](docs/consuming-kestrel.md) | Using Kestrel in your own app: defining collections/fields/blocks, auto-discovery, the schema lifecycle |
|
|
133
134
|
| [configuration.md](docs/configuration.md) | The single config source (`kestrel.config.ts`), every `KESTREL_*` env var, the auth/session env split |
|
|
134
135
|
| [block-editing.md](docs/block-editing.md) | The block content model + the 3-pane block editor (tree · preview · fields) |
|
|
@@ -153,7 +154,7 @@ The CMS is split into Nuxt layers under `layers/`:
|
|
|
153
154
|
lives in the repo root, dev-only, and is **not** shipped in the package.)
|
|
154
155
|
- **`media`** — uploads, pluggable storage, image derivation, the media library + asset viewer.
|
|
155
156
|
- **`admin`** — the editor SPA: collection list, record editor, the 3-pane block editor.
|
|
156
|
-
- **`public`** — the SSG render path: the catch-all page, `
|
|
157
|
+
- **`public`** — the SSG render path: the catch-all page, `KestrelBlockRenderer`, the JSON-LD head, the literal-key
|
|
157
158
|
artifacts (sitemap / robots / llms.txt / llms-full.txt / redirects.json), deploy.
|
|
158
159
|
|
|
159
160
|
`playground/` is a small consuming example. `templates/starter/` is what the scaffolder writes out;
|
|
@@ -27,7 +27,7 @@ defineExpose({ lang, selectLang, signOut })
|
|
|
27
27
|
<button type="button" class="rail-account__trigger" :aria-label="t('account.label')" :title="t('account.label')">
|
|
28
28
|
<span class="rail-account__avatar" aria-hidden="true">{{ initials }}</span>
|
|
29
29
|
<span class="rail-account__name rail__label">{{ t('account.name') }}</span>
|
|
30
|
-
<
|
|
30
|
+
<KestrelUiIcon name="chevron-down" class="rail-account__caret rail__label" size="1rem" />
|
|
31
31
|
</button>
|
|
32
32
|
</DropdownMenuTrigger>
|
|
33
33
|
|
|
@@ -56,7 +56,7 @@ defineExpose({ lang, selectLang, signOut })
|
|
|
56
56
|
>
|
|
57
57
|
<span class="rail-account__check">
|
|
58
58
|
<DropdownMenuItemIndicator>
|
|
59
|
-
<
|
|
59
|
+
<KestrelUiIcon name="check" size="1rem" />
|
|
60
60
|
</DropdownMenuItemIndicator>
|
|
61
61
|
</span>
|
|
62
62
|
<span>{{ l.toUpperCase() }}</span>
|
|
@@ -66,7 +66,7 @@ defineExpose({ lang, selectLang, signOut })
|
|
|
66
66
|
<DropdownMenuSeparator class="rail-account__sep" />
|
|
67
67
|
|
|
68
68
|
<DropdownMenuItem class="rail-account__item rail-account__item--danger" @select="signOut">
|
|
69
|
-
<
|
|
69
|
+
<KestrelUiIcon name="log-out" size="1rem" class="rail-account__item-icon" />
|
|
70
70
|
<span>{{ t('nav.signOut') }}</span>
|
|
71
71
|
</DropdownMenuItem>
|
|
72
72
|
</DropdownMenuContent>
|
|
@@ -22,7 +22,7 @@ try {
|
|
|
22
22
|
:class="{ 'router-link-active': isNavItemActive(route.path, `/admin/${c.name}`) }"
|
|
23
23
|
:title="collapsed ? (resolveLocalized(c.label?.plural, lang) ?? c.name) : undefined"
|
|
24
24
|
>
|
|
25
|
-
<
|
|
25
|
+
<KestrelUiIcon :name="c.icon ?? 'file-text'" class="rail__icon" size="1.25rem" />
|
|
26
26
|
<span class="rail__label">{{ resolveLocalized(c.label?.plural, lang) ?? c.name }}</span>
|
|
27
27
|
</NuxtLink>
|
|
28
28
|
</nav>
|
|
@@ -5,7 +5,7 @@ import type { LayoutNode } from '../../../core/server/utils/field-layout'
|
|
|
5
5
|
import type { BlockRow } from '../utils/block-tree'
|
|
6
6
|
import { resolveLocalized } from '../../../ui/app/utils/localized'
|
|
7
7
|
|
|
8
|
-
// The fields pane for the currently selected block: delegates to the shared <
|
|
8
|
+
// The fields pane for the currently selected block: delegates to the shared <KestrelFieldLayout> renderer,
|
|
9
9
|
// driven by the selected block's props. `def` is the selected block's resolved definition (the parent
|
|
10
10
|
// already has it). Errors are the id-keyed BlockErrorMap entry for this block (depth-independent); edits
|
|
11
11
|
// emit (key, value) for an id-addressed setProp on the shared tree.
|
|
@@ -33,7 +33,7 @@ const blockLayout = computed(() => (props.def as { fieldLayout?: LayoutNode[] }
|
|
|
33
33
|
<template>
|
|
34
34
|
<div class="block-fields">
|
|
35
35
|
<p class="block-fields__title">{{ resolveLocalized(def?.label, lang) ?? block.type }}</p>
|
|
36
|
-
<
|
|
36
|
+
<KestrelFieldLayout
|
|
37
37
|
:layout="blockLayout"
|
|
38
38
|
:fields="subFields"
|
|
39
39
|
:values="block.props"
|
|
@@ -212,8 +212,8 @@ onUnmounted(() => { ro?.disconnect(); ro = null })
|
|
|
212
212
|
|
|
213
213
|
<div class="block-preview__tools">
|
|
214
214
|
<div class="block-preview__group">
|
|
215
|
-
<
|
|
216
|
-
<
|
|
215
|
+
<KestrelUiTooltip>
|
|
216
|
+
<KestrelUiButton
|
|
217
217
|
type="button"
|
|
218
218
|
variant="ghost"
|
|
219
219
|
size="sm"
|
|
@@ -222,14 +222,14 @@ onUnmounted(() => { ro?.disconnect(); ro = null })
|
|
|
222
222
|
@click="refresh"
|
|
223
223
|
/>
|
|
224
224
|
<template #content>{{ t('preview.refresh') }}</template>
|
|
225
|
-
</
|
|
225
|
+
</KestrelUiTooltip>
|
|
226
226
|
</div>
|
|
227
227
|
|
|
228
228
|
<span class="block-preview__sep" aria-hidden="true" />
|
|
229
229
|
|
|
230
230
|
<div class="block-preview__group" role="group" :aria-label="t('preview.deviceLabel')">
|
|
231
|
-
<
|
|
232
|
-
<
|
|
231
|
+
<KestrelUiTooltip v-for="p in presets" :key="p.key">
|
|
232
|
+
<KestrelUiButton
|
|
233
233
|
type="button"
|
|
234
234
|
variant="ghost"
|
|
235
235
|
size="sm"
|
|
@@ -240,7 +240,7 @@ onUnmounted(() => { ro?.disconnect(); ro = null })
|
|
|
240
240
|
@click="selectPreset(p)"
|
|
241
241
|
/>
|
|
242
242
|
<template #content>{{ t(p.label) }} · {{ p.w }} × {{ p.h === 'auto' ? t('preview.auto') : p.h }}</template>
|
|
243
|
-
</
|
|
243
|
+
</KestrelUiTooltip>
|
|
244
244
|
</div>
|
|
245
245
|
|
|
246
246
|
<span class="block-preview__sep" aria-hidden="true" />
|
|
@@ -253,7 +253,7 @@ onUnmounted(() => { ro?.disconnect(); ro = null })
|
|
|
253
253
|
<div class="block-preview__dims">
|
|
254
254
|
<!-- Either axis can be 'Auto' (fills the pane): typing a number fixes it, clearing the field
|
|
255
255
|
returns it to Auto. Height is Auto under the Desktop preset. -->
|
|
256
|
-
<
|
|
256
|
+
<KestrelUiNumberInput
|
|
257
257
|
slim
|
|
258
258
|
class="block-preview__dim"
|
|
259
259
|
:model-value="width === 'auto' ? null : width"
|
|
@@ -264,7 +264,7 @@ onUnmounted(() => { ro?.disconnect(); ro = null })
|
|
|
264
264
|
@change="onDimCommit('w', $event)"
|
|
265
265
|
/>
|
|
266
266
|
<span class="block-preview__x" aria-hidden="true">×</span>
|
|
267
|
-
<
|
|
267
|
+
<KestrelUiNumberInput
|
|
268
268
|
slim
|
|
269
269
|
class="block-preview__dim"
|
|
270
270
|
:model-value="height === 'auto' ? null : height"
|
|
@@ -47,7 +47,7 @@ function pick(type: string): void {
|
|
|
47
47
|
:aria-pressed="selectedId === null"
|
|
48
48
|
@click="ctx.ops.select(null)"
|
|
49
49
|
>
|
|
50
|
-
<
|
|
50
|
+
<KestrelUiIcon name="file-text" :size="15" class="block-tree__root-icon" />
|
|
51
51
|
<span>{{ t('blocks.page') }}</span>
|
|
52
52
|
</button>
|
|
53
53
|
|
|
@@ -71,13 +71,13 @@ function pick(type: string): void {
|
|
|
71
71
|
>
|
|
72
72
|
<span class="block-tree__node-name">{{ labelOf(block) }}</span>
|
|
73
73
|
<span v-if="errorIds.has(block.id)" class="block-tree__badge" role="img" :aria-label="t('blocks.invalid')" :title="t('blocks.invalid')">!</span>
|
|
74
|
-
<
|
|
74
|
+
<KestrelUiIcon v-if="deadRefIds.has(block.id)" name="triangle-alert" :size="14" class="block-tree__deadbadge" role="img" :aria-label="t('deadRefs.blockBadge')" :title="t('deadRefs.blockBadge')" />
|
|
75
75
|
</button>
|
|
76
76
|
<div class="block-tree__actions">
|
|
77
|
-
<button type="button" class="block-tree__btn" :disabled="disabled || i === 0" :aria-label="t('blocks.moveUp', { n: i + 1 })" @click="ctx.ops.move(block.id, -1)"><
|
|
78
|
-
<button type="button" class="block-tree__btn" :disabled="disabled || i === blocks.length - 1" :aria-label="t('blocks.moveDown', { n: i + 1 })" @click="ctx.ops.move(block.id, 1)"><
|
|
79
|
-
<button type="button" class="block-tree__btn" :disabled="disabled" :aria-label="t('blocks.duplicate', { n: i + 1 })" @click="ctx.ops.duplicate(block.id)"><
|
|
80
|
-
<button type="button" class="block-tree__btn block-tree__btn--danger" :disabled="disabled" :aria-label="t('blocks.remove', { n: i + 1 })" @click="ctx.ops.remove(block.id)"><
|
|
77
|
+
<button type="button" class="block-tree__btn" :disabled="disabled || i === 0" :aria-label="t('blocks.moveUp', { n: i + 1 })" @click="ctx.ops.move(block.id, -1)"><KestrelUiIcon name="chevron-up" :size="15" /></button>
|
|
78
|
+
<button type="button" class="block-tree__btn" :disabled="disabled || i === blocks.length - 1" :aria-label="t('blocks.moveDown', { n: i + 1 })" @click="ctx.ops.move(block.id, 1)"><KestrelUiIcon name="chevron-down" :size="15" /></button>
|
|
79
|
+
<button type="button" class="block-tree__btn" :disabled="disabled" :aria-label="t('blocks.duplicate', { n: i + 1 })" @click="ctx.ops.duplicate(block.id)"><KestrelUiIcon name="copy" :size="15" /></button>
|
|
80
|
+
<button type="button" class="block-tree__btn block-tree__btn--danger" :disabled="disabled" :aria-label="t('blocks.remove', { n: i + 1 })" @click="ctx.ops.remove(block.id)"><KestrelUiIcon name="trash" :size="15" /></button>
|
|
81
81
|
</div>
|
|
82
82
|
</div>
|
|
83
83
|
|
|
@@ -109,13 +109,13 @@ function pick(type: string): void {
|
|
|
109
109
|
:aria-expanded="picking"
|
|
110
110
|
@click="picking = true"
|
|
111
111
|
>
|
|
112
|
-
<
|
|
112
|
+
<KestrelUiIcon name="plus" :size="15" />
|
|
113
113
|
<span>{{ slotName ? t('blocks.addInto', { slot: slotName }) : t('blocks.add') }}</span>
|
|
114
114
|
</button>
|
|
115
115
|
|
|
116
116
|
<!-- The type picker lives in a centered modal (position:fixed) so it escapes the tree pane's
|
|
117
117
|
overflow-y:auto clip; reka's DialogRoot owns Escape/outside-click dismissal + focus restore. -->
|
|
118
|
-
<
|
|
118
|
+
<KestrelUiDialog :open="picking" size="lg" :title="t('blocks.pickType')" @update:open="picking = $event">
|
|
119
119
|
<!-- future: category tabs + a search input mount here, above the grid -->
|
|
120
120
|
<ul class="block-tree__picker" :aria-label="t('blocks.pickType')">
|
|
121
121
|
<li v-for="bt in ctx.allowedTypes" :key="bt.name">
|
|
@@ -127,12 +127,12 @@ function pick(type: string): void {
|
|
|
127
127
|
@click="pick(bt.name)"
|
|
128
128
|
>
|
|
129
129
|
<img v-if="bt.image" :src="bt.image" alt="" loading="lazy" class="block-tree__picker-img" />
|
|
130
|
-
<
|
|
130
|
+
<KestrelUiIcon v-else :name="bt.icon ?? 'layout-grid'" :size="24" class="block-tree__picker-icon" />
|
|
131
131
|
<span class="block-tree__picker-label">{{ resolveLocalized(bt.label, lang) ?? bt.name }}</span>
|
|
132
132
|
</button>
|
|
133
133
|
</li>
|
|
134
134
|
</ul>
|
|
135
|
-
</
|
|
135
|
+
</KestrelUiDialog>
|
|
136
136
|
</div>
|
|
137
137
|
</div>
|
|
138
138
|
</template>
|
|
@@ -69,7 +69,7 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown))
|
|
|
69
69
|
<div class="editor3__tree-head">
|
|
70
70
|
<p class="editor3__pane-label">{{ t('blocks.treeLabel') }}</p>
|
|
71
71
|
</div>
|
|
72
|
-
<
|
|
72
|
+
<KestrelBlockTree
|
|
73
73
|
root
|
|
74
74
|
:blocks="tree.blocks.value"
|
|
75
75
|
:selected-id="selectedId"
|
|
@@ -82,12 +82,12 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown))
|
|
|
82
82
|
|
|
83
83
|
<aside class="editor3__preview" :aria-label="t('preview.ariaLabel')">
|
|
84
84
|
<!-- The "Preview" pane label is folded into BlockPreview's own toolbar row (single-line header). -->
|
|
85
|
-
<
|
|
85
|
+
<KestrelBlockPreview :content="content" :locale="locale" :selected-id="selectedId" @select="tree.select" />
|
|
86
86
|
</aside>
|
|
87
87
|
|
|
88
88
|
<section class="editor3__fields" :aria-label="t('blocks.fieldsLabel')">
|
|
89
89
|
<!-- A block is selected → its fields; otherwise the page (collection) fields + locale switcher. -->
|
|
90
|
-
<
|
|
90
|
+
<KestrelBlockFields
|
|
91
91
|
v-if="selectedBlock"
|
|
92
92
|
:block="selectedBlock"
|
|
93
93
|
:def="byName[selectedBlock.type]"
|
|
@@ -99,7 +99,7 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown))
|
|
|
99
99
|
/>
|
|
100
100
|
<template v-else>
|
|
101
101
|
<p class="editor3__pane-label">{{ t('blocks.pageFields') }}</p>
|
|
102
|
-
<
|
|
102
|
+
<KestrelPageFieldsPane v-bind="ctx.pageFieldsBindings.value" v-on="ctx.pageFieldsHandlers" />
|
|
103
103
|
</template>
|
|
104
104
|
</section>
|
|
105
105
|
</div>
|
|
@@ -11,22 +11,22 @@ const { t } = useT()
|
|
|
11
11
|
</script>
|
|
12
12
|
|
|
13
13
|
<template>
|
|
14
|
-
<
|
|
14
|
+
<KestrelUiDialog :open="open" :title="t('common.delete')" @update:open="(v) => emit('update:open', v)">
|
|
15
15
|
<p v-if="report">{{ t('list.deleteSummary', { n: report.count }) }}</p>
|
|
16
16
|
<div v-if="report && report.referencedCount > 0" class="collection-delete__warn">
|
|
17
17
|
<p>{{ t('list.deleteReferrersWarn', { n: report.referencedCount }) }}</p>
|
|
18
18
|
</div>
|
|
19
19
|
<!-- The referrer lookup FAILED (not "none found"): inbound links are unknown, so caution rather than
|
|
20
20
|
imply a safe delete. Distinct from the reference warning above, which needs a successful check. -->
|
|
21
|
-
<
|
|
21
|
+
<KestrelUiAlert v-if="report && report.checked === false" variant="warning" class="collection-delete__caution">
|
|
22
22
|
{{ t('list.deleteRefsUnverified') }}
|
|
23
|
-
</
|
|
24
|
-
<
|
|
23
|
+
</KestrelUiAlert>
|
|
24
|
+
<KestrelUiAlert v-if="error" variant="error">{{ error }}</KestrelUiAlert>
|
|
25
25
|
<template #footer>
|
|
26
|
-
<
|
|
27
|
-
<
|
|
26
|
+
<KestrelUiButton variant="ghost" :disabled="busy" @click="emit('update:open', false)">{{ t('common.cancel') }}</KestrelUiButton>
|
|
27
|
+
<KestrelUiButton variant="danger" :disabled="busy" @click="emit('confirm')">{{ t('common.delete') }}</KestrelUiButton>
|
|
28
28
|
</template>
|
|
29
|
-
</
|
|
29
|
+
</KestrelUiDialog>
|
|
30
30
|
</template>
|
|
31
31
|
|
|
32
32
|
<style lang="scss" scoped>
|
|
@@ -272,11 +272,11 @@ onUnmounted(() => {
|
|
|
272
272
|
|
|
273
273
|
<!-- The editor body is chosen by the collection's `editor` type (fields · blocks · an extension). -->
|
|
274
274
|
<component :is="bodyComponent" v-if="bodyComponent" />
|
|
275
|
-
<
|
|
275
|
+
<KestrelEditorUnsupported v-else :editor="editorType" />
|
|
276
276
|
|
|
277
277
|
<div v-if="actions" class="editor__actions">
|
|
278
|
-
<
|
|
279
|
-
<
|
|
278
|
+
<KestrelUiButton type="submit" variant="primary" icon="check" :loading="saving">{{ t('common.save') }}</KestrelUiButton>
|
|
279
|
+
<KestrelUiButton type="button" variant="secondary" icon="x" :disabled="saving" @click="emit('cancel')">{{ t('common.cancel') }}</KestrelUiButton>
|
|
280
280
|
</div>
|
|
281
281
|
</form>
|
|
282
282
|
</template>
|