@poveste/app 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/dist/bundled/App.vue_vue_type_script_setup_true_lang.js +1 -1
  2. package/dist/bundled/app.css +31 -31
  3. package/dist/bundled/components/HomeView.vue_vue_type_script_setup_true_lang.js +1 -1
  4. package/dist/bundled/components/app/AppLogo.vue_vue_type_script_setup_true_lang.js +2 -2
  5. package/dist/bundled/components/app/Breadcrumb.js +1 -1
  6. package/dist/bundled/components/app/Breadcrumb.vue_vue_type_script_setup_true_lang.js +2 -2
  7. package/dist/bundled/components/app/TopBar.vue_vue_type_script_setup_true_lang.js +4 -3
  8. package/dist/bundled/components/app/TopBarChipItem.js +1 -1
  9. package/dist/bundled/components/base/BaseCheckbox.vue_vue_type_script_setup_true_lang.js +1 -1
  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 +18 -15
  12. package/dist/bundled/components/base/BaseSelect.vue_vue_type_script_setup_true_lang.js +158 -26
  13. package/dist/bundled/components/base/BaseSplitPane.js +1 -1
  14. package/dist/bundled/components/base/BaseSplitPane.vue_vue_type_script_setup_true_lang.js +6 -4
  15. package/dist/bundled/components/command/CommandPrompts.vue_vue_type_script_setup_true_lang.js +1 -1
  16. package/dist/bundled/components/command/CommandPromptsModal.vue_vue_type_script_setup_true_lang.js +4 -3
  17. package/dist/bundled/components/command/PromptSelect.vue_vue_type_script_setup_true_lang.js +2 -2
  18. package/dist/bundled/components/command/PromptText.vue_vue_type_script_setup_true_lang.js +1 -1
  19. package/dist/bundled/components/panel/ControlsComponentPropItem.vue_vue_type_script_setup_true_lang.js +20 -14
  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 +94 -55
  22. package/dist/bundled/components/panel/StoryControls.vue_vue_type_script_setup_true_lang.js +1 -1
  23. package/dist/bundled/components/panel/StoryDocs.vue_vue_type_script_setup_true_lang.js +3 -1
  24. package/dist/bundled/components/panel/StoryEvent.vue_vue_type_script_setup_true_lang.js +5 -1
  25. package/dist/bundled/components/panel/StoryEvents.vue_vue_type_script_setup_true_lang.js +2 -1
  26. package/dist/bundled/components/panel/StorySidePanel.vue_vue_type_script_setup_true_lang.js +8 -6
  27. package/dist/bundled/components/panel/StorySourceCode.js +1 -1
  28. package/dist/bundled/components/panel/StorySourceCode.vue_vue_type_script_setup_true_lang.js +8 -6
  29. package/dist/bundled/components/search/SearchItemContent.vue_vue_type_script_setup_true_lang.js +2 -1
  30. package/dist/bundled/components/search/SearchPane.vue_vue_type_script_setup_true_lang.js +16 -9
  31. package/dist/bundled/components/story/GenericMountStory.vue_vue_type_script_setup_true_lang.js +2 -1
  32. package/dist/bundled/components/story/GenericRenderStory.vue_vue_type_script_setup_true_lang.js +2 -1
  33. package/dist/bundled/components/story/StoryResponsivePreview.js +1 -1
  34. package/dist/bundled/components/story/StoryResponsivePreview.vue_vue_type_script_setup_true_lang.js +23 -8
  35. package/dist/bundled/components/story/StoryVariantGrid.js +1 -1
  36. package/dist/bundled/components/story/StoryVariantGrid.vue_vue_type_script_setup_true_lang.js +15 -10
  37. package/dist/bundled/components/story/StoryVariantGridItem.js +1 -1
  38. package/dist/bundled/components/story/StoryVariantGridItem.vue_vue_type_script_setup_true_lang.js +3 -3
  39. package/dist/bundled/components/story/StoryVariantSinglePreviewRemote.vue_vue_type_script_setup_true_lang.js +9 -6
  40. package/dist/bundled/components/story/StoryView.vue_vue_type_script_setup_true_lang.js +7 -3
  41. package/dist/bundled/components/story/StoryViewer.js +1 -1
  42. package/dist/bundled/components/story/StoryViewer.vue_vue_type_script_setup_true_lang.js +3 -3
  43. package/dist/bundled/components/toolbar/ToolbarBackground.vue_vue_type_script_setup_true_lang.js +1 -1
  44. package/dist/bundled/components/toolbar/ToolbarResponsiveSize.vue_vue_type_script_setup_true_lang.js +17 -16
  45. package/dist/bundled/components/toolbar/ToolbarTitle.js +1 -1
  46. package/dist/bundled/components/toolbar/ToolbarTitle.vue_vue_type_script_setup_true_lang.js +1 -1
  47. package/dist/bundled/components/tree/StoryGroup.vue_vue_type_script_setup_true_lang.js +2 -2
  48. package/dist/bundled/components/tree/StoryList.vue_vue_type_script_setup_true_lang.js +3 -3
  49. package/dist/bundled/components/tree/StoryListFolder.js +1 -1
  50. package/dist/bundled/components/tree/StoryListFolder.vue_vue_type_script_setup_true_lang.js +3 -3
  51. package/dist/bundled/components/tree/StoryListItem.js +1 -1
  52. package/dist/bundled/components/tree/StoryListItem.vue_vue_type_script_setup_true_lang.js +3 -3
  53. package/dist/bundled/plugin.js +4 -3
  54. package/dist/bundled/sandbox.js +5 -4
  55. package/dist/bundled/stores/folder.js +1 -1
  56. package/dist/bundled/stores/story.js +2 -2
  57. package/dist/bundled/util/commands.js +21 -10
  58. package/dist/bundled/util/dark.js +1 -1
  59. package/dist/bundled/util/docs.js +2 -1
  60. package/dist/bundled/util/events.js +1 -1
  61. package/dist/bundled/util/grid-slots.js +2 -2
  62. package/dist/bundled/util/icons.generated.js +0 -1
  63. package/dist/bundled/util/keyboard.js +9 -14
  64. package/dist/bundled/util/mapping.js +3 -3
  65. package/dist/bundled/util/preview-settings.js +1 -1
  66. package/dist/bundled/util/render-tracker.js +33 -0
  67. package/dist/bundled/util/variant.js +12 -2
  68. package/dist/style.css +23 -1
  69. package/package.json +14 -18
  70. package/src/__tests__/render-tracker.spec.ts +38 -0
  71. package/src/app/App.vue +7 -4
  72. package/src/app/components/HomeView.vue +1 -1
  73. package/src/app/components/app/AppLogo.vue +6 -2
  74. package/src/app/components/app/Breadcrumb.vue +2 -1
  75. package/src/app/components/app/TopBar.vue +7 -2
  76. package/src/app/components/app/TopBarChipItem.vue +1 -1
  77. package/src/app/components/base/BaseCheckbox.vue +1 -1
  78. package/src/app/components/base/BaseEmpty.vue +1 -1
  79. package/src/app/components/base/BaseListItemLink.vue +4 -2
  80. package/src/app/components/base/BaseOverflowMenu.vue +25 -18
  81. package/src/app/components/base/BaseSelect.vue +171 -15
  82. package/src/app/components/base/BaseSplitPane.vue +11 -6
  83. package/src/app/components/command/CommandPrompts.vue +1 -1
  84. package/src/app/components/command/CommandPromptsModal.vue +1 -0
  85. package/src/app/components/command/PromptSelect.vue +3 -2
  86. package/src/app/components/command/PromptText.vue +7 -3
  87. package/src/app/components/panel/ControlsComponentPropItem.vue +26 -15
  88. package/src/app/components/panel/PaneTabs.vue +6 -6
  89. package/src/app/components/panel/StatePresets.vue +125 -68
  90. package/src/app/components/panel/StoryControls.vue +1 -1
  91. package/src/app/components/panel/StoryDocs.vue +3 -1
  92. package/src/app/components/panel/StoryEvent.vue +7 -2
  93. package/src/app/components/panel/StoryEvents.vue +4 -1
  94. package/src/app/components/panel/StorySidePanel.vue +3 -1
  95. package/src/app/components/panel/StorySourceCode.vue +12 -8
  96. package/src/app/components/search/SearchItemContent.vue +2 -1
  97. package/src/app/components/search/SearchPane.vue +16 -8
  98. package/src/app/components/story/GenericMountStory.vue +2 -1
  99. package/src/app/components/story/GenericRenderStory.vue +2 -1
  100. package/src/app/components/story/StoryResponsivePreview.vue +36 -7
  101. package/src/app/components/story/StoryVariantGrid.vue +33 -16
  102. package/src/app/components/story/StoryVariantGridItem.vue +1 -1
  103. package/src/app/components/story/StoryVariantSinglePreviewRemote.vue +14 -8
  104. package/src/app/components/story/StoryView.vue +10 -4
  105. package/src/app/components/story/StoryViewer.vue +3 -3
  106. package/src/app/components/toolbar/ToolbarBackground.vue +1 -1
  107. package/src/app/components/toolbar/ToolbarResponsiveSize.vue +23 -3
  108. package/src/app/components/toolbar/ToolbarTitle.vue +2 -2
  109. package/src/app/components/tree/StoryGroup.vue +2 -2
  110. package/src/app/components/tree/StoryList.vue +2 -2
  111. package/src/app/components/tree/StoryListFolder.vue +2 -2
  112. package/src/app/components/tree/StoryListItem.vue +1 -1
  113. package/src/app/plugin.ts +7 -3
  114. package/src/app/sandbox.ts +9 -3
  115. package/src/app/stores/folder.ts +1 -1
  116. package/src/app/stores/story.ts +2 -2
  117. package/src/app/types.ts +6 -6
  118. package/src/app/util/commands.ts +27 -17
  119. package/src/app/util/dark.ts +1 -1
  120. package/src/app/util/docs.ts +2 -1
  121. package/src/app/util/env.ts +0 -1
  122. package/src/app/util/events.ts +6 -5
  123. package/src/app/util/grid-slots.ts +2 -2
  124. package/src/app/util/icons.generated.ts +1 -1
  125. package/src/app/util/keyboard.ts +10 -11
  126. package/src/app/util/mapping.ts +3 -3
  127. package/src/app/util/preview-settings.ts +1 -1
  128. package/src/app/util/render-tracker.ts +31 -0
  129. package/src/app/util/scroll.ts +5 -1
  130. package/src/app/util/state.ts +4 -4
  131. package/src/app/util/variant.ts +16 -3
  132. package/src/app/components/base/BaseCopyIcon.vue +0 -34
@@ -1,4 +1,5 @@
1
1
  <script lang="ts" setup>
2
+ import type { SetupContext, VNode } from 'vue'
2
3
  import { Icon } from '@iconify/vue'
3
4
  import { useResizeObserver } from '@vueuse/core'
4
5
  import { computed, h, onBeforeUnmount, reactive, ref } from 'vue'
@@ -11,9 +12,9 @@ const el = ref<HTMLDivElement>()
11
12
 
12
13
  const availableWidth = ref(0)
13
14
 
14
- useResizeObserver(el, (entries) => {
15
- const containerWidth = entries[0].contentRect.width
16
- availableWidth.value = containerWidth - overflowButtonWidth
15
+ useResizeObserver(el, ([entry]) => {
16
+ if (!entry) return
17
+ availableWidth.value = entry.contentRect.width - overflowButtonWidth
17
18
  })
18
19
 
19
20
  // Children
@@ -28,8 +29,8 @@ const children = ref(new Map<HTMLElement, ChildState>())
28
29
  const visibleChildrenCount = computed(() => {
29
30
  let width = 0
30
31
  const c = [...children.value.values()].sort((a, b) => a.index - b.index)
31
- for (let i = 0; i < c.length; i++) {
32
- width += c[i].width
32
+ for (const [i, child] of c.entries()) {
33
+ width += child.width
33
34
  if (width > availableWidth.value) {
34
35
  return i
35
36
  }
@@ -43,43 +44,49 @@ const visibleChildrenCount = computed(() => {
43
44
  const ChildWrapper = {
44
45
  name: 'ChildWrapper',
45
46
  props: ['index'],
46
- setup(props, { slots }) {
47
+ setup(props: { index: number }, { slots }: SetupContext) {
47
48
  const el = ref<HTMLDivElement>()
48
49
 
49
50
  const state = reactive({ width: 0, index: props.index })
50
51
 
51
- useResizeObserver(el, (entries) => {
52
- const width = entries[0].contentRect.width
53
- if (!children.value.has(el.value)) {
54
- children.value.set(el.value, state)
52
+ useResizeObserver(el, ([entry]) => {
53
+ // The observer only fires for an element it is observing, so this is
54
+ // never null here — but `children` is keyed by the element, and an
55
+ // undefined key would silently make a second entry for the same child.
56
+ const element = el.value
57
+ if (!element || !entry) return
58
+ const width = entry.contentRect.width
59
+ if (!children.value.has(element)) {
60
+ children.value.set(element, state)
55
61
  }
56
62
  state.width = width
57
63
  })
58
64
 
59
65
  onBeforeUnmount(() => {
60
- children.value.delete(el.value)
66
+ const element = el.value
67
+ if (element) children.value.delete(element)
61
68
  })
62
69
 
63
70
  const visible = computed(() => visibleChildrenCount.value > state.index)
64
71
 
65
- return () => h('div', { ref: el, style: { visibility: visible.value ? 'visible' : 'hidden' } }, slots.default())
72
+ return () => h('div', { ref: el, style: { visibility: visible.value ? 'visible' : 'hidden' } }, slots['default']?.())
66
73
  },
67
74
  }
68
75
 
69
76
  /**
70
77
  * Wraps each child with a <ChildWrapper>
71
78
  */
72
- function ChildrenRender(props, { slots }) {
73
- const [fragment] = slots.default()
74
- return fragment.children.map((vnode, index) => h(ChildWrapper, { index }, () => [vnode]))
79
+ function ChildrenRender(_props: unknown, { slots }: SetupContext) {
80
+ const children = slots['default']?.()[0]?.children
81
+ return Array.isArray(children) ? children.map((vnode, index) => h(ChildWrapper, { index }, () => [vnode])) : []
75
82
  }
76
83
 
77
84
  /**
78
85
  * Only renders a part of a children list
79
86
  */
80
- function ChildrenSlice(props, { slots }) {
81
- const [fragment] = slots.default()
82
- return fragment.children.slice(props.start, props.end)
87
+ function ChildrenSlice(props: { start?: number, end?: number }, { slots }: SetupContext) {
88
+ const children = slots['default']?.()[0]?.children
89
+ return Array.isArray(children) ? children.slice(props.start, props.end) as VNode[] : []
83
90
  }
84
91
  </script>
85
92
 
@@ -1,12 +1,22 @@
1
1
  <script lang="ts" setup>
2
2
  import type { ComputedRef } from 'vue'
3
3
  import { Icon } from '@iconify/vue'
4
+ import { refAutoReset } from '@vueuse/core'
4
5
  import { Dropdown as VDropdown } from 'floating-vue'
5
- import { computed } from 'vue'
6
+ import { computed, ref, useId } from 'vue'
7
+
8
+ /*
9
+ * The WAI-ARIA select-only combobox: the trigger keeps focus and names the active
10
+ * option through `aria-activedescendant`, so the popup needs no focus of its own.
11
+ * It was a click-only `<div>` with `<div>` options, which a keyboard could not open
12
+ * and a screen reader did not list (#828).
13
+ */
6
14
 
7
15
  const props = defineProps<{
8
16
  modelValue: string
9
17
  options: Record<string, string> | Array<string>
18
+ /** The accessible name of the picker and its list. */
19
+ label: string
10
20
  }>()
11
21
 
12
22
  const emit = defineEmits<{
@@ -21,23 +31,159 @@ const formattedOptions: ComputedRef<Record<string, string>> = computed(() => {
21
31
  return props.options
22
32
  })
23
33
 
34
+ const entries = computed(() => Object.entries(formattedOptions.value))
35
+
24
36
  const selectedLabel = computed(() => formattedOptions.value[props.modelValue])
25
37
 
26
- function selectValue(value: string, hide: () => void) {
27
- emit('update:modelValue', value)
28
- emit('select', value)
29
- hide()
38
+ const id = useId()
39
+ const listboxId = `${id}-listbox`
40
+ function optionId(index: number) {
41
+ return `${id}-option-${index}`
42
+ }
43
+
44
+ const combobox = ref<HTMLElement>()
45
+ const open = ref(false)
46
+ const activeIndex = ref(-1)
47
+
48
+ function openList(index?: number) {
49
+ const selected = entries.value.findIndex(([value]) => value === props.modelValue)
50
+ activeIndex.value = index ?? Math.max(selected, 0)
51
+ open.value = true
52
+ }
53
+
54
+ /*
55
+ * Type-to-select, as the pattern recommends: typed characters within half a second
56
+ * form one search, and repeating a single character steps through the options
57
+ * that start with it.
58
+ */
59
+ const typed = refAutoReset('', 500)
60
+
61
+ function isTypeaheadKey(event: KeyboardEvent) {
62
+ if (event.ctrlKey || event.metaKey || event.altKey || event.key.length !== 1) {
63
+ return false
64
+ }
65
+ // Space chooses, unless it continues a search already under way.
66
+ return event.key !== ' ' || typed.value !== ''
67
+ }
68
+
69
+ function typeahead(char: string) {
70
+ const previous = typed.value
71
+ typed.value = previous + char.toLowerCase()
72
+ const repeated = [...typed.value].every(c => c === typed.value[0])
73
+ const search = repeated ? typed.value.charAt(0) : typed.value
74
+ // A new or repeated character looks past the active option; a longer search may
75
+ // still match it.
76
+ const start = repeated ? activeIndex.value + 1 : Math.max(activeIndex.value, 0)
77
+ const count = entries.value.length
78
+ for (let offset = 0; offset < count; offset++) {
79
+ const index = (start + offset) % count
80
+ if (entries.value[index]?.[1].toLowerCase().startsWith(search)) {
81
+ activeIndex.value = index
82
+ return
83
+ }
84
+ }
85
+ }
86
+
87
+ function close() {
88
+ open.value = false
30
89
  }
90
+
91
+ function choose(index: number) {
92
+ const entry = entries.value[index]
93
+ if (entry) {
94
+ emit('update:modelValue', entry[0])
95
+ emit('select', entry[0])
96
+ }
97
+ close()
98
+ combobox.value?.focus()
99
+ }
100
+
101
+ function onKeydown(event: KeyboardEvent) {
102
+ const last = entries.value.length - 1
103
+
104
+ if (isTypeaheadKey(event)) {
105
+ event.preventDefault()
106
+ if (!open.value) {
107
+ openList()
108
+ }
109
+ typeahead(event.key)
110
+ return
111
+ }
112
+
113
+ if (!open.value) {
114
+ if (['ArrowDown', 'ArrowUp', 'Enter', ' ', 'Home', 'End'].includes(event.key)) {
115
+ event.preventDefault()
116
+ openList(event.key === 'Home' ? 0 : event.key === 'End' ? last : undefined)
117
+ }
118
+ return
119
+ }
120
+
121
+ switch (event.key) {
122
+ case 'ArrowDown':
123
+ event.preventDefault()
124
+ activeIndex.value = Math.min(activeIndex.value + 1, last)
125
+ break
126
+ case 'ArrowUp':
127
+ event.preventDefault()
128
+ if (event.altKey) {
129
+ choose(activeIndex.value)
130
+ }
131
+ else {
132
+ activeIndex.value = Math.max(activeIndex.value - 1, 0)
133
+ }
134
+ break
135
+ case 'Home':
136
+ event.preventDefault()
137
+ activeIndex.value = 0
138
+ break
139
+ case 'End':
140
+ event.preventDefault()
141
+ activeIndex.value = last
142
+ break
143
+ case 'Enter':
144
+ case ' ':
145
+ event.preventDefault()
146
+ choose(activeIndex.value)
147
+ break
148
+ case 'Escape':
149
+ event.preventDefault()
150
+ close()
151
+ break
152
+ case 'Tab':
153
+ // Keeps the default: focus moves on, with the active option chosen.
154
+ choose(activeIndex.value)
155
+ break
156
+ }
157
+ }
158
+
159
+ defineExpose({
160
+ focus: () => combobox.value?.focus(),
161
+ })
31
162
  </script>
32
163
 
33
164
  <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. -->
34
167
  <VDropdown
168
+ v-model:shown="open"
35
169
  class="poveste-base-select"
170
+ :triggers="[]"
171
+ no-auto-focus
36
172
  auto-size
37
173
  auto-boundary-max-size
38
174
  >
39
175
  <div
40
- class="cursor-pointer w-full outline-none 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"
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"
41
187
  >
42
188
  <div class="flex-1 truncate">
43
189
  <slot :label="selectedLabel">
@@ -49,24 +195,34 @@ function selectValue(value: string, hide: () => void) {
49
195
  class="w-4 h-4 flex-none ml-auto"
50
196
  />
51
197
  </div>
52
- <template #popper="{ hide }">
53
- <div class="flex flex-col bg-gray-50 dark:bg-gray-700">
198
+ <template #popper>
199
+ <div
200
+ :id="listboxId"
201
+ role="listbox"
202
+ :aria-label="label"
203
+ class="flex flex-col bg-gray-50 dark:bg-gray-700"
204
+ >
54
205
  <div
55
- v-for="(label, value) in formattedOptions"
56
- v-bind="{ ...$attrs, class: null, style: null }"
57
- :key="label"
206
+ v-for="([value, optionLabel], index) of entries"
207
+ :id="optionId(index)"
208
+ :key="value"
209
+ role="option"
210
+ :aria-selected="value === modelValue"
58
211
  class="px-2 py-1 cursor-pointer hover:bg-primary-100 dark:hover:bg-primary-700"
59
212
  :class="{
60
- 'bg-primary-200 dark:bg-primary-800': props.modelValue === value,
213
+ 'bg-primary-200 dark:bg-primary-800': value === modelValue,
214
+ 'outline outline-2 -outline-offset-2 outline-primary-500': index === activeIndex,
61
215
  }"
62
- @click="selectValue(value, hide)"
216
+ @mousedown.prevent
217
+ @mousemove="activeIndex = index"
218
+ @click="choose(index)"
63
219
  >
64
220
  <slot
65
221
  name="option"
66
- :label="label"
222
+ :label="optionLabel"
67
223
  :value="value"
68
224
  >
69
- {{ label }}
225
+ {{ optionLabel }}
70
226
  </slot>
71
227
  </div>
72
228
  </div>
@@ -168,15 +168,15 @@ const rightStyle = computed(() => {
168
168
  if (!hasBothPanes.value) {
169
169
  return { [axis.value]: '100%' }
170
170
  }
171
- return { [axis.value]: props.fixed ? null : `${100 - boundSplit.value}%` }
171
+ return { [axis.value]: props.fixed ? undefined : `${100 - boundSplit.value}%` }
172
172
  })
173
173
 
174
174
  const dragging = ref(false)
175
175
  let startPosition = 0
176
176
  let startSplit = 0
177
- const el = ref(null)
177
+ const el = ref<HTMLElement>()
178
178
 
179
- function dragStart(e) {
179
+ function dragStart(e: MouseEvent) {
180
180
  dragging.value = true
181
181
  startPosition = props.orientation === 'landscape' ? e.pageX : e.pageY
182
182
  startSplit = boundSplit.value
@@ -184,17 +184,22 @@ function dragStart(e) {
184
184
  window.addEventListener('mouseup', dragEnd)
185
185
  }
186
186
 
187
- function dragMove(e) {
187
+ function dragMove(e: MouseEvent) {
188
188
  if (dragging.value) {
189
+ // The move listener is on `window`, so it can outlive the element for the
190
+ // rest of a drag if the pane unmounts under it.
191
+ const pane = el.value
192
+ if (!pane) return
193
+
189
194
  let position
190
195
  let totalSize
191
196
  if (props.orientation === 'landscape') {
192
197
  position = e.pageX
193
- totalSize = el.value.offsetWidth
198
+ totalSize = pane.offsetWidth
194
199
  }
195
200
  else {
196
201
  position = e.pageY
197
- totalSize = el.value.offsetHeight
202
+ totalSize = pane.offsetHeight
198
203
  }
199
204
  const dPosition = position - startPosition
200
205
  if (props.fixed) {
@@ -24,7 +24,7 @@ const promptTypes = {
24
24
  const answers = reactive<Record<string, any>>({})
25
25
 
26
26
  // Initial default values
27
- for (const prompt of props.command.prompts) {
27
+ for (const prompt of props.command.prompts ?? []) {
28
28
  let defaultValue
29
29
  if (typeof prompt.defaultValue === 'function') {
30
30
  defaultValue = prompt.defaultValue(answers)
@@ -33,6 +33,7 @@ const commandStore = useCommandStore()
33
33
  />
34
34
  <div class="bg-white dark:bg-gray-900 md:mt-16 md:mx-auto w-screen max-w-[512px] max-h-[80vh] overflow-y-auto scroll-smooth shadow-xl border border-gray-200 dark:border-gray-750 rounded-lg relative divide-y divide-gray-200 dark:divide-gray-850">
35
35
  <CommandPrompts
36
+ v-if="commandStore.selectedCommand"
36
37
  :command="commandStore.selectedCommand"
37
38
  @close="close()"
38
39
  />
@@ -16,8 +16,9 @@ const emit = defineEmits<{
16
16
  (e: 'next'): void
17
17
  }>()
18
18
 
19
+ // Optional coming in, required going out, so the empty case settles here.
19
20
  const model = computed({
20
- get: () => props.modelValue,
21
+ get: () => props.modelValue ?? '',
21
22
  set: (value) => {
22
23
  emit('update:modelValue', value)
23
24
  emit('next')
@@ -76,7 +77,7 @@ const {
76
77
  } = useSelection(formattedOptions)
77
78
 
78
79
  function selectIndex(index: number) {
79
- const result = formattedOptions.value[index].value
80
+ const result = formattedOptions.value[index]?.value
80
81
  if (result) {
81
82
  model.value = result
82
83
  }
@@ -9,11 +9,15 @@ const props = defineProps<{
9
9
  }>()
10
10
 
11
11
  const emit = defineEmits<{
12
- (e: 'update:modelValue', value: string): void
12
+ (e: 'update:modelValue', value: string | undefined): void
13
13
  }>()
14
14
 
15
- const model = computed({
16
- get: () => props.modelValue,
15
+ // The input needs a string to bind to, so the getter fills in `''` — but an
16
+ // absent answer has to leave as absent. `''` is falsy and *not* nullish, so
17
+ // coercing on the way out would stop a command's `answers.x ?? fallback` from
18
+ // firing, which is the trap #440 was about one file over.
19
+ const model = computed<string | undefined>({
20
+ get: () => props.modelValue ?? '',
17
21
  set: value => emit('update:modelValue', value),
18
22
  })
19
23
 
@@ -50,24 +50,35 @@ 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>
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
+ <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
+ >
62
78
  <Icon
63
- v-tooltip="'Remove override'"
64
79
  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"
66
- :class="[
67
- canReset ? 'opacity-50 hover:opacity-100' : 'opacity-25 pointer-events-none',
68
- ]"
69
- @click.stop="reset()"
80
+ class="w-4 h-4"
70
81
  />
71
- </template>
72
- </component>
82
+ </button>
83
+ </div>
73
84
  </template>
@@ -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
  }"