@poveste/app 0.19.0 → 0.20.0

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 (39) hide show
  1. package/dist/bundled/app.css +174 -1
  2. package/dist/bundled/components/HomeView.vue_vue_type_script_setup_true_lang.js +3 -3
  3. package/dist/bundled/components/app/AppLogo.vue_vue_type_script_setup_true_lang.js +4 -4
  4. package/dist/bundled/components/app/TopBarChipItem.js +10 -6
  5. package/dist/bundled/components/layout/LayoutItem.js +1 -0
  6. package/dist/bundled/components/layout/LayoutItem.vue_vue_type_script_setup_true_lang.js +20 -23
  7. package/dist/bundled/components/panel/ComponentDocs.js +7 -0
  8. package/dist/bundled/components/panel/ComponentDocs.vue_vue_type_script_setup_true_lang.js +132 -0
  9. package/dist/bundled/components/panel/StoryDocs.vue_vue_type_script_setup_true_lang.js +32 -21
  10. package/dist/bundled/components/panel/StorySourceCode.js +1 -1
  11. package/dist/bundled/components/panel/StorySourceCode.vue_vue_type_script_setup_true_lang.js +20 -4
  12. package/dist/bundled/components/story/GenericMountStory.vue_vue_type_script_setup_true_lang.js +3 -7
  13. package/dist/bundled/components/story/GenericRenderStory.vue_vue_type_script_setup_true_lang.js +3 -10
  14. package/dist/bundled/components/toolbar/DevOnlyToolbarOpenInEditor.vue_vue_type_script_setup_true_lang.js +0 -1
  15. package/dist/bundled/router.js +15 -12
  16. package/dist/bundled/util/client-plugin.js +22 -0
  17. package/dist/bundled/util/component-docs-data.js +1 -0
  18. package/dist/bundled/util/component-docs.js +57 -0
  19. package/dist/bundled/util/docs.js +1 -1
  20. package/dist/bundled/util/icons.generated.js +2 -2
  21. package/dist/bundled/util/mapping.js +1 -5
  22. package/dist/style.css +5 -46
  23. package/package.json +15 -15
  24. package/src/app/components/HomeView.vue +2 -2
  25. package/src/app/components/app/AppLogo.vue +4 -6
  26. package/src/app/components/app/TopBarChipItem.vue +55 -6
  27. package/src/app/components/layout/LayoutItem.vue +56 -6
  28. package/src/app/components/panel/ComponentDocs.vue +257 -0
  29. package/src/app/components/panel/StoryDocs.vue +11 -2
  30. package/src/app/components/panel/StorySourceCode.vue +27 -3
  31. package/src/app/components/story/GenericMountStory.vue +3 -5
  32. package/src/app/components/story/GenericRenderStory.vue +4 -12
  33. package/src/app/components/toolbar/DevOnlyToolbarOpenInEditor.vue +0 -1
  34. package/src/app/router.ts +26 -15
  35. package/src/app/util/client-plugin.ts +33 -0
  36. package/src/app/util/component-docs-data.ts +3 -0
  37. package/src/app/util/component-docs.ts +78 -0
  38. package/src/app/util/icons.generated.ts +2 -2
  39. package/src/app/util/mapping.ts +1 -5
@@ -0,0 +1,257 @@
1
+ <script lang="ts" setup>
2
+ import type { DocProp } from '@poveste/shared'
3
+ import type { ComponentDocsEntry } from '../../util/component-docs'
4
+ import { tagHref, tagText } from '../../util/component-docs'
5
+
6
+ defineProps<{
7
+ components: ComponentDocsEntry[]
8
+ }>()
9
+
10
+ function shownDefault(prop: DocProp) {
11
+ return prop.default ?? prop.defaultTag
12
+ }
13
+ </script>
14
+
15
+ <template>
16
+ <div
17
+ class="poveste-component-docs"
18
+ data-testid="component-docs"
19
+ >
20
+ <section
21
+ v-for="component of components"
22
+ :key="component.file"
23
+ data-slot="component"
24
+ :data-component="component.name"
25
+ >
26
+ <h3 data-slot="component-name">
27
+ {{ component.name }}
28
+ </h3>
29
+
30
+ <p
31
+ v-if="component.error"
32
+ data-slot="error"
33
+ >
34
+ Could not read this component's types: {{ component.error }}
35
+ </p>
36
+
37
+ <template v-else-if="component.doc">
38
+ <table
39
+ v-if="component.doc.props.length"
40
+ data-slot="props"
41
+ >
42
+ <thead>
43
+ <tr>
44
+ <th>Prop</th>
45
+ <th>Type</th>
46
+ <th>Default</th>
47
+ <th>Description</th>
48
+ </tr>
49
+ </thead>
50
+ <tbody>
51
+ <tr
52
+ v-for="prop of component.doc.props"
53
+ :key="prop.name"
54
+ :data-prop="prop.name"
55
+ >
56
+ <td>
57
+ <code
58
+ :class="{ deprecated: tagText(prop.tags, 'deprecated') !== undefined }"
59
+ >{{ prop.name }}</code><span
60
+ v-if="prop.required"
61
+ title="Required"
62
+ data-slot="required"
63
+ >*</span>
64
+ </td>
65
+ <td><code>{{ prop.type }}</code></td>
66
+ <td>
67
+ <code v-if="shownDefault(prop) !== undefined">{{ shownDefault(prop) }}</code>
68
+ <span
69
+ v-if="__POVESTE_DEV__ && prop.defaultConflict"
70
+ :title="`The code's default is shown; the @default tag says ${prop.defaultTag}`"
71
+ data-slot="default-conflict"
72
+ >≠</span>
73
+ </td>
74
+ <td>
75
+ <span
76
+ v-if="tagText(prop.tags, 'deprecated') !== undefined"
77
+ data-slot="deprecated"
78
+ >Deprecated<template v-if="tagText(prop.tags, 'deprecated')">: {{ tagText(prop.tags, 'deprecated') }}</template></span>
79
+ {{ prop.description }}
80
+ <template
81
+ v-for="tag of prop.tags.filter(tag => tag.name === 'see' || tag.name === 'link')"
82
+ :key="tag.text"
83
+ >
84
+ <a
85
+ v-if="tagHref(tag.text ?? '')"
86
+ :href="tagHref(tag.text ?? '')"
87
+ target="_blank"
88
+ rel="noopener"
89
+ data-slot="see"
90
+ >{{ tag.text }}</a>
91
+ <span
92
+ v-else
93
+ data-slot="see"
94
+ >{{ tag.text }}</span>
95
+ </template>
96
+ <pre
97
+ v-if="tagText(prop.tags, 'example')"
98
+ data-slot="example"
99
+ ><code>{{ tagText(prop.tags, 'example') }}</code></pre>
100
+ </td>
101
+ </tr>
102
+ </tbody>
103
+ </table>
104
+
105
+ <table
106
+ v-if="component.doc.slots.length"
107
+ data-slot="slots"
108
+ >
109
+ <thead>
110
+ <tr>
111
+ <th>Slot</th>
112
+ <th>Type</th>
113
+ <th>Description</th>
114
+ </tr>
115
+ </thead>
116
+ <tbody>
117
+ <tr
118
+ v-for="slot of component.doc.slots"
119
+ :key="slot.name"
120
+ :data-slot-name="slot.name"
121
+ >
122
+ <td><code>{{ slot.name }}</code></td>
123
+ <td><code v-if="slot.type">{{ slot.type }}</code></td>
124
+ <td>{{ slot.description }}</td>
125
+ </tr>
126
+ </tbody>
127
+ </table>
128
+
129
+ <table
130
+ v-if="component.doc.events.length"
131
+ data-slot="events"
132
+ >
133
+ <thead>
134
+ <tr>
135
+ <th>Event</th>
136
+ <th>Payload</th>
137
+ <th>Description</th>
138
+ </tr>
139
+ </thead>
140
+ <tbody>
141
+ <tr
142
+ v-for="event of component.doc.events"
143
+ :key="event.name"
144
+ :data-event="event.name"
145
+ >
146
+ <td><code>{{ event.name }}</code></td>
147
+ <td><code v-if="event.type">{{ event.type }}</code></td>
148
+ <td>{{ event.description }}</td>
149
+ </tr>
150
+ </tbody>
151
+ </table>
152
+ </template>
153
+ </section>
154
+ </div>
155
+ </template>
156
+
157
+ <style scoped>
158
+ .poveste-component-docs {
159
+ padding: 1rem;
160
+ font-size: .875rem;
161
+ color: var(--color-gray-800);
162
+
163
+ /* On the subject, not `.ptw-dark &`: `.ptw-dark` sits above the `@scope` root (#101). */
164
+ &:where(.ptw-dark, .ptw-dark *) {
165
+ color: var(--color-gray-200);
166
+ }
167
+ }
168
+
169
+ section + section {
170
+ margin-top: 1.5rem;
171
+ }
172
+
173
+ h3 {
174
+ font-weight: 600;
175
+ font-size: 1rem;
176
+ margin-bottom: .5rem;
177
+ }
178
+
179
+ table {
180
+ width: 100%;
181
+ border-collapse: collapse;
182
+ margin-bottom: 1rem;
183
+ }
184
+
185
+ th {
186
+ text-align: start;
187
+ font-weight: 500;
188
+ color: var(--color-gray-500);
189
+ }
190
+
191
+ th,
192
+ td {
193
+ padding: .375rem .5rem;
194
+ border-bottom: 1px solid var(--color-gray-200);
195
+ vertical-align: top;
196
+
197
+ &:where(.ptw-dark, .ptw-dark *) {
198
+ border-color: var(--color-gray-750);
199
+ }
200
+ }
201
+
202
+ code {
203
+ font-size: .8125rem;
204
+ background-color: var(--color-gray-100);
205
+ border-radius: .25rem;
206
+ padding: 0 .25rem;
207
+
208
+ &:where(.ptw-dark, .ptw-dark *) {
209
+ background-color: var(--color-gray-750);
210
+ }
211
+ }
212
+
213
+ pre code {
214
+ display: block;
215
+ padding: .5rem;
216
+ margin-top: .25rem;
217
+ white-space: pre-wrap;
218
+ }
219
+
220
+ .deprecated {
221
+ text-decoration: line-through;
222
+ }
223
+
224
+ [data-slot='required'] {
225
+ color: var(--color-red-500);
226
+ margin-inline-start: .125rem;
227
+ }
228
+
229
+ [data-slot='deprecated'],
230
+ [data-slot='default-conflict'] {
231
+ color: var(--color-amber-600);
232
+ font-weight: 500;
233
+
234
+ &:where(.ptw-dark, .ptw-dark *) {
235
+ color: var(--color-amber-400);
236
+ }
237
+ }
238
+
239
+ [data-slot='default-conflict'] {
240
+ margin-inline-start: .25rem;
241
+ cursor: help;
242
+ }
243
+
244
+ [data-slot='see'] {
245
+ display: block;
246
+ margin-top: .25rem;
247
+ }
248
+
249
+ a[data-slot='see'] {
250
+ color: var(--color-primary-500);
251
+ text-decoration: underline;
252
+ }
253
+
254
+ [data-slot='error'] {
255
+ color: var(--color-gray-500);
256
+ }
257
+ </style>
@@ -5,9 +5,11 @@ import { Icon } from '@iconify/vue'
5
5
  import { markdownFiles } from 'virtual:$poveste-markdown-files'
6
6
  import { computed, nextTick, ref, toRefs, watch, watchEffect } from 'vue'
7
7
  import { useRoute, useRouter } from 'vue-router'
8
+ import { useComponentDocs } from '../../util/component-docs.js'
8
9
  import { povesteConfig } from '../../util/config.js'
9
10
  import BaseEmpty from '../base/BaseEmpty.vue'
10
11
  import DevOnlyToolbarOpenInEditor from '../toolbar/DevOnlyToolbarOpenInEditor.vue'
12
+ import ComponentDocs from './ComponentDocs.vue'
11
13
 
12
14
  export function useStoryDoc(story: Ref<Story>) {
13
15
  const renderedDoc = ref('')
@@ -76,6 +78,9 @@ const { story } = toRefs(props)
76
78
 
77
79
  const { renderedDoc } = useStoryDoc(story)
78
80
 
81
+ // Opening the docs is what starts auto-docs extraction (#1159).
82
+ const { components } = useComponentDocs(computed(() => story.value?.id))
83
+
79
84
  // Markdown links to other stories
80
85
  const router = useRouter()
81
86
 
@@ -185,7 +190,7 @@ const filePath = computed(() => story.value.file?.docsFilePath ?? (props.standal
185
190
  </div>
186
191
 
187
192
  <BaseEmpty
188
- v-if="!renderedDoc"
193
+ v-if="!renderedDoc && !components?.length"
189
194
  >
190
195
  <Icon
191
196
  icon="carbon:document-unknown"
@@ -194,11 +199,15 @@ const filePath = computed(() => story.value.file?.docsFilePath ?? (props.standal
194
199
  No documentation available
195
200
  </BaseEmpty>
196
201
  <div
197
- v-else
202
+ v-if="renderedDoc"
198
203
  ref="renderedEl"
199
204
  class="prose dark:prose-invert p-4 max-w-none"
200
205
  data-testid="story-docs"
201
206
  v-html="renderedDoc"
202
207
  />
208
+ <ComponentDocs
209
+ v-if="components?.length"
210
+ :components="components"
211
+ />
203
212
  </div>
204
213
  </template>
@@ -19,8 +19,21 @@ const props = defineProps<{
19
19
  // down were both errors about a type nobody meant to declare.
20
20
  const generateSourceCodeFn = shallowRef<GenerateSourceCode | null>(null)
21
21
 
22
+ // Until a source has settled, an empty one means "not yet", not "none": a copy in
23
+ // that window said "Nothing to copy" with the source a moment away (#1314).
24
+ const dynamicSettled = ref(false)
25
+ const staticSettled = ref(false)
26
+
22
27
  watchEffect(async () => {
23
- const generate = await loadGenerateSourceCode(props.story)
28
+ let generate: GenerateSourceCode | undefined
29
+ try {
30
+ generate = await loadGenerateSourceCode(props.story)
31
+ }
32
+ finally {
33
+ if (!generate) {
34
+ dynamicSettled.value = true
35
+ }
36
+ }
24
37
  if (generate) {
25
38
  generateSourceCodeFn.value = markRaw(generate)
26
39
  }
@@ -40,6 +53,7 @@ watch(() => [props.variant, generateSourceCodeFn.value], async () => {
40
53
  if (!generateSourceCodeFn.value) return
41
54
  error.value = null
42
55
  dynamicSourceCode.value = ''
56
+ dynamicSettled.value = false
43
57
  try {
44
58
  dynamicSourceCode.value = (await getDynamicSourceCode(props.variant, generateSourceCodeFn.value)) ?? ''
45
59
  }
@@ -47,6 +61,7 @@ watch(() => [props.variant, generateSourceCodeFn.value], async () => {
47
61
  console.error(e)
48
62
  error.value = e instanceof Error ? e.message : String(e)
49
63
  }
64
+ dynamicSettled.value = true
50
65
 
51
66
  // Auto-switch
52
67
  if (!dynamicSourceCode.value) {
@@ -62,11 +77,19 @@ watch(() => [props.variant, generateSourceCodeFn.value], async () => {
62
77
  const staticSourceCode = ref('')
63
78
  watch(() => [props.story, props.story?.file?.source], async () => {
64
79
  staticSourceCode.value = ''
65
- staticSourceCode.value = (await getStaticSourceCode(props.story)) ?? ''
80
+ staticSettled.value = false
81
+ try {
82
+ staticSourceCode.value = (await getStaticSourceCode(props.story)) ?? ''
83
+ }
84
+ finally {
85
+ staticSettled.value = true
86
+ }
66
87
  }, {
67
88
  immediate: true,
68
89
  })
69
90
 
91
+ const loading = computed(() => displayedSource.value === 'dynamic' ? !dynamicSettled.value : !staticSettled.value)
92
+
70
93
  const displayedSourceCode = computed(() => {
71
94
  if (displayedSource.value === 'dynamic') {
72
95
  return dynamicSourceCode.value
@@ -189,6 +212,7 @@ watch(sourceHtml, async () => {
189
212
 
190
213
  <HstCopyIcon
191
214
  :content="displayedSourceCode"
215
+ :loading="loading"
192
216
  class="flex-none"
193
217
  />
194
218
  </div>
@@ -205,7 +229,7 @@ watch(sourceHtml, async () => {
205
229
  icon="carbon:code-hide"
206
230
  class="w-8 h-8 opacity-50 mb-6"
207
231
  />
208
- <span>Not available</span>
232
+ <span>{{ loading ? 'Loading' : 'Not available' }}</span>
209
233
  </BaseEmpty>
210
234
 
211
235
  <textarea
@@ -7,8 +7,8 @@ export default {
7
7
  <script lang="ts" setup>
8
8
  import type { Component } from 'vue'
9
9
  import type { Story } from '../../types'
10
- import { clientSupportPlugins } from 'virtual:$poveste-support-plugins-client'
11
10
  import { markRaw, ref, watchEffect } from 'vue'
11
+ import { loadClientPlugin } from '../../util/client-plugin.js'
12
12
 
13
13
  const props = defineProps<{
14
14
  story: Story
@@ -17,10 +17,8 @@ const props = defineProps<{
17
17
  const mountComponent = ref<Component | null>(null)
18
18
 
19
19
  watchEffect(async () => {
20
- const supportPluginId = props.story.file?.supportPluginId
21
- const clientPlugin = supportPluginId ? clientSupportPlugins[supportPluginId] : undefined
22
- if (clientPlugin) {
23
- const pluginModule = await clientPlugin()
20
+ const pluginModule = await loadClientPlugin(props.story.file?.supportPluginId)
21
+ if (pluginModule) {
24
22
  mountComponent.value = markRaw(pluginModule.MountStory)
25
23
  }
26
24
  })
@@ -7,8 +7,8 @@ export default {
7
7
  <script lang="ts" setup>
8
8
  import type { Component } from 'vue'
9
9
  import type { Story } from '../../types'
10
- import { clientSupportPlugins } from 'virtual:$poveste-support-plugins-client'
11
10
  import { markRaw, ref, watchEffect } from 'vue'
11
+ import { loadClientPlugin } from '../../util/client-plugin.js'
12
12
 
13
13
  const props = defineProps<{
14
14
  story: Story
@@ -17,17 +17,9 @@ const props = defineProps<{
17
17
  const mountComponent = ref<Component | null>(null)
18
18
 
19
19
  watchEffect(async () => {
20
- const supportPluginId = props.story.file?.supportPluginId
21
- const clientPlugin = supportPluginId ? clientSupportPlugins[supportPluginId] : undefined
22
- if (clientPlugin) {
23
- try {
24
- const pluginModule = await clientPlugin()
25
- mountComponent.value = markRaw(pluginModule.RenderStory)
26
- }
27
- catch (e) {
28
- console.error(e)
29
- throw e
30
- }
20
+ const pluginModule = await loadClientPlugin(props.story.file?.supportPluginId)
21
+ if (pluginModule) {
22
+ mountComponent.value = markRaw(pluginModule.RenderStory)
31
23
  }
32
24
  })
33
25
  </script>
@@ -14,7 +14,6 @@ defineProps<{
14
14
  <HstTooltip :content="tooltip">
15
15
  <TopBarChipItem
16
16
  aria-label="Open story in editor"
17
- target="_blank"
18
17
  class="poveste-toolbar-open-in-editor"
19
18
  @click="openInEditor(file)"
20
19
  >
package/src/app/router.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Router } from 'vue-router'
1
2
  import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
2
3
  import { povesteConfig } from './util/config'
3
4
 
@@ -12,18 +13,28 @@ function createRouterHistory() {
12
13
  }
13
14
  }
14
15
 
15
- export const router = createRouter({
16
- history: createRouterHistory(),
17
- routes: [
18
- {
19
- path: '/',
20
- name: 'home',
21
- component: () => import('./components/HomeView.vue'),
22
- },
23
- {
24
- path: '/story/:storyId',
25
- name: 'story',
26
- component: () => import('./components/story/StoryView.vue'),
27
- },
28
- ],
29
- })
16
+ function newRouter(): Router {
17
+ return createRouter({
18
+ history: createRouterHistory(),
19
+ routes: [
20
+ {
21
+ path: '/',
22
+ name: 'home',
23
+ component: () => import('./components/HomeView.vue'),
24
+ },
25
+ {
26
+ path: '/story/:storyId',
27
+ name: 'story',
28
+ component: () => import('./components/story/StoryView.vue'),
29
+ },
30
+ ],
31
+ })
32
+ }
33
+
34
+ // A page in `poveste dev` can hold two copies of this module: each importer's
35
+ // URL is stamped when the server serves it, and a story edit stamps most of the
36
+ // chrome. Every copy hands out the router the app installed, or the story store
37
+ // read one still at `/` and the page showed no story until reloaded (#1320).
38
+ export const router: Router = import.meta.hot
39
+ ? (import.meta.hot.data.router ??= newRouter())
40
+ : newRouter()
@@ -0,0 +1,33 @@
1
+ import type { ClientSupportPlugin } from '@poveste/shared'
2
+ import type { Component } from 'vue'
3
+ import { reportStoryError, toStoryError } from '@poveste/shared'
4
+ import { clientSupportPlugins } from 'virtual:$poveste-support-plugins-client'
5
+ import { occupant } from './occupant.js'
6
+
7
+ /*
8
+ * A framework's client imports the book's setup file, so a setup file that throws
9
+ * when it loads fails here, for every story, before any story code runs. In a
10
+ * built book nothing else reported it: Vue's production build only logs an async
11
+ * effect's rejection, and every preview stayed blank with the error in the console
12
+ * alone (#1323). Not rethrown, because Vue's development build would rethrow it as
13
+ * an unhandled rejection, whose report would replace this one.
14
+ */
15
+ export async function loadClientPlugin(supportPluginId: string | undefined): Promise<ClientSupportPlugin<Component> | undefined> {
16
+ const load = supportPluginId ? clientSupportPlugins[supportPluginId] : undefined
17
+ if (!load) {
18
+ return undefined
19
+ }
20
+ try {
21
+ return await load()
22
+ }
23
+ catch (error) {
24
+ const { message, stack } = toStoryError(error)
25
+ const reported = Object.assign(new Error(`The story renderer failed to load, and it imports the setup file: ${message}`), { stack })
26
+ reportStoryError(reported, {
27
+ storyId: occupant.storyId ?? undefined,
28
+ variantId: occupant.variantId ?? undefined,
29
+ })
30
+ console.error(error)
31
+ return undefined
32
+ }
33
+ }
@@ -0,0 +1,3 @@
1
+ // What `poveste build` extracted, in a chunk of its own: loaded when a reader first
2
+ // opens a story's docs, so a book whose docs nobody opens never downloads it.
3
+ export { componentDocs } from 'virtual:$poveste-component-docs'
@@ -0,0 +1,78 @@
1
+ import type { ComponentDoc, DocTag } from '@poveste/shared'
2
+ import type { Ref } from 'vue'
3
+ import { onBeforeUnmount, ref, watch } from 'vue'
4
+
5
+ export interface ComponentDocsEntry {
6
+ /** Relative to the book's root. */
7
+ file: string
8
+ /** The file name without its extension. */
9
+ name: string
10
+ doc?: ComponentDoc
11
+ error?: string
12
+ }
13
+
14
+ interface StoryDocsResult {
15
+ storyId: string
16
+ components: Record<string, { doc?: ComponentDoc, error?: string }>
17
+ }
18
+
19
+ export function toEntries(result: StoryDocsResult): ComponentDocsEntry[] {
20
+ return Object.entries(result.components).map(([file, { doc, error }]) => ({
21
+ file,
22
+ name: file.split('/').pop()!.replace(/\.[^.]+$/, ''),
23
+ ...doc ? { doc } : {},
24
+ ...error ? { error } : {},
25
+ }))
26
+ }
27
+
28
+ export function tagText(tags: DocTag[], name: string): string | undefined {
29
+ const tag = tags.find(tag => tag.name === name)
30
+ return tag ? (tag.text ?? '') : undefined
31
+ }
32
+
33
+ /** A `@see` or `@link` points somewhere only when it carries a URL. */
34
+ export function tagHref(text: string): string | undefined {
35
+ return /https?:\/\/\S+/.exec(text)?.[0]
36
+ }
37
+
38
+ /**
39
+ * The components a story imports, documented. In dev they are asked of the server
40
+ * when this is first used, which is what starts extraction at all (#1159); in a
41
+ * built book they come from what `poveste build` extracted.
42
+ */
43
+ export function useComponentDocs(storyId: Ref<string | undefined>) {
44
+ const components = ref<ComponentDocsEntry[]>()
45
+ const hot = import.meta.hot
46
+ if (!hot) {
47
+ watch(storyId, async (id) => {
48
+ const { componentDocs } = await import('./component-docs-data')
49
+ const docs = id ? componentDocs[id] : undefined
50
+ components.value = id && docs ? toEntries({ storyId: id, components: docs }) : undefined
51
+ }, { immediate: true })
52
+ return { components }
53
+ }
54
+
55
+ function ask() {
56
+ if (storyId.value) {
57
+ hot!.send('poveste:docgen-request', { storyId: storyId.value })
58
+ }
59
+ }
60
+ function onResult(result: StoryDocsResult) {
61
+ if (result.storyId === storyId.value) {
62
+ components.value = toEntries(result)
63
+ }
64
+ }
65
+
66
+ hot.on('poveste:docgen-result', onResult)
67
+ hot.on('poveste:docgen-stale', ask)
68
+ onBeforeUnmount(() => {
69
+ hot.off('poveste:docgen-result', onResult)
70
+ hot.off('poveste:docgen-stale', ask)
71
+ })
72
+ watch(storyId, () => {
73
+ components.value = undefined
74
+ ask()
75
+ }, { immediate: true })
76
+
77
+ return { components }
78
+ }