@rimelight/ui 0.0.43 → 0.0.44
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/LICENSE +21 -0
- package/README.md +14 -12
- package/package.json +11 -14
- package/src/components/avatar/RLAAvatar.astro +4 -12
- package/src/components/badge/RLABadge.astro +2 -1
- package/src/components/badge/badge.theme.ts +0 -4
- package/src/components/banner/RLABanner.astro +1 -1
- package/src/components/banner/banner.theme.ts +1 -1
- package/src/components/breadcrumb/RLABreadcrumb.astro +2 -2
- package/src/components/button/RLAButton.astro +4 -1
- package/src/components/button/button.theme.ts +1 -5
- package/src/components/callout/RLACallout.astro +7 -5
- package/src/components/callout/callout.theme.ts +1 -5
- package/src/components/card/card.theme.ts +3 -3
- package/src/components/carousel/RLACarousel.astro +2 -2
- package/src/components/carousel/carousel.theme.ts +2 -2
- package/src/components/chart/RLAChart.astro +1 -1
- package/src/components/checkbox/RLACheckbox.astro +2 -1
- package/src/components/checkbox/checkbox.theme.ts +0 -4
- package/src/components/chip/RLAChip.astro +2 -1
- package/src/components/chip/chip.theme.ts +0 -4
- package/src/components/collapsible/RLACollapsible.astro +1 -1
- package/src/components/color-area/RLAColorArea.astro +1 -0
- package/src/components/color-area/color-area.theme.ts +1 -5
- package/src/components/color-field/RLAColorField.astro +1 -0
- package/src/components/color-field/color-field.theme.ts +1 -5
- package/src/components/color-slider/RLAColorSlider.astro +1 -0
- package/src/components/color-slider/color-slider.theme.ts +1 -5
- package/src/components/dashboard-group/RLADashboardGroup.astro +5 -7
- package/src/components/dashboard-group/dashboard-group.theme.ts +1 -1
- package/src/components/dashboard-group/dashboard-group.ts +1 -1
- package/src/components/dashboard-navbar/RLADashboardNavbar.astro +4 -4
- package/src/components/dashboard-navbar/dashboard-navbar.theme.ts +3 -3
- package/src/components/dashboard-navbar/dashboard-navbar.ts +1 -1
- package/src/components/dashboard-panel/RLADashboardPanel.astro +9 -8
- package/src/components/dashboard-panel/dashboard-panel.theme.ts +1 -1
- package/src/components/dashboard-sidebar/RLADashboardSidebar.astro +7 -4
- package/src/components/dashboard-sidebar/dashboard-sidebar.theme.ts +6 -5
- package/src/components/dashboard-sidebar-collapse/RLADashboardSidebarCollapse.astro +1 -0
- package/src/components/drawer/RLADrawer.astro +1 -1
- package/src/components/dropdown-menu/dropdown-menu.theme.ts +2 -2
- package/src/components/footer/RLAFooter.astro +2 -0
- package/src/components/footer/footer.theme.ts +0 -8
- package/src/components/gamepad/RLAGamepad.astro +1 -1
- package/src/components/head/UIHead.astro +1 -2
- package/src/components/header/header.theme.ts +1 -1
- package/src/components/icon/RLAIcon.astro +2 -5
- package/src/components/icon/RLSIcon.tsx +2 -2
- package/src/components/icon/RLVIcon.vue +18 -6
- package/src/components/icon/icon.theme.ts +1 -1
- package/src/components/image/image.theme.ts +1 -1
- package/src/components/input/RLAInput.astro +2 -1
- package/src/components/input/input.theme.ts +0 -4
- package/src/components/input-menu/RLAInputMenu.astro +2 -2
- package/src/components/input-menu/input-menu.theme.ts +1 -1
- package/src/components/input-rating/input-rating.theme.ts +2 -2
- package/src/components/kbd/RLAKbd.astro +2 -1
- package/src/components/kbd/kbd.theme.ts +0 -4
- package/src/components/keyboard/RLAKeyboard.astro +1 -1
- package/src/components/link/RLALink.astro +1 -0
- package/src/components/link/link.theme.ts +0 -4
- package/src/components/link-group/RLALinkGroup.astro +1 -1
- package/src/components/link-group/link-group.theme.ts +1 -10
- package/src/components/locale-selector/RLALocaleSelector.astro +4 -36
- package/src/components/locale-selector/locale-selector.theme.ts +3 -11
- package/src/components/logo/RLALogo.astro +1 -2
- package/src/components/modal/RLAModal.astro +1 -1
- package/src/components/modal/RLSModal.tsx +2 -2
- package/src/components/navigation-menu/RLANavigationMenu.astro +17 -21
- package/src/components/navigation-menu/navigation-menu.theme.ts +3 -7
- package/src/components/pagination/RLAPagination.astro +2 -2
- package/src/components/pagination/pagination.theme.ts +1 -1
- package/src/components/popover/RLAPopover.astro +39 -0
- package/src/components/progress/RLAProgress.astro +2 -0
- package/src/components/progress/progress.theme.ts +0 -4
- package/src/components/scroll-area/scroll-area.theme.ts +1 -1
- package/src/components/scroll-to-top/RLAScrollToTop.astro +6 -5
- package/src/components/scroll-to-top/RLVScrollToTop.vue +7 -7
- package/src/components/scroll-to-top/scroll-to-top.theme.ts +3 -7
- package/src/components/search/RLASearch.astro +3 -3
- package/src/components/select/RLASelect.astro +96 -528
- package/src/components/select/RLSSelect.tsx +68 -14
- package/src/components/select/RLVSelect.vue +57 -0
- package/src/components/select/select.theme.ts +11 -96
- package/src/components/shortcuts/RLAShortcuts.astro +9 -9
- package/src/components/sidebar/RLASidebar.astro +3 -3
- package/src/components/slideover/RLASlideover.astro +1 -1
- package/src/components/slider/RLASlider.astro +2 -1
- package/src/components/slider/slider.theme.ts +1 -5
- package/src/components/spinner/RLASpinner.astro +2 -1
- package/src/components/spinner/spinner.theme.ts +1 -5
- package/src/components/splitter/splitter.theme.ts +1 -1
- package/src/components/stepper/RLAStepper.astro +8 -8
- package/src/components/stepper/stepper.theme.ts +6 -7
- package/src/components/steps/RLASteps.astro +3 -3
- package/src/components/switch/RLASwitch.astro +2 -1
- package/src/components/switch/switch.theme.ts +0 -4
- package/src/components/table/table.theme.ts +1 -1
- package/src/components/table-of-contents/RLATableOfContents.astro +5 -5
- package/src/components/theme-selector/RLAThemeSelector.astro +16 -14
- package/src/components/theme-selector/theme-selector.theme.ts +2 -2
- package/src/components/toast/RLAToast.astro +6 -6
- package/src/components/toast/store.ts +1 -1
- package/src/components/tooltip/tooltip.theme.ts +1 -1
- package/src/icons.raw.ts +67 -0
- package/src/icons.ts +31 -64
- package/src/index.ts +19 -18
- package/src/preset.ts +110 -108
- package/src/resolver.ts +127 -15
- package/src/shortcuts.ts +103 -84
- package/src/stores.ts +22 -0
- package/src/types.ts +27 -9
- package/src/virtual.d.ts +4 -0
- package/src/components/editor/PageEditor.vue +0 -459
- package/src/components/editor/PageMention.vue +0 -87
- package/src/components/editor/PagePropertiesEditor.vue +0 -358
- package/src/components/editor/PagePropertiesRenderer.vue +0 -285
- package/src/components/editor/PageRenderer.vue +0 -155
- package/src/components/editor/PageSurround.vue +0 -130
- package/src/components/editor/PageVersionSelector.vue +0 -371
- package/src/components/editor/blocks/AddBlockModal.vue +0 -196
- package/src/components/editor/blocks/Block.vue +0 -142
- package/src/components/editor/blocks/BlockEditRenderer.vue +0 -26
- package/src/components/editor/blocks/BlockEditor.vue +0 -141
- package/src/components/editor/blocks/BlockViewRenderer.vue +0 -71
- package/src/components/editor/blocks/TextRenderer.vue +0 -31
- package/src/components/editor/blocks/editor/CalloutBlockEditor.vue +0 -53
- package/src/components/editor/blocks/editor/ImageBlockEditor.vue +0 -55
- package/src/components/editor/blocks/editor/ParagraphBlockEditor.vue +0 -31
- package/src/components/editor/blocks/editor/SectionBlockEditor.vue +0 -65
- package/src/components/editor/blocks/nodes/LinkNode.vue +0 -28
- package/src/components/editor/blocks/nodes/TextNode.vue +0 -22
- package/src/components/editor/blocks/renderer/CalloutBlockRenderer.vue +0 -65
- package/src/components/editor/blocks/renderer/ImageBlockRenderer.vue +0 -20
- package/src/components/editor/blocks/renderer/ParagraphBlockRenderer.vue +0 -21
- package/src/components/editor/blocks/renderer/SectionBlockRenderer.vue +0 -55
- package/src/components/editor/composables/injectionKeys.ts +0 -3
- package/src/components/editor/composables/useBlockEditor.ts +0 -261
- package/src/components/editor/composables/usePageEditor.ts +0 -97
- package/src/components/editor/modals/CreatePageModal.vue +0 -160
- package/src/components/editor/modals/DeletePageModal.vue +0 -116
- package/src/components/editor/modals/PageTreeModal.vue +0 -275
- package/src/components/editor/types/blocks.ts +0 -100
- package/src/components/editor/types/pages.ts +0 -89
- package/src/components/editor/types/schemas.ts +0 -36
- package/src/components/editor/utils/blocks.ts +0 -42
- package/src/components/editor/utils/editorHelpers.ts +0 -24
- package/src/components/editor/utils/page.ts +0 -219
- package/src/components/editor/utils/richTextHelpers.ts +0 -35
- package/src/components/editor/views/PageEditView.vue +0 -195
- package/src/components/editor/views/PageLiveView.vue +0 -90
- package/src/components/editor/views/PageReviewView.vue +0 -308
- package/src/components/icon/resolver.ts +0 -23
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { computed, type Ref, shallowRef, watch } from "vue"
|
|
2
|
-
import { type Page } from "../types/pages"
|
|
3
|
-
|
|
4
|
-
export function usePageEditor(page: Ref<Page>, maxHistorySize: number = 100) {
|
|
5
|
-
const history = shallowRef<string[]>([])
|
|
6
|
-
const future = shallowRef<string[]>([])
|
|
7
|
-
let lastCapturedState = JSON.stringify(page.value)
|
|
8
|
-
let isPaused = false
|
|
9
|
-
|
|
10
|
-
const canUndo = computed(() => history.value.length > 0)
|
|
11
|
-
const canRedo = computed(() => future.value.length > 0)
|
|
12
|
-
|
|
13
|
-
const captureSnapshot = () => {
|
|
14
|
-
const currentSnapshot = JSON.stringify(page.value)
|
|
15
|
-
if (currentSnapshot === lastCapturedState) return
|
|
16
|
-
|
|
17
|
-
future.value = []
|
|
18
|
-
const newHistory = [...history.value, lastCapturedState]
|
|
19
|
-
if (newHistory.length > maxHistorySize) newHistory.shift()
|
|
20
|
-
history.value = newHistory
|
|
21
|
-
|
|
22
|
-
lastCapturedState = currentSnapshot
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const undo = () => {
|
|
26
|
-
if (history.value.length === 0) return
|
|
27
|
-
|
|
28
|
-
const currentState = JSON.stringify(page.value)
|
|
29
|
-
future.value = [currentState, ...future.value]
|
|
30
|
-
|
|
31
|
-
const previousStateString = history.value[history.value.length - 1]!
|
|
32
|
-
lastCapturedState = previousStateString
|
|
33
|
-
|
|
34
|
-
page.value = JSON.parse(previousStateString)
|
|
35
|
-
|
|
36
|
-
const newHistory = [...history.value]
|
|
37
|
-
newHistory.pop()
|
|
38
|
-
history.value = newHistory
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const redo = () => {
|
|
42
|
-
if (future.value.length === 0) return
|
|
43
|
-
|
|
44
|
-
const currentState = JSON.stringify(page.value)
|
|
45
|
-
history.value = [...history.value, currentState]
|
|
46
|
-
lastCapturedState = currentState
|
|
47
|
-
|
|
48
|
-
const nextStateString = future.value[0]!
|
|
49
|
-
page.value = JSON.parse(nextStateString)
|
|
50
|
-
lastCapturedState = nextStateString
|
|
51
|
-
|
|
52
|
-
const newFuture = [...future.value]
|
|
53
|
-
newFuture.shift()
|
|
54
|
-
future.value = newFuture
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const save = () => {
|
|
58
|
-
return JSON.parse(JSON.stringify(page.value))
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const pauseHistory = () => {
|
|
62
|
-
isPaused = true
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const resumeHistory = () => {
|
|
66
|
-
isPaused = false
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const resetHistory = () => {
|
|
70
|
-
history.value = []
|
|
71
|
-
future.value = []
|
|
72
|
-
lastCapturedState = JSON.stringify(page.value)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
watch(
|
|
76
|
-
[() => page.value.properties, () => page.value.title, () => page.value.blocks],
|
|
77
|
-
() => {
|
|
78
|
-
if (isPaused) {
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
captureSnapshot()
|
|
82
|
-
},
|
|
83
|
-
{ deep: true }
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
undo,
|
|
88
|
-
redo,
|
|
89
|
-
canUndo,
|
|
90
|
-
canRedo,
|
|
91
|
-
save,
|
|
92
|
-
captureSnapshot,
|
|
93
|
-
resetHistory,
|
|
94
|
-
pauseHistory,
|
|
95
|
-
resumeHistory
|
|
96
|
-
}
|
|
97
|
-
}
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { ref, computed, watch } from "vue"
|
|
3
|
-
import type { PageDefinition, PageType, Page } from "../types/pages"
|
|
4
|
-
import { scv, cx } from "css-variants"
|
|
5
|
-
import { useI18n } from "../utils/editorHelpers"
|
|
6
|
-
import RLVButton from "../../button/RLVButton.vue"
|
|
7
|
-
import RLVInput from "../../input/RLVInput.vue"
|
|
8
|
-
import RLVFormField from "../../form-field/RLVFormField.vue"
|
|
9
|
-
|
|
10
|
-
export interface CreatePageModalProps {
|
|
11
|
-
definitions: Record<string, PageDefinition>
|
|
12
|
-
loading?: boolean
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const { loading, definitions } = defineProps<CreatePageModalProps>()
|
|
16
|
-
|
|
17
|
-
export interface CreatePageModalEmits {
|
|
18
|
-
close: []
|
|
19
|
-
confirm: [page: Partial<Page>]
|
|
20
|
-
}
|
|
21
|
-
const emit = defineEmits<CreatePageModalEmits>()
|
|
22
|
-
|
|
23
|
-
const createPageModalStyles = scv({
|
|
24
|
-
slots: ["header", "headerTitle", "closeButton", "body", "field", "footerClass"],
|
|
25
|
-
base: {
|
|
26
|
-
header: "flex items-center justify-between px-4 py-3 border-b border-default",
|
|
27
|
-
headerTitle: "text-base font-semibold leading-6",
|
|
28
|
-
closeButton: "-my-1",
|
|
29
|
-
body: "space-y-4 p-4",
|
|
30
|
-
field: "w-full",
|
|
31
|
-
footerClass: "flex justify-end gap-2 p-4 border-t border-default"
|
|
32
|
-
}
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
const styles = computed(() => createPageModalStyles())
|
|
36
|
-
|
|
37
|
-
const open = defineModel<boolean>("open", { default: false })
|
|
38
|
-
const { t } = useI18n()
|
|
39
|
-
const selectedType = ref<PageType | "">("")
|
|
40
|
-
const title = ref("")
|
|
41
|
-
const slug = ref("")
|
|
42
|
-
|
|
43
|
-
const typeOptions = computed(() => {
|
|
44
|
-
if (!definitions) return []
|
|
45
|
-
return Object.entries(definitions).map(([key, def]) => ({
|
|
46
|
-
label: t(def.typeLabelKey) === def.typeLabelKey ? key : t(def.typeLabelKey),
|
|
47
|
-
value: key
|
|
48
|
-
}))
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
defineOptions({ name: "CreatePageModal" })
|
|
52
|
-
|
|
53
|
-
watch(title, (newTitle) => {
|
|
54
|
-
slug.value = newTitle
|
|
55
|
-
.toLowerCase()
|
|
56
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
57
|
-
.replace(/(^-|-$)/g, "")
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
watch(open, (isOpen) => {
|
|
61
|
-
if (!isOpen) {
|
|
62
|
-
emit("close")
|
|
63
|
-
}
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
const handleConfirm = () => {
|
|
67
|
-
if (!selectedType.value || !definitions) return
|
|
68
|
-
const definition = definitions[selectedType.value]
|
|
69
|
-
if (!definition) {
|
|
70
|
-
console.error(`Definition for type "${selectedType.value}" not found.`)
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const properties: Record<string, any> = {}
|
|
75
|
-
Object.entries(definition.properties).forEach(([groupKey, group]) => {
|
|
76
|
-
properties[groupKey] = { ...group, fields: {} }
|
|
77
|
-
Object.entries(group.fields).forEach(([fieldKey, field]) => {
|
|
78
|
-
properties[groupKey].fields[fieldKey] = { ...field }
|
|
79
|
-
})
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
const newPage: Partial<Page> = {
|
|
83
|
-
type: selectedType.value as any,
|
|
84
|
-
title: { en: title.value },
|
|
85
|
-
slug: slug.value,
|
|
86
|
-
properties: properties as any,
|
|
87
|
-
blocks: definition.initialBlocks ? definition.initialBlocks() : [],
|
|
88
|
-
createdAt: new Date(),
|
|
89
|
-
updatedAt: new Date()
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
emit("confirm", newPage)
|
|
93
|
-
}
|
|
94
|
-
</script>
|
|
95
|
-
|
|
96
|
-
<template>
|
|
97
|
-
<div
|
|
98
|
-
v-if="open"
|
|
99
|
-
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
|
100
|
-
@click.self="open = false"
|
|
101
|
-
>
|
|
102
|
-
<div
|
|
103
|
-
class="bg-white dark:bg-neutral-900 border border-default rounded-2xl w-full max-w-lg shadow-xl overflow-hidden flex flex-col"
|
|
104
|
-
>
|
|
105
|
-
<div :class="styles.header">
|
|
106
|
-
<h3 :class="styles.headerTitle">
|
|
107
|
-
{{ t("page_editor.create_page", "Create New Page") }}
|
|
108
|
-
</h3>
|
|
109
|
-
<RLVButton
|
|
110
|
-
color="neutral"
|
|
111
|
-
variant="ghost"
|
|
112
|
-
leading-leading-icon="i-lucide-x"
|
|
113
|
-
class="!p-1.5"
|
|
114
|
-
:class="styles.closeButton"
|
|
115
|
-
@click="open = false"
|
|
116
|
-
/>
|
|
117
|
-
</div>
|
|
118
|
-
|
|
119
|
-
<div :class="styles.body">
|
|
120
|
-
<RLVFormField :label="t('page_properties.page_template', 'Page Template')" required>
|
|
121
|
-
<select
|
|
122
|
-
v-model="selectedType"
|
|
123
|
-
class="w-full bg-transparent border border-default rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary text-default bg-white dark:bg-neutral-900"
|
|
124
|
-
>
|
|
125
|
-
<option value="" disabled>
|
|
126
|
-
{{ t("editor.template_placeholder", "Select a template...") }}
|
|
127
|
-
</option>
|
|
128
|
-
<option v-for="opt in typeOptions" :key="opt.value" :value="opt.value">
|
|
129
|
-
{{ opt.label }}
|
|
130
|
-
</option>
|
|
131
|
-
</select>
|
|
132
|
-
</RLVFormField>
|
|
133
|
-
|
|
134
|
-
<RLVFormField :label="t('page_properties.title', 'Title')" required>
|
|
135
|
-
<RLVInput v-model="title" placeholder="e.g. My Awesome Article" class="w-full" />
|
|
136
|
-
</RLVFormField>
|
|
137
|
-
|
|
138
|
-
<RLVFormField :label="t('page_properties.slug', 'Slug')" required>
|
|
139
|
-
<RLVInput v-model="slug" placeholder="my-awesome-article" class="w-full" />
|
|
140
|
-
</RLVFormField>
|
|
141
|
-
</div>
|
|
142
|
-
|
|
143
|
-
<div :class="styles.footerClass">
|
|
144
|
-
<RLVButton
|
|
145
|
-
color="neutral"
|
|
146
|
-
variant="ghost"
|
|
147
|
-
:label="t('common.cancel', 'Cancel')"
|
|
148
|
-
@click="open = false"
|
|
149
|
-
/>
|
|
150
|
-
<RLVButton
|
|
151
|
-
color="primary"
|
|
152
|
-
:label="t('page_properties.create_and_edit', 'Create & Edit')"
|
|
153
|
-
:loading="loading"
|
|
154
|
-
:disabled="!selectedType || !title"
|
|
155
|
-
@click="handleConfirm"
|
|
156
|
-
/>
|
|
157
|
-
</div>
|
|
158
|
-
</div>
|
|
159
|
-
</div>
|
|
160
|
-
</template>
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { ref, watch, computed } from "vue"
|
|
3
|
-
import { scv, cx } from "css-variants"
|
|
4
|
-
import { useI18n } from "../utils/editorHelpers"
|
|
5
|
-
import RLVButton from "../../button/RLVButton.vue"
|
|
6
|
-
import RLVInput from "../../input/RLVInput.vue"
|
|
7
|
-
import RLVFormField from "../../form-field/RLVFormField.vue"
|
|
8
|
-
|
|
9
|
-
export interface DeletePageModalProps {
|
|
10
|
-
loading?: boolean
|
|
11
|
-
pageTitle: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const { loading, pageTitle } = defineProps<DeletePageModalProps>()
|
|
15
|
-
|
|
16
|
-
export interface DeletePageModalEmits {
|
|
17
|
-
close: []
|
|
18
|
-
confirm: []
|
|
19
|
-
}
|
|
20
|
-
const emits = defineEmits<DeletePageModalEmits>()
|
|
21
|
-
|
|
22
|
-
const deletePageModalStyles = scv({
|
|
23
|
-
slots: ["header", "headerTitle", "closeButton", "bodyClass", "footerClass"],
|
|
24
|
-
base: {
|
|
25
|
-
header: "flex items-center justify-between px-4 py-3 border-b border-default",
|
|
26
|
-
headerTitle: "text-base font-semibold leading-6 text-red-600 dark:text-red-500",
|
|
27
|
-
closeButton: "-my-1",
|
|
28
|
-
bodyClass: "text-sm text-neutral-600 dark:text-neutral-400 p-4 space-y-4",
|
|
29
|
-
footerClass: "flex justify-end gap-2 p-4 border-t border-default"
|
|
30
|
-
}
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
const styles = computed(() => deletePageModalStyles())
|
|
34
|
-
|
|
35
|
-
const open = defineModel<boolean>("open", { default: false })
|
|
36
|
-
const { t } = useI18n()
|
|
37
|
-
const confirmationInput = ref("")
|
|
38
|
-
const CONFIRMATION_TEXT = "DELETE"
|
|
39
|
-
|
|
40
|
-
defineOptions({ name: "DeletePageModal" })
|
|
41
|
-
|
|
42
|
-
watch(open, (val) => {
|
|
43
|
-
if (!val) {
|
|
44
|
-
confirmationInput.value = ""
|
|
45
|
-
emits("close")
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
const handleConfirm = () => {
|
|
50
|
-
if (confirmationInput.value === CONFIRMATION_TEXT) {
|
|
51
|
-
emits("confirm")
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
</script>
|
|
55
|
-
|
|
56
|
-
<template>
|
|
57
|
-
<div
|
|
58
|
-
v-if="open"
|
|
59
|
-
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
|
60
|
-
@click.self="open = false"
|
|
61
|
-
>
|
|
62
|
-
<div
|
|
63
|
-
class="bg-white dark:bg-neutral-900 border border-default rounded-2xl w-full max-w-lg shadow-xl overflow-hidden flex flex-col"
|
|
64
|
-
>
|
|
65
|
-
<div :class="styles.header">
|
|
66
|
-
<h3 :class="styles.headerTitle">
|
|
67
|
-
{{ t("editor.delete_page_title", "Delete Page") }}
|
|
68
|
-
</h3>
|
|
69
|
-
<RLVButton
|
|
70
|
-
color="neutral"
|
|
71
|
-
variant="ghost"
|
|
72
|
-
leading-leading-icon="i-lucide-x"
|
|
73
|
-
class="!p-1.5"
|
|
74
|
-
:class="styles.closeButton"
|
|
75
|
-
@click="open = false"
|
|
76
|
-
/>
|
|
77
|
-
</div>
|
|
78
|
-
|
|
79
|
-
<div :class="styles.bodyClass">
|
|
80
|
-
<p>
|
|
81
|
-
Are you sure you want to delete <strong>{{ pageTitle }}</strong
|
|
82
|
-
>? This action is permanent and cannot be undone.
|
|
83
|
-
</p>
|
|
84
|
-
<RLVFormField
|
|
85
|
-
:label="t('editor.delete_confirm_label', `Please type ${CONFIRMATION_TEXT} to confirm`)"
|
|
86
|
-
required
|
|
87
|
-
>
|
|
88
|
-
<RLVInput
|
|
89
|
-
v-model="confirmationInput"
|
|
90
|
-
:placeholder="CONFIRMATION_TEXT"
|
|
91
|
-
color="danger"
|
|
92
|
-
autocomplete="off"
|
|
93
|
-
@keyup.enter="handleConfirm"
|
|
94
|
-
/>
|
|
95
|
-
</RLVFormField>
|
|
96
|
-
</div>
|
|
97
|
-
|
|
98
|
-
<div :class="styles.footerClass">
|
|
99
|
-
<RLVButton
|
|
100
|
-
color="neutral"
|
|
101
|
-
variant="ghost"
|
|
102
|
-
:label="t('common.cancel', 'Cancel')"
|
|
103
|
-
@click="open = false"
|
|
104
|
-
/>
|
|
105
|
-
<RLVButton
|
|
106
|
-
color="danger"
|
|
107
|
-
leading-leading-icon="i-lucide-trash-2"
|
|
108
|
-
:label="t('editor.delete_button', 'Delete Permanently')"
|
|
109
|
-
:loading="loading"
|
|
110
|
-
:disabled="confirmationInput !== CONFIRMATION_TEXT"
|
|
111
|
-
@click="handleConfirm"
|
|
112
|
-
/>
|
|
113
|
-
</div>
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
</template>
|
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed, ref } from "vue"
|
|
3
|
-
import { scv, cx } from "css-variants"
|
|
4
|
-
import { useI18n } from "../utils/editorHelpers"
|
|
5
|
-
import { getLocalizedContent } from "../utils/page"
|
|
6
|
-
import type { Page } from "../types/pages"
|
|
7
|
-
import RLVButton from "../../button/RLVButton.vue"
|
|
8
|
-
|
|
9
|
-
export interface TreeItem {
|
|
10
|
-
label: string
|
|
11
|
-
icon?: string
|
|
12
|
-
slug?: string
|
|
13
|
-
path: string
|
|
14
|
-
children?: TreeItem[]
|
|
15
|
-
defaultExpanded?: boolean
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface PageTreeModalProps {
|
|
19
|
-
loading?: boolean
|
|
20
|
-
pages?: Pick<Page, "title" | "slug">[]
|
|
21
|
-
basePath?: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const { loading, pages = [], basePath } = defineProps<PageTreeModalProps>()
|
|
25
|
-
|
|
26
|
-
export interface PageTreeModalEmits {
|
|
27
|
-
close: []
|
|
28
|
-
navigate: [slug: string]
|
|
29
|
-
}
|
|
30
|
-
const emit = defineEmits<PageTreeModalEmits>()
|
|
31
|
-
|
|
32
|
-
const pageTreeModalStyles = scv({
|
|
33
|
-
slots: [
|
|
34
|
-
"headerClass",
|
|
35
|
-
"headerTitleClass",
|
|
36
|
-
"closeButtonClass",
|
|
37
|
-
"bodyClass",
|
|
38
|
-
"footerClass",
|
|
39
|
-
"loadingWrapper",
|
|
40
|
-
"emptyWrapper",
|
|
41
|
-
"itemWrapper",
|
|
42
|
-
"itemLabel",
|
|
43
|
-
"arrowIcon"
|
|
44
|
-
],
|
|
45
|
-
base: {
|
|
46
|
-
headerClass: "flex items-center justify-between px-4 py-3 border-b border-default",
|
|
47
|
-
headerTitleClass: "text-base font-semibold leading-6",
|
|
48
|
-
closeButtonClass: "-my-1",
|
|
49
|
-
bodyClass: "p-4 min-h-[300px] max-h-[60vh] overflow-y-auto space-y-2",
|
|
50
|
-
footerClass: "flex justify-end gap-2 p-4 border-t border-default",
|
|
51
|
-
loadingWrapper: "p-4 flex justify-center items-center py-12",
|
|
52
|
-
emptyWrapper: "p-4 text-center text-neutral-500 py-12",
|
|
53
|
-
itemWrapper:
|
|
54
|
-
"flex items-center gap-2 py-1.5 w-full hover:bg-neutral-50 dark:hover:bg-neutral-800/50 px-2 rounded-md transition cursor-pointer text-left",
|
|
55
|
-
itemLabel: "text-sm",
|
|
56
|
-
arrowIcon: "w-4 h-4 text-neutral-400 shrink-0"
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
const styles = computed(() => pageTreeModalStyles())
|
|
61
|
-
|
|
62
|
-
const open = defineModel<boolean>("open", { default: false })
|
|
63
|
-
const { t, locale } = useI18n()
|
|
64
|
-
const expandedNodes = ref<Record<string, boolean>>({})
|
|
65
|
-
|
|
66
|
-
const treeItems = computed<TreeItem[]>(() => {
|
|
67
|
-
if (!pages || pages.length === 0) return []
|
|
68
|
-
const nodeMap = new Map<string, TreeItem>()
|
|
69
|
-
const rootNodes: TreeItem[] = []
|
|
70
|
-
const cleanBasePath = basePath ? basePath.replace(/^\/|\/$/g, "") : ""
|
|
71
|
-
|
|
72
|
-
const getNode = (
|
|
73
|
-
path: string,
|
|
74
|
-
partLabel: string,
|
|
75
|
-
pageObj?: Pick<Page, "title" | "slug">
|
|
76
|
-
): TreeItem => {
|
|
77
|
-
if (!nodeMap.has(path)) {
|
|
78
|
-
const label = pageObj
|
|
79
|
-
? getLocalizedContent(pageObj.title, locale.value) || partLabel
|
|
80
|
-
: partLabel.charAt(0).toUpperCase() + partLabel.slice(1)
|
|
81
|
-
const newNode: TreeItem = {
|
|
82
|
-
label,
|
|
83
|
-
path,
|
|
84
|
-
slug: pageObj ? pageObj.slug : undefined,
|
|
85
|
-
icon: pageObj ? "i-lucide-file" : "i-lucide-folder",
|
|
86
|
-
children: []
|
|
87
|
-
}
|
|
88
|
-
nodeMap.set(path, newNode)
|
|
89
|
-
} else if (pageObj) {
|
|
90
|
-
const node = nodeMap.get(path)!
|
|
91
|
-
node.label = getLocalizedContent(pageObj.title, locale.value) || node.label
|
|
92
|
-
node.slug = pageObj.slug
|
|
93
|
-
node.icon = "i-lucide-file-text"
|
|
94
|
-
}
|
|
95
|
-
return nodeMap.get(path)!
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
pages.forEach((page) => {
|
|
99
|
-
const cleanSlug = page.slug.replace(/^\/|\/$/g, "")
|
|
100
|
-
let relativePath = cleanSlug
|
|
101
|
-
if (cleanBasePath && cleanSlug.startsWith(cleanBasePath)) {
|
|
102
|
-
relativePath = cleanSlug.slice(cleanBasePath.length).replace(/^\//, "")
|
|
103
|
-
} else if (cleanBasePath) {
|
|
104
|
-
if (cleanSlug === cleanBasePath) {
|
|
105
|
-
relativePath = ""
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
const parts = relativePath.split("/").filter(Boolean)
|
|
109
|
-
if (parts.length === 0) {
|
|
110
|
-
const node = getNode("__root__", "Overview", page)
|
|
111
|
-
if (!rootNodes.includes(node)) {
|
|
112
|
-
rootNodes.push(node)
|
|
113
|
-
}
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
let currentPath = ""
|
|
117
|
-
let parent: TreeItem | null = null
|
|
118
|
-
parts.forEach((part, index) => {
|
|
119
|
-
currentPath = currentPath ? `${currentPath}/${part}` : part
|
|
120
|
-
const isLast = index === parts.length - 1
|
|
121
|
-
const pageObj = isLast ? page : undefined
|
|
122
|
-
const node = getNode(currentPath, part, pageObj)
|
|
123
|
-
if (index === 0) {
|
|
124
|
-
if (!rootNodes.includes(node)) {
|
|
125
|
-
rootNodes.push(node)
|
|
126
|
-
}
|
|
127
|
-
} else {
|
|
128
|
-
if (parent && !parent.children?.some((c) => c.path === node.path)) {
|
|
129
|
-
if (!parent.children) {
|
|
130
|
-
parent.children = []
|
|
131
|
-
}
|
|
132
|
-
parent.children.push(node)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
parent = node
|
|
136
|
-
})
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
const processNodes = (nodes: TreeItem[]) => {
|
|
140
|
-
nodes.sort((a, b) => a.label.localeCompare(b.label))
|
|
141
|
-
for (const node of nodes) {
|
|
142
|
-
if (node.slug && node.children && node.children.length > 0) {
|
|
143
|
-
const indexNode: TreeItem = {
|
|
144
|
-
label: `Overview: ${node.label}`,
|
|
145
|
-
slug: node.slug,
|
|
146
|
-
path: `${node.path}:index`,
|
|
147
|
-
icon: "i-lucide-file-text",
|
|
148
|
-
children: []
|
|
149
|
-
}
|
|
150
|
-
if (!node.children.some((c) => c.path === indexNode.path)) {
|
|
151
|
-
node.children.unshift(indexNode)
|
|
152
|
-
}
|
|
153
|
-
node.slug = undefined
|
|
154
|
-
node.icon = "i-lucide-folder"
|
|
155
|
-
}
|
|
156
|
-
if (node.children && node.children.length > 0) {
|
|
157
|
-
processNodes(node.children)
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
processNodes(rootNodes)
|
|
163
|
-
return rootNodes
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
defineOptions({ name: "PageTreeModal" })
|
|
167
|
-
|
|
168
|
-
const handleSelect = (node: TreeItem) => {
|
|
169
|
-
if (node.slug) {
|
|
170
|
-
emit("navigate", node.slug)
|
|
171
|
-
open.value = false
|
|
172
|
-
} else {
|
|
173
|
-
expandedNodes.value[node.path] = !expandedNodes.value[node.path]
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const isExpanded = (node: TreeItem) => {
|
|
178
|
-
return expandedNodes.value[node.path] ?? false
|
|
179
|
-
}
|
|
180
|
-
</script>
|
|
181
|
-
|
|
182
|
-
<template>
|
|
183
|
-
<div
|
|
184
|
-
v-if="open"
|
|
185
|
-
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
|
186
|
-
@click.self="open = false"
|
|
187
|
-
>
|
|
188
|
-
<div
|
|
189
|
-
class="bg-white dark:bg-neutral-900 border border-default rounded-2xl w-full max-w-lg shadow-xl overflow-hidden flex flex-col"
|
|
190
|
-
>
|
|
191
|
-
<div :class="styles.headerClass">
|
|
192
|
-
<h3 :class="styles.headerTitleClass">
|
|
193
|
-
{{ t("editor.page_hierarchy", "Page Hierarchy") }}
|
|
194
|
-
</h3>
|
|
195
|
-
<RLVButton
|
|
196
|
-
color="neutral"
|
|
197
|
-
variant="ghost"
|
|
198
|
-
leading-leading-icon="i-lucide-x"
|
|
199
|
-
class="!p-1.5"
|
|
200
|
-
:class="styles.closeButtonClass"
|
|
201
|
-
@click="open = false"
|
|
202
|
-
/>
|
|
203
|
-
</div>
|
|
204
|
-
|
|
205
|
-
<div :class="styles.bodyClass">
|
|
206
|
-
<div v-if="loading" :class="styles.loadingWrapper">
|
|
207
|
-
<span class="i-lucide-loader-2 size-6 animate-spin text-neutral-400" />
|
|
208
|
-
</div>
|
|
209
|
-
|
|
210
|
-
<div v-else-if="treeItems.length === 0" :class="styles.emptyWrapper">
|
|
211
|
-
{{ t("editor.no_pages_found", "No pages found.") }}
|
|
212
|
-
</div>
|
|
213
|
-
|
|
214
|
-
<div v-else class="space-y-1">
|
|
215
|
-
<div v-for="item in treeItems" :key="item.path" class="space-y-1">
|
|
216
|
-
<button :class="styles.itemWrapper" @click="handleSelect(item)">
|
|
217
|
-
<span
|
|
218
|
-
v-if="item.children && item.children.length > 0"
|
|
219
|
-
:class="[
|
|
220
|
-
isExpanded(item) ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right',
|
|
221
|
-
styles.arrowIcon
|
|
222
|
-
]"
|
|
223
|
-
/>
|
|
224
|
-
<span v-else class="w-4 shrink-0" />
|
|
225
|
-
<span v-if="item.icon" :class="[item.icon, 'size-4 shrink-0 opacity-60']" />
|
|
226
|
-
<span
|
|
227
|
-
:class="[
|
|
228
|
-
styles.itemLabel,
|
|
229
|
-
item.slug
|
|
230
|
-
? 'text-neutral-900 dark:text-neutral-100 font-medium'
|
|
231
|
-
: 'text-neutral-500'
|
|
232
|
-
]"
|
|
233
|
-
>
|
|
234
|
-
{{ item.label }}
|
|
235
|
-
</span>
|
|
236
|
-
</button>
|
|
237
|
-
|
|
238
|
-
<div
|
|
239
|
-
v-if="item.children && item.children.length > 0 && isExpanded(item)"
|
|
240
|
-
class="pl-4 border-l border-default ml-4 space-y-1"
|
|
241
|
-
>
|
|
242
|
-
<button
|
|
243
|
-
v-for="sub in item.children"
|
|
244
|
-
:key="sub.path"
|
|
245
|
-
:class="styles.itemWrapper"
|
|
246
|
-
@click="handleSelect(sub)"
|
|
247
|
-
>
|
|
248
|
-
<span v-if="sub.icon" :class="[sub.icon, 'size-4 shrink-0 opacity-60']" />
|
|
249
|
-
<span
|
|
250
|
-
:class="[
|
|
251
|
-
styles.itemLabel,
|
|
252
|
-
sub.slug
|
|
253
|
-
? 'text-neutral-900 dark:text-neutral-100 font-medium'
|
|
254
|
-
: 'text-neutral-500'
|
|
255
|
-
]"
|
|
256
|
-
>
|
|
257
|
-
{{ sub.label }}
|
|
258
|
-
</span>
|
|
259
|
-
</button>
|
|
260
|
-
</div>
|
|
261
|
-
</div>
|
|
262
|
-
</div>
|
|
263
|
-
</div>
|
|
264
|
-
|
|
265
|
-
<div :class="styles.footerClass">
|
|
266
|
-
<RLVButton
|
|
267
|
-
color="neutral"
|
|
268
|
-
variant="ghost"
|
|
269
|
-
:label="t('common.close', 'Close')"
|
|
270
|
-
@click="open = false"
|
|
271
|
-
/>
|
|
272
|
-
</div>
|
|
273
|
-
</div>
|
|
274
|
-
</div>
|
|
275
|
-
</template>
|