@poveste/app 0.1.3 → 0.2.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 (187) hide show
  1. package/dist/app/style/floating.pcss +4 -4
  2. package/dist/app/style/main.pcss +2 -2
  3. package/dist/app/style/sandbox.css +1 -1
  4. package/dist/bundle-main.js +2 -0
  5. package/dist/bundled/App.vue2.js +81 -37
  6. package/dist/bundled/app.css +51 -46
  7. package/dist/bundled/components/HomeView.vue2.js +3 -3
  8. package/dist/bundled/components/app/AppActions.vue.js +5 -0
  9. package/dist/bundled/components/app/AppActions.vue2.js +76 -0
  10. package/dist/bundled/components/app/AppHeader.vue2.js +5 -47
  11. package/dist/bundled/components/app/Breadcrumb.vue.js +1 -1
  12. package/dist/bundled/components/app/Breadcrumb.vue2.js +9 -9
  13. package/dist/bundled/components/app/HomeCounter.vue2.js +5 -5
  14. package/dist/bundled/components/app/InitialLoading.vue2.js +5 -5
  15. package/dist/bundled/components/app/MobileOverlay.vue2.js +5 -5
  16. package/dist/bundled/components/app/TopBar.vue.js +5 -0
  17. package/dist/bundled/components/app/TopBar.vue2.js +73 -0
  18. package/dist/bundled/components/app/TopBarChip.vue.js +13 -0
  19. package/dist/bundled/components/app/TopBarChipItem.vue.js +13 -0
  20. package/dist/bundled/components/base/BaseButton.vue2.js +3 -3
  21. package/dist/bundled/components/base/BaseCheckbox.vue2.js +8 -8
  22. package/dist/bundled/components/base/BaseEmpty.vue.js +2 -2
  23. package/dist/bundled/components/base/BaseKeyboardShortcut.vue2.js +1 -1
  24. package/dist/bundled/components/base/BaseListItem.vue2.js +2 -2
  25. package/dist/bundled/components/base/BaseListItemLink.vue.js +2 -2
  26. package/dist/bundled/components/base/BaseOverflowMenu.vue2.js +4 -4
  27. package/dist/bundled/components/base/BaseOverflowTab.vue.js +3 -3
  28. package/dist/bundled/components/base/BaseSelect.vue2.js +6 -6
  29. package/dist/bundled/components/base/BaseSplitPane.vue.js +1 -1
  30. package/dist/bundled/components/base/BaseSplitPane.vue2.js +20 -16
  31. package/dist/bundled/components/base/BaseTab.vue.js +3 -3
  32. package/dist/bundled/components/base/BaseTag.vue.js +1 -1
  33. package/dist/bundled/components/command/CommandPrompts.vue2.js +5 -5
  34. package/dist/bundled/components/command/CommandPromptsModal.vue2.js +3 -3
  35. package/dist/bundled/components/command/PromptSelect.vue2.js +12 -12
  36. package/dist/bundled/components/command/PromptText.vue2.js +4 -4
  37. package/dist/bundled/components/layout/LayoutItem.vue.js +5 -0
  38. package/dist/bundled/components/layout/LayoutItem.vue2.js +44 -0
  39. package/dist/bundled/components/layout/LayoutItemGroup.vue.js +5 -0
  40. package/dist/bundled/components/layout/LayoutItemGroup.vue2.js +25 -0
  41. package/dist/bundled/components/layout/LayoutModal.vue.js +5 -0
  42. package/dist/bundled/components/layout/LayoutModal.vue2.js +68 -0
  43. package/dist/bundled/components/layout/LayoutPane.vue.js +5 -0
  44. package/dist/bundled/components/layout/LayoutPane.vue2.js +59 -0
  45. package/dist/bundled/components/panel/ControlsComponentPropItem.vue2.js +2 -2
  46. package/dist/bundled/components/panel/ControlsComponentProps.vue2.js +4 -4
  47. package/dist/bundled/components/panel/ControlsComponentState.vue2.js +2 -2
  48. package/dist/bundled/components/panel/PaneTabs.vue2.js +4 -4
  49. package/dist/bundled/components/panel/StatePresets.vue2.js +13 -13
  50. package/dist/bundled/components/panel/StoryControls.vue2.js +6 -6
  51. package/dist/bundled/components/panel/StoryDocs.vue2.js +4 -4
  52. package/dist/bundled/components/panel/StoryEvent.vue2.js +7 -7
  53. package/dist/bundled/components/panel/StoryEvents.vue2.js +1 -1
  54. package/dist/bundled/components/panel/StorySidePanel.vue2.js +58 -47
  55. package/dist/bundled/components/panel/StorySourceCode.vue.js +1 -1
  56. package/dist/bundled/components/panel/StorySourceCode.vue2.js +19 -19
  57. package/dist/bundled/components/search/SearchItem.vue.js +1 -1
  58. package/dist/bundled/components/search/SearchItem.vue2.js +3 -3
  59. package/dist/bundled/components/search/SearchItemContent.vue2.js +9 -9
  60. package/dist/bundled/components/search/SearchLoading.vue2.js +2 -2
  61. package/dist/bundled/components/search/SearchModal.vue2.js +3 -3
  62. package/dist/bundled/components/search/SearchPane.vue2.js +4 -4
  63. package/dist/bundled/components/story/StoryResponsivePreview.vue.js +1 -1
  64. package/dist/bundled/components/story/StoryResponsivePreview.vue2.js +21 -21
  65. package/dist/bundled/components/story/StoryVariantGrid.vue2.js +7 -26
  66. package/dist/bundled/components/story/StoryVariantGridItem.vue.js +1 -1
  67. package/dist/bundled/components/story/StoryVariantGridItem.vue2.js +36 -21
  68. package/dist/bundled/components/story/StoryVariantListItem.vue.js +1 -1
  69. package/dist/bundled/components/story/StoryVariantListItem.vue2.js +5 -5
  70. package/dist/bundled/components/story/StoryVariantSingle.vue.js +1 -1
  71. package/dist/bundled/components/story/StoryVariantSingle.vue2.js +10 -10
  72. package/dist/bundled/components/story/StoryVariantSinglePreviewNative.vue2.js +6 -5
  73. package/dist/bundled/components/story/StoryVariantSinglePreviewRemote.vue2.js +33 -8
  74. package/dist/bundled/components/story/StoryVariantSingleView.vue2.js +8 -39
  75. package/dist/bundled/components/story/StoryView.vue2.js +21 -11
  76. package/dist/bundled/components/story/StoryViewer.vue.js +1 -1
  77. package/dist/bundled/components/story/StoryViewer.vue2.js +2 -2
  78. package/dist/bundled/components/toolbar/DevOnlyToolbarOpenInEditor.vue2.js +13 -9
  79. package/dist/bundled/components/toolbar/ToolbarBackground.vue.js +1 -1
  80. package/dist/bundled/components/toolbar/ToolbarBackground.vue2.js +62 -33
  81. package/dist/bundled/components/toolbar/ToolbarNewTab.vue2.js +13 -10
  82. package/dist/bundled/components/toolbar/ToolbarResponsiveSize.vue2.js +14 -14
  83. package/dist/bundled/components/toolbar/ToolbarTextDirection.vue2.js +13 -9
  84. package/dist/bundled/components/toolbar/ToolbarTitle.vue.js +4 -1
  85. package/dist/bundled/components/toolbar/ToolbarTitle.vue2.js +30 -7
  86. package/dist/bundled/components/tree/StoryGroup.vue2.js +5 -5
  87. package/dist/bundled/components/tree/StoryList.vue2.js +1 -1
  88. package/dist/bundled/components/tree/StoryListFolder.vue.js +1 -1
  89. package/dist/bundled/components/tree/StoryListFolder.vue2.js +7 -7
  90. package/dist/bundled/components/tree/StoryListItem.vue.js +1 -1
  91. package/dist/bundled/components/tree/StoryListItem.vue2.js +8 -8
  92. package/dist/bundled/index.js +3 -0
  93. package/dist/bundled/sandbox.js +41 -15
  94. package/dist/bundled/stores/layout.js +16 -0
  95. package/dist/bundled/stores/preview-settings.js +19 -4
  96. package/dist/bundled/util/color-scheme.js +27 -0
  97. package/dist/bundled/util/const.js +2 -0
  98. package/dist/bundled/util/dark.js +3 -3
  99. package/dist/bundled/util/keyboard.js +60 -32
  100. package/dist/bundled/util/preview-settings.js +11 -0
  101. package/dist/bundled/util/tooltip.js +2 -2
  102. package/dist/style.css +644 -529
  103. package/package.json +8 -8
  104. package/src/app/App.vue +109 -66
  105. package/src/app/components/HomeView.vue +3 -3
  106. package/src/app/components/app/AppActions.vue +58 -0
  107. package/src/app/components/app/AppHeader.vue +4 -47
  108. package/src/app/components/app/Breadcrumb.vue +8 -8
  109. package/src/app/components/app/HomeCounter.vue +5 -5
  110. package/src/app/components/app/InitialLoading.vue +5 -5
  111. package/src/app/components/app/MobileOverlay.vue +5 -5
  112. package/src/app/components/app/TopBar.vue +60 -0
  113. package/src/app/components/app/TopBarChip.vue +5 -0
  114. package/src/app/components/app/TopBarChipItem.vue +7 -0
  115. package/src/app/components/base/BaseButton.vue +3 -3
  116. package/src/app/components/base/BaseCheckbox.vue +9 -9
  117. package/src/app/components/base/BaseCopyIcon.vue +1 -1
  118. package/src/app/components/base/BaseEmpty.vue +4 -4
  119. package/src/app/components/base/BaseKeyboardShortcut.vue +1 -1
  120. package/src/app/components/base/BaseListItem.vue +3 -3
  121. package/src/app/components/base/BaseListItemLink.vue +3 -3
  122. package/src/app/components/base/BaseOverflowMenu.vue +4 -4
  123. package/src/app/components/base/BaseOverflowTab.vue +3 -3
  124. package/src/app/components/base/BaseSelect.vue +6 -6
  125. package/src/app/components/base/BaseSplitPane.vue +21 -16
  126. package/src/app/components/base/BaseTab.vue +3 -3
  127. package/src/app/components/base/BaseTag.vue +1 -1
  128. package/src/app/components/command/CommandPrompts.vue +5 -5
  129. package/src/app/components/command/CommandPromptsModal.vue +3 -3
  130. package/src/app/components/command/PromptSelect.vue +14 -14
  131. package/src/app/components/command/PromptText.vue +4 -4
  132. package/src/app/components/layout/LayoutItem.vue +36 -0
  133. package/src/app/components/layout/LayoutItemGroup.vue +18 -0
  134. package/src/app/components/layout/LayoutModal.vue +60 -0
  135. package/src/app/components/layout/LayoutPane.vue +45 -0
  136. package/src/app/components/panel/ControlsComponentPropItem.vue +2 -2
  137. package/src/app/components/panel/ControlsComponentProps.vue +3 -3
  138. package/src/app/components/panel/ControlsComponentState.vue +2 -2
  139. package/src/app/components/panel/PaneTabs.vue +4 -4
  140. package/src/app/components/panel/StatePresets.vue +13 -13
  141. package/src/app/components/panel/StoryControls.vue +6 -6
  142. package/src/app/components/panel/StoryDocs.vue +4 -4
  143. package/src/app/components/panel/StoryEvent.vue +7 -7
  144. package/src/app/components/panel/StoryEvents.vue +1 -1
  145. package/src/app/components/panel/StorySidePanel.vue +51 -41
  146. package/src/app/components/panel/StorySourceCode.vue +19 -19
  147. package/src/app/components/search/SearchItem.vue +2 -2
  148. package/src/app/components/search/SearchItemContent.vue +9 -9
  149. package/src/app/components/search/SearchLoading.vue +2 -2
  150. package/src/app/components/search/SearchModal.vue +3 -3
  151. package/src/app/components/search/SearchPane.vue +4 -4
  152. package/src/app/components/story/StoryResponsivePreview.vue +27 -27
  153. package/src/app/components/story/StoryVariantGrid.vue +5 -24
  154. package/src/app/components/story/StoryVariantGridItem.vue +36 -17
  155. package/src/app/components/story/StoryVariantListItem.vue +4 -4
  156. package/src/app/components/story/StoryVariantSingle.vue +9 -9
  157. package/src/app/components/story/StoryVariantSinglePreviewNative.vue +7 -5
  158. package/src/app/components/story/StoryVariantSinglePreviewRemote.vue +37 -7
  159. package/src/app/components/story/StoryVariantSingleView.vue +1 -34
  160. package/src/app/components/story/StoryView.vue +23 -8
  161. package/src/app/components/story/StoryViewer.vue +1 -1
  162. package/src/app/components/toolbar/DevOnlyToolbarOpenInEditor.vue +5 -4
  163. package/src/app/components/toolbar/ToolbarBackground.vue +62 -25
  164. package/src/app/components/toolbar/ToolbarNewTab.vue +5 -4
  165. package/src/app/components/toolbar/ToolbarResponsiveSize.vue +15 -15
  166. package/src/app/components/toolbar/ToolbarTextDirection.vue +5 -4
  167. package/src/app/components/toolbar/ToolbarTitle.vue +32 -8
  168. package/src/app/components/tree/StoryGroup.vue +5 -5
  169. package/src/app/components/tree/StoryList.vue +1 -1
  170. package/src/app/components/tree/StoryListFolder.vue +6 -6
  171. package/src/app/components/tree/StoryListItem.vue +6 -6
  172. package/src/app/index.ts +3 -0
  173. package/src/app/sandbox.ts +59 -15
  174. package/src/app/stores/layout.ts +22 -0
  175. package/src/app/stores/preview-settings.ts +26 -2
  176. package/src/app/style/floating.pcss +4 -4
  177. package/src/app/style/main.pcss +2 -2
  178. package/src/app/style/sandbox.css +1 -1
  179. package/src/app/types.ts +49 -0
  180. package/src/app/util/color-scheme.ts +36 -0
  181. package/src/app/util/const.ts +1 -0
  182. package/src/app/util/dark.ts +3 -3
  183. package/src/app/util/keyboard.ts +65 -32
  184. package/src/app/util/preview-settings.ts +16 -0
  185. package/src/app/util/tooltip.ts +2 -2
  186. package/src/bundle-main.js +2 -0
  187. package/src/shim.d.ts +2 -2
@@ -1,5 +1,6 @@
1
1
  import type { StoryFile } from './types'
2
2
  import { applyState } from '@poveste/shared'
3
+ import { usePreferredDark } from '@vueuse/core'
3
4
  import { createPinia } from 'pinia'
4
5
  import { files } from 'virtual:$poveste-stories'
5
6
  import { computed, createApp, h, onMounted, ref, watch } from 'vue'
@@ -7,16 +8,44 @@ import { parseQuery } from 'vue-router'
7
8
  import GenericMountStory from './components/story/GenericMountStory.vue'
8
9
  import GenericRenderStory from './components/story/GenericRenderStory.vue'
9
10
  import { setupPluginApi } from './plugin.js'
11
+ import { resolvePreviewDark } from './util/color-scheme.js'
10
12
  import { povesteConfig } from './util/config.js'
11
- import { PREVIEW_SETTINGS_SYNC, SANDBOX_READY, STATE_SYNC } from './util/const.js'
13
+ import { PREVIEW_SETTINGS_SYNC, SANDBOX_HEIGHT, SANDBOX_READY, STATE_SYNC } from './util/const.js'
12
14
  import { isDark } from './util/dark.js'
13
15
  import { mapFile } from './util/mapping'
14
- import { applyPreviewSettings } from './util/preview-settings.js'
16
+ import { applyPreviewSettings, loadStoredPreviewSettings, receivedSettings } from './util/preview-settings.js'
15
17
  import { toRawDeep } from './util/state.js'
16
18
 
17
19
  const query = parseQuery(window.location.search)
18
20
  const file = ref<StoryFile>(mapFile(files.find(f => f.id === query.storyId)))
19
21
 
22
+ // Sandboxes opened on their own (the "open in a new tab" toolbar button) never
23
+ // receive a PREVIEW_SETTINGS_SYNC, and inside the app the first one only lands
24
+ // after the iframe is already visible. Both read the same storage the app
25
+ // writes to, so seed from it and let the sync take over from there.
26
+ const storedSettings = loadStoredPreviewSettings()
27
+ if (storedSettings) {
28
+ applyPreviewSettings(storedSettings)
29
+ }
30
+
31
+ // The story preview has its own color scheme, independent from the app chrome.
32
+ // The fallback covers settings stored before the option existed.
33
+ const prefersDark = usePreferredDark()
34
+ const previewDark = computed(() => resolvePreviewDark(receivedSettings.colorScheme, prefersDark.value, isDark.value))
35
+
36
+ watch(previewDark, (value) => {
37
+ if (value) {
38
+ document.documentElement.classList.add(povesteConfig.sandboxDarkClass) // @TODO remove
39
+ document.documentElement.classList.add(povesteConfig.theme.darkClass)
40
+ }
41
+ else {
42
+ document.documentElement.classList.remove(povesteConfig.sandboxDarkClass) // @TODO remove
43
+ document.documentElement.classList.remove(povesteConfig.theme.darkClass)
44
+ }
45
+ }, {
46
+ immediate: true,
47
+ })
48
+
20
49
  const app = createApp({
21
50
  name: 'SandboxApp',
22
51
 
@@ -64,7 +93,7 @@ const app = createApp({
64
93
 
65
94
  render() {
66
95
  return [
67
- h('div', { class: 'htw-sandbox-hidden' }, [
96
+ h('div', { class: 'ptw-sandbox-hidden' }, [
68
97
  h(GenericMountStory, {
69
98
  key: file.value.story.id,
70
99
  story: file.value.story,
@@ -83,18 +112,33 @@ const app = createApp({
83
112
  app.use(createPinia())
84
113
  app.mount('#app')
85
114
 
86
- watch(isDark, (value) => {
87
- if (value) {
88
- document.documentElement.classList.add(povesteConfig.sandboxDarkClass) // @TODO remove
89
- document.documentElement.classList.add(povesteConfig.theme.darkClass)
90
- }
91
- else {
92
- document.documentElement.classList.remove(povesteConfig.sandboxDarkClass) // @TODO remove
93
- document.documentElement.classList.remove(povesteConfig.theme.darkClass)
94
- }
95
- }, {
96
- immediate: true,
97
- })
115
+ // Tagging body itself as a story render root puts components teleported to
116
+ // document.body (floating-vue popper, dialogs) inside the user-CSS @scope
117
+ // boundary applied in dev. The custom-controls class opts out of poveste-app
118
+ // source-level rules that add overflow/min-height to the render root.
119
+ document.body.classList.add('__poveste-render-story', '__poveste-render-custom-controls')
120
+
121
+ let pendingFrame: number | null = null
122
+ let lastReportedHeight = -1
123
+ function reportHeight() {
124
+ pendingFrame = null
125
+ const renderRoot = document.querySelector('.__poveste-render-story')
126
+ const h = renderRoot
127
+ ? Math.ceil(renderRoot.getBoundingClientRect().height)
128
+ : Math.ceil(document.body.scrollHeight)
129
+ if (h === lastReportedHeight) return
130
+ lastReportedHeight = h
131
+ // Same-origin: parent and iframe are both served by the poveste dev /
132
+ // build server, so scope the target rather than broadcasting.
133
+ window.parent?.postMessage({ type: SANDBOX_HEIGHT, h }, window.location.origin)
134
+ }
135
+ function scheduleReport() {
136
+ if (pendingFrame !== null) return
137
+ pendingFrame = requestAnimationFrame(reportHeight)
138
+ }
139
+ const ro = new ResizeObserver(scheduleReport)
140
+ ro.observe(document.body)
141
+ requestAnimationFrame(scheduleReport)
98
142
 
99
143
  if (import.meta.hot) {
100
144
  /* #__PURE__ */ setupPluginApi()
@@ -0,0 +1,22 @@
1
+ import { useStorage } from '@vueuse/core'
2
+ import { defineStore } from 'pinia'
3
+
4
+ export type StoryOptionsPlacement = 'right' | 'bottom'
5
+
6
+ export interface LayoutSettings {
7
+ storyListVisible: boolean
8
+ storyOptionsVisible: boolean
9
+ storyOptionsPlacement: StoryOptionsPlacement
10
+ }
11
+
12
+ export const useLayoutStore = defineStore('layout', () => {
13
+ const settings = useStorage<LayoutSettings>('_poveste-layout-v1', {
14
+ storyListVisible: true,
15
+ storyOptionsVisible: true,
16
+ storyOptionsPlacement: 'right',
17
+ })
18
+
19
+ return {
20
+ settings,
21
+ }
22
+ })
@@ -1,18 +1,42 @@
1
1
  import type { PreviewSettings } from '../types'
2
2
  import { useStorage } from '@vueuse/core'
3
3
  import { defineStore } from 'pinia'
4
+ import { defaultPreviewColorScheme } from '../util/color-scheme'
5
+ import { povesteConfig } from '../util/config'
6
+ import { PREVIEW_SETTINGS_STORAGE_KEY } from '../util/preview-settings'
4
7
 
5
8
  export const usePreviewSettingsStore = defineStore('preview-settings', () => {
6
- const currentSettings = useStorage<PreviewSettings>('_poveste-sandbox-settings-v3', {
9
+ const currentSettings = useStorage<PreviewSettings>(PREVIEW_SETTINGS_STORAGE_KEY, {
7
10
  responsiveWidth: 720,
8
11
  responsiveHeight: null,
9
12
  rotate: false,
10
- backgroundColor: 'transparent',
13
+ backgroundColor: povesteConfig.defaultBackgroundColor ?? 'transparent',
14
+ backgroundColorPicked: false,
11
15
  checkerboard: false,
12
16
  textDirection: 'ltr',
17
+ colorScheme: defaultPreviewColorScheme,
13
18
  })
14
19
 
20
+ // useStorage honors its default only on first run, so already-stored settings
21
+ // would never see `defaultBackgroundColor` (nor a later change to it). Re-apply
22
+ // it on every load until the user picks a color from the toolbar; the flag lives
23
+ // in the settings object so it travels with the value it describes.
24
+ const configBg = povesteConfig.defaultBackgroundColor
25
+ if (configBg && !currentSettings.value.backgroundColorPicked) {
26
+ currentSettings.value.backgroundColor = configBg
27
+ }
28
+
29
+ // Same reason: settings stored before `colorScheme` existed would leave it
30
+ // undefined, so the toolbar would show no active option.
31
+ currentSettings.value.colorScheme ??= defaultPreviewColorScheme
32
+
33
+ function setBackgroundColor(color: string) {
34
+ currentSettings.value.backgroundColor = color
35
+ currentSettings.value.backgroundColorPicked = true
36
+ }
37
+
15
38
  return {
16
39
  currentSettings,
40
+ setBackgroundColor,
17
41
  }
18
42
  })
@@ -5,17 +5,17 @@
5
5
  }
6
6
 
7
7
  .v-popper--theme-dropdown {
8
- .htw-dark & {
8
+ .ptw-dark & {
9
9
  .v-popper__inner {
10
- @apply htw-bg-gray-700 htw-border-gray-850 htw-text-gray-100;
10
+ @apply ptw-bg-gray-700 ptw-border-gray-850 ptw-text-gray-100;
11
11
  }
12
12
 
13
13
  .v-popper__arrow-inner {
14
- @apply htw-border-gray-700;
14
+ @apply ptw-border-gray-700;
15
15
  }
16
16
 
17
17
  .v-popper__arrow-outer {
18
- @apply htw-border-gray-850;
18
+ @apply ptw-border-gray-850;
19
19
  }
20
20
  }
21
21
 
@@ -73,7 +73,7 @@ button {
73
73
  background: transparent !important;
74
74
  }
75
75
 
76
- .htw-prose {
76
+ .ptw-prose {
77
77
  h1, h2, h3, h4 {
78
78
  &:not(:hover) {
79
79
  .header-anchor[aria-hidden="true"] {
@@ -93,7 +93,7 @@ button {
93
93
  background-repeat: no-repeat;
94
94
  background-position: right;
95
95
 
96
- .htw-dark & {
96
+ .ptw-dark & {
97
97
  background-image: linear-gradient(to left, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0));
98
98
  }
99
99
  }
@@ -14,7 +14,7 @@ html {
14
14
  font-family: 'Noto Sans Display', system-ui, sans-serif;
15
15
  }
16
16
 
17
- .htw-sandbox-hidden {
17
+ .ptw-sandbox-hidden {
18
18
  display: none;
19
19
  }
20
20
 
package/src/app/types.ts CHANGED
@@ -45,13 +45,62 @@ export type SearchResult = SearchResultBase & ({
45
45
  onActivate: () => unknown
46
46
  })
47
47
 
48
+ export type SandboxColorScheme = 'auto' | 'light' | 'dark'
49
+
48
50
  export interface PreviewSettings {
51
+ /**
52
+ * Width of the responsive preview, in pixels. `null` sizes it to the
53
+ * available space.
54
+ *
55
+ * @default 720
56
+ */
49
57
  responsiveWidth: number
58
+ /**
59
+ * Height of the responsive preview, in pixels. `null` sizes it to the
60
+ * available space.
61
+ *
62
+ * @default null
63
+ */
50
64
  responsiveHeight: number
65
+ /**
66
+ * Swap `responsiveWidth` and `responsiveHeight`, to preview a size in
67
+ * landscape without editing both values.
68
+ *
69
+ * @default false
70
+ */
51
71
  rotate: boolean
72
+ /**
73
+ * Background color of the story preview. Any CSS color, not only the ones
74
+ * from `backgroundPresets`.
75
+ *
76
+ * @default 'transparent'
77
+ */
52
78
  backgroundColor: string
79
+ /**
80
+ * Whether `backgroundColor` came from an explicit toolbar pick rather than
81
+ * from the `defaultBackgroundColor` config option.
82
+ *
83
+ * @default false
84
+ */
85
+ backgroundColorPicked: boolean
86
+ /**
87
+ * Show a checkerboard pattern behind the story, to spot transparency.
88
+ *
89
+ * @default false
90
+ */
53
91
  checkerboard: boolean
92
+ /**
93
+ * `dir` attribute set on the story preview.
94
+ *
95
+ * @default 'ltr'
96
+ */
54
97
  textDirection: 'ltr' | 'rtl'
98
+ /**
99
+ * Color scheme of the story preview, independent from the app chrome.
100
+ *
101
+ * @default 'auto'
102
+ */
103
+ colorScheme: SandboxColorScheme
55
104
  }
56
105
 
57
106
  declare module 'vue' {
@@ -0,0 +1,36 @@
1
+ import type { PreviewSettings, SandboxColorScheme } from '../types'
2
+ import { usePreferredDark } from '@vueuse/core'
3
+ import { computed } from 'vue'
4
+ import { povesteConfig } from './config'
5
+ import { isDark } from './dark'
6
+
7
+ /**
8
+ * Color scheme the story preview starts on, before the user picks one in the
9
+ * toolbar. Shared with the app chrome so a book configured as dark starts dark
10
+ * on both sides.
11
+ */
12
+ export const defaultPreviewColorScheme: SandboxColorScheme = povesteConfig.theme.defaultColorScheme ?? 'auto'
13
+
14
+ /**
15
+ * The story preview has its own color scheme, independent from the app chrome.
16
+ * `chromeDark` only covers settings that predate the option (and the sandbox
17
+ * boot window), where we keep the old behavior of following the chrome.
18
+ */
19
+ export function resolvePreviewDark(colorScheme: SandboxColorScheme | undefined, prefersDark: boolean, chromeDark: boolean) {
20
+ switch (colorScheme) {
21
+ case 'light': return false
22
+ case 'dark': return true
23
+ case 'auto': return prefersDark
24
+ default: return chromeDark
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Whether the story preview should be rendered dark, for previews rendered by
30
+ * the app itself. The sandbox resolves the same thing from the settings it
31
+ * receives over `PREVIEW_SETTINGS_SYNC`.
32
+ */
33
+ export function usePreviewDark(settings: PreviewSettings) {
34
+ const prefersDark = usePreferredDark()
35
+ return computed(() => resolvePreviewDark(settings.colorScheme, prefersDark.value, isDark.value))
36
+ }
@@ -2,3 +2,4 @@ export const STATE_SYNC = '__poveste:state-sync'
2
2
  export const SANDBOX_READY = '__poveste:sandbox-ready'
3
3
  export const EVENT_SEND = '__poveste:event'
4
4
  export const PREVIEW_SETTINGS_SYNC = '__poveste:preview-settings-sync'
5
+ export const SANDBOX_HEIGHT = '__poveste:sandbox-height'
@@ -3,7 +3,7 @@ import { watch } from 'vue'
3
3
  import { povesteConfig } from './config.js'
4
4
 
5
5
  export const isDark = useDark({
6
- valueDark: 'htw-dark',
6
+ valueDark: 'ptw-dark',
7
7
  initialValue: povesteConfig.theme.defaultColorScheme,
8
8
  storageKey: 'poveste-color-scheme',
9
9
  storage: povesteConfig.theme.storeColorScheme ? localStorage : sessionStorage,
@@ -11,7 +11,7 @@ export const isDark = useDark({
11
11
  export const toggleDark = useToggle(isDark)
12
12
 
13
13
  function applyDarkToControls() {
14
- window.__hst_controls_dark?.forEach((ref) => {
14
+ window.__pvt_controls_dark?.forEach((ref) => {
15
15
  ref.value = isDark.value
16
16
  })
17
17
  }
@@ -22,6 +22,6 @@ watch(isDark, () => {
22
22
  immediate: true,
23
23
  })
24
24
 
25
- window.__hst_controls_dark_ready = () => {
25
+ window.__pvt_controls_dark_ready = () => {
26
26
  applyDarkToControls()
27
27
  }
@@ -1,6 +1,6 @@
1
1
  import type { Ref } from 'vue'
2
2
  import { useEventListener } from '@vueuse/core'
3
- import { isRef } from 'vue'
3
+ import { isRef, shallowRef } from 'vue'
4
4
  import { isMac } from './env.js'
5
5
 
6
6
  export type KeyboardShortcut = string[]
@@ -11,14 +11,6 @@ export interface KeyboardShortcutOptions {
11
11
  event?: 'keyup' | 'keydown' | 'keypress'
12
12
  }
13
13
 
14
- export function onKeyboardShortcut(shortcut: KeyboardShortcut | Ref<KeyboardShortcut>, handler: KeyboardHandler, options: KeyboardShortcutOptions = {}) {
15
- useEventListener(options.event ?? 'keydown', (event) => {
16
- if (isMatchingShortcut(isRef(shortcut) ? shortcut.value : shortcut)) {
17
- handler(event)
18
- }
19
- })
20
- }
21
-
22
14
  const modifiers: { [i: string]: { key: string, pressed: boolean } } = {
23
15
  ctrl: { key: 'Control', pressed: false },
24
16
  alt: { key: 'Alt', pressed: false },
@@ -28,37 +20,78 @@ const modifiers: { [i: string]: { key: string, pressed: boolean } } = {
28
20
 
29
21
  const pressedKeys = new Set<string>()
30
22
 
31
- window.addEventListener('keydown', (event) => {
32
- for (const i in modifiers) {
33
- const mod = modifiers[i]
34
- if (mod.key === event.key) {
35
- mod.pressed = true
36
- return
37
- }
38
- }
39
- pressedKeys.add(event.key.toLocaleLowerCase())
40
- })
23
+ const trackedWindows = shallowRef<Window[]>([])
41
24
 
42
- window.addEventListener('keyup', (event) => {
43
- requestAnimationFrame(() => {
44
- pressedKeys.clear()
25
+ function bindTracking(target: Window) {
26
+ const onKeydown = (event: KeyboardEvent) => {
45
27
  for (const i in modifiers) {
46
28
  const mod = modifiers[i]
47
29
  if (mod.key === event.key) {
48
- mod.pressed = false
49
- break
30
+ mod.pressed = true
31
+ return
50
32
  }
51
33
  }
52
- })
53
- })
34
+ pressedKeys.add(event.key.toLocaleLowerCase())
35
+ }
36
+ const onKeyup = (event: KeyboardEvent) => {
37
+ requestAnimationFrame(() => {
38
+ pressedKeys.clear()
39
+ for (const i in modifiers) {
40
+ const mod = modifiers[i]
41
+ if (mod.key === event.key) {
42
+ mod.pressed = false
43
+ break
44
+ }
45
+ }
46
+ })
47
+ }
48
+ const onBlur = () => {
49
+ pressedKeys.clear()
50
+ for (const i in modifiers) {
51
+ modifiers[i].pressed = false
52
+ }
53
+ }
54
+ target.addEventListener('keydown', onKeydown)
55
+ target.addEventListener('keyup', onKeyup)
56
+ target.addEventListener('blur', onBlur)
57
+ return () => {
58
+ target.removeEventListener('keydown', onKeydown)
59
+ target.removeEventListener('keyup', onKeyup)
60
+ target.removeEventListener('blur', onBlur)
61
+ }
62
+ }
54
63
 
55
- window.addEventListener('blur', () => {
56
- pressedKeys.clear()
57
- for (const i in modifiers) {
58
- const mod = modifiers[i]
59
- mod.pressed = false
64
+ /**
65
+ * Opt an additional window (typically a sandbox iframe `contentWindow`) into
66
+ * shortcut tracking, so keys pressed while it has focus still reach the app.
67
+ */
68
+ export function trackWindow(target: Window): () => void {
69
+ if (trackedWindows.value.includes(target)) {
70
+ return () => {}
71
+ }
72
+ const cleanup = bindTracking(target)
73
+ trackedWindows.value = [...trackedWindows.value, target]
74
+ return () => {
75
+ cleanup()
76
+ trackedWindows.value = trackedWindows.value.filter(w => w !== target)
60
77
  }
61
- })
78
+ }
79
+
80
+ trackWindow(window)
81
+
82
+ export function onKeyboardShortcut(shortcut: KeyboardShortcut | Ref<KeyboardShortcut>, handler: KeyboardHandler, options: KeyboardShortcutOptions = {}) {
83
+ useEventListener(trackedWindows, options.event ?? 'keydown', (event: KeyboardEvent) => {
84
+ // Sync modifier state from the event so a blur-clear (e.g. focusing an
85
+ // iframe while holding a modifier) doesn't drop the shortcut.
86
+ modifiers.ctrl.pressed = event.ctrlKey
87
+ modifiers.alt.pressed = event.altKey
88
+ modifiers.shift.pressed = event.shiftKey
89
+ modifiers.meta.pressed = event.metaKey
90
+ if (isMatchingShortcut(isRef(shortcut) ? shortcut.value : shortcut)) {
91
+ handler(event)
92
+ }
93
+ })
94
+ }
62
95
 
63
96
  function isMatchingShortcut(shortcut: KeyboardShortcut): boolean {
64
97
  for (const combination of shortcut) {
@@ -2,8 +2,24 @@ import type { PreviewSettings } from '../types'
2
2
  import { reactive } from 'vue'
3
3
  import { povesteConfig } from './config'
4
4
 
5
+ export const PREVIEW_SETTINGS_STORAGE_KEY = '_poveste-sandbox-settings-v3'
6
+
5
7
  export const receivedSettings = reactive<PreviewSettings>({} as PreviewSettings)
6
8
 
9
+ /**
10
+ * Read the settings the app persists, for sandboxes that can't wait for (or
11
+ * will never get) a `PREVIEW_SETTINGS_SYNC` message.
12
+ */
13
+ export function loadStoredPreviewSettings(): PreviewSettings | null {
14
+ try {
15
+ const raw = localStorage.getItem(PREVIEW_SETTINGS_STORAGE_KEY)
16
+ return raw ? JSON.parse(raw) : null
17
+ }
18
+ catch {
19
+ return null
20
+ }
21
+ }
22
+
7
23
  export function applyPreviewSettings(settings: PreviewSettings) {
8
24
  Object.assign(receivedSettings, settings)
9
25
 
@@ -3,11 +3,11 @@ import { formatKey } from './keyboard.js'
3
3
 
4
4
  export function makeTooltip(descriptionHtml: string, keyboardShortcut: (options: { isMac: boolean }) => string) {
5
5
  return {
6
- content: `<div>${descriptionHtml}</div><div class="htw-flex htw-items-center htw-gap-1 htw-mt-2 htw-text-sm">${genKeyboardShortcutHtml(keyboardShortcut({ isMac }))}</div>`,
6
+ content: `<div>${descriptionHtml}</div><div class="ptw-flex ptw-items-center ptw-gap-1 ptw-mt-2 ptw-text-sm">${genKeyboardShortcutHtml(keyboardShortcut({ isMac }))}</div>`,
7
7
  html: true,
8
8
  }
9
9
  }
10
10
 
11
11
  function genKeyboardShortcutHtml(shortcut: string) {
12
- return `<span class="htw-border htw-border-gray-600 htw-px-1 htw-rounded-sm htw-text-gray-400">${shortcut.split('+').map(k => formatKey(k.trim())).join(' ')}</span>`
12
+ return `<span class="ptw-border ptw-border-gray-600 ptw-px-1 ptw-rounded-sm ptw-text-gray-400">${shortcut.split('+').map(k => formatKey(k.trim())).join(' ')}</span>`
13
13
  }
@@ -1,3 +1,5 @@
1
+ /* eslint-disable perfectionist/sort-imports -- global styles must load before app bundle */
2
+ import 'virtual:$poveste-global-styles'
1
3
  import { mountMainApp } from './bundled/index.js'
2
4
  import './style.css'
3
5
  import './bundled/app.css'
package/src/shim.d.ts CHANGED
@@ -4,7 +4,7 @@ import type { Ref } from '@poveste/vendors/vue'
4
4
 
5
5
  global {
6
6
  interface Window {
7
- __hst_controls_dark: Ref<boolean>[]
8
- __hst_controls_dark_ready: () => void
7
+ __pvt_controls_dark: Ref<boolean>[]
8
+ __pvt_controls_dark_ready: () => void
9
9
  }
10
10
  }