@poveste/app 0.5.3 → 0.6.1
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/app/style/font.css +78 -0
- package/dist/app/style/main.pcss +4 -1
- package/dist/app/style/sandbox.css +3 -0
- package/dist/bundle-main-dev.js +1 -0
- package/dist/bundle-main.js +3 -0
- package/dist/bundled/app.css +28 -20
- package/dist/bundled/components/app/AppActions.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/bundled/components/base/BaseSelect.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/bundled/components/layout/LayoutModal.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/bundled/components/layout/LayoutPane.vue_vue_type_script_setup_true_lang.js +4 -4
- package/dist/bundled/components/panel/StatePresets.vue_vue_type_script_setup_true_lang.js +5 -5
- 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 +1 -1
- package/dist/bundled/components/panel/StoryEvent.vue_vue_type_script_setup_true_lang.js +1 -1
- 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 +2 -2
- package/dist/bundled/components/search/SearchItem.js +1 -1
- package/dist/bundled/components/search/SearchItem.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/bundled/components/search/SearchModal.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/bundled/components/search/SearchPane.vue_vue_type_script_setup_true_lang.js +1 -0
- package/dist/bundled/components/story/StoryResponsivePreview.js +1 -1
- package/dist/bundled/components/story/StoryResponsivePreview.vue_vue_type_script_setup_true_lang.js +69 -64
- package/dist/bundled/components/story/StoryVariantGrid.js +3 -1
- package/dist/bundled/components/story/StoryVariantGrid.vue_vue_type_script_setup_true_lang.js +82 -35
- package/dist/bundled/components/story/StoryVariantGridItem.js +1 -1
- package/dist/bundled/components/story/StoryVariantGridItem.vue_vue_type_script_setup_true_lang.js +20 -12
- package/dist/bundled/components/story/StoryVariantListItem.js +1 -1
- package/dist/bundled/components/story/StoryVariantListItem.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/bundled/components/story/StoryVariantSinglePreviewNative.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/bundled/components/story/StoryVariantSinglePreviewRemote.vue_vue_type_script_setup_true_lang.js +97 -32
- package/dist/bundled/components/story/StoryVariantSingleView.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/bundled/components/toolbar/ToolbarBackground.js +1 -1
- package/dist/bundled/components/toolbar/ToolbarBackground.vue_vue_type_script_setup_true_lang.js +7 -7
- package/dist/bundled/components/tree/StoryGroup.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/bundled/components/tree/StoryListFolder.js +1 -1
- package/dist/bundled/components/tree/StoryListFolder.vue_vue_type_script_setup_true_lang.js +2 -2
- 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/index.js +3 -0
- package/dist/bundled/sandbox.js +53 -24
- package/dist/bundled/util/const.js +2 -1
- package/dist/bundled/util/grid-cell-height.js +18 -0
- package/dist/bundled/util/grid-slots.js +64 -0
- package/dist/bundled/util/icons.generated.js +104 -0
- package/dist/bundled/util/state-bridge.js +75 -0
- package/dist/bundled/util/state.js +19 -1
- package/dist/style.css +7 -2
- package/package.json +12 -5
- package/src/__tests__/font.spec.ts +30 -0
- package/src/__tests__/grid-slots.spec.ts +84 -0
- package/src/__tests__/icons.spec.ts +26 -0
- package/src/__tests__/state-bridge.spec.ts +155 -0
- package/src/__tests__/state-clone.spec.ts +66 -0
- package/src/app/components/app/AppActions.vue +2 -2
- package/src/app/components/layout/LayoutModal.vue +2 -2
- package/src/app/components/layout/LayoutPane.vue +4 -4
- package/src/app/components/panel/StatePresets.vue +5 -5
- package/src/app/components/panel/StoryControls.vue +1 -1
- package/src/app/components/panel/StoryDocs.vue +1 -1
- package/src/app/components/panel/StoryEvent.vue +1 -1
- package/src/app/components/panel/StorySidePanel.vue +1 -1
- package/src/app/components/panel/StorySourceCode.vue +2 -2
- package/src/app/components/search/SearchItem.vue +1 -1
- package/src/app/components/search/SearchModal.vue +1 -1
- package/src/app/components/search/SearchPane.vue +8 -0
- package/src/app/components/story/StoryResponsivePreview.vue +85 -50
- package/src/app/components/story/StoryVariantGrid.vue +126 -49
- package/src/app/components/story/StoryVariantGridItem.vue +35 -10
- package/src/app/components/story/StoryVariantListItem.vue +1 -1
- package/src/app/components/story/StoryVariantSinglePreviewNative.vue +1 -1
- package/src/app/components/story/StoryVariantSinglePreviewRemote.vue +165 -35
- package/src/app/components/story/StoryVariantSingleView.vue +1 -1
- package/src/app/components/toolbar/ToolbarBackground.vue +4 -4
- package/src/app/components/tree/StoryGroup.vue +1 -1
- package/src/app/components/tree/StoryListFolder.vue +1 -1
- package/src/app/components/tree/StoryListItem.vue +1 -1
- package/src/app/index.ts +10 -0
- package/src/app/sandbox.ts +95 -31
- package/src/app/style/font.css +78 -0
- package/src/app/style/main.pcss +4 -1
- package/src/app/style/sandbox.css +3 -0
- package/src/app/util/const.ts +1 -0
- package/src/app/util/grid-cell-height.ts +16 -0
- package/src/app/util/grid-slots.ts +72 -0
- package/src/app/util/icons.generated.ts +9 -0
- package/src/app/util/state-bridge.ts +95 -0
- package/src/app/util/state.ts +19 -0
- package/src/bundle-main-dev.js +1 -0
- package/src/bundle-main.js +3 -0
|
@@ -9,9 +9,16 @@ import { usePreviewSettingsStore } from '../../stores/preview-settings'
|
|
|
9
9
|
import CheckerboardPattern from '../misc/CheckerboardPattern.vue'
|
|
10
10
|
import HatchedPattern from '../misc/HatchedPattern.vue'
|
|
11
11
|
|
|
12
|
-
const props = defineProps<{
|
|
12
|
+
const props = withDefaults(defineProps<{
|
|
13
13
|
variant: Variant
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Whether this preview is one a reader can size. False in a grid, where the
|
|
16
|
+
* cell is sized by the column and the story it renders (#257).
|
|
17
|
+
*/
|
|
18
|
+
responsive?: boolean
|
|
19
|
+
}>(), {
|
|
20
|
+
responsive: true,
|
|
21
|
+
})
|
|
15
22
|
|
|
16
23
|
const settings = usePreviewSettingsStore().currentSettings
|
|
17
24
|
|
|
@@ -116,7 +123,7 @@ const finalHeight = computed(() => settings.rotate ? settings.responsiveWidth :
|
|
|
116
123
|
|
|
117
124
|
// Disabled responsive
|
|
118
125
|
|
|
119
|
-
const isResponsiveEnabled = computed(() => !props.variant.responsiveDisabled)
|
|
126
|
+
const isResponsiveEnabled = computed(() => props.responsive && !props.variant.responsiveDisabled)
|
|
120
127
|
|
|
121
128
|
const sizeTooltip = computed(() => `${responsiveWidth.value ?? 'Auto'} × ${responsiveHeight.value ?? 'Auto'}`)
|
|
122
129
|
</script>
|
|
@@ -137,22 +144,47 @@ const sizeTooltip = computed(() => `${responsiveWidth.value ?? 'Auto'} × ${resp
|
|
|
137
144
|
class="h-full overflow-auto relative"
|
|
138
145
|
>
|
|
139
146
|
<div
|
|
140
|
-
class="
|
|
141
|
-
:class="
|
|
142
|
-
'
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
class="rounded-lg relative"
|
|
148
|
+
:class="[
|
|
149
|
+
// The surface, the reader's background, the checkerboard and the
|
|
150
|
+
// padding below are all skipped in a grid cell, where the item around
|
|
151
|
+
// this preview already paints and pads them — rendering them again
|
|
152
|
+
// stacked a second identical box inside the first and doubled the
|
|
153
|
+
// padding (#264). Gated on `responsive`, not `isResponsiveEnabled`:
|
|
154
|
+
// a single-view story with `responsiveDisabled` still needs its own
|
|
155
|
+
// chrome, it just loses the draggers.
|
|
156
|
+
responsive ? 'bg-white dark:bg-gray-700' : '',
|
|
157
|
+
isResponsiveEnabled ? {
|
|
158
|
+
'w-fit': !!finalWidth,
|
|
159
|
+
'h-fit': !!finalHeight,
|
|
160
|
+
'h-full': !finalHeight,
|
|
161
|
+
} : 'h-full',
|
|
162
|
+
// Sized to content, so nothing can overflow it and clipping keeps the
|
|
163
|
+
// corners. Sized by its container, a taller story overflows — and
|
|
164
|
+
// clipping there put the content past the fold out of reach entirely,
|
|
165
|
+
// since the scroll parent then sees nothing to scroll (#258).
|
|
166
|
+
isResponsiveEnabled && finalHeight ? 'overflow-hidden' : 'overflow-auto',
|
|
167
|
+
]"
|
|
146
168
|
>
|
|
169
|
+
<!-- `min-h-full`, not `h-full`: when this paints the reader's preview
|
|
170
|
+
background the box above it scrolls, so a story taller than the
|
|
171
|
+
preview would otherwise be shown against the bare box below the
|
|
172
|
+
first screenful. The checkerboard is measured against this for the
|
|
173
|
+
same reason — but the draggers are not, since they belong to the
|
|
174
|
+
visible box rather than to the story's full height. -->
|
|
147
175
|
<div
|
|
148
|
-
class="
|
|
149
|
-
|
|
176
|
+
class="rounded-lg min-h-full relative"
|
|
177
|
+
:class="responsive ? 'bind-preview-bg' : ''"
|
|
178
|
+
:data-testid="responsive ? 'responsive-preview-bg' : undefined"
|
|
150
179
|
>
|
|
151
180
|
<CheckerboardPattern
|
|
152
|
-
v-if="settings.checkerboard"
|
|
181
|
+
v-if="settings.checkerboard && responsive"
|
|
153
182
|
class="absolute inset-0 w-full h-full text-gray-500/20"
|
|
154
183
|
/>
|
|
155
|
-
<div
|
|
184
|
+
<div
|
|
185
|
+
class="h-full relative"
|
|
186
|
+
:class="responsive ? 'p-8' : ''"
|
|
187
|
+
>
|
|
156
188
|
<div class="w-full h-full relative">
|
|
157
189
|
<div class="absolute inset-0" />
|
|
158
190
|
|
|
@@ -164,46 +196,49 @@ const sizeTooltip = computed(() => `${responsiveWidth.value ?? 'Auto'} × ${resp
|
|
|
164
196
|
/>
|
|
165
197
|
</div>
|
|
166
198
|
|
|
167
|
-
<!-- Markers
|
|
168
|
-
|
|
169
|
-
<
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
199
|
+
<!-- Markers: the ruler ticks for the size the draggers set, so they
|
|
200
|
+
go wherever the draggers do (#257). -->
|
|
201
|
+
<template v-if="isResponsiveEnabled">
|
|
202
|
+
<div class="absolute top-5 left-8 h-2 w-px bg-gray-400/25" />
|
|
203
|
+
<div class="absolute top-5 right-8 h-2 w-px bg-gray-400/25" />
|
|
204
|
+
<div class="absolute bottom-5 left-8 h-2 w-px bg-gray-400/25" />
|
|
205
|
+
<div class="absolute bottom-5 right-8 h-2 w-px bg-gray-400/25" />
|
|
206
|
+
<div class="absolute left-5 top-8 w-2 h-px bg-gray-400/25" />
|
|
207
|
+
<div class="absolute left-5 bottom-8 w-2 h-px bg-gray-400/25" />
|
|
208
|
+
<div class="absolute right-5 top-8 w-2 h-px bg-gray-400/25" />
|
|
209
|
+
<div class="absolute right-5 bottom-8 w-2 h-px bg-gray-400/25" />
|
|
210
|
+
</template>
|
|
176
211
|
</div>
|
|
212
|
+
</div>
|
|
177
213
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
/>
|
|
189
|
-
</div>
|
|
190
|
-
<div
|
|
191
|
-
ref="verticalDragger"
|
|
192
|
-
v-tooltip.bottom="sizeTooltip"
|
|
193
|
-
class="absolute h-4 left-0 right-4 bottom-0 hover:bg-primary-500/30 flex items-center justify-center cursor-ns-resize group hover:text-primary-500"
|
|
194
|
-
>
|
|
195
|
-
<Icon
|
|
196
|
-
icon="mdi:drag-horizontal-variant"
|
|
197
|
-
class="w-4 h-4 opacity-20 group-hover:opacity-90"
|
|
198
|
-
/>
|
|
199
|
-
</div>
|
|
200
|
-
<div
|
|
201
|
-
ref="cornerDragger"
|
|
202
|
-
v-tooltip.bottom="sizeTooltip"
|
|
203
|
-
class="absolute w-4 h-4 right-0 bottom-0 hover:bg-primary-500/30 flex items-center justify-center cursor-nwse-resize group hover:text-primary-500"
|
|
214
|
+
<!-- Resize Dragger -->
|
|
215
|
+
<template v-if="isResponsiveEnabled">
|
|
216
|
+
<div
|
|
217
|
+
ref="horizontalDragger"
|
|
218
|
+
v-tooltip.right="sizeTooltip"
|
|
219
|
+
class="absolute w-4 top-0 bottom-4 right-0 hover:bg-primary-500/30 flex items-center justify-center cursor-ew-resize group hover:text-primary-500"
|
|
220
|
+
>
|
|
221
|
+
<Icon
|
|
222
|
+
icon="mdi:drag-vertical-variant"
|
|
223
|
+
class="w-4 h-4 opacity-20 group-hover:opacity-90"
|
|
204
224
|
/>
|
|
205
|
-
</
|
|
206
|
-
|
|
225
|
+
</div>
|
|
226
|
+
<div
|
|
227
|
+
ref="verticalDragger"
|
|
228
|
+
v-tooltip.bottom="sizeTooltip"
|
|
229
|
+
class="absolute h-4 left-0 right-4 bottom-0 hover:bg-primary-500/30 flex items-center justify-center cursor-ns-resize group hover:text-primary-500"
|
|
230
|
+
>
|
|
231
|
+
<Icon
|
|
232
|
+
icon="mdi:drag-horizontal-variant"
|
|
233
|
+
class="w-4 h-4 opacity-20 group-hover:opacity-90"
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
<div
|
|
237
|
+
ref="cornerDragger"
|
|
238
|
+
v-tooltip.bottom="sizeTooltip"
|
|
239
|
+
class="absolute w-4 h-4 right-0 bottom-0 hover:bg-primary-500/30 flex items-center justify-center cursor-nwse-resize group hover:text-primary-500"
|
|
240
|
+
/>
|
|
241
|
+
</template>
|
|
207
242
|
</div>
|
|
208
243
|
</div>
|
|
209
244
|
</div>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
+
import type { Slot } from '../../util/grid-slots'
|
|
2
3
|
import { useResizeObserver } from '@vueuse/core'
|
|
3
|
-
import { computed, onMounted, ref, watch } from 'vue'
|
|
4
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
4
5
|
import { useStoryStore } from '../../stores/story'
|
|
6
|
+
import { assignSlots, trimSlots } from '../../util/grid-slots'
|
|
5
7
|
import StoryVariantGridItem from './StoryVariantGridItem.vue'
|
|
6
8
|
|
|
7
9
|
const storyStore = useStoryStore()
|
|
@@ -24,65 +26,44 @@ const gridTemplateWidth = computed(() => {
|
|
|
24
26
|
return layoutWidth
|
|
25
27
|
})
|
|
26
28
|
|
|
27
|
-
const margin = 16
|
|
28
29
|
const gap = 16
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
// Rows kept mounted on each side of the viewport, so a scroll does not expose a
|
|
32
|
+
// blank band before the next update lands.
|
|
33
|
+
const overscanRows = 2
|
|
34
|
+
|
|
35
|
+
// What to render before any item has reported a height. One row would be enough
|
|
36
|
+
// to measure with, but the row width is not known yet either.
|
|
37
|
+
const initialCount = 10
|
|
38
|
+
|
|
31
39
|
const maxItemHeight = ref(0)
|
|
32
|
-
const maxCount = ref(10)
|
|
33
|
-
const countPerRow = ref(0)
|
|
34
|
-
const visibleRows = ref(0)
|
|
35
40
|
|
|
36
41
|
const el = ref<HTMLDivElement>(null)
|
|
42
|
+
const scrollTop = ref(0)
|
|
43
|
+
const viewportHeight = ref(0)
|
|
44
|
+
|
|
45
|
+
const variants = computed(() => storyStore.currentStory.variants)
|
|
37
46
|
|
|
38
47
|
useResizeObserver(el, () => {
|
|
39
|
-
|
|
48
|
+
updateWindow()
|
|
40
49
|
updateSize()
|
|
41
50
|
})
|
|
42
51
|
|
|
43
|
-
function
|
|
44
|
-
if (!
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const height = el.value!.clientHeight
|
|
48
|
-
const scrollTop = el.value!.scrollTop
|
|
49
|
-
|
|
50
|
-
// width = (countPerRow - 1) * gap + countPerRow * itemWidth.value
|
|
51
|
-
|
|
52
|
-
// W = (C - 1) * G + C * I
|
|
53
|
-
// W = C * G - G + C * I
|
|
54
|
-
// W + G = C * G + C * I
|
|
55
|
-
// W + G = C * (G + I)
|
|
56
|
-
// (W + G) / (G + I) = C
|
|
57
|
-
|
|
58
|
-
countPerRow.value = Math.floor((width + gap) / (itemWidth.value + gap))
|
|
59
|
-
visibleRows.value = Math.ceil((height + scrollTop + gap) / (maxItemHeight.value + gap))
|
|
60
|
-
const newMaxCount = countPerRow.value * visibleRows.value
|
|
61
|
-
if (maxCount.value < newMaxCount) {
|
|
62
|
-
maxCount.value = newMaxCount
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (storyStore.currentVariant) {
|
|
66
|
-
const index = storyStore.currentStory.variants.indexOf(storyStore.currentVariant)
|
|
67
|
-
if (index + 1 > maxCount.value) {
|
|
68
|
-
maxCount.value = index + 1
|
|
69
|
-
}
|
|
70
|
-
}
|
|
52
|
+
function updateWindow() {
|
|
53
|
+
if (!el.value) return
|
|
54
|
+
scrollTop.value = el.value.scrollTop
|
|
55
|
+
viewportHeight.value = el.value.clientHeight
|
|
71
56
|
}
|
|
72
57
|
|
|
73
|
-
|
|
74
|
-
|
|
58
|
+
// The reported width is ignored: `columnCount` is what CSS lays out, so it is
|
|
59
|
+
// the authority on how many items sit in a row.
|
|
60
|
+
function onItemResize(_w: number, h: number) {
|
|
75
61
|
if (maxItemHeight.value < h) {
|
|
76
62
|
maxItemHeight.value = h
|
|
77
|
-
|
|
63
|
+
updateWindow()
|
|
78
64
|
}
|
|
79
65
|
}
|
|
80
66
|
|
|
81
|
-
watch(() => storyStore.currentVariant, () => {
|
|
82
|
-
maxItemHeight.value = 0 // Reset max height
|
|
83
|
-
updateMaxCount()
|
|
84
|
-
})
|
|
85
|
-
|
|
86
67
|
// Grid size
|
|
87
68
|
|
|
88
69
|
const gridEl = ref<HTMLDivElement>(null)
|
|
@@ -105,13 +86,98 @@ function updateSize() {
|
|
|
105
86
|
|
|
106
87
|
onMounted(() => {
|
|
107
88
|
updateSize()
|
|
89
|
+
updateWindow()
|
|
108
90
|
})
|
|
109
91
|
|
|
110
92
|
useResizeObserver(gridEl, () => {
|
|
111
93
|
updateSize()
|
|
112
94
|
})
|
|
113
95
|
|
|
114
|
-
const columnCount = computed(() => Math.min(
|
|
96
|
+
const columnCount = computed(() => Math.max(1, Math.min(variants.value.length, Math.floor((viewWidth.value + gap) / (gridColumnWidth.value + gap)))))
|
|
97
|
+
|
|
98
|
+
// The window is derived from what is on screen — a start *and* an end — rather
|
|
99
|
+
// than from a high-water mark of everything scrolled past. `columnCount` is what
|
|
100
|
+
// CSS actually lays out, so the row maths uses it rather than a second count
|
|
101
|
+
// derived from the measured item width.
|
|
102
|
+
const rowHeight = computed(() => maxItemHeight.value + gap)
|
|
103
|
+
const measured = computed(() => maxItemHeight.value > 0)
|
|
104
|
+
const rowCount = computed(() => Math.ceil(variants.value.length / columnCount.value))
|
|
105
|
+
|
|
106
|
+
const startIndex = computed(() => {
|
|
107
|
+
if (!measured.value) return 0
|
|
108
|
+
const firstRow = Math.floor(scrollTop.value / rowHeight.value) - overscanRows
|
|
109
|
+
return Math.max(0, firstRow) * columnCount.value
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const endIndex = computed(() => {
|
|
113
|
+
if (!measured.value) return Math.min(variants.value.length, initialCount)
|
|
114
|
+
const lastRow = Math.ceil((scrollTop.value + viewportHeight.value) / rowHeight.value) + overscanRows
|
|
115
|
+
return Math.min(variants.value.length, Math.max(lastRow, 1) * columnCount.value)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const visibleVariants = computed(() => variants.value.slice(startIndex.value, endIndex.value))
|
|
119
|
+
|
|
120
|
+
/*
|
|
121
|
+
* Cells are slots, not variants (#240). Keyed by variant, a cell that scrolls
|
|
122
|
+
* out of the window unmounts — and with it the sandbox realm it had booted —
|
|
123
|
+
* and a cell scrolling in boots a new one. Keyed by slot, the same component and
|
|
124
|
+
* the same iframe stay mounted and are handed the next variant to show, which
|
|
125
|
+
* the preview turns into a retarget rather than a new document. DOM order is
|
|
126
|
+
* slot order, so each cell carries its visual position as `order` for the grid
|
|
127
|
+
* to lay it out where the variant belongs; see `grid-slots` for the rules.
|
|
128
|
+
*/
|
|
129
|
+
const slots = ref<Slot[]>([])
|
|
130
|
+
|
|
131
|
+
// Idle slots are let go once the window has stopped moving. Not before it is
|
|
132
|
+
// measured: a story switch resets the measurement, which collapses the window
|
|
133
|
+
// to its initial ten until the first cell reports, and trimming on that would
|
|
134
|
+
// throw away the realms the re-grown window is about to ask for.
|
|
135
|
+
const TRIM_DELAY = 2_000
|
|
136
|
+
let trimTimer: ReturnType<typeof setTimeout> | undefined
|
|
137
|
+
|
|
138
|
+
function scheduleTrim() {
|
|
139
|
+
clearTimeout(trimTimer)
|
|
140
|
+
trimTimer = setTimeout(() => {
|
|
141
|
+
if (!measured.value) {
|
|
142
|
+
scheduleTrim()
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
slots.value = trimSlots(slots.value)
|
|
146
|
+
}, TRIM_DELAY)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
watch(visibleVariants, (visible) => {
|
|
150
|
+
slots.value = assignSlots(slots.value, storyStore.currentStory.id, visible)
|
|
151
|
+
scheduleTrim()
|
|
152
|
+
}, { immediate: true })
|
|
153
|
+
|
|
154
|
+
onBeforeUnmount(() => {
|
|
155
|
+
clearTimeout(trimTimer)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// The spacer holds the full scroll extent; the grid is translated into place
|
|
159
|
+
// inside it. Before anything is measured there is nothing to reserve, and a
|
|
160
|
+
// height derived from a zero row height would be `Infinitypx` — dropped by the
|
|
161
|
+
// browser, which is what left the grid with no scroll extent at all.
|
|
162
|
+
const totalHeight = computed(() => measured.value ? rowCount.value * rowHeight.value - gap : undefined)
|
|
163
|
+
const offsetY = computed(() => measured.value ? (startIndex.value / columnCount.value) * rowHeight.value : 0)
|
|
164
|
+
|
|
165
|
+
// Selecting a variant that is outside the window has to move the scroller,
|
|
166
|
+
// since the item it would otherwise scroll to is not mounted.
|
|
167
|
+
watch(() => storyStore.currentVariant, (variant) => {
|
|
168
|
+
maxItemHeight.value = 0 // Reset max height
|
|
169
|
+
updateWindow()
|
|
170
|
+
|
|
171
|
+
if (!variant || !el.value || !measured.value) return
|
|
172
|
+
const index = variants.value.indexOf(variant)
|
|
173
|
+
if (index === -1) return
|
|
174
|
+
|
|
175
|
+
const top = Math.floor(index / columnCount.value) * rowHeight.value
|
|
176
|
+
if (top < scrollTop.value || top + rowHeight.value > scrollTop.value + viewportHeight.value) {
|
|
177
|
+
el.value.scrollTop = top
|
|
178
|
+
updateWindow()
|
|
179
|
+
}
|
|
180
|
+
})
|
|
115
181
|
</script>
|
|
116
182
|
|
|
117
183
|
<template>
|
|
@@ -119,27 +185,29 @@ const columnCount = computed(() => Math.min(storyStore.currentStory.variants.len
|
|
|
119
185
|
<div
|
|
120
186
|
ref="el"
|
|
121
187
|
class="overflow-y-auto flex flex-1"
|
|
122
|
-
@scroll="
|
|
188
|
+
@scroll="updateWindow()"
|
|
123
189
|
>
|
|
124
190
|
<div class="flex w-0 flex-1 mx-4">
|
|
125
191
|
<div
|
|
126
192
|
class="m-auto"
|
|
127
193
|
:style="{
|
|
128
|
-
minHeight:
|
|
194
|
+
minHeight: totalHeight != null ? `${totalHeight}px` : undefined,
|
|
129
195
|
}"
|
|
130
196
|
>
|
|
131
197
|
<div
|
|
132
198
|
ref="gridEl"
|
|
133
|
-
class="grid gap-4 my-4"
|
|
199
|
+
class="poveste-story-variant-grid-cells grid gap-4 my-4"
|
|
134
200
|
:style="{
|
|
135
201
|
gridTemplateColumns: `repeat(${columnCount}, ${gridColumnWidth}px)`,
|
|
202
|
+
transform: offsetY ? `translateY(${offsetY}px)` : undefined,
|
|
136
203
|
}"
|
|
137
204
|
>
|
|
138
205
|
<StoryVariantGridItem
|
|
139
|
-
v-for="(
|
|
206
|
+
v-for="(slot, index) of slots"
|
|
140
207
|
:key="index"
|
|
141
|
-
:variant="variant"
|
|
208
|
+
:variant="slot.variant"
|
|
142
209
|
:story="storyStore.currentStory"
|
|
210
|
+
:style="{ order: slot.order, display: slot.visible ? undefined : 'none' }"
|
|
143
211
|
@resize="onItemResize"
|
|
144
212
|
/>
|
|
145
213
|
</div>
|
|
@@ -148,3 +216,12 @@ const columnCount = computed(() => Math.min(storyStore.currentStory.variants.len
|
|
|
148
216
|
</div>
|
|
149
217
|
</div>
|
|
150
218
|
</template>
|
|
219
|
+
|
|
220
|
+
<style scoped>
|
|
221
|
+
/* Cells are placed with `order`, so without this Tab and a screen reader walk
|
|
222
|
+
them in slot order — the order they were recycled in, not the screen's.
|
|
223
|
+
Chromium only for now; elsewhere the rule is ignored. */
|
|
224
|
+
.poveste-story-variant-grid-cells {
|
|
225
|
+
reading-flow: grid-order;
|
|
226
|
+
}
|
|
227
|
+
</style>
|
|
@@ -4,7 +4,7 @@ import type { Story, Variant } from '../../types'
|
|
|
4
4
|
import { Icon } from '@iconify/vue'
|
|
5
5
|
import { HstCopyIcon } from '@poveste/controls'
|
|
6
6
|
import { useResizeObserver } from '@vueuse/core'
|
|
7
|
-
import { computed, ref, toRefs } from 'vue'
|
|
7
|
+
import { computed, nextTick, ref, toRefs, watch } from 'vue'
|
|
8
8
|
import { useRouter } from 'vue-router'
|
|
9
9
|
import { usePreviewSettingsStore } from '../../stores/preview-settings'
|
|
10
10
|
import { useStoryStore } from '../../stores/story'
|
|
@@ -42,6 +42,16 @@ const storyStore = useStoryStore()
|
|
|
42
42
|
|
|
43
43
|
storyStore.setPreviewReady(props.variant, false)
|
|
44
44
|
|
|
45
|
+
// This cell is a slot (#240): it is handed other variants without being
|
|
46
|
+
// recreated, so the setup-time reset above runs once. Neither the variant
|
|
47
|
+
// leaving nor the one arriving is shown anywhere until something renders it
|
|
48
|
+
// again, and a stale `previewReady` lets the size report below and the
|
|
49
|
+
// preview's state sync run against a render that has not happened.
|
|
50
|
+
watch(() => props.variant, (next, previous) => {
|
|
51
|
+
if (previous) storyStore.setPreviewReady(previous, false)
|
|
52
|
+
storyStore.setPreviewReady(next, false)
|
|
53
|
+
})
|
|
54
|
+
|
|
45
55
|
function onReady() {
|
|
46
56
|
storyStore.setPreviewReady(props.variant, true)
|
|
47
57
|
}
|
|
@@ -56,12 +66,25 @@ const el = ref<HTMLDivElement>()
|
|
|
56
66
|
|
|
57
67
|
const { autoScroll } = useScrollOnActive(isActive, el)
|
|
58
68
|
|
|
59
|
-
|
|
60
|
-
if (props.variant.previewReady)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
function reportSize() {
|
|
70
|
+
if (!el.value || !props.variant.previewReady) return
|
|
71
|
+
emit('resize', el.value.clientWidth, el.value.clientHeight)
|
|
72
|
+
if (isActive.value) {
|
|
73
|
+
autoScroll()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
useResizeObserver(el, reportSize)
|
|
78
|
+
|
|
79
|
+
// The observer fires once on mount and then only on a real size change. The
|
|
80
|
+
// sandbox reports ready about a second later, and the item often never changes
|
|
81
|
+
// size again — so the one callback that did happen was skipped by the guard
|
|
82
|
+
// above and the grid never learned any item's height. It then divided by a
|
|
83
|
+
// zero row count, produced `Infinitypx` for its scroll spacer, and stayed stuck
|
|
84
|
+
// at its initial ten items with the rest unreachable (#103).
|
|
85
|
+
watch(() => props.variant.previewReady, (ready) => {
|
|
86
|
+
if (ready) {
|
|
87
|
+
nextTick(reportSize)
|
|
65
88
|
}
|
|
66
89
|
})
|
|
67
90
|
|
|
@@ -127,13 +150,13 @@ const useIframe = computed(() => {
|
|
|
127
150
|
'border-gray-100 dark:border-gray-800': !isActive,
|
|
128
151
|
'border-primary-200 dark:border-primary-900': isActive,
|
|
129
152
|
}"
|
|
130
|
-
data-
|
|
153
|
+
data-testid="sandbox-render"
|
|
131
154
|
@click.stop="selectVariant()"
|
|
132
155
|
@keyup="selectVariant()"
|
|
133
156
|
>
|
|
134
157
|
<div
|
|
135
158
|
class="absolute inset-0 rounded bind-preview-bg"
|
|
136
|
-
data-
|
|
159
|
+
data-testid="responsive-preview-bg"
|
|
137
160
|
/>
|
|
138
161
|
|
|
139
162
|
<CheckerboardPattern
|
|
@@ -150,12 +173,14 @@ const useIframe = computed(() => {
|
|
|
150
173
|
'color': autoApplyContrastColor ? contrastColor : undefined,
|
|
151
174
|
}"
|
|
152
175
|
>
|
|
176
|
+
<!-- Not keyed by variant: the realm is reused and retargeted when
|
|
177
|
+
this cell is handed another variant (#240). -->
|
|
153
178
|
<StoryVariantSinglePreviewRemote
|
|
154
179
|
v-if="useIframe"
|
|
155
|
-
:key="`iframe-${story.id}-${variant.id}`"
|
|
156
180
|
:story="story"
|
|
157
181
|
:variant="variant"
|
|
158
182
|
auto-height
|
|
183
|
+
:responsive="false"
|
|
159
184
|
/>
|
|
160
185
|
<GenericRenderStory
|
|
161
186
|
v-else
|