@poveste/app 0.13.1 → 0.14.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 +16 -16
- 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/base/BaseCheckbox.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/bundled/components/base/BaseOverflowMenu.vue_vue_type_script_setup_true_lang.js +5 -2
- 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 +1 -1
- package/dist/bundled/components/command/PromptText.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/bundled/components/panel/StatePresets.vue_vue_type_script_setup_true_lang.js +30 -20
- 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 +7 -5
- package/dist/bundled/components/panel/StorySourceCode.js +1 -1
- package/dist/bundled/components/panel/StorySourceCode.vue_vue_type_script_setup_true_lang.js +3 -2
- package/dist/bundled/components/search/SearchPane.vue_vue_type_script_setup_true_lang.js +15 -7
- 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 +17 -6
- 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/StoryView.vue_vue_type_script_setup_true_lang.js +3 -2
- 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/plugin.js +4 -3
- package/dist/bundled/sandbox.js +2 -2
- package/dist/bundled/stores/folder.js +1 -1
- package/dist/bundled/util/commands.js +20 -9
- package/dist/bundled/util/docs.js +2 -1
- package/dist/bundled/util/grid-slots.js +1 -1
- package/dist/bundled/util/icons.generated.js +0 -1
- package/dist/bundled/util/keyboard.js +1 -0
- package/dist/bundled/util/mapping.js +1 -1
- package/dist/bundled/util/preview-settings.js +1 -1
- package/package.json +5 -5
- package/src/app/App.vue +6 -3
- 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/base/BaseCheckbox.vue +1 -1
- package/src/app/components/base/BaseOverflowMenu.vue +9 -3
- package/src/app/components/base/BaseSplitPane.vue +9 -4
- 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 +2 -1
- package/src/app/components/command/PromptText.vue +7 -3
- package/src/app/components/panel/StatePresets.vue +26 -5
- 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 +2 -0
- package/src/app/components/panel/StorySourceCode.vue +6 -3
- package/src/app/components/search/SearchPane.vue +14 -5
- 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 +30 -5
- package/src/app/components/story/StoryVariantGrid.vue +33 -16
- package/src/app/components/story/StoryView.vue +7 -3
- 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/plugin.ts +7 -3
- package/src/app/sandbox.ts +1 -1
- package/src/app/stores/folder.ts +1 -1
- package/src/app/types.ts +2 -2
- package/src/app/util/commands.ts +26 -16
- package/src/app/util/docs.ts +2 -1
- package/src/app/util/grid-slots.ts +1 -1
- package/src/app/util/icons.generated.ts +1 -1
- package/src/app/util/keyboard.ts +3 -0
- package/src/app/util/mapping.ts +1 -1
- package/src/app/util/preview-settings.ts +1 -1
- package/src/app/util/scroll.ts +5 -1
- package/src/app/util/state.ts +1 -1
- package/src/app/util/variant.ts +4 -1
- package/src/app/components/base/BaseCopyIcon.vue +0 -34
|
@@ -168,13 +168,13 @@ const rightStyle = computed(() => {
|
|
|
168
168
|
if (!hasBothPanes.value) {
|
|
169
169
|
return { [axis.value]: '100%' }
|
|
170
170
|
}
|
|
171
|
-
return { [axis.value]: props.fixed ?
|
|
171
|
+
return { [axis.value]: props.fixed ? undefined : `${100 - boundSplit.value}%` }
|
|
172
172
|
})
|
|
173
173
|
|
|
174
174
|
const dragging = ref(false)
|
|
175
175
|
let startPosition = 0
|
|
176
176
|
let startSplit = 0
|
|
177
|
-
const el = ref(
|
|
177
|
+
const el = ref<HTMLElement>()
|
|
178
178
|
|
|
179
179
|
function dragStart(e) {
|
|
180
180
|
dragging.value = true
|
|
@@ -186,15 +186,20 @@ function dragStart(e) {
|
|
|
186
186
|
|
|
187
187
|
function dragMove(e) {
|
|
188
188
|
if (dragging.value) {
|
|
189
|
+
// The move listener is on `window`, so it can outlive the element for the
|
|
190
|
+
// rest of a drag if the pane unmounts under it.
|
|
191
|
+
const pane = el.value
|
|
192
|
+
if (!pane) return
|
|
193
|
+
|
|
189
194
|
let position
|
|
190
195
|
let totalSize
|
|
191
196
|
if (props.orientation === 'landscape') {
|
|
192
197
|
position = e.pageX
|
|
193
|
-
totalSize =
|
|
198
|
+
totalSize = pane.offsetWidth
|
|
194
199
|
}
|
|
195
200
|
else {
|
|
196
201
|
position = e.pageY
|
|
197
|
-
totalSize =
|
|
202
|
+
totalSize = pane.offsetHeight
|
|
198
203
|
}
|
|
199
204
|
const dPosition = position - startPosition
|
|
200
205
|
if (props.fixed) {
|
|
@@ -24,7 +24,7 @@ const promptTypes = {
|
|
|
24
24
|
const answers = reactive<Record<string, any>>({})
|
|
25
25
|
|
|
26
26
|
// Initial default values
|
|
27
|
-
for (const prompt of props.command.prompts) {
|
|
27
|
+
for (const prompt of props.command.prompts ?? []) {
|
|
28
28
|
let defaultValue
|
|
29
29
|
if (typeof prompt.defaultValue === 'function') {
|
|
30
30
|
defaultValue = prompt.defaultValue(answers)
|
|
@@ -33,6 +33,7 @@ const commandStore = useCommandStore()
|
|
|
33
33
|
/>
|
|
34
34
|
<div class="bg-white dark:bg-gray-900 md:mt-16 md:mx-auto w-screen max-w-[512px] max-h-[80vh] overflow-y-auto scroll-smooth shadow-xl border border-gray-200 dark:border-gray-750 rounded-lg relative divide-y divide-gray-200 dark:divide-gray-850">
|
|
35
35
|
<CommandPrompts
|
|
36
|
+
v-if="commandStore.selectedCommand"
|
|
36
37
|
:command="commandStore.selectedCommand"
|
|
37
38
|
@close="close()"
|
|
38
39
|
/>
|
|
@@ -16,8 +16,9 @@ const emit = defineEmits<{
|
|
|
16
16
|
(e: 'next'): void
|
|
17
17
|
}>()
|
|
18
18
|
|
|
19
|
+
// Optional coming in, required going out, so the empty case settles here.
|
|
19
20
|
const model = computed({
|
|
20
|
-
get: () => props.modelValue,
|
|
21
|
+
get: () => props.modelValue ?? '',
|
|
21
22
|
set: (value) => {
|
|
22
23
|
emit('update:modelValue', value)
|
|
23
24
|
emit('next')
|
|
@@ -9,11 +9,15 @@ const props = defineProps<{
|
|
|
9
9
|
}>()
|
|
10
10
|
|
|
11
11
|
const emit = defineEmits<{
|
|
12
|
-
(e: 'update:modelValue', value: string): void
|
|
12
|
+
(e: 'update:modelValue', value: string | undefined): void
|
|
13
13
|
}>()
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// The input needs a string to bind to, so the getter fills in `''` — but an
|
|
16
|
+
// absent answer has to leave as absent. `''` is falsy and *not* nullish, so
|
|
17
|
+
// coercing on the way out would stop a command's `answers.x ?? fallback` from
|
|
18
|
+
// firing, which is the trap #440 was about one file over.
|
|
19
|
+
const model = computed<string | undefined>({
|
|
20
|
+
get: () => props.modelValue ?? '',
|
|
17
21
|
set: value => emit('update:modelValue', value),
|
|
18
22
|
})
|
|
19
23
|
|
|
@@ -57,8 +57,13 @@ function applyPreset(id) {
|
|
|
57
57
|
if (id === DEFAULT_ID) {
|
|
58
58
|
resetState()
|
|
59
59
|
}
|
|
60
|
-
else
|
|
61
|
-
|
|
60
|
+
else {
|
|
61
|
+
// `has` then `get` is two lookups and only the second one is the value;
|
|
62
|
+
// reading it once says the same thing and carries the type with it.
|
|
63
|
+
const preset = presetStates.value.get(id)
|
|
64
|
+
if (preset) {
|
|
65
|
+
applyState(props.variant.state, clone(toRawDeep(preset.state)))
|
|
66
|
+
}
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
|
|
@@ -80,9 +85,22 @@ async function createPreset() {
|
|
|
80
85
|
selectedOption.value = id
|
|
81
86
|
isEditing.value = true
|
|
82
87
|
await nextTick()
|
|
83
|
-
input
|
|
88
|
+
// The input is rendered by the edit state set above, so it exists by now —
|
|
89
|
+
// unless editing was cancelled during the tick, which unmounts it.
|
|
90
|
+
input.value?.select()
|
|
84
91
|
}
|
|
85
92
|
|
|
93
|
+
// The template bound `v-model` straight to `presetStates.get(selectedOption).label`,
|
|
94
|
+
// which is a Map lookup that can miss. Naming it once keeps the template honest
|
|
95
|
+
// and gives the miss somewhere to go.
|
|
96
|
+
const editingLabel = computed({
|
|
97
|
+
get: () => presetStates.value.get(selectedOption.value)?.label ?? '',
|
|
98
|
+
set: (value) => {
|
|
99
|
+
const preset = presetStates.value.get(selectedOption.value)
|
|
100
|
+
if (preset) preset.label = value
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
|
|
86
104
|
const savedNotif = ref(false)
|
|
87
105
|
const savedTimeout = useTimeoutFn(() => {
|
|
88
106
|
savedNotif.value = false
|
|
@@ -92,6 +110,7 @@ async function savePreset() {
|
|
|
92
110
|
if (!canEdit.value) return
|
|
93
111
|
|
|
94
112
|
const preset = presetStates.value.get(selectedOption.value)
|
|
113
|
+
if (!preset) return
|
|
95
114
|
preset.state = toPresetState(props.variant.state, omitKeys)
|
|
96
115
|
savedNotif.value = true
|
|
97
116
|
savedTimeout.start()
|
|
@@ -117,7 +136,9 @@ async function startEditing() {
|
|
|
117
136
|
|
|
118
137
|
isEditing.value = true
|
|
119
138
|
await nextTick()
|
|
120
|
-
input
|
|
139
|
+
// The input is rendered by the edit state set above, so it exists by now —
|
|
140
|
+
// unless editing was cancelled during the tick, which unmounts it.
|
|
141
|
+
input.value?.select()
|
|
121
142
|
}
|
|
122
143
|
|
|
123
144
|
function stopEditing() {
|
|
@@ -145,7 +166,7 @@ onClickOutside(select, stopEditing)
|
|
|
145
166
|
<input
|
|
146
167
|
v-if="isEditing"
|
|
147
168
|
ref="input"
|
|
148
|
-
v-model="
|
|
169
|
+
v-model="editingLabel"
|
|
149
170
|
type="text"
|
|
150
171
|
class="text-inherit bg-transparent w-full h-full outline-none"
|
|
151
172
|
@click.stop.prevent
|
|
@@ -28,7 +28,7 @@ watch(() => props.variant, () => {
|
|
|
28
28
|
ready.value = false
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
-
const hasCustomControls = computed(() => props.variant.slots().controls || props.story.slots().controls)
|
|
31
|
+
const hasCustomControls = computed(() => props.variant.slots?.().controls || props.story.slots?.().controls)
|
|
32
32
|
|
|
33
33
|
const hasInitState = computed(() => Object
|
|
34
34
|
.entries(props.variant.state || {})
|
|
@@ -14,7 +14,9 @@ export function useStoryDoc(story: Ref<Story>) {
|
|
|
14
14
|
|
|
15
15
|
watchEffect(async () => {
|
|
16
16
|
// Markdown file
|
|
17
|
-
const
|
|
17
|
+
const filePath = story.value?.file?.filePath
|
|
18
|
+
if (!filePath) return
|
|
19
|
+
const mdKey = filePath.replace(/\.(\w*)$/, '.md')
|
|
18
20
|
if (markdownFiles[mdKey]) {
|
|
19
21
|
const md = await markdownFiles[mdKey]()
|
|
20
22
|
renderedDoc.value = md.html
|
|
@@ -10,8 +10,13 @@ const formattedArgument = computed(() => {
|
|
|
10
10
|
switch (typeof props.event.argument) {
|
|
11
11
|
case 'string':
|
|
12
12
|
return `"${props.event.argument}"`
|
|
13
|
-
case 'object':
|
|
14
|
-
|
|
13
|
+
case 'object': {
|
|
14
|
+
// `argument` is `unknown`, and `typeof x === 'object'` narrows it only as
|
|
15
|
+
// far as `object | null` — enough for `Object.keys`, not for indexing.
|
|
16
|
+
const argument = props.event.argument as Record<string, unknown> | null
|
|
17
|
+
if (!argument) return 'null'
|
|
18
|
+
return `{ ${Object.keys(argument).map(key => `${key}: ${argument[key]}`).join(', ')} }`
|
|
19
|
+
}
|
|
15
20
|
default:
|
|
16
21
|
return props.event.argument
|
|
17
22
|
}
|
|
@@ -19,7 +19,10 @@ async function resetUnseen() {
|
|
|
19
19
|
eventsStore.unseen = 0
|
|
20
20
|
}
|
|
21
21
|
await nextTick()
|
|
22
|
-
|
|
22
|
+
// Read after the await, and it is a template ref: the panel can be closed
|
|
23
|
+
// during the tick, and then there is nothing to scroll.
|
|
24
|
+
const events = eventsElement.value
|
|
25
|
+
events?.scrollTo({ top: events.scrollHeight })
|
|
23
26
|
}
|
|
24
27
|
</script>
|
|
25
28
|
|
|
@@ -60,6 +60,7 @@ const panelContentComponent = computed(() => {
|
|
|
60
60
|
<template #first>
|
|
61
61
|
<div class="flex flex-col h-full">
|
|
62
62
|
<PaneTabs
|
|
63
|
+
v-if="storyStore.currentStory"
|
|
63
64
|
:story="storyStore.currentStory"
|
|
64
65
|
:variant="storyStore.currentVariant"
|
|
65
66
|
/>
|
|
@@ -75,6 +76,7 @@ const panelContentComponent = computed(() => {
|
|
|
75
76
|
|
|
76
77
|
<template #last>
|
|
77
78
|
<StorySourceCode
|
|
79
|
+
v-if="storyStore.currentStory"
|
|
78
80
|
:story="storyStore.currentStory"
|
|
79
81
|
:variant="storyStore.currentVariant"
|
|
80
82
|
class="h-full"
|
|
@@ -15,10 +15,13 @@ const props = defineProps<{
|
|
|
15
15
|
variant: Variant
|
|
16
16
|
}>()
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// `ref(null)` infers `Ref<null>`, so the assignment below and the call further
|
|
19
|
+
// down were both errors about a type nobody meant to declare.
|
|
20
|
+
const generateSourceCodeFn = shallowRef<((variant: Variant) => Promise<string>) | null>(null)
|
|
19
21
|
|
|
20
22
|
watchEffect(async () => {
|
|
21
|
-
const
|
|
23
|
+
const supportPluginId = props.story.file?.supportPluginId
|
|
24
|
+
const clientPlugin = supportPluginId ? clientSupportPlugins[supportPluginId] : undefined
|
|
22
25
|
if (clientPlugin) {
|
|
23
26
|
const pluginModule = await clientPlugin()
|
|
24
27
|
generateSourceCodeFn.value = markRaw(pluginModule.generateSourceCode)
|
|
@@ -28,7 +31,7 @@ watchEffect(async () => {
|
|
|
28
31
|
const highlighter = shallowRef<HighlighterCore>()
|
|
29
32
|
|
|
30
33
|
const dynamicSourceCode = ref('')
|
|
31
|
-
const error = ref<string>(null)
|
|
34
|
+
const error = ref<string | null>(null)
|
|
32
35
|
|
|
33
36
|
// Above the watch that assigns it: the callback is async but runs immediately,
|
|
34
37
|
// and a story whose `slots()` throws reaches the auto-switch below before its
|
|
@@ -45,7 +45,9 @@ watch(() => props.shown, (value) => {
|
|
|
45
45
|
if (value) {
|
|
46
46
|
requestAnimationFrame(() => {
|
|
47
47
|
focused.value = true
|
|
48
|
-
|
|
48
|
+
// A whole frame later, so the pane may have been closed again — a
|
|
49
|
+
// template ref is undefined once its element unmounts.
|
|
50
|
+
input.value?.select()
|
|
49
51
|
})
|
|
50
52
|
}
|
|
51
53
|
})
|
|
@@ -127,7 +129,11 @@ watch(rateLimitedSearch, async (value) => {
|
|
|
127
129
|
if (!idMapData) continue
|
|
128
130
|
switch (idMapData.kind) {
|
|
129
131
|
case 'story': {
|
|
130
|
-
|
|
132
|
+
// The search index is built from the stories and outlives an
|
|
133
|
+
// individual one, so a hit can name a story that is no longer loaded.
|
|
134
|
+
const story = storyStore.getStoryById(idMapData.id)
|
|
135
|
+
if (!story) continue
|
|
136
|
+
list.push(storyResultFactory(story, rank))
|
|
131
137
|
rank++
|
|
132
138
|
break
|
|
133
139
|
}
|
|
@@ -135,6 +141,7 @@ watch(rateLimitedSearch, async (value) => {
|
|
|
135
141
|
const [storyId] = idMapData.id.split(':')
|
|
136
142
|
const story = storyStore.getStoryById(storyId)
|
|
137
143
|
const variant = storyStore.getVariantById(idMapData.id)
|
|
144
|
+
if (!story || !variant) continue
|
|
138
145
|
list.push(variantResultFactory(story, variant, rank))
|
|
139
146
|
rank++
|
|
140
147
|
break
|
|
@@ -158,7 +165,9 @@ async function searchOnDocField(query: string) {
|
|
|
158
165
|
if (!idMapData) continue
|
|
159
166
|
switch (idMapData.kind) {
|
|
160
167
|
case 'story': {
|
|
161
|
-
|
|
168
|
+
const story = storyStore.getStoryById(idMapData.id)
|
|
169
|
+
if (!story) continue
|
|
170
|
+
list.push(storyResultFactory(story, rank, 'docs'))
|
|
162
171
|
rank++
|
|
163
172
|
break
|
|
164
173
|
}
|
|
@@ -188,7 +197,7 @@ function storyResultFactory(story: Story, rank: number, type: SearchResultType =
|
|
|
188
197
|
: {},
|
|
189
198
|
},
|
|
190
199
|
},
|
|
191
|
-
path: story.file
|
|
200
|
+
path: story.file?.path.slice(0, -1) ?? [],
|
|
192
201
|
icon: story.icon,
|
|
193
202
|
iconColor: story.iconColor,
|
|
194
203
|
}
|
|
@@ -212,7 +221,7 @@ function variantResultFactory(story: Story, variant: Variant, rank: number, type
|
|
|
212
221
|
: {},
|
|
213
222
|
},
|
|
214
223
|
},
|
|
215
|
-
path: [...story.file
|
|
224
|
+
path: [...story.file?.path ?? [], story.title],
|
|
216
225
|
icon: variant.icon,
|
|
217
226
|
iconColor: variant.iconColor,
|
|
218
227
|
}
|
|
@@ -16,7 +16,8 @@ const props = defineProps<{
|
|
|
16
16
|
const mountComponent = ref(null)
|
|
17
17
|
|
|
18
18
|
watchEffect(async () => {
|
|
19
|
-
const
|
|
19
|
+
const supportPluginId = props.story.file?.supportPluginId
|
|
20
|
+
const clientPlugin = supportPluginId ? clientSupportPlugins[supportPluginId] : undefined
|
|
20
21
|
if (clientPlugin) {
|
|
21
22
|
const pluginModule = await clientPlugin()
|
|
22
23
|
mountComponent.value = markRaw(pluginModule.MountStory)
|
|
@@ -16,7 +16,8 @@ const props = defineProps<{
|
|
|
16
16
|
const mountComponent = ref(null)
|
|
17
17
|
|
|
18
18
|
watchEffect(async () => {
|
|
19
|
-
const
|
|
19
|
+
const supportPluginId = props.story.file?.supportPluginId
|
|
20
|
+
const clientPlugin = supportPluginId ? clientSupportPlugins[supportPluginId] : undefined
|
|
20
21
|
if (clientPlugin) {
|
|
21
22
|
try {
|
|
22
23
|
const pluginModule = await clientPlugin()
|
|
@@ -43,12 +43,34 @@ function addWindowListener(event: string, listener: (event: any) => unknown) {
|
|
|
43
43
|
|
|
44
44
|
const previewWrapper = ref<HTMLDivElement>()
|
|
45
45
|
|
|
46
|
-
function useDragger(el: Ref<HTMLDivElement>, value: Ref<number>, min: number, max: number, axis: 'x' | 'y') {
|
|
46
|
+
function useDragger(el: Ref<HTMLDivElement | undefined>, value: Ref<number | null>, min: number, max: number, axis: 'x' | 'y') {
|
|
47
|
+
// The wrapper is a template ref, so it is undefined before mount and after
|
|
48
|
+
// unmount. Every read below happens during a drag, when it is on screen — but
|
|
49
|
+
// the move and up listeners are on `window`, so "during a drag" is not the
|
|
50
|
+
// same as "while this component is alive".
|
|
51
|
+
function wrapperSize() {
|
|
52
|
+
const wrapper = previewWrapper.value
|
|
53
|
+
if (!wrapper) return null
|
|
54
|
+
return axis === 'x' ? wrapper.clientWidth : wrapper.clientHeight
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// `null` is the value that means "size to the available space", so a drag
|
|
58
|
+
// beginning from auto has to begin from the size actually rendered. The touch
|
|
59
|
+
// path had no fallback and `null + delta` coerces to `delta`, which collapsed
|
|
60
|
+
// a full-height preview to the minimum on the first move (#440).
|
|
61
|
+
function startFrom() {
|
|
62
|
+
if (value.value !== null) return value.value
|
|
63
|
+
const size = wrapperSize()
|
|
64
|
+
// Nothing to measure against, so start from the floor rather than from NaN.
|
|
65
|
+
if (size === null) return min
|
|
66
|
+
return size - (axis === 'x' ? 67 : 70)
|
|
67
|
+
}
|
|
68
|
+
|
|
47
69
|
function onMouseDown(event: MouseEvent) {
|
|
48
70
|
event.preventDefault()
|
|
49
71
|
event.stopPropagation()
|
|
50
72
|
const start = axis === 'x' ? event.clientX : event.clientY
|
|
51
|
-
const startValue =
|
|
73
|
+
const startValue = startFrom()
|
|
52
74
|
resizing.value = true
|
|
53
75
|
|
|
54
76
|
const removeListeners = [
|
|
@@ -57,11 +79,14 @@ function useDragger(el: Ref<HTMLDivElement>, value: Ref<number>, min: number, ma
|
|
|
57
79
|
]
|
|
58
80
|
|
|
59
81
|
function onMouseMove(event: MouseEvent) {
|
|
60
|
-
const snapTarget = (
|
|
82
|
+
const snapTarget = wrapperSize()
|
|
61
83
|
const delta = (axis === 'x' ? event.clientX : event.clientY) - start
|
|
62
84
|
value.value = Math.max(min, Math.min(max, startValue + delta))
|
|
63
85
|
|
|
64
|
-
|
|
86
|
+
// 67 on both axes, unlike `startFrom`. Left as it was: the snap threshold
|
|
87
|
+
// is a feel, not a measurement, and changing it here would be a UX edit
|
|
88
|
+
// wearing a type fix's clothes.
|
|
89
|
+
if (snapTarget !== null && Math.abs(value.value - (snapTarget - 67)) < 16) {
|
|
65
90
|
value.value = null
|
|
66
91
|
}
|
|
67
92
|
}
|
|
@@ -77,7 +102,7 @@ function useDragger(el: Ref<HTMLDivElement>, value: Ref<number>, min: number, ma
|
|
|
77
102
|
event.preventDefault()
|
|
78
103
|
event.stopPropagation()
|
|
79
104
|
const start = axis === 'x' ? event.touches[0].clientX : event.touches[0].clientY
|
|
80
|
-
const startValue =
|
|
105
|
+
const startValue = startFrom()
|
|
81
106
|
resizing.value = true
|
|
82
107
|
|
|
83
108
|
const removeListeners = [
|
|
@@ -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>
|
|
@@ -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,
|
|
@@ -61,7 +65,7 @@ function setVariant(variantId: string) {
|
|
|
61
65
|
|
|
62
66
|
// Docs auto-scroll to top
|
|
63
67
|
|
|
64
|
-
const docsOnlyScroller = ref<HTMLElement>(
|
|
68
|
+
const docsOnlyScroller = ref<HTMLElement>()
|
|
65
69
|
|
|
66
70
|
function scrollDocsToTop() {
|
|
67
71
|
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">
|
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
|
@@ -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)) {
|
package/src/app/stores/folder.ts
CHANGED