@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/src/locale.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { localeOf } from '@lincyaw/kernel/contracts'
2
+ import type { Locale } from '@lincyaw/kernel/contracts'
3
+ import type { ComputedRef } from 'vue'
4
+ import { computed, getCurrentInstance, inject, shallowRef } from 'vue'
5
+ import { HOST, installed } from './key.ts'
6
+
7
+ export type Catalog<T> = Record<Locale, T>
8
+
9
+ const htmlLang = shallowRef<string>()
10
+ let observed = false
11
+
12
+ function observeHtmlLang(): void {
13
+ if (observed || typeof document === 'undefined') return
14
+ observed = true
15
+ const root = document.documentElement
16
+ htmlLang.value = root.lang
17
+ if (typeof MutationObserver !== 'undefined') {
18
+ new MutationObserver(() => {
19
+ htmlLang.value = root.lang
20
+ }).observe(root, { attributes: true, attributeFilter: ['lang'] })
21
+ }
22
+ }
23
+
24
+ function resolveLocale(fallbackLang?: string): Locale {
25
+ observeHtmlLang()
26
+ const browser = typeof navigator === 'undefined' ? undefined : navigator.language
27
+ return localeOf(htmlLang.value || fallbackLang) ?? localeOf(browser) ?? 'en'
28
+ }
29
+
30
+ export function currentLocale(): Locale {
31
+ return resolveLocale(installed.adapter?.lang)
32
+ }
33
+
34
+ export function useLocale(): ComputedRef<Locale> {
35
+ const host = getCurrentInstance() ? inject(HOST, undefined) : undefined
36
+ return computed(() => resolveLocale(host?.lang))
37
+ }
38
+
39
+ export function useMessages<T>(catalog: Catalog<T>): ComputedRef<T> {
40
+ const locale = useLocale()
41
+ return computed(() => catalog[locale.value])
42
+ }
@@ -0,0 +1,51 @@
1
+ import type { PluginStatus } from '@lincyaw/kernel/contracts'
2
+ import type { Catalog } from './locale.ts'
3
+
4
+ export interface HostMessages {
5
+ panels: string
6
+ startReport: string
7
+ starting: string
8
+ noPlugins: string
9
+ status: Record<PluginStatus, string>
10
+ because: string
11
+ componentError(component: string, message: string): string
12
+ problems(count: number): string
13
+ approvals(count: number): string
14
+ approvalsTitle: string
15
+ allow: string
16
+ deny: string
17
+ from(who: string): string
18
+ }
19
+
20
+ export const HOST_MESSAGES: Catalog<HostMessages> = {
21
+ 'zh-CN': {
22
+ panels: '课堂与工具',
23
+ startReport: '启动报告',
24
+ starting: '内核启动中……',
25
+ noPlugins: '没有注册插件。',
26
+ status: { started: '已启动', degraded: '已降级', disabled: '已禁用', failed: '启动失败' },
27
+ because: ':',
28
+ componentError: (component, message) => `${component} 出错:${message}`,
29
+ problems: count => `${count} 个问题`,
30
+ approvals: count => `待确认 ${count}`,
31
+ approvalsTitle: '待确认的操作',
32
+ allow: '允许',
33
+ deny: '拒绝',
34
+ from: who => `来自 ${who}`,
35
+ },
36
+ 'en': {
37
+ panels: 'Panels',
38
+ startReport: 'Start report',
39
+ starting: 'Starting the kernel…',
40
+ noPlugins: 'No plugins registered.',
41
+ status: { started: 'started', degraded: 'degraded', disabled: 'disabled', failed: 'failed to start' },
42
+ because: ': ',
43
+ componentError: (component, message) => `${component} failed: ${message}`,
44
+ problems: count => `${count} ${count === 1 ? 'problem' : 'problems'}`,
45
+ approvals: count => `Approvals ${count}`,
46
+ approvalsTitle: 'Operations to approve',
47
+ allow: 'Allow',
48
+ deny: 'Deny',
49
+ from: who => `from ${who}`,
50
+ },
51
+ }
@@ -0,0 +1,76 @@
1
+ import type { Operation, Unsubscribe } from '@lincyaw/kernel/contracts'
2
+ import { ref } from 'vue'
3
+ import type { Operations, ReplayHandler, StatusProvider, StatusRegistry } from './types.ts'
4
+
5
+ export function createOperations(): Operations {
6
+ const listeners = new Set<(operation: Operation) => void>()
7
+ const handlers = new Map<string, ReplayHandler>()
8
+ const replaying = ref(false)
9
+ let unsettled = 0
10
+ return {
11
+ report(operation) {
12
+ if (replaying.value) return
13
+ for (const listener of [...listeners]) {
14
+ try {
15
+ listener(operation)
16
+ } catch (error) {
17
+ console.error('[presmium] an operations subscriber failed', error)
18
+ }
19
+ }
20
+ },
21
+ subscribe(listener) {
22
+ listeners.add(listener)
23
+ return () => {
24
+ listeners.delete(listener)
25
+ }
26
+ },
27
+ onReplay(type, handler): Unsubscribe {
28
+ handlers.set(type, handler)
29
+ return () => {
30
+ if (handlers.get(type) === handler) handlers.delete(type)
31
+ }
32
+ },
33
+ replay(operation, seeking = false) {
34
+ const handler = handlers.get(operation.type)
35
+ if (!handler) return false
36
+ unsettled++
37
+ replaying.value = true
38
+ try {
39
+ handler.apply(operation.data, seeking)
40
+ } catch (error) {
41
+ console.error(`[presmium] the replay of ${operation.type} failed`, error)
42
+ } finally {
43
+ setTimeout(() => {
44
+ if (--unsettled === 0) replaying.value = false
45
+ }, 0)
46
+ }
47
+ return true
48
+ },
49
+ clear() {
50
+ for (const [type, handler] of handlers) {
51
+ try {
52
+ handler.clear?.()
53
+ } catch (error) {
54
+ console.error(`[presmium] clearing ${type} failed`, error)
55
+ }
56
+ }
57
+ },
58
+ replaying,
59
+ }
60
+ }
61
+
62
+ export function createStatusRegistry(onChange?: () => void): StatusRegistry {
63
+ const providers = new Map<string, StatusProvider>()
64
+ return {
65
+ register(provider) {
66
+ providers.set(provider.id, provider)
67
+ onChange?.()
68
+ return () => {
69
+ if (providers.get(provider.id) !== provider) return
70
+ providers.delete(provider.id)
71
+ onChange?.()
72
+ }
73
+ },
74
+ list: () => [...providers.values()],
75
+ }
76
+ }
package/src/panels.ts ADDED
@@ -0,0 +1,60 @@
1
+ import type { PresenterPanel, PresenterPanels, Unsubscribe } from '@lincyaw/kernel'
2
+ import { currentLocale } from './locale.ts'
3
+ import { HOST_MESSAGES } from './messages.ts'
4
+ import StartReportPanel from './StartReportPanel.vue'
5
+ import type { Toolbar, ToolbarItem } from './types.ts'
6
+
7
+ interface Item {
8
+ id: string
9
+ order?: number
10
+ }
11
+
12
+ interface Registry<T extends Item> {
13
+ register(item: T): Unsubscribe
14
+ list(): T[]
15
+ subscribe(listener: () => void): Unsubscribe
16
+ }
17
+
18
+ function createRegistry<T extends Item>(kind: string): Registry<T> {
19
+ const items = new Map<string, T>()
20
+ const listeners = new Set<() => void>()
21
+ const notify = () => listeners.forEach(listener => listener())
22
+ return {
23
+ register(item) {
24
+ if (items.has(item.id)) throw new Error(`${kind} ${item.id} is already registered`)
25
+ items.set(item.id, item)
26
+ notify()
27
+ return () => {
28
+ if (items.get(item.id) !== item) return
29
+ items.delete(item.id)
30
+ notify()
31
+ }
32
+ },
33
+ list() {
34
+ return [...items.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
35
+ },
36
+ subscribe(listener) {
37
+ listeners.add(listener)
38
+ return () => {
39
+ listeners.delete(listener)
40
+ }
41
+ },
42
+ }
43
+ }
44
+
45
+ export function createPresenterPanels(): PresenterPanels {
46
+ const service = createRegistry<PresenterPanel>('presenter panel')
47
+ service.register({
48
+ id: 'presmium.core.start-report',
49
+ get title() {
50
+ return HOST_MESSAGES[currentLocale()].startReport
51
+ },
52
+ component: StartReportPanel,
53
+ order: -1,
54
+ })
55
+ return service
56
+ }
57
+
58
+ export function createToolbar(): Toolbar {
59
+ return createRegistry<ToolbarItem>('toolbar item')
60
+ }
package/src/scope.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { InjectionKey, MaybeRefOrGetter, Ref } from 'vue'
2
+ import { computed, defineComponent, inject, provide, ref, toValue } from 'vue'
3
+
4
+ const STEP_SCOPE: InjectionKey<Readonly<Ref<number>>> = Symbol('presmium.stepScope')
5
+
6
+ const NO_SCOPE = ref(0)
7
+
8
+ export function provideStepScope(at: MaybeRefOrGetter<number>): void {
9
+ provide(STEP_SCOPE, computed(() => toValue(at)))
10
+ }
11
+
12
+ export interface StepScope {
13
+ readonly at: Readonly<Ref<number>>
14
+ step(k: number): number
15
+ }
16
+
17
+ export function useStepScope(): StepScope {
18
+ const at = inject(STEP_SCOPE, NO_SCOPE)
19
+ return { at, step: k => at.value + k }
20
+ }
21
+
22
+ export const PmStepScope = defineComponent({
23
+ name: 'PmStepScope',
24
+ props: { at: { type: Number, default: 0 } },
25
+ setup(props, { slots }) {
26
+ provideStepScope(() => props.at)
27
+ return () => slots.default?.()
28
+ },
29
+ })
package/src/server.ts ADDED
@@ -0,0 +1,173 @@
1
+ import { SESSION } from '@lincyaw/kernel/contracts'
2
+ import type { ApprovalRequest, Member, ServiceToken, Unsubscribe } from '@lincyaw/kernel/contracts'
3
+ import type { Ref } from 'vue'
4
+ import { computed, shallowRef, watch } from 'vue'
5
+ import { currentLocale } from './locale.ts'
6
+ import type { Navigation, NavigationPosition, ServerClient, StatusRegistry, ViewRole } from './types.ts'
7
+
8
+ export const SERVER_PATH = '/__presmium/presmium.server'
9
+
10
+ const UNAVAILABLE: ServerClient = {
11
+ available: false,
12
+ member: computed(() => undefined),
13
+ fetch: () => Promise.reject(new Error('no Presmium server serves this page')),
14
+ events() {
15
+ throw new Error('no Presmium server serves this page')
16
+ },
17
+ }
18
+
19
+ const current = shallowRef<ServerClient>(UNAVAILABLE)
20
+ const documents = new WeakMap<ServerClient, string>()
21
+
22
+ const facade: ServerClient = {
23
+ get available() {
24
+ return current.value.available
25
+ },
26
+ get member() {
27
+ return current.value.member
28
+ },
29
+ fetch: (plugin, path, init) => current.value.fetch(plugin, path, init),
30
+ events: (plugin, path) => current.value.events(plugin, path),
31
+ }
32
+
33
+ export function useServer(): ServerClient {
34
+ return facade
35
+ }
36
+
37
+ export function provideServer(client: ServerClient | undefined): void {
38
+ current.value = client ?? UNAVAILABLE
39
+ }
40
+
41
+ function documentHeader(document: string): string {
42
+ return encodeURIComponent(document)
43
+ }
44
+
45
+ export function createServerClient(options: { document?: string } = {}): ServerClient {
46
+ const document = options.document ?? ''
47
+ const member = shallowRef<Member>()
48
+ const url = (plugin: string, path: string) => `/__presmium/${plugin}/${path.replace(/^\//, '')}`
49
+ const client: ServerClient = {
50
+ available: true,
51
+ member: computed(() => member.value),
52
+ fetch(plugin, path, init = {}) {
53
+ const headers = new Headers(init.headers)
54
+ headers.set('Presmium-Document', documentHeader(document))
55
+ if (!headers.has('Accept-Language')) headers.set('Accept-Language', currentLocale())
56
+ return fetch(url(plugin, path), { ...init, headers })
57
+ },
58
+ events(plugin, path) {
59
+ const target = url(plugin, path)
60
+ return new EventSource(`${target}${target.includes('?') ? '&' : '?'}document=${documentHeader(document)}`)
61
+ },
62
+ }
63
+ void fetch(`${SERVER_PATH}/hello`, { headers: { 'Presmium-Document': documentHeader(document) } })
64
+ .then(response => (response.ok ? response.json() : response.text().then(() => undefined)))
65
+ .then((hello: { member?: Member } | undefined) => {
66
+ member.value = hello?.member
67
+ })
68
+ .catch(() => undefined)
69
+ documents.set(client, document)
70
+ return client
71
+ }
72
+
73
+ export interface PendingApproval {
74
+ id: string
75
+ request: ApprovalRequest
76
+ }
77
+
78
+ export interface ApprovalState {
79
+ pending: Ref<PendingApproval[]>
80
+ answer(id: string, allow: boolean): Promise<void>
81
+ }
82
+
83
+ const pending = shallowRef<PendingApproval[]>([])
84
+ let answering: (id: string, allow: boolean) => Promise<void> = async () => {}
85
+
86
+ export const approvals: ApprovalState = {
87
+ pending,
88
+ answer: (id, allow) => answering(id, allow),
89
+ }
90
+
91
+ function roleOf(position: NavigationPosition): ViewRole {
92
+ if (position.view === 'presenter') return 'presenter'
93
+ if (position.view === 'present') return 'audience'
94
+ if (position.view !== undefined) return 'reader'
95
+ return typeof location !== 'undefined' && /\/presenter(?:\/|$)/.test(location.pathname) ? 'presenter' : 'audience'
96
+ }
97
+
98
+ export function connectControl(client: ServerClient, host: {
99
+ navigation: Navigation
100
+ status: StatusRegistry
101
+ service<T>(token: ServiceToken<T>): T | undefined
102
+ }): Unsubscribe {
103
+ const { navigation, status } = host
104
+ const document = documents.get(client) ?? ''
105
+ const id = Math.random().toString(36).slice(2, 12)
106
+ const post = (body: Record<string, unknown>) => fetch(`${SERVER_PATH}/control`, {
107
+ method: 'POST',
108
+ headers: { 'Content-Type': 'application/json', 'Presmium-Document': documentHeader(document) },
109
+ body: JSON.stringify({ window: id, ...body }),
110
+ }).then(async response => ({ ok: response.ok, status: response.status, text: await response.text() }))
111
+ answering = async (approval, allow) => {
112
+ const response = await post({ type: 'answer', id: approval, allow })
113
+ if (!response.ok) throw new Error(`the server did not take the answer (${response.status})`)
114
+ pending.value = pending.value.filter(p => p.id !== approval)
115
+ }
116
+ const source = new EventSource(`${SERVER_PATH}/control?window=${id}&document=${documentHeader(document)}`)
117
+ const on = <T>(event: string, handle: (data: T) => void) => source.addEventListener(event, (message) => {
118
+ try {
119
+ handle(JSON.parse((message as MessageEvent<string>).data) as T)
120
+ } catch (error) {
121
+ console.error(`[presmium] control channel: bad ${event} message`, error)
122
+ }
123
+ })
124
+ on<PendingApproval>('approval', (approval) => {
125
+ if (!pending.value.some(p => p.id === approval.id)) pending.value = [...pending.value, approval]
126
+ })
127
+ on<{ id: string }>('settled', (settled) => {
128
+ pending.value = pending.value.filter(p => p.id !== settled.id)
129
+ })
130
+ on<NavigationPosition>('go', position => navigation.go(position))
131
+ on<{ action: 'start' | 'end', name?: string }>('session', ({ action, name }) => {
132
+ const session = host.service(SESSION)
133
+ if (!session) return
134
+ void (action === 'start' ? session.create(name ?? '') : session.end()).catch((error: unknown) => {
135
+ console.error('[presmium] the classroom command failed', error)
136
+ })
137
+ })
138
+
139
+ let reported = ''
140
+ const report = () => {
141
+ const position = navigation.position.value
142
+ const summaries = Object.fromEntries(status.list().map((provider) => {
143
+ try {
144
+ return [provider.id, provider.read()]
145
+ } catch {
146
+ return [provider.id, null]
147
+ }
148
+ }))
149
+ const body = JSON.stringify({ position, role: roleOf(position), status: summaries })
150
+ if (body === reported) return
151
+ reported = body
152
+ void post({ type: 'report', ...JSON.parse(body) as Record<string, unknown> }).catch(() => undefined)
153
+ }
154
+ source.addEventListener('open', () => {
155
+ reported = ''
156
+ report()
157
+ })
158
+ let soon: ReturnType<typeof setTimeout> | undefined
159
+ const stopWatch = watch(() => navigation.position.value, () => {
160
+ soon ??= setTimeout(() => {
161
+ soon = undefined
162
+ report()
163
+ }, 200)
164
+ }, { deep: true })
165
+ const timer = setInterval(report, 1000)
166
+ return () => {
167
+ stopWatch()
168
+ clearTimeout(soon)
169
+ clearInterval(timer)
170
+ source.close()
171
+ pending.value = []
172
+ }
173
+ }
package/src/session.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { SESSION } from '@lincyaw/kernel'
2
+ import type { SessionService, SessionSnapshot } from '@lincyaw/kernel'
3
+ import type { ComputedRef, Ref } from 'vue'
4
+ import { computed, getCurrentScope, onScopeDispose, shallowRef, watchEffect } from 'vue'
5
+ import { useService } from './host.ts'
6
+
7
+ export function useSessionSnapshot(): { session: ComputedRef<SessionService | undefined>, snapshot: Readonly<Ref<SessionSnapshot | undefined>> } {
8
+ const session = useService(SESSION)
9
+ const snapshot = shallowRef<SessionSnapshot>()
10
+ watchEffect((onCleanup) => {
11
+ const current = session.value
12
+ snapshot.value = current?.snapshot()
13
+ if (current) onCleanup(current.subscribe(next => snapshot.value = next))
14
+ })
15
+ return { session, snapshot }
16
+ }
17
+
18
+ const groups = new Map<string, Ref<symbol[]>>()
19
+
20
+ export function claimFirst(group: string): { owns: Readonly<Ref<boolean>>, release(): void } {
21
+ let owners = groups.get(group)
22
+ if (!owners) {
23
+ owners = shallowRef<symbol[]>([])
24
+ groups.set(group, owners)
25
+ }
26
+ const list = owners
27
+ const token = Symbol(group)
28
+ list.value = [...list.value, token]
29
+ const release = () => {
30
+ list.value = list.value.filter(t => t !== token)
31
+ }
32
+ if (getCurrentScope()) onScopeDispose(release)
33
+ return { owns: computed(() => list.value[0] === token), release }
34
+ }
package/src/shared.ts ADDED
@@ -0,0 +1,85 @@
1
+ import type { Ref } from 'vue'
2
+ import { effectScope, ref, toRaw, watch } from 'vue'
3
+ import type { SharedStateScope, SyncPublish, SyncTransport, SyncTransports } from './types.ts'
4
+ import { joinWindows } from './windows.ts'
5
+
6
+ interface Channel {
7
+ name: string
8
+ receive(patch: Record<string, unknown>): void
9
+ value(): unknown
10
+ publishers: Map<SyncTransport, SyncPublish>
11
+ }
12
+
13
+ export interface SharedStates {
14
+ sharedState<T>(key: string, initial: T, scope: SharedStateScope): Ref<T>
15
+ transports: SyncTransports
16
+ }
17
+
18
+ const plain = (value: unknown): unknown => value === undefined ? undefined : JSON.parse(JSON.stringify(toRaw(value)))
19
+
20
+ export function createSharedStates(deck: string): SharedStates {
21
+ const states = new Map<string, Ref<unknown>>()
22
+ const channels = new Map<string, Channel>()
23
+ const transports = new Set<SyncTransport>()
24
+ const detached = effectScope(true)
25
+
26
+ const connect = (transport: SyncTransport, channel: Channel) => {
27
+ const publish = transport.connect(channel.name, channel.receive, { value: channel.value() }, false)
28
+ if (publish) channel.publishers.set(transport, publish)
29
+ }
30
+
31
+ function create(id: string, initial: unknown, scope: SharedStateScope): Ref<unknown> {
32
+ const state = ref(plain(initial))
33
+ if (scope === 'view') return state
34
+ const name = `presmium.shared:${deck}:${id}`
35
+ let from: 'window' | 'remote' | undefined
36
+ let changed = false
37
+ const apply = (value: unknown, source: 'window' | 'remote') => {
38
+ from = source
39
+ state.value = value
40
+ from = undefined
41
+ }
42
+ const windows = joinWindows(name, value => apply(value, 'window'), () => changed ? plain(state.value) : undefined)
43
+ const channel: Channel = {
44
+ name,
45
+ receive(patch) {
46
+ if ('value' in patch) apply(patch.value, 'remote')
47
+ },
48
+ value: () => plain(state.value),
49
+ publishers: new Map(),
50
+ }
51
+ watch(state, (value) => {
52
+ changed = true
53
+ const data = plain(value)
54
+ if (from === undefined) windows?.send(data)
55
+ channel.publishers.forEach(publish => publish({ value: data }, from !== undefined))
56
+ }, { deep: true, flush: 'sync' })
57
+ if (scope === 'remote') {
58
+ channels.set(id, channel)
59
+ transports.forEach(transport => connect(transport, channel))
60
+ }
61
+ return state
62
+ }
63
+
64
+ return {
65
+ sharedState<T>(key: string, initial: T, scope: SharedStateScope): Ref<T> {
66
+ const id = `${scope}:${key}`
67
+ let state = states.get(id)
68
+ if (!state) {
69
+ state = detached.run(() => create(id, initial, scope))!
70
+ states.set(id, state)
71
+ }
72
+ return state as Ref<T>
73
+ },
74
+ transports: {
75
+ add(transport) {
76
+ transports.add(transport)
77
+ channels.forEach(channel => connect(transport, channel))
78
+ return () => {
79
+ transports.delete(transport)
80
+ channels.forEach(channel => channel.publishers.delete(transport))
81
+ }
82
+ },
83
+ },
84
+ }
85
+ }
package/src/types.ts ADDED
@@ -0,0 +1,107 @@
1
+ import type { Ref } from 'vue'
2
+ import { defineService } from '@lincyaw/kernel/contracts'
3
+ import type { Cursor, Member, Operation, PresentationMode, ServiceToken, StartReport, Unsubscribe } from '@lincyaw/kernel/contracts'
4
+ import type { IconName } from '@lincyaw/design'
5
+
6
+ export type ViewRole = 'audience' | 'presenter' | 'reader'
7
+
8
+ export type SharedStateScope = 'view' | 'local' | 'remote'
9
+
10
+ export interface HostAdapter {
11
+ readonly mode: PresentationMode
12
+ readonly lang?: string
13
+ useCursor(): Readonly<Ref<Cursor>>
14
+ useStep(): Readonly<Ref<number>>
15
+ useViewRole(): Readonly<Ref<ViewRole>>
16
+ useStaticRender(): Readonly<Ref<boolean>>
17
+ useSectionConfig(pluginId: string): Readonly<Ref<Record<string, unknown>>>
18
+ useSharedState<T>(key: string, initial: T, scope: SharedStateScope): Ref<T>
19
+ getService<T>(token: ServiceToken<T>): T | undefined
20
+ startReport(): StartReport | undefined
21
+ useDarkMode?(): Ref<boolean>
22
+ }
23
+
24
+ export type SyncPublish = (state: Readonly<Record<string, unknown>>, fromRemote: boolean) => void
25
+
26
+ export interface SyncTransport {
27
+ connect(
28
+ channel: string,
29
+ receive: (patch: Record<string, unknown>) => void,
30
+ state: Readonly<Record<string, unknown>>,
31
+ persist: boolean,
32
+ ): SyncPublish | undefined
33
+ }
34
+
35
+ export interface SyncTransports {
36
+ add(transport: SyncTransport): Unsubscribe
37
+ }
38
+
39
+ export const SYNC_TRANSPORTS = defineService<SyncTransports>('presmium.syncTransports')
40
+
41
+ export interface NavigationPosition extends Cursor {
42
+ view?: string
43
+ section?: string
44
+ }
45
+
46
+ export interface Navigation {
47
+ readonly position: Readonly<Ref<NavigationPosition>>
48
+ go(position: NavigationPosition): void
49
+ }
50
+
51
+ export const NAVIGATION = defineService<Navigation>('presmium.navigation')
52
+
53
+ export type ToolbarSlot = 'tools' | 'settings' | 'status'
54
+
55
+ export type ToolbarPlace = 'bar' | 'status'
56
+
57
+ export interface ToolbarItem {
58
+ id: string
59
+ title: string
60
+ component: unknown
61
+ order?: number
62
+ slot?: ToolbarSlot
63
+ icon?: IconName
64
+ }
65
+
66
+ export interface Toolbar {
67
+ register(item: ToolbarItem): Unsubscribe
68
+ list(): ToolbarItem[]
69
+ subscribe(listener: () => void): Unsubscribe
70
+ }
71
+
72
+ export const TOOLBAR = defineService<Toolbar>('presmium.toolbar')
73
+
74
+ export interface ServerClient {
75
+ readonly available: boolean
76
+ readonly member: Readonly<Ref<Member | undefined>>
77
+ fetch(plugin: string, path: string, init?: RequestInit): Promise<Response>
78
+ events(plugin: string, path: string): EventSource
79
+ }
80
+
81
+ export interface StatusProvider {
82
+ id: string
83
+ read(): unknown
84
+ }
85
+
86
+ export interface StatusRegistry {
87
+ register(provider: StatusProvider): Unsubscribe
88
+ list(): StatusProvider[]
89
+ }
90
+
91
+ export const STATUS = defineService<StatusRegistry>('presmium.status')
92
+
93
+ export interface ReplayHandler {
94
+ apply(data: Record<string, unknown>, seeking: boolean): void
95
+ clear?(): void
96
+ }
97
+
98
+ export interface Operations {
99
+ report(operation: Operation): void
100
+ subscribe(listener: (operation: Operation) => void): Unsubscribe
101
+ onReplay(type: string, handler: ReplayHandler): Unsubscribe
102
+ replay(operation: Operation, seeking?: boolean): boolean
103
+ clear(): void
104
+ readonly replaying: Ref<boolean>
105
+ }
106
+
107
+ export const OPERATIONS = defineService<Operations>('presmium.operations')
@@ -0,0 +1,6 @@
1
+ declare module '*.vue' {
2
+ import type { DefineComponent } from 'vue'
3
+
4
+ const component: DefineComponent
5
+ export default component
6
+ }