@poveste/app 0.13.0 → 0.14.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 (110) hide show
  1. package/dist/bundled/App.vue_vue_type_script_setup_true_lang.js +26 -34
  2. package/dist/bundled/app.css +18 -24
  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/base/BaseCheckbox.vue_vue_type_script_setup_true_lang.js +1 -1
  9. package/dist/bundled/components/base/BaseListItemLink.js +6 -3
  10. package/dist/bundled/components/base/BaseListItemLink.vue_vue_type_script_lang.js +10 -4
  11. package/dist/bundled/components/base/BaseOverflowMenu.vue_vue_type_script_setup_true_lang.js +5 -2
  12. package/dist/bundled/components/base/BaseOverflowTab.js +6 -3
  13. package/dist/bundled/components/base/BaseOverflowTab.vue_vue_type_script_lang.js +4 -0
  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 +6 -4
  16. package/dist/bundled/components/base/BaseTab.js +6 -3
  17. package/dist/bundled/components/base/BaseTab.vue_vue_type_script_lang.js +4 -0
  18. package/dist/bundled/components/command/CommandPrompts.vue_vue_type_script_setup_true_lang.js +1 -1
  19. package/dist/bundled/components/command/CommandPromptsModal.vue_vue_type_script_setup_true_lang.js +4 -3
  20. package/dist/bundled/components/command/PromptSelect.vue_vue_type_script_setup_true_lang.js +1 -1
  21. package/dist/bundled/components/command/PromptText.vue_vue_type_script_setup_true_lang.js +1 -1
  22. package/dist/bundled/components/panel/PaneTabs.vue_vue_type_script_setup_true_lang.js +4 -4
  23. package/dist/bundled/components/panel/StatePresets.vue_vue_type_script_setup_true_lang.js +30 -20
  24. package/dist/bundled/components/panel/StoryControls.vue_vue_type_script_setup_true_lang.js +1 -1
  25. package/dist/bundled/components/panel/StoryDocs.vue_vue_type_script_setup_true_lang.js +3 -1
  26. package/dist/bundled/components/panel/StoryEvent.vue_vue_type_script_setup_true_lang.js +5 -1
  27. package/dist/bundled/components/panel/StoryEvents.vue_vue_type_script_setup_true_lang.js +2 -1
  28. package/dist/bundled/components/panel/StorySidePanel.vue_vue_type_script_setup_true_lang.js +7 -5
  29. package/dist/bundled/components/panel/StorySourceCode.js +1 -1
  30. package/dist/bundled/components/panel/StorySourceCode.vue_vue_type_script_setup_true_lang.js +6 -5
  31. package/dist/bundled/components/search/SearchPane.vue_vue_type_script_setup_true_lang.js +15 -7
  32. package/dist/bundled/components/story/GenericMountStory.vue_vue_type_script_setup_true_lang.js +2 -1
  33. package/dist/bundled/components/story/GenericRenderStory.vue_vue_type_script_setup_true_lang.js +2 -1
  34. package/dist/bundled/components/story/StoryResponsivePreview.js +1 -1
  35. package/dist/bundled/components/story/StoryResponsivePreview.vue_vue_type_script_setup_true_lang.js +17 -6
  36. package/dist/bundled/components/story/StoryVariantGrid.js +1 -1
  37. package/dist/bundled/components/story/StoryVariantGrid.vue_vue_type_script_setup_true_lang.js +15 -10
  38. package/dist/bundled/components/story/StoryVariantSingle.js +1 -1
  39. package/dist/bundled/components/story/StoryVariantSingle.vue_vue_type_script_setup_true_lang.js +21 -33
  40. package/dist/bundled/components/story/StoryVariantSingleView.vue_vue_type_script_setup_true_lang.js +1 -1
  41. package/dist/bundled/components/story/StoryView.vue_vue_type_script_setup_true_lang.js +3 -2
  42. package/dist/bundled/components/story/StoryViewer.js +1 -1
  43. package/dist/bundled/components/story/StoryViewer.vue_vue_type_script_setup_true_lang.js +3 -3
  44. package/dist/bundled/components/toolbar/ToolbarBackground.js +1 -3
  45. package/dist/bundled/components/toolbar/ToolbarBackground.vue_vue_type_script_setup_true_lang.js +2 -6
  46. package/dist/bundled/components/toolbar/ToolbarResponsiveSize.vue_vue_type_script_setup_true_lang.js +17 -16
  47. package/dist/bundled/plugin.js +4 -3
  48. package/dist/bundled/sandbox.js +2 -2
  49. package/dist/bundled/stores/folder.js +1 -1
  50. package/dist/bundled/util/commands.js +20 -9
  51. package/dist/bundled/util/docs.js +2 -1
  52. package/dist/bundled/util/grid-slots.js +1 -1
  53. package/dist/bundled/util/highlighter.js +1 -1
  54. package/dist/bundled/util/icons.generated.js +0 -1
  55. package/dist/bundled/util/keyboard.js +1 -0
  56. package/dist/bundled/util/mapping.js +6 -2
  57. package/dist/bundled/util/preview-settings.js +1 -1
  58. package/dist/style.css +14 -2
  59. package/package.json +8 -6
  60. package/src/app/App.vue +36 -29
  61. package/src/app/components/HomeView.vue +1 -1
  62. package/src/app/components/app/AppLogo.vue +6 -2
  63. package/src/app/components/app/Breadcrumb.vue +2 -1
  64. package/src/app/components/app/TopBar.vue +7 -2
  65. package/src/app/components/base/BaseCheckbox.vue +1 -1
  66. package/src/app/components/base/BaseListItemLink.vue +12 -1
  67. package/src/app/components/base/BaseOverflowMenu.vue +9 -3
  68. package/src/app/components/base/BaseOverflowTab.vue +12 -1
  69. package/src/app/components/base/BaseSplitPane.vue +9 -4
  70. package/src/app/components/base/BaseTab.vue +12 -1
  71. package/src/app/components/command/CommandPrompts.vue +10 -2
  72. package/src/app/components/command/CommandPromptsModal.vue +1 -0
  73. package/src/app/components/command/PromptSelect.vue +2 -1
  74. package/src/app/components/command/PromptText.vue +7 -3
  75. package/src/app/components/panel/PaneTabs.vue +4 -4
  76. package/src/app/components/panel/StatePresets.vue +26 -5
  77. package/src/app/components/panel/StoryControls.vue +1 -1
  78. package/src/app/components/panel/StoryDocs.vue +3 -1
  79. package/src/app/components/panel/StoryEvent.vue +7 -2
  80. package/src/app/components/panel/StoryEvents.vue +4 -1
  81. package/src/app/components/panel/StorySidePanel.vue +2 -0
  82. package/src/app/components/panel/StorySourceCode.vue +11 -8
  83. package/src/app/components/search/SearchPane.vue +14 -5
  84. package/src/app/components/story/GenericMountStory.vue +2 -1
  85. package/src/app/components/story/GenericRenderStory.vue +2 -1
  86. package/src/app/components/story/StoryResponsivePreview.vue +30 -5
  87. package/src/app/components/story/StoryVariantGrid.vue +33 -16
  88. package/src/app/components/story/StoryVariantSingle.vue +44 -52
  89. package/src/app/components/story/StoryVariantSingleView.vue +1 -1
  90. package/src/app/components/story/StoryView.vue +7 -3
  91. package/src/app/components/story/StoryViewer.vue +3 -3
  92. package/src/app/components/toolbar/ToolbarBackground.vue +1 -8
  93. package/src/app/components/toolbar/ToolbarResponsiveSize.vue +23 -3
  94. package/src/app/components/tree/StoryGroup.vue +1 -1
  95. package/src/app/plugin.ts +7 -3
  96. package/src/app/sandbox.ts +1 -1
  97. package/src/app/stores/folder.ts +1 -1
  98. package/src/app/types.ts +2 -2
  99. package/src/app/util/commands.ts +26 -16
  100. package/src/app/util/docs.ts +2 -1
  101. package/src/app/util/grid-slots.ts +1 -1
  102. package/src/app/util/highlighter.ts +9 -1
  103. package/src/app/util/icons.generated.ts +1 -1
  104. package/src/app/util/keyboard.ts +3 -0
  105. package/src/app/util/mapping.ts +6 -2
  106. package/src/app/util/preview-settings.ts +1 -1
  107. package/src/app/util/scroll.ts +5 -1
  108. package/src/app/util/state.ts +1 -1
  109. package/src/app/util/variant.ts +4 -1
  110. package/src/app/components/base/BaseCopyIcon.vue +0 -34
@@ -1,10 +1,20 @@
1
1
  <script lang="ts">
2
+ import type { PropType } from 'vue'
3
+ import type { RouteLocationRaw } from 'vue-router'
2
4
  import { defineComponent } from 'vue'
3
5
 
4
6
  export default defineComponent({
5
7
  inheritAttrs: false,
6
8
 
7
9
  props: {
10
+ // Declared rather than passed through `$attrs`: with
11
+ // `inheritAttrs: false` the checker cannot see it there, and
12
+ // router-link requires it.
13
+ to: {
14
+ type: [String, Object] as PropType<RouteLocationRaw>,
15
+ required: true,
16
+ },
17
+
8
18
  isActive: {
9
19
  type: Boolean,
10
20
  default: undefined,
@@ -31,6 +41,7 @@ export default defineComponent({
31
41
  <template>
32
42
  <RouterLink
33
43
  v-slot="{ isActive: linkIsActive, href, navigate }"
44
+ :to="to"
34
45
  class="poveste-base-list-item-link"
35
46
  v-bind="$attrs"
36
47
  custom
@@ -42,7 +53,7 @@ export default defineComponent({
42
53
  :class="[
43
54
  $attrs.class,
44
55
  (isActive != null ? isActive : linkIsActive)
45
- ? 'active bg-primary-500 hover:bg-primary-600 text-white dark:text-black'
56
+ ? 'active bg-primary-700 hover:bg-primary-800 dark:bg-primary-500 dark:hover:bg-primary-600 text-white dark:text-black'
46
57
  : 'hover:bg-primary-100 dark:hover:bg-primary-900',
47
58
  ]"
48
59
  @click="handleNavigate($event, navigate)"
@@ -49,15 +49,21 @@ const ChildWrapper = {
49
49
  const state = reactive({ width: 0, index: props.index })
50
50
 
51
51
  useResizeObserver(el, (entries) => {
52
+ // The observer only fires for an element it is observing, so this is
53
+ // never null here — but `children` is keyed by the element, and an
54
+ // undefined key would silently make a second entry for the same child.
55
+ const element = el.value
56
+ if (!element) return
52
57
  const width = entries[0].contentRect.width
53
- if (!children.value.has(el.value)) {
54
- children.value.set(el.value, state)
58
+ if (!children.value.has(element)) {
59
+ children.value.set(element, state)
55
60
  }
56
61
  state.width = width
57
62
  })
58
63
 
59
64
  onBeforeUnmount(() => {
60
- children.value.delete(el.value)
65
+ const element = el.value
66
+ if (element) children.value.delete(element)
61
67
  })
62
68
 
63
69
  const visible = computed(() => visibleChildrenCount.value > state.index)
@@ -1,10 +1,20 @@
1
1
  <script lang="ts">
2
+ import type { PropType } from 'vue'
3
+ import type { RouteLocationRaw } from 'vue-router'
2
4
  import { defineComponent } from 'vue'
3
5
 
4
6
  export default defineComponent({
5
7
  inheritAttrs: false,
6
8
 
7
9
  props: {
10
+ // Declared rather than passed through `$attrs`: with
11
+ // `inheritAttrs: false` the checker cannot see it there, and
12
+ // router-link requires it.
13
+ to: {
14
+ type: [String, Object] as PropType<RouteLocationRaw>,
15
+ required: true,
16
+ },
17
+
8
18
  exact: {
9
19
  type: Boolean,
10
20
  default: false,
@@ -21,6 +31,7 @@ export default defineComponent({
21
31
  <template>
22
32
  <router-link
23
33
  v-slot="{ isActive, isExactActive, href, navigate }"
34
+ :to="to"
24
35
  class="poveste-base-overflow-tab"
25
36
  v-bind="$attrs"
26
37
  custom
@@ -30,7 +41,7 @@ export default defineComponent({
30
41
  :href="href"
31
42
  class="px-4 h-10 min-w-[150px] inline-flex items-center hover:bg-primary-50 dark:hover:bg-primary-900 relative text-gray-900 dark:text-gray-100"
32
43
  :class="{
33
- 'text-primary-500 dark:text-primary-400': matched != null ? matched : (exact && isExactActive) || (!exact && isActive),
44
+ 'text-primary-700 dark:text-primary-400': matched != null ? matched : (exact && isExactActive) || (!exact && isActive),
34
45
  }"
35
46
  @click="navigate"
36
47
  >
@@ -168,13 +168,13 @@ 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
179
  function dragStart(e) {
180
180
  dragging.value = true
@@ -186,15 +186,20 @@ function dragStart(e) {
186
186
 
187
187
  function dragMove(e) {
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) {
@@ -1,10 +1,20 @@
1
1
  <script lang="ts">
2
+ import type { PropType } from 'vue'
3
+ import type { RouteLocationRaw } from 'vue-router'
2
4
  import { defineComponent } from 'vue'
3
5
 
4
6
  export default defineComponent({
5
7
  inheritAttrs: false,
6
8
 
7
9
  props: {
10
+ // Declared rather than passed through `$attrs`: with
11
+ // `inheritAttrs: false` the checker cannot see it there, and
12
+ // router-link requires it.
13
+ to: {
14
+ type: [String, Object] as PropType<RouteLocationRaw>,
15
+ required: true,
16
+ },
17
+
8
18
  exact: {
9
19
  type: Boolean,
10
20
  default: false,
@@ -21,6 +31,7 @@ export default defineComponent({
21
31
  <template>
22
32
  <router-link
23
33
  v-slot="{ isActive, isExactActive, href, navigate }"
34
+ :to="to"
24
35
  class="poveste-base-tab"
25
36
  v-bind="$attrs"
26
37
  custom
@@ -30,7 +41,7 @@ export default defineComponent({
30
41
  :href="href"
31
42
  class="px-4 h-full inline-flex items-center hover:bg-primary-50 dark:hover:bg-primary-900 relative text-gray-900 dark:text-gray-100"
32
43
  :class="{
33
- 'text-primary-500 dark:text-primary-400': matched != null ? matched : (exact && isExactActive) || (!exact && isActive),
44
+ 'text-primary-700 dark:text-primary-400': matched != null ? matched : (exact && isExactActive) || (!exact && isActive),
34
45
  }"
35
46
  @click="navigate"
36
47
  >
@@ -1,5 +1,6 @@
1
1
  <script lang="ts" setup>
2
2
  import type { ClientCommand } from '@poveste/shared'
3
+ import type { Component } from 'vue'
3
4
  import { nextTick, onMounted, reactive, ref } from 'vue'
4
5
  import { executeCommand, getCommandContext } from '../../util/commands.js'
5
6
  import BaseButton from '../base/BaseButton.vue'
@@ -23,7 +24,7 @@ const promptTypes = {
23
24
  const answers = reactive<Record<string, any>>({})
24
25
 
25
26
  // Initial default values
26
- for (const prompt of props.command.prompts) {
27
+ for (const prompt of props.command.prompts ?? []) {
27
28
  let defaultValue
28
29
  if (typeof prompt.defaultValue === 'function') {
29
30
  defaultValue = prompt.defaultValue(answers)
@@ -67,8 +68,15 @@ onMounted(() => {
67
68
  {{ command.label }}
68
69
  </div>
69
70
 
71
+ <!--
72
+ `promptTypes[prompt.type]` resolves to the component whose `prompt` prop
73
+ is that member of the union, but nothing states the correlation and the
74
+ checker cannot infer it. Splitting this into `v-if` branches per type is
75
+ what makes it checkable; that is a refactor of untested UI, so it waits
76
+ for coverage rather than riding along with the type gate (#441).
77
+ -->
70
78
  <component
71
- :is="promptTypes[prompt.type]"
79
+ :is="promptTypes[prompt.type] as Component"
72
80
  v-for="(prompt, index) of command.prompts"
73
81
  :key="prompt.field"
74
82
  ref="promptComps"
@@ -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')
@@ -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
 
@@ -33,7 +33,7 @@ const hasEvents = computed(() => eventsStore.events.length)
33
33
  :to="{ ...$route, query: { ...$route.query, tab: 'docs' } }"
34
34
  :matched="$route.query.tab === 'docs'"
35
35
  :class="{
36
- 'opacity-50': !renderedDoc,
36
+ 'opacity-70': !renderedDoc,
37
37
  }"
38
38
  >
39
39
  Docs
@@ -42,7 +42,7 @@ const hasEvents = computed(() => eventsStore.events.length)
42
42
  :to="{ ...$route, query: { ...$route.query, tab: 'events' } }"
43
43
  :matched="$route.query.tab === 'events'"
44
44
  :class="{
45
- 'opacity-50': !hasEvents,
45
+ 'opacity-70': !hasEvents,
46
46
  }"
47
47
  >
48
48
  Events
@@ -62,7 +62,7 @@ const hasEvents = computed(() => eventsStore.events.length)
62
62
  :to="{ ...$route, query: { ...$route.query, tab: 'docs' } }"
63
63
  :matched="$route.query.tab === 'docs'"
64
64
  :class="{
65
- 'opacity-50': !renderedDoc,
65
+ 'opacity-70': !renderedDoc,
66
66
  }"
67
67
  >
68
68
  Docs
@@ -71,7 +71,7 @@ const hasEvents = computed(() => eventsStore.events.length)
71
71
  :to="{ ...$route, query: { ...$route.query, tab: 'events' } }"
72
72
  :matched="$route.query.tab === 'events'"
73
73
  :class="{
74
- 'opacity-50': !hasEvents,
74
+ 'opacity-70': !hasEvents,
75
75
  }"
76
76
  >
77
77
  Events
@@ -57,8 +57,13 @@ function applyPreset(id) {
57
57
  if (id === DEFAULT_ID) {
58
58
  resetState()
59
59
  }
60
- else if (presetStates.value.has(id)) {
61
- applyState(props.variant.state, clone(toRawDeep(presetStates.value.get(id).state)))
60
+ else {
61
+ // `has` then `get` is two lookups and only the second one is the value;
62
+ // reading it once says the same thing and carries the type with it.
63
+ const preset = presetStates.value.get(id)
64
+ if (preset) {
65
+ applyState(props.variant.state, clone(toRawDeep(preset.state)))
66
+ }
62
67
  }
63
68
  }
64
69
 
@@ -80,9 +85,22 @@ async function createPreset() {
80
85
  selectedOption.value = id
81
86
  isEditing.value = true
82
87
  await nextTick()
83
- input.value.select()
88
+ // The input is rendered by the edit state set above, so it exists by now —
89
+ // unless editing was cancelled during the tick, which unmounts it.
90
+ input.value?.select()
84
91
  }
85
92
 
93
+ // The template bound `v-model` straight to `presetStates.get(selectedOption).label`,
94
+ // which is a Map lookup that can miss. Naming it once keeps the template honest
95
+ // and gives the miss somewhere to go.
96
+ const editingLabel = computed({
97
+ get: () => presetStates.value.get(selectedOption.value)?.label ?? '',
98
+ set: (value) => {
99
+ const preset = presetStates.value.get(selectedOption.value)
100
+ if (preset) preset.label = value
101
+ },
102
+ })
103
+
86
104
  const savedNotif = ref(false)
87
105
  const savedTimeout = useTimeoutFn(() => {
88
106
  savedNotif.value = false
@@ -92,6 +110,7 @@ async function savePreset() {
92
110
  if (!canEdit.value) return
93
111
 
94
112
  const preset = presetStates.value.get(selectedOption.value)
113
+ if (!preset) return
95
114
  preset.state = toPresetState(props.variant.state, omitKeys)
96
115
  savedNotif.value = true
97
116
  savedTimeout.start()
@@ -117,7 +136,9 @@ async function startEditing() {
117
136
 
118
137
  isEditing.value = true
119
138
  await nextTick()
120
- input.value.select()
139
+ // The input is rendered by the edit state set above, so it exists by now —
140
+ // unless editing was cancelled during the tick, which unmounts it.
141
+ input.value?.select()
121
142
  }
122
143
 
123
144
  function stopEditing() {
@@ -145,7 +166,7 @@ onClickOutside(select, stopEditing)
145
166
  <input
146
167
  v-if="isEditing"
147
168
  ref="input"
148
- v-model="presetStates.get(selectedOption).label"
169
+ v-model="editingLabel"
149
170
  type="text"
150
171
  class="text-inherit bg-transparent w-full h-full outline-none"
151
172
  @click.stop.prevent
@@ -28,7 +28,7 @@ watch(() => props.variant, () => {
28
28
  ready.value = false
29
29
  })
30
30
 
31
- const hasCustomControls = computed(() => props.variant.slots().controls || props.story.slots().controls)
31
+ const hasCustomControls = computed(() => props.variant.slots?.().controls || props.story.slots?.().controls)
32
32
 
33
33
  const hasInitState = computed(() => Object
34
34
  .entries(props.variant.state || {})
@@ -14,7 +14,9 @@ export function useStoryDoc(story: Ref<Story>) {
14
14
 
15
15
  watchEffect(async () => {
16
16
  // Markdown file
17
- const mdKey = story.value.file.filePath.replace(/\.(\w*)$/, '.md')
17
+ const filePath = story.value?.file?.filePath
18
+ if (!filePath) return
19
+ const mdKey = filePath.replace(/\.(\w*)$/, '.md')
18
20
  if (markdownFiles[mdKey]) {
19
21
  const md = await markdownFiles[mdKey]()
20
22
  renderedDoc.value = md.html
@@ -10,8 +10,13 @@ const formattedArgument = computed(() => {
10
10
  switch (typeof props.event.argument) {
11
11
  case 'string':
12
12
  return `"${props.event.argument}"`
13
- case 'object':
14
- return `{ ${Object.keys(props.event.argument).map(key => `${key}: ${props.event.argument[key]}`).join(', ')} }`
13
+ case 'object': {
14
+ // `argument` is `unknown`, and `typeof x === 'object'` narrows it only as
15
+ // far as `object | null` — enough for `Object.keys`, not for indexing.
16
+ const argument = props.event.argument as Record<string, unknown> | null
17
+ if (!argument) return 'null'
18
+ return `{ ${Object.keys(argument).map(key => `${key}: ${argument[key]}`).join(', ')} }`
19
+ }
15
20
  default:
16
21
  return props.event.argument
17
22
  }
@@ -19,7 +19,10 @@ async function resetUnseen() {
19
19
  eventsStore.unseen = 0
20
20
  }
21
21
  await nextTick()
22
- eventsElement.value.scrollTo({ top: eventsElement.value.scrollHeight })
22
+ // Read after the await, and it is a template ref: the panel can be closed
23
+ // during the tick, and then there is nothing to scroll.
24
+ const events = eventsElement.value
25
+ events?.scrollTo({ top: events.scrollHeight })
23
26
  }
24
27
  </script>
25
28
 
@@ -60,6 +60,7 @@ const panelContentComponent = computed(() => {
60
60
  <template #first>
61
61
  <div class="flex flex-col h-full">
62
62
  <PaneTabs
63
+ v-if="storyStore.currentStory"
63
64
  :story="storyStore.currentStory"
64
65
  :variant="storyStore.currentVariant"
65
66
  />
@@ -75,6 +76,7 @@ const panelContentComponent = computed(() => {
75
76
 
76
77
  <template #last>
77
78
  <StorySourceCode
79
+ v-if="storyStore.currentStory"
78
80
  :story="storyStore.currentStory"
79
81
  :variant="storyStore.currentVariant"
80
82
  class="h-full"
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import type { Highlighter } from 'shiki'
2
+ import type { HighlighterCore } from 'shiki/core'
3
3
  import type { Story, Variant } from '../../types'
4
4
  import { Icon } from '@iconify/vue'
5
5
  import { HstCopyIcon } from '@poveste/controls'
@@ -15,20 +15,23 @@ const props = defineProps<{
15
15
  variant: Variant
16
16
  }>()
17
17
 
18
- const generateSourceCodeFn = ref(null)
18
+ // `ref(null)` infers `Ref<null>`, so the assignment below and the call further
19
+ // down were both errors about a type nobody meant to declare.
20
+ const generateSourceCodeFn = shallowRef<((variant: Variant) => Promise<string>) | null>(null)
19
21
 
20
22
  watchEffect(async () => {
21
- const clientPlugin = clientSupportPlugins[props.story.file?.supportPluginId]
23
+ const supportPluginId = props.story.file?.supportPluginId
24
+ const clientPlugin = supportPluginId ? clientSupportPlugins[supportPluginId] : undefined
22
25
  if (clientPlugin) {
23
26
  const pluginModule = await clientPlugin()
24
27
  generateSourceCodeFn.value = markRaw(pluginModule.generateSourceCode)
25
28
  }
26
29
  })
27
30
 
28
- const highlighter = shallowRef<Highlighter>()
31
+ const highlighter = shallowRef<HighlighterCore>()
29
32
 
30
33
  const dynamicSourceCode = ref('')
31
- const error = ref<string>(null)
34
+ const error = ref<string | null>(null)
32
35
 
33
36
  // Above the watch that assigns it: the callback is async but runs immediately,
34
37
  // and a story whose `slots()` throws reaches the auto-switch below before its
@@ -103,7 +106,7 @@ onMounted(async () => {
103
106
  const sourceHtml = computed(() => displayedSourceCode.value
104
107
  ? highlighter.value?.codeToHtml(displayedSourceCode.value, {
105
108
  lang: 'html',
106
- theme: isDark.value ? 'github-dark' : 'github-light',
109
+ theme: isDark.value ? 'github-dark' : 'github-light-high-contrast',
107
110
  })
108
111
  : '')
109
112
 
@@ -152,7 +155,7 @@ watch(sourceHtml, async () => {
152
155
  v-tooltip="!dynamicSourceCode ? 'Dynamic source code is not available' : displayedSource !== 'dynamic' ? 'Switch to dynamic source' : null"
153
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"
154
157
  :class="[
155
- displayedSource !== 'dynamic' ? 'max-w-6 opacity-70' : 'max-w-[82px] text-primary-600 dark:text-primary-400',
158
+ displayedSource !== 'dynamic' ? 'max-w-6 opacity-70' : 'max-w-[82px] text-primary-800 dark:text-primary-400',
156
159
  dynamicSourceCode ? 'cursor-pointer hover:bg-gray-500/30 active:bg-gray-600/50' : 'opacity-50',
157
160
  ]"
158
161
  @click="dynamicSourceCode && (displayedSource = 'dynamic')"
@@ -174,7 +177,7 @@ watch(sourceHtml, async () => {
174
177
  v-tooltip="!staticSourceCode ? 'Static source code is not available' : displayedSource !== 'static' ? 'Switch to static source' : null"
175
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"
176
179
  :class="[
177
- displayedSource !== 'static' ? 'max-w-6 opacity-70' : 'max-w-[63px] text-primary-600 dark:text-primary-400',
180
+ displayedSource !== 'static' ? 'max-w-6 opacity-70' : 'max-w-[63px] text-primary-800 dark:text-primary-400',
178
181
  staticSourceCode ? 'cursor-pointer hover:bg-gray-500/30 active:bg-gray-600/50' : 'opacity-50',
179
182
  ]"
180
183
  @click="staticSourceCode && (displayedSource = 'static')"
@@ -45,7 +45,9 @@ watch(() => props.shown, (value) => {
45
45
  if (value) {
46
46
  requestAnimationFrame(() => {
47
47
  focused.value = true
48
- input.value.select()
48
+ // A whole frame later, so the pane may have been closed again — a
49
+ // template ref is undefined once its element unmounts.
50
+ input.value?.select()
49
51
  })
50
52
  }
51
53
  })
@@ -127,7 +129,11 @@ watch(rateLimitedSearch, async (value) => {
127
129
  if (!idMapData) continue
128
130
  switch (idMapData.kind) {
129
131
  case 'story': {
130
- list.push(storyResultFactory(storyStore.getStoryById(idMapData.id), rank))
132
+ // The search index is built from the stories and outlives an
133
+ // individual one, so a hit can name a story that is no longer loaded.
134
+ const story = storyStore.getStoryById(idMapData.id)
135
+ if (!story) continue
136
+ list.push(storyResultFactory(story, rank))
131
137
  rank++
132
138
  break
133
139
  }
@@ -135,6 +141,7 @@ watch(rateLimitedSearch, async (value) => {
135
141
  const [storyId] = idMapData.id.split(':')
136
142
  const story = storyStore.getStoryById(storyId)
137
143
  const variant = storyStore.getVariantById(idMapData.id)
144
+ if (!story || !variant) continue
138
145
  list.push(variantResultFactory(story, variant, rank))
139
146
  rank++
140
147
  break
@@ -158,7 +165,9 @@ async function searchOnDocField(query: string) {
158
165
  if (!idMapData) continue
159
166
  switch (idMapData.kind) {
160
167
  case 'story': {
161
- list.push(storyResultFactory(storyStore.getStoryById(idMapData.id), rank, 'docs'))
168
+ const story = storyStore.getStoryById(idMapData.id)
169
+ if (!story) continue
170
+ list.push(storyResultFactory(story, rank, 'docs'))
162
171
  rank++
163
172
  break
164
173
  }
@@ -188,7 +197,7 @@ function storyResultFactory(story: Story, rank: number, type: SearchResultType =
188
197
  : {},
189
198
  },
190
199
  },
191
- path: story.file.path.slice(0, -1),
200
+ path: story.file?.path.slice(0, -1) ?? [],
192
201
  icon: story.icon,
193
202
  iconColor: story.iconColor,
194
203
  }
@@ -212,7 +221,7 @@ function variantResultFactory(story: Story, variant: Variant, rank: number, type
212
221
  : {},
213
222
  },
214
223
  },
215
- path: [...story.file.path ?? [], story.title],
224
+ path: [...story.file?.path ?? [], story.title],
216
225
  icon: variant.icon,
217
226
  iconColor: variant.iconColor,
218
227
  }
@@ -16,7 +16,8 @@ const props = defineProps<{
16
16
  const mountComponent = ref(null)
17
17
 
18
18
  watchEffect(async () => {
19
- const clientPlugin = clientSupportPlugins[props.story.file?.supportPluginId]
19
+ const supportPluginId = props.story.file?.supportPluginId
20
+ const clientPlugin = supportPluginId ? clientSupportPlugins[supportPluginId] : undefined
20
21
  if (clientPlugin) {
21
22
  const pluginModule = await clientPlugin()
22
23
  mountComponent.value = markRaw(pluginModule.MountStory)
@@ -16,7 +16,8 @@ const props = defineProps<{
16
16
  const mountComponent = ref(null)
17
17
 
18
18
  watchEffect(async () => {
19
- const clientPlugin = clientSupportPlugins[props.story.file?.supportPluginId]
19
+ const supportPluginId = props.story.file?.supportPluginId
20
+ const clientPlugin = supportPluginId ? clientSupportPlugins[supportPluginId] : undefined
20
21
  if (clientPlugin) {
21
22
  try {
22
23
  const pluginModule = await clientPlugin()
@@ -43,12 +43,34 @@ function addWindowListener(event: string, listener: (event: any) => unknown) {
43
43
 
44
44
  const previewWrapper = ref<HTMLDivElement>()
45
45
 
46
- function useDragger(el: Ref<HTMLDivElement>, value: Ref<number>, min: number, max: number, axis: 'x' | 'y') {
46
+ function useDragger(el: Ref<HTMLDivElement | undefined>, value: Ref<number | null>, min: number, max: number, axis: 'x' | 'y') {
47
+ // The wrapper is a template ref, so it is undefined before mount and after
48
+ // unmount. Every read below happens during a drag, when it is on screen — but
49
+ // the move and up listeners are on `window`, so "during a drag" is not the
50
+ // same as "while this component is alive".
51
+ function wrapperSize() {
52
+ const wrapper = previewWrapper.value
53
+ if (!wrapper) return null
54
+ return axis === 'x' ? wrapper.clientWidth : wrapper.clientHeight
55
+ }
56
+
57
+ // `null` is the value that means "size to the available space", so a drag
58
+ // beginning from auto has to begin from the size actually rendered. The touch
59
+ // path had no fallback and `null + delta` coerces to `delta`, which collapsed
60
+ // a full-height preview to the minimum on the first move (#440).
61
+ function startFrom() {
62
+ if (value.value !== null) return value.value
63
+ const size = wrapperSize()
64
+ // Nothing to measure against, so start from the floor rather than from NaN.
65
+ if (size === null) return min
66
+ return size - (axis === 'x' ? 67 : 70)
67
+ }
68
+
47
69
  function onMouseDown(event: MouseEvent) {
48
70
  event.preventDefault()
49
71
  event.stopPropagation()
50
72
  const start = axis === 'x' ? event.clientX : event.clientY
51
- const startValue = value.value ?? (axis === 'x' ? previewWrapper.value.clientWidth - 67 : previewWrapper.value.clientHeight - 70)
73
+ const startValue = startFrom()
52
74
  resizing.value = true
53
75
 
54
76
  const removeListeners = [
@@ -57,11 +79,14 @@ function useDragger(el: Ref<HTMLDivElement>, value: Ref<number>, min: number, ma
57
79
  ]
58
80
 
59
81
  function onMouseMove(event: MouseEvent) {
60
- const snapTarget = (axis === 'x' ? previewWrapper.value.clientWidth : previewWrapper.value.clientHeight)
82
+ const snapTarget = wrapperSize()
61
83
  const delta = (axis === 'x' ? event.clientX : event.clientY) - start
62
84
  value.value = Math.max(min, Math.min(max, startValue + delta))
63
85
 
64
- if (Math.abs(value.value - (snapTarget - 67)) < 16) {
86
+ // 67 on both axes, unlike `startFrom`. Left as it was: the snap threshold
87
+ // is a feel, not a measurement, and changing it here would be a UX edit
88
+ // wearing a type fix's clothes.
89
+ if (snapTarget !== null && Math.abs(value.value - (snapTarget - 67)) < 16) {
65
90
  value.value = null
66
91
  }
67
92
  }
@@ -77,7 +102,7 @@ function useDragger(el: Ref<HTMLDivElement>, value: Ref<number>, min: number, ma
77
102
  event.preventDefault()
78
103
  event.stopPropagation()
79
104
  const start = axis === 'x' ? event.touches[0].clientX : event.touches[0].clientY
80
- const startValue = value.value
105
+ const startValue = startFrom()
81
106
  resizing.value = true
82
107
 
83
108
  const removeListeners = [