@lincyaw/host 0.0.0-stage → 1.0.0-rc.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/LICENSE +202 -0
- package/README.md +39 -2
- package/package.json +39 -3
- package/src/PresenterPanels.vue +91 -0
- package/src/StartReportPanel.vue +45 -0
- package/src/Toolbar.vue +49 -0
- package/src/approvals.ts +58 -0
- package/src/blocks.ts +82 -0
- package/src/boundary.ts +56 -0
- package/src/design/components.ts +327 -0
- package/src/design/design.css +1 -0
- package/src/design/theme.ts +32 -0
- package/src/env.d.ts +7 -0
- package/src/host.ts +82 -0
- package/src/index.ts +84 -0
- package/src/kernel.ts +131 -0
- package/src/key.ts +6 -0
- package/src/locale.ts +42 -0
- package/src/messages.ts +51 -0
- package/src/operations.ts +76 -0
- package/src/panels.ts +60 -0
- package/src/scope.ts +29 -0
- package/src/server.ts +173 -0
- package/src/session.ts +34 -0
- package/src/shared.ts +85 -0
- package/src/types.ts +107 -0
- package/src/vue-shim.d.ts +6 -0
- package/src/windows.ts +28 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import type { InjectionKey, PropType, VNode } from 'vue'
|
|
2
|
+
import { defineComponent, cloneVNode, h, inject, provide, ref, useId } from 'vue'
|
|
3
|
+
import { DialogClose, DialogContent, DialogPortal, DialogRoot, DialogTitle, DialogTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuRoot, DropdownMenuTrigger, PopoverAnchor, PopoverContent, PopoverPortal, PopoverRoot, TabsContent, TabsList, TabsRoot, TabsTrigger, ToggleGroupItem, ToggleGroupRoot } from 'reka-ui'
|
|
4
|
+
import type { ToolbarPlace } from '../types.ts'
|
|
5
|
+
import { ICONS } from '@lincyaw/design'
|
|
6
|
+
import type { IconName } from '@lincyaw/design'
|
|
7
|
+
|
|
8
|
+
const PLACE: InjectionKey<ToolbarPlace> = Symbol('presmium.toolbarPlace')
|
|
9
|
+
const OVERLAY_Z: InjectionKey<string> = Symbol('presmium.overlayZ')
|
|
10
|
+
|
|
11
|
+
function overlayStyle(kind: 'popover' | 'dialog') {
|
|
12
|
+
const parent = inject(OVERLAY_Z, undefined)
|
|
13
|
+
const zIndex = parent ? `calc(${parent} + 1)` : `var(--pm-z-${kind})`
|
|
14
|
+
provide(OVERLAY_Z, zIndex)
|
|
15
|
+
return { zIndex }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
/** @experimental */
|
|
20
|
+
export function useToolbarPlace(): ToolbarPlace {
|
|
21
|
+
return inject(PLACE, 'bar')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @experimental */
|
|
25
|
+
export function provideToolbarPlace(place: ToolbarPlace): void {
|
|
26
|
+
provide(PLACE, place)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const PmIcon = defineComponent({
|
|
30
|
+
name: 'PmIcon',
|
|
31
|
+
props: {
|
|
32
|
+
name: { type: String as PropType<IconName>, required: true },
|
|
33
|
+
size: { type: [Number, String], default: 20 },
|
|
34
|
+
strokeWidth: { type: Number, default: 1.7 },
|
|
35
|
+
},
|
|
36
|
+
setup(props) {
|
|
37
|
+
return () => h('svg', {
|
|
38
|
+
'class': 'pm-ui-icon',
|
|
39
|
+
'width': props.size,
|
|
40
|
+
'height': props.size,
|
|
41
|
+
'viewBox': '0 0 24 24',
|
|
42
|
+
'fill': 'none',
|
|
43
|
+
'stroke': 'currentColor',
|
|
44
|
+
'stroke-width': props.strokeWidth,
|
|
45
|
+
'stroke-linecap': 'round',
|
|
46
|
+
'stroke-linejoin': 'round',
|
|
47
|
+
'aria-hidden': 'true',
|
|
48
|
+
'focusable': 'false',
|
|
49
|
+
}, (ICONS[props.name] ?? []).map(([tag, attrs]) => h(tag, attrs)))
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
export type ButtonVariant = 'default' | 'primary' | 'accent' | 'ghost' | 'danger'
|
|
54
|
+
|
|
55
|
+
export const PmButton = defineComponent({
|
|
56
|
+
name: 'PmButton',
|
|
57
|
+
props: {
|
|
58
|
+
variant: { type: String as PropType<ButtonVariant>, default: 'default' },
|
|
59
|
+
small: Boolean,
|
|
60
|
+
icon: String as PropType<IconName>,
|
|
61
|
+
type: { type: String as PropType<'button' | 'submit' | 'reset'>, default: 'button' },
|
|
62
|
+
},
|
|
63
|
+
setup(props, { slots }) {
|
|
64
|
+
return () => h('button', {
|
|
65
|
+
type: props.type,
|
|
66
|
+
class: ['pm-ui-button', props.variant !== 'default' && `pm-ui-button--${props.variant}`, props.small && 'pm-ui-button--small'],
|
|
67
|
+
}, [props.icon ? h(PmIcon, { name: props.icon, size: props.small ? 14 : 16, strokeWidth: 2 }) : null, slots.default?.()])
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
export const PmIconButton = defineComponent({
|
|
72
|
+
name: 'PmIconButton',
|
|
73
|
+
props: {
|
|
74
|
+
icon: { type: String as PropType<IconName>, required: true },
|
|
75
|
+
label: { type: String, required: true },
|
|
76
|
+
small: Boolean,
|
|
77
|
+
},
|
|
78
|
+
setup(props) {
|
|
79
|
+
return () => h('button', {
|
|
80
|
+
'type': 'button',
|
|
81
|
+
'class': ['pm-ui-icon-button', props.small && 'pm-ui-icon-button--small'],
|
|
82
|
+
'aria-label': props.label,
|
|
83
|
+
'title': props.label,
|
|
84
|
+
}, [h(PmIcon, { name: props.icon, size: props.small ? 16 : 20 })])
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
export type Tone = 'neutral' | 'accent' | 'ok' | 'warn' | 'danger' | 'outline'
|
|
89
|
+
|
|
90
|
+
export const PmBadge = defineComponent({
|
|
91
|
+
name: 'PmBadge',
|
|
92
|
+
props: {
|
|
93
|
+
tone: { type: String as PropType<Tone>, default: 'neutral' },
|
|
94
|
+
pill: Boolean,
|
|
95
|
+
},
|
|
96
|
+
setup(props, { slots }) {
|
|
97
|
+
return () => h('span', { class: ['pm-ui-badge', props.tone !== 'neutral' && `pm-ui-badge--${props.tone}`, props.pill && 'pm-ui-badge--pill'] }, slots.default?.())
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
export const PmCard = defineComponent({
|
|
102
|
+
name: 'PmCard',
|
|
103
|
+
props: {
|
|
104
|
+
variant: { type: String as PropType<'default' | 'interactive' | 'dashed'>, default: 'default' },
|
|
105
|
+
tag: { type: String, default: 'div' },
|
|
106
|
+
},
|
|
107
|
+
setup(props, { slots }) {
|
|
108
|
+
return () => h(props.tag, { class: ['pm-ui-card', props.variant !== 'default' && `pm-ui-card--${props.variant}`] }, slots.default?.())
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
export const PmField = defineComponent({
|
|
113
|
+
name: 'PmField',
|
|
114
|
+
props: {
|
|
115
|
+
label: { type: String, required: true },
|
|
116
|
+
inline: Boolean,
|
|
117
|
+
},
|
|
118
|
+
setup(props, { slots }) {
|
|
119
|
+
return () => h('label', { class: ['pm-ui-field', props.inline && 'pm-ui-field--inline'] }, props.inline
|
|
120
|
+
? [slots.default?.(), h('span', { class: 'pm-ui-field__label' }, props.label)]
|
|
121
|
+
: [h('span', { class: 'pm-ui-field__label' }, props.label), slots.default?.()])
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
export interface Choice {
|
|
126
|
+
value: string
|
|
127
|
+
label: string
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const PmSegmented = defineComponent({
|
|
131
|
+
name: 'PmSegmented',
|
|
132
|
+
props: {
|
|
133
|
+
modelValue: { type: String, required: true },
|
|
134
|
+
options: { type: Array as PropType<Choice[]>, required: true },
|
|
135
|
+
label: { type: String, required: true },
|
|
136
|
+
},
|
|
137
|
+
emits: ['update:modelValue'],
|
|
138
|
+
setup(props, { emit }) {
|
|
139
|
+
return () => h(ToggleGroupRoot, {
|
|
140
|
+
'type': 'single', 'class': 'pm-ui-segmented', 'aria-label': props.label, 'modelValue': props.modelValue,
|
|
141
|
+
'onUpdate:modelValue': value => { if (typeof value === 'string' && value) emit('update:modelValue', value) },
|
|
142
|
+
}, () => props.options.map(option => h(ToggleGroupItem, {
|
|
143
|
+
key: option.value, value: option.value, class: 'pm-ui-segmented__option',
|
|
144
|
+
}, () => option.label)))
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
export const PmTabs = defineComponent({
|
|
149
|
+
name: 'PmTabs',
|
|
150
|
+
props: {
|
|
151
|
+
modelValue: { type: String, required: true },
|
|
152
|
+
tabs: { type: Array as PropType<Choice[]>, required: true },
|
|
153
|
+
label: { type: String, required: true },
|
|
154
|
+
},
|
|
155
|
+
emits: ['update:modelValue'],
|
|
156
|
+
setup(props, { emit, slots }) {
|
|
157
|
+
return () => h(TabsRoot, {
|
|
158
|
+
'class': 'pm-ui-tabset', 'modelValue': props.modelValue,
|
|
159
|
+
'onUpdate:modelValue': value => emit('update:modelValue', String(value)),
|
|
160
|
+
}, () => [
|
|
161
|
+
h(TabsList, { class: 'pm-ui-tabs', 'aria-label': props.label }, () => props.tabs.map(tab =>
|
|
162
|
+
h(TabsTrigger, { key: tab.value, value: tab.value, class: 'pm-ui-tab' }, () => tab.label))),
|
|
163
|
+
slots.default ? h(TabsContent, { key: props.modelValue, value: props.modelValue, class: 'pm-ui-tabpanel', tabindex: 0 }, () => slots.default?.({ value: props.modelValue })) : null,
|
|
164
|
+
])
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
export type PopoverPlacement = 'below-end' | 'below-start' | 'above-end' | 'above-start'
|
|
169
|
+
|
|
170
|
+
function placement(value: PopoverPlacement) {
|
|
171
|
+
return { side: value.startsWith('above') ? 'top' as const : 'bottom' as const, align: value.endsWith('start') ? 'start' as const : 'end' as const, sideOffset: 6, collisionPadding: 16 }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const PmPopover = defineComponent({
|
|
175
|
+
name: 'PmPopover',
|
|
176
|
+
props: {
|
|
177
|
+
open: Boolean,
|
|
178
|
+
label: { type: String, required: true },
|
|
179
|
+
placement: { type: String as PropType<PopoverPlacement>, default: 'below-end' },
|
|
180
|
+
name: String,
|
|
181
|
+
},
|
|
182
|
+
emits: ['update:open'],
|
|
183
|
+
setup(props, { emit, slots }) {
|
|
184
|
+
const style = overlayStyle('popover')
|
|
185
|
+
const anchor = ref<HTMLElement>()
|
|
186
|
+
const id = useId()
|
|
187
|
+
let outside = false
|
|
188
|
+
return () => h(PopoverRoot, { 'open': props.open, 'onUpdate:open': value => emit('update:open', value) }, () => [
|
|
189
|
+
h(PopoverAnchor, { asChild: true }, () => h('span', { ref: anchor, class: ['pm-ui-anchor', props.name] }, slots.trigger?.().map(node => cloneVNode(node, {
|
|
190
|
+
'aria-haspopup': 'dialog', 'aria-expanded': props.open, 'aria-controls': props.open ? id : undefined,
|
|
191
|
+
})))),
|
|
192
|
+
h(PopoverPortal, {}, () => h(PopoverContent, {
|
|
193
|
+
...placement(props.placement), 'id': id, style,
|
|
194
|
+
onOpenAutoFocus: () => { outside = false },
|
|
195
|
+
onInteractOutside: () => { outside = true },
|
|
196
|
+
onCloseAutoFocus: (event: Event) => { event.preventDefault(); if (!outside) anchor.value?.querySelector<HTMLElement>('button, [href]')?.focus() },
|
|
197
|
+
'class': ['pm-ui-popover', 'pm-ui-floating', 'pm-ui', props.name && `${props.name}__panel`], 'aria-label': props.label,
|
|
198
|
+
}, slots.default)),
|
|
199
|
+
])
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
const dialogProps = {
|
|
204
|
+
open: Boolean,
|
|
205
|
+
label: { type: String, required: true },
|
|
206
|
+
closeLabel: { type: String, required: true },
|
|
207
|
+
name: String,
|
|
208
|
+
} as const
|
|
209
|
+
|
|
210
|
+
function dialogComponent(drawer: boolean) {
|
|
211
|
+
return defineComponent({
|
|
212
|
+
name: drawer ? 'PmDrawer' : 'PmDialog',
|
|
213
|
+
props: dialogProps,
|
|
214
|
+
emits: ['update:open'],
|
|
215
|
+
setup(props, { emit, slots }) {
|
|
216
|
+
const style = overlayStyle(drawer ? 'popover' : 'dialog')
|
|
217
|
+
const kind = drawer ? 'pm-ui-drawer' : 'pm-ui-dialog'
|
|
218
|
+
let opener: HTMLElement | undefined
|
|
219
|
+
return () => h(DialogRoot, { 'open': props.open, 'onUpdate:open': value => emit('update:open', value) }, () => [
|
|
220
|
+
slots.trigger ? h(DialogTrigger, { asChild: true }, slots.trigger) : null,
|
|
221
|
+
h(DialogPortal, {}, () => h(DialogContent, {
|
|
222
|
+
'class': [kind, 'pm-ui', props.name && `${props.name}__panel`], 'style': style, 'aria-describedby': undefined,
|
|
223
|
+
onOpenAutoFocus: () => { opener = document.activeElement instanceof HTMLElement ? document.activeElement : undefined },
|
|
224
|
+
onCloseAutoFocus: (event: Event) => {
|
|
225
|
+
if (slots.trigger || !opener?.isConnected) return
|
|
226
|
+
event.preventDefault()
|
|
227
|
+
opener.focus()
|
|
228
|
+
},
|
|
229
|
+
}, () => [
|
|
230
|
+
h('div', { class: `${kind}__header` }, [
|
|
231
|
+
h(DialogTitle, { as: 'h2', class: `${kind}__title` }, () => props.label),
|
|
232
|
+
slots.actions?.(),
|
|
233
|
+
h(DialogClose, { asChild: true }, () => h(PmIconButton, { icon: 'close', label: props.closeLabel, small: true })),
|
|
234
|
+
]),
|
|
235
|
+
h('div', { class: `${kind}__body` }, slots.default?.()),
|
|
236
|
+
])),
|
|
237
|
+
])
|
|
238
|
+
},
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export const PmDrawer = dialogComponent(true)
|
|
243
|
+
export const PmDialog = dialogComponent(false)
|
|
244
|
+
|
|
245
|
+
export interface MenuItem {
|
|
246
|
+
id: string
|
|
247
|
+
label: string
|
|
248
|
+
icon?: IconName
|
|
249
|
+
danger?: boolean
|
|
250
|
+
disabled?: boolean
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export const PmMenu = defineComponent({
|
|
254
|
+
name: 'PmMenu',
|
|
255
|
+
props: {
|
|
256
|
+
label: { type: String, required: true },
|
|
257
|
+
items: { type: Array as PropType<MenuItem[]>, required: true },
|
|
258
|
+
icon: { type: String as PropType<IconName>, default: 'more' },
|
|
259
|
+
placement: { type: String as PropType<PopoverPlacement>, default: 'below-end' },
|
|
260
|
+
},
|
|
261
|
+
emits: ['select'],
|
|
262
|
+
setup(props, { emit }) {
|
|
263
|
+
const style = overlayStyle('popover')
|
|
264
|
+
return () => h(DropdownMenuRoot, {}, () => [
|
|
265
|
+
h(DropdownMenuTrigger, { asChild: true }, () => h(PmIconButton, { icon: props.icon, label: props.label, small: true })),
|
|
266
|
+
h(DropdownMenuPortal, {}, () => h(DropdownMenuContent, { ...placement(props.placement), style, class: ['pm-ui-menu', 'pm-ui-floating', 'pm-ui'], 'aria-label': props.label }, () => props.items.map(item =>
|
|
267
|
+
h(DropdownMenuItem, {
|
|
268
|
+
key: item.id, disabled: item.disabled, textValue: item.label,
|
|
269
|
+
class: ['pm-ui-menu__item', item.danger && 'pm-ui-menu__item--danger'],
|
|
270
|
+
onSelect: () => emit('select', item.id),
|
|
271
|
+
}, () => [item.icon ? h(PmIcon, { name: item.icon, size: 16 }) : null, item.label])))),
|
|
272
|
+
])
|
|
273
|
+
},
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
export const PmEmpty = defineComponent({
|
|
277
|
+
name: 'PmEmpty',
|
|
278
|
+
props: {
|
|
279
|
+
title: String,
|
|
280
|
+
icon: String as PropType<IconName>,
|
|
281
|
+
},
|
|
282
|
+
setup(props, { slots }) {
|
|
283
|
+
return () => h('div', { class: 'pm-ui-empty' }, [
|
|
284
|
+
props.icon ? h(PmIcon, { name: props.icon, size: 28 }) : null,
|
|
285
|
+
props.title ? h('p', { class: 'pm-ui-empty__title' }, props.title) : null,
|
|
286
|
+
slots.default ? h('p', { style: { margin: 0 } }, slots.default()) : null,
|
|
287
|
+
slots.action?.(),
|
|
288
|
+
])
|
|
289
|
+
},
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
export const PmNotice = defineComponent({
|
|
293
|
+
name: 'PmNotice',
|
|
294
|
+
props: {
|
|
295
|
+
tone: { type: String as PropType<'neutral' | 'accent' | 'ok' | 'warn' | 'danger'>, default: 'neutral' },
|
|
296
|
+
},
|
|
297
|
+
setup(props, { slots }) {
|
|
298
|
+
return (): VNode => h('p', { class: ['pm-ui-notice', props.tone !== 'neutral' && `pm-ui-notice--${props.tone}`], role: props.tone === 'danger' ? 'alert' : 'status' }, slots.default?.())
|
|
299
|
+
},
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
export const PmToolbarEntry = defineComponent({
|
|
303
|
+
name: 'PmToolbarEntry',
|
|
304
|
+
props: {
|
|
305
|
+
name: { type: String, required: true },
|
|
306
|
+
label: { type: String, required: true },
|
|
307
|
+
closeLabel: { type: String, required: true },
|
|
308
|
+
icon: String as PropType<IconName>,
|
|
309
|
+
open: Boolean,
|
|
310
|
+
},
|
|
311
|
+
emits: ['update:open'],
|
|
312
|
+
setup(props, { emit, slots }) {
|
|
313
|
+
const place = useToolbarPlace()
|
|
314
|
+
const update = (open: boolean) => emit('update:open', open)
|
|
315
|
+
const toggleAttrs = () => ({
|
|
316
|
+
'class': `${props.name}__toggle`,
|
|
317
|
+
'aria-expanded': props.open ? 'true' : 'false',
|
|
318
|
+
'onClick': () => update(!props.open),
|
|
319
|
+
})
|
|
320
|
+
return () => {
|
|
321
|
+
return h(PmPopover, { 'open': props.open, 'label': props.label, 'name': props.name, 'placement': place === 'status' ? 'above-start' : 'below-end', 'onUpdate:open': update }, {
|
|
322
|
+
trigger: () => h(PmButton, { variant: 'ghost', small: true, icon: props.icon, ...toggleAttrs() }, () => props.label),
|
|
323
|
+
default: () => slots.default?.(),
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import '@lincyaw/design/design.css';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Ref } from 'vue'
|
|
2
|
+
import { getCurrentInstance, inject, readonly, shallowRef } from 'vue'
|
|
3
|
+
import { createGlobalState, useMutationObserver } from '@vueuse/core'
|
|
4
|
+
import { HOST, installed } from '../key.ts'
|
|
5
|
+
|
|
6
|
+
export interface Theme {
|
|
7
|
+
readonly dark: Readonly<Ref<boolean>>
|
|
8
|
+
setDark(dark: boolean): void
|
|
9
|
+
toggle(): void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const observeDarkClass = createGlobalState(() => {
|
|
13
|
+
const root = () => typeof document === 'undefined' ? undefined : document.documentElement
|
|
14
|
+
const dark = shallowRef(root()?.classList.contains('dark') ?? false)
|
|
15
|
+
useMutationObserver(root, () => dark.value = root()?.classList.contains('dark') ?? false, { attributes: true, attributeFilter: ['class'] })
|
|
16
|
+
return dark
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export function useTheme(): Theme {
|
|
20
|
+
const host = getCurrentInstance() ? inject(HOST, undefined) : installed.adapter
|
|
21
|
+
const own = getCurrentInstance() ? host?.useDarkMode?.() : undefined
|
|
22
|
+
const dark = own ?? observeDarkClass()
|
|
23
|
+
const setDark = (value: boolean) => {
|
|
24
|
+
if (own) own.value = value
|
|
25
|
+
else if (typeof document !== 'undefined') document.documentElement.classList.toggle('dark', value)
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
dark: readonly(dark),
|
|
29
|
+
setDark,
|
|
30
|
+
toggle: () => setDark(!dark.value),
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/env.d.ts
ADDED
package/src/host.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { stepStateAt } from '@lincyaw/kernel/contracts'
|
|
2
|
+
import type { PresentationMode, ServiceToken, StartReport, StepRange, StepState } from '@lincyaw/kernel/contracts'
|
|
3
|
+
import type { App, ComputedRef, Ref } from 'vue'
|
|
4
|
+
import { computed, getCurrentInstance, getCurrentScope, inject, onScopeDispose, readonly, ref } from 'vue'
|
|
5
|
+
import { PmBoundary } from './boundary.ts'
|
|
6
|
+
import { HOST, installed } from './key.ts'
|
|
7
|
+
import { PmStepScope, useStepScope } from './scope.ts'
|
|
8
|
+
import type { HostAdapter, SharedStateScope } from './types.ts'
|
|
9
|
+
|
|
10
|
+
export function installHost(app: App, adapter: HostAdapter): void {
|
|
11
|
+
installed.adapter = adapter
|
|
12
|
+
app.provide(HOST, adapter)
|
|
13
|
+
app.component('PmBoundary', PmBoundary)
|
|
14
|
+
app.component('PmStepScope', PmStepScope)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useHost(): HostAdapter {
|
|
18
|
+
const host = inject(HOST, undefined)
|
|
19
|
+
if (!host) throw new Error('[presmium] no host installed: the host adapter must call installHost(app, adapter)')
|
|
20
|
+
return host
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useStepState(range: StepRange): Readonly<Ref<StepState>> {
|
|
24
|
+
const step = useHost().useStep()
|
|
25
|
+
const { at } = useStepScope()
|
|
26
|
+
return computed(() => stepStateAt(range, step.value - at.value))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useCursor() {
|
|
30
|
+
return useHost().useCursor()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useViewRole() {
|
|
34
|
+
return useHost().useViewRole()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function usePresentationMode(): PresentationMode {
|
|
38
|
+
return useHost().mode
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function useService<T>(token: ServiceToken<T>): ComputedRef<T | undefined> {
|
|
42
|
+
const host = useHost()
|
|
43
|
+
return computed(() => host.getService(token))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function useStartReport(): ComputedRef<StartReport | undefined> {
|
|
47
|
+
const host = useHost()
|
|
48
|
+
return computed(() => host.startReport())
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function useStaticRender(): Readonly<Ref<boolean>> {
|
|
52
|
+
return useHost().useStaticRender()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function useSectionConfig(pluginId: string): Readonly<Ref<Record<string, unknown>>> {
|
|
56
|
+
return useHost().useSectionConfig(pluginId)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SharedStateOptions {
|
|
60
|
+
scope?: SharedStateScope
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useSharedState<T>(key: string, initial: T, options: SharedStateOptions = {}): Ref<T> {
|
|
64
|
+
const host = (getCurrentInstance() ? inject(HOST, undefined) : undefined) ?? installed.adapter
|
|
65
|
+
if (!host) throw new Error('[presmium] no host installed: the host adapter must call installHost(app, adapter)')
|
|
66
|
+
return host.useSharedState(key, initial, options.scope ?? 'local')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const REDUCED_MOTION = '(prefers-reduced-motion: reduce)'
|
|
70
|
+
|
|
71
|
+
export function usePrefersReducedMotion(): Readonly<Ref<boolean>> {
|
|
72
|
+
const query = typeof window === 'undefined' ? undefined : window.matchMedia?.(REDUCED_MOTION)
|
|
73
|
+
const reduced = ref(query?.matches ?? false)
|
|
74
|
+
if (query) {
|
|
75
|
+
const update = (event: MediaQueryListEvent) => {
|
|
76
|
+
reduced.value = event.matches
|
|
77
|
+
}
|
|
78
|
+
query.addEventListener('change', update)
|
|
79
|
+
if (getCurrentScope()) onScopeDispose(() => query.removeEventListener('change', update))
|
|
80
|
+
}
|
|
81
|
+
return readonly(reduced)
|
|
82
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import './design/design.css'
|
|
2
|
+
|
|
3
|
+
export { blockAt, blockElement, blockKey, codeLines, lineNumbers, linesText, positionSlide, splitBlockKey } from './blocks.ts'
|
|
4
|
+
export type { BlockPoint } from './blocks.ts'
|
|
5
|
+
export { componentErrors, PmBoundary } from './boundary.ts'
|
|
6
|
+
export type { ComponentError } from './boundary.ts'
|
|
7
|
+
export {
|
|
8
|
+
PmBadge,
|
|
9
|
+
PmButton,
|
|
10
|
+
PmCard,
|
|
11
|
+
PmDialog,
|
|
12
|
+
PmDrawer,
|
|
13
|
+
PmEmpty,
|
|
14
|
+
PmField,
|
|
15
|
+
PmIcon,
|
|
16
|
+
PmIconButton,
|
|
17
|
+
PmMenu,
|
|
18
|
+
PmNotice,
|
|
19
|
+
PmPopover,
|
|
20
|
+
PmSegmented,
|
|
21
|
+
PmTabs,
|
|
22
|
+
PmToolbarEntry,
|
|
23
|
+
provideToolbarPlace,
|
|
24
|
+
useToolbarPlace,
|
|
25
|
+
} from './design/components.ts'
|
|
26
|
+
export type { ButtonVariant, Choice, MenuItem, PopoverPlacement, Tone } from './design/components.ts'
|
|
27
|
+
export { iconMarkup, ICONS } from '@lincyaw/design'
|
|
28
|
+
export type { IconName } from '@lincyaw/design'
|
|
29
|
+
export { useTheme } from './design/theme.ts'
|
|
30
|
+
export type { Theme } from './design/theme.ts'
|
|
31
|
+
export {
|
|
32
|
+
installHost,
|
|
33
|
+
useCursor,
|
|
34
|
+
useHost,
|
|
35
|
+
usePrefersReducedMotion,
|
|
36
|
+
usePresentationMode,
|
|
37
|
+
useSectionConfig,
|
|
38
|
+
useService,
|
|
39
|
+
useSharedState,
|
|
40
|
+
useStartReport,
|
|
41
|
+
useStaticRender,
|
|
42
|
+
useStepState,
|
|
43
|
+
useViewRole,
|
|
44
|
+
} from './host.ts'
|
|
45
|
+
export type { SharedStateOptions } from './host.ts'
|
|
46
|
+
export { pluginsOf, presmiumReport, presmiumService, registerPresmiumPlugin, reportPluginFailure, startHost, startPresmium, stopPresmium, stopPresmiumOnPageHide } from './kernel.ts'
|
|
47
|
+
export type { HostStart, StartOptions } from './kernel.ts'
|
|
48
|
+
export { currentLocale, useLocale, useMessages } from './locale.ts'
|
|
49
|
+
export type { Catalog } from './locale.ts'
|
|
50
|
+
export { PmApprovals } from './approvals.ts'
|
|
51
|
+
export { createOperations, createStatusRegistry } from './operations.ts'
|
|
52
|
+
export { createPresenterPanels, createToolbar } from './panels.ts'
|
|
53
|
+
export { createServerClient, provideServer, useServer } from './server.ts'
|
|
54
|
+
export { default as PmPresenterPanels } from './PresenterPanels.vue'
|
|
55
|
+
export { PmStepScope, provideStepScope, useStepScope } from './scope.ts'
|
|
56
|
+
export { claimFirst, useSessionSnapshot } from './session.ts'
|
|
57
|
+
export type { StepScope } from './scope.ts'
|
|
58
|
+
export { createSharedStates } from './shared.ts'
|
|
59
|
+
export type { SharedStates } from './shared.ts'
|
|
60
|
+
export { default as PmToolbar } from './Toolbar.vue'
|
|
61
|
+
export { NAVIGATION, OPERATIONS, STATUS, SYNC_TRANSPORTS, TOOLBAR } from './types.ts'
|
|
62
|
+
export { deckChannel, joinWindows } from './windows.ts'
|
|
63
|
+
export type { WindowChannel } from './windows.ts'
|
|
64
|
+
export type {
|
|
65
|
+
HostAdapter,
|
|
66
|
+
Navigation,
|
|
67
|
+
NavigationPosition,
|
|
68
|
+
Operations,
|
|
69
|
+
ReplayHandler,
|
|
70
|
+
ServerClient,
|
|
71
|
+
SharedStateScope,
|
|
72
|
+
StatusProvider,
|
|
73
|
+
StatusRegistry,
|
|
74
|
+
SyncPublish,
|
|
75
|
+
SyncTransport,
|
|
76
|
+
SyncTransports,
|
|
77
|
+
Toolbar,
|
|
78
|
+
ToolbarItem,
|
|
79
|
+
ToolbarPlace,
|
|
80
|
+
ToolbarSlot,
|
|
81
|
+
ViewRole,
|
|
82
|
+
} from './types.ts'
|
|
83
|
+
export { localeOf } from '@lincyaw/kernel/contracts'
|
|
84
|
+
export type { Locale } from '@lincyaw/kernel/contracts'
|
package/src/kernel.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Kernel, PRESENTER_PANELS } from '@lincyaw/kernel'
|
|
2
|
+
import type { EnvironmentSpec, Logger, PluginDefinition, PluginReport, PresentationMode, ServiceToken, StartReport } from '@lincyaw/kernel'
|
|
3
|
+
import { shallowRef } from 'vue'
|
|
4
|
+
import { PmApprovals } from './approvals.ts'
|
|
5
|
+
import { currentLocale } from './locale.ts'
|
|
6
|
+
import { HOST_MESSAGES } from './messages.ts'
|
|
7
|
+
import { createOperations, createStatusRegistry } from './operations.ts'
|
|
8
|
+
import { createPresenterPanels, createToolbar } from './panels.ts'
|
|
9
|
+
import { connectControl, provideServer } from './server.ts'
|
|
10
|
+
import { NAVIGATION, OPERATIONS, STATUS, SYNC_TRANSPORTS, TOOLBAR } from './types.ts'
|
|
11
|
+
import type { Navigation, ServerClient, SyncTransports } from './types.ts'
|
|
12
|
+
|
|
13
|
+
const registered: PluginDefinition[] = []
|
|
14
|
+
const running = shallowRef<Kernel>()
|
|
15
|
+
let starting: Promise<StartReport> | undefined
|
|
16
|
+
let disconnect: (() => void) | undefined
|
|
17
|
+
const failures = shallowRef<PluginReport[]>([])
|
|
18
|
+
|
|
19
|
+
export function registerPresmiumPlugin(plugin: PluginDefinition): void {
|
|
20
|
+
const { id } = plugin.manifest
|
|
21
|
+
if (starting) console.error(`[presmium] ${id} was registered after the kernel started; ignored`)
|
|
22
|
+
else if (registered.some(p => p.manifest.id === id)) console.error(`[presmium] plugin ${id} is already registered; ignored`)
|
|
23
|
+
else registered.push(plugin)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isPluginDefinition(value: unknown): value is PluginDefinition {
|
|
27
|
+
const plugin = value as Partial<PluginDefinition> | null
|
|
28
|
+
return typeof plugin === 'object' && plugin !== null && typeof plugin.setup === 'function' && typeof plugin.manifest?.id === 'string'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function pluginsOf(module: unknown, config: Record<string, unknown> = {}): PluginDefinition[] {
|
|
32
|
+
const exports = (module ?? {}) as Record<string, unknown>
|
|
33
|
+
const declared = typeof exports.presmiumPlugins === 'function' ? [(exports.presmiumPlugins as (config: Record<string, unknown>) => unknown)(config)] : []
|
|
34
|
+
const values = [...Object.values(exports), ...declared].flatMap(v => (Array.isArray(v) ? v : [v]))
|
|
35
|
+
return [...new Set(values.filter(isPluginDefinition))]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface StartOptions {
|
|
39
|
+
mode: PresentationMode
|
|
40
|
+
config?: Record<string, unknown>
|
|
41
|
+
beforeStart?(kernel: Kernel): void
|
|
42
|
+
log?: Logger
|
|
43
|
+
environments?: Record<string, EnvironmentSpec>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function startPresmium(options: StartOptions): Promise<StartReport> {
|
|
47
|
+
starting ??= (async () => {
|
|
48
|
+
const kernel = new Kernel({ mode: options.mode, config: options.config, log: options.log, environments: options.environments })
|
|
49
|
+
options.beforeStart?.(kernel)
|
|
50
|
+
for (const plugin of registered) kernel.register(plugin)
|
|
51
|
+
const report = await kernel.start()
|
|
52
|
+
running.value = kernel
|
|
53
|
+
return report
|
|
54
|
+
})()
|
|
55
|
+
return starting
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function stopPresmium(): Promise<void> {
|
|
59
|
+
const current = starting
|
|
60
|
+
if (!current) return
|
|
61
|
+
starting = undefined
|
|
62
|
+
disconnect?.()
|
|
63
|
+
disconnect = undefined
|
|
64
|
+
await current.catch(() => undefined)
|
|
65
|
+
const kernel = running.value
|
|
66
|
+
running.value = undefined
|
|
67
|
+
await kernel?.dispose()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function stopPresmiumOnPageHide(start: () => void): void {
|
|
71
|
+
window.addEventListener('pagehide', () => void stopPresmium())
|
|
72
|
+
window.addEventListener('pageshow', (event) => {
|
|
73
|
+
if (event.persisted) start()
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function presmiumService<T>(token: ServiceToken<T>): T | undefined {
|
|
78
|
+
return running.value?.getService(token)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function reportPluginFailure(id: string, reason: string): void {
|
|
82
|
+
failures.value = [...failures.value.filter(report => report.id !== id), { id, status: 'failed', reason }]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function presmiumReport(): StartReport | undefined {
|
|
86
|
+
const report = running.value?.report()
|
|
87
|
+
return report && failures.value.length ? { ...report, plugins: [...report.plugins, ...failures.value] } : report
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface HostStart {
|
|
91
|
+
mode: PresentationMode
|
|
92
|
+
config: Record<string, unknown>
|
|
93
|
+
navigation: Navigation
|
|
94
|
+
transports: SyncTransports
|
|
95
|
+
environments?: Record<string, EnvironmentSpec>
|
|
96
|
+
server?: ServerClient
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function startHost(options: HostStart): Promise<StartReport> {
|
|
100
|
+
if (options.server) provideServer(options.server)
|
|
101
|
+
const start = () => startPresmium({
|
|
102
|
+
mode: options.mode,
|
|
103
|
+
config: options.config,
|
|
104
|
+
environments: options.environments,
|
|
105
|
+
beforeStart(kernel) {
|
|
106
|
+
const toolbar = createToolbar()
|
|
107
|
+
const status = createStatusRegistry()
|
|
108
|
+
kernel.provide(PRESENTER_PANELS, createPresenterPanels())
|
|
109
|
+
kernel.provide(SYNC_TRANSPORTS, options.transports)
|
|
110
|
+
kernel.provide(NAVIGATION, options.navigation)
|
|
111
|
+
kernel.provide(TOOLBAR, toolbar)
|
|
112
|
+
kernel.provide(OPERATIONS, createOperations())
|
|
113
|
+
kernel.provide(STATUS, status)
|
|
114
|
+
disconnect?.()
|
|
115
|
+
disconnect = undefined
|
|
116
|
+
const { server } = options
|
|
117
|
+
if (!server?.available) return
|
|
118
|
+
toolbar.register({
|
|
119
|
+
id: 'presmium.approvals',
|
|
120
|
+
get title() {
|
|
121
|
+
return HOST_MESSAGES[currentLocale()].approvalsTitle
|
|
122
|
+
},
|
|
123
|
+
component: PmApprovals,
|
|
124
|
+
order: 1000,
|
|
125
|
+
})
|
|
126
|
+
disconnect = connectControl(server, { navigation: options.navigation, status, service: token => kernel.getService(token) })
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
stopPresmiumOnPageHide(() => void start())
|
|
130
|
+
return start()
|
|
131
|
+
}
|