feat: shared loading state + cleanup loading state management (#5835)
* feat: implement shared loading bar component and polished loading states across the app * feat: align loading states + ensureQueryData changes * fix: lint + bugs * fix: skeleton for manage servers page * fix: merge conflict fix
This commit is contained in:
97
packages/ui/src/stories/base/LoadingBar.stories.ts
Normal file
97
packages/ui/src/stories/base/LoadingBar.stories.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import LoadingBar from '../../components/base/LoadingBar.vue'
|
||||
import { createLoadingStateCore } from '../../composables/use-loading-state-core'
|
||||
import { provideLoadingState } from '../../providers/loading-state'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/LoadingBar',
|
||||
component: LoadingBar,
|
||||
} satisfies Meta<typeof LoadingBar>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Idle: Story = {
|
||||
render: () => ({
|
||||
components: { LoadingBar },
|
||||
setup() {
|
||||
provideLoadingState(createLoadingStateCore())
|
||||
return {}
|
||||
},
|
||||
template: `
|
||||
<div class="relative h-32 w-full">
|
||||
<LoadingBar />
|
||||
<p class="text-secondary">Loading bar is idle (no active tokens).</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const SinglePending: Story = {
|
||||
render: () => ({
|
||||
components: { LoadingBar },
|
||||
setup() {
|
||||
const core = createLoadingStateCore()
|
||||
provideLoadingState(core)
|
||||
onMounted(() => {
|
||||
core.begin()
|
||||
})
|
||||
return {}
|
||||
},
|
||||
template: `
|
||||
<div class="relative h-32 w-full">
|
||||
<LoadingBar />
|
||||
<p class="text-secondary">One token registered — bar fills to 100% over 1s.</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const StackedPending: Story = {
|
||||
render: () => ({
|
||||
components: { LoadingBar },
|
||||
setup() {
|
||||
const core = createLoadingStateCore()
|
||||
provideLoadingState(core)
|
||||
const tokens: symbol[] = []
|
||||
onMounted(() => {
|
||||
tokens.push(core.begin())
|
||||
tokens.push(core.begin())
|
||||
setTimeout(() => core.end(tokens[0]!), 1500)
|
||||
setTimeout(() => core.end(tokens[1]!), 3000)
|
||||
})
|
||||
return {}
|
||||
},
|
||||
template: `
|
||||
<div class="relative h-32 w-full">
|
||||
<LoadingBar />
|
||||
<p class="text-secondary">Two tokens. First releases at 1.5s, second at 3s — bar stays visible until both end.</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const ManualRefresh: Story = {
|
||||
render: () => ({
|
||||
components: { LoadingBar },
|
||||
setup() {
|
||||
const core = createLoadingStateCore()
|
||||
provideLoadingState(core)
|
||||
const last = ref<string>('idle')
|
||||
function trigger() {
|
||||
core.beginManual(800)
|
||||
last.value = `beginManual(800) at ${new Date().toLocaleTimeString()}`
|
||||
}
|
||||
return { trigger, last }
|
||||
},
|
||||
template: `
|
||||
<div class="relative h-32 w-full">
|
||||
<LoadingBar />
|
||||
<button class="rounded bg-button-bg px-3 py-2 text-contrast" @click="trigger">Manual refresh</button>
|
||||
<p class="text-secondary mt-2">{{ last }}</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
151
packages/ui/src/stories/base/ReadyTransition.stories.ts
Normal file
151
packages/ui/src/stories/base/ReadyTransition.stories.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import LoadingBar from '../../components/base/LoadingBar.vue'
|
||||
import ReadyTransition from '../../components/base/ReadyTransition.vue'
|
||||
import { createLoadingStateCore } from '../../composables/use-loading-state-core'
|
||||
import { provideLoadingState } from '../../providers/loading-state'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/ReadyTransition',
|
||||
component: ReadyTransition,
|
||||
} satisfies Meta<typeof ReadyTransition>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Idle: Story = {
|
||||
render: () => ({
|
||||
components: { ReadyTransition, LoadingBar },
|
||||
setup() {
|
||||
provideLoadingState(createLoadingStateCore())
|
||||
return { pending: ref(false) }
|
||||
},
|
||||
template: `
|
||||
<div class="relative">
|
||||
<LoadingBar />
|
||||
<ReadyTransition :pending="pending">
|
||||
<div class="rounded bg-bg-raised p-4 text-contrast">Slot content (already ready).</div>
|
||||
</ReadyTransition>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
/** Pending false from mount — no enter fade (cache-hit path). */
|
||||
export const CacheHit: Story = {
|
||||
render: () => ({
|
||||
components: { ReadyTransition, LoadingBar },
|
||||
setup() {
|
||||
provideLoadingState(createLoadingStateCore())
|
||||
return { pending: ref(false) }
|
||||
},
|
||||
template: `
|
||||
<div class="relative">
|
||||
<LoadingBar />
|
||||
<p class="text-secondary mb-4">pending stays false — content should appear with no fade-in.</p>
|
||||
<ReadyTransition :pending="pending">
|
||||
<div class="rounded bg-bg-raised p-4 text-contrast">Cached content visible immediately.</div>
|
||||
</ReadyTransition>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
/** Cold load: pending true then false — fade-in runs. */
|
||||
export const ColdLoad: Story = {
|
||||
render: () => ({
|
||||
components: { ReadyTransition, LoadingBar },
|
||||
setup() {
|
||||
provideLoadingState(createLoadingStateCore())
|
||||
const pending = ref(true)
|
||||
onMounted(() => {
|
||||
setTimeout(() => (pending.value = false), 600)
|
||||
})
|
||||
return { pending }
|
||||
},
|
||||
template: `
|
||||
<div class="relative">
|
||||
<LoadingBar />
|
||||
<p class="text-secondary mb-4">Pending 600ms then ready — content fades in; bar runs while pending.</p>
|
||||
<ReadyTransition :pending="pending">
|
||||
<div class="rounded bg-bg-raised p-4 text-contrast">Content after cold load.</div>
|
||||
</ReadyTransition>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const PendingThenReady: Story = {
|
||||
render: () => ({
|
||||
components: { ReadyTransition, LoadingBar },
|
||||
setup() {
|
||||
provideLoadingState(createLoadingStateCore())
|
||||
const pending = ref(true)
|
||||
onMounted(() => {
|
||||
setTimeout(() => (pending.value = false), 2000)
|
||||
})
|
||||
return { pending }
|
||||
},
|
||||
template: `
|
||||
<div class="relative">
|
||||
<LoadingBar />
|
||||
<p class="text-secondary mb-4">Pending for 2s, then content fades in. Bar runs at top.</p>
|
||||
<ReadyTransition :pending="pending">
|
||||
<div class="rounded bg-bg-raised p-4 text-contrast">Slot content revealed.</div>
|
||||
</ReadyTransition>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const StackedTwoTransitions: Story = {
|
||||
render: () => ({
|
||||
components: { ReadyTransition, LoadingBar },
|
||||
setup() {
|
||||
provideLoadingState(createLoadingStateCore())
|
||||
const a = ref(true)
|
||||
const b = ref(true)
|
||||
onMounted(() => {
|
||||
setTimeout(() => (a.value = false), 1500)
|
||||
setTimeout(() => (b.value = false), 3000)
|
||||
})
|
||||
return { a, b }
|
||||
},
|
||||
template: `
|
||||
<div class="relative grid gap-4">
|
||||
<LoadingBar />
|
||||
<ReadyTransition :pending="a">
|
||||
<div class="rounded bg-bg-raised p-4 text-contrast">Panel A (ready at 1.5s).</div>
|
||||
</ReadyTransition>
|
||||
<ReadyTransition :pending="b">
|
||||
<div class="rounded bg-bg-raised p-4 text-contrast">Panel B (ready at 3s).</div>
|
||||
</ReadyTransition>
|
||||
<p class="text-secondary">Bar stays visible until BOTH panels resolve.</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Silent: Story = {
|
||||
render: () => ({
|
||||
components: { ReadyTransition, LoadingBar },
|
||||
setup() {
|
||||
provideLoadingState(createLoadingStateCore())
|
||||
const pending = ref(true)
|
||||
onMounted(() => {
|
||||
setTimeout(() => (pending.value = false), 1500)
|
||||
})
|
||||
return { pending }
|
||||
},
|
||||
template: `
|
||||
<div class="relative">
|
||||
<LoadingBar />
|
||||
<p class="text-secondary mb-4">silent=true — fades locally but does NOT raise the loading bar.</p>
|
||||
<ReadyTransition :pending="pending" silent>
|
||||
<div class="rounded bg-bg-raised p-4 text-contrast">Silent slot content.</div>
|
||||
</ReadyTransition>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
Reference in New Issue
Block a user