@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.
Files changed (50) hide show
  1. package/dist/app/style/font.css +11 -7
  2. package/dist/app/style/fonts/LICENSE +93 -0
  3. package/dist/app/style/fonts/noto-sans-display-cyrillic-400-normal.woff2 +0 -0
  4. package/dist/app/style/fonts/noto-sans-display-cyrillic-ext-400-normal.woff2 +0 -0
  5. package/dist/app/style/fonts/noto-sans-display-greek-400-normal.woff2 +0 -0
  6. package/dist/app/style/fonts/noto-sans-display-greek-ext-400-normal.woff2 +0 -0
  7. package/dist/app/style/fonts/noto-sans-display-latin-400-normal.woff2 +0 -0
  8. package/dist/app/style/fonts/noto-sans-display-latin-ext-400-normal.woff2 +0 -0
  9. package/dist/app/style/fonts/noto-sans-display-vietnamese-400-normal.woff2 +0 -0
  10. package/dist/bundled/App.vue_vue_type_script_setup_true_lang.js +19 -19
  11. package/dist/bundled/app.css +10 -10
  12. package/dist/bundled/components/base/BaseSplitPane.js +1 -1
  13. package/dist/bundled/components/base/BaseSplitPane.vue_vue_type_script_setup_true_lang.js +67 -29
  14. package/dist/bundled/components/story/StoryVariantGridItem.vue_vue_type_script_setup_true_lang.js +1 -1
  15. package/dist/bundled/components/story/StoryVariantSingle.js +1 -1
  16. package/dist/bundled/components/story/StoryVariantSingle.vue_vue_type_script_setup_true_lang.js +20 -23
  17. package/dist/bundled/components/story/StoryVariantSinglePreviewNative.vue_vue_type_script_setup_true_lang.js +5 -2
  18. package/dist/bundled/components/story/StoryVariantSinglePreviewRemote.vue_vue_type_script_setup_true_lang.js +4 -0
  19. package/dist/bundled/components/story/StoryVariantSingleView.vue_vue_type_script_setup_true_lang.js +23 -13
  20. package/dist/bundled/components/story/StoryView.vue_vue_type_script_setup_true_lang.js +8 -11
  21. package/dist/bundled/sandbox.js +6 -1
  22. package/dist/bundled/util/events.js +4 -1
  23. package/dist/bundled/util/highlighter.js +11 -4
  24. package/dist/bundled/util/occupant.js +16 -0
  25. package/dist/bundled/util/variant.js +12 -1
  26. package/dist/style.css +4 -4
  27. package/package.json +5 -5
  28. package/src/__tests__/font.spec.ts +48 -2
  29. package/src/app/App.vue +56 -46
  30. package/src/app/components/base/BaseSplitPane.vue +95 -29
  31. package/src/app/components/story/StoryVariantSingle.vue +72 -66
  32. package/src/app/components/story/StoryVariantSinglePreviewNative.vue +13 -2
  33. package/src/app/components/story/StoryVariantSinglePreviewRemote.vue +8 -0
  34. package/src/app/components/story/StoryVariantSingleView.vue +59 -13
  35. package/src/app/components/story/StoryView.vue +17 -17
  36. package/src/app/sandbox.ts +9 -1
  37. package/src/app/style/font.css +11 -7
  38. package/src/app/style/fonts/LICENSE +93 -0
  39. package/src/app/style/fonts/noto-sans-display-cyrillic-400-normal.woff2 +0 -0
  40. package/src/app/style/fonts/noto-sans-display-cyrillic-ext-400-normal.woff2 +0 -0
  41. package/src/app/style/fonts/noto-sans-display-greek-400-normal.woff2 +0 -0
  42. package/src/app/style/fonts/noto-sans-display-greek-ext-400-normal.woff2 +0 -0
  43. package/src/app/style/fonts/noto-sans-display-latin-400-normal.woff2 +0 -0
  44. package/src/app/style/fonts/noto-sans-display-latin-ext-400-normal.woff2 +0 -0
  45. package/src/app/style/fonts/noto-sans-display-vietnamese-400-normal.woff2 +0 -0
  46. package/src/app/util/events.ts +3 -0
  47. package/src/app/util/highlighter.ts +16 -9
  48. package/src/app/util/occupant.ts +13 -0
  49. package/src/app/util/variant.ts +13 -1
  50. package/LICENSE +0 -22
@@ -3,6 +3,7 @@ import { Icon } from '@iconify/vue'
3
3
  import { computed } from 'vue'
4
4
  import { useStoryStore } from '../../stores/story'
5
5
  import { isMobile } from '../../util/responsive'
6
+ import { autoSelectsVariant } from '../../util/variant'
6
7
  import BaseSplitPane from '../base/BaseSplitPane.vue'
7
8
  import StoryVariantListItem from './StoryVariantListItem.vue'
8
9
  import StoryVariantSingleView from './StoryVariantSingleView.vue'
@@ -15,87 +16,92 @@ const storyStore = useStoryStore()
15
16
 
16
17
  const hasSingleVariant = computed(() => (storyStore.currentStory?.variants.length === 1))
17
18
 
18
- const variant = computed(() => storyStore.currentVariant)
19
+ const variant = computed(() => storyStore.currentVariant ?? null)
20
+
21
+ // The preview area exists while a variant is showing or on its way. Dropping it
22
+ // the moment the variant goes null would unmount the preview across the two-step
23
+ // route change, which is the whole thing #328 is about; keeping it when no
24
+ // variant is coming leaves an empty box and, on mobile, a divider under the
25
+ // picker. Same predicate the view below releases its held pair on.
26
+ const showsPreview = computed(() => !!variant.value || autoSelectsVariant(storyStore.currentStory))
19
27
  </script>
20
28
 
21
29
  <template>
22
30
  <div
23
- v-if="hasSingleVariant && variant"
24
- class="poveste-story-variant-single p-2 h-full __poveste-pane-shadow-from-right"
31
+ v-if="isMobile"
32
+ class="divide-y divide-gray-100 dark:divide-gray-800 h-full flex flex-col"
25
33
  >
26
- <StoryVariantSingleView
27
- :variant="variant"
28
- :story="storyStore.currentStory"
29
- />
30
- </div>
31
- <template v-else>
34
+ <a
35
+ v-if="!hasSingleVariant || !variant"
36
+ class="px-6 h-12 hover:text-primary-500 dark:hover:text-primary-400 cursor-pointer flex gap-2 flex-wrap w-full items-center flex-none"
37
+ @click="$emit('openVariantMenu')"
38
+ >
39
+ <template v-if="variant">
40
+ <Icon
41
+ :icon="variant.icon ?? 'carbon:cube'"
42
+ class="w-5 h-5 flex-none"
43
+ :class="{
44
+ 'text-gray-500': !variant.iconColor,
45
+ 'bind-icon-color': variant.iconColor,
46
+ }"
47
+ />
48
+ {{ variant.title }}
49
+ </template>
50
+ <template v-else>
51
+ Select a variant...
52
+ </template>
53
+
54
+ <Icon
55
+ icon="carbon:chevron-sort"
56
+ class="w-5 h-5 shrink-0 ml-auto"
57
+ />
58
+ </a>
32
59
  <div
33
- v-if="isMobile"
34
- class="divide-y divide-gray-100 dark:divide-gray-800 h-full flex flex-col"
60
+ v-if="showsPreview"
61
+ class="poveste-story-variant-single p-2 h-full __poveste-pane-shadow-from-right"
35
62
  >
36
- <a
37
- class="px-6 h-12 hover:text-primary-500 dark:hover:text-primary-400 cursor-pointer flex gap-2 flex-wrap w-full items-center flex-none"
38
- @click="$emit('openVariantMenu')"
39
- >
40
- <template v-if="variant">
41
- <Icon
42
- :icon="variant.icon ?? 'carbon:cube'"
43
- class="w-5 h-5 flex-none"
44
- :class="{
45
- 'text-gray-500': !variant.iconColor,
46
- 'bind-icon-color': variant.iconColor,
47
- }"
48
- />
49
- {{ variant.title }}
50
- </template>
51
- <template v-else>
52
- Select a variant...
53
- </template>
63
+ <StoryVariantSingleView
64
+ :variant="variant"
65
+ :story="storyStore.currentStory"
66
+ />
67
+ </div>
68
+ </div>
54
69
 
55
- <Icon
56
- icon="carbon:chevron-sort"
57
- class="w-5 h-5 shrink-0 ml-auto"
70
+ <!--
71
+ One split pane whether or not there is a list to put in it. A story with a
72
+ single variant hides the first pane instead of taking a different branch, so
73
+ the preview stays at one position in the tree and survives the move between
74
+ a one-variant story and a many-variant one (#328).
75
+ -->
76
+ <BaseSplitPane
77
+ v-else
78
+ save-id="story-single-main-split"
79
+ :min="5"
80
+ :max="40"
81
+ :default-split="17"
82
+ :show-first="!hasSingleVariant"
83
+ >
84
+ <template #first>
85
+ <div class="h-full overflow-y-auto">
86
+ <StoryVariantListItem
87
+ v-for="(v, index) of storyStore.currentStory?.variants ?? []"
88
+ :key="index"
89
+ :variant="v"
58
90
  />
59
- </a>
91
+ </div>
92
+ </template>
93
+ <template #last>
60
94
  <div
61
- v-if="storyStore.currentVariant"
62
- class="p-2 h-full"
95
+ v-if="showsPreview"
96
+ class="poveste-story-variant-single p-2 h-full __poveste-pane-shadow-from-right"
63
97
  >
64
98
  <StoryVariantSingleView
65
- :variant="storyStore.currentVariant"
99
+ :variant="variant"
66
100
  :story="storyStore.currentStory"
67
101
  />
68
102
  </div>
69
- </div>
70
- <BaseSplitPane
71
- v-else
72
- save-id="story-single-main-split"
73
- :min="5"
74
- :max="40"
75
- :default-split="17"
76
- >
77
- <template #first>
78
- <div class="h-full overflow-y-auto">
79
- <StoryVariantListItem
80
- v-for="(v, index) of storyStore.currentStory.variants"
81
- :key="index"
82
- :variant="v"
83
- />
84
- </div>
85
- </template>
86
- <template #last>
87
- <div
88
- v-if="storyStore.currentVariant"
89
- class="p-2 h-full __poveste-pane-shadow-from-right"
90
- >
91
- <StoryVariantSingleView
92
- :variant="storyStore.currentVariant"
93
- :story="storyStore.currentStory"
94
- />
95
- </div>
96
- </template>
97
- </BaseSplitPane>
98
- </template>
103
+ </template>
104
+ </BaseSplitPane>
99
105
  </template>
100
106
 
101
107
  <style scoped>
@@ -1,6 +1,6 @@
1
1
  <script lang="ts" setup>
2
2
  import type { Story, Variant } from '../../types'
3
- import { computed } from 'vue'
3
+ import { computed, watch } from 'vue'
4
4
  import { usePreviewSettingsStore } from '../../stores/preview-settings'
5
5
  import { useStoryStore } from '../../stores/story'
6
6
  import { previewDarkClasses, usePreviewDark } from '../../util/color-scheme'
@@ -16,7 +16,18 @@ const props = defineProps<{
16
16
 
17
17
  const storyStore = useStoryStore()
18
18
 
19
- storyStore.setPreviewReady(props.variant, false)
19
+ // This component now outlives a story change (#328), so the flag is cleared per
20
+ // variant rather than once at setup. Both ends, as `StoryVariantGridItem` does
21
+ // for the same reason: the one arriving has not rendered yet, and the one
22
+ // leaving is no longer rendered anywhere.
23
+ watch(() => props.variant, (variant, previous) => {
24
+ if (previous) {
25
+ storyStore.setPreviewReady(previous, false)
26
+ }
27
+ storyStore.setPreviewReady(variant, false)
28
+ }, {
29
+ immediate: true,
30
+ })
20
31
 
21
32
  function onReady() {
22
33
  storyStore.setPreviewReady(props.variant, true)
@@ -110,6 +110,7 @@ useEventListener(window, 'message', (event) => {
110
110
  updateVariantState(event.data.state)
111
111
  break
112
112
  case EVENT_SEND:
113
+ if (!fromCurrentOccupant(event.data)) break
113
114
  logEvent(event.data.event)
114
115
  break
115
116
  case SANDBOX_READY:
@@ -198,6 +199,9 @@ const isIframeLoaded = ref(false)
198
199
  let retargets = 0
199
200
  // The story whose document the realm holds — booted or retargeted into it.
200
201
  let servedStory = props.story
202
+ // Its variant, kept so the one being replaced can have its `previewReady`
203
+ // cleared: it is no longer rendered anywhere once the realm moves on.
204
+ let servedVariant = props.variant
201
205
 
202
206
  let stopTrackKeyboard: (() => void) | undefined
203
207
  let unmounted = false
@@ -222,6 +226,10 @@ watch(sandboxUrl, (url) => {
222
226
  // A new occupant has agreed to nothing, so neither has this end. Kept as a
223
227
  // new bridge rather than a reset so there is one way to be in that state.
224
228
  bridge = createStateBridge(postState)
229
+ if (servedVariant && servedVariant !== props.variant) {
230
+ storyStore.setPreviewReady(servedVariant, false)
231
+ }
232
+ servedVariant = props.variant
225
233
  storyStore.setPreviewReady(props.variant, false)
226
234
 
227
235
  const reuse = props.story.layout?.isolate !== true && servedStory.layout?.isolate !== true
@@ -1,28 +1,74 @@
1
1
  <script lang="ts" setup>
2
2
  import type { Story, Variant } from '../../types'
3
+ import { shallowRef, watchEffect } from 'vue'
4
+ import { autoSelectsVariant } from '../../util/variant'
3
5
  import StoryVariantSinglePreviewNative from './StoryVariantSinglePreviewNative.vue'
4
6
  import StoryVariantSinglePreviewRemote from './StoryVariantSinglePreviewRemote.vue'
5
7
 
6
- defineProps<{
7
- variant: Variant
8
- story: Story
8
+ const props = defineProps<{
9
+ story: Story | undefined
10
+ variant: Variant | null
9
11
  }>()
12
+
13
+ /*
14
+ * The pair the preview renders, which is not always the pair the route names.
15
+ *
16
+ * Opening a story is two route changes — the story, then the variant
17
+ * `StoryView` picks for it — and `currentVariant` is null in between. The
18
+ * preview used to be mounted under a `v-if` on that variant, so every sidebar
19
+ * click destroyed it and rebuilt it, and the realm reuse in the preview
20
+ * components below was never reached: the component was gone before its
21
+ * `watch(sandboxUrl)` could run (#328).
22
+ *
23
+ * Holding the last pair keeps one instance across the gap. The pair rather than
24
+ * the variant alone because the story changes first, and a new story with the
25
+ * previous story's variant resolves to a sandbox URL that names neither.
26
+ *
27
+ * The hold is released when the new story is not going to select a variant at
28
+ * all, because then there is no gap to bridge — only a reader looking at a
29
+ * variant list. Holding there would leave the story they left running behind a
30
+ * hidden iframe for as long as they stay, timers and all.
31
+ */
32
+ const shown = shallowRef<{ story: Story, variant: Variant } | null>(null)
33
+
34
+ watchEffect(() => {
35
+ // Read outside the branch: while `variant` is null this effect would
36
+ // otherwise not depend on `story`, and an HMR update that replaces the story
37
+ // object would leave the pair below pointing at the discarded one.
38
+ const story = props.story
39
+
40
+ if (story && props.variant) {
41
+ shown.value = { story, variant: props.variant }
42
+ }
43
+ else if (shown.value && shown.value.story !== story && !autoSelectsVariant(story)) {
44
+ shown.value = null
45
+ }
46
+ })
10
47
  </script>
11
48
 
12
49
  <template>
13
50
  <div
14
51
  class="poveste-story-variant-single-view h-full flex flex-col"
52
+ :class="{ invisible: !variant }"
15
53
  data-testid="story-variant-single-view"
16
54
  >
17
- <StoryVariantSinglePreviewNative
18
- v-if="story.layout?.iframe === false"
19
- :story="story"
20
- :variant="variant"
21
- />
22
- <StoryVariantSinglePreviewRemote
23
- v-else
24
- :story="story"
25
- :variant="variant"
26
- />
55
+ <!--
56
+ Hidden rather than unmounted across the gap. What it holds then is the
57
+ story the reader has just left, and showing that is the trap the block
58
+ above is about: the preview is only correct once the route names a
59
+ variant again.
60
+ -->
61
+ <template v-if="shown">
62
+ <StoryVariantSinglePreviewNative
63
+ v-if="shown.story.layout?.iframe === false"
64
+ :story="shown.story"
65
+ :variant="shown.variant"
66
+ />
67
+ <StoryVariantSinglePreviewRemote
68
+ v-else
69
+ :story="shown.story"
70
+ :variant="shown.variant"
71
+ />
72
+ </template>
27
73
  </div>
28
74
  </template>
@@ -4,8 +4,9 @@ import { computed, ref, watch } from 'vue'
4
4
  import { useRoute, useRouter } from 'vue-router'
5
5
  import { useLayoutStore } from '../../stores/layout'
6
6
  import { useStoryStore } from '../../stores/story'
7
-
8
7
  import { isMobile } from '../../util/responsive'
8
+
9
+ import { autoSelectsVariant } from '../../util/variant'
9
10
  import BaseEmpty from '../base/BaseEmpty.vue'
10
11
  import BaseSplitPane from '../base/BaseSplitPane.vue'
11
12
  import StoryDocs from '../panel/StoryDocs.vue'
@@ -15,8 +16,11 @@ import StoryViewer from './StoryViewer.vue'
15
16
  const storyStore = useStoryStore()
16
17
  const layoutStore = useLayoutStore()
17
18
 
19
+ // Coerced: a settings object stored before a key existed leaves it `undefined`,
20
+ // which the pane's Boolean prop would read as its `true` default — the inverse
21
+ // of what the branch this replaced did with the same value (#596).
18
22
  const effectiveStoryOptionsVisible = computed(() =>
19
- storyStore.currentStory?.meta?.storyOptions ?? layoutStore.settings.storyOptionsVisible,
23
+ !!(storyStore.currentStory?.meta?.storyOptions ?? layoutStore.settings.storyOptionsVisible),
20
24
  )
21
25
 
22
26
  const placement = computed(() => layoutStore.settings.storyOptionsPlacement)
@@ -37,15 +41,9 @@ watch(() => storyStore.currentVariant, (value) => {
37
41
  })
38
42
 
39
43
  watch(() => [storyStore.currentStory, storyStore.currentVariant], () => {
40
- if (!storyStore.currentVariant) {
41
- if (storyStore.currentStory?.lastSelectedVariant) {
42
- setVariant(storyStore.currentStory.lastSelectedVariant.id)
43
- return
44
- }
45
-
46
- if (storyStore.currentStory?.variants.length === 1) {
47
- setVariant(storyStore.currentStory.variants[0].id)
48
- }
44
+ if (!storyStore.currentVariant && autoSelectsVariant(storyStore.currentStory)) {
45
+ const variant = storyStore.currentStory.lastSelectedVariant ?? storyStore.currentStory.variants[0]
46
+ setVariant(variant.id)
49
47
  }
50
48
  }, {
51
49
  immediate: true,
@@ -97,12 +95,13 @@ function scrollDocsToTop() {
97
95
  @scroll-top="scrollDocsToTop()"
98
96
  />
99
97
  </div>
100
- <template v-else-if="isMobile">
101
- <StoryViewer />
102
- </template>
103
- <template v-else-if="!effectiveStoryOptionsVisible">
104
- <StoryViewer />
105
- </template>
98
+ <!--
99
+ One split pane whether or not the options pane is showing, and whether or
100
+ not the viewport is mobile — both used to render `StoryViewer` from a
101
+ different branch, which moved it in the tree and cold-booted the sandbox
102
+ under it. `isMobile` is a live media query, so that one fired on a resize
103
+ (#596).
104
+ -->
106
105
  <BaseSplitPane
107
106
  v-else
108
107
  :save-id="`story-main-${placement}`"
@@ -111,6 +110,7 @@ function scrollDocsToTop() {
111
110
  :max="95"
112
111
  :default-split="splitDefaultSplit"
113
112
  :show-divider="false"
113
+ :show-last="!isMobile && effectiveStoryOptionsVisible"
114
114
  class="h-full"
115
115
  >
116
116
  <template #first>
@@ -3,7 +3,7 @@ import { reportStoryError } from '@poveste/shared'
3
3
  import { usePreferredDark } from '@vueuse/core'
4
4
  import { createPinia } from 'pinia'
5
5
  import { files, onUpdate } from 'virtual:$poveste-stories'
6
- import { computed, createApp, h, onMounted, ref, watch } from 'vue'
6
+ import { computed, createApp, h, onMounted, ref, watch, watchEffect } from 'vue'
7
7
  import { parseQuery } from 'vue-router'
8
8
  import GenericMountStory from './components/story/GenericMountStory.vue'
9
9
  import GenericRenderStory from './components/story/GenericRenderStory.vue'
@@ -11,6 +11,7 @@ import { previewDarkClasses, resolvePreviewDark } from './util/color-scheme.js'
11
11
  import { PREVIEW_SETTINGS_REQUEST, PREVIEW_SETTINGS_SYNC, SANDBOX_HEIGHT, SANDBOX_READY, SANDBOX_RETARGET, STATE_SYNC } from './util/const.js'
12
12
  import { isDark } from './util/dark.js'
13
13
  import { mapFile } from './util/mapping'
14
+ import { occupant } from './util/occupant.js'
14
15
  import { applyPreviewSettings, loadStoredPreviewSettings, receivedSettings } from './util/preview-settings.js'
15
16
  import { createStateBridge } from './util/state-bridge.js'
16
17
  import { toRawDeep } from './util/state.js'
@@ -24,6 +25,13 @@ const query = parseQuery(window.location.search)
24
25
  const storyId = ref(typeof query.storyId === 'string' ? query.storyId : null)
25
26
  const variantId = ref(typeof query.variantId === 'string' ? query.variantId : null)
26
27
 
28
+ // Kept in step so `logEvent` can name the occupant it is speaking for; it posts
29
+ // from the story's own code and cannot see these refs.
30
+ watchEffect(() => {
31
+ occupant.storyId = storyId.value
32
+ occupant.variantId = variantId.value
33
+ })
34
+
27
35
  // `files` is `[]` until the dev server's first collect lands. The app's iframe
28
36
  // never sees that — it mounts after — but a sandbox opened on its own does, and
29
37
  // the list only arrives through `onUpdate`.
@@ -4,6 +4,10 @@
4
4
  * in each book. Copied rather than imported because the package pairs the two
5
5
  * formats in every `src`; `font.spec.ts` fails if a bump moves the subsets.
6
6
  *
7
+ * Vendored rather than resolved from the package, which publishes the family
8
+ * whole: 5.9 MB installed to ship 108 KB (#306). OFL-1.1, so `fonts/LICENSE`
9
+ * travels with the files.
10
+ *
7
11
  * Regenerate: node packages/poveste-app/scripts/font.mjs
8
12
  */
9
13
 
@@ -13,7 +17,7 @@
13
17
  font-style: normal;
14
18
  font-display: swap;
15
19
  font-weight: 400;
16
- src: url('@fontsource/noto-sans-display/files/noto-sans-display-cyrillic-ext-400-normal.woff2') format('woff2');
20
+ src: url('./fonts/noto-sans-display-cyrillic-ext-400-normal.woff2') format('woff2');
17
21
  unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
18
22
  }
19
23
 
@@ -23,7 +27,7 @@
23
27
  font-style: normal;
24
28
  font-display: swap;
25
29
  font-weight: 400;
26
- src: url('@fontsource/noto-sans-display/files/noto-sans-display-cyrillic-400-normal.woff2') format('woff2');
30
+ src: url('./fonts/noto-sans-display-cyrillic-400-normal.woff2') format('woff2');
27
31
  unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
28
32
  }
29
33
 
@@ -33,7 +37,7 @@
33
37
  font-style: normal;
34
38
  font-display: swap;
35
39
  font-weight: 400;
36
- src: url('@fontsource/noto-sans-display/files/noto-sans-display-greek-ext-400-normal.woff2') format('woff2');
40
+ src: url('./fonts/noto-sans-display-greek-ext-400-normal.woff2') format('woff2');
37
41
  unicode-range: U+1F00-1FFF;
38
42
  }
39
43
 
@@ -43,7 +47,7 @@
43
47
  font-style: normal;
44
48
  font-display: swap;
45
49
  font-weight: 400;
46
- src: url('@fontsource/noto-sans-display/files/noto-sans-display-greek-400-normal.woff2') format('woff2');
50
+ src: url('./fonts/noto-sans-display-greek-400-normal.woff2') format('woff2');
47
51
  unicode-range: U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;
48
52
  }
49
53
 
@@ -53,7 +57,7 @@
53
57
  font-style: normal;
54
58
  font-display: swap;
55
59
  font-weight: 400;
56
- src: url('@fontsource/noto-sans-display/files/noto-sans-display-vietnamese-400-normal.woff2') format('woff2');
60
+ src: url('./fonts/noto-sans-display-vietnamese-400-normal.woff2') format('woff2');
57
61
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
58
62
  }
59
63
 
@@ -63,7 +67,7 @@
63
67
  font-style: normal;
64
68
  font-display: swap;
65
69
  font-weight: 400;
66
- src: url('@fontsource/noto-sans-display/files/noto-sans-display-latin-ext-400-normal.woff2') format('woff2');
70
+ src: url('./fonts/noto-sans-display-latin-ext-400-normal.woff2') format('woff2');
67
71
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
68
72
  }
69
73
 
@@ -73,6 +77,6 @@
73
77
  font-style: normal;
74
78
  font-display: swap;
75
79
  font-weight: 400;
76
- src: url('@fontsource/noto-sans-display/files/noto-sans-display-latin-400-normal.woff2') format('woff2');
80
+ src: url('./fonts/noto-sans-display-latin-400-normal.woff2') format('woff2');
77
81
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
78
82
  }
@@ -0,0 +1,93 @@
1
+ Copyright 2015 Google Inc. All Rights Reserved. NotoSansDisplay-Italic[wdth,wght].ttf: Copyright 2015 Google Inc. All Rights Reserved.
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -1,4 +1,5 @@
1
1
  import { EVENT_SEND } from './const'
2
+ import { occupant } from './occupant'
2
3
 
3
4
  export async function logEvent(name: string, argument) {
4
5
  const event = {
@@ -9,6 +10,8 @@ export async function logEvent(name: string, argument) {
9
10
  window.parent?.postMessage({
10
11
  type: EVENT_SEND,
11
12
  event,
13
+ storyId: occupant.storyId,
14
+ variantId: occupant.variantId,
12
15
  })
13
16
  }
14
17
  else {
@@ -1,5 +1,6 @@
1
- import type { Highlighter } from 'shiki'
2
- import { createHighlighter } from 'shiki'
1
+ import type { HighlighterCore } from 'shiki/core'
2
+ import { createHighlighterCore } from 'shiki/core'
3
+ import { createOnigurumaEngine } from 'shiki/engine/oniguruma'
3
4
 
4
5
  /**
5
6
  * Shiki is meant to be used as a singleton: each instance carries its own WASM
@@ -9,18 +10,24 @@ import { createHighlighter } from 'shiki'
9
10
  *
10
11
  * The langs and themes are fixed, so one shared instance is always correct.
11
12
  */
12
- let highlighterPromise: Promise<Highlighter> | undefined
13
+ let highlighterPromise: Promise<HighlighterCore> | undefined
13
14
 
14
- export function getHighlighter(): Promise<Highlighter> {
15
- highlighterPromise ??= createHighlighter({
15
+ /**
16
+ * `shiki/core`, not `shiki`: the latter is the full-bundle entry and ships every
17
+ * grammar and theme whatever the options ask for — 10 MB a book (#304). The
18
+ * dynamic imports are the interface, not a lazy-loading choice.
19
+ */
20
+ export function getHighlighter(): Promise<HighlighterCore> {
21
+ highlighterPromise ??= createHighlighterCore({
16
22
  langs: [
17
- 'html',
18
- 'jsx',
23
+ import('shiki/langs/html.mjs'),
24
+ import('shiki/langs/jsx.mjs'),
19
25
  ],
20
26
  themes: [
21
- 'github-light',
22
- 'github-dark',
27
+ import('shiki/themes/github-light.mjs'),
28
+ import('shiki/themes/github-dark.mjs'),
23
29
  ],
30
+ engine: createOnigurumaEngine(import('shiki/wasm')),
24
31
  }).catch((e) => {
25
32
  // Don't let a cached rejection disable highlighting for the rest of the
26
33
  // session — drop it so the next mount can try again.