@poveste/app 0.12.3 → 0.13.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/app/style/font.css +11 -7
- package/dist/app/style/fonts/LICENSE +93 -0
- package/dist/app/style/fonts/noto-sans-display-cyrillic-400-normal.woff2 +0 -0
- package/dist/app/style/fonts/noto-sans-display-cyrillic-ext-400-normal.woff2 +0 -0
- package/dist/app/style/fonts/noto-sans-display-greek-400-normal.woff2 +0 -0
- package/dist/app/style/fonts/noto-sans-display-greek-ext-400-normal.woff2 +0 -0
- package/dist/app/style/fonts/noto-sans-display-latin-400-normal.woff2 +0 -0
- package/dist/app/style/fonts/noto-sans-display-latin-ext-400-normal.woff2 +0 -0
- package/dist/app/style/fonts/noto-sans-display-vietnamese-400-normal.woff2 +0 -0
- package/dist/bundled/App.vue_vue_type_script_setup_true_lang.js +19 -19
- package/dist/bundled/app.css +10 -10
- package/dist/bundled/components/base/BaseSplitPane.js +1 -1
- package/dist/bundled/components/base/BaseSplitPane.vue_vue_type_script_setup_true_lang.js +67 -29
- package/dist/bundled/components/story/StoryVariantGridItem.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/bundled/components/story/StoryVariantSingle.js +1 -1
- package/dist/bundled/components/story/StoryVariantSingle.vue_vue_type_script_setup_true_lang.js +20 -23
- package/dist/bundled/components/story/StoryVariantSinglePreviewNative.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/bundled/components/story/StoryVariantSinglePreviewRemote.vue_vue_type_script_setup_true_lang.js +4 -0
- package/dist/bundled/components/story/StoryVariantSingleView.vue_vue_type_script_setup_true_lang.js +23 -13
- package/dist/bundled/components/story/StoryView.vue_vue_type_script_setup_true_lang.js +8 -11
- package/dist/bundled/sandbox.js +6 -1
- package/dist/bundled/util/events.js +4 -1
- package/dist/bundled/util/highlighter.js +11 -4
- package/dist/bundled/util/occupant.js +16 -0
- package/dist/bundled/util/variant.js +12 -1
- package/dist/style.css +4 -4
- package/package.json +5 -5
- package/src/__tests__/font.spec.ts +48 -2
- package/src/app/App.vue +56 -46
- package/src/app/components/base/BaseSplitPane.vue +95 -29
- package/src/app/components/story/StoryVariantSingle.vue +72 -66
- package/src/app/components/story/StoryVariantSinglePreviewNative.vue +13 -2
- package/src/app/components/story/StoryVariantSinglePreviewRemote.vue +8 -0
- package/src/app/components/story/StoryVariantSingleView.vue +59 -13
- package/src/app/components/story/StoryView.vue +17 -17
- package/src/app/sandbox.ts +9 -1
- package/src/app/style/font.css +11 -7
- package/src/app/style/fonts/LICENSE +93 -0
- package/src/app/style/fonts/noto-sans-display-cyrillic-400-normal.woff2 +0 -0
- package/src/app/style/fonts/noto-sans-display-cyrillic-ext-400-normal.woff2 +0 -0
- package/src/app/style/fonts/noto-sans-display-greek-400-normal.woff2 +0 -0
- package/src/app/style/fonts/noto-sans-display-greek-ext-400-normal.woff2 +0 -0
- package/src/app/style/fonts/noto-sans-display-latin-400-normal.woff2 +0 -0
- package/src/app/style/fonts/noto-sans-display-latin-ext-400-normal.woff2 +0 -0
- package/src/app/style/fonts/noto-sans-display-vietnamese-400-normal.woff2 +0 -0
- package/src/app/util/events.ts +3 -0
- package/src/app/util/highlighter.ts +16 -9
- package/src/app/util/occupant.ts +13 -0
- package/src/app/util/variant.ts +13 -1
- package/LICENSE +0 -22
package/dist/bundled/components/story/StoryVariantSingleView.vue_vue_type_script_setup_true_lang.js
CHANGED
|
@@ -1,28 +1,38 @@
|
|
|
1
|
+
import { autoSelectsVariant } from "../../util/variant.js";
|
|
1
2
|
import StoryVariantSinglePreviewRemote_default from "./StoryVariantSinglePreviewRemote.js";
|
|
2
3
|
import StoryVariantSinglePreviewNative_default from "./StoryVariantSinglePreviewNative.js";
|
|
3
|
-
import { createBlock, createElementBlock, defineComponent, openBlock } from "@poveste/vendors/vue";
|
|
4
|
+
import { Fragment, createBlock, createCommentVNode, createElementBlock, defineComponent, normalizeClass, openBlock, shallowRef, watchEffect } from "@poveste/vendors/vue";
|
|
4
5
|
//#region src/app/components/story/StoryVariantSingleView.vue?vue&type=script&setup=true&lang.ts
|
|
5
|
-
var _hoisted_1 = {
|
|
6
|
-
class: "poveste-story-variant-single-view h-full flex flex-col",
|
|
7
|
-
"data-testid": "story-variant-single-view"
|
|
8
|
-
};
|
|
9
6
|
var StoryVariantSingleView_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
|
10
7
|
__name: "StoryVariantSingleView",
|
|
11
8
|
props: {
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
story: {},
|
|
10
|
+
variant: {}
|
|
14
11
|
},
|
|
15
12
|
setup(__props) {
|
|
13
|
+
const props = __props;
|
|
14
|
+
const shown = shallowRef(null);
|
|
15
|
+
watchEffect(() => {
|
|
16
|
+
const story = props.story;
|
|
17
|
+
if (story && props.variant) shown.value = {
|
|
18
|
+
story,
|
|
19
|
+
variant: props.variant
|
|
20
|
+
};
|
|
21
|
+
else if (shown.value && shown.value.story !== story && !autoSelectsVariant(story)) shown.value = null;
|
|
22
|
+
});
|
|
16
23
|
return (_ctx, _cache) => {
|
|
17
|
-
return openBlock(), createElementBlock("div",
|
|
24
|
+
return openBlock(), createElementBlock("div", {
|
|
25
|
+
class: normalizeClass(["poveste-story-variant-single-view h-full flex flex-col", { invisible: !__props.variant }]),
|
|
26
|
+
"data-testid": "story-variant-single-view"
|
|
27
|
+
}, [shown.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [shown.value.story.layout?.iframe === false ? (openBlock(), createBlock(StoryVariantSinglePreviewNative_default, {
|
|
18
28
|
key: 0,
|
|
19
|
-
story:
|
|
20
|
-
variant:
|
|
29
|
+
story: shown.value.story,
|
|
30
|
+
variant: shown.value.variant
|
|
21
31
|
}, null, 8, ["story", "variant"])) : (openBlock(), createBlock(StoryVariantSinglePreviewRemote_default, {
|
|
22
32
|
key: 1,
|
|
23
|
-
story:
|
|
24
|
-
variant:
|
|
25
|
-
}, null, 8, ["story", "variant"]))]);
|
|
33
|
+
story: shown.value.story,
|
|
34
|
+
variant: shown.value.variant
|
|
35
|
+
}, null, 8, ["story", "variant"]))], 64)) : createCommentVNode("", true)], 2);
|
|
26
36
|
};
|
|
27
37
|
}
|
|
28
38
|
});
|
|
@@ -3,6 +3,7 @@ import { useStoryStore } from "../../stores/story.js";
|
|
|
3
3
|
import BaseSplitPane_default from "../base/BaseSplitPane.js";
|
|
4
4
|
import { useLayoutStore } from "../../stores/layout.js";
|
|
5
5
|
import BaseEmpty_default from "../base/BaseEmpty.js";
|
|
6
|
+
import { autoSelectsVariant } from "../../util/variant.js";
|
|
6
7
|
import StoryDocs_default from "../panel/StoryDocs.js";
|
|
7
8
|
import StorySidePanel_default from "../panel/StorySidePanel.js";
|
|
8
9
|
import StoryViewer_default from "./StoryViewer.js";
|
|
@@ -19,7 +20,7 @@ var StoryView_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
|
|
|
19
20
|
setup(__props) {
|
|
20
21
|
const storyStore = useStoryStore();
|
|
21
22
|
const layoutStore = useLayoutStore();
|
|
22
|
-
const effectiveStoryOptionsVisible = computed(() => storyStore.currentStory?.meta?.storyOptions ?? layoutStore.settings.storyOptionsVisible);
|
|
23
|
+
const effectiveStoryOptionsVisible = computed(() => !!(storyStore.currentStory?.meta?.storyOptions ?? layoutStore.settings.storyOptionsVisible));
|
|
23
24
|
const placement = computed(() => layoutStore.settings.storyOptionsPlacement);
|
|
24
25
|
const splitOrientation = computed(() => placement.value === "bottom" ? "portrait" : "landscape");
|
|
25
26
|
const splitDefaultSplit = computed(() => placement.value === "bottom" ? 60 : 75);
|
|
@@ -29,13 +30,7 @@ var StoryView_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
|
|
|
29
30
|
if (value) storyStore.currentStory.lastSelectedVariant = value;
|
|
30
31
|
}, { immediate: true });
|
|
31
32
|
watch(() => [storyStore.currentStory, storyStore.currentVariant], () => {
|
|
32
|
-
if (!storyStore.currentVariant)
|
|
33
|
-
if (storyStore.currentStory?.lastSelectedVariant) {
|
|
34
|
-
setVariant(storyStore.currentStory.lastSelectedVariant.id);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
if (storyStore.currentStory?.variants.length === 1) setVariant(storyStore.currentStory.variants[0].id);
|
|
38
|
-
}
|
|
33
|
+
if (!storyStore.currentVariant && autoSelectsVariant(storyStore.currentStory)) setVariant((storyStore.currentStory.lastSelectedVariant ?? storyStore.currentStory.variants[0]).id);
|
|
39
34
|
}, { immediate: true });
|
|
40
35
|
function setVariant(variantId) {
|
|
41
36
|
router.replace({
|
|
@@ -70,14 +65,15 @@ var StoryView_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
|
|
|
70
65
|
standalone: "",
|
|
71
66
|
class: "md:p-12 w-full md:max-w-[600px] lg:max-w-[800px] xl:max-w-[900px]",
|
|
72
67
|
onScrollTop: _cache[0] || (_cache[0] = ($event) => scrollDocsToTop())
|
|
73
|
-
}, null, 8, ["story"])], 512)) :
|
|
74
|
-
key:
|
|
68
|
+
}, null, 8, ["story"])], 512)) : (openBlock(), createBlock(BaseSplitPane_default, {
|
|
69
|
+
key: 1,
|
|
75
70
|
"save-id": `story-main-${placement.value}`,
|
|
76
71
|
orientation: splitOrientation.value,
|
|
77
72
|
min: 30,
|
|
78
73
|
max: 95,
|
|
79
74
|
"default-split": splitDefaultSplit.value,
|
|
80
75
|
"show-divider": false,
|
|
76
|
+
"show-last": !unref(isMobile) && effectiveStoryOptionsVisible.value,
|
|
81
77
|
class: "h-full"
|
|
82
78
|
}, {
|
|
83
79
|
first: withCtx(() => [createVNode(StoryViewer_default)]),
|
|
@@ -86,7 +82,8 @@ var StoryView_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
|
|
|
86
82
|
}, 8, [
|
|
87
83
|
"save-id",
|
|
88
84
|
"orientation",
|
|
89
|
-
"default-split"
|
|
85
|
+
"default-split",
|
|
86
|
+
"show-last"
|
|
90
87
|
]))]));
|
|
91
88
|
};
|
|
92
89
|
}
|
package/dist/bundled/sandbox.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isDark } from "./util/dark.js";
|
|
2
2
|
import { PREVIEW_SETTINGS_REQUEST, SANDBOX_HEIGHT, SANDBOX_READY, STATE_SYNC } from "./util/const.js";
|
|
3
|
+
import { occupant } from "./util/occupant.js";
|
|
3
4
|
import { previewDarkClasses, resolvePreviewDark } from "./util/color-scheme.js";
|
|
4
5
|
import { applyPreviewSettings, loadStoredPreviewSettings, receivedSettings } from "./util/preview-settings.js";
|
|
5
6
|
import GenericMountStory_default from "./components/story/GenericMountStory.js";
|
|
@@ -9,7 +10,7 @@ import { createStateBridge } from "./util/state-bridge.js";
|
|
|
9
10
|
import { toRawDeep } from "./util/state.js";
|
|
10
11
|
import { isBenignResizeObserverEvent } from "./util/uncaught.js";
|
|
11
12
|
import { usePreferredDark } from "@poveste/vendors/vue-use";
|
|
12
|
-
import { computed, createApp, h, onMounted, ref, watch } from "@poveste/vendors/vue";
|
|
13
|
+
import { computed, createApp, h, onMounted, ref, watch, watchEffect } from "@poveste/vendors/vue";
|
|
13
14
|
import { createPinia } from "@poveste/vendors/pinia";
|
|
14
15
|
import { files, onUpdate } from "virtual:$poveste-stories";
|
|
15
16
|
import { parseQuery } from "@poveste/vendors/vue-router";
|
|
@@ -18,6 +19,10 @@ import { reportStoryError } from "@poveste/shared";
|
|
|
18
19
|
var query = parseQuery(window.location.search);
|
|
19
20
|
var storyId = ref(typeof query.storyId === "string" ? query.storyId : null);
|
|
20
21
|
var variantId = ref(typeof query.variantId === "string" ? query.variantId : null);
|
|
22
|
+
watchEffect(() => {
|
|
23
|
+
occupant.storyId = storyId.value;
|
|
24
|
+
occupant.variantId = variantId.value;
|
|
25
|
+
});
|
|
21
26
|
var file = ref(null);
|
|
22
27
|
function resolveFile(list = files) {
|
|
23
28
|
const found = list.find((f) => f.id === storyId.value);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EVENT_SEND } from "./const.js";
|
|
2
|
+
import { occupant } from "./occupant.js";
|
|
2
3
|
//#region src/app/util/events.ts
|
|
3
4
|
async function logEvent(name, argument) {
|
|
4
5
|
const event = {
|
|
@@ -7,7 +8,9 @@ async function logEvent(name, argument) {
|
|
|
7
8
|
};
|
|
8
9
|
if (location.href.includes("__sandbox")) window.parent?.postMessage({
|
|
9
10
|
type: EVENT_SEND,
|
|
10
|
-
event
|
|
11
|
+
event,
|
|
12
|
+
storyId: occupant.storyId,
|
|
13
|
+
variantId: occupant.variantId
|
|
11
14
|
});
|
|
12
15
|
else {
|
|
13
16
|
const { useEventsStore } = await import("../stores/events.js");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHighlighterCore } from "shiki/core";
|
|
2
|
+
import { createOnigurumaEngine } from "shiki/engine/oniguruma";
|
|
2
3
|
//#region src/app/util/highlighter.ts
|
|
3
4
|
/**
|
|
4
5
|
* Shiki is meant to be used as a singleton: each instance carries its own WASM
|
|
@@ -9,10 +10,16 @@ import { createHighlighter } from "shiki";
|
|
|
9
10
|
* The langs and themes are fixed, so one shared instance is always correct.
|
|
10
11
|
*/
|
|
11
12
|
var highlighterPromise;
|
|
13
|
+
/**
|
|
14
|
+
* `shiki/core`, not `shiki`: the latter is the full-bundle entry and ships every
|
|
15
|
+
* grammar and theme whatever the options ask for — 10 MB a book (#304). The
|
|
16
|
+
* dynamic imports are the interface, not a lazy-loading choice.
|
|
17
|
+
*/
|
|
12
18
|
function getHighlighter() {
|
|
13
|
-
highlighterPromise ??=
|
|
14
|
-
langs: ["html", "jsx"],
|
|
15
|
-
themes: ["github-light", "github-dark"]
|
|
19
|
+
highlighterPromise ??= createHighlighterCore({
|
|
20
|
+
langs: [import("shiki/langs/html.mjs"), import("shiki/langs/jsx.mjs")],
|
|
21
|
+
themes: [import("shiki/themes/github-light.mjs"), import("shiki/themes/github-dark.mjs")],
|
|
22
|
+
engine: createOnigurumaEngine(import("shiki/wasm"))
|
|
16
23
|
}).catch((e) => {
|
|
17
24
|
highlighterPromise = void 0;
|
|
18
25
|
throw e;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/app/util/occupant.ts
|
|
2
|
+
/**
|
|
3
|
+
* What the sandbox realm is currently showing.
|
|
4
|
+
*
|
|
5
|
+
* The realm is reused: a `SANDBOX_RETARGET` hands it another story rather than
|
|
6
|
+
* booting a new document, so a message can outlive the occupant that queued it.
|
|
7
|
+
* Every message `sandbox.ts` posts already carries these ids for that reason;
|
|
8
|
+
* this module exists so `logEvent` — which posts from wherever a story calls it,
|
|
9
|
+
* not from `sandbox.ts` — can carry them too (#328).
|
|
10
|
+
*/
|
|
11
|
+
var occupant = {
|
|
12
|
+
storyId: null,
|
|
13
|
+
variantId: null
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { occupant };
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { computed } from "@poveste/vendors/vue";
|
|
2
2
|
import { useRoute } from "@poveste/vendors/vue-router";
|
|
3
3
|
//#region src/app/util/variant.ts
|
|
4
|
+
/**
|
|
5
|
+
* Whether opening this story will put a variant in the route by itself.
|
|
6
|
+
*
|
|
7
|
+
* `StoryView` resolves the variant one tick after the story, so anything that
|
|
8
|
+
* has to tell "a variant is on its way" from "this story is waiting for the
|
|
9
|
+
* reader to pick one" needs the same answer it will give — hence one predicate
|
|
10
|
+
* rather than two copies of the condition (#328).
|
|
11
|
+
*/
|
|
12
|
+
function autoSelectsVariant(story) {
|
|
13
|
+
return !!story && (!!story.lastSelectedVariant || story.variants.length === 1);
|
|
14
|
+
}
|
|
4
15
|
function useCurrentVariantRoute(variant) {
|
|
5
16
|
const route = useRoute();
|
|
6
17
|
return {
|
|
@@ -15,4 +26,4 @@ function useCurrentVariantRoute(variant) {
|
|
|
15
26
|
};
|
|
16
27
|
}
|
|
17
28
|
//#endregion
|
|
18
|
-
export { useCurrentVariantRoute };
|
|
29
|
+
export { autoSelectsVariant, useCurrentVariantRoute };
|
package/dist/style.css
CHANGED
|
@@ -174,10 +174,6 @@
|
|
|
174
174
|
.bind-col-size {
|
|
175
175
|
grid-template-columns: repeat(auto-fill, minmax(var(--poveste-col-size), 1fr));
|
|
176
176
|
}
|
|
177
|
-
.__poveste-json-code[data-v-e4c89a1b] .cm-editor {
|
|
178
|
-
min-width: 280px;
|
|
179
|
-
height: 100%;
|
|
180
|
-
}
|
|
181
177
|
.v-popper {
|
|
182
178
|
line-height: 0;
|
|
183
179
|
}
|
|
@@ -278,6 +274,10 @@
|
|
|
278
274
|
inherits: false;
|
|
279
275
|
initial-value: solid;
|
|
280
276
|
}
|
|
277
|
+
.__poveste-json-code[data-v-e4c89a1b] .cm-editor {
|
|
278
|
+
min-width: 280px;
|
|
279
|
+
height: 100%;
|
|
280
|
+
}
|
|
281
281
|
.v-popper {
|
|
282
282
|
line-height: 0;
|
|
283
283
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poveste/app",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.13.0",
|
|
5
5
|
"description": "Pre-bundled UI for Poveste",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Sorin Gitlan",
|
|
@@ -38,14 +38,14 @@
|
|
|
38
38
|
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@
|
|
42
|
-
"@poveste/
|
|
43
|
-
"@poveste/
|
|
44
|
-
"@poveste/vendors": "^0.12.3",
|
|
41
|
+
"@poveste/controls": "^0.13.0",
|
|
42
|
+
"@poveste/shared": "^0.13.0",
|
|
43
|
+
"@poveste/vendors": "^0.13.0",
|
|
45
44
|
"fuse.js": "^7.0.0",
|
|
46
45
|
"shiki": "^3.0.0"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
48
|
+
"@fontsource/noto-sans-display": "^5.3.0",
|
|
49
49
|
"@iconify-json/carbon": "^1.2.25",
|
|
50
50
|
"@iconify-json/fluent": "1.2.56",
|
|
51
51
|
"@iconify-json/mdi": "^1.2.3",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { describe, expect, it } from 'vitest'
|
|
4
4
|
// @ts-expect-error plain script, no types
|
|
5
|
-
import { FONT_PACKAGE, generateFontCss } from '../../scripts/font.mjs'
|
|
5
|
+
import { FONT_PACKAGE, generateFontCss, referencedFiles } from '../../scripts/font.mjs'
|
|
6
6
|
|
|
7
7
|
// Vitest runs from the package root, and stubs `?raw` CSS imports to empty.
|
|
8
8
|
// Newlines are normalised: the shipped sheet is checked in, so a Windows
|
|
@@ -13,6 +13,52 @@ const read = (path: string) => readFileSync(join(process.cwd(), path), 'utf8').r
|
|
|
13
13
|
const shipped = read('src/app/style/font.css')
|
|
14
14
|
const upstream = read(join('node_modules', FONT_PACKAGE, 'index.css'))
|
|
15
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
|
+
|
|
16
62
|
describe('the bundled app face', () => {
|
|
17
63
|
it('matches the installed package, so a bump cannot go unnoticed', () => {
|
|
18
64
|
expect(shipped).toBe(generateFontCss(upstream))
|
package/src/app/App.vue
CHANGED
|
@@ -107,6 +107,15 @@ onMounted(() => {
|
|
|
107
107
|
|
|
108
108
|
const commandStore = useCommandStore()
|
|
109
109
|
const layoutStore = useLayoutStore()
|
|
110
|
+
|
|
111
|
+
/*
|
|
112
|
+
* Coerced once, because the pane prop and the heading below have to be exact
|
|
113
|
+
* complements. A stored settings object written before a key existed leaves it
|
|
114
|
+
* `undefined`, which a Boolean prop reads as its `true` default while `!` reads
|
|
115
|
+
* as `false` — so both the real header and its stand-in rendered, giving the
|
|
116
|
+
* page two `h1`s and two banners (#596).
|
|
117
|
+
*/
|
|
118
|
+
const storyListVisible = computed(() => !!layoutStore.settings.storyListVisible)
|
|
110
119
|
</script>
|
|
111
120
|
|
|
112
121
|
<template>
|
|
@@ -152,58 +161,59 @@ const layoutStore = useLayoutStore()
|
|
|
152
161
|
</main>
|
|
153
162
|
</div>
|
|
154
163
|
|
|
155
|
-
<BaseSplitPane
|
|
156
|
-
v-else-if="layoutStore.settings.storyListVisible"
|
|
157
|
-
save-id="main-horiz"
|
|
158
|
-
:min="5"
|
|
159
|
-
:max="50"
|
|
160
|
-
:default-split="15"
|
|
161
|
-
class="h-full"
|
|
162
|
-
>
|
|
163
|
-
<template #first>
|
|
164
|
-
<div class="flex flex-col h-full bg-gray-100 dark:bg-gray-750 __poveste-pane-shadow-from-right">
|
|
165
|
-
<AppHeader class="flex-none" />
|
|
166
|
-
<nav
|
|
167
|
-
aria-label="Stories"
|
|
168
|
-
class="flex-1 flex flex-col min-h-0"
|
|
169
|
-
>
|
|
170
|
-
<StoryList
|
|
171
|
-
:tree="tree"
|
|
172
|
-
:stories="stories"
|
|
173
|
-
class="flex-1"
|
|
174
|
-
/>
|
|
175
|
-
</nav>
|
|
176
|
-
</div>
|
|
177
|
-
</template>
|
|
178
|
-
|
|
179
|
-
<template #last>
|
|
180
|
-
<main class="flex flex-col h-full">
|
|
181
|
-
<TopBar
|
|
182
|
-
@layout="isLayoutOpen = true"
|
|
183
|
-
@search="isSearchOpen = true"
|
|
184
|
-
/>
|
|
185
|
-
<RouterView class="flex-1 min-h-0" />
|
|
186
|
-
</main>
|
|
187
|
-
</template>
|
|
188
|
-
</BaseSplitPane>
|
|
189
164
|
<template v-else>
|
|
190
165
|
<!--
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
carries its own (#310).
|
|
166
|
+
With the story list hidden `AppHeader` goes with it, taking the page's
|
|
167
|
+
only `h1` and its banner. A heading has to exist in every layout, and
|
|
168
|
+
it has to sit inside a landmark, so this stands in for it (#310).
|
|
195
169
|
-->
|
|
196
|
-
<header
|
|
170
|
+
<header
|
|
171
|
+
v-if="!storyListVisible"
|
|
172
|
+
class="sr-only"
|
|
173
|
+
>
|
|
197
174
|
<h1>{{ povesteConfig.theme?.title ?? 'Poveste' }}</h1>
|
|
198
175
|
</header>
|
|
199
176
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
177
|
+
<!--
|
|
178
|
+
One split pane whether or not the story list is showing. Two branches
|
|
179
|
+
each carrying their own `RouterView` moved the whole view in the tree
|
|
180
|
+
when the list was toggled, cold-booting the sandbox under it — the same
|
|
181
|
+
remount #328 removed from sidebar clicks (#596).
|
|
182
|
+
-->
|
|
183
|
+
<BaseSplitPane
|
|
184
|
+
save-id="main-horiz"
|
|
185
|
+
:min="5"
|
|
186
|
+
:max="50"
|
|
187
|
+
:default-split="15"
|
|
188
|
+
:show-first="storyListVisible"
|
|
189
|
+
class="h-full"
|
|
190
|
+
>
|
|
191
|
+
<template #first>
|
|
192
|
+
<div class="flex flex-col h-full bg-gray-100 dark:bg-gray-750 __poveste-pane-shadow-from-right">
|
|
193
|
+
<AppHeader class="flex-none" />
|
|
194
|
+
<nav
|
|
195
|
+
aria-label="Stories"
|
|
196
|
+
class="flex-1 flex flex-col min-h-0"
|
|
197
|
+
>
|
|
198
|
+
<StoryList
|
|
199
|
+
:tree="tree"
|
|
200
|
+
:stories="stories"
|
|
201
|
+
class="flex-1"
|
|
202
|
+
/>
|
|
203
|
+
</nav>
|
|
204
|
+
</div>
|
|
205
|
+
</template>
|
|
206
|
+
|
|
207
|
+
<template #last>
|
|
208
|
+
<main class="flex flex-col h-full">
|
|
209
|
+
<TopBar
|
|
210
|
+
@layout="isLayoutOpen = true"
|
|
211
|
+
@search="isSearchOpen = true"
|
|
212
|
+
/>
|
|
213
|
+
<RouterView class="flex-1 min-h-0" />
|
|
214
|
+
</main>
|
|
215
|
+
</template>
|
|
216
|
+
</BaseSplitPane>
|
|
207
217
|
</template>
|
|
208
218
|
|
|
209
219
|
<LayoutModal
|
|
@@ -48,6 +48,34 @@ const props = defineProps({
|
|
|
48
48
|
type: Boolean,
|
|
49
49
|
default: true,
|
|
50
50
|
},
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether the first pane exists at all.
|
|
54
|
+
*
|
|
55
|
+
* Dropping it here rather than at the call site keeps the other slot at one
|
|
56
|
+
* position in the tree, so a layout that sometimes has nothing to put beside
|
|
57
|
+
* its content does not remount that content when it gains or loses the pane
|
|
58
|
+
* (#328). A `v-if` inside this component leaves a comment placeholder, so the
|
|
59
|
+
* surviving pane keeps its index and Vue patches it rather than rebuilding it;
|
|
60
|
+
* a `v-if` at the call site, choosing between a pane and a bare element, is
|
|
61
|
+
* what does not.
|
|
62
|
+
*/
|
|
63
|
+
showFirst: {
|
|
64
|
+
type: Boolean,
|
|
65
|
+
default: true,
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The same for the last pane — see `showFirst` (#596).
|
|
70
|
+
*
|
|
71
|
+
* At most one of the two is meant to be false. Both false renders an empty
|
|
72
|
+
* box that still persists a split, which is a caller mistake rather than a
|
|
73
|
+
* mode this component supports.
|
|
74
|
+
*/
|
|
75
|
+
showLast: {
|
|
76
|
+
type: Boolean,
|
|
77
|
+
default: true,
|
|
78
|
+
},
|
|
51
79
|
})
|
|
52
80
|
|
|
53
81
|
const emit = defineEmits({
|
|
@@ -66,36 +94,48 @@ watch(() => props.split, (value) => {
|
|
|
66
94
|
immediate: true,
|
|
67
95
|
})
|
|
68
96
|
|
|
69
|
-
|
|
70
|
-
|
|
97
|
+
/*
|
|
98
|
+
* The key follows `saveId`, which callers build from reactive state —
|
|
99
|
+
* `story-main-${placement}` changes when the reader moves the options pane.
|
|
100
|
+
* Reading it once at setup only looked right while a layout change happened to
|
|
101
|
+
* remount this component; hiding a pane instead of replacing it removed that,
|
|
102
|
+
* and a stale key writes one placement's split over the other's (#596).
|
|
103
|
+
*/
|
|
104
|
+
const storageKey = computed(() => props.saveId ? `${SAVE_PREFIX}-split-pane-${props.saveId}` : null)
|
|
105
|
+
|
|
106
|
+
watch(storageKey, (key) => {
|
|
107
|
+
if (!key) {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
71
110
|
|
|
72
|
-
const savedValue = localStorage.getItem(
|
|
111
|
+
const savedValue = localStorage.getItem(key)
|
|
112
|
+
let parsedValue
|
|
73
113
|
if (savedValue != null) {
|
|
74
|
-
let parsedValue
|
|
75
114
|
try {
|
|
76
115
|
parsedValue = JSON.parse(savedValue)
|
|
77
116
|
}
|
|
78
117
|
catch (e) {
|
|
79
118
|
console.error(e)
|
|
80
119
|
}
|
|
81
|
-
|
|
82
|
-
if (typeof parsedValue === 'number') {
|
|
83
|
-
currentSplit.value = parsedValue
|
|
84
|
-
}
|
|
85
120
|
}
|
|
86
121
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
122
|
+
// A key with nothing behind it means this layout has never been sized, so it
|
|
123
|
+
// starts from its own default rather than keeping the previous layout's.
|
|
124
|
+
currentSplit.value = typeof parsedValue === 'number' ? parsedValue : props.defaultSplit
|
|
125
|
+
}, {
|
|
126
|
+
immediate: true,
|
|
127
|
+
})
|
|
90
128
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
}
|
|
129
|
+
watch(currentSplit, (value) => {
|
|
130
|
+
if (storageKey.value) {
|
|
131
|
+
localStorage.setItem(storageKey.value, JSON.stringify(value))
|
|
132
|
+
}
|
|
133
|
+
if (value !== props.split) {
|
|
134
|
+
emit('update:split', value)
|
|
135
|
+
}
|
|
136
|
+
}, {
|
|
137
|
+
immediate: true,
|
|
138
|
+
})
|
|
99
139
|
|
|
100
140
|
const boundSplit = computed(() => {
|
|
101
141
|
if (currentSplit.value < props.min) {
|
|
@@ -109,13 +149,27 @@ const boundSplit = computed(() => {
|
|
|
109
149
|
}
|
|
110
150
|
})
|
|
111
151
|
|
|
112
|
-
const
|
|
113
|
-
[props.orientation === 'landscape' ? 'width' : 'height']: props.fixed ? `${boundSplit.value}px` : `${boundSplit.value}%`,
|
|
114
|
-
}))
|
|
152
|
+
const axis = computed(() => props.orientation === 'landscape' ? 'width' : 'height')
|
|
115
153
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
154
|
+
/** Whether there are two panes to divide, drag between, or share the axis. */
|
|
155
|
+
const hasBothPanes = computed(() => props.showFirst && props.showLast)
|
|
156
|
+
|
|
157
|
+
// A sole pane takes the axis whichever half it is, and loses the `flex-none` /
|
|
158
|
+
// `flex-1` modifiers below that only mean something against a sibling — the
|
|
159
|
+
// `fixed` case included, where a pixel width would pin it beside dead space.
|
|
160
|
+
const leftStyle = computed(() => {
|
|
161
|
+
if (!hasBothPanes.value) {
|
|
162
|
+
return { [axis.value]: '100%' }
|
|
163
|
+
}
|
|
164
|
+
return { [axis.value]: props.fixed ? `${boundSplit.value}px` : `${boundSplit.value}%` }
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const rightStyle = computed(() => {
|
|
168
|
+
if (!hasBothPanes.value) {
|
|
169
|
+
return { [axis.value]: '100%' }
|
|
170
|
+
}
|
|
171
|
+
return { [axis.value]: props.fixed ? null : `${100 - boundSplit.value}%` }
|
|
172
|
+
})
|
|
119
173
|
|
|
120
174
|
const dragging = ref(false)
|
|
121
175
|
let startPosition = 0
|
|
@@ -157,6 +211,15 @@ function dragEnd() {
|
|
|
157
211
|
removeDragListeners()
|
|
158
212
|
}
|
|
159
213
|
|
|
214
|
+
// The dragger goes with the boundary, but the listeners are on `window` and
|
|
215
|
+
// this component now outlives the toggle that removed it — so a drag in flight
|
|
216
|
+
// would otherwise keep writing a split for a divider that is no longer there.
|
|
217
|
+
watch(hasBothPanes, (both) => {
|
|
218
|
+
if (!both && dragging.value) {
|
|
219
|
+
dragEnd()
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
|
|
160
223
|
function removeDragListeners() {
|
|
161
224
|
window.removeEventListener('mousemove', dragMove)
|
|
162
225
|
window.removeEventListener('mouseup', dragEnd)
|
|
@@ -179,17 +242,19 @@ onUnmounted(() => {
|
|
|
179
242
|
}"
|
|
180
243
|
>
|
|
181
244
|
<div
|
|
245
|
+
v-if="showFirst"
|
|
182
246
|
class="relative top-0 left-0 z-20"
|
|
183
247
|
:class="{
|
|
184
248
|
'pointer-events-none': dragging,
|
|
185
|
-
'border-r border-gray-300/30 dark:border-gray-800': orientation === 'landscape' && showDivider,
|
|
186
|
-
'flex-none': fixed,
|
|
249
|
+
'border-r border-gray-300/30 dark:border-gray-800': orientation === 'landscape' && showDivider && hasBothPanes,
|
|
250
|
+
'flex-none': fixed && hasBothPanes,
|
|
187
251
|
}"
|
|
188
252
|
:style="leftStyle"
|
|
189
253
|
>
|
|
190
254
|
<slot name="first" />
|
|
191
255
|
|
|
192
256
|
<div
|
|
257
|
+
v-if="hasBothPanes"
|
|
193
258
|
class="dragger absolute z-100 hover:bg-primary-500/50 transition-colors duration-150 delay-150"
|
|
194
259
|
:class="{
|
|
195
260
|
'top-0 bottom-0 cursor-ew-resize': orientation === 'landscape',
|
|
@@ -201,11 +266,12 @@ onUnmounted(() => {
|
|
|
201
266
|
/>
|
|
202
267
|
</div>
|
|
203
268
|
<div
|
|
269
|
+
v-if="showLast"
|
|
204
270
|
class="relative bottom-0 right-0"
|
|
205
271
|
:class="{
|
|
206
272
|
'pointer-events-none': dragging,
|
|
207
|
-
'border-t border-gray-300/30 dark:border-gray-800': orientation === 'portrait' && showDivider,
|
|
208
|
-
'flex-1': fixed,
|
|
273
|
+
'border-t border-gray-300/30 dark:border-gray-800': orientation === 'portrait' && showDivider && hasBothPanes,
|
|
274
|
+
'flex-1': fixed && hasBothPanes,
|
|
209
275
|
}"
|
|
210
276
|
:style="rightStyle"
|
|
211
277
|
>
|