@poveste/app 0.20.0 → 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 +55 -27
- package/dist/bundled/components/layout/LayoutModal.vue_vue_type_script_setup_true_lang.js +44 -5
- package/dist/bundled/components/panel/ComponentDocs.js +1 -1
- package/dist/bundled/components/panel/ComponentDocs.vue_vue_type_script_setup_true_lang.js +72 -40
- package/dist/bundled/components/panel/StoryDocs.vue_vue_type_script_setup_true_lang.js +34 -10
- package/dist/bundled/components/panel/StorySourceCode.js +1 -1
- package/dist/bundled/components/panel/StorySourceCode.vue_vue_type_script_setup_true_lang.js +2 -2
- 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/index.js +5 -1
- package/dist/bundled/sandbox.js +2 -0
- package/dist/bundled/util/component-docs-data.js +5 -1
- package/dist/bundled/util/component-docs.js +90 -14
- package/dist/bundled/util/dark.js +3 -1
- package/dist/bundled/util/docs.js +12 -2
- package/dist/bundled/util/highlighter.js +1 -1
- package/dist/bundled/util/icons.generated.js +1 -0
- package/dist/bundled/util/keyboard.js +5 -4
- package/dist/style.css +317 -150
- package/package.json +7 -7
- package/src/app/components/layout/LayoutModal.vue +55 -2
- package/src/app/components/panel/ComponentDocs.vue +83 -17
- package/src/app/components/panel/StoryDocs.vue +30 -3
- package/src/app/components/panel/StorySourceCode.vue +2 -2
- 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/index.ts +12 -3
- package/src/app/sandbox.ts +5 -0
- package/src/app/util/component-docs-data.ts +6 -3
- package/src/app/util/component-docs.ts +102 -11
- 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 +1 -1
- package/src/app/util/keyboard.ts +8 -4
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poveste/app",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.21.0",
|
|
5
5
|
"description": "Pre-bundled UI for Poveste",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Sorin Gitlan",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"node": ">=22.22.2"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@poveste/controls": "^0.
|
|
40
|
-
"@poveste/shared": "^0.
|
|
41
|
-
"@poveste/vendors": "^0.
|
|
39
|
+
"@poveste/controls": "^0.21.0",
|
|
40
|
+
"@poveste/shared": "^0.21.0",
|
|
41
|
+
"@poveste/vendors": "^0.21.0",
|
|
42
42
|
"fuse.js": "^7.0.0",
|
|
43
43
|
"shiki": "^4.5.0"
|
|
44
44
|
},
|
|
@@ -61,13 +61,13 @@
|
|
|
61
61
|
"fs-extra": "^11.4.1",
|
|
62
62
|
"globby": "^16.2.4",
|
|
63
63
|
"pathe": "^2.0.3",
|
|
64
|
-
"pinia": "^4.0
|
|
65
|
-
"postcss": "^8.5.
|
|
64
|
+
"pinia": "^4.1.0",
|
|
65
|
+
"postcss": "^8.5.31",
|
|
66
66
|
"postcss-cli": "^12.0.0",
|
|
67
67
|
"tailwindcss": "^4.3.3",
|
|
68
68
|
"typescript": "6.0.3",
|
|
69
69
|
"vite": "^8.3.4",
|
|
70
|
-
"vitest": "^5.0.
|
|
70
|
+
"vitest": "^5.0.5",
|
|
71
71
|
"vue": "^3.5.44",
|
|
72
72
|
"vue-router": "^5.4.0",
|
|
73
73
|
"vue-tsc": "^3.3.13"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { Icon } from '@iconify/vue'
|
|
3
|
+
import { nextTick, ref, watch } from 'vue'
|
|
3
4
|
import { onKeyboardShortcut } from '../../util/keyboard'
|
|
4
5
|
import LayoutPane from './LayoutPane.vue'
|
|
5
6
|
|
|
@@ -23,6 +24,48 @@ onKeyboardShortcut(['escape'], () => {
|
|
|
23
24
|
close()
|
|
24
25
|
}
|
|
25
26
|
})
|
|
27
|
+
|
|
28
|
+
// A dialog to the keyboard as well: focus moves in when it opens, Tab stays inside,
|
|
29
|
+
// and closing hands focus back to whatever opened it, the layout chip or the page
|
|
30
|
+
// behind a shortcut. Focus used to stay on the chip, behind the pane (#1372).
|
|
31
|
+
const panel = ref<HTMLElement>()
|
|
32
|
+
let opener: HTMLElement | null = null
|
|
33
|
+
|
|
34
|
+
const FOCUSABLE = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
35
|
+
|
|
36
|
+
function focusables() {
|
|
37
|
+
return [...panel.value?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? []]
|
|
38
|
+
.filter(element => !element.hasAttribute('disabled') && element.getClientRects().length > 0)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
watch(() => props.shown, async (shown) => {
|
|
42
|
+
if (shown) {
|
|
43
|
+
opener = document.activeElement instanceof HTMLElement ? document.activeElement : null
|
|
44
|
+
await nextTick()
|
|
45
|
+
focusables()[0]?.focus()
|
|
46
|
+
}
|
|
47
|
+
else if (opener?.isConnected) {
|
|
48
|
+
opener.focus()
|
|
49
|
+
opener = null
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
function keepFocusInside(event: KeyboardEvent) {
|
|
54
|
+
const elements = focusables()
|
|
55
|
+
const first = elements[0]
|
|
56
|
+
const last = elements.at(-1)
|
|
57
|
+
if (!first || !last) {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
61
|
+
event.preventDefault()
|
|
62
|
+
last.focus()
|
|
63
|
+
}
|
|
64
|
+
else if (!event.shiftKey && document.activeElement === last) {
|
|
65
|
+
event.preventDefault()
|
|
66
|
+
first.focus()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
26
69
|
</script>
|
|
27
70
|
|
|
28
71
|
<template>
|
|
@@ -35,9 +78,19 @@ onKeyboardShortcut(['escape'], () => {
|
|
|
35
78
|
class="absolute inset-0"
|
|
36
79
|
@click="close()"
|
|
37
80
|
/>
|
|
38
|
-
<div
|
|
81
|
+
<div
|
|
82
|
+
ref="panel"
|
|
83
|
+
role="dialog"
|
|
84
|
+
aria-modal="true"
|
|
85
|
+
aria-labelledby="poveste-layout-modal-title"
|
|
86
|
+
class="bg-white dark:bg-gray-900 md:mt-16 md:mx-auto w-screen max-w-[512px] shadow-xl border border-gray-200 dark:border-gray-750 rounded-lg relative divide-y divide-gray-200 dark:divide-gray-850"
|
|
87
|
+
@keydown.tab="keepFocusInside"
|
|
88
|
+
>
|
|
39
89
|
<div class="flex items-center px-6 py-3 text-gray-900 dark:text-gray-100">
|
|
40
|
-
<h2
|
|
90
|
+
<h2
|
|
91
|
+
id="poveste-layout-modal-title"
|
|
92
|
+
class="flex-1 text-sm font-medium m-0"
|
|
93
|
+
>
|
|
41
94
|
Customize Layout
|
|
42
95
|
</h2>
|
|
43
96
|
<button
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import type { DocProp } from '@poveste/shared'
|
|
3
3
|
import type { ComponentDocsEntry } from '../../util/component-docs'
|
|
4
|
-
import {
|
|
4
|
+
import { computed } from 'vue'
|
|
5
|
+
import { modelLabel, OPEN_PARTS, rootFirst, tagHref, tagText } from '../../util/component-docs'
|
|
5
6
|
|
|
6
|
-
defineProps<{
|
|
7
|
+
const props = defineProps<{
|
|
7
8
|
components: ComponentDocsEntry[]
|
|
9
|
+
/** The story's file, which names the family whose root opens first. */
|
|
10
|
+
storyFile?: string | undefined
|
|
8
11
|
}>()
|
|
9
12
|
|
|
13
|
+
const ordered = computed(() => rootFirst(props.components, props.storyFile))
|
|
14
|
+
|
|
10
15
|
function shownDefault(prop: DocProp) {
|
|
11
16
|
return prop.default ?? prop.defaultTag
|
|
12
17
|
}
|
|
@@ -17,15 +22,22 @@ function shownDefault(prop: DocProp) {
|
|
|
17
22
|
class="poveste-component-docs"
|
|
18
23
|
data-testid="component-docs"
|
|
19
24
|
>
|
|
20
|
-
<
|
|
21
|
-
v-for="component of
|
|
25
|
+
<details
|
|
26
|
+
v-for="(component, index) of ordered"
|
|
22
27
|
:key="component.file"
|
|
28
|
+
:open="ordered.length <= OPEN_PARTS || index === 0"
|
|
23
29
|
data-slot="component"
|
|
24
30
|
:data-component="component.name"
|
|
25
31
|
>
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
<summary>
|
|
33
|
+
<h3 data-slot="component-name">
|
|
34
|
+
{{ component.name }}<code
|
|
35
|
+
v-if="component.doc?.generic"
|
|
36
|
+
title="The component's type parameters, as its script declares them"
|
|
37
|
+
data-slot="generic"
|
|
38
|
+
><{{ component.doc.generic }}></code>
|
|
39
|
+
</h3>
|
|
40
|
+
</summary>
|
|
29
41
|
|
|
30
42
|
<p
|
|
31
43
|
v-if="component.error"
|
|
@@ -61,6 +73,11 @@ function shownDefault(prop: DocProp) {
|
|
|
61
73
|
title="Required"
|
|
62
74
|
data-slot="required"
|
|
63
75
|
>*</span>
|
|
76
|
+
<code
|
|
77
|
+
v-if="modelLabel(component.doc, prop.name)"
|
|
78
|
+
:title="`Bound two ways with ${modelLabel(component.doc, prop.name)}, through the update:${prop.name} event`"
|
|
79
|
+
data-slot="model"
|
|
80
|
+
>{{ modelLabel(component.doc, prop.name) }}</code>
|
|
64
81
|
</td>
|
|
65
82
|
<td><code>{{ prop.type }}</code></td>
|
|
66
83
|
<td>
|
|
@@ -114,15 +131,26 @@ function shownDefault(prop: DocProp) {
|
|
|
114
131
|
</tr>
|
|
115
132
|
</thead>
|
|
116
133
|
<tbody>
|
|
117
|
-
<
|
|
134
|
+
<template
|
|
118
135
|
v-for="slot of component.doc.slots"
|
|
119
136
|
:key="slot.name"
|
|
120
|
-
:data-slot-name="slot.name"
|
|
121
137
|
>
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
138
|
+
<tr :data-slot-name="slot.name">
|
|
139
|
+
<td><code>{{ slot.name }}</code></td>
|
|
140
|
+
<!-- Its props are listed below, one row each; the type string would only repeat them. -->
|
|
141
|
+
<td><code v-if="slot.type && !slot.props?.length">{{ slot.type }}</code></td>
|
|
142
|
+
<td>{{ slot.description }}</td>
|
|
143
|
+
</tr>
|
|
144
|
+
<tr
|
|
145
|
+
v-for="slotProp of slot.props ?? []"
|
|
146
|
+
:key="`${slot.name}.${slotProp.name}`"
|
|
147
|
+
:data-slot-prop="slotProp.name"
|
|
148
|
+
>
|
|
149
|
+
<td><code>{{ slotProp.name }}</code></td>
|
|
150
|
+
<td><code>{{ slotProp.type }}</code></td>
|
|
151
|
+
<td>{{ slotProp.description }}</td>
|
|
152
|
+
</tr>
|
|
153
|
+
</template>
|
|
126
154
|
</tbody>
|
|
127
155
|
</table>
|
|
128
156
|
|
|
@@ -143,14 +171,21 @@ function shownDefault(prop: DocProp) {
|
|
|
143
171
|
:key="event.name"
|
|
144
172
|
:data-event="event.name"
|
|
145
173
|
>
|
|
146
|
-
<td
|
|
174
|
+
<td>
|
|
175
|
+
<code>{{ event.name }}</code>
|
|
176
|
+
<code
|
|
177
|
+
v-if="modelLabel(component.doc, event.name)"
|
|
178
|
+
:title="`Bound two ways with ${modelLabel(component.doc, event.name)}, with the ${event.name.slice('update:'.length)} prop`"
|
|
179
|
+
data-slot="model"
|
|
180
|
+
>{{ modelLabel(component.doc, event.name) }}</code>
|
|
181
|
+
</td>
|
|
147
182
|
<td><code v-if="event.type">{{ event.type }}</code></td>
|
|
148
183
|
<td>{{ event.description }}</td>
|
|
149
184
|
</tr>
|
|
150
185
|
</tbody>
|
|
151
186
|
</table>
|
|
152
187
|
</template>
|
|
153
|
-
</
|
|
188
|
+
</details>
|
|
154
189
|
</div>
|
|
155
190
|
</template>
|
|
156
191
|
|
|
@@ -166,14 +201,19 @@ function shownDefault(prop: DocProp) {
|
|
|
166
201
|
}
|
|
167
202
|
}
|
|
168
203
|
|
|
169
|
-
|
|
204
|
+
details + details {
|
|
170
205
|
margin-top: 1.5rem;
|
|
171
206
|
}
|
|
172
207
|
|
|
208
|
+
summary {
|
|
209
|
+
cursor: pointer;
|
|
210
|
+
margin-bottom: .5rem;
|
|
211
|
+
}
|
|
212
|
+
|
|
173
213
|
h3 {
|
|
214
|
+
display: inline;
|
|
174
215
|
font-weight: 600;
|
|
175
216
|
font-size: 1rem;
|
|
176
|
-
margin-bottom: .5rem;
|
|
177
217
|
}
|
|
178
218
|
|
|
179
219
|
table {
|
|
@@ -221,11 +261,37 @@ pre code {
|
|
|
221
261
|
text-decoration: line-through;
|
|
222
262
|
}
|
|
223
263
|
|
|
264
|
+
/* Its own line: a constraint wrapped beside the name broke mid-phrase in the narrow panel. */
|
|
265
|
+
/* A prop a slot passes its content, under the slot it belongs to. */
|
|
266
|
+
[data-slot-prop] td:first-child {
|
|
267
|
+
padding-inline-start: 1rem;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
[data-slot='generic'] {
|
|
271
|
+
display: block;
|
|
272
|
+
width: fit-content;
|
|
273
|
+
margin-top: .25rem;
|
|
274
|
+
font-size: .75rem;
|
|
275
|
+
font-weight: 400;
|
|
276
|
+
cursor: help;
|
|
277
|
+
}
|
|
278
|
+
|
|
224
279
|
[data-slot='required'] {
|
|
225
280
|
color: var(--color-red-500);
|
|
226
281
|
margin-inline-start: .125rem;
|
|
227
282
|
}
|
|
228
283
|
|
|
284
|
+
[data-slot='model'] {
|
|
285
|
+
display: inline-block;
|
|
286
|
+
margin-inline-start: .375rem;
|
|
287
|
+
color: var(--color-primary-600);
|
|
288
|
+
cursor: help;
|
|
289
|
+
|
|
290
|
+
&:where(.ptw-dark, .ptw-dark *) {
|
|
291
|
+
color: var(--color-primary-400);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
229
295
|
[data-slot='deprecated'],
|
|
230
296
|
[data-slot='default-conflict'] {
|
|
231
297
|
color: var(--color-amber-600);
|
|
@@ -5,7 +5,7 @@ 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
|
+
import { onlySections, useComponentDocs } from '../../util/component-docs.js'
|
|
9
9
|
import { povesteConfig } from '../../util/config.js'
|
|
10
10
|
import BaseEmpty from '../base/BaseEmpty.vue'
|
|
11
11
|
import DevOnlyToolbarOpenInEditor from '../toolbar/DevOnlyToolbarOpenInEditor.vue'
|
|
@@ -79,7 +79,8 @@ const { story } = toRefs(props)
|
|
|
79
79
|
const { renderedDoc } = useStoryDoc(story)
|
|
80
80
|
|
|
81
81
|
// Opening the docs is what starts auto-docs extraction (#1159).
|
|
82
|
-
const { components } = useComponentDocs(computed(() => story.value?.id))
|
|
82
|
+
const { components: allComponents, pending, off } = useComponentDocs(computed(() => story.value?.id))
|
|
83
|
+
const components = computed(() => allComponents.value && onlySections(allComponents.value, story.value?.autoDocs))
|
|
83
84
|
|
|
84
85
|
// Markdown links to other stories
|
|
85
86
|
const router = useRouter()
|
|
@@ -189,8 +190,33 @@ const filePath = computed(() => story.value.file?.docsFilePath ?? (props.standal
|
|
|
189
190
|
/>
|
|
190
191
|
</div>
|
|
191
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
|
+
-->
|
|
192
197
|
<BaseEmpty
|
|
193
|
-
v-if="!renderedDoc && !components?.length"
|
|
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>
|
|
218
|
+
<BaseEmpty
|
|
219
|
+
v-else-if="!pending && !renderedDoc && !components?.length"
|
|
194
220
|
>
|
|
195
221
|
<Icon
|
|
196
222
|
icon="carbon:document-unknown"
|
|
@@ -208,6 +234,7 @@ const filePath = computed(() => story.value.file?.docsFilePath ?? (props.standal
|
|
|
208
234
|
<ComponentDocs
|
|
209
235
|
v-if="components?.length"
|
|
210
236
|
:components="components"
|
|
237
|
+
:story-file="story.file?.filePath"
|
|
211
238
|
/>
|
|
212
239
|
</div>
|
|
213
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
|
|
|
@@ -112,7 +112,7 @@ onMounted(async () => {
|
|
|
112
112
|
|
|
113
113
|
const sourceHtml = computed(() => displayedSourceCode.value
|
|
114
114
|
? highlighter.value?.codeToHtml(displayedSourceCode.value, {
|
|
115
|
-
lang:
|
|
115
|
+
lang: sourceLang(props.story),
|
|
116
116
|
theme: isDark.value ? 'github-dark' : 'github-light-high-contrast',
|
|
117
117
|
})
|
|
118
118
|
: '')
|
|
@@ -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
|
})
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { ComponentDoc, DocTag } from '@poveste/shared'
|
|
1
|
+
import type { AutoDocsSection, ComponentDoc, DocTag } from '@poveste/shared'
|
|
2
2
|
import type { Ref } from 'vue'
|
|
3
3
|
import { onBeforeUnmount, ref, watch } from 'vue'
|
|
4
4
|
|
|
5
5
|
export interface ComponentDocsEntry {
|
|
6
6
|
/** Relative to the book's root. */
|
|
7
7
|
file: string
|
|
8
|
-
/** The file name without its extension. */
|
|
8
|
+
/** The component's own name, else the file name without its extension. */
|
|
9
9
|
name: string
|
|
10
10
|
doc?: ComponentDoc
|
|
11
11
|
error?: string
|
|
@@ -13,18 +13,78 @@ export interface ComponentDocsEntry {
|
|
|
13
13
|
|
|
14
14
|
interface StoryDocsResult {
|
|
15
15
|
storyId: string
|
|
16
|
-
components: Record<string, { doc?: ComponentDoc, error?: 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
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export function toEntries(result: StoryDocsResult): ComponentDocsEntry[] {
|
|
20
|
-
return Object.entries(result.components).map(([file, { doc, error }]) => ({
|
|
22
|
+
return Object.entries(result.components).map(([file, { doc, error, name }]) => ({
|
|
21
23
|
file,
|
|
22
|
-
|
|
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)?\.[^.]+$/, ''),
|
|
23
27
|
...doc ? { doc } : {},
|
|
24
28
|
...error ? { error } : {},
|
|
25
29
|
}))
|
|
26
30
|
}
|
|
27
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
|
+
|
|
28
88
|
export function tagText(tags: DocTag[], name: string): string | undefined {
|
|
29
89
|
const tag = tags.find(tag => tag.name === name)
|
|
30
90
|
return tag ? (tag.text ?? '') : undefined
|
|
@@ -42,24 +102,53 @@ export function tagHref(text: string): string | undefined {
|
|
|
42
102
|
*/
|
|
43
103
|
export function useComponentDocs(storyId: Ref<string | undefined>) {
|
|
44
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>()
|
|
45
108
|
const hot = import.meta.hot
|
|
46
109
|
if (!hot) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
}
|
|
51
137
|
}, { immediate: true })
|
|
52
|
-
return { components }
|
|
138
|
+
return { components, pending, off }
|
|
53
139
|
}
|
|
54
140
|
|
|
55
141
|
function ask() {
|
|
56
142
|
if (storyId.value) {
|
|
143
|
+
pending.value = true
|
|
57
144
|
hot!.send('poveste:docgen-request', { storyId: storyId.value })
|
|
58
145
|
}
|
|
59
146
|
}
|
|
60
147
|
function onResult(result: StoryDocsResult) {
|
|
61
148
|
if (result.storyId === storyId.value) {
|
|
62
149
|
components.value = toEntries(result)
|
|
150
|
+
off.value = result.off
|
|
151
|
+
pending.value = false
|
|
63
152
|
}
|
|
64
153
|
}
|
|
65
154
|
|
|
@@ -71,8 +160,10 @@ export function useComponentDocs(storyId: Ref<string | undefined>) {
|
|
|
71
160
|
})
|
|
72
161
|
watch(storyId, () => {
|
|
73
162
|
components.value = undefined
|
|
163
|
+
off.value = undefined
|
|
164
|
+
pending.value = false
|
|
74
165
|
ask()
|
|
75
166
|
}, { immediate: true })
|
|
76
167
|
|
|
77
|
-
return { components }
|
|
168
|
+
return { components, pending, off }
|
|
78
169
|
}
|