@poveste/app 0.13.1 → 0.15.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.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/bundled/app.css +31 -31
- package/dist/bundled/components/HomeView.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/bundled/components/app/AppLogo.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/bundled/components/app/Breadcrumb.js +1 -1
- package/dist/bundled/components/app/Breadcrumb.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/bundled/components/app/TopBar.vue_vue_type_script_setup_true_lang.js +4 -3
- package/dist/bundled/components/app/TopBarChipItem.js +1 -1
- package/dist/bundled/components/base/BaseCheckbox.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/bundled/components/base/BaseEmpty.js +2 -2
- package/dist/bundled/components/base/BaseOverflowMenu.vue_vue_type_script_setup_true_lang.js +18 -15
- package/dist/bundled/components/base/BaseSelect.vue_vue_type_script_setup_true_lang.js +158 -26
- package/dist/bundled/components/base/BaseSplitPane.js +1 -1
- package/dist/bundled/components/base/BaseSplitPane.vue_vue_type_script_setup_true_lang.js +6 -4
- package/dist/bundled/components/command/CommandPrompts.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/bundled/components/command/CommandPromptsModal.vue_vue_type_script_setup_true_lang.js +4 -3
- package/dist/bundled/components/command/PromptSelect.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/bundled/components/command/PromptText.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/bundled/components/panel/ControlsComponentPropItem.vue_vue_type_script_setup_true_lang.js +20 -14
- package/dist/bundled/components/panel/PaneTabs.vue_vue_type_script_setup_true_lang.js +6 -6
- package/dist/bundled/components/panel/StatePresets.vue_vue_type_script_setup_true_lang.js +94 -55
- package/dist/bundled/components/panel/StoryControls.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/bundled/components/panel/StoryDocs.vue_vue_type_script_setup_true_lang.js +3 -1
- package/dist/bundled/components/panel/StoryEvent.vue_vue_type_script_setup_true_lang.js +5 -1
- package/dist/bundled/components/panel/StoryEvents.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/bundled/components/panel/StorySidePanel.vue_vue_type_script_setup_true_lang.js +8 -6
- package/dist/bundled/components/panel/StorySourceCode.js +1 -1
- package/dist/bundled/components/panel/StorySourceCode.vue_vue_type_script_setup_true_lang.js +8 -6
- package/dist/bundled/components/search/SearchItemContent.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/bundled/components/search/SearchPane.vue_vue_type_script_setup_true_lang.js +16 -9
- package/dist/bundled/components/story/GenericMountStory.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/bundled/components/story/GenericRenderStory.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/bundled/components/story/StoryResponsivePreview.js +1 -1
- package/dist/bundled/components/story/StoryResponsivePreview.vue_vue_type_script_setup_true_lang.js +23 -8
- package/dist/bundled/components/story/StoryVariantGrid.js +1 -1
- package/dist/bundled/components/story/StoryVariantGrid.vue_vue_type_script_setup_true_lang.js +15 -10
- package/dist/bundled/components/story/StoryVariantGridItem.js +1 -1
- package/dist/bundled/components/story/StoryVariantGridItem.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/bundled/components/story/StoryVariantSinglePreviewRemote.vue_vue_type_script_setup_true_lang.js +9 -6
- package/dist/bundled/components/story/StoryView.vue_vue_type_script_setup_true_lang.js +7 -3
- package/dist/bundled/components/story/StoryViewer.js +1 -1
- package/dist/bundled/components/story/StoryViewer.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/bundled/components/toolbar/ToolbarBackground.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/bundled/components/toolbar/ToolbarResponsiveSize.vue_vue_type_script_setup_true_lang.js +17 -16
- package/dist/bundled/components/toolbar/ToolbarTitle.js +1 -1
- package/dist/bundled/components/toolbar/ToolbarTitle.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/bundled/components/tree/StoryGroup.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/bundled/components/tree/StoryList.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/bundled/components/tree/StoryListFolder.js +1 -1
- package/dist/bundled/components/tree/StoryListFolder.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/bundled/components/tree/StoryListItem.js +1 -1
- package/dist/bundled/components/tree/StoryListItem.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/bundled/plugin.js +4 -3
- package/dist/bundled/sandbox.js +5 -4
- package/dist/bundled/stores/folder.js +1 -1
- package/dist/bundled/stores/story.js +2 -2
- package/dist/bundled/util/commands.js +21 -10
- package/dist/bundled/util/dark.js +1 -1
- package/dist/bundled/util/docs.js +2 -1
- package/dist/bundled/util/events.js +1 -1
- package/dist/bundled/util/grid-slots.js +2 -2
- package/dist/bundled/util/icons.generated.js +0 -1
- package/dist/bundled/util/keyboard.js +9 -14
- package/dist/bundled/util/mapping.js +3 -3
- package/dist/bundled/util/preview-settings.js +1 -1
- package/dist/bundled/util/render-tracker.js +33 -0
- package/dist/bundled/util/variant.js +12 -2
- package/dist/style.css +23 -1
- package/package.json +14 -18
- package/src/__tests__/render-tracker.spec.ts +38 -0
- package/src/app/App.vue +7 -4
- package/src/app/components/HomeView.vue +1 -1
- package/src/app/components/app/AppLogo.vue +6 -2
- package/src/app/components/app/Breadcrumb.vue +2 -1
- package/src/app/components/app/TopBar.vue +7 -2
- package/src/app/components/app/TopBarChipItem.vue +1 -1
- package/src/app/components/base/BaseCheckbox.vue +1 -1
- package/src/app/components/base/BaseEmpty.vue +1 -1
- package/src/app/components/base/BaseListItemLink.vue +4 -2
- package/src/app/components/base/BaseOverflowMenu.vue +25 -18
- package/src/app/components/base/BaseSelect.vue +171 -15
- package/src/app/components/base/BaseSplitPane.vue +11 -6
- package/src/app/components/command/CommandPrompts.vue +1 -1
- package/src/app/components/command/CommandPromptsModal.vue +1 -0
- package/src/app/components/command/PromptSelect.vue +3 -2
- package/src/app/components/command/PromptText.vue +7 -3
- package/src/app/components/panel/ControlsComponentPropItem.vue +26 -15
- package/src/app/components/panel/PaneTabs.vue +6 -6
- package/src/app/components/panel/StatePresets.vue +125 -68
- package/src/app/components/panel/StoryControls.vue +1 -1
- package/src/app/components/panel/StoryDocs.vue +3 -1
- package/src/app/components/panel/StoryEvent.vue +7 -2
- package/src/app/components/panel/StoryEvents.vue +4 -1
- package/src/app/components/panel/StorySidePanel.vue +3 -1
- package/src/app/components/panel/StorySourceCode.vue +12 -8
- package/src/app/components/search/SearchItemContent.vue +2 -1
- package/src/app/components/search/SearchPane.vue +16 -8
- package/src/app/components/story/GenericMountStory.vue +2 -1
- package/src/app/components/story/GenericRenderStory.vue +2 -1
- package/src/app/components/story/StoryResponsivePreview.vue +36 -7
- package/src/app/components/story/StoryVariantGrid.vue +33 -16
- package/src/app/components/story/StoryVariantGridItem.vue +1 -1
- package/src/app/components/story/StoryVariantSinglePreviewRemote.vue +14 -8
- package/src/app/components/story/StoryView.vue +10 -4
- package/src/app/components/story/StoryViewer.vue +3 -3
- package/src/app/components/toolbar/ToolbarBackground.vue +1 -1
- package/src/app/components/toolbar/ToolbarResponsiveSize.vue +23 -3
- package/src/app/components/toolbar/ToolbarTitle.vue +2 -2
- package/src/app/components/tree/StoryGroup.vue +2 -2
- package/src/app/components/tree/StoryList.vue +2 -2
- package/src/app/components/tree/StoryListFolder.vue +2 -2
- package/src/app/components/tree/StoryListItem.vue +1 -1
- package/src/app/plugin.ts +7 -3
- package/src/app/sandbox.ts +9 -3
- package/src/app/stores/folder.ts +1 -1
- package/src/app/stores/story.ts +2 -2
- package/src/app/types.ts +6 -6
- package/src/app/util/commands.ts +27 -17
- package/src/app/util/dark.ts +1 -1
- package/src/app/util/docs.ts +2 -1
- package/src/app/util/env.ts +0 -1
- package/src/app/util/events.ts +6 -5
- package/src/app/util/grid-slots.ts +2 -2
- package/src/app/util/icons.generated.ts +1 -1
- package/src/app/util/keyboard.ts +10 -11
- package/src/app/util/mapping.ts +3 -3
- package/src/app/util/preview-settings.ts +1 -1
- package/src/app/util/render-tracker.ts +31 -0
- package/src/app/util/scroll.ts +5 -1
- package/src/app/util/state.ts +4 -4
- package/src/app/util/variant.ts +16 -3
- package/src/app/components/base/BaseCopyIcon.vue +0 -34
|
@@ -8,12 +8,19 @@ import StoryVariantGridItem from './StoryVariantGridItem.vue'
|
|
|
8
8
|
|
|
9
9
|
const storyStore = useStoryStore()
|
|
10
10
|
|
|
11
|
+
// `currentStory` is a `.find()` over the loaded stories, so it is undefined for
|
|
12
|
+
// a route naming a story that is not there, and `layout` is optional on top of
|
|
13
|
+
// that. `StoryView` only mounts this component inside its `v-else` on the same
|
|
14
|
+
// value, so neither is ever absent here — but that guarantee lives in another
|
|
15
|
+
// component's template and cannot be seen from this one, which is why these
|
|
16
|
+
// read through `?.` rather than an assertion.
|
|
11
17
|
const gridTemplateWidth = computed(() => {
|
|
12
|
-
|
|
18
|
+
const layout = storyStore.currentStory?.layout
|
|
19
|
+
if (layout?.type !== 'grid') {
|
|
13
20
|
return
|
|
14
21
|
}
|
|
15
22
|
|
|
16
|
-
const layoutWidth =
|
|
23
|
+
const layoutWidth = layout.width
|
|
17
24
|
|
|
18
25
|
if (!layoutWidth) {
|
|
19
26
|
return '200px'
|
|
@@ -38,11 +45,11 @@ const initialCount = 10
|
|
|
38
45
|
|
|
39
46
|
const maxItemHeight = ref(0)
|
|
40
47
|
|
|
41
|
-
const el = ref<HTMLDivElement>(
|
|
48
|
+
const el = ref<HTMLDivElement>()
|
|
42
49
|
const scrollTop = ref(0)
|
|
43
50
|
const viewportHeight = ref(0)
|
|
44
51
|
|
|
45
|
-
const variants = computed(() => storyStore.currentStory
|
|
52
|
+
const variants = computed(() => storyStore.currentStory?.variants ?? [])
|
|
46
53
|
|
|
47
54
|
useResizeObserver(el, () => {
|
|
48
55
|
updateWindow()
|
|
@@ -103,7 +110,7 @@ function onItemResize(_w: number, h: number) {
|
|
|
103
110
|
|
|
104
111
|
// Grid size
|
|
105
112
|
|
|
106
|
-
const gridEl = ref<HTMLDivElement>(
|
|
113
|
+
const gridEl = ref<HTMLDivElement>()
|
|
107
114
|
const gridColumnWidth = ref(1)
|
|
108
115
|
const viewWidth = ref(1)
|
|
109
116
|
|
|
@@ -113,8 +120,12 @@ function updateSize() {
|
|
|
113
120
|
|
|
114
121
|
if (!gridEl.value) return
|
|
115
122
|
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
// Undefined when the story is not a grid, which is when this never runs.
|
|
124
|
+
const templateWidth = gridTemplateWidth.value
|
|
125
|
+
if (!templateWidth) return
|
|
126
|
+
|
|
127
|
+
if (templateWidth.endsWith('%')) {
|
|
128
|
+
gridColumnWidth.value = viewWidth.value * Number.parseInt(templateWidth) / 100 - gap
|
|
118
129
|
}
|
|
119
130
|
else {
|
|
120
131
|
gridColumnWidth.value = Number.parseInt(gridTemplateWidth.value)
|
|
@@ -184,7 +195,9 @@ function scheduleTrim() {
|
|
|
184
195
|
}
|
|
185
196
|
|
|
186
197
|
watch(visibleVariants, (visible) => {
|
|
187
|
-
|
|
198
|
+
const story = storyStore.currentStory
|
|
199
|
+
if (!story) return
|
|
200
|
+
slots.value = assignSlots(slots.value, story.id, visible)
|
|
188
201
|
scheduleTrim()
|
|
189
202
|
}, { immediate: true })
|
|
190
203
|
|
|
@@ -241,14 +254,18 @@ watch(() => storyStore.currentVariant, (variant) => {
|
|
|
241
254
|
transform: offsetY ? `translateY(${offsetY}px)` : undefined,
|
|
242
255
|
}"
|
|
243
256
|
>
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
257
|
+
<!-- A `template`, so the guard adds no element: `gridEl` above is
|
|
258
|
+
measured by a resize observer and must not move. -->
|
|
259
|
+
<template v-if="storyStore.currentStory">
|
|
260
|
+
<StoryVariantGridItem
|
|
261
|
+
v-for="(slot, index) of slots"
|
|
262
|
+
:key="index"
|
|
263
|
+
:variant="slot.variant"
|
|
264
|
+
:story="storyStore.currentStory"
|
|
265
|
+
:style="{ order: slot.order, display: slot.visible ? undefined : 'none' }"
|
|
266
|
+
@resize="onItemResize"
|
|
267
|
+
/>
|
|
268
|
+
</template>
|
|
252
269
|
</div>
|
|
253
270
|
</div>
|
|
254
271
|
</div>
|
|
@@ -132,7 +132,7 @@ const useIframe = computed(() => {
|
|
|
132
132
|
</RouterLink>
|
|
133
133
|
|
|
134
134
|
<!-- Toolbar -->
|
|
135
|
-
<div class="flex-none ml-auto hidden group-hover:flex items-center">
|
|
135
|
+
<div class="flex-none ml-auto hidden group-hover:flex group-focus-within:flex items-center">
|
|
136
136
|
<HstCopyIcon
|
|
137
137
|
:content="() => getSourceCode(story, variant)"
|
|
138
138
|
/>
|
|
@@ -12,6 +12,7 @@ import { useStoryErrorStore } from '../../stores/story-errors'
|
|
|
12
12
|
import { EVENT_SEND, PREVIEW_SETTINGS_REQUEST, PREVIEW_SETTINGS_SYNC, SANDBOX_HEIGHT, SANDBOX_READY, SANDBOX_RETARGET, STATE_SYNC } from '../../util/const'
|
|
13
13
|
import { firstReportedHeight } from '../../util/grid-cell-height'
|
|
14
14
|
import { trackWindow } from '../../util/keyboard'
|
|
15
|
+
import { createRenderTracker } from '../../util/render-tracker'
|
|
15
16
|
import { getSandboxUrl } from '../../util/sandbox'
|
|
16
17
|
import { toRawDeep } from '../../util/state'
|
|
17
18
|
import { createStateBridge } from '../../util/state-bridge'
|
|
@@ -31,11 +32,7 @@ const errorStore = useStoryErrorStore()
|
|
|
31
32
|
|
|
32
33
|
const storyError = computed(() => errorStore.forVariant(props.story.id, props.variant.id))
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
// render that produced it — otherwise a fixed story stays marked until reload.
|
|
36
|
-
watch(() => [props.story.id, props.variant.id], () => {
|
|
37
|
-
errorStore.clear(props.story.id, props.variant.id)
|
|
38
|
-
})
|
|
35
|
+
const render = createRenderTracker()
|
|
39
36
|
|
|
40
37
|
// Iframe
|
|
41
38
|
|
|
@@ -103,6 +100,8 @@ function fromCurrentOccupant(data: { storyId?: string, variantId?: string }) {
|
|
|
103
100
|
useEventListener(window, 'message', (event) => {
|
|
104
101
|
// With many grid iframes mounted, every sandbox postMessage hits every
|
|
105
102
|
// parent listener. Skip events that didn't originate from this iframe.
|
|
103
|
+
// Paired with the origin check in `sandbox.ts`, which guards the other
|
|
104
|
+
// direction (#379).
|
|
106
105
|
if (event.source !== iframe.value?.contentWindow) return
|
|
107
106
|
switch (event.data?.type) {
|
|
108
107
|
case STATE_SYNC:
|
|
@@ -115,10 +114,15 @@ useEventListener(window, 'message', (event) => {
|
|
|
115
114
|
break
|
|
116
115
|
case SANDBOX_READY:
|
|
117
116
|
if (!fromCurrentOccupant(event.data)) break
|
|
117
|
+
// Last render wins, which is what makes a fixed story stop being marked
|
|
118
|
+
// without a reload. Clearing on the id change instead cleared the pair
|
|
119
|
+
// being navigated *to*, so arriving at a broken story hid its error (#597).
|
|
120
|
+
if (render.wasClean()) errorStore.clear(props.story.id, props.variant.id)
|
|
118
121
|
setPreviewReady()
|
|
119
122
|
break
|
|
120
123
|
case SANDBOX_ERROR:
|
|
121
124
|
if (!fromCurrentOccupant(event.data)) break
|
|
125
|
+
render.threw()
|
|
122
126
|
errorStore.report({
|
|
123
127
|
message: event.data.message,
|
|
124
128
|
stack: event.data.stack,
|
|
@@ -207,6 +211,7 @@ let stopTrackKeyboard: (() => void) | undefined
|
|
|
207
211
|
let unmounted = false
|
|
208
212
|
|
|
209
213
|
function reload(url: string) {
|
|
214
|
+
render.begin()
|
|
210
215
|
retargets = 0
|
|
211
216
|
retargeting.value = false
|
|
212
217
|
clearTimeout(retargetTimer)
|
|
@@ -223,6 +228,7 @@ function reload(url: string) {
|
|
|
223
228
|
}
|
|
224
229
|
|
|
225
230
|
watch(sandboxUrl, (url) => {
|
|
231
|
+
render.begin()
|
|
226
232
|
// A new occupant has agreed to nothing, so neither has this end. Kept as a
|
|
227
233
|
// new bridge rather than a reset so there is one way to be in that state.
|
|
228
234
|
bridge = createStateBridge(postState)
|
|
@@ -285,12 +291,12 @@ function previewStyle(isResponsiveEnabled: boolean, finalWidth: number | null, f
|
|
|
285
291
|
const style: Record<string, string> = {}
|
|
286
292
|
|
|
287
293
|
if (isResponsiveEnabled) {
|
|
288
|
-
if (finalWidth) style
|
|
289
|
-
if (finalHeight) style
|
|
294
|
+
if (finalWidth) style['width'] = `${finalWidth}px`
|
|
295
|
+
if (finalHeight) style['height'] = `${finalHeight}px`
|
|
290
296
|
}
|
|
291
297
|
|
|
292
298
|
if (props.autoHeight) {
|
|
293
|
-
style
|
|
299
|
+
style['height'] = `${Math.min(reportedHeight.value ?? placeholderHeight.value, MAX_AUTO_HEIGHT)}px`
|
|
294
300
|
}
|
|
295
301
|
|
|
296
302
|
return style
|
|
@@ -33,8 +33,12 @@ const route = useRoute()
|
|
|
33
33
|
// Restore variant selection
|
|
34
34
|
|
|
35
35
|
watch(() => storyStore.currentVariant, (value) => {
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// A current variant is found *inside* the current story, so one implies the
|
|
37
|
+
// other — but that is a fact about two computeds in the store, not something
|
|
38
|
+
// the compiler can see from here.
|
|
39
|
+
const story = storyStore.currentStory
|
|
40
|
+
if (value && story) {
|
|
41
|
+
story.lastSelectedVariant = value
|
|
38
42
|
}
|
|
39
43
|
}, {
|
|
40
44
|
immediate: true,
|
|
@@ -43,7 +47,9 @@ watch(() => storyStore.currentVariant, (value) => {
|
|
|
43
47
|
watch(() => [storyStore.currentStory, storyStore.currentVariant], () => {
|
|
44
48
|
if (!storyStore.currentVariant && autoSelectsVariant(storyStore.currentStory)) {
|
|
45
49
|
const variant = storyStore.currentStory.lastSelectedVariant ?? storyStore.currentStory.variants[0]
|
|
46
|
-
|
|
50
|
+
if (variant) {
|
|
51
|
+
setVariant(variant.id)
|
|
52
|
+
}
|
|
47
53
|
}
|
|
48
54
|
}, {
|
|
49
55
|
immediate: true,
|
|
@@ -61,7 +67,7 @@ function setVariant(variantId: string) {
|
|
|
61
67
|
|
|
62
68
|
// Docs auto-scroll to top
|
|
63
69
|
|
|
64
|
-
const docsOnlyScroller = ref<HTMLElement>(
|
|
70
|
+
const docsOnlyScroller = ref<HTMLElement>()
|
|
65
71
|
|
|
66
72
|
function scrollDocsToTop() {
|
|
67
73
|
docsOnlyScroller.value?.scrollTo(0, 0)
|
|
@@ -24,10 +24,10 @@ watch(variant, () => {
|
|
|
24
24
|
<template>
|
|
25
25
|
<div class="poveste-story-viewer bg-gray-50 h-full dark:bg-gray-750">
|
|
26
26
|
<StoryVariantGrid
|
|
27
|
-
v-if="storyStore.currentStory
|
|
27
|
+
v-if="storyStore.currentStory?.layout?.type === 'grid'"
|
|
28
28
|
/>
|
|
29
29
|
<StoryVariantSingle
|
|
30
|
-
v-else-if="storyStore.currentStory
|
|
30
|
+
v-else-if="storyStore.currentStory?.layout?.type === 'single'"
|
|
31
31
|
@open-variant-menu="isMenuOpened = true"
|
|
32
32
|
/>
|
|
33
33
|
</div>
|
|
@@ -38,7 +38,7 @@ watch(variant, () => {
|
|
|
38
38
|
@close="closeMenu"
|
|
39
39
|
>
|
|
40
40
|
<StoryVariantListItem
|
|
41
|
-
v-for="(v, index) of storyStore.currentStory
|
|
41
|
+
v-for="(v, index) of storyStore.currentStory?.variants ?? []"
|
|
42
42
|
:key="index"
|
|
43
43
|
:variant="v"
|
|
44
44
|
/>
|
|
@@ -17,7 +17,7 @@ const colorSchemeOptions: { value: SandboxColorScheme, label: string, icon: stri
|
|
|
17
17
|
// A book that hides the app color scheme switch doesn't want the preview one
|
|
18
18
|
// either — it keeps whatever `theme.defaultColorScheme` says.
|
|
19
19
|
const showColorScheme = !povesteConfig.theme.hideColorSchemeSwitch
|
|
20
|
-
const showDropdown = showColorScheme || !!povesteConfig.backgroundPresets
|
|
20
|
+
const showDropdown = showColorScheme || !!povesteConfig.backgroundPresets?.length
|
|
21
21
|
</script>
|
|
22
22
|
|
|
23
23
|
<template>
|
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import { Icon } from '@iconify/vue'
|
|
3
|
+
import { computed } from 'vue'
|
|
3
4
|
import { usePreviewSettingsStore } from '../../stores/preview-settings'
|
|
4
5
|
import { povesteConfig } from '../../util/config'
|
|
5
6
|
import BaseCheckbox from '../base/BaseCheckbox.vue'
|
|
6
7
|
|
|
7
8
|
const settings = usePreviewSettingsStore().currentSettings
|
|
9
|
+
|
|
10
|
+
// Not `v-model.number`: Vue's `looseToNumber` returns the original value when
|
|
11
|
+
// `parseFloat` gives NaN, so clearing the field stored the string `''`. That is
|
|
12
|
+
// a third value for a two-valued setting, and it is not nullish — so the `??`
|
|
13
|
+
// in the size tooltip and in the dragger's start value both stopped firing, and
|
|
14
|
+
// dragging after clearing the field set the size to the drag distance (#440).
|
|
15
|
+
// The placeholder already says empty means auto, so empty becomes `null`.
|
|
16
|
+
function autoWhenEmpty(key: 'responsiveWidth' | 'responsiveHeight') {
|
|
17
|
+
return computed<number | null>({
|
|
18
|
+
get: () => settings[key],
|
|
19
|
+
set: (value) => {
|
|
20
|
+
const parsed = typeof value === 'number' ? value : Number.parseFloat(value ?? '')
|
|
21
|
+
settings[key] = Number.isFinite(parsed) ? parsed : null
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const responsiveWidth = autoWhenEmpty('responsiveWidth')
|
|
27
|
+
const responsiveHeight = autoWhenEmpty('responsiveHeight')
|
|
8
28
|
</script>
|
|
9
29
|
|
|
10
30
|
<template>
|
|
@@ -40,7 +60,7 @@ const settings = usePreviewSettingsStore().currentSettings
|
|
|
40
60
|
|
|
41
61
|
<div class="flex items-center gap-2 px-4 py-3">
|
|
42
62
|
<input
|
|
43
|
-
v-model
|
|
63
|
+
v-model="responsiveWidth"
|
|
44
64
|
v-tooltip="'Responsive width (px)'"
|
|
45
65
|
type="number"
|
|
46
66
|
class="bg-transparent border border-gray-200 dark:border-gray-850 rounded w-20 opacity-50 focus:opacity-100 flex-1 min-w-0"
|
|
@@ -49,7 +69,7 @@ const settings = usePreviewSettingsStore().currentSettings
|
|
|
49
69
|
>
|
|
50
70
|
<span class="opacity-50">×</span>
|
|
51
71
|
<input
|
|
52
|
-
v-model
|
|
72
|
+
v-model="responsiveHeight"
|
|
53
73
|
v-tooltip="'Responsive height (px)'"
|
|
54
74
|
type="number"
|
|
55
75
|
class="bg-transparent border border-gray-200 dark:border-gray-850 rounded w-20 opacity-50 focus:opacity-100 flex-1 min-w-0"
|
|
@@ -67,7 +87,7 @@ const settings = usePreviewSettingsStore().currentSettings
|
|
|
67
87
|
? 'bg-primary-500 hover:bg-primary-600 text-white dark:text-black'
|
|
68
88
|
: 'bg-transparent hover:bg-primary-100 dark:hover:bg-primary-700',
|
|
69
89
|
]"
|
|
70
|
-
@click="settings.responsiveWidth = preset.width;settings.responsiveHeight = preset.height;hide()"
|
|
90
|
+
@click="settings.responsiveWidth = preset.width;settings.responsiveHeight = preset.height ?? null;hide()"
|
|
71
91
|
>
|
|
72
92
|
{{ preset.label }}
|
|
73
93
|
<span class="ml-auto opacity-70 flex gap-1">
|
|
@@ -4,8 +4,8 @@ import { Icon } from '@iconify/vue'
|
|
|
4
4
|
import { computed } from 'vue'
|
|
5
5
|
|
|
6
6
|
const props = defineProps<{
|
|
7
|
-
variant?: Variant
|
|
8
|
-
story?: Story
|
|
7
|
+
variant?: Variant | undefined
|
|
8
|
+
story?: Story | undefined
|
|
9
9
|
}>()
|
|
10
10
|
|
|
11
11
|
const icon = computed(() => props.variant?.icon ?? props.story?.icon ?? 'carbon:cube')
|
|
@@ -63,8 +63,8 @@ function toggleOpen() {
|
|
|
63
63
|
:depth="0"
|
|
64
64
|
/>
|
|
65
65
|
<StoryListItem
|
|
66
|
-
v-else
|
|
67
|
-
:story="stories[(element as TreeLeaf).index]"
|
|
66
|
+
v-else-if="stories[(element as TreeLeaf).index]"
|
|
67
|
+
:story="stories[(element as TreeLeaf).index]!"
|
|
68
68
|
:depth="0"
|
|
69
69
|
/>
|
|
70
70
|
</template>
|
|
@@ -72,8 +72,8 @@ const folderPadding = computed(() => {
|
|
|
72
72
|
:depth="depth + 1"
|
|
73
73
|
/>
|
|
74
74
|
<StoryListItem
|
|
75
|
-
v-else
|
|
76
|
-
:story="stories[(element as TreeLeaf).index]"
|
|
75
|
+
v-else-if="stories[(element as TreeLeaf).index]"
|
|
76
|
+
:story="stories[(element as TreeLeaf).index]!"
|
|
77
77
|
:depth="depth + 1"
|
|
78
78
|
/>
|
|
79
79
|
</template>
|
|
@@ -25,7 +25,7 @@ const filePadding = computed(() => {
|
|
|
25
25
|
})
|
|
26
26
|
|
|
27
27
|
const route = useRoute()
|
|
28
|
-
const isActive = computed(() => route.params
|
|
28
|
+
const isActive = computed(() => route.params['storyId'] === props.story.id)
|
|
29
29
|
const el = ref<HTMLDivElement>()
|
|
30
30
|
useScrollOnActive(isActive, el)
|
|
31
31
|
</script>
|
package/src/app/plugin.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { router } from './router.js'
|
|
2
2
|
|
|
3
3
|
export function setupPluginApi() {
|
|
4
|
-
|
|
4
|
+
// Captured rather than re-read: the narrowing from this guard does not reach
|
|
5
|
+
// into the callbacks below, and `hot?.send` there would leave `sendEvent`'s
|
|
6
|
+
// promise waiting for a reply to a message that was never sent.
|
|
7
|
+
const hot = import.meta.hot
|
|
8
|
+
if (!hot) return
|
|
5
9
|
|
|
6
10
|
const listeners: Record<string, Set<(result: any) => unknown>> = {}
|
|
7
11
|
|
|
8
|
-
|
|
12
|
+
hot.on('poveste:dev-event-result', ({ event, result }) => {
|
|
9
13
|
const set = listeners[event]
|
|
10
14
|
if (set) {
|
|
11
15
|
for (const listener of set) {
|
|
@@ -36,7 +40,7 @@ export function setupPluginApi() {
|
|
|
36
40
|
window.__HST_PLUGIN_API__ = {
|
|
37
41
|
sendEvent: (event: string, payload?: any) => {
|
|
38
42
|
return new Promise((resolve) => {
|
|
39
|
-
|
|
43
|
+
hot.send(`poveste:dev-event`, {
|
|
40
44
|
event,
|
|
41
45
|
payload,
|
|
42
46
|
})
|
package/src/app/sandbox.ts
CHANGED
|
@@ -22,8 +22,8 @@ const query = parseQuery(window.location.search)
|
|
|
22
22
|
// What this realm serves. Set from the URL on boot, and again by a
|
|
23
23
|
// SANDBOX_RETARGET from the host: a warm realm is handed the next story or
|
|
24
24
|
// variant instead of being torn down for a new document (#240).
|
|
25
|
-
const storyId = ref(typeof query
|
|
26
|
-
const variantId = ref(typeof query
|
|
25
|
+
const storyId = ref(typeof query['storyId'] === 'string' ? query['storyId'] : null)
|
|
26
|
+
const variantId = ref(typeof query['variantId'] === 'string' ? query['variantId'] : null)
|
|
27
27
|
|
|
28
28
|
// Kept in step so `logEvent` can name the occupant it is speaking for; it posts
|
|
29
29
|
// from the story's own code and cannot see these refs.
|
|
@@ -72,7 +72,7 @@ if (window.parent && window.parent !== window) {
|
|
|
72
72
|
// window, and no framework hook sees an error raised outside its own call
|
|
73
73
|
// stack (#323).
|
|
74
74
|
function reportUncaught(error: unknown) {
|
|
75
|
-
reportStoryError(error, { storyId: storyId.value, variantId: variantId.value })
|
|
75
|
+
reportStoryError(error, { storyId: storyId.value ?? undefined, variantId: variantId.value ?? undefined })
|
|
76
76
|
}
|
|
77
77
|
window.addEventListener('error', (event) => {
|
|
78
78
|
if (!isBenignResizeObserverEvent(event)) {
|
|
@@ -147,6 +147,12 @@ const app = createApp({
|
|
|
147
147
|
let mounted = false
|
|
148
148
|
|
|
149
149
|
window.addEventListener('message', (event) => {
|
|
150
|
+
// The sandbox end of the check the host makes in
|
|
151
|
+
// `StoryVariantSinglePreviewRemote`. `__sandbox.html` is a public URL in
|
|
152
|
+
// every published book, and a page that iframes it is its `parent`, so
|
|
153
|
+
// without this anyone embedding it could write story state, retarget it,
|
|
154
|
+
// or restyle it. Only the book drives its own sandbox (#379).
|
|
155
|
+
if (event.origin !== window.location.origin) return
|
|
150
156
|
// console.log('[sandbox] received message', event.data)
|
|
151
157
|
if (event.data?.type === STATE_SYNC) {
|
|
152
158
|
if (!mounted || !variant.value) return
|
package/src/app/stores/folder.ts
CHANGED
package/src/app/stores/story.ts
CHANGED
|
@@ -9,9 +9,9 @@ export const useStoryStore = defineStore('story', () => {
|
|
|
9
9
|
stories.value = value
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const currentStory = computed(() => stories.value.find(s => s.id === router.currentRoute.value.params
|
|
12
|
+
const currentStory = computed(() => stories.value.find(s => s.id === router.currentRoute.value.params['storyId']))
|
|
13
13
|
|
|
14
|
-
const currentVariant = computed(() => currentStory.value?.variants.find(v => v.id === router.currentRoute.value.query
|
|
14
|
+
const currentVariant = computed(() => currentStory.value?.variants.find(v => v.id === router.currentRoute.value.query['variantId']))
|
|
15
15
|
|
|
16
16
|
const maps = computed(() => {
|
|
17
17
|
const storyMap = new Map<string, Story>()
|
package/src/app/types.ts
CHANGED
|
@@ -33,10 +33,10 @@ export interface SearchResultBase {
|
|
|
33
33
|
rank: number
|
|
34
34
|
id: string
|
|
35
35
|
title: string
|
|
36
|
-
path?: string[]
|
|
37
|
-
icon?: string
|
|
38
|
-
iconColor?: string
|
|
39
|
-
type?: SearchResultType
|
|
36
|
+
path?: string[] | undefined
|
|
37
|
+
icon?: string | undefined
|
|
38
|
+
iconColor?: string | undefined
|
|
39
|
+
type?: SearchResultType | undefined
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export type SearchResult = SearchResultBase & ({
|
|
@@ -54,14 +54,14 @@ export interface PreviewSettings {
|
|
|
54
54
|
*
|
|
55
55
|
* @default 720
|
|
56
56
|
*/
|
|
57
|
-
responsiveWidth: number
|
|
57
|
+
responsiveWidth: number | null
|
|
58
58
|
/**
|
|
59
59
|
* Height of the responsive preview, in pixels. `null` sizes it to the
|
|
60
60
|
* available space.
|
|
61
61
|
*
|
|
62
62
|
* @default null
|
|
63
63
|
*/
|
|
64
|
-
responsiveHeight: number
|
|
64
|
+
responsiveHeight: number | null
|
|
65
65
|
/**
|
|
66
66
|
* Swap `responsiveWidth` and `responsiveHeight`, to preview a size in
|
|
67
67
|
* landscape without editing both values.
|
package/src/app/util/commands.ts
CHANGED
|
@@ -1,29 +1,39 @@
|
|
|
1
|
-
import type { ClientCommand, ClientCommandContext } from '@poveste/shared'
|
|
1
|
+
import type { ClientCommand, ClientCommandContext, Story } from '@poveste/shared'
|
|
2
2
|
import { router } from '../router.js'
|
|
3
3
|
import { useStoryStore } from '../stores/story.js'
|
|
4
4
|
import { openInEditor } from './open-in-editor.js'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* The path "Open file in editor" should open, or undefined when there is none.
|
|
8
|
+
*
|
|
9
|
+
* `Story.file` is optional, and `let file: string` used to be assigned
|
|
10
|
+
* `story.file?.filePath` in both branches — so a story without a file produced
|
|
11
|
+
* `file: undefined`, and `openInEditor` sent
|
|
12
|
+
* `__open-in-editor?file=undefined` to the dev server: a request to open a file
|
|
13
|
+
* literally named "undefined", with nothing said in the UI (#667).
|
|
14
|
+
*
|
|
15
|
+
* A docs-only story prefers its markdown, falling back to the component file
|
|
16
|
+
* when the story has both.
|
|
17
|
+
*/
|
|
18
|
+
function editableFilePath(story: Story | undefined): string | undefined {
|
|
19
|
+
const file = story?.file
|
|
20
|
+
if (!file) return undefined
|
|
21
|
+
return story?.docsOnly ? file.docsFilePath ?? file.filePath : file.filePath
|
|
22
|
+
}
|
|
23
|
+
|
|
6
24
|
export const builtinCommands: ClientCommand[] = [
|
|
7
25
|
{
|
|
8
26
|
id: 'builtin:open-in-editor',
|
|
9
27
|
label: 'Open file in editor',
|
|
10
28
|
icon: 'carbon:script-reference',
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
else {
|
|
19
|
-
file = story.file?.filePath
|
|
20
|
-
}
|
|
21
|
-
return {
|
|
22
|
-
file,
|
|
23
|
-
}
|
|
24
|
-
},
|
|
29
|
+
// On the path rather than on the story: a story with no file has nothing
|
|
30
|
+
// to open, and offering the command was how the bad request got sent.
|
|
31
|
+
showIf: ({ route }) => route.name === 'story' && !!editableFilePath(useStoryStore().currentStory),
|
|
32
|
+
getParams: () => ({
|
|
33
|
+
file: editableFilePath(useStoryStore().currentStory),
|
|
34
|
+
}),
|
|
25
35
|
clientAction: ({ file }) => {
|
|
26
|
-
openInEditor(file)
|
|
36
|
+
if (file) openInEditor(file)
|
|
27
37
|
},
|
|
28
38
|
},
|
|
29
39
|
{
|
|
@@ -31,7 +41,7 @@ export const builtinCommands: ClientCommand[] = [
|
|
|
31
41
|
label: 'Open Poveste Documentation',
|
|
32
42
|
icon: 'carbon:help',
|
|
33
43
|
clientAction: () => {
|
|
34
|
-
window.open('https://poveste.dev/guide/getting-started
|
|
44
|
+
window.open('https://poveste.dev/guide/getting-started', '_blank')
|
|
35
45
|
},
|
|
36
46
|
},
|
|
37
47
|
]
|
package/src/app/util/dark.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { povesteConfig } from './config.js'
|
|
|
4
4
|
|
|
5
5
|
export const isDark = useDark({
|
|
6
6
|
valueDark: 'ptw-dark',
|
|
7
|
-
initialValue: povesteConfig.theme.defaultColorScheme,
|
|
7
|
+
...povesteConfig.theme.defaultColorScheme ? { initialValue: povesteConfig.theme.defaultColorScheme } : {},
|
|
8
8
|
storageKey: 'poveste-color-scheme',
|
|
9
9
|
storage: povesteConfig.theme.storeColorScheme ? localStorage : sessionStorage,
|
|
10
10
|
})
|
package/src/app/util/docs.ts
CHANGED
|
@@ -13,7 +13,8 @@ export async function getSourceCode(story: Story, variant: Variant) {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
else {
|
|
16
|
-
const
|
|
16
|
+
const supportPluginId = story.file?.supportPluginId
|
|
17
|
+
const clientPlugin = supportPluginId ? clientSupportPlugins[supportPluginId] : undefined
|
|
17
18
|
if (clientPlugin) {
|
|
18
19
|
const pluginModule = await clientPlugin()
|
|
19
20
|
return pluginModule.generateSourceCode(variant)
|
package/src/app/util/env.ts
CHANGED
|
@@ -2,5 +2,4 @@
|
|
|
2
2
|
// `navigator.userAgentData` is Chromium-only, so reading it here would simply
|
|
3
3
|
// stop detecting macOS in Safari and Firefox — the two places where getting the
|
|
4
4
|
// ⌘ shortcuts right matters most.
|
|
5
|
-
// eslint-disable-next-line ts/no-deprecated
|
|
6
5
|
export const isMac = navigator.platform.toLowerCase().includes('mac')
|
package/src/app/util/events.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EVENT_SEND } from './const'
|
|
2
2
|
import { occupant } from './occupant'
|
|
3
3
|
|
|
4
|
-
export async function logEvent(name: string, argument) {
|
|
4
|
+
export async function logEvent(name: string, argument: unknown) {
|
|
5
5
|
const event = {
|
|
6
6
|
name,
|
|
7
7
|
argument: JSON.parse(stringifyEvent(argument)), // Needed for HTMLEvent that can't be cloned
|
|
@@ -20,10 +20,11 @@ export async function logEvent(name: string, argument) {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function stringifyEvent(e) {
|
|
24
|
-
const obj = {}
|
|
25
|
-
for
|
|
26
|
-
|
|
23
|
+
function stringifyEvent(e: unknown) {
|
|
24
|
+
const obj: Record<string, unknown> = {}
|
|
25
|
+
// `for...in` rather than `Object.entries`: an event's fields are inherited.
|
|
26
|
+
for (const k in e as object) {
|
|
27
|
+
obj[k] = Reflect.get(e as object, k)
|
|
27
28
|
}
|
|
28
29
|
return JSON.stringify(obj, (k, v) => {
|
|
29
30
|
if (v instanceof Node) return 'Node'
|
|
@@ -26,7 +26,7 @@ export function assignSlots(slots: Slot[], storyId: string, visible: Variant[]):
|
|
|
26
26
|
const position = new Map(visible.map((v, i) => [v.id, i]))
|
|
27
27
|
const next: Slot[] = slots.map(slot => (
|
|
28
28
|
slot.storyId === storyId && position.has(slot.variant.id)
|
|
29
|
-
? { ...slot, visible: true, order: position.get(slot.variant.id) }
|
|
29
|
+
? { ...slot, visible: true, order: position.get(slot.variant.id) ?? 0 }
|
|
30
30
|
: { ...slot, visible: false, order: 0 }
|
|
31
31
|
))
|
|
32
32
|
const placed = new Set(next.filter(s => s.visible).map(s => s.variant.id))
|
|
@@ -67,6 +67,6 @@ export function trimSlots(slots: Slot[]): Slot[] {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
let end = next.length
|
|
70
|
-
while (end > 0 && !next[end - 1]
|
|
70
|
+
while (end > 0 && !next[end - 1]?.visible) end--
|
|
71
71
|
return end === next.length ? next : next.slice(0, end)
|
|
72
72
|
}
|