@xleddyl/nuxt-cms 0.2.3 → 0.4.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.
Files changed (39) hide show
  1. package/README.md +8 -1
  2. package/dist/module.d.mts +3 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +113 -3
  5. package/dist/runtime/app/components/cms/BrandMark.vue +16 -1
  6. package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +2 -0
  7. package/dist/runtime/app/components/cms/EntryForm.vue +34 -3
  8. package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +2 -0
  9. package/dist/runtime/app/components/cms/MediaDetail.vue +25 -13
  10. package/dist/runtime/app/components/cms/Search.d.vue.ts +7 -0
  11. package/dist/runtime/app/components/cms/Search.vue +347 -0
  12. package/dist/runtime/app/components/cms/Search.vue.d.ts +7 -0
  13. package/dist/runtime/app/components/cms/VisualEditor.d.vue.ts +2 -0
  14. package/dist/runtime/app/components/cms/VisualEditor.vue +21 -1
  15. package/dist/runtime/app/components/cms/VisualEditor.vue.d.ts +2 -0
  16. package/dist/runtime/app/components/cms/WelcomeModal.vue +7 -1
  17. package/dist/runtime/app/composables/cms-field-focus.d.ts +4 -0
  18. package/dist/runtime/app/composables/cms-field-focus.js +20 -0
  19. package/dist/runtime/app/layouts/cms-admin.d.vue.ts +2 -2
  20. package/dist/runtime/app/layouts/cms-admin.vue +8 -3
  21. package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
  22. package/dist/runtime/app/pages/admin-collection.vue +3 -0
  23. package/dist/runtime/app/pages/admin-content.vue +3 -0
  24. package/dist/runtime/app/pages/admin-entry.vue +3 -0
  25. package/dist/runtime/assets/main.css +1 -1
  26. package/dist/runtime/server/api/collection.get.js +2 -4
  27. package/dist/runtime/server/api/media-download.get.d.ts +2 -0
  28. package/dist/runtime/server/api/media-download.get.js +90 -0
  29. package/dist/runtime/server/api/search.get.d.ts +3 -0
  30. package/dist/runtime/server/api/search.get.js +24 -0
  31. package/dist/runtime/server/utils/media-download.d.ts +1 -0
  32. package/dist/runtime/server/utils/media-download.js +8 -0
  33. package/dist/runtime/server/utils/media.d.ts +1 -0
  34. package/dist/runtime/server/utils/media.js +18 -3
  35. package/dist/runtime/server/utils/search.d.ts +20 -0
  36. package/dist/runtime/server/utils/search.js +233 -0
  37. package/dist/runtime/shared/search.d.ts +52 -0
  38. package/dist/runtime/shared/search.js +207 -0
  39. package/package.json +2 -2
@@ -0,0 +1,347 @@
1
+ <template>
2
+ <button
3
+ ref="trigger"
4
+ type="button"
5
+ class="cms-search-trigger"
6
+ aria-haspopup="dialog"
7
+ :aria-expanded="open"
8
+ aria-keyshortcuts="Meta+K Control+K"
9
+ @click="show"
10
+ >
11
+ <CmsIcon name="magnifying-glass" class="cms-search-trigger-icon size-4 shrink-0" />
12
+ <span class="cms-search-trigger-text">Search</span>
13
+ <kbd class="cms-kbd">{{ shortcut }}</kbd>
14
+ </button>
15
+
16
+ <Teleport to="body">
17
+ <Transition name="cms-modal">
18
+ <div v-if="open" class="cms-scope cms-overlay cms-search-overlay" @click.self="close">
19
+ <div
20
+ class="cms-modal cms-search"
21
+ role="dialog"
22
+ aria-modal="true"
23
+ aria-labelledby="cms-search-label"
24
+ >
25
+ <div class="cms-search-bar">
26
+ <label id="cms-search-label" for="cms-search-input" class="sr-only">
27
+ Search the CMS
28
+ </label>
29
+ <CmsIcon name="magnifying-glass" class="cms-search-bar-icon size-5 shrink-0" />
30
+ <input
31
+ id="cms-search-input"
32
+ ref="input"
33
+ v-model="query"
34
+ type="text"
35
+ role="combobox"
36
+ class="cms-search-input"
37
+ placeholder="Search content, fields and pages"
38
+ autocomplete="off"
39
+ spellcheck="false"
40
+ aria-autocomplete="list"
41
+ aria-controls="cms-search-results"
42
+ :aria-expanded="hits.length > 0"
43
+ :aria-activedescendant="activeId"
44
+ @keydown="onKeydown"
45
+ />
46
+ <CmsSpinner v-if="loading" />
47
+ <kbd class="cms-kbd">Esc</kbd>
48
+ </div>
49
+
50
+ <div class="cms-search-body">
51
+ <p v-if="hint" class="cms-search-hint">{{ hint }}</p>
52
+ <div
53
+ id="cms-search-results"
54
+ role="listbox"
55
+ aria-labelledby="cms-search-label"
56
+ class="cms-search-results"
57
+ >
58
+ <div
59
+ v-for="(group, groupIndex) in groups"
60
+ :key="group.key"
61
+ role="group"
62
+ :aria-labelledby="`cms-search-group-${groupIndex}`"
63
+ class="cms-search-group"
64
+ >
65
+ <div :id="`cms-search-group-${groupIndex}`" class="cms-search-group-title">
66
+ <span>{{ group.label }}</span>
67
+ <span class="cms-search-group-kind">{{ KIND_LABELS[group.kind] }}</span>
68
+ </div>
69
+ <div
70
+ v-for="item in group.items"
71
+ :id="optionId(item.index)"
72
+ :key="item.index"
73
+ role="option"
74
+ class="cms-search-option"
75
+ :class="{ 'is-active': item.index === active }"
76
+ :aria-selected="item.index === active"
77
+ @mousemove="active = item.index"
78
+ @click="go(item.hit)"
79
+ >
80
+ <CmsIcon
81
+ :name="iconOf(item.hit)"
82
+ class="cms-search-option-icon size-4 shrink-0"
83
+ />
84
+ <span class="cms-search-option-text">
85
+ <template v-if="isValueHit(item.hit)">
86
+ <span class="cms-search-option-head">
87
+ <span class="cms-search-option-title">
88
+ {{ item.hit.title ?? item.hit.entryLabel }}
89
+ </span>
90
+ <span v-if="item.hit.fieldLabel" class="cms-search-option-meta">
91
+ {{ item.hit.fieldLabel }}
92
+ </span>
93
+ <span v-if="item.hit.locale" class="cms-badge is-muted">
94
+ {{ item.hit.locale }}
95
+ </span>
96
+ </span>
97
+ <span class="cms-search-snippet">
98
+ {{ item.hit.snippet.before
99
+ }}<mark class="cms-search-mark">{{
100
+ item.hit.snippet.match
101
+ }}</mark
102
+ >{{ item.hit.snippet.after }}
103
+ </span>
104
+ </template>
105
+ <template v-else>
106
+ <span class="cms-search-option-title">
107
+ {{ item.hit.snippet.before
108
+ }}<mark class="cms-search-mark">{{
109
+ item.hit.snippet.match
110
+ }}</mark
111
+ >{{ item.hit.snippet.after }}
112
+ </span>
113
+ <span class="cms-search-option-meta">{{
114
+ labelMeta(item.hit)
115
+ }}</span>
116
+ </template>
117
+ </span>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ <p v-if="truncated" class="cms-search-hint">
122
+ Showing the first results. Type more to narrow them down.
123
+ </p>
124
+ </div>
125
+
126
+ <div class="cms-search-footer" aria-hidden="true">
127
+ <span><kbd class="cms-kbd">↑</kbd><kbd class="cms-kbd">↓</kbd> to move</span>
128
+ <span><kbd class="cms-kbd">↵</kbd> to open</span>
129
+ <span><kbd class="cms-kbd">Esc</kbd> to close</span>
130
+ </div>
131
+ <p class="sr-only" aria-live="polite">{{ status }}</p>
132
+ </div>
133
+ </div>
134
+ </Transition>
135
+ </Teleport>
136
+ </template>
137
+
138
+ <script setup>
139
+ import { computed, navigateTo, nextTick, onBeforeUnmount, onMounted, ref, watch } from "#imports";
140
+ import { isSearchableQuery, normalizeSearchQuery } from "../../../shared/search";
141
+ import { useCmsOverlay } from "../../composables/cms-overlay";
142
+ import { cmsApi } from "../../utils/api";
143
+ const DEBOUNCE_MS = 180;
144
+ const KIND_LABELS = {
145
+ collection: "Collection",
146
+ single: "Single",
147
+ content: "Content",
148
+ page: "Pages",
149
+ media: "Library"
150
+ };
151
+ const KIND_ICONS = {
152
+ collection: "square-3-stack-3d",
153
+ single: "document-text",
154
+ content: "newspaper",
155
+ page: "window",
156
+ media: "photo"
157
+ };
158
+ const TYPE_LABELS = {
159
+ entry: "Section",
160
+ page: "Page",
161
+ tab: "Tab",
162
+ field: "Field",
163
+ block: "Block",
164
+ value: "Value",
165
+ media: "Media"
166
+ };
167
+ const TYPE_ICONS = {
168
+ page: "window",
169
+ tab: "rectangle-group",
170
+ field: "tag",
171
+ block: "squares-2x2",
172
+ value: "bars-3-bottom-left",
173
+ media: "photo"
174
+ };
175
+ const emit = defineEmits(["open"]);
176
+ const open = ref(false);
177
+ const query = ref("");
178
+ const hits = ref([]);
179
+ const truncated = ref(false);
180
+ const loading = ref(false);
181
+ const failed = ref(false);
182
+ const searched = ref("");
183
+ const active = ref(0);
184
+ const shortcut = ref("Ctrl K");
185
+ const trigger = ref(null);
186
+ const input = ref(null);
187
+ let timer;
188
+ let request = 0;
189
+ let returnFocus = null;
190
+ const groups = computed(() => {
191
+ const map = /* @__PURE__ */ new Map();
192
+ hits.value.forEach((hit, index2) => {
193
+ const key = hit.collection ?? hit.kind;
194
+ let group = map.get(key);
195
+ if (!group) {
196
+ group = { key, label: hit.entryLabel, kind: hit.kind, items: [] };
197
+ map.set(key, group);
198
+ }
199
+ group.items.push({ hit, index: index2 });
200
+ });
201
+ const ordered = [...map.values()];
202
+ let index = 0;
203
+ for (const group of ordered) {
204
+ for (const item of group.items) item.index = index++;
205
+ }
206
+ return ordered;
207
+ });
208
+ const flat = computed(() => groups.value.flatMap((group) => group.items.map((item) => item.hit)));
209
+ const activeId = computed(() => flat.value.length ? optionId(active.value) : void 0);
210
+ const hint = computed(() => {
211
+ if (!isSearchableQuery(query.value)) return "Type at least 2 characters to search.";
212
+ if (failed.value) return "Search failed. Try again.";
213
+ if (!loading.value && searched.value && !flat.value.length) {
214
+ return `No results for "${searched.value}".`;
215
+ }
216
+ return "";
217
+ });
218
+ const status = computed(() => {
219
+ if (hint.value) return hint.value;
220
+ if (loading.value) return "Searching";
221
+ return `${flat.value.length} results`;
222
+ });
223
+ function optionId(index) {
224
+ return `cms-search-option-${index}`;
225
+ }
226
+ function isValueHit(hit) {
227
+ return hit.type === "value" || hit.type === "media";
228
+ }
229
+ function iconOf(hit) {
230
+ return hit.type === "entry" ? KIND_ICONS[hit.kind] : TYPE_ICONS[hit.type] ?? "tag";
231
+ }
232
+ function labelMeta(hit) {
233
+ const parts = [TYPE_LABELS[hit.type]];
234
+ if (hit.title) parts.push(hit.title);
235
+ if (hit.fieldLabel && hit.type !== "tab") parts.push(hit.fieldLabel);
236
+ if (hit.type === "page" && hit.id) parts.push(hit.to);
237
+ return parts.join(" \xB7 ");
238
+ }
239
+ async function run(term) {
240
+ const current = ++request;
241
+ loading.value = true;
242
+ failed.value = false;
243
+ try {
244
+ const response = await cmsApi("/api/cms/search", { query: { q: term } });
245
+ if (current !== request) return;
246
+ hits.value = response.hits;
247
+ truncated.value = response.truncated;
248
+ searched.value = response.query;
249
+ active.value = 0;
250
+ } catch {
251
+ if (current !== request) return;
252
+ hits.value = [];
253
+ truncated.value = false;
254
+ failed.value = true;
255
+ } finally {
256
+ if (current === request) loading.value = false;
257
+ }
258
+ }
259
+ watch(query, (value) => {
260
+ clearTimeout(timer);
261
+ const term = normalizeSearchQuery(value);
262
+ if (!isSearchableQuery(term)) {
263
+ request++;
264
+ hits.value = [];
265
+ truncated.value = false;
266
+ loading.value = false;
267
+ failed.value = false;
268
+ searched.value = "";
269
+ return;
270
+ }
271
+ timer = setTimeout(() => run(term), DEBOUNCE_MS);
272
+ });
273
+ watch(active, (index) => {
274
+ void nextTick(
275
+ () => document.getElementById(optionId(index))?.scrollIntoView({ block: "nearest" })
276
+ );
277
+ });
278
+ function focusInput() {
279
+ void nextTick(() => {
280
+ input.value?.focus();
281
+ input.value?.select();
282
+ });
283
+ }
284
+ function show() {
285
+ if (open.value) {
286
+ focusInput();
287
+ return;
288
+ }
289
+ returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
290
+ emit("open");
291
+ open.value = true;
292
+ }
293
+ function close() {
294
+ open.value = false;
295
+ }
296
+ const overlay = useCmsOverlay(close);
297
+ watch(open, (value) => {
298
+ if (value) {
299
+ overlay.activate();
300
+ focusInput();
301
+ } else {
302
+ overlay.deactivate();
303
+ const target = returnFocus ?? trigger.value;
304
+ returnFocus = null;
305
+ void nextTick(() => target?.focus());
306
+ }
307
+ });
308
+ function go(hit) {
309
+ if (!hit) return;
310
+ returnFocus = null;
311
+ close();
312
+ void navigateTo(hit.to);
313
+ }
314
+ function onKeydown(event) {
315
+ const count = flat.value.length;
316
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
317
+ event.preventDefault();
318
+ if (!count) return;
319
+ const step = event.key === "ArrowDown" ? 1 : -1;
320
+ active.value = (active.value + step + count) % count;
321
+ } else if (event.key === "Home" && count && event.ctrlKey) {
322
+ event.preventDefault();
323
+ active.value = 0;
324
+ } else if (event.key === "End" && count && event.ctrlKey) {
325
+ event.preventDefault();
326
+ active.value = count - 1;
327
+ } else if (event.key === "Enter") {
328
+ event.preventDefault();
329
+ go(flat.value[active.value]);
330
+ }
331
+ }
332
+ function onGlobalKeydown(event) {
333
+ if (event.key.toLowerCase() !== "k" || event.altKey || event.shiftKey) return;
334
+ if (!event.metaKey && !event.ctrlKey) return;
335
+ event.preventDefault();
336
+ show();
337
+ }
338
+ onMounted(() => {
339
+ const platform = navigator.userAgentData?.platform ?? navigator.platform ?? navigator.userAgent;
340
+ if (/mac|iphone|ipad|ipod/i.test(platform)) shortcut.value = "\u2318K";
341
+ document.addEventListener("keydown", onGlobalKeydown);
342
+ });
343
+ onBeforeUnmount(() => {
344
+ clearTimeout(timer);
345
+ document.removeEventListener("keydown", onGlobalKeydown);
346
+ });
347
+ </script>
@@ -0,0 +1,7 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
2
+ open: () => any;
3
+ }, string, import("vue").PublicProps, Readonly<{}> & Readonly<{
4
+ onOpen?: (() => any) | undefined;
5
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -1,10 +1,12 @@
1
1
  import type { FieldConfig } from '#nuxt-cms';
2
+ import type { CmsSearchFocus } from '../../../shared/search.js';
2
3
  type __VLS_Props = {
3
4
  entryName: string;
4
5
  fieldKey: string;
5
6
  fields: Record<string, FieldConfig>;
6
7
  title: string;
7
8
  content?: boolean;
9
+ focus?: CmsSearchFocus | null;
8
10
  };
9
11
  declare function validate(): boolean;
10
12
  type __VLS_ModelProps = {
@@ -75,6 +75,7 @@
75
75
 
76
76
  <div
77
77
  :id="`cms-editor-tabpanel-${activeTab}`"
78
+ ref="panel"
78
79
  class="cms-editor-panel-body"
79
80
  role="tabpanel"
80
81
  :aria-labelledby="tabList.length > 1 ? `cms-editor-tab-${activeTab}` : void 0"
@@ -213,6 +214,8 @@
213
214
  :icon="fieldIcon(fields[key])"
214
215
  :name="key"
215
216
  :required="fields[key].required"
217
+ :data-cms-field="key"
218
+ :class="{ 'is-search-focus': highlighted === key }"
216
219
  >
217
220
  <CmsFieldInput
218
221
  :model-value="state[key]"
@@ -267,6 +270,7 @@ import {
267
270
  toggleBlockHidden
268
271
  } from "../../../shared/preview";
269
272
  import { useCmsConfirm } from "../../composables/cms-confirm";
273
+ import { useCmsFieldFocus } from "../../composables/cms-field-focus";
270
274
  import { useCmsRuntime } from "../../composables/cms-runtime";
271
275
  import { entryFormLayout, useCmsSettingsState } from "../../composables/cms-settings";
272
276
  import { CMS_FORM_ERRORS, fieldIcon } from "../../utils/ui";
@@ -280,7 +284,8 @@ const props = defineProps({
280
284
  fieldKey: { type: String, required: true },
281
285
  fields: { type: Object, required: true },
282
286
  title: { type: String, required: true },
283
- content: { type: Boolean, required: false }
287
+ content: { type: Boolean, required: false },
288
+ focus: { type: [Object, null], required: false }
284
289
  });
285
290
  const state = defineModel("state", { type: Object, ...{ required: true } });
286
291
  const runtime = useCmsRuntime();
@@ -465,9 +470,24 @@ async function loadLibrary() {
465
470
  library.value = /* @__PURE__ */ new Map();
466
471
  }
467
472
  }
473
+ const panel = ref(null);
474
+ const { highlighted, reveal } = useCmsFieldFocus();
475
+ function applyFocus(focus) {
476
+ if (focus.locale && i18n.locales.includes(focus.locale)) locale.value = focus.locale;
477
+ const key = focus.field;
478
+ if (!key || !Object.hasOwn(props.fields, key)) return;
479
+ if (key === props.fieldKey) {
480
+ activeTab.value = "block";
481
+ if (focus.block != null && focus.block < blocks.value.length) select(focus.block);
482
+ return;
483
+ }
484
+ activeTab.value = tabOfKey(key);
485
+ reveal(() => panel.value, key);
486
+ }
468
487
  onMounted(() => {
469
488
  window.addEventListener("message", onMessage);
470
489
  void loadLibrary();
490
+ if (props.focus) applyFocus(props.focus);
471
491
  });
472
492
  onBeforeUnmount(() => {
473
493
  window.removeEventListener("message", onMessage);
@@ -1,10 +1,12 @@
1
1
  import type { FieldConfig } from '#nuxt-cms';
2
+ import type { CmsSearchFocus } from '../../../shared/search.js';
2
3
  type __VLS_Props = {
3
4
  entryName: string;
4
5
  fieldKey: string;
5
6
  fields: Record<string, FieldConfig>;
6
7
  title: string;
7
8
  content?: boolean;
9
+ focus?: CmsSearchFocus | null;
8
10
  };
9
11
  declare function validate(): boolean;
10
12
  type __VLS_ModelProps = {
@@ -1,5 +1,10 @@
1
1
  <template>
2
- <CmsModal :open="open" size="sm" title="Welcome to nuxt-cms" @update:open="(v) => !v && keep()">
2
+ <CmsModal
3
+ :open="open"
4
+ size="sm"
5
+ :title="`Welcome to ${cmsBrand.title}`"
6
+ @update:open="(v) => !v && keep()"
7
+ >
3
8
  <template #body>
4
9
  <CmsForm :state="state" @submit="save">
5
10
  <p class="cms-settings-hint">
@@ -32,6 +37,7 @@
32
37
 
33
38
  <script setup>
34
39
  import { MIN_PASSWORD_LENGTH } from "#nuxt-cms";
40
+ import { cmsBrand } from "#cms-brand";
35
41
  import { computed, ref } from "#imports";
36
42
  import { useCmsAccount } from "../../composables/cms-account";
37
43
  import { useCmsToast } from "../../composables/cms-toast";
@@ -0,0 +1,4 @@
1
+ export declare function useCmsFieldFocus(): {
2
+ highlighted: import("vue").Ref<string | null, string | null>;
3
+ reveal: (root: () => HTMLElement | null | undefined, key: string) => void;
4
+ };
@@ -0,0 +1,20 @@
1
+ import { nextTick, onBeforeUnmount, ref } from "#imports";
2
+ const HIGHLIGHT_MS = 2400;
3
+ export function useCmsFieldFocus() {
4
+ const highlighted = ref(null);
5
+ let timer;
6
+ function reveal(root, key) {
7
+ void nextTick(() => {
8
+ const element = root()?.querySelector(`[data-cms-field="${CSS.escape(key)}"]`);
9
+ if (!element) return;
10
+ element.scrollIntoView({ block: "center", behavior: "smooth" });
11
+ highlighted.value = key;
12
+ clearTimeout(timer);
13
+ timer = setTimeout(() => {
14
+ highlighted.value = null;
15
+ }, HIGHLIGHT_MS);
16
+ });
17
+ }
18
+ onBeforeUnmount(() => clearTimeout(timer));
19
+ return { highlighted, reveal };
20
+ }
@@ -1,6 +1,6 @@
1
- declare var __VLS_34: {};
1
+ declare var __VLS_41: {};
2
2
  type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_34) => any;
3
+ default?: (props: typeof __VLS_41) => any;
4
4
  };
5
5
  declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
6
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -5,11 +5,15 @@
5
5
  <div class="cms-sidebar-brand">
6
6
  <CmsBrandMark />
7
7
  <div class="cms-brand-text">
8
- <span class="cms-brand-name">nuxt-cms</span>
9
- <span class="cms-brand-meta">Content studio</span>
8
+ <span class="cms-brand-name">{{ cmsBrand.title }}</span>
9
+ <span v-if="cmsBrand.subtitle" class="cms-brand-meta">{{
10
+ cmsBrand.subtitle
11
+ }}</span>
10
12
  </div>
11
13
  </div>
12
14
 
15
+ <CmsSearch @open="navOpen = false" />
16
+
13
17
  <nav class="cms-sidebar-nav">
14
18
  <div v-for="group in groups" :key="group.title" class="cms-sidebar-group">
15
19
  <div class="cms-label">
@@ -62,7 +66,7 @@
62
66
  @click="navOpen = true"
63
67
  />
64
68
  <CmsBrandMark />
65
- <span class="cms-brand-name">nuxt-cms</span>
69
+ <span class="cms-brand-name">{{ cmsBrand.title }}</span>
66
70
  </header>
67
71
  <div class="cms-main-inner">
68
72
  <slot />
@@ -80,6 +84,7 @@
80
84
  <script setup>
81
85
  import { computed, navigateTo, ref, useRoute, useUserSession, watch } from "#imports";
82
86
  import cmsConfig from "#cms-config";
87
+ import { cmsBrand } from "#cms-brand";
83
88
  import { useCmsTheme } from "../composables/cms-theme";
84
89
  import { useCmsAccount } from "../composables/cms-account";
85
90
  import { loadCmsSettings } from "../composables/cms-settings";
@@ -1,6 +1,6 @@
1
- declare var __VLS_34: {};
1
+ declare var __VLS_41: {};
2
2
  type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_34) => any;
3
+ default?: (props: typeof __VLS_41) => any;
4
4
  };
5
5
  declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
6
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -35,6 +35,7 @@
35
35
  :entry-name="name"
36
36
  :form-id="FORM_ID"
37
37
  :loading="saving"
38
+ :focus="focus"
38
39
  @submit="saveSingle"
39
40
  />
40
41
  </div>
@@ -219,6 +220,7 @@ import {
219
220
  watch
220
221
  } from "#imports";
221
222
  import cmsConfig from "#cms-config";
223
+ import { readSearchFocus } from "../../shared/search";
222
224
  import { useCmsConfirm } from "../composables/cms-confirm";
223
225
  import { useCmsRuntime } from "../composables/cms-runtime";
224
226
  import { entryListColumns, useCmsSettingsState } from "../composables/cms-settings";
@@ -234,6 +236,7 @@ definePageMeta({
234
236
  const FORM_ID = "cms-single-form";
235
237
  const route = useRoute();
236
238
  const toast = useCmsToast();
239
+ const focus = readSearchFocus(route.query);
237
240
  const name = route.params.collection;
238
241
  const config = cmsConfig[name];
239
242
  if (!config) {
@@ -6,6 +6,7 @@
6
6
  :field-key="CONTENT_BODY_FIELD"
7
7
  :fields="config.fields"
8
8
  :title="headerTitle"
9
+ :focus="focus"
9
10
  content
10
11
  >
11
12
  <template #start>
@@ -120,6 +121,7 @@ import {
120
121
  watch
121
122
  } from "#imports";
122
123
  import cmsConfig from "#cms-config";
124
+ import { readSearchFocus } from "../../shared/search";
123
125
  import { useCmsConfirm } from "../composables/cms-confirm";
124
126
  import { useCmsRuntime } from "../composables/cms-runtime";
125
127
  import { useCmsToast } from "../composables/cms-toast";
@@ -133,6 +135,7 @@ definePageMeta({
133
135
  const route = useRoute();
134
136
  const toast = useCmsToast();
135
137
  const confirmAction = useCmsConfirm();
138
+ const focus = readSearchFocus(route.query);
136
139
  const { i18n } = useCmsRuntime();
137
140
  const name = String(route.meta.cmsEntry ?? route.path.split("/")[2] ?? "");
138
141
  const config = cmsConfig[name];
@@ -27,6 +27,7 @@
27
27
  :drafts="drafts"
28
28
  :form-id="FORM_ID"
29
29
  :loading="saving"
30
+ :focus="focus"
30
31
  @submit="save"
31
32
  @error="revertStatus"
32
33
  />
@@ -49,6 +50,7 @@ import {
49
50
  watch
50
51
  } from "#imports";
51
52
  import cmsConfig from "#cms-config";
53
+ import { readSearchFocus } from "../../shared/search";
52
54
  import { useCmsConfirm } from "../composables/cms-confirm";
53
55
  import { useCmsToast } from "../composables/cms-toast";
54
56
  import { cmsApi } from "../utils/api";
@@ -62,6 +64,7 @@ definePageMeta({
62
64
  });
63
65
  const route = useRoute();
64
66
  const toast = useCmsToast();
67
+ const focus = readSearchFocus(route.query);
65
68
  const name = route.params.collection;
66
69
  const config = cmsConfig[name];
67
70
  if (!config || config.kind !== "collection" && config.kind !== "page") {