rimelight-components 2.1.11 → 2.1.13

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 (27) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +1 -1
  3. package/dist/runtime/components/page/PageEditor.d.vue.ts +8 -1
  4. package/dist/runtime/components/page/PageEditor.vue +236 -72
  5. package/dist/runtime/components/page/PageEditor.vue.d.ts +8 -1
  6. package/dist/runtime/components/page/PagePropertiesEditor.vue +172 -81
  7. package/dist/runtime/components/page/PagePropertiesRenderer.vue +6 -23
  8. package/dist/runtime/components/page/PageRenderer.d.vue.ts +1 -1
  9. package/dist/runtime/components/page/PageRenderer.vue +2 -2
  10. package/dist/runtime/components/page/PageRenderer.vue.d.ts +1 -1
  11. package/dist/runtime/components/page/PageTOC.vue +1 -1
  12. package/dist/runtime/components/page/modals/CreatePageModal.d.vue.ts +35 -0
  13. package/dist/runtime/components/page/modals/CreatePageModal.vue +108 -0
  14. package/dist/runtime/components/page/modals/CreatePageModal.vue.d.ts +35 -0
  15. package/dist/runtime/components/page/modals/DeletePageModal.d.vue.ts +28 -0
  16. package/dist/runtime/components/page/modals/DeletePageModal.vue +84 -0
  17. package/dist/runtime/components/page/modals/DeletePageModal.vue.d.ts +28 -0
  18. package/dist/runtime/composables/index.d.ts +1 -0
  19. package/dist/runtime/composables/index.js +1 -0
  20. package/dist/runtime/composables/index.mjs +1 -0
  21. package/dist/runtime/composables/useInfobox.d.ts +8 -0
  22. package/dist/runtime/composables/useInfobox.js +31 -0
  23. package/dist/runtime/composables/useInfobox.mjs +31 -0
  24. package/dist/runtime/composables/usePageEditor.js +2 -4
  25. package/dist/runtime/composables/usePageEditor.mjs +2 -4
  26. package/dist/runtime/types/pages.d.ts +1 -0
  27. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rimelight-components",
3
- "version": "2.1.11",
3
+ "version": "2.1.13",
4
4
  "docs": "https://rimelight.com/tools/rimelight-components",
5
5
  "configKey": "rimelightComponents",
6
6
  "compatibility": {
package/dist/module.mjs CHANGED
@@ -4,7 +4,7 @@ import { readdirSync } from 'node:fs';
4
4
  import { basename } from 'node:path';
5
5
 
6
6
  const name = "rimelight-components";
7
- const version = "2.1.11";
7
+ const version = "2.1.13";
8
8
  const homepage = "https://rimelight.com/tools/rimelight-components";
9
9
 
10
10
  const defaultOptions = {
@@ -1,6 +1,13 @@
1
- import type { Page } from "../../types/index.js";
1
+ import { type Page, type PageSurround, type PageDefinition } from "../../types/index.js";
2
2
  interface PageEditorProps {
3
3
  isSaving: boolean;
4
+ useSurround?: boolean;
5
+ surround?: PageSurround | null;
6
+ surroundStatus?: 'idle' | 'pending' | 'success' | 'error';
7
+ resolvePage: (id: string) => Promise<Pick<Page, 'title' | 'icon' | 'slug'>>;
8
+ pageDefinitions: Record<string, PageDefinition>;
9
+ onCreatePage?: (page: Partial<Page>) => Promise<void>;
10
+ onDeletePage?: (id: string) => Promise<void>;
4
11
  }
5
12
  type __VLS_Props = PageEditorProps;
6
13
  type __VLS_ModelProps = {
@@ -1,11 +1,22 @@
1
1
  <script setup>
2
- import { ref, computed, useTemplateRef } from "vue";
3
- import { usePageEditor } from "../../composables";
2
+ import { ref, computed, useTemplateRef, provide } from "vue";
3
+ import {} from "../../types";
4
+ import { usePageEditor, usePageRegistry } from "../../composables";
4
5
  import { getLocalizedContent } from "../../utils";
6
+ import { useI18n } from "vue-i18n";
7
+ const { getTypeLabelKey } = usePageRegistry();
8
+ const { t, locale } = useI18n();
5
9
  const page = defineModel({ type: null, ...{ required: true } });
6
10
  const { undo, redo, canUndo, canRedo, captureSnapshot } = usePageEditor(page);
7
- const { isSaving } = defineProps({
8
- isSaving: { type: Boolean, required: true }
11
+ const { isSaving, useSurround = false, surroundStatus = "idle", surround = null, resolvePage, onCreatePage, onDeletePage } = defineProps({
12
+ isSaving: { type: Boolean, required: true },
13
+ useSurround: { type: Boolean, required: false },
14
+ surround: { type: [Object, null], required: false },
15
+ surroundStatus: { type: String, required: false },
16
+ resolvePage: { type: Function, required: true },
17
+ pageDefinitions: { type: Object, required: true },
18
+ onCreatePage: { type: Function, required: false },
19
+ onDeletePage: { type: Function, required: false }
9
20
  });
10
21
  const emit = defineEmits(["save"]);
11
22
  const handleSave = () => {
@@ -20,12 +31,69 @@ defineExpose({
20
31
  });
21
32
  const editorRef = useTemplateRef("editor");
22
33
  const showPreview = ref(false);
23
- const editorPanelClass = computed(() => ({
24
- // When the preview is visible, both editor/preview share 1/2 span.
25
- "col-span-1 w-full": showPreview.value,
26
- // When the preview is hidden, the editor uses the full grid space.
27
- "col-span-2 w-full": !showPreview.value
28
- }));
34
+ provide("page-resolver", resolvePage);
35
+ const previousPage = computed(() => surround?.previous);
36
+ const nextPage = computed(() => surround?.next);
37
+ const hasSurround = computed(() => !!(surround?.previous || surround?.next));
38
+ const containerRef = useTemplateRef("split-container");
39
+ const editorWidth = ref(50);
40
+ const isResizing = ref(false);
41
+ const SNAP_THRESHOLD = 1.5;
42
+ const handleMouseMove = (e) => {
43
+ if (!isResizing.value || !containerRef.value) return;
44
+ const containerRect = containerRef.value.getBoundingClientRect();
45
+ let newWidth = (e.clientX - containerRect.left) / containerRect.width * 100;
46
+ newWidth = Math.min(Math.max(newWidth, 20), 80);
47
+ if (Math.abs(newWidth - 50) < SNAP_THRESHOLD) {
48
+ editorWidth.value = 50;
49
+ } else {
50
+ editorWidth.value = newWidth;
51
+ }
52
+ };
53
+ const cursorClass = computed(() => {
54
+ if (isResizing.value) return "cursor-grabbing";
55
+ return "cursor-grab";
56
+ });
57
+ const startResizing = (e) => {
58
+ isResizing.value = true;
59
+ window.addEventListener("mousemove", handleMouseMove);
60
+ window.addEventListener("mouseup", stopResizing);
61
+ document.body.style.cursor = "col-resize";
62
+ document.body.style.userSelect = "none";
63
+ };
64
+ const stopResizing = () => {
65
+ isResizing.value = false;
66
+ window.removeEventListener("mousemove", handleMouseMove);
67
+ window.removeEventListener("mouseup", stopResizing);
68
+ document.body.style.cursor = "";
69
+ document.body.style.userSelect = "";
70
+ };
71
+ const isCreateModalOpen = ref(false);
72
+ const isCreating = ref(false);
73
+ const handleCreateConfirm = async (newPageData) => {
74
+ if (!onCreatePage) return;
75
+ try {
76
+ isCreating.value = true;
77
+ await onCreatePage(newPageData);
78
+ isCreateModalOpen.value = false;
79
+ } finally {
80
+ isCreating.value = false;
81
+ }
82
+ };
83
+ const isDeleteModalOpen = ref(false);
84
+ const isDeleting = ref(false);
85
+ const handleDeleteConfirm = async () => {
86
+ if (!onDeletePage || !page.value.id) return;
87
+ try {
88
+ isDeleting.value = true;
89
+ await onDeletePage(page.value.id);
90
+ isDeleteModalOpen.value = false;
91
+ } catch (err) {
92
+ console.error("Failed to delete page:", err);
93
+ } finally {
94
+ isDeleting.value = false;
95
+ }
96
+ };
29
97
  </script>
30
98
 
31
99
  <template>
@@ -69,73 +137,169 @@ const editorPanelClass = computed(() => ({
69
137
  :loading="isSaving"
70
138
  @click="handleSave"
71
139
  />
140
+ <RCCreatePageModal
141
+ :is-open="isCreateModalOpen"
142
+ :definitions="pageDefinitions"
143
+ :loading="isCreating"
144
+ @close="isCreateModalOpen = false"
145
+ @confirm="handleCreateConfirm"
146
+ >
147
+ <UButton
148
+ icon="lucide:file-plus"
149
+ label="Create Page"
150
+ color="primary"
151
+ size="xs"
152
+ />
153
+ </RCCreatePageModal>
154
+ <RCDeletePageModal
155
+ :is-open="isDeleteModalOpen"
156
+ :loading="isDeleting"
157
+ :page-title="getLocalizedContent(page.title, locale)"
158
+ @close="isDeleteModalOpen = false"
159
+ @confirm="handleDeleteConfirm"
160
+ >
161
+ <UButton
162
+ icon="lucide:file-plus"
163
+ label="Delete Page"
164
+ color="error"
165
+ size="xs"
166
+ />
167
+ </RCDeletePageModal>
72
168
  </div>
73
169
  </template>
74
170
  </UHeader>
75
- <UContainer
76
- class="mt-24 grid gap-xl"
77
- :class="showPreview ? 'grid-cols-2 max-w-full' : 'grid-cols-1'"
171
+ <main
172
+ ref="split-container"
173
+ class="flex w-full overflow-hidden"
78
174
  >
79
- <div :class="editorPanelClass" class="grid grid-cols-1 lg:grid-cols-4 gap-8">
80
- <UPageAside class="order-1 lg:order-2 lg:col-span-1">
81
- <RCPagePropertiesEditor v-model="page" />
82
- </UPageAside>
83
- <div class="order-2 lg:order-1 lg:col-span-3">
84
- <UPageHeader
85
- :title="getLocalizedContent(page.title, 'en')"
86
- :description="getLocalizedContent(page.description, 'en') ?? ''"
87
- />
88
- <RCBlockEditor
89
- ref="editor"
90
- v-model="page.blocks"
91
- :class="editorPanelClass"
92
- @mutation="captureSnapshot"
93
- />
94
- <div class="flex flex-col gap-xs text-xs">
95
- <h6>Metadata</h6>
96
- <span>Page ID: {{ page.id }}</span>
97
- <span
98
- >Created At:
99
- <NuxtTime
100
- :datetime="page.created_at ?? ''"
101
- year="numeric"
102
- month="numeric"
103
- day="numeric"
104
- hour="numeric"
105
- minute="numeric"
106
- second="numeric"
107
- time-zone-name="short"
108
- /></span>
109
- <span
110
- >Posted At:
111
- <NuxtTime
112
- :datetime="page.created_at ?? ''"
113
- year="numeric"
114
- month="numeric"
115
- day="numeric"
116
- hour="numeric"
117
- minute="numeric"
118
- second="numeric"
119
- time-zone-name="short"
120
- /></span>
121
- <span
122
- >Updated At:
123
- <NuxtTime
124
- :datetime="page.created_at ?? ''"
125
- year="numeric"
126
- month="numeric"
127
- day="numeric"
128
- hour="numeric"
129
- minute="numeric"
130
- second="numeric"
131
- time-zone-name="short"
132
- /></span>
175
+ <div
176
+ class="h-full overflow-y-auto"
177
+ :style="{ width: showPreview ? `${editorWidth}%` : '100%' }"
178
+ >
179
+ <UContainer class="flex flex-col py-16">
180
+ <div class="grid grid-cols-1 lg:grid-cols-24 gap-xl items-start">
181
+ <RCPageTOC
182
+ :page-blocks="page.blocks"
183
+ :levels="[2, 3, 4]"
184
+ class="hidden lg:flex lg:col-span-4 sticky top-20 self-start"
185
+ >
186
+ <template #bottom> </template>
187
+ </RCPageTOC>
188
+ <RCPagePropertiesEditor v-model="page" class="order-1 lg:order-2 lg:col-span-6" />
189
+ <div class="order-2 lg:order-1 lg:col-span-14 flex flex-col gap-xl">
190
+ <NuxtImg
191
+ v-if="page.banner?.src"
192
+ :src="page.banner?.src"
193
+ :alt="page.banner?.alt"
194
+ class="rounded-xl w-full object-cover"
195
+ />
196
+ <UPageHeader
197
+ :headline="t(getTypeLabelKey(page.type))"
198
+ :description="getLocalizedContent(page.description, 'en') ?? ''"
199
+ :ui="{ root: 'pt-0' }"
200
+ >
201
+ <template #title>
202
+ <div class="flex flex-row gap-sm">
203
+ <NuxtImg
204
+ v-if="page.icon?.src"
205
+ :src="page.icon?.src"
206
+ :alt="page.icon?.alt"
207
+ class="rounded-full w-12 h-12 object-cover"
208
+ />
209
+ <h1>{{ getLocalizedContent(page.title, locale) }}</h1>
210
+ </div>
211
+ </template>
212
+ </UPageHeader>
213
+ <RCBlockEditor
214
+ ref="editor"
215
+ v-model="page.blocks"
216
+ @mutation="captureSnapshot"
217
+ />
218
+ <template v-if="useSurround">
219
+ <div v-if="surroundStatus === 'pending'" class="grid grid-cols-1 gap-md sm:grid-cols-2">
220
+ <USkeleton class="h-48 w-full rounded-xl" />
221
+ <USkeleton class="h-48 w-full rounded-xl" />
222
+ </div>
223
+
224
+ <LazyRCPageSurround
225
+ v-else-if="surroundStatus === 'success' && hasSurround"
226
+ hydrate-on-visible
227
+ :pageType="getTypeLabelKey(page.type)"
228
+ :previousTitle="getLocalizedContent(previousPage?.title, locale)"
229
+ :previousDescription="getLocalizedContent(previousPage?.description, locale)"
230
+ :previousTo="`/${previousPage?.slug}`"
231
+ :nextTitle="getLocalizedContent(nextPage?.title, locale)"
232
+ :nextDescription="getLocalizedContent(nextPage?.description, locale)"
233
+ :nextTo="`/${nextPage?.slug}`"
234
+ />
235
+
236
+ <USeparator />
237
+
238
+ <div class="flex flex-col gap-xs text-xs text-dimmed p-xl">
239
+ <h6>Metadata</h6>
240
+ <span>Page ID: {{ page.id }}</span>
241
+ <span
242
+ >Created At:
243
+ <NuxtTime
244
+ :datetime="page.created_at ?? ''"
245
+ year="numeric"
246
+ month="numeric"
247
+ day="numeric"
248
+ hour="numeric"
249
+ minute="numeric"
250
+ second="numeric"
251
+ time-zone-name="short"
252
+ /></span>
253
+ <span
254
+ >Posted At:
255
+ <NuxtTime
256
+ :datetime="page.created_at ?? ''"
257
+ year="numeric"
258
+ month="numeric"
259
+ day="numeric"
260
+ hour="numeric"
261
+ minute="numeric"
262
+ second="numeric"
263
+ time-zone-name="short"
264
+ /></span>
265
+ <span
266
+ >Updated At:
267
+ <NuxtTime
268
+ :datetime="page.created_at ?? ''"
269
+ year="numeric"
270
+ month="numeric"
271
+ day="numeric"
272
+ hour="numeric"
273
+ minute="numeric"
274
+ second="numeric"
275
+ time-zone-name="short"
276
+ /></span>
277
+ </div>
278
+ </template>
279
+ </div>
133
280
  </div>
134
- </div>
281
+ </UContainer>
135
282
  </div>
136
- <div class="flex flex-row gap-xl">
137
- <USeparator orientation="vertical" />
138
- <RCPageRenderer v-if="showPreview" v-model="page" />
283
+
284
+ <div
285
+ v-if="showPreview"
286
+ :class="cursorClass"
287
+ class="relative flex flex-col items-center justify-center w-6 cursor-col-resize group px-1 py-16"
288
+ @mousedown="startResizing"
289
+ @dblclick="editorWidth = 50"
290
+ >
291
+ <USeparator
292
+ orientation="vertical"
293
+ :ui="{}"
294
+ />
295
+ </div>
296
+
297
+ <div
298
+ v-if="showPreview"
299
+ class="h-full overflow-y-auto"
300
+ :style="{ width: `${100 - editorWidth}%` }"
301
+ >
302
+ <RCPageRenderer v-model="page" :resolve-page="resolvePage" />
139
303
  </div>
140
- </UContainer>
304
+ </main>
141
305
  </template>
@@ -1,6 +1,13 @@
1
- import type { Page } from "../../types/index.js";
1
+ import { type Page, type PageSurround, type PageDefinition } from "../../types/index.js";
2
2
  interface PageEditorProps {
3
3
  isSaving: boolean;
4
+ useSurround?: boolean;
5
+ surround?: PageSurround | null;
6
+ surroundStatus?: 'idle' | 'pending' | 'success' | 'error';
7
+ resolvePage: (id: string) => Promise<Pick<Page, 'title' | 'icon' | 'slug'>>;
8
+ pageDefinitions: Record<string, PageDefinition>;
9
+ onCreatePage?: (page: Partial<Page>) => Promise<void>;
10
+ onDeletePage?: (id: string) => Promise<void>;
4
11
  }
5
12
  type __VLS_Props = PageEditorProps;
6
13
  type __VLS_ModelProps = {
@@ -1,101 +1,192 @@
1
1
  <script setup>
2
+ import { computed } from "vue";
2
3
  import { useI18n } from "vue-i18n";
3
- import { usePageRegistry } from "../../composables";
4
+ import { usePageRegistry, useInfobox } from "../../composables";
4
5
  import { getLocalizedContent } from "../../utils";
6
+ import {} from "@nuxt/ui/components/Tabs.vue";
5
7
  import {} from "../../types";
6
- const { getTypeLabelKey } = usePageRegistry();
7
8
  const page = defineModel({ type: null, ...{ required: true } });
9
+ const { getTypeLabelKey } = usePageRegistry();
10
+ const { isFieldVisible, shouldRenderGroup, getSortedFields, getSortedGroups } = useInfobox(page.value.properties);
8
11
  const { locale, t } = useI18n();
9
- const isFieldVisible = (fieldSchema) => {
10
- if (!fieldSchema.visibleIf) return true;
11
- return fieldSchema.visibleIf(page.value.properties);
12
- };
13
- const getSortedFields = (fields) => {
14
- return Object.entries(fields).sort(([, a], [, b]) => (a.order ?? 0) - (b.order ?? 0));
12
+ const imageTabs = computed(() => {
13
+ if (!page.value.images?.length) return [];
14
+ return page.value.images.map((img, index) => {
15
+ const localizedName = getLocalizedContent(img.name, locale.value);
16
+ return {
17
+ label: localizedName || `${t("label_image")} ${index + 1}`,
18
+ value: `image-${index}`,
19
+ img
20
+ };
21
+ });
22
+ });
23
+ const updateTextArray = (schema, vals) => {
24
+ schema.value = vals.map((str) => ({
25
+ ...schema.value.find((i) => i.en === str),
26
+ // Preserve other locales if they exist
27
+ en: str
28
+ }));
15
29
  };
16
30
  </script>
17
31
 
18
32
  <template>
19
- <UCard
20
- variant="soft"
21
- :ui="{ root: 'divide-none', header: 'bg-elevated text-center', body: 'bg-muted' }"
22
- >
23
- <template #header>
24
- <h3>
25
- {{ getLocalizedContent(page.title, locale) }}
26
- </h3>
27
- <UBadge variant="subtle" size="sm" color="primary" :label="t(getTypeLabelKey(page.type))" />
28
- </template>
33
+ <aside class="flex flex-col gap-md">
34
+ <UCard
35
+ variant="soft"
36
+ :ui="{ root: 'divide-none', header: 'bg-accented text-center', body: 'p-0 sm:p-0 bg-muted' }"
37
+ >
38
+ <template #header>
39
+ <div class="flex flex-col gap-xs items-center">
40
+ <NuxtImg
41
+ v-if="page.icon?.src"
42
+ :src="page.icon?.src"
43
+ :alt="page.icon?.alt"
44
+ class="rounded-full w-12 h-12 object-cover"
45
+ />
29
46
 
30
- <div v-for="(group, groupId) in page.properties" :key="groupId">
31
- <div class="flex items-center gap-3">
32
- <span class="text-[10px] font-bold uppercase tracking-widest text-primary">
33
- {{ group.label[locale] }}
34
- </span>
35
- <div class="h-px flex-1 bg-border/50"></div>
36
- </div>
47
+ <h3>
48
+ {{ getLocalizedContent(page.title, locale) }}
49
+ </h3>
37
50
 
38
- <div class="grid gap-y-4 px-1">
39
- <template v-for="[fieldKey, schema] in getSortedFields(group.fields)" :key="fieldKey">
40
- <UFormField
41
- v-if="isFieldVisible(schema)"
42
- :label="getLocalizedContent(schema.label, locale)"
43
- :name="fieldKey"
44
- >
45
- <UInput
46
- v-if="schema.type === 'text'"
47
- v-model="schema.value[locale]"
48
- variant="subtle"
49
- placeholder="..."
50
- />
51
+ <span class="text-sm">{{ t(getTypeLabelKey(page.type)) }}</span>
51
52
 
52
- <UInput
53
- v-else-if="schema.type === 'number'"
54
- v-model.number="schema.value"
55
- type="number"
56
- variant="subtle"
57
- />
53
+ <div v-if="page.tags?.length" class="flex flex-row flex-wrap gap-xs">
54
+ <UBadge
55
+ v-for="tag in page.tags"
56
+ :key="tag[locale]"
57
+ variant="soft"
58
+ size="xs"
59
+ color="neutral"
60
+ >
61
+ {{ tag[locale] }}
62
+ </UBadge>
63
+ </div>
58
64
 
59
- <USelect
60
- v-else-if="schema.type === 'enum'"
61
- v-model="schema.value"
62
- :items="schema.options || []"
63
- variant="subtle"
64
- />
65
+ <div v-if="page.images?.length" class="w-full">
66
+ <UTabs
67
+ v-if="page.images.length > 1"
68
+ :items="imageTabs"
69
+ default-value="image-0"
70
+ variant="link"
71
+ size="xs"
72
+ color="neutral"
73
+ class="w-full"
74
+ >
75
+ <template #content="{ item }">
76
+ <NuxtImg :src="item.img.src" :alt="item.img.alt" class="w-full object-cover" />
77
+ </template>
78
+ </UTabs>
65
79
 
66
- <UInputMenu
67
- v-else-if="schema.type === 'text-array'"
68
- :model-value="schema.value.map((v) => v[locale])"
69
- @update:model-value="(vals) => schema.value = vals.map((str) => ({ [locale]: str }))"
70
- multiple
71
- creatable
72
- variant="subtle"
73
- placeholder="Add item..."
74
- />
80
+ <div v-else-if="page.images[0]">
81
+ <NuxtImg
82
+ :src="page.images[0].src"
83
+ :alt="page.images[0].alt"
84
+ class="w-full object-cover"
85
+ />
86
+ </div>
87
+ </div>
88
+ </div>
89
+ </template>
75
90
 
76
- <UInput
77
- v-else-if="schema.type === 'page'"
78
- v-model="schema.value"
79
- icon="lucide:link-2"
80
- variant="subtle"
81
- :placeholder="`Select ${schema.allowedPageTypes?.join('/')}`"
82
- />
83
- </UFormField>
84
- </template>
85
- </div>
86
- </div>
91
+ <template #default>
92
+ <template v-for="[groupId, group] in getSortedGroups(page.properties)" :key="groupId">
93
+ <UCollapsible v-if="shouldRenderGroup(group, false)" :default-open="group.defaultOpen">
94
+ <template #default>
95
+ <UButton
96
+ :label="getLocalizedContent(group.label, locale)"
97
+ variant="soft"
98
+ trailing-icon="lucide:chevron-down"
99
+ :ui="{
100
+ trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200'
101
+ }"
102
+ block
103
+ class="group rounded-none bg-elevated text-default"
104
+ />
105
+ </template>
106
+
107
+ <template #content>
108
+ <dl class="p-sm flex flex-col gap-xs">
109
+ <template v-for="[fieldKey, schema] in getSortedFields(group.fields)" :key="fieldKey">
110
+ <UFormField
111
+ v-if="isFieldVisible(schema, false)"
112
+ :label="getLocalizedContent(schema.label, locale)"
113
+ :name="fieldKey"
114
+ >
115
+ <UInput
116
+ v-if="schema.type === 'text'"
117
+ v-model="schema.value.en"
118
+ variant="subtle"
119
+ placeholder="Type here..."
120
+ class="w-full"
121
+ />
87
122
 
88
- <USeparator class="my-6" />
123
+ <UInput
124
+ v-else-if="schema.type === 'number'"
125
+ v-model.number="schema.value"
126
+ type="number"
127
+ variant="subtle"
128
+ class="w-full"
129
+ />
89
130
 
90
- <UFormField label="Global Search Tags">
91
- <UInputMenu
92
- v-model="page.tags"
93
- multiple
94
- creatable
95
- icon="lucide:tag"
96
- variant="subtle"
97
- placeholder="Add tags..."
131
+ <USelect
132
+ v-else-if="schema.type === 'enum'"
133
+ v-model="schema.value"
134
+ :items="schema.options || []"
135
+ variant="subtle"
136
+ class="w-full"
137
+ />
138
+
139
+ <UInputMenu
140
+ v-else-if="schema.type === 'text-array'"
141
+ :model-value="schema.value.map((v) => v.en)"
142
+ @update:model-value="(vals) => updateTextArray(schema, vals)"
143
+ multiple
144
+ creatable
145
+ variant="subtle"
146
+ placeholder="Add item..."
147
+ class="w-full"
148
+ />
149
+
150
+ <UInput
151
+ v-else-if="schema.type === 'page'"
152
+ v-model="schema.value"
153
+ icon="lucide:link-2"
154
+ variant="subtle"
155
+ :placeholder="`Select ${schema.allowedPageTypes?.join('/')}`"
156
+ class="w-full"
157
+ />
158
+
159
+ <USelectMenu
160
+ v-else-if="schema.type === 'page-array'"
161
+ v-model="schema.value"
162
+ icon="lucide:link-2"
163
+ variant="subtle"
164
+ :placeholder="`Select ${schema.allowedPageTypes?.join('/')}`"
165
+ class="w-full"
166
+ />
167
+ </UFormField>
168
+ </template>
169
+ </dl>
170
+ </template>
171
+ </UCollapsible>
172
+ </template>
173
+ </template>
174
+ </UCard>
175
+ <div class="flex flex-col gap-xs">
176
+ <h6>Links</h6>
177
+ <UButton
178
+ v-for="(link, index) in page.links"
179
+ :key="index"
180
+ :label="link.label"
181
+ :icon="link.icon"
182
+ :to="link.to"
183
+ :target="link.to ? '_blank' : void 0"
184
+ :external="!!link.to"
185
+ :variant="link.variant || 'link'"
186
+ :color="link.color || 'neutral'"
187
+ size="sm"
188
+ :ui="{ base: 'pl-0' }"
98
189
  />
99
- </UFormField>
100
- </UCard>
190
+ </div>
191
+ </aside>
101
192
  </template>
@@ -1,36 +1,19 @@
1
1
  <script setup>
2
2
  import { computed } from "vue";
3
3
  import { getLocalizedContent } from "../../utils";
4
- import { usePageRegistry } from "../../composables";
4
+ import { usePageRegistry, useInfobox } from "../../composables";
5
5
  import { useToast } from "@nuxt/ui/composables";
6
6
  import {} from "@nuxt/ui/components/Tabs.vue";
7
7
  import { useI18n } from "vue-i18n";
8
8
  import { useShare, useClipboard } from "@vueuse/core";
9
9
  import {} from "../../types";
10
- const { getTypeLabelKey } = usePageRegistry();
11
10
  const page = defineModel({ type: null, ...{ required: true } });
11
+ const { getTypeLabelKey } = usePageRegistry();
12
+ const { isFieldVisible, shouldRenderGroup, getSortedFields, getSortedGroups } = useInfobox(page.value.properties);
12
13
  const { t, locale } = useI18n();
13
14
  const { share } = useShare();
14
15
  const { copy } = useClipboard();
15
16
  const toast = useToast();
16
- const shouldRenderField = (schema) => {
17
- const isVisible = !schema.visibleIf || schema.visibleIf(page.value.properties);
18
- if (!isVisible) return false;
19
- const val = schema.value;
20
- if (schema.type === "text") {
21
- return !!val?.[locale.value];
22
- }
23
- if (schema.type === "text-array") {
24
- return Array.isArray(val) && val.length > 0;
25
- }
26
- return val !== void 0 && val !== null && val !== "";
27
- };
28
- const shouldRenderGroup = (group) => {
29
- return Object.values(group.fields || {}).some((schema) => shouldRenderField(schema));
30
- };
31
- const getSortedFields = (fields) => {
32
- return Object.entries(fields).sort(([, a], [, b]) => (a.order ?? 0) - (b.order ?? 0));
33
- };
34
17
  const sharePage = async () => {
35
18
  if (!page.value) {
36
19
  return;
@@ -142,8 +125,8 @@ const imageTabs = computed(() => {
142
125
  </template>
143
126
 
144
127
  <template #default>
145
- <template v-for="(group, groupId) in page.properties" :key="groupId">
146
- <UCollapsible v-if="shouldRenderGroup(group)" :default-open="group.defaultOpen">
128
+ <template v-for="[groupId, group] in getSortedGroups(page.properties)" :key="groupId">
129
+ <UCollapsible v-if="shouldRenderGroup(group, true)" :default-open="group.defaultOpen">
147
130
  <template #default>
148
131
  <UButton
149
132
  :label="getLocalizedContent(group.label, locale)"
@@ -164,7 +147,7 @@ const imageTabs = computed(() => {
164
147
  :key="fieldKey"
165
148
  >
166
149
  <div
167
- v-if="shouldRenderField(schema)"
150
+ v-if="isFieldVisible(schema, true)"
168
151
  class="grid grid-cols-3 gap-xs items-baseline"
169
152
  >
170
153
  <dt class="text-xs font-semibold text-dimmed">
@@ -3,7 +3,7 @@ interface PageRendererProps {
3
3
  useSurround?: boolean;
4
4
  surround?: PageSurround | null;
5
5
  surroundStatus?: 'idle' | 'pending' | 'success' | 'error';
6
- resolvePage?: (id: string) => Promise<Pick<Page, 'title' | 'icon' | 'slug'>>;
6
+ resolvePage: (id: string) => Promise<Pick<Page, 'title' | 'icon' | 'slug'>>;
7
7
  }
8
8
  type __VLS_Props = PageRendererProps;
9
9
  type __VLS_ModelProps = {
@@ -11,7 +11,7 @@ const { useSurround = false, surroundStatus = "idle", surround = null, resolvePa
11
11
  useSurround: { type: Boolean, required: false },
12
12
  surround: { type: [Object, null], required: false },
13
13
  surroundStatus: { type: String, required: false },
14
- resolvePage: { type: Function, required: false }
14
+ resolvePage: { type: Function, required: true }
15
15
  });
16
16
  const emit = defineEmits([]);
17
17
  provide("page-resolver", resolvePage);
@@ -26,7 +26,7 @@ const hasSurround = computed(() => !!(surround?.previous || surround?.next));
26
26
  <RCPageTOC
27
27
  :page-blocks="page.blocks"
28
28
  :levels="[2, 3, 4]"
29
- class="hidden lg:flex lg:col-span-4 sticky top-32"
29
+ class="hidden lg:flex lg:col-span-4 sticky top-16"
30
30
  >
31
31
  <template #bottom> </template>
32
32
  </RCPageTOC>
@@ -3,7 +3,7 @@ interface PageRendererProps {
3
3
  useSurround?: boolean;
4
4
  surround?: PageSurround | null;
5
5
  surroundStatus?: 'idle' | 'pending' | 'success' | 'error';
6
- resolvePage?: (id: string) => Promise<Pick<Page, 'title' | 'icon' | 'slug'>>;
6
+ resolvePage: (id: string) => Promise<Pick<Page, 'title' | 'icon' | 'slug'>>;
7
7
  }
8
8
  type __VLS_Props = PageRendererProps;
9
9
  type __VLS_ModelProps = {
@@ -84,7 +84,7 @@ onMounted(() => {
84
84
  </script>
85
85
 
86
86
  <template>
87
- <nav class="flex flex-col gap-sm self-start w-full" aria-label="Table of Contents">
87
+ <nav class="flex flex-col gap-sm w-full" aria-label="Table of Contents">
88
88
  <h5 v-if="title">
89
89
  {{ t(title) }}
90
90
  </h5>
@@ -0,0 +1,35 @@
1
+ import { type PageDefinition, type Page } from "../../../types/index.js";
2
+ export interface CreatePageModalProps {
3
+ isOpen: boolean;
4
+ definitions: Record<string, PageDefinition>;
5
+ loading?: boolean;
6
+ }
7
+ export interface CreatePageModalEmits {
8
+ close: [];
9
+ confirm: [page: Partial<Page>];
10
+ }
11
+ declare var __VLS_10: {};
12
+ type __VLS_Slots = {} & {
13
+ default?: (props: typeof __VLS_10) => any;
14
+ };
15
+ declare const __VLS_base: import("vue").DefineComponent<CreatePageModalProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
16
+ close: () => any;
17
+ confirm: (page: Partial<{
18
+ type: "Default";
19
+ properties: import("../../../types/index.js").BasePageProperties;
20
+ } & import("../../../types/index.js").BasePage>) => any;
21
+ }, string, import("vue").PublicProps, Readonly<CreatePageModalProps> & Readonly<{
22
+ onClose?: (() => any) | undefined;
23
+ onConfirm?: ((page: Partial<{
24
+ type: "Default";
25
+ properties: import("../../../types/index.js").BasePageProperties;
26
+ } & import("../../../types/index.js").BasePage>) => any) | undefined;
27
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
28
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
29
+ declare const _default: typeof __VLS_export;
30
+ export default _default;
31
+ type __VLS_WithSlots<T, S> = T & {
32
+ new (): {
33
+ $slots: S;
34
+ };
35
+ };
@@ -0,0 +1,108 @@
1
+ <script setup>
2
+ import { ref, computed, watch } from "vue";
3
+ import {} from "../../../types";
4
+ import { tv } from "tailwind-variants";
5
+ import { useI18n } from "vue-i18n";
6
+ const { t } = useI18n();
7
+ const { isOpen, loading, definitions } = defineProps({
8
+ isOpen: { type: Boolean, required: true },
9
+ definitions: { type: Object, required: true },
10
+ loading: { type: Boolean, required: false }
11
+ });
12
+ const emits = defineEmits(["close", "confirm"]);
13
+ const createPageModalStyles = tv({
14
+ slots: {
15
+ base: "flex flex-col gap-6",
16
+ group: "border border-neutral-200 rounded-lg p-4 bg-white shadow-sm",
17
+ groupTitle: "text-lg font-bold text-neutral-900 mb-4",
18
+ field: "flex flex-col gap-1.5",
19
+ label: "text-sm font-medium text-neutral-700",
20
+ input: "w-full px-3 py-2 rounded-md border border-neutral-300 focus:ring-2 focus:ring-primary-500 transition-all outline-hidden",
21
+ select: "w-full px-3 py-2 rounded-md border border-neutral-300 bg-white"
22
+ }
23
+ });
24
+ const styles = createPageModalStyles();
25
+ const selectedType = ref("");
26
+ const title = ref("");
27
+ const slug = ref("");
28
+ const typeOptions = computed(() => {
29
+ return Object.entries(definitions).map(([key, def]) => ({
30
+ label: t(def.typeLabelKey),
31
+ value: key
32
+ }));
33
+ });
34
+ watch(title, (newTitle) => {
35
+ slug.value = newTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
36
+ });
37
+ const handleConfirm = () => {
38
+ if (!selectedType.value) return;
39
+ const definition = definitions[selectedType.value];
40
+ if (!definition) {
41
+ console.error(`Definition for type "${selectedType.value}" not found.`);
42
+ return;
43
+ }
44
+ const properties = {};
45
+ Object.entries(definition.properties).forEach(([groupKey, group]) => {
46
+ properties[groupKey] = {};
47
+ Object.entries(group.fields).forEach(([fieldKey, field]) => {
48
+ properties[groupKey][fieldKey] = field.value;
49
+ });
50
+ });
51
+ const newPage = {
52
+ type: selectedType.value,
53
+ title: { en: title.value },
54
+ slug: slug.value,
55
+ properties,
56
+ blocks: definition.initialBlocks ? definition.initialBlocks() : []
57
+ };
58
+ emits("confirm", newPage);
59
+ };
60
+ </script>
61
+
62
+ <template>
63
+ <UModal :model-value="isOpen" @update:model-value="emits('close')">
64
+ <slot/>
65
+ <template #content>
66
+ <UCard>
67
+ <template #header>
68
+ <div class="flex items-center justify-between">
69
+ <h3 class="text-base font-semibold leading-6">Create New Page</h3>
70
+ <UButton color="neutral" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="emits('close')" />
71
+ </div>
72
+ </template>
73
+
74
+ <div class="space-y-4 py-4">
75
+ <UFormField label="Page Template" required>
76
+ <USelect
77
+ v-model="selectedType"
78
+ :items="typeOptions"
79
+ :placeholder="t('editor.template_placeholder', 'Select a template...')"
80
+ class="w-full"
81
+ />
82
+ </UFormField>
83
+
84
+ <UFormField label="Title" required>
85
+ <UInput v-model="title" placeholder="e.g. My Awesome Movie" />
86
+ </UFormField>
87
+
88
+ <UFormField label="Slug" required>
89
+ <UInput v-model="slug" placeholder="my-awesome-movie" />
90
+ </UFormField>
91
+ </div>
92
+
93
+ <template #footer>
94
+ <div class="flex justify-end gap-2">
95
+ <UButton color="neutral" variant="ghost" label="Cancel" @click="emits('close')" />
96
+ <UButton
97
+ color="primary"
98
+ label="Create & Edit"
99
+ :loading="loading"
100
+ :disabled="!selectedType || !title"
101
+ @click="handleConfirm"
102
+ />
103
+ </div>
104
+ </template>
105
+ </UCard>
106
+ </template>
107
+ </UModal>
108
+ </template>
@@ -0,0 +1,35 @@
1
+ import { type PageDefinition, type Page } from "../../../types/index.js";
2
+ export interface CreatePageModalProps {
3
+ isOpen: boolean;
4
+ definitions: Record<string, PageDefinition>;
5
+ loading?: boolean;
6
+ }
7
+ export interface CreatePageModalEmits {
8
+ close: [];
9
+ confirm: [page: Partial<Page>];
10
+ }
11
+ declare var __VLS_10: {};
12
+ type __VLS_Slots = {} & {
13
+ default?: (props: typeof __VLS_10) => any;
14
+ };
15
+ declare const __VLS_base: import("vue").DefineComponent<CreatePageModalProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
16
+ close: () => any;
17
+ confirm: (page: Partial<{
18
+ type: "Default";
19
+ properties: import("../../../types/index.js").BasePageProperties;
20
+ } & import("../../../types/index.js").BasePage>) => any;
21
+ }, string, import("vue").PublicProps, Readonly<CreatePageModalProps> & Readonly<{
22
+ onClose?: (() => any) | undefined;
23
+ onConfirm?: ((page: Partial<{
24
+ type: "Default";
25
+ properties: import("../../../types/index.js").BasePageProperties;
26
+ } & import("../../../types/index.js").BasePage>) => any) | undefined;
27
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
28
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
29
+ declare const _default: typeof __VLS_export;
30
+ export default _default;
31
+ type __VLS_WithSlots<T, S> = T & {
32
+ new (): {
33
+ $slots: S;
34
+ };
35
+ };
@@ -0,0 +1,28 @@
1
+ export interface DeletePageModalProps {
2
+ isOpen: boolean;
3
+ loading?: boolean;
4
+ pageTitle: string;
5
+ }
6
+ export interface DeletePageModalEmits {
7
+ (e: 'close'): void;
8
+ (e: 'confirm'): void;
9
+ }
10
+ declare var __VLS_10: {};
11
+ type __VLS_Slots = {} & {
12
+ default?: (props: typeof __VLS_10) => any;
13
+ };
14
+ declare const __VLS_base: import("vue").DefineComponent<DeletePageModalProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
15
+ close: () => any;
16
+ confirm: () => any;
17
+ }, string, import("vue").PublicProps, Readonly<DeletePageModalProps> & Readonly<{
18
+ onClose?: (() => any) | undefined;
19
+ onConfirm?: (() => any) | undefined;
20
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
22
+ declare const _default: typeof __VLS_export;
23
+ export default _default;
24
+ type __VLS_WithSlots<T, S> = T & {
25
+ new (): {
26
+ $slots: S;
27
+ };
28
+ };
@@ -0,0 +1,84 @@
1
+ <script setup>
2
+ import { ref, watch } from "vue";
3
+ import { useI18n } from "vue-i18n";
4
+ const { isOpen, loading, pageTitle } = defineProps({
5
+ isOpen: { type: Boolean, required: true },
6
+ loading: { type: Boolean, required: false },
7
+ pageTitle: { type: String, required: true }
8
+ });
9
+ const emits = defineEmits(["close", "confirm"]);
10
+ const { t } = useI18n();
11
+ const confirmationInput = ref("");
12
+ const CONFIRMATION_TEXT = "DELETE";
13
+ watch(() => isOpen, (val) => {
14
+ if (!val) confirmationInput.value = "";
15
+ });
16
+ const handleConfirm = () => {
17
+ if (confirmationInput.value === CONFIRMATION_TEXT) {
18
+ emits("confirm");
19
+ }
20
+ };
21
+ </script>
22
+
23
+ <template>
24
+ <UModal :model-value="isOpen" @update:model-value="emits('close')">
25
+ <slot/>
26
+ <template #content>
27
+ <UCard :ui="{ body: 'space-y-4' }">
28
+ <template #header>
29
+ <div class="flex items-center justify-between">
30
+ <h3 class="text-base font-semibold leading-6 text-error-600">
31
+ {{ t("editor.delete_page_title", "Delete Page") }}
32
+ </h3>
33
+ <UButton
34
+ color="neutral"
35
+ variant="ghost"
36
+ icon="lucide:x"
37
+ class="-my-1"
38
+ @click="emits('close')"
39
+ />
40
+ </div>
41
+ </template>
42
+
43
+ <div class="text-sm text-neutral-600 dark:text-neutral-400">
44
+ <p>
45
+ Are you sure you want to delete <strong>{{ pageTitle }}</strong>?
46
+ This action is permanent and cannot be undone.
47
+ </p>
48
+ </div>
49
+
50
+ <UFormField
51
+ :label="t('editor.delete_confirm_label', `Please type ${CONFIRMATION_TEXT} to confirm`)"
52
+ required
53
+ >
54
+ <UInput
55
+ v-model="confirmationInput"
56
+ :placeholder="CONFIRMATION_TEXT"
57
+ color="error"
58
+ autocomplete="off"
59
+ @keyup.enter="handleConfirm"
60
+ />
61
+ </UFormField>
62
+
63
+ <template #footer>
64
+ <div class="flex justify-end gap-2">
65
+ <UButton
66
+ color="neutral"
67
+ variant="ghost"
68
+ :label="t('common.cancel', 'Cancel')"
69
+ @click="emits('close')"
70
+ />
71
+ <UButton
72
+ color="error"
73
+ icon="lucide:trash-2"
74
+ :label="t('editor.delete_button', 'Delete Permanently')"
75
+ :loading="loading"
76
+ :disabled="confirmationInput !== CONFIRMATION_TEXT"
77
+ @click="handleConfirm"
78
+ />
79
+ </div>
80
+ </template>
81
+ </UCard>
82
+ </template>
83
+ </UModal>
84
+ </template>
@@ -0,0 +1,28 @@
1
+ export interface DeletePageModalProps {
2
+ isOpen: boolean;
3
+ loading?: boolean;
4
+ pageTitle: string;
5
+ }
6
+ export interface DeletePageModalEmits {
7
+ (e: 'close'): void;
8
+ (e: 'confirm'): void;
9
+ }
10
+ declare var __VLS_10: {};
11
+ type __VLS_Slots = {} & {
12
+ default?: (props: typeof __VLS_10) => any;
13
+ };
14
+ declare const __VLS_base: import("vue").DefineComponent<DeletePageModalProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
15
+ close: () => any;
16
+ confirm: () => any;
17
+ }, string, import("vue").PublicProps, Readonly<DeletePageModalProps> & Readonly<{
18
+ onClose?: (() => any) | undefined;
19
+ onConfirm?: (() => any) | undefined;
20
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
22
+ declare const _default: typeof __VLS_export;
23
+ export default _default;
24
+ type __VLS_WithSlots<T, S> = T & {
25
+ new (): {
26
+ $slots: S;
27
+ };
28
+ };
@@ -2,3 +2,4 @@ export * from "./useDateRange.js";
2
2
  export * from "./usePageEditor.js";
3
3
  export * from "./useBlockEditor.js";
4
4
  export * from "./usePageRegistry.js";
5
+ export * from "./useInfobox.js";
@@ -2,3 +2,4 @@ export * from "./useDateRange.js";
2
2
  export * from "./usePageEditor.js";
3
3
  export * from "./useBlockEditor.js";
4
4
  export * from "./usePageRegistry.js";
5
+ export * from "./useInfobox.js";
@@ -2,3 +2,4 @@ export * from "./useDateRange.mjs";
2
2
  export * from "./usePageEditor.mjs";
3
3
  export * from "./useBlockEditor.mjs";
4
4
  export * from "./usePageRegistry.mjs";
5
+ export * from "./useInfobox.mjs";
@@ -0,0 +1,8 @@
1
+ import type { Property, PropertyGroup, BasePageProperties } from '../types/index.js';
2
+ export declare const useInfobox: (properties: BasePageProperties) => {
3
+ isFieldVisible: (schema: Property, isReadOnly: boolean) => boolean;
4
+ shouldRenderGroup: (group: PropertyGroup, isReadOnly: boolean) => boolean;
5
+ getSortedFields: (fields: Record<string, Property>) => [string, Property<any>][];
6
+ getSortedGroups: (props: BasePageProperties) => [string, PropertyGroup][];
7
+ locale: import("vue").WritableComputedRef<string, string>;
8
+ };
@@ -0,0 +1,31 @@
1
+ import { useI18n } from "vue-i18n";
2
+ export const useInfobox = (properties) => {
3
+ const { locale } = useI18n();
4
+ const isFieldVisible = (schema, isReadOnly) => {
5
+ const passesLogic = !schema.visibleIf || schema.visibleIf(properties);
6
+ if (!passesLogic) return false;
7
+ if (isReadOnly) {
8
+ const val = schema.value;
9
+ if (schema.type === "text") return !!val?.[locale.value];
10
+ if (schema.type === "text-array") return Array.isArray(val) && val.length > 0;
11
+ return val !== void 0 && val !== null && val !== "";
12
+ }
13
+ return true;
14
+ };
15
+ const shouldRenderGroup = (group, isReadOnly) => {
16
+ return Object.values(group.fields).some((schema) => isFieldVisible(schema, isReadOnly));
17
+ };
18
+ const getSortedFields = (fields) => {
19
+ return Object.entries(fields).sort(([, a], [, b]) => (a.order ?? 0) - (b.order ?? 0));
20
+ };
21
+ const getSortedGroups = (props) => {
22
+ return Object.entries(props).sort(([, a], [, b]) => (a.order ?? 0) - (b.order ?? 0));
23
+ };
24
+ return {
25
+ isFieldVisible,
26
+ shouldRenderGroup,
27
+ getSortedFields,
28
+ getSortedGroups,
29
+ locale
30
+ };
31
+ };
@@ -0,0 +1,31 @@
1
+ import { useI18n } from "vue-i18n";
2
+ export const useInfobox = (properties) => {
3
+ const { locale } = useI18n();
4
+ const isFieldVisible = (schema, isReadOnly) => {
5
+ const passesLogic = !schema.visibleIf || schema.visibleIf(properties);
6
+ if (!passesLogic) return false;
7
+ if (isReadOnly) {
8
+ const val = schema.value;
9
+ if (schema.type === "text") return !!val?.[locale.value];
10
+ if (schema.type === "text-array") return Array.isArray(val) && val.length > 0;
11
+ return val !== void 0 && val !== null && val !== "";
12
+ }
13
+ return true;
14
+ };
15
+ const shouldRenderGroup = (group, isReadOnly) => {
16
+ return Object.values(group.fields).some((schema) => isFieldVisible(schema, isReadOnly));
17
+ };
18
+ const getSortedFields = (fields) => {
19
+ return Object.entries(fields).sort(([, a], [, b]) => (a.order ?? 0) - (b.order ?? 0));
20
+ };
21
+ const getSortedGroups = (props) => {
22
+ return Object.entries(props).sort(([, a], [, b]) => (a.order ?? 0) - (b.order ?? 0));
23
+ };
24
+ return {
25
+ isFieldVisible,
26
+ shouldRenderGroup,
27
+ getSortedFields,
28
+ getSortedGroups,
29
+ locale
30
+ };
31
+ };
@@ -13,14 +13,12 @@ export function usePageEditor(page, maxHistorySize = 100) {
13
13
  const undo = () => {
14
14
  if (history.value.length === 0) return;
15
15
  future.value = [JSON.stringify(page.value), ...future.value];
16
- const previous = JSON.parse(history.value.pop());
17
- page.value = previous;
16
+ page.value = JSON.parse(history.value.pop());
18
17
  };
19
18
  const redo = () => {
20
19
  if (future.value.length === 0) return;
21
20
  history.value = [...history.value, JSON.stringify(page.value)];
22
- const next = JSON.parse(future.value.shift());
23
- page.value = next;
21
+ page.value = JSON.parse(future.value.shift());
24
22
  };
25
23
  const canUndo = computed(() => history.value.length > 0);
26
24
  const canRedo = computed(() => future.value.length > 0);
@@ -13,14 +13,12 @@ export function usePageEditor(page, maxHistorySize = 100) {
13
13
  const undo = () => {
14
14
  if (history.value.length === 0) return;
15
15
  future.value = [JSON.stringify(page.value), ...future.value];
16
- const previous = JSON.parse(history.value.pop());
17
- page.value = previous;
16
+ page.value = JSON.parse(history.value.pop());
18
17
  };
19
18
  const redo = () => {
20
19
  if (future.value.length === 0) return;
21
20
  history.value = [...history.value, JSON.stringify(page.value)];
22
- const next = JSON.parse(future.value.shift());
23
- page.value = next;
21
+ page.value = JSON.parse(future.value.shift());
24
22
  };
25
23
  const canUndo = computed(() => history.value.length > 0);
26
24
  const canRedo = computed(() => future.value.length > 0);
@@ -32,6 +32,7 @@ export interface PageDefinition {
32
32
  initialBlocks?: () => Block[];
33
33
  }
34
34
  export interface BasePageProperties {
35
+ [key: string]: PropertyGroup | undefined;
35
36
  }
36
37
  /**
37
38
  * Common fields shared by every page regardless of type.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rimelight-components",
3
- "version": "2.1.11",
3
+ "version": "2.1.13",
4
4
  "description": "A component library by Rimelight Entertainment.",
5
5
  "keywords": [
6
6
  "nuxt",