Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 7 additions & 15 deletions src/runtime/components/prose/Accordion.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface ProseAccordionSlots {
</script>

<script setup lang="ts">
import { computed, ref, onBeforeUpdate } from 'vue'
import { computed } from 'vue'
import { useAppConfig } from '#imports'
import { useComponentUI } from '../../composables/useComponentUI'
import { transformUI } from '../../utils'
Expand All @@ -36,18 +36,12 @@ const uiProp = useComponentUI('prose.accordion', props)
// eslint-disable-next-line vue/no-dupe-keys
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.prose?.accordion || {}) }))

const rerenderCount = ref(1)

const items = computed<{
index: number
label: string
icon: string
component: any
}[]>(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
rerenderCount.value
// Slot children are collected and transformed via getItems(), called from the template
// (render function). This ensures slots.default?.() is invoked during the render phase,
// avoiding: "[Vue warn]: Slot "default" invoked outside of the render function"
function getItems() {
return slots.default?.()?.flatMap(transformSlot).filter(Boolean) || []
})
}

function transformSlot(slot: any, index: number) {
if (typeof slot.type === 'symbol') {
Expand All @@ -62,12 +56,10 @@ function transformSlot(slot: any, index: number) {
component: slot
}
}

onBeforeUpdate(() => rerenderCount.value++)
</script>

<template>
<UAccordion :type="type" :items="items" :unmount-on-hide="false" :class="props.class" :ui="transformUI(ui(), uiProp)">
<UAccordion :type="type" :items="getItems()" :unmount-on-hide="false" :class="props.class" :ui="transformUI(ui(), uiProp)">
<template #content="{ item }">
<component :is="item.component" />
</template>
Expand Down
24 changes: 8 additions & 16 deletions src/runtime/components/prose/CodeGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface ProseCodeGroupSlots {
</script>

<script setup lang="ts">
import { computed, watch, onMounted, ref, onBeforeUpdate } from 'vue'
import { computed, watch, onMounted } from 'vue'
import { TabsRoot, TabsList, TabsIndicator, TabsTrigger, TabsContent } from 'reka-ui'
import { useState, useAppConfig } from '#imports'
import { useComponentUI } from '../../composables/useComponentUI'
Expand All @@ -45,18 +45,12 @@ const uiProp = useComponentUI('prose.codeGroup', props)
// eslint-disable-next-line vue/no-dupe-keys
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.prose?.codeGroup || {}) })())

const rerenderCount = ref(1)

const items = computed<{
index: number
label: string
icon: string
component: any
}[]>(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
rerenderCount.value
// Slot children are collected and transformed via getItems(), called from the template
// (render function). This ensures slots.default?.() is invoked during the render phase,
// avoiding: "[Vue warn]: Slot "default" invoked outside of the render function"
function getItems() {
return slots.default?.()?.flatMap(transformSlot).filter(Boolean) || []
})
}

function transformSlot(slot: any, index: number) {
if (typeof slot.type === 'symbol') {
Expand Down Expand Up @@ -89,23 +83,21 @@ onMounted(() => {
})
}
})

onBeforeUpdate(() => rerenderCount.value++)
</script>

<template>
<TabsRoot v-model="model" :default-value="defaultValue" :unmount-on-hide="false" :class="ui.root({ class: [uiProp?.root, props.class] })">
<TabsList :class="ui.list({ class: uiProp?.list })">
<TabsIndicator :class="ui.indicator({ class: uiProp?.indicator })" />

<TabsTrigger v-for="(item, index) of items" :key="index" :value="String(index)" :class="ui.trigger({ class: uiProp?.trigger })">
<TabsTrigger v-for="(item, index) of getItems()" :key="index" :value="String(index)" :class="ui.trigger({ class: uiProp?.trigger })">
<UCodeIcon :icon="item.icon" :filename="item.label" :class="ui.triggerIcon({ class: uiProp?.triggerIcon })" />

<span :class="ui.triggerLabel({ class: uiProp?.triggerLabel })">{{ item.label }}</span>
</TabsTrigger>
</TabsList>

<TabsContent v-for="(item, index) of items" :key="index" :value="String(index)" as-child>
<TabsContent v-for="(item, index) of getItems()" :key="index" :value="String(index)" as-child>
<component :is="item.component" hide-header tabindex="-1" />
</TabsContent>
</TabsRoot>
Expand Down
77 changes: 47 additions & 30 deletions src/runtime/components/prose/CodeTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export interface ProseCodeTreeSlots {
</script>

<script setup lang="ts">
import { computed, watch, onBeforeUpdate, ref } from 'vue'
import { computed, watch, ref } from 'vue'
import { TreeRoot, TreeItem } from 'reka-ui'
import { createReusableTemplate } from '@vueuse/core'
import { useAppConfig } from '#imports'
Expand Down Expand Up @@ -92,16 +92,48 @@ watch(() => props.modelValue, (value) => {
}
}
})
const rerenderCount = ref(1)
// Slot children are collected and transformed via getTreeItems(), called from the
// template (render function). This ensures slots.default?.() is invoked during the
// render phase, avoiding:
// "[Vue warn]: Slot "default" invoked outside of the render function"
// Mutable state is wrapped inside the IIFE closure to prevent Vue from exposing it
// to the template context, which would cause devalue serialization errors during SSR
// (VNodes are non-POJO objects).
const { getTreeItems, getFlatItems } = (() => {
let flatItems: TreeItem[] = []
let treeItems: TreeNode[] = []
let prevLabels = ''

function getTreeItems() {
const newFlatItems: TreeItem[] = props.items || slots.default?.()?.flatMap(transformSlot).filter(Boolean) || []
const newLabels = newFlatItems.map(i => i.label).join('\n')

if (newLabels !== prevLabels) {
// Re-expand all when flatItems actually change and expandAll is true
if (prevLabels && props.expandAll) {
expanded.value = getExpandedPaths(undefined, newFlatItems)
}

const flatItems = computed<TreeItem[]>(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
rerenderCount.value
return props.items || slots.default?.()?.flatMap(transformSlot).filter(Boolean) || []
})
flatItems = newFlatItems
treeItems = buildTree(newFlatItems)
prevLabels = newLabels

// eslint-disable-next-line vue/no-dupe-keys
const items = computed(() => buildTree(flatItems.value))
// Update lastSelectedItem when items change
const selectedItem = newFlatItems.find(item => model.value?.path === item.label)
if (selectedItem?.component) {
lastSelectedItem.value = selectedItem
}
}

return treeItems
}

function getFlatItems() {
return flatItems
}

return { getTreeItems, getFlatItems }
})()

function buildTree(items: { label: string }[]): TreeNode[] {
const map = new Map<string, TreeNode>()
Expand Down Expand Up @@ -147,10 +179,10 @@ function transformSlot(slot: any, index: number): TreeItem {
}
}

function getExpandedPaths(path?: string) {
function getExpandedPaths(path?: string, items?: TreeItem[]) {
if (props.expandAll) {
const allPaths = new Set<string>()
flatItems.value.forEach((item) => {
;(items || getFlatItems()).forEach((item) => {
const parts = item.label.split('/')
for (let i = 1; i < parts.length; i++) {
allPaths.add(parts.slice(0, i).join('/'))
Expand All @@ -169,27 +201,12 @@ function getExpandedPaths(path?: string) {

const expanded = ref(getExpandedPaths(model.value?.path))

// Re-expand all when flatItems actually change and expandAll is true
watch(flatItems, (newItems, oldItems) => {
if (!props.expandAll) return

// Compare labels to detect actual changes (not just re-renders from rerenderCount)
const newLabels = newItems.map(i => i.label).join('\n')
const oldLabels = oldItems?.map(i => i.label).join('\n') ?? ''

if (newLabels !== oldLabels) {
expanded.value = getExpandedPaths()
}
})

watch(model, (value) => {
const item = flatItems.value.find(item => value?.path === item.label)
const item = getFlatItems().find(item => value?.path === item.label)
if (item?.component) {
lastSelectedItem.value = item
}
}, { immediate: true })

onBeforeUpdate(() => rerenderCount.value++)
})
</script>

<!-- eslint-disable vue/no-template-shadow -->
Expand Down Expand Up @@ -250,10 +267,10 @@ onBeforeUpdate(() => rerenderCount.value++)
v-model="model"
v-model:expanded="expanded"
:class="ui.list({ class: uiProp?.list })"
:items="items"
:items="getTreeItems()"
:get-key="(item) => item.path"
>
<ReuseTreeTemplate :items="items" :level="1" />
<ReuseTreeTemplate :items="getTreeItems()" :level="1" />
</TreeRoot>

<div :class="ui.content({ class: uiProp?.content })">
Expand Down
22 changes: 7 additions & 15 deletions src/runtime/components/prose/Tabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export interface ProseTabsSlots {
</script>

<script setup lang="ts">
import { computed, watch, onMounted, ref, onBeforeUpdate } from 'vue'
import { computed, watch, onMounted } from 'vue'
import { useState, useAppConfig } from '#imports'
import { useComponentUI } from '../../composables/useComponentUI'
import { transformUI } from '../../utils'
Expand All @@ -50,18 +50,12 @@ const uiProp = useComponentUI('prose.tabs', props)
// eslint-disable-next-line vue/no-dupe-keys
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.prose?.tabs || {}) }))

const rerenderCount = ref(1)

const items = computed<{
index: number
label: string
icon: string
component: any
}[]>(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
rerenderCount.value
// Slot children are collected and transformed via getItems(), called from the template
// (render function). This ensures slots.default?.() is invoked during the render phase,
// avoiding: "[Vue warn]: Slot "default" invoked outside of the render function"
function getItems() {
return slots.default?.()?.flatMap(transformSlot).filter(Boolean) || []
})
}

function transformSlot(slot: any, index: number) {
if (typeof slot.type === 'symbol') {
Expand Down Expand Up @@ -105,16 +99,14 @@ async function onUpdateModelValue() {
}, 200)
}
}

onBeforeUpdate(() => rerenderCount.value++)
</script>

<template>
<UTabs
v-model="model"
color="primary"
variant="link"
:items="items"
:items="getItems()"
:class="props.class"
:unmount-on-hide="false"
:ui="transformUI(ui(), uiProp)"
Expand Down
Loading