@lincyaw/web 1.0.0-rc.0 → 1.0.0-rc.2

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 (58) hide show
  1. package/README.md +33 -4
  2. package/package.json +20 -19
  3. package/src/app/base.css +117 -0
  4. package/src/app/block-playback.ts +81 -0
  5. package/src/app/block-report.ts +104 -0
  6. package/src/app/document-modules.ts +30 -0
  7. package/src/app/env.d.ts +12 -0
  8. package/src/app/main.ts +198 -0
  9. package/src/app/page-app.ts +166 -0
  10. package/src/app/router.ts +109 -0
  11. package/src/app/server.ts +32 -0
  12. package/src/app/site/home.tsx +117 -0
  13. package/src/app/site/layout.tsx +92 -0
  14. package/src/app/site/search.tsx +22 -0
  15. package/src/app/site.css +1450 -0
  16. package/src/client/document.css +172 -0
  17. package/src/client/host.ts +22 -94
  18. package/src/client/preview-frame.ts +152 -0
  19. package/src/client/runtime.ts +159 -0
  20. package/src/domain/app-config.ts +91 -0
  21. package/src/domain/config.ts +17 -9
  22. package/src/domain/dev-updates.ts +12 -0
  23. package/src/domain/index.ts +17 -7
  24. package/src/domain/landing.ts +83 -0
  25. package/src/domain/links.ts +23 -18
  26. package/src/domain/messages.ts +74 -63
  27. package/src/domain/pages.ts +23 -41
  28. package/src/domain/themes.ts +59 -0
  29. package/src/domain/workspace-home.ts +12 -0
  30. package/src/node/client-modules.ts +92 -0
  31. package/src/node/document-bundle.ts +227 -0
  32. package/src/node/landing-template.ts +34 -0
  33. package/src/node/landing.ts +82 -0
  34. package/src/node/offline.ts +34 -11
  35. package/src/node/publish.ts +83 -15
  36. package/src/node/renderer.ts +78 -117
  37. package/src/node/site-assets.ts +17 -0
  38. package/src/node/site-build.ts +304 -0
  39. package/src/node/site-files.ts +87 -0
  40. package/src/node/site-model.ts +337 -0
  41. package/src/node/site-page.ts +82 -0
  42. package/src/node/site-server.ts +822 -0
  43. package/src/node/slides.ts +135 -0
  44. package/src/node/viewer-app.ts +504 -0
  45. package/src/node/viewer-build.ts +38 -0
  46. package/custom-nav-controls.vue +0 -17
  47. package/src/client/env.d.ts +0 -1
  48. package/src/client/home.ts +0 -182
  49. package/src/client/index.ts +0 -4
  50. package/src/client/page.ts +0 -38
  51. package/src/client/preview.ts +0 -71
  52. package/src/client/style.css +0 -427
  53. package/src/client/theme.ts +0 -322
  54. package/src/node/addons.ts +0 -76
  55. package/src/node/site.ts +0 -349
  56. package/src/node/sites.ts +0 -531
  57. package/src/node/vite.ts +0 -166
  58. package/src/node/vitepress.ts +0 -72
@@ -1,17 +0,0 @@
1
- <script setup lang="ts">
2
- import { useMessages } from '@lincyaw/host'
3
- import { configs, useNav } from '@slidev/client'
4
- import { computed } from 'vue'
5
- import { formOptions, WEB_MESSAGES, webUrl } from './src/domain/index.ts'
6
-
7
- const t = useMessages(WEB_MESSAGES)
8
- const { currentSlideNo, clicks } = useNav()
9
- const base = computed(() => formOptions(configs.presmium).webUrl ?? new URL('../web', new URL(import.meta.env.BASE_URL, location.href)).href)
10
- const href = computed(() => webUrl(base.value, currentSlideNo.value, clicks.value))
11
- </script>
12
-
13
- <template>
14
- <a class="slidev-icon-btn pm-read-web" :href="href" :title="t.readOnWeb" :aria-label="t.readOnWeb">
15
- <div class="i-carbon:document" />
16
- </a>
17
- </template>
@@ -1 +0,0 @@
1
- /// <reference types="vitepress/client" />
@@ -1,182 +0,0 @@
1
- import { createSharedStates, currentLocale, installHost, PmEmpty, PmIcon, pluginsOf, PmToolbar, presmiumReport, presmiumService, registerPresmiumPlugin, startHost, useMessages } from '@lincyaw/host'
2
- import type { HostAdapter, Navigation } from '@lincyaw/host'
3
- import { FINAL_STEP } from '@lincyaw/viewer'
4
- import type { Theme } from 'vitepress'
5
- import { inBrowser, useData } from 'vitepress'
6
- import DefaultTheme from 'vitepress/theme-without-fonts'
7
- import { computed, defineComponent, h, onBeforeUnmount, onMounted, readonly, ref } from 'vue'
8
- import { samePage, WEB_MESSAGES } from '../domain/index.ts'
9
- import './style.css'
10
-
11
- export interface SiteHomeInput {
12
- plugins: (() => Promise<unknown>)[]
13
- }
14
-
15
- interface HomeDocument {
16
- dir: string
17
- title: string
18
- lang?: string
19
- tags?: string[]
20
- href: string
21
- description?: string
22
- pages?: number
23
- problems?: { errors: number, warnings: number }
24
- }
25
-
26
- const query = ref('')
27
-
28
- function useDocuments() {
29
- const { theme } = useData()
30
- return computed<HomeDocument[]>(() => theme.value.presmium?.documents ?? [])
31
- }
32
-
33
- function card(doc: HomeDocument, t: typeof WEB_MESSAGES[keyof typeof WEB_MESSAGES]) {
34
- const badge = (text: string, tone = '') => h('span', { class: `pm-ui-badge${tone ? ` pm-ui-badge--${tone}` : ''}` }, text)
35
- const problems = doc.problems
36
- return h('li', { 'class': 'pm-site-home__document pm-ui-card pm-ui-card--interactive', 'data-document': doc.dir }, [
37
- h('a', { class: 'pm-site-home__link', href: doc.href, ...doc.lang ? { lang: doc.lang } : {} }, doc.title),
38
- doc.description ? h('p', { class: 'pm-site-home__description' }, doc.description) : null,
39
- h('div', { class: 'pm-ui-badges' }, [
40
- h('span', { class: 'pm-site-home__dir pm-ui-badge' }, `${doc.dir}/`),
41
- doc.pages ? badge(t.pages(doc.pages)) : null,
42
- doc.lang ? badge(doc.lang) : null,
43
- ...(doc.tags ?? []).map(name => h('span', { class: 'pm-site-home__badge pm-ui-badge' }, name)),
44
- problems ? problems.errors ? badge(t.errors(problems.errors), 'danger') : problems.warnings ? badge(t.warnings(problems.warnings), 'warn') : badge(t.checked, 'ok') : null,
45
- ]),
46
- ])
47
- }
48
-
49
- const SearchBox = defineComponent({
50
- name: 'PresmiumSiteSearch',
51
- setup() {
52
- const t = useMessages(WEB_MESSAGES)
53
- const input = ref<HTMLInputElement>()
54
- const onKey = (event: KeyboardEvent) => {
55
- if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') {
56
- event.preventDefault()
57
- input.value?.focus()
58
- }
59
- }
60
- onMounted(() => window.addEventListener('keydown', onKey))
61
- onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
62
- return () => h('label', { class: 'pm-ui-search pm-site-home__search-box' }, [
63
- h(PmIcon, { name: 'search', size: 15, strokeWidth: 1.8 }),
64
- h('input', {
65
- 'ref': input,
66
- 'class': 'pm-site-home__search',
67
- 'type': 'search',
68
- 'value': query.value,
69
- 'placeholder': t.value.searchDocuments,
70
- 'aria-label': t.value.searchDocuments,
71
- 'onInput': (event: Event) => query.value = (event.target as HTMLInputElement).value,
72
- }),
73
- h('kbd', { class: 'pm-ui-kbd' }, 'Ctrl K'),
74
- ])
75
- },
76
- })
77
-
78
- const SiteHome = defineComponent({
79
- name: 'PresmiumSiteHome',
80
- setup() {
81
- const { site } = useData()
82
- const documents = useDocuments()
83
- const tag = ref<string>()
84
- const t = computed(() => WEB_MESSAGES[currentLocale()])
85
- const tags = computed(() => [...new Set(documents.value.flatMap(doc => doc.tags ?? []))].sort())
86
- const shown = computed(() => {
87
- const words = query.value.trim().toLowerCase().split(/\s+/).filter(Boolean)
88
- return documents.value.filter(doc => (!tag.value || doc.tags?.includes(tag.value))
89
- && words.every(word => `${doc.title} ${doc.dir} ${doc.description ?? ''} ${(doc.tags ?? []).join(' ')}`.toLowerCase().includes(word)))
90
- })
91
- return () => h('div', { class: 'pm-site-home pm-ui' }, [
92
- h('h1', { class: 'pm-site-home__title' }, site.value.title),
93
- h('section', { 'class': 'pm-site-home__section', 'aria-labelledby': 'pm-site-home-documents' }, [
94
- h('div', { class: 'pm-site-home__filters' }, [
95
- h('h2', { id: 'pm-site-home-documents', class: 'pm-ui-heading' }, t.value.documentsHeading),
96
- h(SearchBox),
97
- tags.value.length > 0
98
- ? h('div', { 'class': 'pm-site-home__tags pm-ui-segmented', 'role': 'group', 'aria-label': t.value.tags }, [
99
- h('button', { 'type': 'button', 'class': 'pm-site-home__tag pm-ui-segmented__option', 'aria-pressed': tag.value === undefined, 'onClick': () => tag.value = undefined }, t.value.allTags),
100
- ...tags.value.map(name => h('button', { 'type': 'button', 'class': 'pm-site-home__tag pm-ui-segmented__option', 'aria-pressed': tag.value === name, 'onClick': () => tag.value = tag.value === name ? undefined : name }, name)),
101
- ])
102
- : null,
103
- ]),
104
- h('div', { class: 'pm-site-home__grid' }, [
105
- shown.value.length === 0
106
- ? h(PmEmpty, { class: 'pm-site-home__empty', icon: 'document' }, () => t.value.noDocuments)
107
- : h('ul', { class: 'pm-site-home__documents', role: 'list' }, shown.value.map(doc => card(doc, t.value))),
108
- ]),
109
- ]),
110
- ])
111
- },
112
- })
113
-
114
- const SiteRedirect = defineComponent({
115
- name: 'PresmiumSiteRedirect',
116
- setup() {
117
- const { theme, page } = useData()
118
- const href = computed(() => theme.value.presmium?.redirect as string | undefined)
119
- onMounted(() => {
120
- if (href.value) location.replace(href.value)
121
- })
122
- return () => h('p', { class: 'pm-site-home' }, [h('a', { class: 'pm-site-home__link', href: href.value }, page.value.title)])
123
- },
124
- })
125
-
126
- function homeHost(lang: string, sharedState: HostAdapter['useSharedState']): HostAdapter {
127
- const cursor = readonly(ref({ slide: 1, step: 0 }))
128
- return {
129
- mode: 'document',
130
- lang,
131
- useCursor: () => cursor,
132
- useStep: () => readonly(ref(FINAL_STEP)),
133
- useViewRole: () => readonly(ref('reader' as const)),
134
- useStaticRender: () => readonly(ref(false)),
135
- useSectionConfig: () => readonly(ref({})),
136
- useSharedState: sharedState,
137
- getService: presmiumService,
138
- startReport: presmiumReport,
139
- useDarkMode: () => useData().isDark,
140
- }
141
- }
142
-
143
- const Layout = defineComponent({
144
- name: 'PresmiumSiteHomeLayout',
145
- setup() {
146
- return () => h(DefaultTheme.Layout, null, {
147
- 'nav-bar-content-after': () => h(PmToolbar, { class: 'pm-toolbar--bar' }),
148
- 'nav-screen-content-after': () => h(PmToolbar, { class: 'pm-toolbar--screen' }),
149
- })
150
- },
151
- })
152
-
153
- export function createSiteHomeTheme(input: SiteHomeInput): Theme {
154
- return {
155
- extends: DefaultTheme,
156
- Layout,
157
- enhanceApp({ app, router, siteData }) {
158
- app.component('PresmiumSiteHome', SiteHome)
159
- app.component('PresmiumSiteRedirect', SiteRedirect)
160
- router.onBeforeRouteChange = (to) => {
161
- if (!inBrowser) return
162
- const url = new URL(to, location.href)
163
- const { base } = siteData.value
164
- const own = [base, `${base}index.html`, siteData.value.themeConfig.presmium?.list]
165
- if (url.origin !== location.origin || samePage(url.pathname, location.pathname) || own.includes(url.pathname)) return
166
- location.assign(url.href)
167
- return false
168
- }
169
- const shared = createSharedStates(`${siteData.value.title} (home)`)
170
- installHost(app, homeHost(siteData.value.lang, shared.sharedState))
171
- if (!inBrowser) return
172
- const navigation: Navigation = { position: computed(() => ({ slide: 1, step: 0 })), go: () => {} }
173
- void Promise.all(input.plugins.map(load => load().catch((error: unknown) => {
174
- console.error('[presmium] a plugin module failed to load', error)
175
- return {}
176
- }))).then((modules) => {
177
- modules.flatMap(module => pluginsOf(module, {})).forEach(registerPresmiumPlugin)
178
- return startHost({ mode: 'document', config: {}, navigation, transports: shared.transports })
179
- })
180
- },
181
- }
182
- }
@@ -1,4 +0,0 @@
1
- export { createViewerHost, createViewerNavigation } from './host.ts'
2
- export { PresmiumPage, ViewMenu } from './page.ts'
3
- export { createPresmiumTheme } from './theme.ts'
4
- export type { ThemeInput } from './theme.ts'
@@ -1,38 +0,0 @@
1
- import { useMessages } from '@lincyaw/host'
2
- import { listViews, useViewer, ViewHost } from '@lincyaw/viewer'
3
- import { useData } from 'vitepress'
4
- import type { ComputedRef, InjectionKey } from 'vue'
5
- import { computed, defineComponent, h, inject } from 'vue'
6
- import { pageOf, toPages, WEB_MESSAGES } from '../domain/index.ts'
7
- import type { WebPage } from '../domain/index.ts'
8
-
9
- export const PAGES: InjectionKey<{ pages: ComputedRef<WebPage[]>, prefix: ComputedRef<string> }> = Symbol('presmium.pages')
10
-
11
- export const PresmiumPage = defineComponent({
12
- name: 'PresmiumPage',
13
- setup() {
14
- const { page } = useData()
15
- const viewer = useViewer()
16
- const known = inject(PAGES, undefined)
17
- const sections = computed(() => {
18
- const prefix = known?.prefix.value.slice(1) ?? ''
19
- const path = prefix && page.value.relativePath.startsWith(`${prefix}/`) ? page.value.relativePath.slice(prefix.length + 1) : page.value.relativePath
20
- return pageOf(known?.pages.value ?? toPages(viewer.doc.value), path)?.sections ?? []
21
- })
22
- return () => h(ViewHost, { sections: sections.value })
23
- },
24
- })
25
-
26
- export const ViewMenu = defineComponent({
27
- name: 'PresmiumViewMenu',
28
- setup() {
29
- const viewer = useViewer()
30
- const t = useMessages(WEB_MESSAGES)
31
- return () => h('select', {
32
- 'class': 'pm-view-menu',
33
- 'aria-label': t.value.viewMenu,
34
- 'value': viewer.view.value,
35
- 'onChange': (event: Event) => viewer.setView((event.target as HTMLSelectElement).value),
36
- }, listViews().filter(view => !view.hidden || view.id === viewer.view.value).map(view => h('option', { value: view.id }, view.label)))
37
- },
38
- })
@@ -1,71 +0,0 @@
1
- import { componentErrors, presmiumReport } from '@lincyaw/host'
2
- import type { NavigationPosition } from '@lincyaw/host'
3
- import { irBlocks, PREVIEW_REVISION, startPreviewAgent, stepKeys, stepTrail } from '@lincyaw/kernel'
4
- import type { PreviewAgent } from '@lincyaw/kernel'
5
- import type { FollowAnchor, Viewer } from '@lincyaw/viewer'
6
- import type { Ref } from 'vue'
7
- import { nextTick, watch } from 'vue'
8
- import { createViewerNavigation } from './host.ts'
9
-
10
- export interface WebPreviewOptions {
11
- origins: string[]
12
- document: string
13
- }
14
-
15
- export interface WebPreviewPage {
16
- viewer: Viewer
17
- dark: Ref<boolean>
18
- }
19
-
20
- export function startWebPreview(page: WebPreviewPage, options: WebPreviewOptions): void {
21
- const { viewer, dark } = page
22
- const navigation = createViewerNavigation(viewer)
23
- const revision = () => {
24
- const value = viewer.doc.value.meta[PREVIEW_REVISION]
25
- return typeof value === 'string' ? value : undefined
26
- }
27
- const blockStep = (section: string, block: string) => {
28
- const found = viewer.section(section)
29
- return found ? [...irBlocks(found.blocks)].find(b => b.id === block)?.at : undefined
30
- }
31
- const agent: PreviewAgent | undefined = startPreviewAgent({
32
- renderer: 'web',
33
- document: options.document,
34
- origins: options.origins,
35
- revision,
36
- position() {
37
- const { section, step, trail, view } = navigation.position.value as NavigationPosition & FollowAnchor
38
- return section === undefined ? undefined : { section, step, trail: trail ?? [], ...view ? { view } : {} }
39
- },
40
- go: ({ section, step, trail, view }) => navigation.go({ slide: 0, step: step ?? 0, section, ...trail ? { trail } : {}, ...view ? { view } : {} }),
41
- reveal: ({ section, block }) => viewer.show({ section, step: (block && blockStep(section, block)) || 0 }),
42
- setDark(value) {
43
- dark.value = value
44
- },
45
- stepOf({ section, block }) {
46
- const step = blockStep(section, block)
47
- const found = viewer.section(section)
48
- return step === undefined || !found ? undefined : { step, trail: stepTrail(stepKeys(found), step) }
49
- },
50
- })
51
- if (!agent) return
52
- watch(() => navigation.position.value, () => agent.moved())
53
- watch(revision, () => void nextTick(() => requestAnimationFrame(() => agent.updated())))
54
- watchProblems(agent)
55
- }
56
-
57
- function watchProblems(agent: PreviewAgent): void {
58
- const told = new Set<string>()
59
- watch(() => [presmiumReport()?.plugins ?? [], componentErrors()] as const, ([plugins, errors]) => {
60
- const problems = [
61
- ...plugins.filter(p => p.status === 'failed' || p.status === 'disabled').map(p => ({ level: p.status === 'failed' ? 'error' as const : 'warning' as const, plugin: p.id, message: p.reason ?? p.status })),
62
- ...errors.map(e => ({ level: 'error' as const, plugin: e.component, message: e.message })),
63
- ]
64
- for (const problem of problems) {
65
- const key = JSON.stringify(problem)
66
- if (told.has(key)) continue
67
- told.add(key)
68
- agent.report(problem)
69
- }
70
- }, { immediate: true })
71
- }