@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.
Files changed (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +14 -12
  3. package/package.json +11 -14
  4. package/src/components/avatar/RLAAvatar.astro +4 -12
  5. package/src/components/badge/RLABadge.astro +2 -1
  6. package/src/components/badge/badge.theme.ts +0 -4
  7. package/src/components/banner/RLABanner.astro +1 -1
  8. package/src/components/banner/banner.theme.ts +1 -1
  9. package/src/components/breadcrumb/RLABreadcrumb.astro +2 -2
  10. package/src/components/button/RLAButton.astro +4 -1
  11. package/src/components/button/button.theme.ts +1 -5
  12. package/src/components/callout/RLACallout.astro +7 -5
  13. package/src/components/callout/callout.theme.ts +1 -5
  14. package/src/components/card/card.theme.ts +3 -3
  15. package/src/components/carousel/RLACarousel.astro +2 -2
  16. package/src/components/carousel/carousel.theme.ts +2 -2
  17. package/src/components/chart/RLAChart.astro +1 -1
  18. package/src/components/checkbox/RLACheckbox.astro +2 -1
  19. package/src/components/checkbox/checkbox.theme.ts +0 -4
  20. package/src/components/chip/RLAChip.astro +2 -1
  21. package/src/components/chip/chip.theme.ts +0 -4
  22. package/src/components/collapsible/RLACollapsible.astro +1 -1
  23. package/src/components/color-area/RLAColorArea.astro +1 -0
  24. package/src/components/color-area/color-area.theme.ts +1 -5
  25. package/src/components/color-field/RLAColorField.astro +1 -0
  26. package/src/components/color-field/color-field.theme.ts +1 -5
  27. package/src/components/color-slider/RLAColorSlider.astro +1 -0
  28. package/src/components/color-slider/color-slider.theme.ts +1 -5
  29. package/src/components/dashboard-group/RLADashboardGroup.astro +5 -7
  30. package/src/components/dashboard-group/dashboard-group.theme.ts +1 -1
  31. package/src/components/dashboard-group/dashboard-group.ts +1 -1
  32. package/src/components/dashboard-navbar/RLADashboardNavbar.astro +4 -4
  33. package/src/components/dashboard-navbar/dashboard-navbar.theme.ts +3 -3
  34. package/src/components/dashboard-navbar/dashboard-navbar.ts +1 -1
  35. package/src/components/dashboard-panel/RLADashboardPanel.astro +9 -8
  36. package/src/components/dashboard-panel/dashboard-panel.theme.ts +1 -1
  37. package/src/components/dashboard-sidebar/RLADashboardSidebar.astro +7 -4
  38. package/src/components/dashboard-sidebar/dashboard-sidebar.theme.ts +6 -5
  39. package/src/components/dashboard-sidebar-collapse/RLADashboardSidebarCollapse.astro +1 -0
  40. package/src/components/drawer/RLADrawer.astro +1 -1
  41. package/src/components/dropdown-menu/dropdown-menu.theme.ts +2 -2
  42. package/src/components/footer/RLAFooter.astro +2 -0
  43. package/src/components/footer/footer.theme.ts +0 -8
  44. package/src/components/gamepad/RLAGamepad.astro +1 -1
  45. package/src/components/head/UIHead.astro +1 -2
  46. package/src/components/header/header.theme.ts +1 -1
  47. package/src/components/icon/RLAIcon.astro +2 -5
  48. package/src/components/icon/RLSIcon.tsx +2 -2
  49. package/src/components/icon/RLVIcon.vue +18 -6
  50. package/src/components/icon/icon.theme.ts +1 -1
  51. package/src/components/image/image.theme.ts +1 -1
  52. package/src/components/input/RLAInput.astro +2 -1
  53. package/src/components/input/input.theme.ts +0 -4
  54. package/src/components/input-menu/RLAInputMenu.astro +2 -2
  55. package/src/components/input-menu/input-menu.theme.ts +1 -1
  56. package/src/components/input-rating/input-rating.theme.ts +2 -2
  57. package/src/components/kbd/RLAKbd.astro +2 -1
  58. package/src/components/kbd/kbd.theme.ts +0 -4
  59. package/src/components/keyboard/RLAKeyboard.astro +1 -1
  60. package/src/components/link/RLALink.astro +1 -0
  61. package/src/components/link/link.theme.ts +0 -4
  62. package/src/components/link-group/RLALinkGroup.astro +1 -1
  63. package/src/components/link-group/link-group.theme.ts +1 -10
  64. package/src/components/locale-selector/RLALocaleSelector.astro +4 -36
  65. package/src/components/locale-selector/locale-selector.theme.ts +3 -11
  66. package/src/components/logo/RLALogo.astro +1 -2
  67. package/src/components/modal/RLAModal.astro +1 -1
  68. package/src/components/modal/RLSModal.tsx +2 -2
  69. package/src/components/navigation-menu/RLANavigationMenu.astro +17 -21
  70. package/src/components/navigation-menu/navigation-menu.theme.ts +3 -7
  71. package/src/components/pagination/RLAPagination.astro +2 -2
  72. package/src/components/pagination/pagination.theme.ts +1 -1
  73. package/src/components/popover/RLAPopover.astro +39 -0
  74. package/src/components/progress/RLAProgress.astro +2 -0
  75. package/src/components/progress/progress.theme.ts +0 -4
  76. package/src/components/scroll-area/scroll-area.theme.ts +1 -1
  77. package/src/components/scroll-to-top/RLAScrollToTop.astro +6 -5
  78. package/src/components/scroll-to-top/RLVScrollToTop.vue +7 -7
  79. package/src/components/scroll-to-top/scroll-to-top.theme.ts +3 -7
  80. package/src/components/search/RLASearch.astro +3 -3
  81. package/src/components/select/RLASelect.astro +96 -528
  82. package/src/components/select/RLSSelect.tsx +68 -14
  83. package/src/components/select/RLVSelect.vue +57 -0
  84. package/src/components/select/select.theme.ts +11 -96
  85. package/src/components/shortcuts/RLAShortcuts.astro +9 -9
  86. package/src/components/sidebar/RLASidebar.astro +3 -3
  87. package/src/components/slideover/RLASlideover.astro +1 -1
  88. package/src/components/slider/RLASlider.astro +2 -1
  89. package/src/components/slider/slider.theme.ts +1 -5
  90. package/src/components/spinner/RLASpinner.astro +2 -1
  91. package/src/components/spinner/spinner.theme.ts +1 -5
  92. package/src/components/splitter/splitter.theme.ts +1 -1
  93. package/src/components/stepper/RLAStepper.astro +8 -8
  94. package/src/components/stepper/stepper.theme.ts +6 -7
  95. package/src/components/steps/RLASteps.astro +3 -3
  96. package/src/components/switch/RLASwitch.astro +2 -1
  97. package/src/components/switch/switch.theme.ts +0 -4
  98. package/src/components/table/table.theme.ts +1 -1
  99. package/src/components/table-of-contents/RLATableOfContents.astro +5 -5
  100. package/src/components/theme-selector/RLAThemeSelector.astro +16 -14
  101. package/src/components/theme-selector/theme-selector.theme.ts +2 -2
  102. package/src/components/toast/RLAToast.astro +6 -6
  103. package/src/components/toast/store.ts +1 -1
  104. package/src/components/tooltip/tooltip.theme.ts +1 -1
  105. package/src/icons.raw.ts +67 -0
  106. package/src/icons.ts +31 -64
  107. package/src/index.ts +19 -18
  108. package/src/preset.ts +110 -108
  109. package/src/resolver.ts +127 -15
  110. package/src/shortcuts.ts +103 -84
  111. package/src/stores.ts +22 -0
  112. package/src/types.ts +27 -9
  113. package/src/virtual.d.ts +4 -0
  114. package/src/components/editor/PageEditor.vue +0 -459
  115. package/src/components/editor/PageMention.vue +0 -87
  116. package/src/components/editor/PagePropertiesEditor.vue +0 -358
  117. package/src/components/editor/PagePropertiesRenderer.vue +0 -285
  118. package/src/components/editor/PageRenderer.vue +0 -155
  119. package/src/components/editor/PageSurround.vue +0 -130
  120. package/src/components/editor/PageVersionSelector.vue +0 -371
  121. package/src/components/editor/blocks/AddBlockModal.vue +0 -196
  122. package/src/components/editor/blocks/Block.vue +0 -142
  123. package/src/components/editor/blocks/BlockEditRenderer.vue +0 -26
  124. package/src/components/editor/blocks/BlockEditor.vue +0 -141
  125. package/src/components/editor/blocks/BlockViewRenderer.vue +0 -71
  126. package/src/components/editor/blocks/TextRenderer.vue +0 -31
  127. package/src/components/editor/blocks/editor/CalloutBlockEditor.vue +0 -53
  128. package/src/components/editor/blocks/editor/ImageBlockEditor.vue +0 -55
  129. package/src/components/editor/blocks/editor/ParagraphBlockEditor.vue +0 -31
  130. package/src/components/editor/blocks/editor/SectionBlockEditor.vue +0 -65
  131. package/src/components/editor/blocks/nodes/LinkNode.vue +0 -28
  132. package/src/components/editor/blocks/nodes/TextNode.vue +0 -22
  133. package/src/components/editor/blocks/renderer/CalloutBlockRenderer.vue +0 -65
  134. package/src/components/editor/blocks/renderer/ImageBlockRenderer.vue +0 -20
  135. package/src/components/editor/blocks/renderer/ParagraphBlockRenderer.vue +0 -21
  136. package/src/components/editor/blocks/renderer/SectionBlockRenderer.vue +0 -55
  137. package/src/components/editor/composables/injectionKeys.ts +0 -3
  138. package/src/components/editor/composables/useBlockEditor.ts +0 -261
  139. package/src/components/editor/composables/usePageEditor.ts +0 -97
  140. package/src/components/editor/modals/CreatePageModal.vue +0 -160
  141. package/src/components/editor/modals/DeletePageModal.vue +0 -116
  142. package/src/components/editor/modals/PageTreeModal.vue +0 -275
  143. package/src/components/editor/types/blocks.ts +0 -100
  144. package/src/components/editor/types/pages.ts +0 -89
  145. package/src/components/editor/types/schemas.ts +0 -36
  146. package/src/components/editor/utils/blocks.ts +0 -42
  147. package/src/components/editor/utils/editorHelpers.ts +0 -24
  148. package/src/components/editor/utils/page.ts +0 -219
  149. package/src/components/editor/utils/richTextHelpers.ts +0 -35
  150. package/src/components/editor/views/PageEditView.vue +0 -195
  151. package/src/components/editor/views/PageLiveView.vue +0 -90
  152. package/src/components/editor/views/PageReviewView.vue +0 -308
  153. 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>