@poveste/app 0.15.0 → 0.16.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.
Files changed (77) 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 +80 -17
  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/base/BaseDropdown.js +6 -0
  8. package/dist/bundled/components/base/BaseDropdown.vue_vue_type_script_setup_true_lang.js +68 -0
  9. package/dist/bundled/components/base/BaseOverflowMenu.vue_vue_type_script_setup_true_lang.js +5 -5
  10. package/dist/bundled/components/base/BaseSelect.js +1 -0
  11. package/dist/bundled/components/base/BaseSelect.vue_vue_type_script_setup_true_lang.js +80 -51
  12. package/dist/bundled/components/base/BaseSplitPane.js +1 -1
  13. package/dist/bundled/components/base/BaseSplitPane.vue_vue_type_script_setup_true_lang.js +65 -4
  14. package/dist/bundled/components/panel/ControlsComponentPropItem.vue_vue_type_script_setup_true_lang.js +15 -13
  15. package/dist/bundled/components/panel/ControlsComponentProps.vue_vue_type_script_setup_true_lang.js +9 -6
  16. package/dist/bundled/components/panel/ControlsComponentState.vue_vue_type_script_setup_true_lang.js +9 -6
  17. package/dist/bundled/components/panel/StatePresets.vue_vue_type_script_setup_true_lang.js +69 -54
  18. package/dist/bundled/components/panel/StoryEvent.vue_vue_type_script_setup_true_lang.js +5 -5
  19. package/dist/bundled/components/panel/StorySidePanel.vue_vue_type_script_setup_true_lang.js +1 -0
  20. package/dist/bundled/components/panel/StorySourceCode.js +1 -1
  21. package/dist/bundled/components/panel/StorySourceCode.vue_vue_type_script_setup_true_lang.js +21 -16
  22. package/dist/bundled/components/story/StoryResponsivePreview.js +1 -1
  23. package/dist/bundled/components/story/StoryResponsivePreview.vue_vue_type_script_setup_true_lang.js +46 -43
  24. package/dist/bundled/components/story/StoryVariantGridItem.js +1 -1
  25. package/dist/bundled/components/story/StoryVariantGridItem.vue_vue_type_script_setup_true_lang.js +21 -19
  26. package/dist/bundled/components/story/StoryVariantSingle.js +1 -1
  27. package/dist/bundled/components/story/StoryVariantSingle.vue_vue_type_script_setup_true_lang.js +2 -1
  28. package/dist/bundled/components/story/StoryView.vue_vue_type_script_setup_true_lang.js +1 -0
  29. package/dist/bundled/components/toolbar/DevOnlyToolbarOpenInEditor.vue_vue_type_script_setup_true_lang.js +15 -12
  30. package/dist/bundled/components/toolbar/ToolbarBackground.vue_vue_type_script_setup_true_lang.js +20 -16
  31. package/dist/bundled/components/toolbar/ToolbarNewTab.vue_vue_type_script_setup_true_lang.js +16 -13
  32. package/dist/bundled/components/toolbar/ToolbarResponsiveSize.vue_vue_type_script_setup_true_lang.js +35 -24
  33. package/dist/bundled/components/toolbar/ToolbarTextDirection.vue_vue_type_script_setup_true_lang.js +15 -12
  34. package/dist/bundled/index.js +0 -13
  35. package/dist/bundled/sandbox.js +2 -1
  36. package/dist/bundled/util/icons.generated.js +4 -2
  37. package/dist/bundled/util/scroll.js +26 -5
  38. package/dist/bundled/util/state.js +105 -5
  39. package/dist/style.css +731 -357
  40. package/package.json +19 -15
  41. package/src/__tests__/base-select.spec.ts +25 -0
  42. package/src/__tests__/reset.spec.ts +53 -0
  43. package/src/__tests__/state-clone.spec.ts +55 -1
  44. package/src/__tests__/state-types.spec.ts +175 -0
  45. package/src/app/App.vue +43 -34
  46. package/src/app/components/app/AppActions.vue +42 -30
  47. package/src/app/components/app/ShortcutTooltip.vue +29 -0
  48. package/src/app/components/base/BaseDropdown.vue +142 -0
  49. package/src/app/components/base/BaseOverflowMenu.vue +10 -5
  50. package/src/app/components/base/BaseSelect.vue +133 -60
  51. package/src/app/components/base/BaseSplitPane.vue +73 -2
  52. package/src/app/components/panel/ControlsComponentPropItem.vue +18 -17
  53. package/src/app/components/panel/ControlsComponentProps.vue +7 -5
  54. package/src/app/components/panel/ControlsComponentState.vue +7 -5
  55. package/src/app/components/panel/StatePresets.vue +82 -76
  56. package/src/app/components/panel/StoryEvent.vue +10 -9
  57. package/src/app/components/panel/StorySidePanel.vue +1 -0
  58. package/src/app/components/panel/StorySourceCode.vue +47 -41
  59. package/src/app/components/story/StoryResponsivePreview.vue +55 -27
  60. package/src/app/components/story/StoryVariantGridItem.vue +19 -18
  61. package/src/app/components/story/StoryVariantSingle.vue +1 -0
  62. package/src/app/components/story/StoryView.vue +1 -0
  63. package/src/app/components/toolbar/DevOnlyToolbarOpenInEditor.vue +14 -12
  64. package/src/app/components/toolbar/ToolbarBackground.vue +22 -23
  65. package/src/app/components/toolbar/ToolbarNewTab.vue +14 -12
  66. package/src/app/components/toolbar/ToolbarResponsiveSize.vue +31 -25
  67. package/src/app/components/toolbar/ToolbarTextDirection.vue +14 -10
  68. package/src/app/index.ts +0 -17
  69. package/src/app/sandbox.ts +8 -1
  70. package/src/app/style/main.pcss +18 -44
  71. package/src/app/util/icons.generated.ts +2 -2
  72. package/src/app/util/scroll.ts +47 -9
  73. package/src/app/util/state.ts +139 -6
  74. package/dist/app/style/floating.pcss +0 -40
  75. package/dist/bundled/util/tooltip.js +0 -14
  76. package/src/app/style/floating.pcss +0 -40
  77. package/src/app/util/tooltip.ts +0 -13
@@ -0,0 +1,142 @@
1
+ <script lang="ts" setup>
2
+ import { HstTooltip, PopoverAnchor, PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger, portalTarget } from '@poveste/controls'
3
+ import { onMounted, ref, shallowRef } from 'vue'
4
+
5
+ // The wrapper below is what a caller's class and `data-testid` are for, the way
6
+ // they were on `VDropdown` itself.
7
+ defineOptions({ inheritAttrs: false })
8
+
9
+ const props = withDefaults(defineProps<{
10
+ side?: 'top' | 'right' | 'bottom' | 'left'
11
+ align?: 'start' | 'center' | 'end'
12
+ /** floating-vue's `skidding`: a shift along the trigger's own edge. */
13
+ alignOffset?: number
14
+ /**
15
+ * Refuses to open, rather than disabling the button. A disabled button takes
16
+ * no pointer events, and the one site that sets this still wants its tooltip.
17
+ */
18
+ disabled?: boolean
19
+ /**
20
+ * Rendered here rather than around the trigger at the call site, because
21
+ * where the tooltip sits relative to the anchor below decides whether this
22
+ * opens in the right place at all.
23
+ */
24
+ tooltip?: string | null | undefined
25
+ }>(), {
26
+ side: 'bottom',
27
+ align: 'center',
28
+ alignOffset: 0,
29
+ disabled: false,
30
+ tooltip: undefined,
31
+ })
32
+
33
+ const open = ref(false)
34
+
35
+ function setOpen(value: boolean) {
36
+ open.value = value && !props.disabled
37
+ }
38
+
39
+ // After mount, not during setup: the chrome's root is only in the document once
40
+ // the tree it wraps has been inserted.
41
+ const target = shallowRef<HTMLElement>()
42
+ onMounted(() => {
43
+ target.value = portalTarget()
44
+ })
45
+ </script>
46
+
47
+ <template>
48
+ <PopoverRoot
49
+ :open="open"
50
+ @update:open="setOpen"
51
+ >
52
+ <!-- The anchor is the wrapper, not the trigger, and it has to sit outside
53
+ the tooltip. A tooltip is a popper too, so a trigger rendered inside one
54
+ registers itself as *its* anchor, and the panel then has none: it opens at
55
+ the top-left corner, off-screen, which a test asking only whether it is
56
+ visible still passes. -->
57
+ <PopoverAnchor as-child>
58
+ <div
59
+ v-bind="$attrs"
60
+ class="poveste-dropdown-trigger"
61
+ >
62
+ <!-- No tooltip while the panel is up: it would sit on top of the first
63
+ row. -->
64
+ <HstTooltip :content="open ? null : tooltip">
65
+ <PopoverTrigger as-child>
66
+ <slot :open="open" />
67
+ </PopoverTrigger>
68
+ </HstTooltip>
69
+ </div>
70
+ </PopoverAnchor>
71
+
72
+ <PopoverPortal
73
+ v-if="target"
74
+ :to="target"
75
+ >
76
+ <PopoverContent
77
+ class="poveste-dropdown"
78
+ :side="side"
79
+ :align="align"
80
+ :side-offset="8"
81
+ :align-offset="alignOffset"
82
+ :collision-padding="4"
83
+ >
84
+ <slot
85
+ name="popper"
86
+ :close="() => setOpen(false)"
87
+ />
88
+ </PopoverContent>
89
+ </PopoverPortal>
90
+ </PopoverRoot>
91
+ </template>
92
+
93
+ <style lang="pcss">
94
+ /* floating-vue put the same zero on its own wrapper, and the triggers that
95
+ wanted text to sit normally say `leading-normal` because of it. */
96
+ .poveste-dropdown-trigger {
97
+ line-height: 0;
98
+ }
99
+
100
+ .poveste-dropdown {
101
+ background: #fff;
102
+ color: #000;
103
+ border: 1px solid #ddd;
104
+ border-radius: 6px;
105
+ box-shadow: 0 6px 30px rgb(0 0 0 / .1);
106
+ /* A row that paints to the panel's edge has to stop at the rounded corner,
107
+ which the `overflow-y: auto` on floating-vue's inner element did. */
108
+ overflow: hidden;
109
+ z-index: 100;
110
+
111
+ /*
112
+ * What Tailwind's `dark:` compiles to, spelled out because `@apply` in a
113
+ * single-file component needs a `@reference` and no component here has one.
114
+ * Not `.ptw-dark &`: the chrome CSS is wrapped in `@scope` and `.ptw-dark`
115
+ * sits on `<html>`, above the scope root, so a descendant rule keyed on it
116
+ * never matches and the panel falls back to the light colours (#101). This
117
+ * puts the condition on the subject instead, and adds no specificity — so it
118
+ * has to come after the light values to win.
119
+ */
120
+ &:where(.ptw-dark, .ptw-dark *) {
121
+ background: var(--color-gray-700);
122
+ border-color: var(--color-gray-850);
123
+ color: var(--color-gray-100);
124
+ }
125
+
126
+ &:focus-visible {
127
+ outline: none;
128
+ }
129
+
130
+ &[data-state='open'] {
131
+ transform-origin: var(--reka-popover-content-transform-origin);
132
+ animation: poveste-dropdown-open .15s cubic-bezier(0, 1, .5, 1);
133
+ }
134
+ }
135
+
136
+ @keyframes poveste-dropdown-open {
137
+ from {
138
+ opacity: 0;
139
+ transform: scale(.75);
140
+ }
141
+ }
142
+ </style>
@@ -3,6 +3,7 @@ import type { SetupContext, VNode } from 'vue'
3
3
  import { Icon } from '@iconify/vue'
4
4
  import { useResizeObserver } from '@vueuse/core'
5
5
  import { computed, h, onBeforeUnmount, reactive, ref } from 'vue'
6
+ import BaseDropdown from './BaseDropdown.vue'
6
7
 
7
8
  // Container
8
9
 
@@ -99,18 +100,22 @@ function ChildrenSlice(props: { start?: number, end?: number }, { slots }: Setup
99
100
  <slot />
100
101
  </ChildrenRender>
101
102
 
102
- <VDropdown
103
+ <BaseDropdown
103
104
  v-if="visibleChildrenCount < children.size"
104
105
  >
105
- <div
106
- role="button"
106
+ <!-- A `<button>`, where this was a `role="button"` div with no `tabindex`:
107
+ the popover writes `aria-expanded` onto whatever it is given, and axe
108
+ rejects that on an element with no role it can name (#924). A keyboard
109
+ reaches the hidden tabs now, which it could not before. -->
110
+ <button
111
+ aria-label="More"
107
112
  class="cursor-pointer hover:bg-primary-50 dark:hover:bg-primary-900 w-8 h-full flex items-center justify-center absolute top-0 right-0"
108
113
  >
109
114
  <Icon
110
115
  icon="carbon:caret-down"
111
116
  class="w-4 h-4 opacity-50 group-hover:opacity-100"
112
117
  />
113
- </div>
118
+ </button>
114
119
 
115
120
  <template #popper>
116
121
  <div class="flex flex-col items-stretch">
@@ -121,6 +126,6 @@ function ChildrenSlice(props: { start?: number, end?: number }, { slots }: Setup
121
126
  </ChildrenSlice>
122
127
  </div>
123
128
  </template>
124
- </VDropdown>
129
+ </BaseDropdown>
125
130
  </div>
126
131
  </template>
@@ -1,9 +1,9 @@
1
1
  <script lang="ts" setup>
2
2
  import type { ComputedRef } from 'vue'
3
3
  import { Icon } from '@iconify/vue'
4
+ import { PopoverAnchor, PopoverContent, PopoverPortal, PopoverRoot, portalTarget } from '@poveste/controls'
4
5
  import { refAutoReset } from '@vueuse/core'
5
- import { Dropdown as VDropdown } from 'floating-vue'
6
- import { computed, ref, useId } from 'vue'
6
+ import { computed, nextTick, onMounted, ref, shallowRef, useId, watch } from 'vue'
7
7
 
8
8
  /*
9
9
  * The WAI-ARIA select-only combobox: the trigger keeps focus and names the active
@@ -156,76 +156,149 @@ function onKeydown(event: KeyboardEvent) {
156
156
  }
157
157
  }
158
158
 
159
+ // After mount, not during setup: the chrome's root is only in the document once
160
+ // the tree it wraps has been inserted.
161
+ const target = shallowRef<HTMLElement>()
162
+ onMounted(() => {
163
+ target.value = portalTarget()
164
+ })
165
+
166
+ /*
167
+ * Reka spares its own trigger from the dismiss it fires on any pointer or focus
168
+ * landing outside the popup — but only a `PopoverTrigger` registers as one, and
169
+ * this trigger cannot be that: `PopoverTrigger` writes its own `aria-expanded`,
170
+ * `aria-controls` and `aria-haspopup="dialog"` over the listbox wiring the
171
+ * combobox pattern is built on. So the dismiss is declined here instead.
172
+ *
173
+ * Without it the popup is impossible to close by clicking the picker: the
174
+ * dismiss lands on `pointerdown`, the handler below then reads `open` as false
175
+ * and opens it again, and the whole thing looks like a click that did nothing.
176
+ * Neither aria spec catches it — both drive this from the keyboard, on purpose.
177
+ */
178
+ function keepOpenOnTheTrigger(event: Event) {
179
+ if (combobox.value?.contains(event.target as Node)) {
180
+ event.preventDefault()
181
+ }
182
+ }
183
+
184
+ /*
185
+ * A capped list scrolls, and `aria-activedescendant` moves an option the browser
186
+ * has no reason to reveal — the focus never leaves the trigger, so nothing
187
+ * scrolls on its own.
188
+ */
189
+ watch([open, activeIndex], async ([isOpen, index]) => {
190
+ if (!isOpen || index < 0) {
191
+ return
192
+ }
193
+ await nextTick()
194
+ target.value?.ownerDocument.getElementById(optionId(index))?.scrollIntoView({ block: 'nearest' })
195
+ })
196
+
159
197
  defineExpose({
160
198
  focus: () => combobox.value?.focus(),
161
199
  })
162
200
  </script>
163
201
 
164
202
  <template>
165
- <!-- `no-auto-focus`: floating-vue otherwise moves focus into the popup once it
166
- shows, and the combobox pattern keeps it on the trigger, which owns the keys. -->
167
- <VDropdown
168
- v-model:shown="open"
169
- class="poveste-base-select"
170
- :triggers="[]"
171
- no-auto-focus
172
- auto-size
173
- auto-boundary-max-size
174
- >
175
- <div
176
- ref="combobox"
177
- role="combobox"
178
- tabindex="0"
179
- :aria-label="label"
180
- aria-haspopup="listbox"
181
- :aria-expanded="open"
182
- :aria-controls="open ? listboxId : undefined"
183
- :aria-activedescendant="open && activeIndex >= 0 ? optionId(activeIndex) : undefined"
184
- class="cursor-pointer w-full outline-none focus-visible:border-primary-500 px-2 h-[27px] -my-1 border border-solid border-black/25 dark:border-white/25 hover:border-primary-500 dark:hover:border-primary-500 rounded-sm flex gap-2 items-center leading-normal"
185
- @click="open ? close() : openList()"
186
- @keydown="onKeydown"
203
+ <PopoverRoot v-model:open="open">
204
+ <PopoverAnchor
205
+ as-child
206
+ class="poveste-base-select"
187
207
  >
188
- <div class="flex-1 truncate">
189
- <slot :label="selectedLabel">
190
- {{ selectedLabel }}
191
- </slot>
192
- </div>
193
- <Icon
194
- icon="carbon:chevron-sort"
195
- class="w-4 h-4 flex-none ml-auto"
196
- />
197
- </div>
198
- <template #popper>
199
208
  <div
200
- :id="listboxId"
201
- role="listbox"
209
+ ref="combobox"
210
+ role="combobox"
211
+ tabindex="0"
202
212
  :aria-label="label"
203
- class="flex flex-col bg-gray-50 dark:bg-gray-700"
213
+ aria-haspopup="listbox"
214
+ :aria-expanded="open"
215
+ :aria-controls="open ? listboxId : undefined"
216
+ :aria-activedescendant="open && activeIndex >= 0 ? optionId(activeIndex) : undefined"
217
+ class="cursor-pointer w-full outline-none focus-visible:border-primary-500 px-2 h-[27px] -my-1 border border-solid border-black/25 dark:border-white/25 hover:border-primary-500 dark:hover:border-primary-500 rounded-sm flex gap-2 items-center leading-normal"
218
+ @click="open ? close() : openList()"
219
+ @keydown="onKeydown"
220
+ >
221
+ <div class="flex-1 truncate">
222
+ <slot :label="selectedLabel">
223
+ {{ selectedLabel }}
224
+ </slot>
225
+ </div>
226
+ <Icon
227
+ icon="carbon:chevron-sort"
228
+ class="w-4 h-4 flex-none ml-auto"
229
+ />
230
+ </div>
231
+ </PopoverAnchor>
232
+
233
+ <PopoverPortal
234
+ v-if="target"
235
+ :to="target"
236
+ >
237
+ <!-- The popup takes no focus at either end: the combobox pattern keeps it on
238
+ the trigger, which owns the keys. Deliberate rather than load-bearing
239
+ today — Reka moves focus into the content on open, and only skips it
240
+ here because the options are divs with nothing focusable in them. The
241
+ aria specs cannot fail on this, so removing it would look safe. -->
242
+ <PopoverContent
243
+ class="poveste-base-select-popper"
244
+ align="start"
245
+ :side-offset="6"
246
+ :collision-padding="8"
247
+ @open-auto-focus.prevent
248
+ @close-auto-focus.prevent
249
+ @interact-outside="keepOpenOnTheTrigger"
204
250
  >
205
251
  <div
206
- v-for="([value, optionLabel], index) of entries"
207
- :id="optionId(index)"
208
- :key="value"
209
- role="option"
210
- :aria-selected="value === modelValue"
211
- class="px-2 py-1 cursor-pointer hover:bg-primary-100 dark:hover:bg-primary-700"
212
- :class="{
213
- 'bg-primary-200 dark:bg-primary-800': value === modelValue,
214
- 'outline outline-2 -outline-offset-2 outline-primary-500': index === activeIndex,
215
- }"
216
- @mousedown.prevent
217
- @mousemove="activeIndex = index"
218
- @click="choose(index)"
252
+ :id="listboxId"
253
+ role="listbox"
254
+ :aria-label="label"
255
+ class="poveste-base-select-options flex flex-col bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100"
219
256
  >
220
- <slot
221
- name="option"
222
- :label="optionLabel"
223
- :value="value"
257
+ <div
258
+ v-for="([value, optionLabel], index) of entries"
259
+ :id="optionId(index)"
260
+ :key="value"
261
+ role="option"
262
+ :aria-selected="value === modelValue"
263
+ class="px-2 py-1 cursor-pointer hover:bg-primary-100 dark:hover:bg-primary-700"
264
+ :class="{
265
+ 'bg-primary-200 dark:bg-primary-800': value === modelValue,
266
+ 'outline outline-2 -outline-offset-2 outline-primary-500': index === activeIndex,
267
+ }"
268
+ @mousedown.prevent
269
+ @mousemove="activeIndex = index"
270
+ @click="choose(index)"
224
271
  >
225
- {{ optionLabel }}
226
- </slot>
272
+ <slot
273
+ name="option"
274
+ :label="optionLabel"
275
+ :value="value"
276
+ >
277
+ {{ optionLabel }}
278
+ </slot>
279
+ </div>
227
280
  </div>
228
- </div>
229
- </template>
230
- </VDropdown>
281
+ </PopoverContent>
282
+ </PopoverPortal>
283
+ </PopoverRoot>
231
284
  </template>
285
+
286
+ <style lang="pcss">
287
+ .poveste-base-select-popper {
288
+ /* The trigger's own width, which is what floating-vue's `auto-size` gave. */
289
+ min-width: var(--reka-popper-anchor-width);
290
+ z-index: 100;
291
+
292
+ &:focus-visible {
293
+ outline: none;
294
+ }
295
+ }
296
+
297
+ .poveste-base-select-options {
298
+ /* The room Reka measured between the trigger and the viewport edge, which is
299
+ what `auto-boundary-max-size` gave: a story with more presets than fit below
300
+ its picker otherwise drew them off the screen, reachable by nothing. */
301
+ max-height: var(--reka-popover-content-available-height);
302
+ overflow-y: auto;
303
+ }
304
+ </style>
@@ -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
  *
@@ -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
@@ -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<{
@@ -64,21 +64,22 @@ const canReset = computed(() => props.variant.state?._hPropState?.[props.compone
64
64
  class="poveste-controls-component-prop-item grow min-w-0"
65
65
  :title="`${definition.name}${canReset ? ' *' : ''}`"
66
66
  />
67
- <button
68
- v-tooltip="'Remove override'"
69
- type="button"
70
- :aria-label="`Remove override of ${definition.name}`"
71
- :disabled="!canReset"
72
- class="flex-none flex mr-2 mb-[10px] p-0 bg-transparent border-0 cursor-pointer text-gray-900 dark:text-gray-100 hover:text-primary-500 dark:hover:text-primary-400"
73
- :class="[
74
- canReset ? 'opacity-50 hover:opacity-100 focus-visible:opacity-100' : 'opacity-25 pointer-events-none',
75
- ]"
76
- @click="reset()"
77
- >
78
- <Icon
79
- icon="carbon:erase"
80
- class="w-4 h-4"
81
- />
82
- </button>
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"
73
+ :class="[
74
+ canReset ? 'opacity-50 hover:opacity-100 focus-visible:opacity-100' : 'opacity-25 pointer-events-none',
75
+ ]"
76
+ @click="reset()"
77
+ >
78
+ <Icon
79
+ icon="carbon:erase"
80
+ class="w-4 h-4"
81
+ />
82
+ </button>
83
+ </HstTooltip>
83
84
  </div>
84
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>