@poveste/app 0.18.3 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/THIRD_PARTY_NOTICES.md +159 -0
  2. package/dist/bundled/app.css +4 -4
  3. package/dist/bundled/components/command/PromptSelect.vue_vue_type_script_setup_true_lang.js +1 -1
  4. package/dist/bundled/components/story/StoryNativeScope.js +14 -0
  5. package/dist/bundled/components/story/StoryVariantGridItem.js +1 -1
  6. package/dist/bundled/components/story/StoryVariantGridItem.vue_vue_type_script_setup_true_lang.js +23 -16
  7. package/dist/bundled/components/story/StoryVariantSinglePreviewNative.vue_vue_type_script_setup_true_lang.js +22 -15
  8. package/dist/bundled/components/story/StoryVariantSinglePreviewRemote.vue_vue_type_script_setup_true_lang.js +8 -2
  9. package/dist/bundled/index.js +12 -3
  10. package/dist/bundled/router.js +15 -12
  11. package/dist/bundled/sandbox.js +32 -2
  12. package/dist/bundled/stores/preview-settings.js +29 -4
  13. package/dist/bundled/util/color-scheme.js +41 -1
  14. package/dist/bundled/util/commands.js +16 -4
  15. package/dist/bundled/util/const.js +2 -1
  16. package/package.json +6 -4
  17. package/src/app/components/command/PromptSelect.vue +2 -1
  18. package/src/app/components/story/StoryNativeScope.vue +23 -0
  19. package/src/app/components/story/StoryVariantGridItem.vue +14 -10
  20. package/src/app/components/story/StoryVariantSinglePreviewNative.vue +15 -10
  21. package/src/app/components/story/StoryVariantSinglePreviewRemote.vue +13 -2
  22. package/src/app/index.ts +14 -2
  23. package/src/app/router.ts +26 -15
  24. package/src/app/sandbox.ts +41 -2
  25. package/src/app/stores/preview-settings.ts +48 -10
  26. package/src/app/types.ts +7 -1
  27. package/src/app/util/color-scheme.ts +44 -0
  28. package/src/app/util/commands.ts +19 -7
  29. package/src/app/util/const.ts +1 -0
  30. package/src/__tests__/base-select.spec.ts +0 -25
  31. package/src/__tests__/color-scheme.spec.ts +0 -55
  32. package/src/__tests__/font.spec.ts +0 -76
  33. package/src/__tests__/grid-slots.spec.ts +0 -84
  34. package/src/__tests__/icons.spec.ts +0 -26
  35. package/src/__tests__/postcss-scope-wrapper.spec.ts +0 -65
  36. package/src/__tests__/render-tracker.spec.ts +0 -38
  37. package/src/__tests__/reset.spec.ts +0 -53
  38. package/src/__tests__/source-code.spec.ts +0 -55
  39. package/src/__tests__/state-bridge.spec.ts +0 -155
  40. package/src/__tests__/state-clone.spec.ts +0 -120
  41. package/src/__tests__/state-types.spec.ts +0 -175
  42. package/src/__tests__/uncaught.spec.ts +0 -36
@@ -6,5 +6,6 @@ var PREVIEW_SETTINGS_SYNC = "__poveste:preview-settings-sync";
6
6
  var PREVIEW_SETTINGS_REQUEST = "__poveste:preview-settings-request";
7
7
  var SANDBOX_HEIGHT = "__poveste:sandbox-height";
8
8
  var SANDBOX_RETARGET = "__poveste:sandbox-retarget";
9
+ var SANDBOX_COLOR_SCHEME = "__poveste:sandbox-color-scheme";
9
10
  //#endregion
10
- export { EVENT_SEND, PREVIEW_SETTINGS_REQUEST, PREVIEW_SETTINGS_SYNC, SANDBOX_HEIGHT, SANDBOX_READY, SANDBOX_RETARGET, STATE_SYNC };
11
+ export { EVENT_SEND, PREVIEW_SETTINGS_REQUEST, PREVIEW_SETTINGS_SYNC, SANDBOX_COLOR_SCHEME, SANDBOX_HEIGHT, SANDBOX_READY, SANDBOX_RETARGET, STATE_SYNC };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@poveste/app",
3
3
  "type": "module",
4
- "version": "0.18.3",
4
+ "version": "0.19.1",
5
5
  "description": "Pre-bundled UI for Poveste",
6
6
  "author": {
7
7
  "name": "Sorin Gitlan",
@@ -27,6 +27,8 @@
27
27
  "./*": "./*"
28
28
  },
29
29
  "files": [
30
+ "!src/**/*.spec.ts",
31
+ "!src/**/__tests__",
30
32
  "dist",
31
33
  "src"
32
34
  ],
@@ -34,9 +36,9 @@
34
36
  "node": ">=22.22.2"
35
37
  },
36
38
  "dependencies": {
37
- "@poveste/controls": "^0.18.3",
38
- "@poveste/shared": "^0.18.3",
39
- "@poveste/vendors": "^0.18.3",
39
+ "@poveste/controls": "^0.19.1",
40
+ "@poveste/shared": "^0.19.1",
41
+ "@poveste/vendors": "^0.19.1",
40
42
  "fuse.js": "^7.0.0",
41
43
  "shiki": "^4.4.3"
42
44
  },
@@ -46,7 +46,8 @@ watchEffect(async () => {
46
46
  const rId = ++requestId
47
47
  const result = await props.prompt.options(search.value, props.answers)
48
48
  if (rId === requestId) {
49
- options.value = result
49
+ // Undefined when no plugin answered, or the one that should have threw (#1170).
50
+ options.value = result ?? []
50
51
  }
51
52
  }
52
53
  else {
@@ -0,0 +1,23 @@
1
+ <!--
2
+ Around a story the app renders itself, so the dark class on its render root is
3
+ reachable by the book's CSS (#332).
4
+
5
+ A book's CSS is wrapped in `@scope (.__poveste-render-story)`, where a rule
6
+ shaped `.dark .foo` needs `.dark` strictly inside a scope root: on the root
7
+ itself it matches nothing. This is a second root one level out, which makes the
8
+ render root an element inside a scope. The sandbox has the same two levels,
9
+ `body` and `#app` (#101).
10
+
11
+ No box, so nothing is laid out differently, and `!important` because a book's
12
+ `:root { display }` rule lands on every scope root. The custom-controls class
13
+ keeps this from being the boundary the chrome's own CSS stops at, which stays
14
+ the render root, as `body` does in the sandbox.
15
+ -->
16
+ <template>
17
+ <div
18
+ class="__poveste-render-story __poveste-render-custom-controls"
19
+ style="display: contents !important"
20
+ >
21
+ <slot />
22
+ </div>
23
+ </template>
@@ -8,7 +8,7 @@ import { computed, nextTick, ref, toRefs, watch } from 'vue'
8
8
  import { useRouter } from 'vue-router'
9
9
  import { usePreviewSettingsStore } from '../../stores/preview-settings'
10
10
  import { useStoryStore } from '../../stores/story'
11
- import { previewDarkClasses, usePreviewDark } from '../../util/color-scheme'
11
+ import { previewDarkClasses, usePreviewColorScheme, usePreviewDark } from '../../util/color-scheme'
12
12
  import { povesteConfig } from '../../util/config'
13
13
  import { getSourceCode } from '../../util/docs'
14
14
  import { getContrastColor } from '../../util/preview-settings'
@@ -17,6 +17,7 @@ import { useCurrentVariantRoute } from '../../util/variant'
17
17
  import CheckerboardPattern from '../misc/CheckerboardPattern.vue'
18
18
  import ToolbarNewTab from '../toolbar/ToolbarNewTab.vue'
19
19
  import GenericRenderStory from './GenericRenderStory.vue'
20
+ import StoryNativeScope from './StoryNativeScope.vue'
20
21
  import StoryVariantSinglePreviewRemote from './StoryVariantSinglePreviewRemote.vue'
21
22
 
22
23
  const props = defineProps({
@@ -91,6 +92,7 @@ watch(() => props.variant.previewReady, (ready) => {
91
92
  const settings = usePreviewSettingsStore().currentSettings
92
93
 
93
94
  const previewDark = usePreviewDark(settings)
95
+ const previewColorScheme = usePreviewColorScheme(settings)
94
96
 
95
97
  const contrastColor = computed(() => getContrastColor(settings))
96
98
  const autoApplyContrastColor = computed(() => !!povesteConfig.autoApplyContrastColor)
@@ -183,15 +185,17 @@ const useIframe = computed(() => {
183
185
  auto-height
184
186
  :responsive="false"
185
187
  />
186
- <GenericRenderStory
187
- v-else
188
- :key="`${story.id}-${variant.id}`"
189
- :variant="variant"
190
- :story="story"
191
- :dir="settings.textDirection"
192
- :class="previewDark ? previewDarkClasses() : undefined"
193
- @ready="onReady"
194
- />
188
+ <StoryNativeScope v-else>
189
+ <GenericRenderStory
190
+ :key="`${story.id}-${variant.id}`"
191
+ :variant="variant"
192
+ :story="story"
193
+ :dir="settings.textDirection"
194
+ :class="previewDark ? previewDarkClasses() : undefined"
195
+ :style="{ '--poveste-color-scheme': previewColorScheme }"
196
+ @ready="onReady"
197
+ />
198
+ </StoryNativeScope>
195
199
  </div>
196
200
  </div>
197
201
  </div>
@@ -3,10 +3,11 @@ import type { Story, Variant } from '../../types'
3
3
  import { computed, watch } from 'vue'
4
4
  import { usePreviewSettingsStore } from '../../stores/preview-settings'
5
5
  import { useStoryStore } from '../../stores/story'
6
- import { previewDarkClasses, usePreviewDark } from '../../util/color-scheme'
6
+ import { previewDarkClasses, usePreviewColorScheme, usePreviewDark } from '../../util/color-scheme'
7
7
  import { povesteConfig } from '../../util/config'
8
8
  import { getContrastColor } from '../../util/preview-settings'
9
9
  import GenericRenderStory from './GenericRenderStory.vue'
10
+ import StoryNativeScope from './StoryNativeScope.vue'
10
11
  import StoryResponsivePreview from './StoryResponsivePreview.vue'
11
12
 
12
13
  const props = defineProps<{
@@ -36,6 +37,7 @@ function onReady() {
36
37
  const settings = usePreviewSettingsStore().currentSettings
37
38
 
38
39
  const previewDark = usePreviewDark(settings)
40
+ const previewColorScheme = usePreviewColorScheme(settings)
39
41
 
40
42
  const contrastColor = computed(() => getContrastColor(settings))
41
43
  const autoApplyContrastColor = computed(() => !!povesteConfig.autoApplyContrastColor)
@@ -63,15 +65,18 @@ const autoApplyContrastColor = computed(() => !!povesteConfig.autoApplyContrastC
63
65
  class="relative"
64
66
  data-testid="sandbox-render"
65
67
  >
66
- <GenericRenderStory
67
- :key="`${story.id}-${variant.id}`"
68
- :variant="variant"
69
- :story="story"
70
- class="h-full"
71
- :class="previewDark ? previewDarkClasses() : undefined"
72
- :dir="settings.textDirection"
73
- @ready="onReady"
74
- />
68
+ <StoryNativeScope>
69
+ <GenericRenderStory
70
+ :key="`${story.id}-${variant.id}`"
71
+ :variant="variant"
72
+ :story="story"
73
+ class="h-full"
74
+ :class="previewDark ? previewDarkClasses() : undefined"
75
+ :style="{ '--poveste-color-scheme': previewColorScheme }"
76
+ :dir="settings.textDirection"
77
+ @ready="onReady"
78
+ />
79
+ </StoryNativeScope>
75
80
  </div>
76
81
  </StoryResponsivePreview>
77
82
  </template>
@@ -9,7 +9,8 @@ import { useEventsStore } from '../../stores/events'
9
9
  import { usePreviewSettingsStore } from '../../stores/preview-settings'
10
10
  import { useStoryStore } from '../../stores/story'
11
11
  import { useStoryErrorStore } from '../../stores/story-errors'
12
- import { EVENT_SEND, PREVIEW_SETTINGS_REQUEST, PREVIEW_SETTINGS_SYNC, SANDBOX_HEIGHT, SANDBOX_READY, SANDBOX_RETARGET, STATE_SYNC } from '../../util/const'
12
+ import { usePreviewColorScheme } from '../../util/color-scheme'
13
+ import { EVENT_SEND, PREVIEW_SETTINGS_REQUEST, PREVIEW_SETTINGS_SYNC, SANDBOX_COLOR_SCHEME, SANDBOX_HEIGHT, SANDBOX_READY, SANDBOX_RETARGET, STATE_SYNC } from '../../util/const'
13
14
  import { firstReportedHeight } from '../../util/grid-cell-height'
14
15
  import { trackWindow } from '../../util/keyboard'
15
16
  import { createRenderTracker } from '../../util/render-tracker'
@@ -28,6 +29,10 @@ const props = withDefaults(defineProps<{
28
29
  })
29
30
 
30
31
  const settings = usePreviewSettingsStore().currentSettings
32
+ const previewColorScheme = usePreviewColorScheme(settings)
33
+ // What the sandbox root computed, which a book's own rule can make differ from
34
+ // the setting. Until its document reports, the setting is the best guess.
35
+ const sandboxColorScheme = ref<string | null>(null)
31
36
  const errorStore = useStoryErrorStore()
32
37
 
33
38
  const storyError = computed(() => errorStore.forVariant(props.story.id, props.variant.id))
@@ -133,6 +138,9 @@ useEventListener(window, 'message', (event) => {
133
138
  case PREVIEW_SETTINGS_REQUEST:
134
139
  syncSettings()
135
140
  break
141
+ case SANDBOX_COLOR_SCHEME:
142
+ if (typeof event.data.scheme === 'string') sandboxColorScheme.value = event.data.scheme
143
+ break
136
144
  case SANDBOX_HEIGHT:
137
145
  if (!fromCurrentOccupant(event.data)) break
138
146
  // The sandbox reports on its first frames, before the story mounts, and
@@ -288,7 +296,10 @@ watch(() => settings, () => {
288
296
  // unreadable. A stored `responsiveHeight` of 600 gives 729px cells holding one
289
297
  // small button, which is the shape #198 was filed about.
290
298
  function previewStyle(isResponsiveEnabled: boolean, finalWidth: number | null, finalHeight: number | null) {
291
- const style: Record<string, string> = {}
299
+ // The sandbox root's own value, on the element that embeds it. Where the two
300
+ // differ the browser paints the frame opaque, and the background preset and
301
+ // checkerboard behind it are gone (#1167).
302
+ const style: Record<string, string> = { colorScheme: sandboxColorScheme.value ?? previewColorScheme.value }
292
303
 
293
304
  if (isResponsiveEnabled) {
294
305
  if (finalWidth) style['width'] = `${finalWidth}px`
package/src/app/index.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { addCollection } from '@iconify/vue'
2
2
  import { createPinia } from 'pinia'
3
3
  import { generation } from 'virtual:$poveste-stories'
4
- import { createApp } from 'vue'
4
+ import { createApp, watch } from 'vue'
5
5
  import App from './App.vue'
6
6
  import { setupPluginApi } from './plugin.js'
7
7
  import { router } from './router'
8
+ import { installRenderRootColorScheme } from './util/color-scheme.js'
9
+ import { isDark } from './util/dark.js'
8
10
  import { iconCollections } from './util/icons.generated.js'
9
11
  import 'virtual:$poveste-theme'
10
12
 
@@ -17,6 +19,14 @@ export async function mountMainApp() {
17
19
  addCollection(collection)
18
20
  }
19
21
 
22
+ // The chrome's own scheme, for the browser: it paints the chrome's native
23
+ // controls and scrollbars, and reads no class (#991). Here rather than in the
24
+ // chrome stylesheet, which style isolation rewrites from `html` to the app root.
25
+ watch(isDark, (dark) => {
26
+ document.documentElement.style.colorScheme = dark ? 'dark' : 'light'
27
+ }, { immediate: true })
28
+ installRenderRootColorScheme()
29
+
20
30
  const app = createApp(App)
21
31
  app.use(createPinia())
22
32
  app.use(router)
@@ -26,6 +36,8 @@ export async function mountMainApp() {
26
36
  // Which story list this page holds, so a page that missed its update is sent it (#1218).
27
37
  import.meta.hot.send('poveste:mount', { generation })
28
38
 
29
- /* #__PURE__ */ setupPluginApi()
39
+ // Not `#__PURE__`: the call is nothing but side effects, and that annotation
40
+ // let the bundler drop it, so dev never had the plugin API here (#1064).
41
+ setupPluginApi()
30
42
  }
31
43
  }
package/src/app/router.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Router } from 'vue-router'
1
2
  import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
2
3
  import { povesteConfig } from './util/config'
3
4
 
@@ -12,18 +13,28 @@ function createRouterHistory() {
12
13
  }
13
14
  }
14
15
 
15
- export const router = createRouter({
16
- history: createRouterHistory(),
17
- routes: [
18
- {
19
- path: '/',
20
- name: 'home',
21
- component: () => import('./components/HomeView.vue'),
22
- },
23
- {
24
- path: '/story/:storyId',
25
- name: 'story',
26
- component: () => import('./components/story/StoryView.vue'),
27
- },
28
- ],
29
- })
16
+ function newRouter(): Router {
17
+ return createRouter({
18
+ history: createRouterHistory(),
19
+ routes: [
20
+ {
21
+ path: '/',
22
+ name: 'home',
23
+ component: () => import('./components/HomeView.vue'),
24
+ },
25
+ {
26
+ path: '/story/:storyId',
27
+ name: 'story',
28
+ component: () => import('./components/story/StoryView.vue'),
29
+ },
30
+ ],
31
+ })
32
+ }
33
+
34
+ // A page in `poveste dev` can hold two copies of this module: each importer's
35
+ // URL is stamped when the server serves it, and a story edit stamps most of the
36
+ // chrome. Every copy hands out the router the app installed, or the story store
37
+ // read one still at `/` and the page showed no story until reloaded (#1320).
38
+ export const router: Router = import.meta.hot
39
+ ? (import.meta.hot.data.router ??= newRouter())
40
+ : newRouter()
@@ -1,4 +1,5 @@
1
1
  import type { StoryFile } from './types'
2
+ import { addCollection } from '@iconify/vue'
2
3
  import { reportStoryError } from '@poveste/shared'
3
4
  import { usePreferredDark } from '@vueuse/core'
4
5
  import { createPinia } from 'pinia'
@@ -7,9 +8,10 @@ import { computed, createApp, h, onMounted, ref, watch, watchEffect } from 'vue'
7
8
  import { parseQuery } from 'vue-router'
8
9
  import GenericMountStory from './components/story/GenericMountStory.vue'
9
10
  import GenericRenderStory from './components/story/GenericRenderStory.vue'
10
- import { previewDarkClasses, resolvePreviewDark } from './util/color-scheme.js'
11
- import { PREVIEW_SETTINGS_REQUEST, PREVIEW_SETTINGS_SYNC, SANDBOX_HEIGHT, SANDBOX_READY, SANDBOX_RETARGET, STATE_SYNC } from './util/const.js'
11
+ import { lowPriorityColorScheme, previewColorScheme, previewDarkClasses, resolvePreviewDark } from './util/color-scheme.js'
12
+ import { PREVIEW_SETTINGS_REQUEST, PREVIEW_SETTINGS_SYNC, SANDBOX_COLOR_SCHEME, SANDBOX_HEIGHT, SANDBOX_READY, SANDBOX_RETARGET, STATE_SYNC } from './util/const.js'
12
13
  import { isDark } from './util/dark.js'
14
+ import { iconCollections } from './util/icons.generated.js'
13
15
  import { mapFile } from './util/mapping'
14
16
  import { occupant } from './util/occupant.js'
15
17
  import { applyPreviewSettings, loadStoredPreviewSettings, receivedSettings } from './util/preview-settings.js'
@@ -117,11 +119,46 @@ watch(previewDark, (value) => {
117
119
  immediate: true,
118
120
  })
119
121
 
122
+ // The `<iframe>` element has to declare what this root ends up with, or the
123
+ // browser paints the frame opaque over the background preset (#1167). A book
124
+ // rule may win over the setting, so only this side knows: report the computed
125
+ // value whenever a stylesheet or the root's class can have changed it.
126
+ let reportedColorScheme = ''
127
+ function reportColorScheme() {
128
+ if (window.parent === window) return
129
+ const scheme = getComputedStyle(document.documentElement).colorScheme
130
+ if (scheme === reportedColorScheme) return
131
+ reportedColorScheme = scheme
132
+ window.parent.postMessage({ type: SANDBOX_COLOR_SCHEME, scheme }, window.location.origin)
133
+ }
134
+ new MutationObserver(reportColorScheme).observe(document.head, { childList: true, subtree: true, characterData: true })
135
+ new MutationObserver(reportColorScheme).observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style'] })
136
+ // A `<link>` applies when it loads, after the mutation that inserted it.
137
+ document.addEventListener('load', reportColorScheme, true)
138
+
139
+ // The sandbox is a document of its own, so the property goes on its root, where
140
+ // it reaches every native widget the story renders (#991).
141
+ const setRootColorScheme = lowPriorityColorScheme(':root')
142
+ watch(() => previewColorScheme(receivedSettings.colorScheme, isDark.value), (value) => {
143
+ setRootColorScheme(value)
144
+ reportColorScheme()
145
+ }, {
146
+ immediate: true,
147
+ })
148
+
120
149
  // Height reporting state, declared here because the retarget handler inside the
121
150
  // app resets it.
122
151
  let pendingFrame: number | null = null
123
152
  let lastReportedHeight = -1
124
153
 
154
+ // A builtin control rendered in a story draws the chrome's icons, and each
155
+ // realm has an icon store of its own. Without the data here, a select or a date
156
+ // control in a story fetched its icon from api.iconify.design, in every sandbox,
157
+ // and had none offline (#255).
158
+ for (const collection of iconCollections) {
159
+ addCollection(collection)
160
+ }
161
+
125
162
  const app = createApp({
126
163
  name: 'SandboxApp',
127
164
 
@@ -249,6 +286,8 @@ document.body.classList.add('__poveste-render-story', '__poveste-render-custom-c
249
286
 
250
287
  function reportHeight() {
251
288
  pendingFrame = null
289
+ // A story mounting or resizing can change which book rules match the root.
290
+ reportColorScheme()
252
291
  const renderRoot = document.querySelector('.__poveste-render-story')
253
292
  const h = renderRoot
254
293
  ? Math.ceil(renderRoot.getBoundingClientRect().height)
@@ -1,33 +1,71 @@
1
1
  import type { PreviewSettings } from '../types'
2
2
  import { useStorage } from '@vueuse/core'
3
3
  import { defineStore } from 'pinia'
4
+ import { watch } from 'vue'
4
5
  import { defaultPreviewColorScheme } from '../util/color-scheme'
5
6
  import { povesteConfig } from '../util/config'
6
7
  import { PREVIEW_SETTINGS_STORAGE_KEY } from '../util/preview-settings'
7
8
 
9
+ // What a book can open the preview at, as groups of settings that change
10
+ // together: a preset or a drag sets the width and height at once.
11
+ const UNITS: (keyof PreviewSettings)[][] = [
12
+ ['responsiveWidth', 'responsiveHeight'],
13
+ ['backgroundColor'],
14
+ ['rotate'],
15
+ ['checkerboard'],
16
+ ['textDirection'],
17
+ ]
18
+
19
+ /** The book's `preview` config, with the deprecated `defaultBackgroundColor` under it. */
20
+ function configured(): Partial<PreviewSettings> {
21
+ // eslint-disable-next-line ts/no-deprecated
22
+ const legacy = povesteConfig.defaultBackgroundColor
23
+ return { ...(legacy === undefined ? {} : { backgroundColor: legacy }), ...povesteConfig.preview }
24
+ }
25
+
8
26
  export const usePreviewSettingsStore = defineStore('preview-settings', () => {
27
+ const book = configured()
9
28
  const currentSettings = useStorage<PreviewSettings>(PREVIEW_SETTINGS_STORAGE_KEY, {
10
29
  responsiveWidth: 720,
11
30
  responsiveHeight: null,
12
31
  rotate: false,
13
- backgroundColor: povesteConfig.defaultBackgroundColor ?? 'transparent',
32
+ backgroundColor: 'transparent',
14
33
  backgroundColorPicked: false,
15
34
  checkerboard: false,
16
35
  textDirection: 'ltr',
17
36
  colorScheme: defaultPreviewColorScheme,
37
+ ...book,
18
38
  })
19
39
 
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
40
+ // useStorage honours its default only on first run, so a reader with stored
41
+ // settings would never see the book's values, nor a later change to them.
42
+ // Re-applied on every load, per unit, until the reader changes that unit. A
43
+ // stored `backgroundColorPicked` predates `picked` and counts as picking.
44
+ const picked = new Set(currentSettings.value.picked ?? [])
45
+ if (currentSettings.value.backgroundColorPicked) {
46
+ picked.add('backgroundColor')
47
+ }
48
+ for (const unit of UNITS) {
49
+ if (picked.has(unit[0]!)) {
50
+ continue
51
+ }
52
+ for (const key of unit) {
53
+ if (book[key] !== undefined) {
54
+ Object.assign(currentSettings.value, { [key]: book[key] })
55
+ }
56
+ }
57
+ }
58
+
59
+ // Set up after the re-apply, so only the reader's own changes count: the
60
+ // toolbar, the size inputs and the drag handles all write these settings.
61
+ for (const unit of UNITS) {
62
+ watch(() => unit.map(key => currentSettings.value[key]), () => {
63
+ currentSettings.value.picked = [...new Set([...(currentSettings.value.picked ?? []), unit[0]!])]
64
+ })
27
65
  }
28
66
 
29
- // Same reason: settings stored before `colorScheme` existed would leave it
30
- // undefined, so the toolbar would show no active option.
67
+ // Settings stored before `colorScheme` existed would leave it undefined, so
68
+ // the toolbar would show no active option.
31
69
  currentSettings.value.colorScheme ??= defaultPreviewColorScheme
32
70
 
33
71
  function setBackgroundColor(color: string) {
package/src/app/types.ts CHANGED
@@ -78,11 +78,17 @@ export interface PreviewSettings {
78
78
  backgroundColor: string
79
79
  /**
80
80
  * Whether `backgroundColor` came from an explicit toolbar pick rather than
81
- * from the `defaultBackgroundColor` config option.
81
+ * from the book's config. Kept for settings stored before `picked`.
82
82
  *
83
83
  * @default false
84
84
  */
85
85
  backgroundColorPicked: boolean
86
+ /**
87
+ * The settings the reader has changed from the toolbar, which a book's
88
+ * `preview` config stops re-applying. The size is one entry,
89
+ * `responsiveWidth`, for both its width and height.
90
+ */
91
+ picked?: string[]
86
92
  /**
87
93
  * Show a checkerboard pattern behind the story, to spot transparency.
88
94
  *
@@ -25,6 +25,50 @@ export function resolvePreviewDark(colorScheme: SandboxColorScheme | undefined,
25
25
  }
26
26
  }
27
27
 
28
+ /**
29
+ * The CSS `color-scheme` a preview declares, from the same inputs as the class.
30
+ *
31
+ * The class tells the book's CSS; this tells the browser, which paints native
32
+ * widgets, scrollbars and the canvas itself and reads no class (#991). `auto`
33
+ * becomes `light dark`, so the browser follows the OS preference on its own
34
+ * rather than being told the answer `prefersDark` already computed.
35
+ */
36
+ export function previewColorScheme(colorScheme: SandboxColorScheme | undefined, chromeDark: boolean): 'light' | 'dark' | 'light dark' {
37
+ switch (colorScheme) {
38
+ case 'light': return 'light'
39
+ case 'dark': return 'dark'
40
+ case 'auto': return 'light dark'
41
+ default: return chromeDark ? 'dark' : 'light'
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Declares `color-scheme` on `selector` beneath any rule a book writes: in the
47
+ * first cascade layer of the document, at zero specificity. Set inline, it beat
48
+ * a book's own `:root { color-scheme }` on every render path (#1167).
49
+ */
50
+ export function lowPriorityColorScheme(selector: string) {
51
+ const style = document.createElement('style')
52
+ document.head.prepend(style)
53
+ return (value: string) => {
54
+ style.textContent = `@layer poveste-color-scheme { :where(${selector}) { color-scheme: ${value}; } }`
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Previews the app renders itself carry their scheme as a custom property, which
60
+ * this applies to their root at the same low priority. Style isolation moves a
61
+ * book's own `:root` rule onto that root, so it wins.
62
+ */
63
+ export function installRenderRootColorScheme() {
64
+ lowPriorityColorScheme('.__poveste-render-story')('var(--poveste-color-scheme)')
65
+ }
66
+
67
+ /** `previewColorScheme`, for previews rendered by the app itself. */
68
+ export function usePreviewColorScheme(settings: PreviewSettings) {
69
+ return computed(() => previewColorScheme(settings.colorScheme, isDark.value))
70
+ }
71
+
28
72
  /**
29
73
  * Whether the story preview should be rendered dark, for previews rendered by
30
74
  * the app itself. The sandbox resolves the same thing from the settings it
@@ -46,15 +46,27 @@ export const builtinCommands: ClientCommand[] = [
46
46
  },
47
47
  ]
48
48
 
49
- export function executeCommand(command: ClientCommand, params: Record<string, any>) {
50
- if (import.meta.hot) {
51
- import.meta.hot.send('poveste:dev-command', {
52
- id: command.id,
53
- params,
54
- })
49
+ export async function executeCommand(command: ClientCommand, params: Record<string, any>) {
50
+ const hot = import.meta.hot
51
+ if (!hot) return
55
52
 
56
- command.clientAction?.(params, getCommandContext())
53
+ // The client half waits for the server's: a generated story was opened before its
54
+ // file existed, and opened anyway when writing it failed (#1176).
55
+ const result = await new Promise<{ id: string, error?: string }>((resolve) => {
56
+ const listener = (data: { id: string, error?: string }) => {
57
+ if (data.id !== command.id) return
58
+ hot.off('poveste:dev-command-result', listener)
59
+ resolve(data)
60
+ }
61
+ hot.on('poveste:dev-command-result', listener)
62
+ hot.send('poveste:dev-command', { id: command.id, params })
63
+ })
64
+
65
+ if (result.error) {
66
+ console.error(`[poveste] ${command.label}: ${result.error}`)
67
+ return
57
68
  }
69
+ command.clientAction?.(params, getCommandContext())
58
70
  }
59
71
 
60
72
  export function getCommandContext(): ClientCommandContext {
@@ -5,3 +5,4 @@ export const PREVIEW_SETTINGS_SYNC = '__poveste:preview-settings-sync'
5
5
  export const PREVIEW_SETTINGS_REQUEST = '__poveste:preview-settings-request'
6
6
  export const SANDBOX_HEIGHT = '__poveste:sandbox-height'
7
7
  export const SANDBOX_RETARGET = '__poveste:sandbox-retarget'
8
+ export const SANDBOX_COLOR_SCHEME = '__poveste:sandbox-color-scheme'
@@ -1,25 +0,0 @@
1
- import { readFileSync } from 'node:fs'
2
- import { resolve } from 'node:path'
3
- import { describe, expect, it } from 'vitest'
4
-
5
- /*
6
- * The same defect as `@poveste/controls`' own select, in the chrome's copy: the
7
- * options are portalled to `body`, which carries no colour, so a background
8
- * without a text colour left them at the UA default — black on the dark panel.
9
- *
10
- * `__dirname` rather than `import.meta.url`, which vitest's transform does not
11
- * leave intact here — the trap `icons.spec.ts` records.
12
- */
13
- const source = readFileSync(resolve(__dirname, '../app/components/base/BaseSelect.vue'), 'utf8')
14
-
15
- describe('the chrome select options', () => {
16
- const classes = /class="poveste-base-select-options([^"]*)"/.exec(source)?.[1] ?? ''
17
-
18
- it('carries a text colour, not only a background', () => {
19
- expect(classes).toMatch(/\btext-gray-\d+\b/)
20
- })
21
-
22
- it('carries one for dark as well', () => {
23
- expect(classes).toMatch(/\bdark:text-gray-\d+\b/)
24
- })
25
- })