@lincyaw/viewer 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 (70) hide show
  1. package/README.md +6 -4
  2. package/package.json +20 -8
  3. package/src/client/block.ts +70 -0
  4. package/src/client/code/code-block.ts +54 -0
  5. package/src/client/code/code-editor.ts +73 -0
  6. package/src/client/code/code-view.ts +22 -0
  7. package/src/{highlight.ts → client/code/highlight.ts} +2 -2
  8. package/src/{outputs.ts → client/code/outputs.ts} +23 -36
  9. package/src/client/content-marks.ts +15 -0
  10. package/src/client/element.ts +27 -0
  11. package/src/client/index.ts +40 -0
  12. package/src/client/layout.ts +22 -0
  13. package/src/{markdown.ts → client/markdown/handlers.ts} +22 -116
  14. package/src/client/markdown/hast.ts +20 -0
  15. package/src/client/markdown/layout-handlers.ts +57 -0
  16. package/src/client/markdown/math.ts +10 -0
  17. package/src/client/markdown/mermaid.ts +75 -0
  18. package/src/client/markdown/render.ts +79 -0
  19. package/src/client/markdown/role-mark.ts +9 -0
  20. package/src/client/markdown/tables.ts +49 -0
  21. package/src/client/markdown/tabs.ts +52 -0
  22. package/src/client/markdown/tree-cache.ts +77 -0
  23. package/src/client/operations.ts +9 -0
  24. package/src/client/registry.ts +21 -0
  25. package/src/client/section-context.ts +5 -0
  26. package/src/client/section.ts +30 -0
  27. package/src/client/slidev-compat/click-directives.ts +16 -0
  28. package/src/client/slidev-compat/slide-globals.ts +3 -0
  29. package/src/{style.css → client/style.css} +306 -81
  30. package/src/client/viewer-messages.ts +7 -0
  31. package/src/{plugins.ts → client/viewer-plugins.ts} +3 -2
  32. package/src/client/viewer-state.ts +47 -0
  33. package/src/client/views/actions.ts +52 -0
  34. package/src/client/views/controls.css +44 -0
  35. package/src/client/views/document.ts +12 -0
  36. package/src/client/views/outlets.ts +37 -0
  37. package/src/client/views/overlay.ts +16 -0
  38. package/src/client/views/present.ts +132 -0
  39. package/src/client/views/presenter.ts +76 -0
  40. package/src/client/views/scrolly.ts +4 -0
  41. package/src/client/views/slide.css +8 -0
  42. package/src/client/views/slide.ts +57 -0
  43. package/src/domain/clicks.ts +1 -1
  44. package/src/domain/code-width.ts +11 -0
  45. package/src/domain/document-content.ts +6 -0
  46. package/src/domain/{text.ts → footnotes.ts} +0 -12
  47. package/src/domain/index.ts +5 -4
  48. package/src/domain/live.ts +11 -10
  49. package/src/domain/messages.ts +54 -30
  50. package/src/domain/origin.ts +17 -32
  51. package/src/domain/outputs.ts +27 -0
  52. package/src/domain/steps.ts +2 -1
  53. package/src/block.ts +0 -122
  54. package/src/cards.ts +0 -130
  55. package/src/clicks.ts +0 -124
  56. package/src/code.ts +0 -235
  57. package/src/context.ts +0 -16
  58. package/src/dark.ts +0 -5
  59. package/src/domain/follow.ts +0 -32
  60. package/src/globals.ts +0 -66
  61. package/src/i18n.ts +0 -7
  62. package/src/index.ts +0 -55
  63. package/src/operations.ts +0 -28
  64. package/src/present.ts +0 -327
  65. package/src/registry.ts +0 -81
  66. package/src/rich.ts +0 -155
  67. package/src/section.ts +0 -209
  68. package/src/viewer.ts +0 -116
  69. package/src/views.ts +0 -101
  70. /package/src/{env.d.ts → client/env.d.ts} +0 -0
@@ -3,40 +3,52 @@ import type { Locale } from '@lincyaw/kernel/contracts'
3
3
  const zhStep = (step: number, steps: number) => `第 ${step + 1} 步,共 ${steps + 1} 步`
4
4
 
5
5
  const zh = {
6
- views: { document: '文档', scrolly: '滚动叙事', present: '放映', presenter: '演讲者' },
6
+ contentRoles: { visual: '上屏内容', narration: '讲稿 · 字幕', reading: '仅供阅读' },
7
+ views: { document: '网页', scrolly: '滚动叙事', present: '放映', presenter: '演示者视图' },
7
8
  present: {
8
9
  region: '放映',
10
+ jumpStep: '跳转步骤',
11
+ captions: '字幕(按步骤)',
12
+ captionsHint: '字幕随步骤切换',
13
+ noCaptions: '本步没有讲稿',
14
+ step: (step: number) => `第 ${step} 步`,
9
15
  counter: (index: number, total: number, step: number, steps: number) => `${index} / ${total}${steps > 0 ? ` · ${zhStep(step, steps)}` : ''}`,
10
16
  prev: '上一步',
11
17
  next: '下一步',
12
18
  overview: '总览',
13
- presenter: '演讲者窗口',
19
+ presenter: '演示者视图',
20
+ refresh: '更新',
14
21
  fullscreen: '全屏',
15
- exit: '退出放映',
22
+ exitFullscreen: '退出全屏',
23
+ exit: '结束放映',
16
24
  },
17
25
  presenter: {
18
26
  step: (step: number, steps: number) => (steps > 0 ? zhStep(step, steps) : ''),
19
27
  timer: '计时',
20
28
  restart: '重新计时',
21
- notes: '讲解',
22
- noNotes: '本节没有讲解',
29
+ notes: '讲稿',
30
+ noNotes: '本页没有讲稿',
23
31
  upcoming: '下一步',
24
32
  end: '已到结尾',
25
- nextSection: (title: string) => `下一节:${title}`,
33
+ nextSection: (title: string) => `下一页:${title}`,
26
34
  nextStep: (step: number) => `第 ${step + 1} 步`,
27
35
  },
28
36
  section: {
29
37
  anchor: (title: string) => `链接到“${title}”`,
30
- openSource: '在编辑器中打开源码',
31
- sourceTitle: (where: string) => `${where}(Alt+单击任一块,打开该块的源码)`,
38
+ openSource: '在编辑器中打开',
39
+ sourceTitle: (where: string) => `${where}(按住 Alt 单击任一内容块,在编辑器中打开)`,
32
40
  },
33
41
  code: {
34
- executorFailed: (id: string, message: string) => `执行器 ${id} 出错:${message}`,
42
+ actions: '代码操作',
43
+ executorFailed: (_id: string, message: string) => `无法运行代码:${message}`,
35
44
  run: '运行',
36
- rerun: '重跑',
37
- running: '运行中…',
38
- unavailable: '执行服务不可用',
39
- resetSession: '重置会话',
45
+ rerun: '重新运行',
46
+ running: '正在运行…',
47
+ unavailable: '此处无法运行代码',
48
+ resetRunSession: '重置会话',
49
+ edit: '编辑代码',
50
+ doneEditing: '完成编辑',
51
+ revert: '还原代码',
40
52
  },
41
53
  markdown: {
42
54
  admonitions: {
@@ -53,46 +65,58 @@ const zh = {
53
65
  } as Record<string, string>,
54
66
  footnote: (label: string) => `脚注 ${label}`,
55
67
  },
56
- missingComponent: (name: string) => `未注册的组件 ${name}`,
68
+ missingComponent: (_name: string) => '无法显示此内容块',
57
69
  }
58
70
 
59
71
  export type Messages = typeof zh
60
72
 
61
73
  const en: Messages = {
62
- views: { document: 'Document', scrolly: 'Scrollytelling', present: 'Present', presenter: 'Presenter' },
74
+ contentRoles: { visual: 'On screen', narration: 'Script · Captions', reading: 'Reading only' },
75
+ views: { document: 'Web', scrolly: 'Scrollytelling', present: 'Present', presenter: 'Presenter View' },
63
76
  present: {
64
77
  region: 'Presentation',
78
+ jumpStep: 'Jump to Step',
79
+ captions: 'Captions by step',
80
+ captionsHint: 'Captions follow the selected step',
81
+ noCaptions: 'No script for this step',
82
+ step: step => `Step ${step}`,
65
83
  counter: (index, total, step, steps) => `${index} / ${total}${steps > 0 ? ` · step ${step + 1} of ${steps + 1}` : ''}`,
66
84
  prev: 'Previous',
67
85
  next: 'Next',
68
86
  overview: 'Overview',
69
- presenter: 'Presenter window',
70
- fullscreen: 'Full screen',
71
- exit: 'Exit presentation',
87
+ presenter: 'Presenter View',
88
+ refresh: 'Update',
89
+ fullscreen: 'Full Screen',
90
+ exitFullscreen: 'Exit Full Screen',
91
+ exit: 'End Show',
72
92
  },
73
93
  presenter: {
74
94
  step: (step, steps) => (steps > 0 ? `Step ${step + 1} of ${steps + 1}` : ''),
75
95
  timer: 'Timer',
76
- restart: 'Restart timer',
77
- notes: 'Narration',
78
- noNotes: 'No narration in this section',
96
+ restart: 'Restart Timer',
97
+ notes: 'Script',
98
+ noNotes: 'No script on this page',
79
99
  upcoming: 'Next',
80
100
  end: 'End of the document',
81
- nextSection: title => `Next section: ${title}`,
101
+ nextSection: title => `Next page: ${title}`,
82
102
  nextStep: step => `Step ${step + 1}`,
83
103
  },
84
104
  section: {
85
- anchor: title => `Link to "${title}"`,
86
- openSource: 'Open the source in the editor',
87
- sourceTitle: where => `${where} (Alt+click a block to open its source)`,
105
+ anchor: title => `Link to “${title}”`,
106
+ openSource: 'Open in Editor',
107
+ sourceTitle: where => `${where} (Alt+click any block to open it in the editor)`,
88
108
  },
89
109
  code: {
90
- executorFailed: (id, message) => `Executor ${id} failed: ${message}`,
110
+ actions: 'Code actions',
111
+ executorFailed: (_id, message) => `Couldn’t run the code: ${message}`,
91
112
  run: 'Run',
92
- rerun: 'Run again',
113
+ rerun: 'Run Again',
93
114
  running: 'Running…',
94
- unavailable: 'Execution service unavailable',
95
- resetSession: 'Reset session',
115
+ unavailable: 'Code can’t run here',
116
+ resetRunSession: 'Reset Session',
117
+ edit: 'Edit Code',
118
+ doneEditing: 'Done Editing',
119
+ revert: 'Restore Code',
96
120
  },
97
121
  markdown: {
98
122
  admonitions: {
@@ -109,7 +133,7 @@ const en: Messages = {
109
133
  },
110
134
  footnote: label => `Footnote ${label}`,
111
135
  },
112
- missingComponent: name => `Unregistered component ${name}`,
136
+ missingComponent: () => 'This block can’t be shown',
113
137
  }
114
138
 
115
139
  export const VIEWER_MESSAGES: Record<Locale, Messages> = { 'zh-CN': zh, 'en': en }
@@ -1,42 +1,36 @@
1
- import type { CodeNode, ResolutionStatus, RunResult } from '@lincyaw/kernel'
1
+ import type { CodeNode, ResolutionStatus } from '@lincyaw/kernel'
2
2
  import type { Locale } from '@lincyaw/kernel/contracts'
3
3
 
4
4
  const zh = {
5
- workingTree: '工作区',
6
- status: { moved: '已移动', fuzzy: '待确认', lost: '已失效' } as Partial<Record<ResolutionStatus, string>>,
5
+ workingTree: '当前代码',
6
+ status: { moved: '已移动', fuzzy: '待核对', lost: '已失效' } as Partial<Record<ResolutionStatus, string>>,
7
7
  statusTitle: {
8
- moved: '引用的代码在文件中移动了位置,显示的是移动后的代码',
9
- fuzzy: '引用的代码有改动,找到的位置有待确认;运行 presmium check 查看',
10
- lost: '找不到引用的代码;运行 presmium check 查看',
8
+ moved: '引用的代码在文件中移动了位置,显示的是移动后的代码。',
9
+ fuzzy: '引用的代码有改动,找到的位置有待确认;请运行 presmium check 查看。',
10
+ lost: '找不到引用的代码。请运行 presmium check 查看原因。',
11
11
  } as Partial<Record<ResolutionStatus, string>>,
12
12
  drifted: '已漂移',
13
- driftedTitle: '工作区中的代码已改变,运行 presmium check 查看差异',
13
+ driftedTitle: '当前代码已改变。请运行 presmium check 查看差异。',
14
14
  synced: '已锁定',
15
- syncedTitle: '锁定的代码与工作区一致',
15
+ syncedTitle: '锁定的代码与当前代码一致',
16
16
  openFile: (file: string) => `在仓库中打开 ${file}`,
17
- timedOut: '超时',
18
- exitCode: (code: number) => `退出码 ${code}`,
19
- recorded: '预录输出',
20
17
  }
21
18
 
22
19
  export type OriginMessages = typeof zh
23
20
 
24
21
  const en: OriginMessages = {
25
- workingTree: 'working tree',
26
- status: { moved: 'Moved', fuzzy: 'Unconfirmed', lost: 'Lost' },
22
+ workingTree: 'Current Code',
23
+ status: { moved: 'Moved', fuzzy: 'Unverified', lost: 'Lost' },
27
24
  statusTitle: {
28
- moved: 'The referenced code moved in its file; this is the code where it is now',
29
- fuzzy: 'The referenced code changed and the place found is uncertain; run presmium check to see it',
30
- lost: 'The referenced code is no longer found; run presmium check to see why',
25
+ moved: 'The referenced code moved in its file; this is the code where it is now.',
26
+ fuzzy: 'The referenced code changed and the place found is uncertain; run presmium check to see it.',
27
+ lost: 'The referenced code is no longer found; run presmium check to see why.',
31
28
  },
32
29
  drifted: 'Drifted',
33
- driftedTitle: 'The code changed in the working tree; run presmium check to see the difference',
30
+ driftedTitle: 'The current code changed; run presmium check to see the difference.',
34
31
  synced: 'Pinned',
35
- syncedTitle: 'The pinned code matches the working tree',
36
- openFile: file => `Open ${file} in the repository`,
37
- timedOut: 'Timed out',
38
- exitCode: code => `Exit code ${code}`,
39
- recorded: 'Recorded output',
32
+ syncedTitle: 'The pinned code matches the current code',
33
+ openFile: file => `Open ${file} in the Repository`,
40
34
  }
41
35
 
42
36
  export const ORIGIN_MESSAGES: Record<Locale, OriginMessages> = { 'zh-CN': zh, 'en': en }
@@ -60,7 +54,7 @@ function lines(start: number, end = start): string {
60
54
  return end > start ? `${start}-${end}` : `${start}`
61
55
  }
62
56
 
63
- export function originParts(node: CodeNode, shown: number | undefined, locale: Locale): OriginBar | undefined {
57
+ export function originBar(node: CodeNode, shown: number | undefined, locale: Locale): OriginBar | undefined {
64
58
  const t = ORIGIN_MESSAGES[locale]
65
59
  const { origin } = node
66
60
  if (!origin) return node.title ? { parts: [{ kind: 'file', text: node.title }] } : undefined
@@ -82,12 +76,3 @@ export function originParts(node: CodeNode, shown: number | undefined, locale: L
82
76
  }
83
77
  return { anchor: origin.anchor, parts }
84
78
  }
85
-
86
- export function outputNotes(run: RunResult, locale: Locale): string[] {
87
- const t = ORIGIN_MESSAGES[locale]
88
- return [
89
- run.timedOut ? t.timedOut : '',
90
- run.exitCode !== null && run.exitCode !== 0 ? t.exitCode(run.exitCode) : '',
91
- run.source === 'recorded' ? t.recorded : '',
92
- ].filter(Boolean)
93
- }
@@ -0,0 +1,27 @@
1
+ import type { RunResult } from '@lincyaw/kernel'
2
+ import type { Locale } from '@lincyaw/kernel/contracts'
3
+
4
+ const zh = {
5
+ timedOut: '超时',
6
+ exitCode: (code: number) => `退出码 ${code}`,
7
+ recorded: '预录输出',
8
+ }
9
+
10
+ export type OutputMessages = typeof zh
11
+
12
+ const en: OutputMessages = {
13
+ timedOut: 'Timed out',
14
+ exitCode: code => `Exit code ${code}`,
15
+ recorded: 'Recorded output',
16
+ }
17
+
18
+ export const OUTPUT_MESSAGES: Record<Locale, OutputMessages> = { 'zh-CN': zh, 'en': en }
19
+
20
+ export function outputNotes(run: RunResult, locale: Locale): string[] {
21
+ const t = OUTPUT_MESSAGES[locale]
22
+ return [
23
+ run.timedOut ? t.timedOut : '',
24
+ run.exitCode !== null && run.exitCode !== 0 ? t.exitCode(run.exitCode) : '',
25
+ run.source === 'recorded' ? t.recorded : '',
26
+ ].filter(Boolean)
27
+ }
@@ -5,6 +5,7 @@ export const FINAL_STEP = Number.POSITIVE_INFINITY
5
5
  export interface Position {
6
6
  section: string
7
7
  step: number
8
+ boundary?: 'start' | 'end'
8
9
  }
9
10
 
10
11
  export function itemVisible(block: IRBlock, item: number, step: number): boolean {
@@ -19,7 +20,7 @@ export function clampPosition(doc: IRDocument, position: Position): Position {
19
20
  const section = doc.sections.find(s => s.id === position.section) ?? doc.sections[0]
20
21
  if (!section) return position
21
22
  const step = Number.isFinite(position.step) ? Math.min(Math.max(Math.round(position.step), 0), section.steps) : section.steps
22
- return { section: section.id, step }
23
+ return { section: section.id, step, ...position.boundary ? { boundary: position.boundary } : {} }
23
24
  }
24
25
 
25
26
  export function keepPosition(prev: IRDocument, next: IRDocument, position: Position): Position {
package/src/block.ts DELETED
@@ -1,122 +0,0 @@
1
- import { PmBoundary, provideStepScope } from '@lincyaw/host'
2
- import { blockVisible, fnv1a64 } from '@lincyaw/kernel'
3
- import type { ComponentNode, IRBlock, IRSection } from '@lincyaw/kernel'
4
- import type { InjectionKey, PropType, VNode } from 'vue'
5
- import { computed, defineComponent, h, inject, onBeforeUnmount, onMounted, provide, shallowRef } from 'vue'
6
- import { BLOCK_CLASS, trackClicks } from './clicks.ts'
7
- import { CodeBlockView } from './code.ts'
8
- import { IN_COMPONENT } from './context.ts'
9
- import { FINAL_STEP } from './domain/index.ts'
10
- import { messages } from './i18n.ts'
11
- import { hastToVNodes, markdownToHast, MarkdownView } from './markdown.ts'
12
- import { getComponent, getNodeRenderer } from './registry.ts'
13
- import { PmMermaid } from './rich.ts'
14
- import { VIEWER } from './viewer.ts'
15
-
16
- const blockProps = {
17
- block: { type: Object as PropType<IRBlock>, required: true },
18
- section: { type: Object as PropType<IRSection>, required: true },
19
- step: { type: Number, required: true },
20
- expanded: Boolean,
21
- } as const
22
-
23
- const COMPONENT_NODE: InjectionKey<{ node: ComponentNode, section: string }> = Symbol('presmium.viewer.componentNode')
24
-
25
- function fallbackOf(node: ComponentNode, section: string): VNode[] {
26
- return node.fallback ? hastToVNodes(markdownToHast(node.fallback, section).children, {}) as VNode[] : []
27
- }
28
-
29
- export const PmComponentFallback = defineComponent({
30
- name: 'PmComponentFallback',
31
- setup() {
32
- const current = inject(COMPONENT_NODE, undefined)
33
- return () => (current ? fallbackOf(current.node, current.section) : null)
34
- },
35
- })
36
-
37
- const ComponentView = defineComponent({
38
- name: 'PmComponentNode',
39
- props: blockProps,
40
- setup(props) {
41
- provide(IN_COMPONENT, true)
42
- provide(COMPONENT_NODE, {
43
- get node() {
44
- return props.block.node as ComponentNode
45
- },
46
- get section() {
47
- return props.section.id
48
- },
49
- })
50
- const content = computed(() => {
51
- const { props: values, children } = props.block.node as ComponentNode
52
- return fnv1a64(JSON.stringify([values, children ?? []]))
53
- })
54
- return () => {
55
- const node = props.block.node as ComponentNode
56
- const component = getComponent(node.name)
57
- if (!component) {
58
- const fallback = fallbackOf(node, props.section.id)
59
- return h('div', { 'class': 'pm-component-missing', 'data-component': node.name }, fallback.length ? fallback : messages().missingComponent(node.name))
60
- }
61
- const children = node.children?.length
62
- ? { default: () => node.children!.map(child => h(BlockView, { key: child.id, block: child, section: props.section, step: props.step, expanded: props.expanded })) }
63
- : undefined
64
- const resetKey = props.expanded ? content.value : `${content.value}@${props.step}`
65
- return h(PmBoundary, { name: node.name, resetKey }, () => h(component, { ...node.props }, children))
66
- }
67
- },
68
- })
69
-
70
- function content(props: { block: IRBlock, section: IRSection, step: number, expanded: boolean }): VNode {
71
- const { node } = props.block
72
- const custom = (node.type === 'component' && getNodeRenderer(node.name))
73
- || (node.type === 'code' && getNodeRenderer(`code:${node.lang}`))
74
- || getNodeRenderer(node.type)
75
- if (custom) return h(custom, props)
76
- switch (node.type) {
77
- case 'markdown': return h(MarkdownView, { block: props.block, step: props.step, expanded: props.expanded, prefix: props.section.id })
78
- case 'code': return node.lang === 'mermaid'
79
- ? h(PmMermaid, { code: node.value })
80
- : h(CodeBlockView, { block: props.block, step: props.step, expanded: props.expanded })
81
- case 'component': return h(ComponentView, props)
82
- }
83
- }
84
-
85
- export function isOverlay(block: IRBlock): boolean {
86
- return block.node.type === 'component' && block.node.layer === 'overlay'
87
- }
88
-
89
- export const BlockView = defineComponent({
90
- name: 'PmBlock',
91
- props: blockProps,
92
- setup(props) {
93
- const viewer = inject(VIEWER, undefined)
94
- const el = shallowRef<HTMLElement>()
95
- provideStepScope(() => props.block.at)
96
- const clicks = computed(() => (props.expanded ? FINAL_STEP : props.step))
97
- let stop: (() => void) | undefined
98
- onMounted(() => {
99
- if (el.value) stop = trackClicks(el.value, clicks)
100
- })
101
- onBeforeUnmount(() => stop?.())
102
- const openSource = (event: MouseEvent) => {
103
- if (!event.altKey || !viewer?.options.openSource) return
104
- event.preventDefault()
105
- event.stopPropagation()
106
- viewer.options.openSource(props.block.source)
107
- }
108
- return () => {
109
- const { block } = props
110
- const visible = props.expanded || blockVisible(block, props.step)
111
- return h('div', {
112
- 'ref': el,
113
- 'class': [BLOCK_CLASS, `pm-block--${block.role}`, `pm-block--${block.node.type}`, { 'pm-step-range': block.until !== undefined, 'pm-block--overlay': isOverlay(block), 'pm-hidden': !visible }],
114
- 'data-block': block.id,
115
- 'data-at': block.at,
116
- 'data-component': block.node.type === 'component' ? block.node.name : undefined,
117
- 'aria-hidden': visible ? undefined : 'true',
118
- 'onClick': viewer?.options.openSource ? openSource : undefined,
119
- }, [content({ block, section: props.section, step: props.step, expanded: props.expanded })])
120
- }
121
- },
122
- })
package/src/cards.ts DELETED
@@ -1,130 +0,0 @@
1
- import { TabsContent, TabsList, TabsRoot, TabsTrigger } from 'reka-ui'
2
- import type { MdastNode } from '@lincyaw/kernel'
3
- import type { Element, ElementContent, Properties } from 'hast'
4
- import type { Handler, State } from 'mdast-util-to-hast'
5
- import type { PropType, VNodeChild } from 'vue'
6
- import { defineComponent, h, reactive, ref, watch } from 'vue'
7
-
8
- const WIDTHS = ['xs', 'sm', 'md', 'lg'] as const
9
-
10
- function element(tagName: string, properties: Properties, children: ElementContent[] = []): Element {
11
- return { type: 'element', tagName, properties, children }
12
- }
13
-
14
- function classes(node: MdastNode): string[] {
15
- return typeof node.class === 'string' ? node.class.split(/\s+/).filter(Boolean) : []
16
- }
17
-
18
- export function isButton(node: MdastNode): boolean {
19
- return classes(node).includes('button')
20
- }
21
-
22
- function gridColumns(node: MdastNode): number[] {
23
- const given = Array.isArray(node.columns) ? node.columns.map(Number).filter(n => Number.isInteger(n) && n >= 1) : []
24
- return WIDTHS.map((_, i) => given[Math.min(i, given.length - 1)] ?? 1)
25
- }
26
-
27
- const style = (values: Record<string, number>) => Object.entries(values).map(([key, value]) => `${key}: ${value}`).join('; ')
28
-
29
- const grid: Handler = (state, node: MdastNode) => {
30
- const columns = gridColumns(node)
31
- const children = state.all(node as never).map((child) => {
32
- const twelfths = child.type === 'element' ? Number(child.properties.dataPmColumns) : Number.NaN
33
- if (child.type !== 'element' || !(twelfths >= 1)) return child
34
- const spans = Object.fromEntries(WIDTHS.map((width, i) => [`--pm-span-${width}`, Math.min(columns[i]!, Math.max(1, Math.round(twelfths * columns[i]! / 12)))]))
35
- return { ...child, properties: { ...child.properties, style: style(spans) } }
36
- })
37
- const widths = Object.fromEntries(WIDTHS.map((width, i) => [`--pm-grid-${width}`, columns[i]!]))
38
- return element('div', { className: ['pm-grid', ...classes(node)], style: style(widths) }, children)
39
- }
40
-
41
- const gridItem: Handler = (state, node: MdastNode) =>
42
- element('div', { className: ['pm-grid__item', ...classes(node)], dataPmColumns: typeof node.columns === 'number' ? node.columns : undefined }, state.all(node as never))
43
-
44
- const card: Handler = (state: State, node: MdastNode) => {
45
- const items = node.children ?? []
46
- const part = (type: string) => items.find(child => child.type === type)
47
- const url = typeof node.url === 'string' && node.url ? node.url : undefined
48
- const header = part('header')
49
- const footer = part('footer')
50
- const title = part('cardTitle')
51
- const content: ElementContent[] = []
52
- if (header) content.push(element('header', { className: ['pm-card__header'] }, state.all(header as never)))
53
- if (title || url) {
54
- const words: MdastNode[] = title?.children?.length ? title.children : [{ type: 'text', value: url ?? '' }]
55
- const inner = url ? [state.one({ type: 'link', url, children: words } as never, node as never) as ElementContent] : state.all({ type: 'paragraph', children: words } as never)
56
- content.push(element('p', { className: ['pm-card__title'] }, inner))
57
- }
58
- const body = items.filter(child => child !== header && child !== title && child !== footer)
59
- content.push(element('div', { className: ['pm-card__body'] }, state.all({ type: 'root', children: body } as never)))
60
- if (footer) content.push(element('footer', { className: ['pm-card__footer'] }, state.all(footer as never)))
61
- return element('div', { className: ['pm-card', ...url ? ['pm-card--link'] : [], ...classes(node)] }, content)
62
- }
63
-
64
- const tabSet: Handler = (state, node: MdastNode) => {
65
- const panels = (node.children ?? []).filter(child => child.type === 'tabItem').map(item => element('div', {
66
- className: ['pm-tabs__panel'],
67
- dataTitle: typeof item.title === 'string' ? item.title : '',
68
- dataSync: typeof item.sync === 'string' && item.sync ? item.sync : undefined,
69
- dataSelected: item.selected === true ? 'true' : undefined,
70
- }, state.all(item as never)))
71
- return element('div', { className: ['pm-tabs', ...classes(node)], dataPmTabs: '' }, panels)
72
- }
73
-
74
- export function cardHandlers(): Record<string, Handler> {
75
- return {
76
- 'grid': grid,
77
- 'grid-item': gridItem,
78
- 'card': card,
79
- 'tabSet': tabSet,
80
- 'tabItem': (state, node) => element('div', {}, state.all(node)),
81
- }
82
- }
83
-
84
- export interface TabEntry {
85
- title: string
86
- sync?: string
87
- selected: boolean
88
- content: () => VNodeChild[]
89
- }
90
-
91
- export function tabEntries(set: Element, render: (nodes: ElementContent[]) => VNodeChild[]): TabEntry[] {
92
- return set.children.flatMap((panel): TabEntry[] => panel.type !== 'element' ? [] : [{
93
- title: String(panel.properties.dataTitle ?? ''),
94
- sync: typeof panel.properties.dataSync === 'string' ? panel.properties.dataSync : undefined,
95
- selected: panel.properties.dataSelected === 'true',
96
- content: () => render(panel.children),
97
- }])
98
- }
99
-
100
- const synced = reactive({ key: undefined as string | undefined, choices: 0 })
101
-
102
- export const TabSetView = defineComponent({
103
- name: 'PmTabSet',
104
- props: {
105
- tabs: { type: Array as PropType<TabEntry[]>, required: true },
106
- },
107
- setup(props) {
108
- const withKey = (key?: string) => (key ? props.tabs.findIndex(tab => tab.sync === key) : -1)
109
- const active = ref([withKey(synced.key), props.tabs.findIndex(tab => tab.selected), 0].find(k => k >= 0)!)
110
- watch(() => synced.choices, () => {
111
- const k = withKey(synced.key)
112
- if (k >= 0) active.value = k
113
- })
114
- const choose = (k: number) => {
115
- active.value = k
116
- const key = props.tabs[k]?.sync
117
- if (key) {
118
- synced.key = key
119
- synced.choices++
120
- }
121
- }
122
- return () => h(TabsRoot, { modelValue: String(active.value), 'onUpdate:modelValue': value => choose(Number(value)) }, () => [
123
- h(TabsList, { class: 'pm-tabs__list' }, () => props.tabs.map((tab, k) =>
124
- h(TabsTrigger, { key: k, value: String(k), class: 'pm-tabs__tab' }, () => tab.title))),
125
- ...props.tabs.map((tab, k) => h(TabsContent, {
126
- key: k, value: String(k), class: 'pm-tabs__panel', forceMount: true, hidden: k !== active.value, tabindex: 0,
127
- }, tab.content)),
128
- ])
129
- },
130
- })
package/src/clicks.ts DELETED
@@ -1,124 +0,0 @@
1
- import type { App, Directive, DirectiveBinding, Ref } from 'vue'
2
- import { defineComponent, h, onBeforeUnmount, onMounted, shallowRef, watchEffect, withDirectives } from 'vue'
3
- import { clickVisible, resolveClicks } from './domain/index.ts'
4
- import type { ClickRange, ClickSpec } from './domain/index.ts'
5
-
6
- interface Controller {
7
- add(el: HTMLElement, spec: ClickSpec): void
8
- remove(el: HTMLElement): void
9
- }
10
-
11
- export const BLOCK_CLASS = 'pm-block'
12
-
13
- const controllers = new WeakMap<Element, Controller>()
14
- const owners = new WeakMap<HTMLElement, Controller>()
15
- const pending = new Map<HTMLElement, ClickSpec>()
16
-
17
- function register(el: HTMLElement, spec: ClickSpec): void {
18
- const block = el.closest(`.${BLOCK_CLASS}`)
19
- const controller = block ? controllers.get(block) : undefined
20
- if (!controller) {
21
- pending.set(el, spec)
22
- return
23
- }
24
- owners.set(el, controller)
25
- controller.add(el, spec)
26
- }
27
-
28
- function unregister(el: HTMLElement): void {
29
- pending.delete(el)
30
- owners.get(el)?.remove(el)
31
- owners.delete(el)
32
- }
33
-
34
- export function trackClicks(block: HTMLElement, step: Readonly<Ref<number>>): () => void {
35
- const specs = new Map<HTMLElement, ClickSpec>()
36
- const ranges = shallowRef(new Map<HTMLElement, ClickRange>())
37
- const resolve = () => {
38
- const ordered = [...specs.keys()].sort((a, b) => (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1))
39
- const resolved = resolveClicks(ordered.map(el => specs.get(el)!))
40
- ranges.value = new Map(ordered.map((el, i) => [el, resolved[i]!]))
41
- }
42
- const controller: Controller = {
43
- add(el, spec) {
44
- specs.set(el, spec)
45
- resolve()
46
- },
47
- remove(el) {
48
- if (specs.delete(el)) resolve()
49
- },
50
- }
51
- controllers.set(block, controller)
52
- for (const [el, spec] of pending) {
53
- if (!block.contains(el)) continue
54
- pending.delete(el)
55
- owners.set(el, controller)
56
- specs.set(el, spec)
57
- }
58
- resolve()
59
- const stop = watchEffect(() => {
60
- for (const [el, range] of ranges.value) el.dataset.pmClick = clickVisible(range, step.value) ? 'shown' : 'hidden'
61
- })
62
- return () => {
63
- stop()
64
- controllers.delete(block)
65
- }
66
- }
67
-
68
- function clickDirective(kind: ClickSpec['kind'], hide = false): Directive<HTMLElement, unknown> {
69
- const spec = (binding: DirectiveBinding<unknown>): ClickSpec => ({ kind, at: binding.value, hide: hide || Boolean(binding.modifiers.hide) })
70
- return {
71
- mounted(el, binding) {
72
- register(el, spec(binding))
73
- },
74
- updated(el, binding) {
75
- if (JSON.stringify(binding.value) === JSON.stringify(binding.oldValue)) return
76
- unregister(el)
77
- register(el, spec(binding))
78
- },
79
- unmounted(el) {
80
- unregister(el)
81
- },
82
- }
83
- }
84
-
85
- const vClick = clickDirective('click')
86
- const vAfter = clickDirective('after')
87
-
88
- const clickProps = { at: { type: [Number, String, Array], default: undefined }, hide: Boolean }
89
-
90
- function clickComponent(name: string, directive: Directive) {
91
- return defineComponent({
92
- name,
93
- props: clickProps,
94
- setup(props, { slots }) {
95
- return () => withDirectives(h('span', { class: 'pm-v-click' }, slots.default?.()), [[directive, props.at, '', { hide: props.hide }]])
96
- },
97
- })
98
- }
99
-
100
- const VClicks = defineComponent({
101
- name: 'VClicks',
102
- props: { every: { type: [Number, String], default: 1 }, hide: Boolean },
103
- setup(props, { slots }) {
104
- const root = shallowRef<HTMLElement>()
105
- let items: HTMLElement[] = []
106
- onMounted(() => {
107
- const children = [...root.value!.children] as HTMLElement[]
108
- items = children.flatMap(child => (/^(UL|OL)$/.test(child.tagName) ? [...child.children] as HTMLElement[] : [child]))
109
- const every = Math.max(1, Number(props.every) || 1)
110
- items.forEach((item, i) => register(item, { kind: i % every === 0 ? 'click' : 'after', hide: props.hide }))
111
- })
112
- onBeforeUnmount(() => items.forEach(unregister))
113
- return () => h('div', { ref: root, class: 'pm-v-clicks' }, slots.default?.())
114
- },
115
- })
116
-
117
- export function installClicks(app: App): void {
118
- app.directive('click', vClick)
119
- app.directive('after', vAfter)
120
- app.directive('click-hide', clickDirective('click', true))
121
- app.component('VClick', clickComponent('VClick', vClick))
122
- app.component('VAfter', clickComponent('VAfter', vAfter))
123
- app.component('VClicks', VClicks)
124
- }