@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
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import { computed, onUnmounted, ref, watch } from 'vue'
2
+ import { computed, onUnmounted, ref, useId, watch } from 'vue'
3
3
 
4
4
  const props = defineProps({
5
5
  orientation: {
@@ -49,6 +49,17 @@ const props = defineProps({
49
49
  default: true,
50
50
  },
51
51
 
52
+ /**
53
+ * What the divider is called to a screen reader.
54
+ *
55
+ * Four are on screen at once and they resize different things, so a shared
56
+ * default would announce four identical separators.
57
+ */
58
+ label: {
59
+ type: String,
60
+ default: 'Resize panels',
61
+ },
62
+
52
63
  /**
53
64
  * Whether the first pane exists at all.
54
65
  *
@@ -176,7 +187,7 @@ let startPosition = 0
176
187
  let startSplit = 0
177
188
  const el = ref<HTMLElement>()
178
189
 
179
- function dragStart(e) {
190
+ function dragStart(e: MouseEvent) {
180
191
  dragging.value = true
181
192
  startPosition = props.orientation === 'landscape' ? e.pageX : e.pageY
182
193
  startSplit = boundSplit.value
@@ -184,7 +195,7 @@ function dragStart(e) {
184
195
  window.addEventListener('mouseup', dragEnd)
185
196
  }
186
197
 
187
- function dragMove(e) {
198
+ function dragMove(e: MouseEvent) {
188
199
  if (dragging.value) {
189
200
  // The move listener is on `window`, so it can outlive the element for the
190
201
  // rest of a drag if the pane unmounts under it.
@@ -225,6 +236,56 @@ watch(hasBothPanes, (both) => {
225
236
  }
226
237
  })
227
238
 
239
+ /*
240
+ * The keyboard half, which the divider had none of: it was a `div` with a
241
+ * `mousedown` handler, so the layout could only be changed with a pointer and a
242
+ * screen reader was told nothing was there (#995).
243
+ *
244
+ * The arrow step is one unit of whatever `split` is measured in — percent
245
+ * normally, pixels under `fixed` — and `PageUp`/`PageDown` move by ten, because
246
+ * one pixel per press is not a usable way to move a pixel-sized pane.
247
+ */
248
+ const STEP = 1
249
+ const PAGE_STEP = 10
250
+
251
+ function moveBy(amount: number) {
252
+ currentSplit.value = Math.min(props.max, Math.max(props.min, boundSplit.value + amount))
253
+ }
254
+
255
+ function onKeydown(event: KeyboardEvent) {
256
+ const back = props.orientation === 'landscape' ? 'ArrowLeft' : 'ArrowUp'
257
+ const forward = props.orientation === 'landscape' ? 'ArrowRight' : 'ArrowDown'
258
+
259
+ switch (event.key) {
260
+ case back:
261
+ moveBy(-STEP)
262
+ break
263
+ case forward:
264
+ moveBy(STEP)
265
+ break
266
+ case 'PageUp':
267
+ moveBy(-PAGE_STEP)
268
+ break
269
+ case 'PageDown':
270
+ moveBy(PAGE_STEP)
271
+ break
272
+ case 'Home':
273
+ currentSplit.value = props.min
274
+ break
275
+ case 'End':
276
+ currentSplit.value = props.max
277
+ break
278
+ default:
279
+ return
280
+ }
281
+
282
+ // Only for a key this handles: the arrows scroll the pane otherwise, and a
283
+ // blanket `prevent` would stop that everywhere the divider has focus.
284
+ event.preventDefault()
285
+ }
286
+
287
+ const firstPaneId = useId()
288
+
228
289
  function removeDragListeners() {
229
290
  window.removeEventListener('mousemove', dragMove)
230
291
  window.removeEventListener('mouseup', dragEnd)
@@ -248,6 +309,7 @@ onUnmounted(() => {
248
309
  >
249
310
  <div
250
311
  v-if="showFirst"
312
+ :id="firstPaneId"
251
313
  class="relative top-0 left-0 z-20"
252
314
  :class="{
253
315
  'pointer-events-none': dragging,
@@ -260,7 +322,15 @@ onUnmounted(() => {
260
322
 
261
323
  <div
262
324
  v-if="hasBothPanes"
263
- class="dragger absolute z-100 hover:bg-primary-500/50 transition-colors duration-150 delay-150"
325
+ role="separator"
326
+ tabindex="0"
327
+ :aria-label="label"
328
+ :aria-orientation="orientation === 'landscape' ? 'vertical' : 'horizontal'"
329
+ :aria-controls="firstPaneId"
330
+ :aria-valuenow="Math.round(boundSplit)"
331
+ :aria-valuemin="min"
332
+ :aria-valuemax="max"
333
+ class="dragger absolute z-100 hover:bg-primary-500/50 focus-visible:bg-primary-500/50 focus-visible:outline-2 focus-visible:outline-primary-500 transition-colors duration-150 delay-150"
264
334
  :class="{
265
335
  'top-0 bottom-0 cursor-ew-resize': orientation === 'landscape',
266
336
  'left-0 right-0 cursor-ns-resize': orientation === 'portrait',
@@ -268,6 +338,7 @@ onUnmounted(() => {
268
338
  'bg-primary-500/25': dragging,
269
339
  }"
270
340
  @mousedown.prevent="dragStart"
341
+ @keydown="onKeydown"
271
342
  />
272
343
  </div>
273
344
  <div
@@ -77,7 +77,7 @@ const {
77
77
  } = useSelection(formattedOptions)
78
78
 
79
79
  function selectIndex(index: number) {
80
- const result = formattedOptions.value[index].value
80
+ const result = formattedOptions.value[index]?.value
81
81
  if (result) {
82
82
  model.value = result
83
83
  }
@@ -2,7 +2,7 @@
2
2
  import type { AutoPropComponentDefinition, PropDefinition } from '@poveste/shared'
3
3
  import type { Variant } from '../../types'
4
4
  import { Icon } from '@iconify/vue'
5
- import { HstCheckbox, HstJson, HstNumber, HstText } from '@poveste/controls'
5
+ import { HstCheckbox, HstJson, HstNumber, HstText, HstTooltip } from '@poveste/controls'
6
6
  import { computed } from 'vue'
7
7
 
8
8
  const props = defineProps<{
@@ -50,24 +50,36 @@ const canReset = computed(() => props.variant.state?._hPropState?.[props.compone
50
50
  </script>
51
51
 
52
52
  <template>
53
- <component
54
- :is="comp"
53
+ <!-- The remove action is beside the control, not in its actions slot. Inside
54
+ the control's <label> or checkbox, a label forwarded clicks to it, its
55
+ name joined the control's name, and Space on it toggled the checkbox (#827). -->
56
+ <div
55
57
  v-if="comp"
56
- v-model="model"
57
- :placeholder="model === undefined ? definition?.default : null"
58
- class="poveste-controls-component-prop-item"
59
- :title="`${definition.name}${canReset ? ' *' : ''}`"
58
+ class="flex items-end hover:bg-primary-100 dark:hover:bg-primary-800"
60
59
  >
61
- <template #actions>
62
- <Icon
63
- v-tooltip="'Remove override'"
64
- icon="carbon:erase"
65
- class="cursor-pointer w-4 h-4 hover:text-primary-500 dark:hover:text-primary-400 text-gray-900 dark:text-gray-100"
60
+ <component
61
+ :is="comp"
62
+ v-model="model"
63
+ :placeholder="model === undefined ? definition?.default : null"
64
+ class="poveste-controls-component-prop-item grow min-w-0"
65
+ :title="`${definition.name}${canReset ? ' *' : ''}`"
66
+ />
67
+ <HstTooltip content="Remove override">
68
+ <button
69
+ type="button"
70
+ :aria-label="`Remove override of ${definition.name}`"
71
+ :disabled="!canReset"
72
+ class="flex-none flex mr-2 mb-[10px] p-0 bg-transparent border-0 cursor-pointer text-gray-900 dark:text-gray-100 hover:text-primary-500 dark:hover:text-primary-400"
66
73
  :class="[
67
- canReset ? 'opacity-50 hover:opacity-100' : 'opacity-25 pointer-events-none',
74
+ canReset ? 'opacity-50 hover:opacity-100 focus-visible:opacity-100' : 'opacity-25 pointer-events-none',
68
75
  ]"
69
- @click.stop="reset()"
70
- />
71
- </template>
72
- </component>
76
+ @click="reset()"
77
+ >
78
+ <Icon
79
+ icon="carbon:erase"
80
+ class="w-4 h-4"
81
+ />
82
+ </button>
83
+ </HstTooltip>
84
+ </div>
73
85
  </template>
@@ -2,6 +2,7 @@
2
2
  import type { AutoPropComponentDefinition } from '@poveste/shared'
3
3
  import type { Variant } from '../../types'
4
4
  import { Icon } from '@iconify/vue'
5
+ import { HstTooltip } from '@poveste/controls'
5
6
  import ControlsComponentPropItem from './ControlsComponentPropItem.vue'
6
7
 
7
8
  defineProps<{
@@ -13,11 +14,12 @@ defineProps<{
13
14
  <template>
14
15
  <div class="poveste-controls-component-props">
15
16
  <div class="font-mono p-2 flex items-center gap-1">
16
- <Icon
17
- v-tooltip="'Auto-detected props'"
18
- icon="carbon:flash"
19
- class="w-4 h-4 text-primary-500 flex-none"
20
- />
17
+ <HstTooltip content="Auto-detected props">
18
+ <Icon
19
+ icon="carbon:flash"
20
+ class="w-4 h-4 text-primary-500 flex-none"
21
+ />
22
+ </HstTooltip>
21
23
  <div>
22
24
  <span class="opacity-30">&lt;</span>{{ definition.name }}<span class="opacity-30">&gt;</span>
23
25
  </div>
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import type { Variant } from '@poveste/shared'
3
3
  import { Icon } from '@iconify/vue'
4
+ import { HstTooltip } from '@poveste/controls'
4
5
  import { computed } from 'vue'
5
6
  import ControlsComponentStateItem from './ControlsComponentStateItem.vue'
6
7
 
@@ -15,11 +16,12 @@ const stateKeys = computed(() => Object.keys(props.variant.state || {})
15
16
  <template>
16
17
  <div class="poveste-controls-component-init-state">
17
18
  <div class="p-2 flex items-center gap-1">
18
- <Icon
19
- v-tooltip="'Auto-detected state'"
20
- icon="carbon:data-vis-1"
21
- class="w-4 h-4 text-primary-500 flex-none"
22
- />
19
+ <HstTooltip content="Auto-detected state">
20
+ <Icon
21
+ icon="carbon:data-vis-1"
22
+ class="w-4 h-4 text-primary-500 flex-none"
23
+ />
24
+ </HstTooltip>
23
25
  <div>
24
26
  State
25
27
  </div>
@@ -25,13 +25,13 @@ const hasEvents = computed(() => eventsStore.events.length)
25
25
  <BaseOverflowMenu class="poveste-pane-tabs h-10 flex-none border-b border-gray-100 dark:border-gray-750">
26
26
  <BaseTab
27
27
  :to="{ ...$route, query: { ...$route.query, tab: '' } }"
28
- :matched="!$route.query.tab"
28
+ :matched="!$route.query['tab']"
29
29
  >
30
30
  Controls
31
31
  </BaseTab>
32
32
  <BaseTab
33
33
  :to="{ ...$route, query: { ...$route.query, tab: 'docs' } }"
34
- :matched="$route.query.tab === 'docs'"
34
+ :matched="$route.query['tab'] === 'docs'"
35
35
  :class="{
36
36
  'opacity-70': !renderedDoc,
37
37
  }"
@@ -40,7 +40,7 @@ const hasEvents = computed(() => eventsStore.events.length)
40
40
  </BaseTab>
41
41
  <BaseTab
42
42
  :to="{ ...$route, query: { ...$route.query, tab: 'events' } }"
43
- :matched="$route.query.tab === 'events'"
43
+ :matched="$route.query['tab'] === 'events'"
44
44
  :class="{
45
45
  'opacity-70': !hasEvents,
46
46
  }"
@@ -54,13 +54,13 @@ const hasEvents = computed(() => eventsStore.events.length)
54
54
  <template #overflow>
55
55
  <BaseOverflowTab
56
56
  :to="{ ...$route, query: { ...$route.query, tab: '' } }"
57
- :matched="!$route.query.tab"
57
+ :matched="!$route.query['tab']"
58
58
  >
59
59
  Controls
60
60
  </BaseOverflowTab>
61
61
  <BaseOverflowTab
62
62
  :to="{ ...$route, query: { ...$route.query, tab: 'docs' } }"
63
- :matched="$route.query.tab === 'docs'"
63
+ :matched="$route.query['tab'] === 'docs'"
64
64
  :class="{
65
65
  'opacity-70': !renderedDoc,
66
66
  }"
@@ -69,7 +69,7 @@ const hasEvents = computed(() => eventsStore.events.length)
69
69
  </BaseOverflowTab>
70
70
  <BaseOverflowTab
71
71
  :to="{ ...$route, query: { ...$route.query, tab: 'events' } }"
72
- :matched="$route.query.tab === 'events'"
72
+ :matched="$route.query['tab'] === 'events'"
73
73
  :class="{
74
74
  'opacity-70': !hasEvents,
75
75
  }"
@@ -1,8 +1,9 @@
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 { applyState, clone } from '@poveste/shared'
5
- import { onClickOutside, useStorage, useTimeoutFn } from '@vueuse/core'
6
+ import { onClickOutside, useLiveAnnouncer, useStorage, useTimeoutFn } from '@vueuse/core'
6
7
  import { computed, nextTick, onMounted, ref } from 'vue'
7
8
  import { toPresetState, toRawDeep } from '../../util/state'
8
9
  import BaseSelect from '../base/BaseSelect.vue'
@@ -41,7 +42,7 @@ const presetStates = useStorage<Map<string, { state: Record<string, unknown>, la
41
42
  )
42
43
 
43
44
  const presetsOptions = computed(() => {
44
- const options = { [DEFAULT_ID]: 'Initial state' }
45
+ const options: Record<string, string> = { [DEFAULT_ID]: 'Initial state' }
45
46
  presetStates.value.forEach((value, key) => {
46
47
  options[key] = value.label
47
48
  })
@@ -53,7 +54,7 @@ function resetState() {
53
54
  applyState(props.variant.state, clone(defaultState))
54
55
  }
55
56
 
56
- function applyPreset(id) {
57
+ function applyPreset(id: string) {
57
58
  if (id === DEFAULT_ID) {
58
59
  resetState()
59
60
  }
@@ -75,6 +76,7 @@ onMounted(() => {
75
76
 
76
77
  const input = ref<HTMLInputElement>()
77
78
  const select = ref<HTMLInputElement>()
79
+ const picker = ref<InstanceType<typeof BaseSelect>>()
78
80
  const canEdit = computed(() => selectedOption.value !== DEFAULT_ID)
79
81
  const isEditing = ref(false)
80
82
 
@@ -102,6 +104,7 @@ const editingLabel = computed({
102
104
  })
103
105
 
104
106
  const savedNotif = ref(false)
107
+ const { polite } = useLiveAnnouncer()
105
108
  const savedTimeout = useTimeoutFn(() => {
106
109
  savedNotif.value = false
107
110
  }, 1000)
@@ -114,9 +117,10 @@ async function savePreset() {
114
117
  preset.state = toPresetState(props.variant.state, omitKeys)
115
118
  savedNotif.value = true
116
119
  savedTimeout.start()
120
+ polite('Preset saved')
117
121
  }
118
122
 
119
- function deletePreset(id) {
123
+ function deletePreset(id: string) {
120
124
  // @TODO custom confirm modal UI
121
125
  // eslint-disable-next-line no-alert
122
126
  if (!confirm('Are you sure you want to delete this preset?')) {
@@ -141,11 +145,20 @@ async function startEditing() {
141
145
  input.value?.select()
142
146
  }
143
147
 
144
- function stopEditing() {
148
+ async function stopEditing(refocus = false) {
149
+ if (!isEditing.value) {
150
+ return
151
+ }
145
152
  isEditing.value = false
153
+ // Finishing a rename from the keyboard unmounts the field that had focus, so
154
+ // hand it to the picker, which now shows the new name.
155
+ if (refocus) {
156
+ await nextTick()
157
+ picker.value?.focus()
158
+ }
146
159
  }
147
160
 
148
- onClickOutside(select, stopEditing)
161
+ onClickOutside(select, () => stopEditing())
149
162
  </script>
150
163
 
151
164
  <template>
@@ -154,79 +167,108 @@ onClickOutside(select, stopEditing)
154
167
  ref="select"
155
168
  class="flex-1 min-w-0"
156
169
  >
170
+ <!-- Swapped for the picker rather than put inside it: a text field inside a
171
+ combobox is a control nested in a control (#828). -->
172
+ <input
173
+ v-if="isEditing"
174
+ ref="input"
175
+ v-model="editingLabel"
176
+ type="text"
177
+ aria-label="Preset name"
178
+ class="text-inherit bg-transparent w-full px-2 h-[27px] -my-1 border border-solid border-primary-500 rounded-sm outline-none"
179
+ @keydown.enter="stopEditing(true)"
180
+ @keydown.escape="stopEditing(true)"
181
+ >
157
182
  <BaseSelect
183
+ v-else
184
+ ref="picker"
158
185
  v-model="selectedOption"
186
+ label="Preset"
159
187
  :options="presetsOptions"
160
188
  @dblclick="startEditing()"
161
- @keydown.enter="stopEditing()"
162
- @keydown.escape="stopEditing()"
163
189
  @select="id => applyPreset(id)"
164
- >
165
- <template #default="{ label }">
166
- <input
167
- v-if="isEditing"
168
- ref="input"
169
- v-model="editingLabel"
170
- type="text"
171
- class="text-inherit bg-transparent w-full h-full outline-none"
172
- @click.stop.prevent
173
- >
174
-
175
- <div
176
- v-else
177
- class="flex items-center gap-2"
178
- >
179
- <span class="flex-1 truncate">
180
- {{ label }}
181
- </span>
182
- <Icon
183
- v-if="canEdit"
184
- v-tooltip="'Rename this preset'"
185
- icon="carbon:edit"
186
- class="flex-none cursor-pointer w-4 h-4 hover:text-primary-500 opacity-50 hover:opacity-100 dark:hover:text-primary-400 text-gray-900 dark:text-gray-100"
187
- @click.stop="startEditing()"
188
- />
189
- </div>
190
- </template>
191
-
192
- <template #option="{ label, value }">
193
- <div class="flex gap-2 items-center">
194
- <span
195
- class="flex-1 truncate"
196
- >{{ label }}</span>
197
- <Icon
198
- v-if="value !== DEFAULT_ID"
199
- v-tooltip="'Delete this preset'"
200
- icon="carbon:trash-can"
201
- class="flex-none cursor-pointer w-4 h-4 hover:text-primary-500 opacity-50 hover:opacity-100 dark:hover:text-primary-400 text-gray-900 dark:text-gray-100"
202
- @click.stop="deletePreset(value)"
203
- />
204
- </div>
205
- </template>
206
- </BaseSelect>
190
+ />
207
191
  </div>
208
- <Icon
209
- v-tooltip="savedNotif ? 'Saved!' : canEdit ? 'Save to preset' : null"
210
- data-testid="preset-save"
211
- :icon="savedNotif ? 'carbon:checkmark' : 'carbon:save'"
212
- class="cursor-pointer w-4 h-4 hover:text-primary-500 dark:hover:text-primary-400 text-gray-900 dark:text-gray-100"
213
- :class="[
214
- canEdit ? 'opacity-50 hover:opacity-100' : 'opacity-25 pointer-events-none',
215
- ]"
216
- @click="savePreset()"
217
- />
218
- <Icon
219
- v-tooltip="'Create new preset'"
220
- data-testid="preset-create"
221
- icon="carbon:add-alt"
222
- class="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"
223
- @click="createPreset()"
224
- />
225
- <Icon
226
- v-tooltip="'Reset to initial state'"
227
- icon="carbon:reset"
228
- class="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"
229
- @click="resetState()"
230
- />
192
+ <HstTooltip :content="canEdit ? 'Rename this preset' : null">
193
+ <button
194
+ type="button"
195
+ data-testid="preset-rename"
196
+ aria-label="Rename preset"
197
+ :disabled="!canEdit"
198
+ 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"
199
+ :class="[
200
+ canEdit ? 'opacity-50 hover:opacity-100 focus-visible:opacity-100' : 'opacity-25 pointer-events-none',
201
+ ]"
202
+ @click="startEditing()"
203
+ >
204
+ <Icon
205
+ icon="carbon:edit"
206
+ class="w-4 h-4"
207
+ />
208
+ </button>
209
+ </HstTooltip>
210
+ <HstTooltip :content="canEdit ? 'Delete this preset' : null">
211
+ <button
212
+ type="button"
213
+ data-testid="preset-delete"
214
+ aria-label="Delete preset"
215
+ :disabled="!canEdit"
216
+ 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"
217
+ :class="[
218
+ canEdit ? 'opacity-50 hover:opacity-100 focus-visible:opacity-100' : 'opacity-25 pointer-events-none',
219
+ ]"
220
+ @click="deletePreset(selectedOption)"
221
+ >
222
+ <Icon
223
+ icon="carbon:trash-can"
224
+ class="w-4 h-4"
225
+ />
226
+ </button>
227
+ </HstTooltip>
228
+ <HstTooltip :content="savedNotif ? 'Saved!' : canEdit ? 'Save to preset' : null">
229
+ <button
230
+ type="button"
231
+ data-testid="preset-save"
232
+ aria-label="Save to preset"
233
+ :disabled="!canEdit"
234
+ 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"
235
+ :class="[
236
+ canEdit ? 'opacity-50 hover:opacity-100 focus-visible:opacity-100' : 'opacity-25 pointer-events-none',
237
+ ]"
238
+ @click="savePreset()"
239
+ >
240
+ <Icon
241
+ :icon="savedNotif ? 'carbon:checkmark' : 'carbon:save'"
242
+ class="w-4 h-4"
243
+ />
244
+ </button>
245
+ </HstTooltip>
246
+ <HstTooltip content="Create new preset">
247
+ <button
248
+ type="button"
249
+ data-testid="preset-create"
250
+ aria-label="Create new preset"
251
+ 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"
252
+ @click="createPreset()"
253
+ >
254
+ <Icon
255
+ icon="carbon:add-alt"
256
+ class="w-4 h-4"
257
+ />
258
+ </button>
259
+ </HstTooltip>
260
+ <HstTooltip content="Reset to initial state">
261
+ <button
262
+ type="button"
263
+ aria-label="Reset to initial state"
264
+ 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"
265
+ @click="resetState()"
266
+ >
267
+ <Icon
268
+ icon="carbon:reset"
269
+ class="w-4 h-4"
270
+ />
271
+ </button>
272
+ </HstTooltip>
231
273
  </div>
232
274
  </template>
@@ -1,6 +1,7 @@
1
1
  <script lang="ts" setup>
2
2
  import type { HstEvent } from '../../stores/events'
3
3
  import { computed } from 'vue'
4
+ import BaseDropdown from '../base/BaseDropdown.vue'
4
5
 
5
6
  const props = defineProps<{
6
7
  event: HstEvent
@@ -24,21 +25,21 @@ const formattedArgument = computed(() => {
24
25
  </script>
25
26
 
26
27
  <template>
27
- <VDropdown
28
+ <BaseDropdown
29
+ side="right"
28
30
  class="poveste-story-event group"
29
- placement="right"
30
31
  data-testid="event-item"
31
32
  >
32
- <template #default="{ shown }">
33
- <div
34
- class="group-hover:bg-primary-100 dark:group-hover:bg-primary-700 cursor-pointer py-2 px-4 flex items-baseline gap-1 leading-normal"
33
+ <template #default="{ open }">
34
+ <button
35
+ class="group-hover:bg-primary-100 dark:group-hover:bg-primary-700 cursor-pointer py-2 px-4 w-full text-left flex items-baseline gap-1 leading-normal"
35
36
  :class="[
36
- shown ? 'bg-primary-50 dark:bg-primary-600' : 'group-odd:bg-gray-100/50 dark:group-odd:bg-gray-750/40',
37
+ open ? 'bg-primary-50 dark:bg-primary-600' : 'group-odd:bg-gray-100/50 dark:group-odd:bg-gray-750/40',
37
38
  ]"
38
39
  >
39
40
  <span
40
41
  :class="{
41
- 'text-primary-500': shown,
42
+ 'text-primary-500': open,
42
43
  }"
43
44
  >
44
45
  {{ event.name }}
@@ -47,7 +48,7 @@ const formattedArgument = computed(() => {
47
48
  v-if="event.argument"
48
49
  class="text-xs opacity-50 truncate"
49
50
  >{{ formattedArgument }}</span>
50
- </div>
51
+ </button>
51
52
  </template>
52
53
 
53
54
  <template #popper>
@@ -55,5 +56,5 @@ const formattedArgument = computed(() => {
55
56
  <pre class="p-4">{{ event.argument }}</pre>
56
57
  </div>
57
58
  </template>
58
- </VDropdown>
59
+ </BaseDropdown>
59
60
  </template>
@@ -22,7 +22,7 @@ const innerOrientation = computed(() =>
22
22
  const route = useRoute()
23
23
 
24
24
  const panelContentComponent = computed(() => {
25
- switch (route.query.tab) {
25
+ switch (route.query['tab']) {
26
26
  case 'docs':
27
27
  return StoryDocs
28
28
  case 'events':
@@ -53,6 +53,7 @@ const panelContentComponent = computed(() => {
53
53
  <BaseSplitPane
54
54
  v-else
55
55
  :save-id="`story-sidepane-${innerOrientation}`"
56
+ label="Resize the controls panel"
56
57
  :orientation="innerOrientation"
57
58
  class="poveste-story-side-panel poveste-loaded h-full"
58
59
  data-testid="story-side-panel"