@poveste/app 0.18.2 → 0.19.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.
- package/dist/THIRD_PARTY_NOTICES.md +159 -0
- package/dist/bundled/app.css +4 -4
- package/dist/bundled/components/command/PromptSelect.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/bundled/components/story/StoryNativeScope.js +14 -0
- package/dist/bundled/components/story/StoryVariantGridItem.js +1 -1
- package/dist/bundled/components/story/StoryVariantGridItem.vue_vue_type_script_setup_true_lang.js +23 -16
- package/dist/bundled/components/story/StoryVariantSinglePreviewNative.vue_vue_type_script_setup_true_lang.js +22 -15
- package/dist/bundled/components/story/StoryVariantSinglePreviewRemote.vue_vue_type_script_setup_true_lang.js +8 -2
- package/dist/bundled/index.js +12 -3
- package/dist/bundled/sandbox.js +32 -2
- package/dist/bundled/stores/preview-settings.js +29 -4
- package/dist/bundled/util/color-scheme.js +41 -1
- package/dist/bundled/util/commands.js +16 -4
- package/dist/bundled/util/const.js +2 -1
- package/package.json +6 -4
- package/src/app/components/command/PromptSelect.vue +2 -1
- package/src/app/components/story/StoryNativeScope.vue +23 -0
- package/src/app/components/story/StoryVariantGridItem.vue +14 -10
- package/src/app/components/story/StoryVariantSinglePreviewNative.vue +15 -10
- package/src/app/components/story/StoryVariantSinglePreviewRemote.vue +13 -2
- package/src/app/index.ts +14 -2
- package/src/app/sandbox.ts +41 -2
- package/src/app/stores/preview-settings.ts +48 -10
- package/src/app/types.ts +7 -1
- package/src/app/util/color-scheme.ts +44 -0
- package/src/app/util/commands.ts +19 -7
- package/src/app/util/const.ts +1 -0
- package/src/__tests__/base-select.spec.ts +0 -25
- package/src/__tests__/color-scheme.spec.ts +0 -55
- package/src/__tests__/font.spec.ts +0 -76
- package/src/__tests__/grid-slots.spec.ts +0 -84
- package/src/__tests__/icons.spec.ts +0 -26
- package/src/__tests__/postcss-scope-wrapper.spec.ts +0 -65
- package/src/__tests__/render-tracker.spec.ts +0 -38
- package/src/__tests__/reset.spec.ts +0 -53
- package/src/__tests__/source-code.spec.ts +0 -55
- package/src/__tests__/state-bridge.spec.ts +0 -155
- package/src/__tests__/state-clone.spec.ts +0 -120
- package/src/__tests__/state-types.spec.ts +0 -175
- package/src/__tests__/uncaught.spec.ts +0 -36
|
@@ -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
|
-
|
|
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
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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/sandbox.ts
CHANGED
|
@@ -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:
|
|
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
|
|
21
|
-
// would never see
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
26
|
-
|
|
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
|
-
//
|
|
30
|
-
//
|
|
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
|
|
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
|
package/src/app/util/commands.ts
CHANGED
|
@@ -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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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 {
|
package/src/app/util/const.ts
CHANGED
|
@@ -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
|
-
})
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
-
|
|
3
|
-
// `util/config` re-exports a virtual module Vite generates from the book's own
|
|
4
|
-
// config, so the tests supply it directly.
|
|
5
|
-
const config: { theme: { darkClass?: string }, sandboxDarkClass?: string } = { theme: {} }
|
|
6
|
-
|
|
7
|
-
vi.mock('../app/util/config', () => ({
|
|
8
|
-
get povesteConfig() {
|
|
9
|
-
return config
|
|
10
|
-
},
|
|
11
|
-
}))
|
|
12
|
-
|
|
13
|
-
// Touches browser storage on import; unrelated to the pure function under test.
|
|
14
|
-
vi.mock('../app/util/dark', () => ({ isDark: { value: false } }))
|
|
15
|
-
|
|
16
|
-
const { previewDarkClasses } = await import('../app/util/color-scheme')
|
|
17
|
-
|
|
18
|
-
function darkClassesFor({ darkClass = 'dark', sandboxDarkClass }: { darkClass?: string, sandboxDarkClass?: string }) {
|
|
19
|
-
config.theme = { darkClass }
|
|
20
|
-
config.sandboxDarkClass = sandboxDarkClass
|
|
21
|
-
return previewDarkClasses()
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
describe('previewDarkClasses', () => {
|
|
25
|
-
it('returns the theme class for a book that configures neither', () => {
|
|
26
|
-
expect(darkClassesFor({})).toEqual(['dark'])
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
// #126: the deprecated option used to default to `dark` and be emitted here
|
|
30
|
-
// too, which is what made the three render paths disagree.
|
|
31
|
-
it('returns the theme class alone when only it is configured', () => {
|
|
32
|
-
expect(darkClassesFor({ darkClass: 'my-dark' })).toEqual(['my-dark'])
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('appends sandboxDarkClass when a book still sets it', () => {
|
|
36
|
-
expect(darkClassesFor({ darkClass: 'my-dark', sandboxDarkClass: 'legacy' })).toEqual(['my-dark', 'legacy'])
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('returns one class when both names are the same', () => {
|
|
40
|
-
expect(darkClassesFor({ darkClass: 'dark', sandboxDarkClass: 'dark' })).toEqual(['dark'])
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
// `classList.toggle('')` throws, and the sandbox runs it during boot, so an
|
|
44
|
-
// empty entry leaves every preview blank rather than merely unstyled. Only
|
|
45
|
-
// the empty string reaches here — defu fills an undefined class.
|
|
46
|
-
describe('when a configured class name is empty', () => {
|
|
47
|
-
it('drops an empty theme class', () => {
|
|
48
|
-
expect(darkClassesFor({ darkClass: '' })).toEqual([])
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('drops an empty sandboxDarkClass', () => {
|
|
52
|
-
expect(darkClassesFor({ sandboxDarkClass: '' })).toEqual(['dark'])
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
})
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
import { describe, expect, it } from 'vitest'
|
|
4
|
-
// @ts-expect-error plain script, no types
|
|
5
|
-
import { FONT_PACKAGE, generateFontCss, referencedFiles } from '../../scripts/font.mjs'
|
|
6
|
-
|
|
7
|
-
// Vitest runs from the package root, and stubs `?raw` CSS imports to empty.
|
|
8
|
-
// Newlines are normalised: the shipped sheet is checked in, so a Windows
|
|
9
|
-
// checkout hands it back with CRLF, and this compares content against the
|
|
10
|
-
// LF-newline installed package, not line endings (#156).
|
|
11
|
-
const read = (path: string) => readFileSync(join(process.cwd(), path), 'utf8').replace(/\r\n/g, '\n')
|
|
12
|
-
|
|
13
|
-
const shipped = read('src/app/style/font.css')
|
|
14
|
-
const upstream = read(join('node_modules', FONT_PACKAGE, 'index.css'))
|
|
15
|
-
|
|
16
|
-
describe('the vendored files', () => {
|
|
17
|
-
// The sheet and the files are one artifact now that the package is a
|
|
18
|
-
// devDependency: a book asking for a face that is not on disk renders the
|
|
19
|
-
// fallback, and nothing else would say so (#306).
|
|
20
|
-
it('ships every file the sheet asks for', () => {
|
|
21
|
-
const missing = referencedFiles(shipped).filter(file => !existsSync(join(process.cwd(), 'src/app/style/fonts', file)))
|
|
22
|
-
|
|
23
|
-
expect(missing).toEqual([])
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
// The other direction: a subset upstream drops or renames leaves a file the
|
|
27
|
-
// sheet no longer names, and `files: ['dist', 'src']` would publish it.
|
|
28
|
-
it('ships nothing the sheet does not ask for', () => {
|
|
29
|
-
const named = new Set([...referencedFiles(shipped), 'LICENSE'])
|
|
30
|
-
const orphans = readdirSync(join(process.cwd(), 'src/app/style/fonts')).filter(file => !named.has(file))
|
|
31
|
-
|
|
32
|
-
expect(orphans).toEqual([])
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('asks for at least one, so a sheet that references nothing is not a pass', () => {
|
|
36
|
-
expect(referencedFiles(shipped).length).toBeGreaterThan(0)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
// SIL OFL requires the notice to travel with the font.
|
|
40
|
-
it('carries the licence the font is under', () => {
|
|
41
|
-
expect(read('src/app/style/fonts/LICENSE')).toContain('SIL Open Font License')
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
// Vendoring froze these bytes. A Fontsource patch that fixes hinting or adds
|
|
45
|
-
// a glyph keeps the filenames and unicode-ranges, so the sheet comparison
|
|
46
|
-
// below stays green while the files it names are stale — the property that
|
|
47
|
-
// installing the package used to provide for free.
|
|
48
|
-
it('matches upstream byte for byte, since a bump no longer ships itself', () => {
|
|
49
|
-
const stale = referencedFiles(shipped).filter(file => !readFileSync(join(process.cwd(), 'src/app/style/fonts', file))
|
|
50
|
-
.equals(readFileSync(join(process.cwd(), 'node_modules', FONT_PACKAGE, 'files', file))))
|
|
51
|
-
|
|
52
|
-
expect(stale).toEqual([])
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
// `read`, not `readFileSync`: the licence is checked in, so a Windows
|
|
56
|
-
// checkout hands it back with CRLF against the LF of the installed copy.
|
|
57
|
-
it('carries upstream\'s own licence text, not a copy that has drifted', () => {
|
|
58
|
-
expect(read('src/app/style/fonts/LICENSE')).toBe(read(join('node_modules', FONT_PACKAGE, 'LICENSE')))
|
|
59
|
-
})
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
describe('the bundled app face', () => {
|
|
63
|
-
it('matches the installed package, so a bump cannot go unnoticed', () => {
|
|
64
|
-
expect(shipped).toBe(generateFontCss(upstream))
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('asks for no format the woff2 already covers', () => {
|
|
68
|
-
expect(shipped).not.toContain('format(\'woff\')')
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('keeps every subset upstream ships, so non-latin text stays on the face', () => {
|
|
72
|
-
const subsets = (css: string) => (css.match(/unicode-range: [^;]+/g) ?? []).sort()
|
|
73
|
-
|
|
74
|
-
expect(subsets(shipped)).toEqual(subsets(upstream))
|
|
75
|
-
})
|
|
76
|
-
})
|