@poveste/app 0.14.0 → 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.css +26 -26
- package/dist/bundled/components/app/TopBarChipItem.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 +14 -14
- 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/command/PromptSelect.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 +78 -49
- package/dist/bundled/components/panel/StorySidePanel.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/bundled/components/panel/StorySourceCode.js +1 -1
- package/dist/bundled/components/panel/StorySourceCode.vue_vue_type_script_setup_true_lang.js +5 -4
- 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 +1 -2
- package/dist/bundled/components/story/StoryResponsivePreview.js +1 -1
- package/dist/bundled/components/story/StoryResponsivePreview.vue_vue_type_script_setup_true_lang.js +8 -4
- 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 +4 -1
- 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/sandbox.js +3 -2
- package/dist/bundled/stores/story.js +2 -2
- package/dist/bundled/util/commands.js +1 -1
- package/dist/bundled/util/dark.js +1 -1
- package/dist/bundled/util/events.js +1 -1
- package/dist/bundled/util/grid-slots.js +1 -1
- package/dist/bundled/util/keyboard.js +8 -14
- package/dist/bundled/util/mapping.js +2 -2
- 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 +13 -17
- package/src/__tests__/render-tracker.spec.ts +38 -0
- package/src/app/App.vue +1 -1
- package/src/app/components/app/TopBarChipItem.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 +17 -16
- package/src/app/components/base/BaseSelect.vue +171 -15
- package/src/app/components/base/BaseSplitPane.vue +2 -2
- package/src/app/components/command/PromptSelect.vue +1 -1
- 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 +100 -64
- package/src/app/components/panel/StorySidePanel.vue +1 -1
- package/src/app/components/panel/StorySourceCode.vue +6 -5
- package/src/app/components/search/SearchItemContent.vue +2 -1
- package/src/app/components/search/SearchPane.vue +2 -3
- package/src/app/components/story/StoryResponsivePreview.vue +6 -2
- 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 +3 -1
- 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/sandbox.ts +8 -2
- package/src/app/stores/story.ts +2 -2
- package/src/app/types.ts +4 -4
- package/src/app/util/commands.ts +1 -1
- package/src/app/util/dark.ts +1 -1
- package/src/app/util/env.ts +0 -1
- package/src/app/util/events.ts +6 -5
- package/src/app/util/grid-slots.ts +1 -1
- package/src/app/util/keyboard.ts +7 -11
- package/src/app/util/mapping.ts +2 -2
- package/src/app/util/render-tracker.ts +31 -0
- package/src/app/util/state.ts +3 -3
- package/src/app/util/variant.ts +12 -2
|
@@ -176,7 +176,7 @@ let startPosition = 0
|
|
|
176
176
|
let startSplit = 0
|
|
177
177
|
const el = ref<HTMLElement>()
|
|
178
178
|
|
|
179
|
-
function dragStart(e) {
|
|
179
|
+
function dragStart(e: MouseEvent) {
|
|
180
180
|
dragging.value = true
|
|
181
181
|
startPosition = props.orientation === 'landscape' ? e.pageX : e.pageY
|
|
182
182
|
startSplit = boundSplit.value
|
|
@@ -184,7 +184,7 @@ function dragStart(e) {
|
|
|
184
184
|
window.addEventListener('mouseup', dragEnd)
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
function dragMove(e) {
|
|
187
|
+
function dragMove(e: MouseEvent) {
|
|
188
188
|
if (dragging.value) {
|
|
189
189
|
// The move listener is on `window`, so it can outlive the element for the
|
|
190
190
|
// rest of a drag if the pane unmounts under it.
|
|
@@ -50,24 +50,35 @@ const canReset = computed(() => props.variant.state?._hPropState?.[props.compone
|
|
|
50
50
|
</script>
|
|
51
51
|
|
|
52
52
|
<template>
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
<!-- The remove action is beside the control, not in its actions slot. Inside
|
|
54
|
+
the control's <label> or checkbox, a label forwarded clicks to it, its
|
|
55
|
+
name joined the control's name, and Space on it toggled the checkbox (#827). -->
|
|
56
|
+
<div
|
|
55
57
|
v-if="comp"
|
|
56
|
-
|
|
57
|
-
:placeholder="model === undefined ? definition?.default : null"
|
|
58
|
-
class="poveste-controls-component-prop-item"
|
|
59
|
-
:title="`${definition.name}${canReset ? ' *' : ''}`"
|
|
58
|
+
class="flex items-end hover:bg-primary-100 dark:hover:bg-primary-800"
|
|
60
59
|
>
|
|
61
|
-
<
|
|
60
|
+
<component
|
|
61
|
+
:is="comp"
|
|
62
|
+
v-model="model"
|
|
63
|
+
:placeholder="model === undefined ? definition?.default : null"
|
|
64
|
+
class="poveste-controls-component-prop-item grow min-w-0"
|
|
65
|
+
:title="`${definition.name}${canReset ? ' *' : ''}`"
|
|
66
|
+
/>
|
|
67
|
+
<button
|
|
68
|
+
v-tooltip="'Remove override'"
|
|
69
|
+
type="button"
|
|
70
|
+
:aria-label="`Remove override of ${definition.name}`"
|
|
71
|
+
:disabled="!canReset"
|
|
72
|
+
class="flex-none flex mr-2 mb-[10px] p-0 bg-transparent border-0 cursor-pointer text-gray-900 dark:text-gray-100 hover:text-primary-500 dark:hover:text-primary-400"
|
|
73
|
+
:class="[
|
|
74
|
+
canReset ? 'opacity-50 hover:opacity-100 focus-visible:opacity-100' : 'opacity-25 pointer-events-none',
|
|
75
|
+
]"
|
|
76
|
+
@click="reset()"
|
|
77
|
+
>
|
|
62
78
|
<Icon
|
|
63
|
-
v-tooltip="'Remove override'"
|
|
64
79
|
icon="carbon:erase"
|
|
65
|
-
class="
|
|
66
|
-
:class="[
|
|
67
|
-
canReset ? 'opacity-50 hover:opacity-100' : 'opacity-25 pointer-events-none',
|
|
68
|
-
]"
|
|
69
|
-
@click.stop="reset()"
|
|
80
|
+
class="w-4 h-4"
|
|
70
81
|
/>
|
|
71
|
-
</
|
|
72
|
-
</
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
73
84
|
</template>
|
|
@@ -25,13 +25,13 @@ const hasEvents = computed(() => eventsStore.events.length)
|
|
|
25
25
|
<BaseOverflowMenu class="poveste-pane-tabs h-10 flex-none border-b border-gray-100 dark:border-gray-750">
|
|
26
26
|
<BaseTab
|
|
27
27
|
:to="{ ...$route, query: { ...$route.query, tab: '' } }"
|
|
28
|
-
:matched="!$route.query
|
|
28
|
+
:matched="!$route.query['tab']"
|
|
29
29
|
>
|
|
30
30
|
Controls
|
|
31
31
|
</BaseTab>
|
|
32
32
|
<BaseTab
|
|
33
33
|
:to="{ ...$route, query: { ...$route.query, tab: 'docs' } }"
|
|
34
|
-
:matched="$route.query
|
|
34
|
+
:matched="$route.query['tab'] === 'docs'"
|
|
35
35
|
:class="{
|
|
36
36
|
'opacity-70': !renderedDoc,
|
|
37
37
|
}"
|
|
@@ -40,7 +40,7 @@ const hasEvents = computed(() => eventsStore.events.length)
|
|
|
40
40
|
</BaseTab>
|
|
41
41
|
<BaseTab
|
|
42
42
|
:to="{ ...$route, query: { ...$route.query, tab: 'events' } }"
|
|
43
|
-
:matched="$route.query
|
|
43
|
+
:matched="$route.query['tab'] === 'events'"
|
|
44
44
|
:class="{
|
|
45
45
|
'opacity-70': !hasEvents,
|
|
46
46
|
}"
|
|
@@ -54,13 +54,13 @@ const hasEvents = computed(() => eventsStore.events.length)
|
|
|
54
54
|
<template #overflow>
|
|
55
55
|
<BaseOverflowTab
|
|
56
56
|
:to="{ ...$route, query: { ...$route.query, tab: '' } }"
|
|
57
|
-
:matched="!$route.query
|
|
57
|
+
:matched="!$route.query['tab']"
|
|
58
58
|
>
|
|
59
59
|
Controls
|
|
60
60
|
</BaseOverflowTab>
|
|
61
61
|
<BaseOverflowTab
|
|
62
62
|
:to="{ ...$route, query: { ...$route.query, tab: 'docs' } }"
|
|
63
|
-
:matched="$route.query
|
|
63
|
+
:matched="$route.query['tab'] === 'docs'"
|
|
64
64
|
:class="{
|
|
65
65
|
'opacity-70': !renderedDoc,
|
|
66
66
|
}"
|
|
@@ -69,7 +69,7 @@ const hasEvents = computed(() => eventsStore.events.length)
|
|
|
69
69
|
</BaseOverflowTab>
|
|
70
70
|
<BaseOverflowTab
|
|
71
71
|
:to="{ ...$route, query: { ...$route.query, tab: 'events' } }"
|
|
72
|
-
:matched="$route.query
|
|
72
|
+
:matched="$route.query['tab'] === 'events'"
|
|
73
73
|
:class="{
|
|
74
74
|
'opacity-70': !hasEvents,
|
|
75
75
|
}"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { Story, Variant } from '../../types'
|
|
3
3
|
import { Icon } from '@iconify/vue'
|
|
4
4
|
import { applyState, clone } from '@poveste/shared'
|
|
5
|
-
import { onClickOutside, useStorage, useTimeoutFn } from '@vueuse/core'
|
|
5
|
+
import { onClickOutside, useLiveAnnouncer, useStorage, useTimeoutFn } from '@vueuse/core'
|
|
6
6
|
import { computed, nextTick, onMounted, ref } from 'vue'
|
|
7
7
|
import { toPresetState, toRawDeep } from '../../util/state'
|
|
8
8
|
import BaseSelect from '../base/BaseSelect.vue'
|
|
@@ -41,7 +41,7 @@ const presetStates = useStorage<Map<string, { state: Record<string, unknown>, la
|
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
const presetsOptions = computed(() => {
|
|
44
|
-
const options = { [DEFAULT_ID]: 'Initial state' }
|
|
44
|
+
const options: Record<string, string> = { [DEFAULT_ID]: 'Initial state' }
|
|
45
45
|
presetStates.value.forEach((value, key) => {
|
|
46
46
|
options[key] = value.label
|
|
47
47
|
})
|
|
@@ -53,7 +53,7 @@ function resetState() {
|
|
|
53
53
|
applyState(props.variant.state, clone(defaultState))
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function applyPreset(id) {
|
|
56
|
+
function applyPreset(id: string) {
|
|
57
57
|
if (id === DEFAULT_ID) {
|
|
58
58
|
resetState()
|
|
59
59
|
}
|
|
@@ -75,6 +75,7 @@ onMounted(() => {
|
|
|
75
75
|
|
|
76
76
|
const input = ref<HTMLInputElement>()
|
|
77
77
|
const select = ref<HTMLInputElement>()
|
|
78
|
+
const picker = ref<InstanceType<typeof BaseSelect>>()
|
|
78
79
|
const canEdit = computed(() => selectedOption.value !== DEFAULT_ID)
|
|
79
80
|
const isEditing = ref(false)
|
|
80
81
|
|
|
@@ -102,6 +103,7 @@ const editingLabel = computed({
|
|
|
102
103
|
})
|
|
103
104
|
|
|
104
105
|
const savedNotif = ref(false)
|
|
106
|
+
const { polite } = useLiveAnnouncer()
|
|
105
107
|
const savedTimeout = useTimeoutFn(() => {
|
|
106
108
|
savedNotif.value = false
|
|
107
109
|
}, 1000)
|
|
@@ -114,9 +116,10 @@ async function savePreset() {
|
|
|
114
116
|
preset.state = toPresetState(props.variant.state, omitKeys)
|
|
115
117
|
savedNotif.value = true
|
|
116
118
|
savedTimeout.start()
|
|
119
|
+
polite('Preset saved')
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
function deletePreset(id) {
|
|
122
|
+
function deletePreset(id: string) {
|
|
120
123
|
// @TODO custom confirm modal UI
|
|
121
124
|
// eslint-disable-next-line no-alert
|
|
122
125
|
if (!confirm('Are you sure you want to delete this preset?')) {
|
|
@@ -141,11 +144,20 @@ async function startEditing() {
|
|
|
141
144
|
input.value?.select()
|
|
142
145
|
}
|
|
143
146
|
|
|
144
|
-
function stopEditing() {
|
|
147
|
+
async function stopEditing(refocus = false) {
|
|
148
|
+
if (!isEditing.value) {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
145
151
|
isEditing.value = false
|
|
152
|
+
// Finishing a rename from the keyboard unmounts the field that had focus, so
|
|
153
|
+
// hand it to the picker, which now shows the new name.
|
|
154
|
+
if (refocus) {
|
|
155
|
+
await nextTick()
|
|
156
|
+
picker.value?.focus()
|
|
157
|
+
}
|
|
146
158
|
}
|
|
147
159
|
|
|
148
|
-
onClickOutside(select, stopEditing)
|
|
160
|
+
onClickOutside(select, () => stopEditing())
|
|
149
161
|
</script>
|
|
150
162
|
|
|
151
163
|
<template>
|
|
@@ -154,79 +166,103 @@ onClickOutside(select, stopEditing)
|
|
|
154
166
|
ref="select"
|
|
155
167
|
class="flex-1 min-w-0"
|
|
156
168
|
>
|
|
169
|
+
<!-- Swapped for the picker rather than put inside it: a text field inside a
|
|
170
|
+
combobox is a control nested in a control (#828). -->
|
|
171
|
+
<input
|
|
172
|
+
v-if="isEditing"
|
|
173
|
+
ref="input"
|
|
174
|
+
v-model="editingLabel"
|
|
175
|
+
type="text"
|
|
176
|
+
aria-label="Preset name"
|
|
177
|
+
class="text-inherit bg-transparent w-full px-2 h-[27px] -my-1 border border-solid border-primary-500 rounded-sm outline-none"
|
|
178
|
+
@keydown.enter="stopEditing(true)"
|
|
179
|
+
@keydown.escape="stopEditing(true)"
|
|
180
|
+
>
|
|
157
181
|
<BaseSelect
|
|
182
|
+
v-else
|
|
183
|
+
ref="picker"
|
|
158
184
|
v-model="selectedOption"
|
|
185
|
+
label="Preset"
|
|
159
186
|
:options="presetsOptions"
|
|
160
187
|
@dblclick="startEditing()"
|
|
161
|
-
@keydown.enter="stopEditing()"
|
|
162
|
-
@keydown.escape="stopEditing()"
|
|
163
188
|
@select="id => applyPreset(id)"
|
|
164
|
-
|
|
165
|
-
<template #default="{ label }">
|
|
166
|
-
<input
|
|
167
|
-
v-if="isEditing"
|
|
168
|
-
ref="input"
|
|
169
|
-
v-model="editingLabel"
|
|
170
|
-
type="text"
|
|
171
|
-
class="text-inherit bg-transparent w-full h-full outline-none"
|
|
172
|
-
@click.stop.prevent
|
|
173
|
-
>
|
|
174
|
-
|
|
175
|
-
<div
|
|
176
|
-
v-else
|
|
177
|
-
class="flex items-center gap-2"
|
|
178
|
-
>
|
|
179
|
-
<span class="flex-1 truncate">
|
|
180
|
-
{{ label }}
|
|
181
|
-
</span>
|
|
182
|
-
<Icon
|
|
183
|
-
v-if="canEdit"
|
|
184
|
-
v-tooltip="'Rename this preset'"
|
|
185
|
-
icon="carbon:edit"
|
|
186
|
-
class="flex-none cursor-pointer w-4 h-4 hover:text-primary-500 opacity-50 hover:opacity-100 dark:hover:text-primary-400 text-gray-900 dark:text-gray-100"
|
|
187
|
-
@click.stop="startEditing()"
|
|
188
|
-
/>
|
|
189
|
-
</div>
|
|
190
|
-
</template>
|
|
191
|
-
|
|
192
|
-
<template #option="{ label, value }">
|
|
193
|
-
<div class="flex gap-2 items-center">
|
|
194
|
-
<span
|
|
195
|
-
class="flex-1 truncate"
|
|
196
|
-
>{{ label }}</span>
|
|
197
|
-
<Icon
|
|
198
|
-
v-if="value !== DEFAULT_ID"
|
|
199
|
-
v-tooltip="'Delete this preset'"
|
|
200
|
-
icon="carbon:trash-can"
|
|
201
|
-
class="flex-none cursor-pointer w-4 h-4 hover:text-primary-500 opacity-50 hover:opacity-100 dark:hover:text-primary-400 text-gray-900 dark:text-gray-100"
|
|
202
|
-
@click.stop="deletePreset(value)"
|
|
203
|
-
/>
|
|
204
|
-
</div>
|
|
205
|
-
</template>
|
|
206
|
-
</BaseSelect>
|
|
189
|
+
/>
|
|
207
190
|
</div>
|
|
208
|
-
<
|
|
191
|
+
<button
|
|
192
|
+
v-tooltip="canEdit ? 'Rename this preset' : null"
|
|
193
|
+
type="button"
|
|
194
|
+
data-testid="preset-rename"
|
|
195
|
+
aria-label="Rename preset"
|
|
196
|
+
:disabled="!canEdit"
|
|
197
|
+
class="flex-none flex p-0 bg-transparent border-0 cursor-pointer text-gray-900 dark:text-gray-100 hover:text-primary-500 dark:hover:text-primary-400"
|
|
198
|
+
:class="[
|
|
199
|
+
canEdit ? 'opacity-50 hover:opacity-100 focus-visible:opacity-100' : 'opacity-25 pointer-events-none',
|
|
200
|
+
]"
|
|
201
|
+
@click="startEditing()"
|
|
202
|
+
>
|
|
203
|
+
<Icon
|
|
204
|
+
icon="carbon:edit"
|
|
205
|
+
class="w-4 h-4"
|
|
206
|
+
/>
|
|
207
|
+
</button>
|
|
208
|
+
<button
|
|
209
|
+
v-tooltip="canEdit ? 'Delete this preset' : null"
|
|
210
|
+
type="button"
|
|
211
|
+
data-testid="preset-delete"
|
|
212
|
+
aria-label="Delete preset"
|
|
213
|
+
:disabled="!canEdit"
|
|
214
|
+
class="flex-none flex p-0 bg-transparent border-0 cursor-pointer text-gray-900 dark:text-gray-100 hover:text-primary-500 dark:hover:text-primary-400"
|
|
215
|
+
:class="[
|
|
216
|
+
canEdit ? 'opacity-50 hover:opacity-100 focus-visible:opacity-100' : 'opacity-25 pointer-events-none',
|
|
217
|
+
]"
|
|
218
|
+
@click="deletePreset(selectedOption)"
|
|
219
|
+
>
|
|
220
|
+
<Icon
|
|
221
|
+
icon="carbon:trash-can"
|
|
222
|
+
class="w-4 h-4"
|
|
223
|
+
/>
|
|
224
|
+
</button>
|
|
225
|
+
<button
|
|
209
226
|
v-tooltip="savedNotif ? 'Saved!' : canEdit ? 'Save to preset' : null"
|
|
227
|
+
type="button"
|
|
210
228
|
data-testid="preset-save"
|
|
211
|
-
|
|
212
|
-
|
|
229
|
+
aria-label="Save to preset"
|
|
230
|
+
:disabled="!canEdit"
|
|
231
|
+
class="flex-none flex p-0 bg-transparent border-0 cursor-pointer text-gray-900 dark:text-gray-100 hover:text-primary-500 dark:hover:text-primary-400"
|
|
213
232
|
:class="[
|
|
214
|
-
canEdit ? 'opacity-50 hover:opacity-100' : 'opacity-25 pointer-events-none',
|
|
233
|
+
canEdit ? 'opacity-50 hover:opacity-100 focus-visible:opacity-100' : 'opacity-25 pointer-events-none',
|
|
215
234
|
]"
|
|
216
235
|
@click="savePreset()"
|
|
217
|
-
|
|
218
|
-
|
|
236
|
+
>
|
|
237
|
+
<Icon
|
|
238
|
+
:icon="savedNotif ? 'carbon:checkmark' : 'carbon:save'"
|
|
239
|
+
class="w-4 h-4"
|
|
240
|
+
/>
|
|
241
|
+
</button>
|
|
242
|
+
<button
|
|
219
243
|
v-tooltip="'Create new preset'"
|
|
244
|
+
type="button"
|
|
220
245
|
data-testid="preset-create"
|
|
221
|
-
|
|
222
|
-
class="cursor-pointer
|
|
246
|
+
aria-label="Create new preset"
|
|
247
|
+
class="flex-none flex p-0 bg-transparent border-0 cursor-pointer text-gray-900 dark:text-gray-100 hover:text-primary-500 dark:hover:text-primary-400 opacity-50 hover:opacity-100 focus-visible:opacity-100"
|
|
223
248
|
@click="createPreset()"
|
|
224
|
-
|
|
225
|
-
|
|
249
|
+
>
|
|
250
|
+
<Icon
|
|
251
|
+
icon="carbon:add-alt"
|
|
252
|
+
class="w-4 h-4"
|
|
253
|
+
/>
|
|
254
|
+
</button>
|
|
255
|
+
<button
|
|
226
256
|
v-tooltip="'Reset to initial state'"
|
|
227
|
-
|
|
228
|
-
|
|
257
|
+
type="button"
|
|
258
|
+
aria-label="Reset to initial state"
|
|
259
|
+
class="flex-none flex p-0 bg-transparent border-0 cursor-pointer text-gray-900 dark:text-gray-100 hover:text-primary-500 dark:hover:text-primary-400 opacity-50 hover:opacity-100 focus-visible:opacity-100"
|
|
229
260
|
@click="resetState()"
|
|
230
|
-
|
|
261
|
+
>
|
|
262
|
+
<Icon
|
|
263
|
+
icon="carbon:reset"
|
|
264
|
+
class="w-4 h-4"
|
|
265
|
+
/>
|
|
266
|
+
</button>
|
|
231
267
|
</div>
|
|
232
268
|
</template>
|
|
@@ -58,7 +58,7 @@ watch(() => [props.variant, generateSourceCodeFn.value], async () => {
|
|
|
58
58
|
}
|
|
59
59
|
catch (e) {
|
|
60
60
|
console.error(e)
|
|
61
|
-
error.value = e.message
|
|
61
|
+
error.value = e instanceof Error ? e.message : String(e)
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// Auto-switch
|
|
@@ -121,8 +121,8 @@ watch(() => props.variant, () => {
|
|
|
121
121
|
|
|
122
122
|
const scroller = ref<HTMLElement>()
|
|
123
123
|
|
|
124
|
-
function onScroll(event) {
|
|
125
|
-
if (sourceHtml.value) {
|
|
124
|
+
function onScroll(event: Event) {
|
|
125
|
+
if (sourceHtml.value && event.target instanceof HTMLElement) {
|
|
126
126
|
lastScroll = event.target.scrollTop
|
|
127
127
|
}
|
|
128
128
|
}
|
|
@@ -156,7 +156,7 @@ watch(sourceHtml, async () => {
|
|
|
156
156
|
class="flex items-center gap-1 h-full px-1 bg-gray-500/10 rounded-l transition-all ease-[cubic-bezier(0,1,.6,1)] duration-300 overflow-hidden"
|
|
157
157
|
:class="[
|
|
158
158
|
displayedSource !== 'dynamic' ? 'max-w-6 opacity-70' : 'max-w-[82px] text-primary-800 dark:text-primary-400',
|
|
159
|
-
dynamicSourceCode ? 'cursor-pointer hover:bg-gray-500/30 active:bg-gray-600/50' : 'opacity-50',
|
|
159
|
+
dynamicSourceCode ? 'cursor-pointer hover:bg-gray-500/30 active:bg-gray-600/50' : displayedSource !== 'dynamic' && 'opacity-50',
|
|
160
160
|
]"
|
|
161
161
|
@click="dynamicSourceCode && (displayedSource = 'dynamic')"
|
|
162
162
|
>
|
|
@@ -178,7 +178,7 @@ watch(sourceHtml, async () => {
|
|
|
178
178
|
class="flex items-center gap-1 h-full px-1 bg-gray-500/10 rounded-r transition-all ease-[cubic-bezier(0,1,.6,1)] duration-300 overflow-hidden"
|
|
179
179
|
:class="[
|
|
180
180
|
displayedSource !== 'static' ? 'max-w-6 opacity-70' : 'max-w-[63px] text-primary-800 dark:text-primary-400',
|
|
181
|
-
staticSourceCode ? 'cursor-pointer hover:bg-gray-500/30 active:bg-gray-600/50' : 'opacity-50',
|
|
181
|
+
staticSourceCode ? 'cursor-pointer hover:bg-gray-500/30 active:bg-gray-600/50' : displayedSource !== 'static' && 'opacity-50',
|
|
182
182
|
]"
|
|
183
183
|
@click="staticSourceCode && (displayedSource = 'static')"
|
|
184
184
|
>
|
|
@@ -223,6 +223,7 @@ watch(sourceHtml, async () => {
|
|
|
223
223
|
ref="scroller"
|
|
224
224
|
class="__poveste-code-placeholder w-full h-full p-4 outline-none bg-transparent resize-none m-0"
|
|
225
225
|
:value="displayedSourceCode"
|
|
226
|
+
aria-label="Source code"
|
|
226
227
|
readonly
|
|
227
228
|
data-testid="story-source-code"
|
|
228
229
|
@scroll="onScroll"
|
|
@@ -138,8 +138,7 @@ watch(rateLimitedSearch, async (value) => {
|
|
|
138
138
|
break
|
|
139
139
|
}
|
|
140
140
|
case 'variant': {
|
|
141
|
-
const
|
|
142
|
-
const story = storyStore.getStoryById(storyId)
|
|
141
|
+
const story = storyStore.getStoryById(idMapData.id.replace(/:.*$/s, ''))
|
|
143
142
|
const variant = storyStore.getVariantById(idMapData.id)
|
|
144
143
|
if (!story || !variant) continue
|
|
145
144
|
list.push(variantResultFactory(story, variant, rank))
|
|
@@ -268,7 +267,7 @@ const results = computed<SearchResult[]>(() => {
|
|
|
268
267
|
...commandResults.value,
|
|
269
268
|
...titleResults.value,
|
|
270
269
|
]
|
|
271
|
-
const seen = {}
|
|
270
|
+
const seen: Record<string, boolean> = {}
|
|
272
271
|
for (const r of titleResults.value) {
|
|
273
272
|
seen[r.id] = true
|
|
274
273
|
}
|
|
@@ -101,7 +101,9 @@ function useDragger(el: Ref<HTMLDivElement | undefined>, value: Ref<number | nul
|
|
|
101
101
|
function onTouchStart(event: TouchEvent) {
|
|
102
102
|
event.preventDefault()
|
|
103
103
|
event.stopPropagation()
|
|
104
|
-
const
|
|
104
|
+
const [touch] = event.touches
|
|
105
|
+
if (!touch) return
|
|
106
|
+
const start = axis === 'x' ? touch.clientX : touch.clientY
|
|
105
107
|
const startValue = startFrom()
|
|
106
108
|
resizing.value = true
|
|
107
109
|
|
|
@@ -112,7 +114,9 @@ function useDragger(el: Ref<HTMLDivElement | undefined>, value: Ref<number | nul
|
|
|
112
114
|
]
|
|
113
115
|
|
|
114
116
|
function onTouchMove(event: TouchEvent) {
|
|
115
|
-
const
|
|
117
|
+
const [touch] = event.touches
|
|
118
|
+
if (!touch) return
|
|
119
|
+
const delta = (axis === 'x' ? touch.clientX : touch.clientY) - start
|
|
116
120
|
value.value = Math.max(min, Math.min(max, startValue + delta))
|
|
117
121
|
}
|
|
118
122
|
|
|
@@ -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
|
|
@@ -47,7 +47,9 @@ watch(() => storyStore.currentVariant, (value) => {
|
|
|
47
47
|
watch(() => [storyStore.currentStory, storyStore.currentVariant], () => {
|
|
48
48
|
if (!storyStore.currentVariant && autoSelectsVariant(storyStore.currentStory)) {
|
|
49
49
|
const variant = storyStore.currentStory.lastSelectedVariant ?? storyStore.currentStory.variants[0]
|
|
50
|
-
|
|
50
|
+
if (variant) {
|
|
51
|
+
setVariant(variant.id)
|
|
52
|
+
}
|
|
51
53
|
}
|
|
52
54
|
}, {
|
|
53
55
|
immediate: true,
|
|
@@ -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/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.
|
|
@@ -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
|