@poveste/app 0.14.0 → 0.16.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.
Files changed (126) hide show
  1. package/dist/app/style/main.pcss +18 -44
  2. package/dist/bundled/App.vue_vue_type_script_setup_true_lang.js +9 -7
  3. package/dist/bundled/app.css +91 -28
  4. package/dist/bundled/components/app/AppActions.vue_vue_type_script_setup_true_lang.js +44 -27
  5. package/dist/bundled/components/app/ShortcutTooltip.js +5 -0
  6. package/dist/bundled/components/app/ShortcutTooltip.vue_vue_type_script_setup_true_lang.js +27 -0
  7. package/dist/bundled/components/app/TopBarChipItem.js +1 -1
  8. package/dist/bundled/components/base/BaseDropdown.js +6 -0
  9. package/dist/bundled/components/base/BaseDropdown.vue_vue_type_script_setup_true_lang.js +68 -0
  10. package/dist/bundled/components/base/BaseEmpty.js +2 -2
  11. package/dist/bundled/components/base/BaseOverflowMenu.vue_vue_type_script_setup_true_lang.js +19 -19
  12. package/dist/bundled/components/base/BaseSelect.js +1 -0
  13. package/dist/bundled/components/base/BaseSelect.vue_vue_type_script_setup_true_lang.js +194 -33
  14. package/dist/bundled/components/base/BaseSplitPane.js +1 -1
  15. package/dist/bundled/components/base/BaseSplitPane.vue_vue_type_script_setup_true_lang.js +65 -4
  16. package/dist/bundled/components/command/PromptSelect.vue_vue_type_script_setup_true_lang.js +1 -1
  17. package/dist/bundled/components/panel/ControlsComponentPropItem.vue_vue_type_script_setup_true_lang.js +24 -16
  18. package/dist/bundled/components/panel/ControlsComponentProps.vue_vue_type_script_setup_true_lang.js +9 -6
  19. package/dist/bundled/components/panel/ControlsComponentState.vue_vue_type_script_setup_true_lang.js +9 -6
  20. package/dist/bundled/components/panel/PaneTabs.vue_vue_type_script_setup_true_lang.js +6 -6
  21. package/dist/bundled/components/panel/StatePresets.vue_vue_type_script_setup_true_lang.js +96 -52
  22. package/dist/bundled/components/panel/StoryEvent.vue_vue_type_script_setup_true_lang.js +5 -5
  23. package/dist/bundled/components/panel/StorySidePanel.vue_vue_type_script_setup_true_lang.js +2 -1
  24. package/dist/bundled/components/panel/StorySourceCode.js +1 -1
  25. package/dist/bundled/components/panel/StorySourceCode.vue_vue_type_script_setup_true_lang.js +24 -18
  26. package/dist/bundled/components/search/SearchItemContent.vue_vue_type_script_setup_true_lang.js +2 -1
  27. package/dist/bundled/components/search/SearchPane.vue_vue_type_script_setup_true_lang.js +1 -2
  28. package/dist/bundled/components/story/StoryResponsivePreview.js +1 -1
  29. package/dist/bundled/components/story/StoryResponsivePreview.vue_vue_type_script_setup_true_lang.js +52 -45
  30. package/dist/bundled/components/story/StoryVariantGridItem.js +1 -1
  31. package/dist/bundled/components/story/StoryVariantGridItem.vue_vue_type_script_setup_true_lang.js +22 -20
  32. package/dist/bundled/components/story/StoryVariantSingle.js +1 -1
  33. package/dist/bundled/components/story/StoryVariantSingle.vue_vue_type_script_setup_true_lang.js +2 -1
  34. package/dist/bundled/components/story/StoryVariantSinglePreviewRemote.vue_vue_type_script_setup_true_lang.js +9 -6
  35. package/dist/bundled/components/story/StoryView.vue_vue_type_script_setup_true_lang.js +5 -1
  36. package/dist/bundled/components/toolbar/DevOnlyToolbarOpenInEditor.vue_vue_type_script_setup_true_lang.js +15 -12
  37. package/dist/bundled/components/toolbar/ToolbarBackground.vue_vue_type_script_setup_true_lang.js +20 -16
  38. package/dist/bundled/components/toolbar/ToolbarNewTab.vue_vue_type_script_setup_true_lang.js +16 -13
  39. package/dist/bundled/components/toolbar/ToolbarResponsiveSize.vue_vue_type_script_setup_true_lang.js +35 -24
  40. package/dist/bundled/components/toolbar/ToolbarTextDirection.vue_vue_type_script_setup_true_lang.js +15 -12
  41. package/dist/bundled/components/toolbar/ToolbarTitle.js +1 -1
  42. package/dist/bundled/components/toolbar/ToolbarTitle.vue_vue_type_script_setup_true_lang.js +1 -1
  43. package/dist/bundled/components/tree/StoryGroup.vue_vue_type_script_setup_true_lang.js +2 -2
  44. package/dist/bundled/components/tree/StoryList.vue_vue_type_script_setup_true_lang.js +3 -3
  45. package/dist/bundled/components/tree/StoryListFolder.js +1 -1
  46. package/dist/bundled/components/tree/StoryListFolder.vue_vue_type_script_setup_true_lang.js +3 -3
  47. package/dist/bundled/components/tree/StoryListItem.js +1 -1
  48. package/dist/bundled/components/tree/StoryListItem.vue_vue_type_script_setup_true_lang.js +3 -3
  49. package/dist/bundled/index.js +0 -13
  50. package/dist/bundled/sandbox.js +5 -3
  51. package/dist/bundled/stores/story.js +2 -2
  52. package/dist/bundled/util/commands.js +1 -1
  53. package/dist/bundled/util/dark.js +1 -1
  54. package/dist/bundled/util/events.js +1 -1
  55. package/dist/bundled/util/grid-slots.js +1 -1
  56. package/dist/bundled/util/icons.generated.js +4 -2
  57. package/dist/bundled/util/keyboard.js +8 -14
  58. package/dist/bundled/util/mapping.js +2 -2
  59. package/dist/bundled/util/render-tracker.js +33 -0
  60. package/dist/bundled/util/scroll.js +26 -5
  61. package/dist/bundled/util/state.js +105 -5
  62. package/dist/bundled/util/variant.js +12 -2
  63. package/dist/style.css +753 -357
  64. package/package.json +28 -28
  65. package/src/__tests__/base-select.spec.ts +25 -0
  66. package/src/__tests__/render-tracker.spec.ts +38 -0
  67. package/src/__tests__/reset.spec.ts +53 -0
  68. package/src/__tests__/state-clone.spec.ts +55 -1
  69. package/src/__tests__/state-types.spec.ts +175 -0
  70. package/src/app/App.vue +44 -35
  71. package/src/app/components/app/AppActions.vue +42 -30
  72. package/src/app/components/app/ShortcutTooltip.vue +29 -0
  73. package/src/app/components/app/TopBarChipItem.vue +1 -1
  74. package/src/app/components/base/BaseDropdown.vue +142 -0
  75. package/src/app/components/base/BaseEmpty.vue +1 -1
  76. package/src/app/components/base/BaseListItemLink.vue +4 -2
  77. package/src/app/components/base/BaseOverflowMenu.vue +27 -21
  78. package/src/app/components/base/BaseSelect.vue +270 -41
  79. package/src/app/components/base/BaseSplitPane.vue +75 -4
  80. package/src/app/components/command/PromptSelect.vue +1 -1
  81. package/src/app/components/panel/ControlsComponentPropItem.vue +29 -17
  82. package/src/app/components/panel/ControlsComponentProps.vue +7 -5
  83. package/src/app/components/panel/ControlsComponentState.vue +7 -5
  84. package/src/app/components/panel/PaneTabs.vue +6 -6
  85. package/src/app/components/panel/StatePresets.vue +116 -74
  86. package/src/app/components/panel/StoryEvent.vue +10 -9
  87. package/src/app/components/panel/StorySidePanel.vue +2 -1
  88. package/src/app/components/panel/StorySourceCode.vue +51 -44
  89. package/src/app/components/search/SearchItemContent.vue +2 -1
  90. package/src/app/components/search/SearchPane.vue +2 -3
  91. package/src/app/components/story/StoryResponsivePreview.vue +61 -29
  92. package/src/app/components/story/StoryVariantGridItem.vue +20 -19
  93. package/src/app/components/story/StoryVariantSingle.vue +1 -0
  94. package/src/app/components/story/StoryVariantSinglePreviewRemote.vue +14 -8
  95. package/src/app/components/story/StoryView.vue +4 -1
  96. package/src/app/components/toolbar/DevOnlyToolbarOpenInEditor.vue +14 -12
  97. package/src/app/components/toolbar/ToolbarBackground.vue +22 -23
  98. package/src/app/components/toolbar/ToolbarNewTab.vue +14 -12
  99. package/src/app/components/toolbar/ToolbarResponsiveSize.vue +31 -25
  100. package/src/app/components/toolbar/ToolbarTextDirection.vue +14 -10
  101. package/src/app/components/toolbar/ToolbarTitle.vue +2 -2
  102. package/src/app/components/tree/StoryGroup.vue +2 -2
  103. package/src/app/components/tree/StoryList.vue +2 -2
  104. package/src/app/components/tree/StoryListFolder.vue +2 -2
  105. package/src/app/components/tree/StoryListItem.vue +1 -1
  106. package/src/app/index.ts +0 -17
  107. package/src/app/sandbox.ts +16 -3
  108. package/src/app/stores/story.ts +2 -2
  109. package/src/app/style/main.pcss +18 -44
  110. package/src/app/types.ts +4 -4
  111. package/src/app/util/commands.ts +1 -1
  112. package/src/app/util/dark.ts +1 -1
  113. package/src/app/util/env.ts +0 -1
  114. package/src/app/util/events.ts +6 -5
  115. package/src/app/util/grid-slots.ts +1 -1
  116. package/src/app/util/icons.generated.ts +2 -2
  117. package/src/app/util/keyboard.ts +7 -11
  118. package/src/app/util/mapping.ts +2 -2
  119. package/src/app/util/render-tracker.ts +31 -0
  120. package/src/app/util/scroll.ts +47 -9
  121. package/src/app/util/state.ts +141 -8
  122. package/src/app/util/variant.ts +12 -2
  123. package/dist/app/style/floating.pcss +0 -40
  124. package/dist/bundled/util/tooltip.js +0 -14
  125. package/src/app/style/floating.pcss +0 -40
  126. package/src/app/util/tooltip.ts +0 -13
@@ -2,7 +2,7 @@
2
2
  import type { HighlighterCore } from 'shiki/core'
3
3
  import type { Story, Variant } from '../../types'
4
4
  import { Icon } from '@iconify/vue'
5
- import { HstCopyIcon } from '@poveste/controls'
5
+ import { HstCopyIcon, HstTooltip } from '@poveste/controls'
6
6
  import { unindent } from '@poveste/shared'
7
7
  import { clientSupportPlugins } from 'virtual:$poveste-support-plugins-client'
8
8
  import { computed, markRaw, nextTick, onMounted, ref, shallowRef, watch, watchEffect } from 'vue'
@@ -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
  }
@@ -151,50 +151,56 @@ watch(sourceHtml, async () => {
151
151
 
152
152
  <!-- Display source modes -->
153
153
  <div class="flex flex-none gap-px h-full py-2">
154
- <button
155
- v-tooltip="!dynamicSourceCode ? 'Dynamic source code is not available' : displayedSource !== 'dynamic' ? 'Switch to dynamic source' : null"
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
- :class="[
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',
160
- ]"
161
- @click="dynamicSourceCode && (displayedSource = 'dynamic')"
154
+ <HstTooltip
155
+ :content="!dynamicSourceCode ? 'Dynamic source code is not available' : displayedSource !== 'dynamic' ? 'Switch to dynamic source' : null"
162
156
  >
163
- <Icon
164
- icon="carbon:flash"
165
- class="w-4 h-4 flex-none"
166
- />
167
- <span
168
- class="transition-opacity duration-300"
169
- :class="{
170
- 'opacity-0': displayedSource !== 'dynamic',
171
- }"
157
+ <button
158
+ 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"
159
+ :class="[
160
+ displayedSource !== 'dynamic' ? 'max-w-6 opacity-70' : 'max-w-[82px] text-primary-800 dark:text-primary-400',
161
+ dynamicSourceCode ? 'cursor-pointer hover:bg-gray-500/30 active:bg-gray-600/50' : displayedSource !== 'dynamic' && 'opacity-50',
162
+ ]"
163
+ @click="dynamicSourceCode && (displayedSource = 'dynamic')"
172
164
  >
173
- Dynamic
174
- </span>
175
- </button>
176
- <button
177
- v-tooltip="!staticSourceCode ? 'Static source code is not available' : displayedSource !== 'static' ? 'Switch to static source' : null"
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
- :class="[
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',
182
- ]"
183
- @click="staticSourceCode && (displayedSource = 'static')"
165
+ <Icon
166
+ icon="carbon:flash"
167
+ class="w-4 h-4 flex-none"
168
+ />
169
+ <span
170
+ class="transition-opacity duration-300"
171
+ :class="{
172
+ 'opacity-0': displayedSource !== 'dynamic',
173
+ }"
174
+ >
175
+ Dynamic
176
+ </span>
177
+ </button>
178
+ </HstTooltip>
179
+ <HstTooltip
180
+ :content="!staticSourceCode ? 'Static source code is not available' : displayedSource !== 'static' ? 'Switch to static source' : null"
184
181
  >
185
- <Icon
186
- icon="carbon:document"
187
- class="w-4 h-4 flex-none"
188
- />
189
- <span
190
- class="transition-opacity duration-300"
191
- :class="{
192
- 'opacity-0': displayedSource !== 'static',
193
- }"
182
+ <button
183
+ 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"
184
+ :class="[
185
+ displayedSource !== 'static' ? 'max-w-6 opacity-70' : 'max-w-[63px] text-primary-800 dark:text-primary-400',
186
+ staticSourceCode ? 'cursor-pointer hover:bg-gray-500/30 active:bg-gray-600/50' : displayedSource !== 'static' && 'opacity-50',
187
+ ]"
188
+ @click="staticSourceCode && (displayedSource = 'static')"
194
189
  >
195
- Static
196
- </span>
197
- </button>
190
+ <Icon
191
+ icon="carbon:document"
192
+ class="w-4 h-4 flex-none"
193
+ />
194
+ <span
195
+ class="transition-opacity duration-300"
196
+ :class="{
197
+ 'opacity-0': displayedSource !== 'static',
198
+ }"
199
+ >
200
+ Static
201
+ </span>
202
+ </button>
203
+ </HstTooltip>
198
204
  </div>
199
205
 
200
206
  <HstCopyIcon
@@ -223,6 +229,7 @@ watch(sourceHtml, async () => {
223
229
  ref="scroller"
224
230
  class="__poveste-code-placeholder w-full h-full p-4 outline-none bg-transparent resize-none m-0"
225
231
  :value="displayedSourceCode"
232
+ aria-label="Source code"
226
233
  readonly
227
234
  data-testid="story-source-code"
228
235
  @scroll="onScroll"
@@ -8,9 +8,10 @@ defineProps<{
8
8
  selected: boolean
9
9
  }>()
10
10
 
11
- const defaultIcons = {
11
+ const defaultIcons: Record<SearchResult['kind'], string> = {
12
12
  story: 'carbon:cube',
13
13
  variant: 'carbon:cube',
14
+ command: 'carbon:chevron-right',
14
15
  }
15
16
 
16
17
  const kindLabels = {
@@ -138,8 +138,7 @@ watch(rateLimitedSearch, async (value) => {
138
138
  break
139
139
  }
140
140
  case 'variant': {
141
- const [storyId] = idMapData.id.split(':')
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
  }
@@ -2,8 +2,8 @@
2
2
  import type { Ref } from 'vue'
3
3
  import type { Variant } from '../../types'
4
4
  import { Icon } from '@iconify/vue'
5
+ import { HstTooltip } from '@poveste/controls'
5
6
  import { useEventListener } from '@vueuse/core'
6
- import { VTooltip as vTooltip } from 'floating-vue'
7
7
  import { computed, onUnmounted, ref } from 'vue'
8
8
  import { usePreviewSettingsStore } from '../../stores/preview-settings'
9
9
  import CheckerboardPattern from '../misc/CheckerboardPattern.vue'
@@ -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 start = axis === 'x' ? event.touches[0].clientX : event.touches[0].clientY
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 delta = (axis === 'x' ? event.touches[0].clientX : event.touches[0].clientY) - start
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
 
@@ -197,9 +201,18 @@ const sizeTooltip = computed(() => `${responsiveWidth.value ?? 'Auto'} × ${resp
197
201
  preview would otherwise be shown against the bare box below the
198
202
  first screenful. The checkerboard is measured against this for the
199
203
  same reason — but the draggers are not, since they belong to the
200
- visible box rather than to the story's full height. -->
204
+ visible box rather than to the story's full height.
205
+ `flex`, and `flex-1` rather than `h-full` below, because that is the
206
+ half `min-h-full` costs: a percentage height resolves against a
207
+ parent's *specified* height, and this one's is `auto`. So `h-full`
208
+ collapsed all the way down to the iframe, which then fell back to
209
+ its own default of 150px and clipped every story taller than that
210
+ (#949). A flex child reads the used height instead, which
211
+ `min-height` does give it — and with `min-height: auto` it still
212
+ refuses to shrink below its content, so a tall story pushes this box
213
+ taller exactly as it did. -->
201
214
  <div
202
- class="rounded-lg min-h-full relative"
215
+ class="rounded-lg min-h-full relative flex flex-col"
203
216
  :class="responsive ? 'bind-preview-bg' : ''"
204
217
  :data-testid="responsive ? 'responsive-preview-bg' : undefined"
205
218
  >
@@ -208,10 +221,17 @@ const sizeTooltip = computed(() => `${responsiveWidth.value ?? 'Auto'} × ${resp
208
221
  class="absolute inset-0 w-full h-full text-gray-500/20"
209
222
  />
210
223
  <div
211
- class="h-full relative"
224
+ class="flex-1 relative flex flex-col"
212
225
  :class="responsive ? 'p-8' : ''"
213
226
  >
214
- <div class="w-full h-full relative">
227
+ <!-- `grid`, so the one in-flow child stretches to this box without
228
+ asking for a percentage of it. `h-full` here read a height that
229
+ is decided by flex, which Chrome treats as indefinite for
230
+ percentage resolution — the same wall one level up. Stretching
231
+ is not shrinking: the child keeps its `min-height: auto`, so an
232
+ inline story taller than the preview still grows this and the
233
+ box above it (#258). -->
234
+ <div class="w-full flex-1 relative grid">
215
235
  <div class="absolute inset-0" />
216
236
 
217
237
  <slot
@@ -239,31 +259,43 @@ const sizeTooltip = computed(() => `${responsiveWidth.value ?? 'Auto'} × ${resp
239
259
 
240
260
  <!-- Resize Dragger -->
241
261
  <template v-if="isResponsiveEnabled">
242
- <div
243
- ref="horizontalDragger"
244
- v-tooltip.right="sizeTooltip"
245
- 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"
262
+ <HstTooltip
263
+ :content="sizeTooltip"
264
+ side="right"
246
265
  >
247
- <Icon
248
- icon="mdi:drag-vertical-variant"
249
- class="w-4 h-4 opacity-20 group-hover:opacity-90"
250
- />
251
- </div>
252
- <div
253
- ref="verticalDragger"
254
- v-tooltip.bottom="sizeTooltip"
255
- 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"
266
+ <div
267
+ ref="horizontalDragger"
268
+ 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"
269
+ >
270
+ <Icon
271
+ icon="mdi:drag-vertical-variant"
272
+ class="w-4 h-4 opacity-20 group-hover:opacity-90"
273
+ />
274
+ </div>
275
+ </HstTooltip>
276
+ <HstTooltip
277
+ :content="sizeTooltip"
278
+ side="bottom"
256
279
  >
257
- <Icon
258
- icon="mdi:drag-horizontal-variant"
259
- class="w-4 h-4 opacity-20 group-hover:opacity-90"
280
+ <div
281
+ ref="verticalDragger"
282
+ 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"
283
+ >
284
+ <Icon
285
+ icon="mdi:drag-horizontal-variant"
286
+ class="w-4 h-4 opacity-20 group-hover:opacity-90"
287
+ />
288
+ </div>
289
+ </HstTooltip>
290
+ <HstTooltip
291
+ :content="sizeTooltip"
292
+ side="bottom"
293
+ >
294
+ <div
295
+ ref="cornerDragger"
296
+ 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"
260
297
  />
261
- </div>
262
- <div
263
- ref="cornerDragger"
264
- v-tooltip.bottom="sizeTooltip"
265
- 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"
266
- />
298
+ </HstTooltip>
267
299
  </template>
268
300
  </div>
269
301
  </div>
@@ -2,7 +2,7 @@
2
2
  import type { PropType } from 'vue'
3
3
  import type { Story, Variant } from '../../types'
4
4
  import { Icon } from '@iconify/vue'
5
- import { HstCopyIcon } from '@poveste/controls'
5
+ import { HstCopyIcon, HstTooltip } from '@poveste/controls'
6
6
  import { useResizeObserver } from '@vueuse/core'
7
7
  import { computed, nextTick, ref, toRefs, watch } from 'vue'
8
8
  import { useRouter } from 'vue-router'
@@ -111,28 +111,29 @@ const useIframe = computed(() => {
111
111
  >
112
112
  <!-- Header -->
113
113
  <div class="flex-none flex items-center">
114
- <RouterLink
115
- v-tooltip="variant.title"
116
- :to="targetRoute"
117
- class="rounded w-max px-2 py-0.5 min-w-16 cursor-pointer flex items-center gap-1 flex-shrink"
118
- :class="{
119
- 'hover:bg-gray-200 text-gray-500 dark:hover:bg-gray-800': !isActive,
120
- 'bg-primary-200 hover:bg-primary-300 text-primary-800 dark:bg-primary-700 dark:hover:bg-primary-800 dark:text-primary-200': isActive,
121
- }"
122
- >
123
- <Icon
124
- :icon="variant.icon ?? 'carbon:cube'"
125
- class="w-4 h-4 opacity-50"
114
+ <HstTooltip :content="variant.title">
115
+ <RouterLink
116
+ :to="targetRoute"
117
+ class="rounded w-max px-2 py-0.5 min-w-16 cursor-pointer flex items-center gap-1 flex-shrink"
126
118
  :class="{
127
- 'text-gray-500': !isActive && !variant.iconColor,
128
- 'bind-icon-color': !isActive && variant.iconColor,
119
+ 'hover:bg-gray-200 text-gray-500 dark:hover:bg-gray-800': !isActive,
120
+ 'bg-primary-200 hover:bg-primary-300 text-primary-800 dark:bg-primary-700 dark:hover:bg-primary-800 dark:text-primary-200': isActive,
129
121
  }"
130
- />
131
- <span class="truncate flex-1">{{ variant.title }}</span>
132
- </RouterLink>
122
+ >
123
+ <Icon
124
+ :icon="variant.icon ?? 'carbon:cube'"
125
+ class="w-4 h-4 opacity-50"
126
+ :class="{
127
+ 'text-gray-500': !isActive && !variant.iconColor,
128
+ 'bind-icon-color': !isActive && variant.iconColor,
129
+ }"
130
+ />
131
+ <span class="truncate flex-1">{{ variant.title }}</span>
132
+ </RouterLink>
133
+ </HstTooltip>
133
134
 
134
135
  <!-- Toolbar -->
135
- <div class="flex-none ml-auto hidden group-hover:flex items-center">
136
+ <div class="flex-none ml-auto hidden group-hover:flex group-focus-within:flex items-center">
136
137
  <HstCopyIcon
137
138
  :content="() => getSourceCode(story, variant)"
138
139
  />
@@ -36,6 +36,7 @@ const showsPreview = computed(() => !!variant.value || autoSelectsVariant(storyS
36
36
  -->
37
37
  <BaseSplitPane
38
38
  save-id="story-single-main-split"
39
+ label="Resize the variant list"
39
40
  :min="5"
40
41
  :max="40"
41
42
  :default-split="17"
@@ -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
- // The realm re-renders on every retarget, so a stale error must not outlive the
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.width = `${finalWidth}px`
289
- if (finalHeight) style.height = `${finalHeight}px`
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.height = `${Math.min(reportedHeight.value ?? placeholderHeight.value, MAX_AUTO_HEIGHT)}px`
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
- setVariant(variant.id)
50
+ if (variant) {
51
+ setVariant(variant.id)
52
+ }
51
53
  }
52
54
  }, {
53
55
  immediate: true,
@@ -109,6 +111,7 @@ function scrollDocsToTop() {
109
111
  <BaseSplitPane
110
112
  v-else
111
113
  :save-id="`story-main-${placement}`"
114
+ label="Resize the preview"
112
115
  :orientation="splitOrientation"
113
116
  :min="30"
114
117
  :max="95"
@@ -1,5 +1,6 @@
1
1
  <script lang="ts" setup>
2
2
  import { Icon } from '@iconify/vue'
3
+ import { HstTooltip } from '@poveste/controls'
3
4
  import { openInEditor } from '../../util/open-in-editor.js'
4
5
  import TopBarChipItem from '../app/TopBarChipItem.vue'
5
6
 
@@ -10,16 +11,17 @@ defineProps<{
10
11
  </script>
11
12
 
12
13
  <template>
13
- <TopBarChipItem
14
- v-tooltip="tooltip"
15
- aria-label="Open story in editor"
16
- target="_blank"
17
- class="poveste-toolbar-open-in-editor"
18
- @click="openInEditor(file)"
19
- >
20
- <Icon
21
- icon="carbon:script-reference"
22
- class="w-4 h-4"
23
- />
24
- </TopBarChipItem>
14
+ <HstTooltip :content="tooltip">
15
+ <TopBarChipItem
16
+ aria-label="Open story in editor"
17
+ target="_blank"
18
+ class="poveste-toolbar-open-in-editor"
19
+ @click="openInEditor(file)"
20
+ >
21
+ <Icon
22
+ icon="carbon:script-reference"
23
+ class="w-4 h-4"
24
+ />
25
+ </TopBarChipItem>
26
+ </HstTooltip>
25
27
  </template>
@@ -4,6 +4,7 @@ import { Icon } from '@iconify/vue'
4
4
  import { usePreviewSettingsStore } from '../../stores/preview-settings'
5
5
  import { povesteConfig } from '../../util/config'
6
6
  import BaseCheckbox from '../base/BaseCheckbox.vue'
7
+ import BaseDropdown from '../base/BaseDropdown.vue'
7
8
 
8
9
  const previewSettings = usePreviewSettingsStore()
9
10
  const settings = previewSettings.currentSettings
@@ -21,32 +22,30 @@ const showDropdown = showColorScheme || !!povesteConfig.backgroundPresets?.lengt
21
22
  </script>
22
23
 
23
24
  <template>
24
- <VDropdown
25
+ <BaseDropdown
25
26
  v-if="showDropdown"
26
- placement="bottom-end"
27
- :skidding="6"
27
+ side="bottom"
28
+ align="end"
29
+ :align-offset="6"
30
+ tooltip="Preview appearance"
28
31
  class="poveste-toolbar-background flex-none"
29
32
  data-testid="toolbar-background"
30
33
  >
31
- <!-- The tooltip would sit on top of the first row of the dropdown, so drop
32
- it while the dropdown is open. -->
33
- <template #default="{ shown }">
34
- <div
35
- v-tooltip="shown ? '' : 'Preview appearance'"
36
- class="flex items-center gap-1 px-2.5 py-1.5 hover:bg-white/50 dark:hover:bg-white/10 hover:text-primary-500 dark:hover:text-primary-400 cursor-pointer text-gray-900 dark:text-gray-100 transition-colors group"
37
- >
38
- <Icon
39
- icon="carbon:color-palette"
40
- class="w-4 h-4"
41
- />
42
- <Icon
43
- icon="carbon:chevron-down"
44
- class="w-3 h-3 opacity-40 group-hover:opacity-70"
45
- />
46
- </div>
47
- </template>
34
+ <button
35
+ class="flex items-center gap-1 px-2.5 py-1.5 hover:bg-white/50 dark:hover:bg-white/10 hover:text-primary-500 dark:hover:text-primary-400 cursor-pointer text-gray-900 dark:text-gray-100 transition-colors group"
36
+ aria-label="Preview appearance"
37
+ >
38
+ <Icon
39
+ icon="carbon:color-palette"
40
+ class="w-4 h-4"
41
+ />
42
+ <Icon
43
+ icon="carbon:chevron-down"
44
+ class="w-3 h-3 opacity-40 group-hover:opacity-70"
45
+ />
46
+ </button>
48
47
 
49
- <template #popper="{ hide }">
48
+ <template #popper="{ close }">
50
49
  <div
51
50
  class="flex flex-col items-stretch"
52
51
  data-testid="background-popper"
@@ -89,7 +88,7 @@ const showDropdown = showColorScheme || !!povesteConfig.backgroundPresets?.lengt
89
88
  ? 'bg-primary-500 hover:bg-primary-600 text-white dark:text-black'
90
89
  : 'bg-transparent hover:bg-primary-100 dark:hover:bg-primary-700',
91
90
  ]"
92
- @click="previewSettings.setBackgroundColor(option.color);hide()"
91
+ @click="previewSettings.setBackgroundColor(option.color);close()"
93
92
  >
94
93
  <span class="mr-auto">{{ option.label }}</span>
95
94
  <template v-if="option.color !== '$checkerboard'">
@@ -107,5 +106,5 @@ const showDropdown = showColorScheme || !!povesteConfig.backgroundPresets?.lengt
107
106
  </button>
108
107
  </div>
109
108
  </template>
110
- </VDropdown>
109
+ </BaseDropdown>
111
110
  </template>
@@ -1,6 +1,7 @@
1
1
  <script lang="ts" setup>
2
2
  import type { Story, Variant } from '../../types'
3
3
  import { Icon } from '@iconify/vue'
4
+ import { HstTooltip } from '@poveste/controls'
4
5
  import { computed } from 'vue'
5
6
  import { getSandboxUrl } from '../../util/sandbox'
6
7
  import TopBarChipItem from '../app/TopBarChipItem.vue'
@@ -16,16 +17,17 @@ const sandboxUrl = computed(() => {
16
17
  </script>
17
18
 
18
19
  <template>
19
- <TopBarChipItem
20
- v-tooltip="'Open variant in new tab'"
21
- :href="sandboxUrl"
22
- target="_blank"
23
- aria-label="Open variant in new tab"
24
- class="poveste-toolbar-new-tab"
25
- >
26
- <Icon
27
- icon="carbon:launch"
28
- class="w-4 h-4"
29
- />
30
- </TopBarChipItem>
20
+ <HstTooltip content="Open variant in new tab">
21
+ <TopBarChipItem
22
+ :href="sandboxUrl"
23
+ target="_blank"
24
+ aria-label="Open variant in new tab"
25
+ class="poveste-toolbar-new-tab"
26
+ >
27
+ <Icon
28
+ icon="carbon:launch"
29
+ class="w-4 h-4"
30
+ />
31
+ </TopBarChipItem>
32
+ </HstTooltip>
31
33
  </template>