@michaelthielemann/kestrel 2.0.0 → 3.0.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 +16 -8
- 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 -788
- 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 +17 -12
- package/layers/admin/app/components/PageFieldsPane.vue +2 -2
- package/layers/admin/app/components/SeoFields.vue +52 -10
- package/layers/admin/app/components/SingletonEditor.vue +7 -7
- package/layers/admin/app/composables/useEditForm.ts +9 -3
- 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/edit-form.ts +9 -2
- 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/modules/kestrel/index.ts +11 -0
- package/layers/core/nuxt.config.ts +1 -0
- package/layers/core/server/api/[collection]/index.put.test.ts +76 -0
- package/layers/core/server/api/[collection]/index.put.ts +18 -1
- package/layers/core/server/utils/collection-types.ts +4 -3
- package/layers/core/server/utils/defineCollection.ts +8 -2
- package/layers/core/server/utils/kestrel-config.ts +35 -1
- package/layers/core/server/utils/seo.ts +18 -0
- package/layers/core/server/utils/serialize-collection.ts +0 -2
- package/layers/core/server/utils/write-effects.ts +40 -0
- package/layers/fields/app/utils/field-factories.ts +1 -1
- package/layers/fields/server/utils/buildCollection.ts +9 -7
- package/layers/media/app/components/KestrelImg.vue +29 -0
- 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 +26 -24
- 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 +64 -15
- package/layers/media/app/components/field/Media.vue +9 -9
- package/layers/media/app/plugins/field-media.client.ts +3 -3
- package/layers/media/app/utils/ai-disclosure.ts +19 -0
- package/layers/media/app/utils/library.ts +2 -0
- package/layers/media/nuxt.config.ts +5 -1
- package/layers/media/server/api/media/[id].patch.test.ts +66 -0
- package/layers/media/server/api/media/[id].patch.ts +25 -1
- package/layers/media/server/api/media/index.post.ts +19 -3
- package/layers/media/server/collections/media.ts +14 -0
- package/layers/media/server/utils/ai-disclosure-enabled.ts +16 -0
- package/layers/media/server/utils/ai-signal-detect.ts +155 -0
- package/layers/media/server/utils/library.ts +2 -1
- package/layers/media/server/utils/record.ts +8 -0
- package/layers/media/server/utils/resolve.ts +12 -0
- package/layers/public/app/pages/[...slug].vue +32 -3
- package/layers/public/app/pages/__kestrel/preview.vue +1 -1
- package/layers/public/app/utils/json-ld.ts +139 -0
- package/layers/public/modules/deploy-output/deploy-output.ts +19 -5
- package/layers/public/modules/prerender-routes/index.ts +5 -2
- package/layers/public/nuxt.config.ts +3 -0
- package/layers/public/server/api/route.get.ts +9 -1
- package/layers/public/server/collections/redirects.ts +75 -0
- package/layers/public/server/plugins/03.redirects.ts +37 -0
- package/layers/public/server/routes/llms-full.txt.get.ts +99 -0
- package/layers/public/server/routes/llms.txt.get.ts +1 -12
- package/layers/public/server/routes/redirects.json.get.ts +58 -0
- package/layers/public/server/routes/robots.txt.get.ts +1 -0
- package/layers/public/server/utils/llms-full.ts +125 -0
- package/layers/public/server/utils/llms.ts +13 -0
- package/layers/public/server/utils/page-resolve.ts +112 -4
- package/layers/public/server/utils/publish/invalidation.ts +48 -3
- package/layers/public/server/utils/publish/publisher.ts +11 -5
- package/layers/public/server/utils/publish/redirect-rules.ts +221 -0
- package/layers/public/server/utils/publish/redirects-artifact.ts +20 -0
- package/layers/public/server/utils/richtext-markdown.ts +260 -0
- package/layers/public/server/utils/site-url.ts +8 -0
- package/layers/public/server/utils/sitemap.ts +5 -3
- package/layers/ui/app/components/field/Choice.vue +6 -1
- package/layers/ui/app/components/field/DeadRefNote.vue +1 -1
- package/layers/ui/app/components/field/Slug.vue +2 -2
- package/layers/ui/app/components/ui/Button.vue +1 -1
- package/layers/ui/app/components/ui/EmptyState.vue +1 -1
- package/layers/ui/app/components/ui/TextInput.vue +1 -1
- package/layers/ui/app/components/ui/Toasts.vue +1 -1
- package/layers/ui/app/composables/useToast.ts +1 -1
- package/layers/ui/app/i18n/de.ts +13 -0
- package/layers/ui/app/i18n/en.ts +13 -0
- package/layers/ui/nuxt.config.ts +5 -1
- package/package.json +2 -1
- package/templates/starter/nuxt.config.ts +3 -0
package/README.md
CHANGED
|
@@ -23,7 +23,8 @@ static host. It is deliberately **not**:
|
|
|
23
23
|
- **SQLite only** — one database, single instance. (A Postgres `Dialect` seam exists; no impl is active.)
|
|
24
24
|
- **No public data API** — only published *page-like* records have a read surface (mirroring the static
|
|
25
25
|
output); everything else stays behind the admin guard. No per-request rate limiting / live API.
|
|
26
|
-
- **No runtime redirect engine** —
|
|
26
|
+
- **No runtime redirect engine** — redirects are *authored* in the CMS and published as a `redirects.json`
|
|
27
|
+
artifact, but Kestrel never answers a 30x itself; an edge (NGINX / njs / CloudFront) has to read it.
|
|
27
28
|
- **No per-file access control on uploads.** The admin guard protects the media *library* — listing,
|
|
28
29
|
editing, deleting — but not the bytes. With `media.driver: 'local'` the files are served from the app
|
|
29
30
|
origin by Nitro's static handler, which runs ahead of every middleware, so anyone who knows a URL can
|
|
@@ -49,13 +50,18 @@ static host. It is deliberately **not**:
|
|
|
49
50
|
- **Block page-builder** — a 3-pane editor (tree · live preview · fields) with nestable block slots; each
|
|
50
51
|
block is a single `app/blocks/*.vue` SFC (schema via field-factory `defineProps` + the display template).
|
|
51
52
|
- **Media library** — upload (magic-byte sniff + SVG sanitize), local or S3 storage, responsive WebP
|
|
52
|
-
derivatives + thumbhash, folders, and a full-screen asset viewer.
|
|
53
|
+
derivatives + thumbhash, folders, and a full-screen asset viewer. Optional **EU AI Act (Art. 50)
|
|
54
|
+
disclosure** fields per asset, with an upload-time scan for AI-origin signals (off by default).
|
|
53
55
|
- **Multilingual content** — optional per-record translations, an editor locale flow, locale-prefixed
|
|
54
56
|
routing, hreflang alternates in the sitemap.
|
|
55
57
|
- **Static output** — `nuxt generate` (full rebuild) or a runtime **incremental publisher** (re-renders
|
|
56
|
-
only the pages a write affects), to a local dir or S3; emits `sitemap.xml`, `robots.txt`,
|
|
57
|
-
**`llms.txt`** (an [llmstxt.org](https://llmstxt.org) site map so AI agents grasp the site)
|
|
58
|
-
in-dashboard preview renders straight from the origin.
|
|
58
|
+
only the pages a write affects), to a local dir or S3; emits `sitemap.xml`, `robots.txt`,
|
|
59
|
+
**`llms.txt`** (an [llmstxt.org](https://llmstxt.org) site map so AI agents grasp the site) and
|
|
60
|
+
`redirects.json`. A live in-dashboard preview renders straight from the origin.
|
|
61
|
+
- **Search + answer engines** — canonical / Open Graph / hreflang tags and a schema.org JSON-LD graph
|
|
62
|
+
(`WebSite` + `WebPage`/`Article` + `BreadcrumbList`) on every page, no wiring. Two opt-in extras publish
|
|
63
|
+
more than the page already showed: `seo.articleMeta` (author / publication date / keywords) and
|
|
64
|
+
`seo.llmsFull` (`/llms-full.txt`, every published page's body in one document).
|
|
59
65
|
- **Reference integrity** — writes invalidate exactly the affected static pages; dead-reference warnings;
|
|
60
66
|
required + globally-unique page slugs per locale.
|
|
61
67
|
|
|
@@ -123,12 +129,13 @@ where to start reading, the cross-layer seams, the gotchas). Then the per-topic
|
|
|
123
129
|
| Doc | Covers |
|
|
124
130
|
|-----|--------|
|
|
125
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 |
|
|
126
133
|
| [consuming-kestrel.md](docs/consuming-kestrel.md) | Using Kestrel in your own app: defining collections/fields/blocks, auto-discovery, the schema lifecycle |
|
|
127
134
|
| [configuration.md](docs/configuration.md) | The single config source (`kestrel.config.ts`), every `KESTREL_*` env var, the auth/session env split |
|
|
128
135
|
| [block-editing.md](docs/block-editing.md) | The block content model + the 3-pane block editor (tree · preview · fields) |
|
|
129
|
-
| [media-uploads.md](docs/media-uploads.md) | Ingest security, storage drivers (local/S3), responsive-image derivation |
|
|
136
|
+
| [media-uploads.md](docs/media-uploads.md) | Ingest security, storage drivers (local/S3), responsive-image derivation, EU AI Act disclosure |
|
|
130
137
|
| [multilingual.md](docs/multilingual.md) | Content locales, the editor locale flow, locale-prefixed routing |
|
|
131
|
-
| [static-output.md](docs/static-output.md) | `nuxt generate` + the runtime incremental publisher, the live editor preview, `sitemap.xml` / `robots.txt` / `llms.txt`, the optional S3 deploy |
|
|
138
|
+
| [static-output.md](docs/static-output.md) | `nuxt generate` + the runtime incremental publisher, the live editor preview, `sitemap.xml` / `robots.txt` / `llms.txt` / `llms-full.txt`, the JSON-LD structured data, CMS-managed redirects, the optional S3 deploy |
|
|
132
139
|
| [reference-integrity.md](docs/reference-integrity.md) | How writes invalidate the static site precisely, dead-reference warnings (a dead link renders `#`, and the editor is warned), and required/unique page slugs |
|
|
133
140
|
| [architecture-decisions.md](docs/architecture-decisions.md) | ADRs (the collection-derived schema engine, reference integrity, the auth/password choice) |
|
|
134
141
|
|
|
@@ -147,7 +154,8 @@ The CMS is split into Nuxt layers under `layers/`:
|
|
|
147
154
|
lives in the repo root, dev-only, and is **not** shipped in the package.)
|
|
148
155
|
- **`media`** — uploads, pluggable storage, image derivation, the media library + asset viewer.
|
|
149
156
|
- **`admin`** — the editor SPA: collection list, record editor, the 3-pane block editor.
|
|
150
|
-
- **`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
|
|
158
|
+
artifacts (sitemap / robots / llms.txt / llms-full.txt / redirects.json), deploy.
|
|
151
159
|
|
|
152
160
|
`playground/` is a small consuming example. `templates/starter/` is what the scaffolder writes out;
|
|
153
161
|
`scripts/kestrel.mjs` is the engine's CLI and `packages/create-kestrel/` the standalone
|
|
@@ -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>
|