rimelight-components 2.1.80 → 2.1.82

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/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rimelight-components",
3
- "version": "2.1.80",
3
+ "version": "2.1.82",
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.80";
7
+ const version = "2.1.82";
8
8
  const homepage = "https://rimelight.com/tools/rimelight-components";
9
9
 
10
10
  const defaultOptions = {
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { computed } from "vue";
2
+ import { computed, ref, reactive } from "vue";
3
3
  import { useI18n } from "vue-i18n";
4
4
  import { usePageRegistry, useInfobox, useRC } from "../../composables";
5
5
  import { getLocalizedContent } from "../../utils";
@@ -61,6 +61,55 @@ const updateTextArray = (schema, vals) => {
61
61
  en: str
62
62
  }));
63
63
  };
64
+ const isLinkModalOpen = ref(false);
65
+ const editingLinkIndex = ref(null);
66
+ const linkDraft = reactive({
67
+ label: "",
68
+ to: "",
69
+ icon: "",
70
+ color: "neutral",
71
+ variant: "link"
72
+ });
73
+ const openLinkModal = (index = null) => {
74
+ editingLinkIndex.value = index;
75
+ if (index !== null && page.value.links?.[index]) {
76
+ const link = page.value.links[index];
77
+ linkDraft.label = link.label;
78
+ linkDraft.to = link.to;
79
+ linkDraft.icon = link.icon;
80
+ linkDraft.color = link.color || "neutral";
81
+ linkDraft.variant = link.variant || "link";
82
+ } else {
83
+ linkDraft.label = "";
84
+ linkDraft.to = "";
85
+ linkDraft.icon = "";
86
+ linkDraft.color = "neutral";
87
+ linkDraft.variant = "link";
88
+ }
89
+ isLinkModalOpen.value = true;
90
+ };
91
+ const saveLink = () => {
92
+ if (!linkDraft.label || !linkDraft.to) return;
93
+ if (!page.value.links) page.value.links = [];
94
+ const newLink = {
95
+ label: linkDraft.label,
96
+ to: linkDraft.to,
97
+ icon: linkDraft.icon,
98
+ color: linkDraft.color,
99
+ variant: linkDraft.variant
100
+ };
101
+ if (editingLinkIndex.value !== null) {
102
+ page.value.links[editingLinkIndex.value] = newLink;
103
+ } else {
104
+ page.value.links.push(newLink);
105
+ }
106
+ isLinkModalOpen.value = false;
107
+ };
108
+ const removeLink = (index) => {
109
+ if (page.value.links) {
110
+ page.value.links.splice(index, 1);
111
+ }
112
+ };
64
113
  </script>
65
114
 
66
115
  <template>
@@ -87,6 +136,16 @@ const updateTextArray = (schema, vals) => {
87
136
  :class="titleInput({ class: rc.titleInput })"
88
137
  />
89
138
 
139
+ <UInput
140
+ v-model="page.slug"
141
+ variant="subtle"
142
+ placeholder="page-slug"
143
+ size="xs"
144
+ prefix="/"
145
+ :ui="{ base: 'text-center text-dimmed font-mono' }"
146
+ class="w-full opacity-60 hover:opacity-100 focus-within:opacity-100 transition-opacity"
147
+ />
148
+
90
149
  <span :class="type({ class: rc.type })">{{ t(getTypeLabelKey(page.type)) }}</span>
91
150
 
92
151
  <div v-if="page.tags?.length" :class="tags({ class: rc.tags })">
@@ -221,20 +280,99 @@ const updateTextArray = (schema, vals) => {
221
280
  </template>
222
281
  </UCard>
223
282
  <div :class="links({ class: rc.links })">
224
- <h6>Links</h6>
225
- <UButton
226
- v-for="(linkItem, index) in page.links"
227
- :key="index"
228
- :label="linkItem.label"
229
- :icon="linkItem.icon"
230
- :to="linkItem.to"
231
- :target="linkItem.to ? '_blank' : void 0"
232
- :external="!!linkItem.to"
233
- :variant="linkItem.variant || 'link'"
234
- :color="linkItem.color || 'neutral'"
235
- size="sm"
236
- :ui="{ base: 'pl-0' }"
237
- />
283
+ <div class="flex items-center justify-between mb-xs">
284
+ <h6>Links</h6>
285
+ <UButton
286
+ icon="lucide:plus"
287
+ size="xs"
288
+ variant="ghost"
289
+ color="primary"
290
+ @click="openLinkModal()"
291
+ />
292
+ </div>
293
+
294
+ <div v-if="page.links?.length" class="flex flex-col gap-xs">
295
+ <div
296
+ v-for="(linkItem, index) in page.links"
297
+ :key="index"
298
+ class="flex items-center justify-between group/link"
299
+ >
300
+ <UButton
301
+ :label="linkItem.label"
302
+ :icon="linkItem.icon"
303
+ :to="linkItem.to"
304
+ :target="linkItem.to ? '_blank' : void 0"
305
+ :external="!!linkItem.to"
306
+ :variant="linkItem.variant || 'link'"
307
+ :color="linkItem.color || 'neutral'"
308
+ size="sm"
309
+ :ui="{ base: 'pl-0' }"
310
+ />
311
+ <div class="flex items-center opacity-0 group-hover/link:opacity-100 transition-opacity">
312
+ <UButton
313
+ icon="lucide:pencil"
314
+ size="xs"
315
+ variant="ghost"
316
+ color="neutral"
317
+ @click="openLinkModal(index)"
318
+ />
319
+ <UButton
320
+ icon="lucide:trash-2"
321
+ size="xs"
322
+ variant="ghost"
323
+ color="error"
324
+ @click="removeLink(index)"
325
+ />
326
+ </div>
327
+ </div>
328
+ </div>
329
+ <p v-else class="text-xs text-dimmed italic">No links added yet.</p>
330
+
331
+ <!-- Link management modal -->
332
+ <UModal v-model:open="isLinkModalOpen" :title="editingLinkIndex !== null ? 'Edit Link' : 'Add Link'">
333
+ <template #content>
334
+ <UCard>
335
+ <div class="flex flex-col gap-sm">
336
+ <UFormField label="Label">
337
+ <UInput v-model="linkDraft.label" placeholder="Check my GitHub" class="w-full" />
338
+ </UFormField>
339
+ <UFormField label="URL (to)">
340
+ <UInput v-model="linkDraft.to" placeholder="https://github.com/..." class="w-full" />
341
+ </UFormField>
342
+ <UFormField label="Icon">
343
+ <UInput v-model="linkDraft.icon" placeholder="lucide:github" class="w-full" />
344
+ </UFormField>
345
+ <div class="grid grid-cols-2 gap-sm">
346
+ <UFormField label="Color">
347
+ <USelect
348
+ v-model="linkDraft.color"
349
+ :items="['primary', 'secondary', 'neutral', 'error', 'warning', 'success', 'info']"
350
+ class="w-full"
351
+ />
352
+ </UFormField>
353
+ <UFormField label="Variant">
354
+ <USelect
355
+ v-model="linkDraft.variant"
356
+ :items="['solid', 'outline', 'subtle', 'soft', 'ghost', 'link']"
357
+ class="w-full"
358
+ />
359
+ </UFormField>
360
+ </div>
361
+ </div>
362
+
363
+ <template #footer>
364
+ <div class="flex justify-end gap-sm">
365
+ <UButton label="Cancel" variant="ghost" color="neutral" @click="isLinkModalOpen = false" />
366
+ <UButton
367
+ :label="editingLinkIndex !== null ? 'Update Link' : 'Add Link'"
368
+ color="primary"
369
+ @click="saveLink"
370
+ />
371
+ </div>
372
+ </template>
373
+ </UCard>
374
+ </template>
375
+ </UModal>
238
376
  </div>
239
377
  </aside>
240
378
  </template>
@@ -0,0 +1,25 @@
1
+ import type { PageVersion } from "../../types/index.js";
2
+ export interface PageVersionSelectorProps {
3
+ pageId: string;
4
+ currentVersionId?: string | null;
5
+ isAdmin?: boolean;
6
+ rc?: {
7
+ root?: string;
8
+ button?: string;
9
+ popover?: string;
10
+ versionItem?: string;
11
+ };
12
+ }
13
+ declare const __VLS_export: import("vue").DefineComponent<PageVersionSelectorProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
14
+ "update:currentVersionId": (value: string | null) => any;
15
+ "version-selected": (version: PageVersion) => any;
16
+ "version-approved": (version: PageVersion) => any;
17
+ "version-reverted": (version: PageVersion) => any;
18
+ }, string, import("vue").PublicProps, Readonly<PageVersionSelectorProps> & Readonly<{
19
+ "onUpdate:currentVersionId"?: ((value: string | null) => any) | undefined;
20
+ "onVersion-selected"?: ((version: PageVersion) => any) | undefined;
21
+ "onVersion-approved"?: ((version: PageVersion) => any) | undefined;
22
+ "onVersion-reverted"?: ((version: PageVersion) => any) | undefined;
23
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
+ declare const _default: typeof __VLS_export;
25
+ export default _default;
@@ -0,0 +1,279 @@
1
+ <script setup>
2
+ import { formatDistanceToNow } from "date-fns";
3
+ import { useToast } from "#imports";
4
+ import { computed, ref, watch } from "vue";
5
+ import { useRC, $api } from "../../composables";
6
+ import { useI18n } from "vue-i18n";
7
+ import { tv } from "../../internal/tv";
8
+ const {
9
+ pageId,
10
+ currentVersionId,
11
+ isAdmin = false,
12
+ rc: rcProp
13
+ } = defineProps({
14
+ pageId: { type: String, required: true },
15
+ currentVersionId: { type: [String, null], required: false },
16
+ isAdmin: { type: Boolean, required: false },
17
+ rc: { type: Object, required: false }
18
+ });
19
+ const emit = defineEmits(["update:currentVersionId", "version-selected", "version-approved", "version-reverted"]);
20
+ const { rc } = useRC("PageVersionSelector", rcProp);
21
+ const { t } = useI18n();
22
+ const toast = useToast();
23
+ const versions = ref([]);
24
+ const isLoading = ref(false);
25
+ const isApproving = ref(null);
26
+ const isReverting = ref(null);
27
+ const isOpen = ref(false);
28
+ const pageVersionSelectorStyles = tv({
29
+ slots: {
30
+ root: "",
31
+ button: "",
32
+ popover: "w-80 p-2",
33
+ versionItem: "px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer rounded"
34
+ }
35
+ });
36
+ const {
37
+ root,
38
+ button: buttonClass,
39
+ popover,
40
+ versionItem
41
+ } = pageVersionSelectorStyles();
42
+ const pendingVersions = computed(() => {
43
+ return versions.value.filter((v) => v.status === "pending");
44
+ });
45
+ const selectedVersionId = computed({
46
+ get: () => currentVersionId,
47
+ set: (value) => emit("update:currentVersionId", value || null)
48
+ });
49
+ const fetchVersions = async () => {
50
+ if (!pageId) return;
51
+ isLoading.value = true;
52
+ try {
53
+ versions.value = await $api(`/api/pages/id/${pageId}/versions`);
54
+ } catch (error) {
55
+ console.error("Failed to fetch versions:", error);
56
+ try {
57
+ toast.add({ color: "error", title: "Failed to load versions" });
58
+ } catch (e) {
59
+ }
60
+ } finally {
61
+ isLoading.value = false;
62
+ }
63
+ };
64
+ const selectVersion = (version) => {
65
+ selectedVersionId.value = version.id;
66
+ emit("version-selected", version);
67
+ isOpen.value = false;
68
+ };
69
+ const approveVersion = async (version) => {
70
+ if (!isAdmin) return;
71
+ isApproving.value = version.id;
72
+ try {
73
+ const result = await $api(`/api/pages/versions/${version.id}/approve`, {
74
+ method: "POST"
75
+ });
76
+ try {
77
+ toast.add({
78
+ color: "success",
79
+ title: "Version approved successfully",
80
+ description: result?.message || "The page has been updated with the approved version"
81
+ });
82
+ } catch (e) {
83
+ }
84
+ emit("version-approved", version);
85
+ await fetchVersions();
86
+ if (selectedVersionId.value === version.id) {
87
+ selectedVersionId.value = null;
88
+ }
89
+ } catch (error) {
90
+ console.error("Failed to approve version:", error);
91
+ try {
92
+ toast.add({
93
+ color: "error",
94
+ title: "Failed to approve version",
95
+ description: error.message || "An error occurred"
96
+ });
97
+ } catch (e) {
98
+ }
99
+ } finally {
100
+ isApproving.value = null;
101
+ }
102
+ };
103
+ const revertVersion = async (version) => {
104
+ if (!isAdmin) return;
105
+ isReverting.value = version.id;
106
+ try {
107
+ const result = await $api(`/api/pages/versions/${version.id}/revert`, {
108
+ method: "POST"
109
+ });
110
+ try {
111
+ toast.add({
112
+ color: "success",
113
+ title: "Version reverted successfully",
114
+ description: result?.message || "The page has been reverted to this version."
115
+ });
116
+ } catch (e) {
117
+ }
118
+ emit("version-reverted", version);
119
+ await fetchVersions();
120
+ selectedVersionId.value = null;
121
+ } catch (error) {
122
+ console.error("Failed to revert version:", error);
123
+ try {
124
+ toast.add({
125
+ color: "error",
126
+ title: "Failed to revert version",
127
+ description: error.message || "An error occurred"
128
+ });
129
+ } catch (e) {
130
+ }
131
+ } finally {
132
+ isReverting.value = null;
133
+ }
134
+ };
135
+ const formatDate = (date) => {
136
+ if (!date) return "";
137
+ const d = typeof date === "string" ? new Date(date) : date;
138
+ return formatDistanceToNow(d, { addSuffix: true });
139
+ };
140
+ const getStatusColor = (status) => {
141
+ switch (status) {
142
+ case "approved":
143
+ return "success";
144
+ case "pending":
145
+ return "warning";
146
+ case "rejected":
147
+ return "error";
148
+ default:
149
+ return "neutral";
150
+ }
151
+ };
152
+ watch(() => pageId, () => {
153
+ if (pageId) fetchVersions();
154
+ }, { immediate: true });
155
+ </script>
156
+
157
+ <template>
158
+ <UPopover v-model:open="isOpen" :popper="{ placement: 'bottom-start' }" :class="root({ class: rc.root })">
159
+ <UButton
160
+ :loading="isLoading"
161
+ color="neutral"
162
+ icon="lucide:git-branch"
163
+ label="Versions"
164
+ size="xs"
165
+ variant="ghost"
166
+ :class="buttonClass({ class: rc.button })"
167
+ />
168
+
169
+ <template #content>
170
+ <div :class="popover({ class: rc.popover })">
171
+ <div class="px-3 py-2 border-b border-gray-200 dark:border-gray-800">
172
+ <h3 class="text-sm font-semibold">Page Versions</h3>
173
+ <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">View and manage page versions</p>
174
+ </div>
175
+
176
+ <div class="max-h-96 overflow-y-auto">
177
+ <div
178
+ :class="[
179
+ versionItem({ class: rc.versionItem }),
180
+ { 'bg-primary-50 dark:bg-primary-900/20': !selectedVersionId }
181
+ ]"
182
+ @click="selectedVersionId = null;
183
+ isOpen = false"
184
+ >
185
+ <div class="flex items-center justify-between">
186
+ <div class="flex-1">
187
+ <div class="flex items-center gap-2">
188
+ <UBadge color="success" size="xs">Live</UBadge>
189
+ <span class="text-sm font-medium">Current Version</span>
190
+ </div>
191
+ <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
192
+ The published version of this page
193
+ </p>
194
+ </div>
195
+ </div>
196
+ </div>
197
+
198
+ <div v-if="isLoading" class="px-3 py-4 text-center">
199
+ <USkeleton class="h-4 w-full mb-2" />
200
+ <USkeleton class="h-3 w-3/4" />
201
+ </div>
202
+
203
+ <div
204
+ v-else-if="versions.length === 0"
205
+ class="px-3 py-4 text-center text-sm text-gray-500"
206
+ >
207
+ No versions yet
208
+ </div>
209
+
210
+ <div v-else class="divide-y divide-gray-200 dark:divide-gray-800">
211
+ <div
212
+ v-for="version in versions"
213
+ :key="version.id"
214
+ :class="[
215
+ versionItem({ class: rc.versionItem }),
216
+ { 'bg-primary-50 dark:bg-primary-900/20': selectedVersionId === version.id }
217
+ ]"
218
+ @click="selectVersion(version)"
219
+ >
220
+ <div class="flex items-start justify-between gap-2">
221
+ <div class="flex-1 min-w-0">
222
+ <div class="flex items-center gap-2 mb-1">
223
+ <UBadge :color="getStatusColor(version.status)" size="xs">
224
+ {{ version.status }}
225
+ </UBadge>
226
+ <span class="text-xs text-gray-500 dark:text-gray-400">
227
+ {{ formatDate(version.createdAt) }}
228
+ </span>
229
+ </div>
230
+ <p class="text-xs text-gray-600 dark:text-gray-300 truncate">
231
+ {{ version.title?.en || version.title || "Untitled" }}
232
+ </p>
233
+ <p
234
+ v-if="version.approvedBy && version.approvedAt"
235
+ class="text-xs text-gray-500 dark:text-gray-400 mt-1"
236
+ >
237
+ Approved {{ formatDate(version.approvedAt) }}
238
+ </p>
239
+ </div>
240
+
241
+ <div v-if="isAdmin" class="shrink-0 flex items-center gap-1">
242
+ <UButton
243
+ v-if="version.status === 'pending'"
244
+ :loading="isApproving === version.id"
245
+ color="success"
246
+ icon="lucide:check"
247
+ size="xs"
248
+ title="Approve version"
249
+ variant="ghost"
250
+ @click.stop="approveVersion(version)"
251
+ />
252
+ <UButton
253
+ v-if="selectedVersionId === version.id"
254
+ :loading="isReverting === version.id"
255
+ color="warning"
256
+ icon="lucide:rotate-ccw"
257
+ size="xs"
258
+ title="Revert to this version"
259
+ variant="ghost"
260
+ @click.stop="revertVersion(version)"
261
+ />
262
+ </div>
263
+ </div>
264
+ </div>
265
+ </div>
266
+ </div>
267
+
268
+ <div
269
+ v-if="pendingVersions.length > 0 && isAdmin"
270
+ class="px-3 py-2 border-t border-gray-200 dark:border-gray-800 mt-2"
271
+ >
272
+ <p class="text-xs text-gray-500 dark:text-gray-400">
273
+ {{ pendingVersions.length }} pending version{{ pendingVersions.length !== 1 ? "s" : "" }} awaiting approval
274
+ </p>
275
+ </div>
276
+ </div>
277
+ </template>
278
+ </UPopover>
279
+ </template>
@@ -0,0 +1,25 @@
1
+ import type { PageVersion } from "../../types/index.js";
2
+ export interface PageVersionSelectorProps {
3
+ pageId: string;
4
+ currentVersionId?: string | null;
5
+ isAdmin?: boolean;
6
+ rc?: {
7
+ root?: string;
8
+ button?: string;
9
+ popover?: string;
10
+ versionItem?: string;
11
+ };
12
+ }
13
+ declare const __VLS_export: import("vue").DefineComponent<PageVersionSelectorProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
14
+ "update:currentVersionId": (value: string | null) => any;
15
+ "version-selected": (version: PageVersion) => any;
16
+ "version-approved": (version: PageVersion) => any;
17
+ "version-reverted": (version: PageVersion) => any;
18
+ }, string, import("vue").PublicProps, Readonly<PageVersionSelectorProps> & Readonly<{
19
+ "onUpdate:currentVersionId"?: ((value: string | null) => any) | undefined;
20
+ "onVersion-selected"?: ((version: PageVersion) => any) | undefined;
21
+ "onVersion-approved"?: ((version: PageVersion) => any) | undefined;
22
+ "onVersion-reverted"?: ((version: PageVersion) => any) | undefined;
23
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
+ declare const _default: typeof __VLS_export;
25
+ export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rimelight-components",
3
- "version": "2.1.80",
3
+ "version": "2.1.82",
4
4
  "description": "A component library by Rimelight Entertainment.",
5
5
  "keywords": [
6
6
  "nuxt",