@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
|
@@ -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,12 +53,17 @@ 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
|
}
|
|
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
|
|
|
@@ -70,6 +75,7 @@ onMounted(() => {
|
|
|
70
75
|
|
|
71
76
|
const input = ref<HTMLInputElement>()
|
|
72
77
|
const select = ref<HTMLInputElement>()
|
|
78
|
+
const picker = ref<InstanceType<typeof BaseSelect>>()
|
|
73
79
|
const canEdit = computed(() => selectedOption.value !== DEFAULT_ID)
|
|
74
80
|
const isEditing = ref(false)
|
|
75
81
|
|
|
@@ -80,10 +86,24 @@ async function createPreset() {
|
|
|
80
86
|
selectedOption.value = id
|
|
81
87
|
isEditing.value = true
|
|
82
88
|
await nextTick()
|
|
83
|
-
input
|
|
89
|
+
// The input is rendered by the edit state set above, so it exists by now —
|
|
90
|
+
// unless editing was cancelled during the tick, which unmounts it.
|
|
91
|
+
input.value?.select()
|
|
84
92
|
}
|
|
85
93
|
|
|
94
|
+
// The template bound `v-model` straight to `presetStates.get(selectedOption).label`,
|
|
95
|
+
// which is a Map lookup that can miss. Naming it once keeps the template honest
|
|
96
|
+
// and gives the miss somewhere to go.
|
|
97
|
+
const editingLabel = computed({
|
|
98
|
+
get: () => presetStates.value.get(selectedOption.value)?.label ?? '',
|
|
99
|
+
set: (value) => {
|
|
100
|
+
const preset = presetStates.value.get(selectedOption.value)
|
|
101
|
+
if (preset) preset.label = value
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
|
|
86
105
|
const savedNotif = ref(false)
|
|
106
|
+
const { polite } = useLiveAnnouncer()
|
|
87
107
|
const savedTimeout = useTimeoutFn(() => {
|
|
88
108
|
savedNotif.value = false
|
|
89
109
|
}, 1000)
|
|
@@ -92,12 +112,14 @@ async function savePreset() {
|
|
|
92
112
|
if (!canEdit.value) return
|
|
93
113
|
|
|
94
114
|
const preset = presetStates.value.get(selectedOption.value)
|
|
115
|
+
if (!preset) return
|
|
95
116
|
preset.state = toPresetState(props.variant.state, omitKeys)
|
|
96
117
|
savedNotif.value = true
|
|
97
118
|
savedTimeout.start()
|
|
119
|
+
polite('Preset saved')
|
|
98
120
|
}
|
|
99
121
|
|
|
100
|
-
function deletePreset(id) {
|
|
122
|
+
function deletePreset(id: string) {
|
|
101
123
|
// @TODO custom confirm modal UI
|
|
102
124
|
// eslint-disable-next-line no-alert
|
|
103
125
|
if (!confirm('Are you sure you want to delete this preset?')) {
|
|
@@ -117,14 +139,25 @@ async function startEditing() {
|
|
|
117
139
|
|
|
118
140
|
isEditing.value = true
|
|
119
141
|
await nextTick()
|
|
120
|
-
input
|
|
142
|
+
// The input is rendered by the edit state set above, so it exists by now —
|
|
143
|
+
// unless editing was cancelled during the tick, which unmounts it.
|
|
144
|
+
input.value?.select()
|
|
121
145
|
}
|
|
122
146
|
|
|
123
|
-
function stopEditing() {
|
|
147
|
+
async function stopEditing(refocus = false) {
|
|
148
|
+
if (!isEditing.value) {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
124
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
|
+
}
|
|
125
158
|
}
|
|
126
159
|
|
|
127
|
-
onClickOutside(select, stopEditing)
|
|
160
|
+
onClickOutside(select, () => stopEditing())
|
|
128
161
|
</script>
|
|
129
162
|
|
|
130
163
|
<template>
|
|
@@ -133,79 +166,103 @@ onClickOutside(select, stopEditing)
|
|
|
133
166
|
ref="select"
|
|
134
167
|
class="flex-1 min-w-0"
|
|
135
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
|
+
>
|
|
136
181
|
<BaseSelect
|
|
182
|
+
v-else
|
|
183
|
+
ref="picker"
|
|
137
184
|
v-model="selectedOption"
|
|
185
|
+
label="Preset"
|
|
138
186
|
:options="presetsOptions"
|
|
139
187
|
@dblclick="startEditing()"
|
|
140
|
-
@keydown.enter="stopEditing()"
|
|
141
|
-
@keydown.escape="stopEditing()"
|
|
142
188
|
@select="id => applyPreset(id)"
|
|
143
|
-
|
|
144
|
-
<template #default="{ label }">
|
|
145
|
-
<input
|
|
146
|
-
v-if="isEditing"
|
|
147
|
-
ref="input"
|
|
148
|
-
v-model="presetStates.get(selectedOption).label"
|
|
149
|
-
type="text"
|
|
150
|
-
class="text-inherit bg-transparent w-full h-full outline-none"
|
|
151
|
-
@click.stop.prevent
|
|
152
|
-
>
|
|
153
|
-
|
|
154
|
-
<div
|
|
155
|
-
v-else
|
|
156
|
-
class="flex items-center gap-2"
|
|
157
|
-
>
|
|
158
|
-
<span class="flex-1 truncate">
|
|
159
|
-
{{ label }}
|
|
160
|
-
</span>
|
|
161
|
-
<Icon
|
|
162
|
-
v-if="canEdit"
|
|
163
|
-
v-tooltip="'Rename this preset'"
|
|
164
|
-
icon="carbon:edit"
|
|
165
|
-
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"
|
|
166
|
-
@click.stop="startEditing()"
|
|
167
|
-
/>
|
|
168
|
-
</div>
|
|
169
|
-
</template>
|
|
170
|
-
|
|
171
|
-
<template #option="{ label, value }">
|
|
172
|
-
<div class="flex gap-2 items-center">
|
|
173
|
-
<span
|
|
174
|
-
class="flex-1 truncate"
|
|
175
|
-
>{{ label }}</span>
|
|
176
|
-
<Icon
|
|
177
|
-
v-if="value !== DEFAULT_ID"
|
|
178
|
-
v-tooltip="'Delete this preset'"
|
|
179
|
-
icon="carbon:trash-can"
|
|
180
|
-
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"
|
|
181
|
-
@click.stop="deletePreset(value)"
|
|
182
|
-
/>
|
|
183
|
-
</div>
|
|
184
|
-
</template>
|
|
185
|
-
</BaseSelect>
|
|
189
|
+
/>
|
|
186
190
|
</div>
|
|
187
|
-
<
|
|
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
|
|
188
226
|
v-tooltip="savedNotif ? 'Saved!' : canEdit ? 'Save to preset' : null"
|
|
227
|
+
type="button"
|
|
189
228
|
data-testid="preset-save"
|
|
190
|
-
|
|
191
|
-
|
|
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"
|
|
192
232
|
:class="[
|
|
193
|
-
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',
|
|
194
234
|
]"
|
|
195
235
|
@click="savePreset()"
|
|
196
|
-
|
|
197
|
-
|
|
236
|
+
>
|
|
237
|
+
<Icon
|
|
238
|
+
:icon="savedNotif ? 'carbon:checkmark' : 'carbon:save'"
|
|
239
|
+
class="w-4 h-4"
|
|
240
|
+
/>
|
|
241
|
+
</button>
|
|
242
|
+
<button
|
|
198
243
|
v-tooltip="'Create new preset'"
|
|
244
|
+
type="button"
|
|
199
245
|
data-testid="preset-create"
|
|
200
|
-
|
|
201
|
-
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"
|
|
202
248
|
@click="createPreset()"
|
|
203
|
-
|
|
204
|
-
|
|
249
|
+
>
|
|
250
|
+
<Icon
|
|
251
|
+
icon="carbon:add-alt"
|
|
252
|
+
class="w-4 h-4"
|
|
253
|
+
/>
|
|
254
|
+
</button>
|
|
255
|
+
<button
|
|
205
256
|
v-tooltip="'Reset to initial state'"
|
|
206
|
-
|
|
207
|
-
|
|
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"
|
|
208
260
|
@click="resetState()"
|
|
209
|
-
|
|
261
|
+
>
|
|
262
|
+
<Icon
|
|
263
|
+
icon="carbon:reset"
|
|
264
|
+
class="w-4 h-4"
|
|
265
|
+
/>
|
|
266
|
+
</button>
|
|
210
267
|
</div>
|
|
211
268
|
</template>
|
|
@@ -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
|
|
|
@@ -22,7 +22,7 @@ const innerOrientation = computed(() =>
|
|
|
22
22
|
const route = useRoute()
|
|
23
23
|
|
|
24
24
|
const panelContentComponent = computed(() => {
|
|
25
|
-
switch (route.query
|
|
25
|
+
switch (route.query['tab']) {
|
|
26
26
|
case 'docs':
|
|
27
27
|
return StoryDocs
|
|
28
28
|
case 'events':
|
|
@@ -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
|
|
@@ -55,7 +58,7 @@ watch(() => [props.variant, generateSourceCodeFn.value], async () => {
|
|
|
55
58
|
}
|
|
56
59
|
catch (e) {
|
|
57
60
|
console.error(e)
|
|
58
|
-
error.value = e.message
|
|
61
|
+
error.value = e instanceof Error ? e.message : String(e)
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
// Auto-switch
|
|
@@ -118,8 +121,8 @@ watch(() => props.variant, () => {
|
|
|
118
121
|
|
|
119
122
|
const scroller = ref<HTMLElement>()
|
|
120
123
|
|
|
121
|
-
function onScroll(event) {
|
|
122
|
-
if (sourceHtml.value) {
|
|
124
|
+
function onScroll(event: Event) {
|
|
125
|
+
if (sourceHtml.value && event.target instanceof HTMLElement) {
|
|
123
126
|
lastScroll = event.target.scrollTop
|
|
124
127
|
}
|
|
125
128
|
}
|
|
@@ -153,7 +156,7 @@ watch(sourceHtml, async () => {
|
|
|
153
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"
|
|
154
157
|
:class="[
|
|
155
158
|
displayedSource !== 'dynamic' ? 'max-w-6 opacity-70' : 'max-w-[82px] text-primary-800 dark:text-primary-400',
|
|
156
|
-
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',
|
|
157
160
|
]"
|
|
158
161
|
@click="dynamicSourceCode && (displayedSource = 'dynamic')"
|
|
159
162
|
>
|
|
@@ -175,7 +178,7 @@ watch(sourceHtml, async () => {
|
|
|
175
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"
|
|
176
179
|
:class="[
|
|
177
180
|
displayedSource !== 'static' ? 'max-w-6 opacity-70' : 'max-w-[63px] text-primary-800 dark:text-primary-400',
|
|
178
|
-
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',
|
|
179
182
|
]"
|
|
180
183
|
@click="staticSourceCode && (displayedSource = 'static')"
|
|
181
184
|
>
|
|
@@ -220,6 +223,7 @@ watch(sourceHtml, async () => {
|
|
|
220
223
|
ref="scroller"
|
|
221
224
|
class="__poveste-code-placeholder w-full h-full p-4 outline-none bg-transparent resize-none m-0"
|
|
222
225
|
:value="displayedSourceCode"
|
|
226
|
+
aria-label="Source code"
|
|
223
227
|
readonly
|
|
224
228
|
data-testid="story-source-code"
|
|
225
229
|
@scroll="onScroll"
|
|
@@ -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,14 +129,18 @@ 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
|
}
|
|
134
140
|
case 'variant': {
|
|
135
|
-
const
|
|
136
|
-
const story = storyStore.getStoryById(storyId)
|
|
141
|
+
const story = storyStore.getStoryById(idMapData.id.replace(/:.*$/s, ''))
|
|
137
142
|
const variant = storyStore.getVariantById(idMapData.id)
|
|
143
|
+
if (!story || !variant) continue
|
|
138
144
|
list.push(variantResultFactory(story, variant, rank))
|
|
139
145
|
rank++
|
|
140
146
|
break
|
|
@@ -158,7 +164,9 @@ async function searchOnDocField(query: string) {
|
|
|
158
164
|
if (!idMapData) continue
|
|
159
165
|
switch (idMapData.kind) {
|
|
160
166
|
case 'story': {
|
|
161
|
-
|
|
167
|
+
const story = storyStore.getStoryById(idMapData.id)
|
|
168
|
+
if (!story) continue
|
|
169
|
+
list.push(storyResultFactory(story, rank, 'docs'))
|
|
162
170
|
rank++
|
|
163
171
|
break
|
|
164
172
|
}
|
|
@@ -188,7 +196,7 @@ function storyResultFactory(story: Story, rank: number, type: SearchResultType =
|
|
|
188
196
|
: {},
|
|
189
197
|
},
|
|
190
198
|
},
|
|
191
|
-
path: story.file
|
|
199
|
+
path: story.file?.path.slice(0, -1) ?? [],
|
|
192
200
|
icon: story.icon,
|
|
193
201
|
iconColor: story.iconColor,
|
|
194
202
|
}
|
|
@@ -212,7 +220,7 @@ function variantResultFactory(story: Story, variant: Variant, rank: number, type
|
|
|
212
220
|
: {},
|
|
213
221
|
},
|
|
214
222
|
},
|
|
215
|
-
path: [...story.file
|
|
223
|
+
path: [...story.file?.path ?? [], story.title],
|
|
216
224
|
icon: variant.icon,
|
|
217
225
|
iconColor: variant.iconColor,
|
|
218
226
|
}
|
|
@@ -259,7 +267,7 @@ const results = computed<SearchResult[]>(() => {
|
|
|
259
267
|
...commandResults.value,
|
|
260
268
|
...titleResults.value,
|
|
261
269
|
]
|
|
262
|
-
const seen = {}
|
|
270
|
+
const seen: Record<string, boolean> = {}
|
|
263
271
|
for (const r of titleResults.value) {
|
|
264
272
|
seen[r.id] = true
|
|
265
273
|
}
|
|
@@ -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
|
}
|
|
@@ -76,8 +101,10 @@ function useDragger(el: Ref<HTMLDivElement>, value: Ref<number>, min: number, ma
|
|
|
76
101
|
function onTouchStart(event: TouchEvent) {
|
|
77
102
|
event.preventDefault()
|
|
78
103
|
event.stopPropagation()
|
|
79
|
-
const
|
|
80
|
-
|
|
104
|
+
const [touch] = event.touches
|
|
105
|
+
if (!touch) return
|
|
106
|
+
const start = axis === 'x' ? touch.clientX : touch.clientY
|
|
107
|
+
const startValue = startFrom()
|
|
81
108
|
resizing.value = true
|
|
82
109
|
|
|
83
110
|
const removeListeners = [
|
|
@@ -87,7 +114,9 @@ function useDragger(el: Ref<HTMLDivElement>, value: Ref<number>, min: number, ma
|
|
|
87
114
|
]
|
|
88
115
|
|
|
89
116
|
function onTouchMove(event: TouchEvent) {
|
|
90
|
-
const
|
|
117
|
+
const [touch] = event.touches
|
|
118
|
+
if (!touch) return
|
|
119
|
+
const delta = (axis === 'x' ? touch.clientX : touch.clientY) - start
|
|
91
120
|
value.value = Math.max(min, Math.min(max, startValue + delta))
|
|
92
121
|
}
|
|
93
122
|
|