@poveste/app 0.19.1 → 0.21.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.
- package/dist/bundled/app.css +208 -7
- package/dist/bundled/components/HomeView.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/bundled/components/app/AppLogo.vue_vue_type_script_setup_true_lang.js +4 -4
- package/dist/bundled/components/app/TopBarChipItem.js +10 -6
- package/dist/bundled/components/layout/LayoutItem.js +1 -0
- package/dist/bundled/components/layout/LayoutItem.vue_vue_type_script_setup_true_lang.js +20 -23
- package/dist/bundled/components/layout/LayoutModal.vue_vue_type_script_setup_true_lang.js +44 -5
- package/dist/bundled/components/panel/ComponentDocs.js +7 -0
- package/dist/bundled/components/panel/ComponentDocs.vue_vue_type_script_setup_true_lang.js +164 -0
- package/dist/bundled/components/panel/StoryDocs.vue_vue_type_script_setup_true_lang.js +57 -22
- package/dist/bundled/components/panel/StorySourceCode.js +1 -1
- package/dist/bundled/components/panel/StorySourceCode.vue_vue_type_script_setup_true_lang.js +22 -6
- package/dist/bundled/components/story/GenericMountStory.vue_vue_type_script_setup_true_lang.js +3 -7
- package/dist/bundled/components/story/GenericRenderStory.vue_vue_type_script_setup_true_lang.js +3 -10
- package/dist/bundled/components/story/StoryResponsivePreview.js +1 -1
- package/dist/bundled/components/story/StoryResponsivePreview.vue_vue_type_script_setup_true_lang.js +5 -3
- package/dist/bundled/components/story/StoryVariantGridItem.js +1 -1
- package/dist/bundled/components/story/StoryVariantGridItem.vue_vue_type_script_setup_true_lang.js +5 -3
- package/dist/bundled/components/story/StoryVariantSinglePreviewRemote.vue_vue_type_script_setup_true_lang.js +3 -2
- package/dist/bundled/components/toolbar/DevOnlyToolbarOpenInEditor.vue_vue_type_script_setup_true_lang.js +0 -1
- package/dist/bundled/index.js +5 -1
- package/dist/bundled/sandbox.js +2 -0
- package/dist/bundled/util/client-plugin.js +22 -0
- package/dist/bundled/util/component-docs-data.js +5 -0
- package/dist/bundled/util/component-docs.js +133 -0
- package/dist/bundled/util/dark.js +3 -1
- package/dist/bundled/util/docs.js +13 -3
- package/dist/bundled/util/highlighter.js +1 -1
- package/dist/bundled/util/icons.generated.js +3 -2
- package/dist/bundled/util/keyboard.js +5 -4
- package/dist/bundled/util/mapping.js +1 -5
- package/dist/style.css +321 -195
- package/package.json +17 -17
- package/src/app/components/HomeView.vue +2 -2
- package/src/app/components/app/AppLogo.vue +4 -6
- package/src/app/components/app/TopBarChipItem.vue +55 -6
- package/src/app/components/layout/LayoutItem.vue +56 -6
- package/src/app/components/layout/LayoutModal.vue +55 -2
- package/src/app/components/panel/ComponentDocs.vue +323 -0
- package/src/app/components/panel/StoryDocs.vue +38 -2
- package/src/app/components/panel/StorySourceCode.vue +29 -5
- package/src/app/components/story/GenericMountStory.vue +3 -5
- package/src/app/components/story/GenericRenderStory.vue +4 -12
- package/src/app/components/story/StoryResponsivePreview.vue +4 -1
- package/src/app/components/story/StoryVariantGridItem.vue +4 -1
- package/src/app/components/story/StoryVariantSinglePreviewRemote.vue +6 -4
- package/src/app/components/toolbar/DevOnlyToolbarOpenInEditor.vue +0 -1
- package/src/app/index.ts +12 -3
- package/src/app/sandbox.ts +5 -0
- package/src/app/util/client-plugin.ts +33 -0
- package/src/app/util/component-docs-data.ts +6 -0
- package/src/app/util/component-docs.ts +169 -0
- package/src/app/util/dark.ts +6 -0
- package/src/app/util/docs.ts +18 -2
- package/src/app/util/highlighter.ts +2 -1
- package/src/app/util/icons.generated.ts +2 -2
- package/src/app/util/keyboard.ts +8 -4
- package/src/app/util/mapping.ts +1 -5
|
@@ -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 { onlySections, 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,10 @@ 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: allComponents, pending, off } = useComponentDocs(computed(() => story.value?.id))
|
|
83
|
+
const components = computed(() => allComponents.value && onlySections(allComponents.value, story.value?.autoDocs))
|
|
84
|
+
|
|
79
85
|
// Markdown links to other stories
|
|
80
86
|
const router = useRouter()
|
|
81
87
|
|
|
@@ -184,8 +190,33 @@ const filePath = computed(() => story.value.file?.docsFilePath ?? (props.standal
|
|
|
184
190
|
/>
|
|
185
191
|
</div>
|
|
186
192
|
|
|
193
|
+
<!--
|
|
194
|
+
The same empty panel said "still reading" and "off, for a reason" alike (#1400).
|
|
195
|
+
A built book's data is a chunk away, so it shows nothing until then, not a spinner.
|
|
196
|
+
-->
|
|
197
|
+
<BaseEmpty
|
|
198
|
+
v-if="__POVESTE_DEV__ && !renderedDoc && !components?.length && pending"
|
|
199
|
+
data-testid="docs-pending"
|
|
200
|
+
>
|
|
201
|
+
<Icon
|
|
202
|
+
icon="carbon:circle-dash"
|
|
203
|
+
class="w-8 h-8 opacity-50 mb-6 animate-spin"
|
|
204
|
+
/>
|
|
205
|
+
Reading component types…
|
|
206
|
+
<span class="text-sm opacity-70 mt-2">The first time can take a while.</span>
|
|
207
|
+
</BaseEmpty>
|
|
208
|
+
<BaseEmpty
|
|
209
|
+
v-else-if="!pending && !renderedDoc && !components?.length && off"
|
|
210
|
+
data-testid="docs-off"
|
|
211
|
+
>
|
|
212
|
+
<Icon
|
|
213
|
+
icon="carbon:document-unknown"
|
|
214
|
+
class="w-8 h-8 opacity-50 mb-6"
|
|
215
|
+
/>
|
|
216
|
+
{{ off }}
|
|
217
|
+
</BaseEmpty>
|
|
187
218
|
<BaseEmpty
|
|
188
|
-
v-if="!renderedDoc"
|
|
219
|
+
v-else-if="!pending && !renderedDoc && !components?.length"
|
|
189
220
|
>
|
|
190
221
|
<Icon
|
|
191
222
|
icon="carbon:document-unknown"
|
|
@@ -194,11 +225,16 @@ const filePath = computed(() => story.value.file?.docsFilePath ?? (props.standal
|
|
|
194
225
|
No documentation available
|
|
195
226
|
</BaseEmpty>
|
|
196
227
|
<div
|
|
197
|
-
v-
|
|
228
|
+
v-if="renderedDoc"
|
|
198
229
|
ref="renderedEl"
|
|
199
230
|
class="prose dark:prose-invert p-4 max-w-none"
|
|
200
231
|
data-testid="story-docs"
|
|
201
232
|
v-html="renderedDoc"
|
|
202
233
|
/>
|
|
234
|
+
<ComponentDocs
|
|
235
|
+
v-if="components?.length"
|
|
236
|
+
:components="components"
|
|
237
|
+
:story-file="story.file?.filePath"
|
|
238
|
+
/>
|
|
203
239
|
</div>
|
|
204
240
|
</template>
|
|
@@ -6,7 +6,7 @@ import { Icon } from '@iconify/vue'
|
|
|
6
6
|
import { HstCopyIcon, HstTooltip } from '@poveste/controls'
|
|
7
7
|
import { computed, markRaw, nextTick, onMounted, ref, shallowRef, watch, watchEffect } from 'vue'
|
|
8
8
|
import { isDark } from '../../util/dark'
|
|
9
|
-
import { getDynamicSourceCode, getStaticSourceCode, loadGenerateSourceCode } from '../../util/docs'
|
|
9
|
+
import { getDynamicSourceCode, getStaticSourceCode, loadGenerateSourceCode, sourceLang } from '../../util/docs'
|
|
10
10
|
import { getHighlighter } from '../../util/highlighter'
|
|
11
11
|
import BaseEmpty from '../base/BaseEmpty.vue'
|
|
12
12
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -89,7 +112,7 @@ onMounted(async () => {
|
|
|
89
112
|
|
|
90
113
|
const sourceHtml = computed(() => displayedSourceCode.value
|
|
91
114
|
? highlighter.value?.codeToHtml(displayedSourceCode.value, {
|
|
92
|
-
lang:
|
|
115
|
+
lang: sourceLang(props.story),
|
|
93
116
|
theme: isDark.value ? 'github-dark' : 'github-light-high-contrast',
|
|
94
117
|
})
|
|
95
118
|
: '')
|
|
@@ -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
|
|
21
|
-
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
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>
|
|
@@ -6,6 +6,7 @@ import { HstTooltip } from '@poveste/controls'
|
|
|
6
6
|
import { useEventListener } from '@vueuse/core'
|
|
7
7
|
import { computed, onUnmounted, ref } from 'vue'
|
|
8
8
|
import { usePreviewSettingsStore } from '../../stores/preview-settings'
|
|
9
|
+
import { usePreviewDark } from '../../util/color-scheme'
|
|
9
10
|
import CheckerboardPattern from '../misc/CheckerboardPattern.vue'
|
|
10
11
|
import HatchedPattern from '../misc/HatchedPattern.vue'
|
|
11
12
|
|
|
@@ -21,6 +22,7 @@ const props = withDefaults(defineProps<{
|
|
|
21
22
|
})
|
|
22
23
|
|
|
23
24
|
const settings = usePreviewSettingsStore().currentSettings
|
|
25
|
+
const previewDark = usePreviewDark(settings)
|
|
24
26
|
|
|
25
27
|
// Resize
|
|
26
28
|
|
|
@@ -183,7 +185,8 @@ const sizeTooltip = computed(() => `${responsiveWidth.value ?? 'Auto'} × ${resp
|
|
|
183
185
|
// padding (#264). Gated on `responsive`, not `isResponsiveEnabled`:
|
|
184
186
|
// a single-view story with `responsiveDisabled` still needs its own
|
|
185
187
|
// chrome, it just loses the draggers.
|
|
186
|
-
|
|
188
|
+
// The preview's scheme, not the chrome's, which `dark:` would follow (#1449).
|
|
189
|
+
responsive ? (previewDark ? 'bg-gray-700' : 'bg-white') : '',
|
|
187
190
|
isResponsiveEnabled ? {
|
|
188
191
|
'w-fit': !!finalWidth,
|
|
189
192
|
'h-fit': !!finalHeight,
|
|
@@ -148,8 +148,11 @@ const useIframe = computed(() => {
|
|
|
148
148
|
|
|
149
149
|
<!-- Body -->
|
|
150
150
|
<div
|
|
151
|
-
class="border
|
|
151
|
+
class="border rounded flex-1 p-4 relative"
|
|
152
152
|
:class="{
|
|
153
|
+
// The preview's scheme, not the chrome's, which `dark:` would follow (#1449).
|
|
154
|
+
'bg-gray-700': previewDark,
|
|
155
|
+
'bg-white': !previewDark,
|
|
153
156
|
'border-gray-100 dark:border-gray-800': !isActive,
|
|
154
157
|
'border-primary-200 dark:border-primary-900': isActive,
|
|
155
158
|
}"
|
|
@@ -50,7 +50,7 @@ function postState(changes: Record<string, any>) {
|
|
|
50
50
|
iframe.value?.contentWindow?.postMessage({
|
|
51
51
|
type: STATE_SYNC,
|
|
52
52
|
state: changes,
|
|
53
|
-
})
|
|
53
|
+
}, window.location.origin)
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
let bridge = createStateBridge(postState)
|
|
@@ -105,9 +105,11 @@ function fromCurrentOccupant(data: { storyId?: string, variantId?: string }) {
|
|
|
105
105
|
useEventListener(window, 'message', (event) => {
|
|
106
106
|
// With many grid iframes mounted, every sandbox postMessage hits every
|
|
107
107
|
// parent listener. Skip events that didn't originate from this iframe.
|
|
108
|
-
// Paired with the origin check in `sandbox.ts`, which guards the other
|
|
109
|
-
// direction (#379).
|
|
110
108
|
if (event.source !== iframe.value?.contentWindow) return
|
|
109
|
+
// And from the book's own origin: the frame keeps its window when a story
|
|
110
|
+
// navigates it off-site, and the site it lands on could post here (#1494).
|
|
111
|
+
// The other direction is `sandbox.ts`'s check (#379).
|
|
112
|
+
if (event.origin !== window.location.origin) return
|
|
111
113
|
switch (event.data?.type) {
|
|
112
114
|
case STATE_SYNC:
|
|
113
115
|
if (!fromCurrentOccupant(event.data)) break
|
|
@@ -279,7 +281,7 @@ function syncSettings() {
|
|
|
279
281
|
iframe.value.contentWindow.postMessage({
|
|
280
282
|
type: PREVIEW_SETTINGS_SYNC,
|
|
281
283
|
settings: toRaw(settings),
|
|
282
|
-
})
|
|
284
|
+
}, window.location.origin)
|
|
283
285
|
}
|
|
284
286
|
}
|
|
285
287
|
|
package/src/app/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { addCollection } from '@iconify/vue'
|
|
2
2
|
import { createPinia } from 'pinia'
|
|
3
|
-
import { generation } from 'virtual:$poveste-stories'
|
|
3
|
+
import { generation, onUpdate, icons as storyIcons } from 'virtual:$poveste-stories'
|
|
4
4
|
import { createApp, watch } from 'vue'
|
|
5
5
|
import App from './App.vue'
|
|
6
6
|
import { setupPluginApi } from './plugin.js'
|
|
@@ -13,11 +13,20 @@ import 'virtual:$poveste-theme'
|
|
|
13
13
|
export async function mountMainApp() {
|
|
14
14
|
// The chrome's icon data ships with the app. Without this every Icon fetched
|
|
15
15
|
// its data from api.iconify.design at runtime — a handful of CDN requests per
|
|
16
|
-
// boot, and no toolbar offline (#219).
|
|
17
|
-
// through the runtime lookup.
|
|
16
|
+
// boot, and no toolbar offline (#219).
|
|
18
17
|
for (const collection of iconCollections) {
|
|
19
18
|
addCollection(collection)
|
|
20
19
|
}
|
|
20
|
+
// And the icons a book's stories name, where the book has the collection
|
|
21
|
+
// installed; the rest still resolve through the runtime lookup (#255).
|
|
22
|
+
for (const collection of storyIcons) {
|
|
23
|
+
addCollection(collection)
|
|
24
|
+
}
|
|
25
|
+
onUpdate((_files: unknown, _tree: unknown, icons: typeof storyIcons = []) => {
|
|
26
|
+
for (const collection of icons) {
|
|
27
|
+
addCollection(collection)
|
|
28
|
+
}
|
|
29
|
+
})
|
|
21
30
|
|
|
22
31
|
// The chrome's own scheme, for the browser: it paints the chrome's native
|
|
23
32
|
// controls and scrollbars, and reads no class (#991). Here rather than in the
|
package/src/app/sandbox.ts
CHANGED
|
@@ -18,6 +18,8 @@ import { applyPreviewSettings, loadStoredPreviewSettings, receivedSettings } fro
|
|
|
18
18
|
import { createStateBridge } from './util/state-bridge.js'
|
|
19
19
|
import { toRawDeep } from './util/state.js'
|
|
20
20
|
import { isBenignResizeObserverEvent } from './util/uncaught.js'
|
|
21
|
+
// The book's `theme.colors`, under the private names the controls' tokens read (#1443).
|
|
22
|
+
import 'virtual:$poveste-theme'
|
|
21
23
|
|
|
22
24
|
const query = parseQuery(window.location.search)
|
|
23
25
|
|
|
@@ -115,6 +117,9 @@ watch(previewDark, (value) => {
|
|
|
115
117
|
el.classList.toggle(className, value)
|
|
116
118
|
}
|
|
117
119
|
}
|
|
120
|
+
// A builtin control in the story keys its dark rules on `.ptw-dark`, so it follows
|
|
121
|
+
// the preview it sits in, not the chrome around it (#1443).
|
|
122
|
+
document.documentElement.classList.toggle('ptw-dark', value)
|
|
118
123
|
}, {
|
|
119
124
|
immediate: true,
|
|
120
125
|
})
|
|
@@ -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,6 @@
|
|
|
1
|
+
import type { ComponentDoc } from '@poveste/shared'
|
|
2
|
+
import { componentDocs as data } from 'virtual:$poveste-component-docs'
|
|
3
|
+
|
|
4
|
+
// What `poveste build` extracted: a small map from each story to its components, each
|
|
5
|
+
// in a chunk of its own, so a reader downloads only the tables of the stories they open (#1429).
|
|
6
|
+
export const componentDocs: Record<string, Record<string, () => Promise<{ default: { doc: ComponentDoc } }>>> = data
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { AutoDocsSection, 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 component's own name, else 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, name?: string }>
|
|
17
|
+
/** Why auto-docs is off for this story, as the terminal says it. */
|
|
18
|
+
off?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function toEntries(result: StoryDocsResult): ComponentDocsEntry[] {
|
|
22
|
+
return Object.entries(result.components).map(([file, { doc, error, name }]) => ({
|
|
23
|
+
file,
|
|
24
|
+
// A JSX story's result is keyed by the story file, so the extractor names the component,
|
|
25
|
+
// and an unnamed one, such as an inline function, goes by the story's file name.
|
|
26
|
+
name: doc?.name ?? name ?? file.split('/').pop()!.replace(/(?:\.story)?\.[^.]+$/, ''),
|
|
27
|
+
...doc ? { doc } : {},
|
|
28
|
+
...error ? { error } : {},
|
|
29
|
+
}))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Only the sections a story's `autoDocs` names, in the table's own order. A component
|
|
34
|
+
* left with nothing to show is dropped; one that failed keeps its error (#1398).
|
|
35
|
+
*/
|
|
36
|
+
export function onlySections(entries: ComponentDocsEntry[], autoDocs: boolean | AutoDocsSection[] | undefined): ComponentDocsEntry[] {
|
|
37
|
+
if (!Array.isArray(autoDocs)) {
|
|
38
|
+
return entries
|
|
39
|
+
}
|
|
40
|
+
return entries.flatMap((entry) => {
|
|
41
|
+
if (!entry.doc) {
|
|
42
|
+
return [entry]
|
|
43
|
+
}
|
|
44
|
+
const doc = {
|
|
45
|
+
...entry.doc,
|
|
46
|
+
props: autoDocs.includes('props') ? entry.doc.props : [],
|
|
47
|
+
slots: autoDocs.includes('slots') ? entry.doc.slots : [],
|
|
48
|
+
events: autoDocs.includes('events') ? entry.doc.events : [],
|
|
49
|
+
}
|
|
50
|
+
return doc.props.length || doc.slots.length || doc.events.length ? [{ ...entry, doc }] : []
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Above this many, a story's components after the first start collapsed: a compound story can name 27 parts (#1432). */
|
|
55
|
+
export const OPEN_PARTS = 3
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A compound component's root first, `SelectRoot` before `SelectTrigger`, else the order
|
|
59
|
+
* they came in. A story rendering two families, such as a Select inside a Calendar, opens
|
|
60
|
+
* the root its own folder or file is named for: `Calendar/story/CalendarSelect.story.vue`
|
|
61
|
+
* opens `CalendarRoot`, not the `SelectRoot` it imported first.
|
|
62
|
+
*/
|
|
63
|
+
export function rootFirst(entries: ComponentDocsEntry[], storyFile?: string): ComponentDocsEntry[] {
|
|
64
|
+
const roots = entries.filter(entry => entry.name.endsWith('Root'))
|
|
65
|
+
const family = (entry: ComponentDocsEntry) => entry.name.slice(0, -'Root'.length)
|
|
66
|
+
const segments = storyFile?.split(/[\\/]/) ?? []
|
|
67
|
+
const fileName = segments.at(-1) ?? ''
|
|
68
|
+
const root = roots.find(entry => segments.slice(0, -1).includes(family(entry)))
|
|
69
|
+
?? roots.find(entry => fileName.startsWith(family(entry)))
|
|
70
|
+
?? roots[0]
|
|
71
|
+
return root ? [root, ...entries.filter(entry => entry !== root)] : entries
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The `v-model` a prop and its `update:` event make together, as a template writes it:
|
|
76
|
+
* `v-model` for `modelValue`, `v-model:open` for `open` (#1436). Asked with either name,
|
|
77
|
+
* so the two rows of a pair, one under Props and one under Events, carry the same mark.
|
|
78
|
+
*/
|
|
79
|
+
export function modelLabel(doc: ComponentDoc, name: string): string | undefined {
|
|
80
|
+
const prop = name.startsWith('update:') ? name.slice('update:'.length) : name
|
|
81
|
+
const paired = doc.props.some(candidate => candidate.name === prop) && doc.events.some(event => event.name === `update:${prop}`)
|
|
82
|
+
if (!paired) {
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
return prop === 'modelValue' ? 'v-model' : `v-model:${prop}`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function tagText(tags: DocTag[], name: string): string | undefined {
|
|
89
|
+
const tag = tags.find(tag => tag.name === name)
|
|
90
|
+
return tag ? (tag.text ?? '') : undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A `@see` or `@link` points somewhere only when it carries a URL. */
|
|
94
|
+
export function tagHref(text: string): string | undefined {
|
|
95
|
+
return /https?:\/\/\S+/.exec(text)?.[0]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The components a story imports, documented. In dev they are asked of the server
|
|
100
|
+
* when this is first used, which is what starts extraction at all (#1159); in a
|
|
101
|
+
* built book they come from what `poveste build` extracted.
|
|
102
|
+
*/
|
|
103
|
+
export function useComponentDocs(storyId: Ref<string | undefined>) {
|
|
104
|
+
const components = ref<ComponentDocsEntry[]>()
|
|
105
|
+
// Asked and not yet answered: the first answer can take many seconds (#1400).
|
|
106
|
+
const pending = ref(false)
|
|
107
|
+
const off = ref<string>()
|
|
108
|
+
const hot = import.meta.hot
|
|
109
|
+
if (!hot) {
|
|
110
|
+
// The chunk loads on first use, and until then the tab said there were no docs (#1400).
|
|
111
|
+
watch(storyId, async (id, _, onCleanup) => {
|
|
112
|
+
// Another story opened while these loaded.
|
|
113
|
+
let stale = false
|
|
114
|
+
onCleanup(() => stale = true)
|
|
115
|
+
pending.value = true
|
|
116
|
+
try {
|
|
117
|
+
const { componentDocs } = await import('./component-docs-data')
|
|
118
|
+
const loaders = id ? componentDocs[id] : undefined
|
|
119
|
+
const docs = loaders && Object.fromEntries(await Promise.all(Object.entries(loaders).map(async ([file, load]) => [file, (await load()).default])))
|
|
120
|
+
if (!stale) {
|
|
121
|
+
components.value = id && docs ? toEntries({ storyId: id, components: docs }) : undefined
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
// A chunk the server no longer has, as in a tab left open across a deploy:
|
|
126
|
+
// no table, rather than a tab that waits forever.
|
|
127
|
+
if (!stale) {
|
|
128
|
+
components.value = undefined
|
|
129
|
+
console.error('[poveste] could not load this story\'s component docs:', error)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
if (!stale) {
|
|
134
|
+
pending.value = false
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}, { immediate: true })
|
|
138
|
+
return { components, pending, off }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function ask() {
|
|
142
|
+
if (storyId.value) {
|
|
143
|
+
pending.value = true
|
|
144
|
+
hot!.send('poveste:docgen-request', { storyId: storyId.value })
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function onResult(result: StoryDocsResult) {
|
|
148
|
+
if (result.storyId === storyId.value) {
|
|
149
|
+
components.value = toEntries(result)
|
|
150
|
+
off.value = result.off
|
|
151
|
+
pending.value = false
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
hot.on('poveste:docgen-result', onResult)
|
|
156
|
+
hot.on('poveste:docgen-stale', ask)
|
|
157
|
+
onBeforeUnmount(() => {
|
|
158
|
+
hot.off('poveste:docgen-result', onResult)
|
|
159
|
+
hot.off('poveste:docgen-stale', ask)
|
|
160
|
+
})
|
|
161
|
+
watch(storyId, () => {
|
|
162
|
+
components.value = undefined
|
|
163
|
+
off.value = undefined
|
|
164
|
+
pending.value = false
|
|
165
|
+
ask()
|
|
166
|
+
}, { immediate: true })
|
|
167
|
+
|
|
168
|
+
return { components, pending, off }
|
|
169
|
+
}
|
package/src/app/util/dark.ts
CHANGED
|
@@ -2,11 +2,17 @@ import { useDark, useToggle } from '@vueuse/core'
|
|
|
2
2
|
import { watch } from 'vue'
|
|
3
3
|
import { povesteConfig } from './config.js'
|
|
4
4
|
|
|
5
|
+
// A sandboxed story's realm reads the chrome's setting but shows the story preview's
|
|
6
|
+
// dark, which `sandbox.ts` puts on its `<html>` for the controls; applying this one
|
|
7
|
+
// there too would fight it (#1443).
|
|
8
|
+
const inSandbox = location.href.includes('__sandbox')
|
|
9
|
+
|
|
5
10
|
export const isDark = useDark({
|
|
6
11
|
valueDark: 'ptw-dark',
|
|
7
12
|
...povesteConfig.theme.defaultColorScheme ? { initialValue: povesteConfig.theme.defaultColorScheme } : {},
|
|
8
13
|
storageKey: 'poveste-color-scheme',
|
|
9
14
|
storage: povesteConfig.theme.storeColorScheme ? localStorage : sessionStorage,
|
|
15
|
+
...inSandbox ? { onChanged: () => {} } : {},
|
|
10
16
|
})
|
|
11
17
|
export const toggleDark = useToggle(isDark)
|
|
12
18
|
|
package/src/app/util/docs.ts
CHANGED
|
@@ -7,8 +7,15 @@ export type GenerateSourceCode = ClientSupportPlugin['generateSourceCode']
|
|
|
7
7
|
|
|
8
8
|
/** What the variant declares, or what its framework generates from it; `undefined` when neither has anything. */
|
|
9
9
|
export async function getDynamicSourceCode(variant: Variant, generate: GenerateSourceCode | undefined): Promise<string | undefined> {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// A Svelte story's `source` can be a function of the variant's state, so the
|
|
11
|
+
// panel follows the controls; handed on as it was, it reached the highlighter and
|
|
12
|
+
// threw (#1511).
|
|
13
|
+
const source: unknown = variant.source
|
|
14
|
+
if (typeof source === 'function') {
|
|
15
|
+
return source(variant.state) || undefined
|
|
16
|
+
}
|
|
17
|
+
if (source) {
|
|
18
|
+
return source as string
|
|
12
19
|
}
|
|
13
20
|
const sourceSlot = variant.slots?.().source
|
|
14
21
|
if (sourceSlot) {
|
|
@@ -18,6 +25,15 @@ export async function getDynamicSourceCode(variant: Variant, generate: GenerateS
|
|
|
18
25
|
return (await generate?.(variant)) || undefined
|
|
19
26
|
}
|
|
20
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The grammar a story's source reads in: a Vue or Svelte story is HTML with script
|
|
30
|
+
* inside, and a Solid or React story file is TypeScript with JSX, which the HTML
|
|
31
|
+
* grammar left plain (#1521). A variant's own source follows its story's.
|
|
32
|
+
*/
|
|
33
|
+
export function sourceLang(story: Story | undefined): 'html' | 'tsx' {
|
|
34
|
+
return /\.[cm]?[jt]sx$/.test(story?.file?.filePath ?? '') ? 'tsx' : 'html'
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
export async function getStaticSourceCode(story: Story): Promise<string | undefined> {
|
|
22
38
|
const sourceLoader = story.file?.source
|
|
23
39
|
return sourceLoader ? (await sourceLoader()).default : undefined
|
|
@@ -19,9 +19,10 @@ let highlighterPromise: Promise<HighlighterCore> | undefined
|
|
|
19
19
|
*/
|
|
20
20
|
export function getHighlighter(): Promise<HighlighterCore> {
|
|
21
21
|
highlighterPromise ??= createHighlighterCore({
|
|
22
|
+
// `tsx` reads JSX too, and a `.story.tsx` file's types besides (#1521).
|
|
22
23
|
langs: [
|
|
23
24
|
import('shiki/langs/html.mjs'),
|
|
24
|
-
import('shiki/langs/
|
|
25
|
+
import('shiki/langs/tsx.mjs'),
|
|
25
26
|
],
|
|
26
27
|
/*
|
|
27
28
|
* `github-light-high-contrast`, not `github-light`. The plain one paints
|