@lincyaw/host 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.
- package/README.md +14 -15
- package/package.json +16 -7
- package/src/actions.ts +22 -0
- package/src/approvals.ts +9 -55
- package/src/blocks.ts +9 -10
- package/src/claim-first.ts +13 -0
- package/src/code-lines-box.ts +11 -0
- package/src/{types.ts → contracts.ts} +62 -12
- package/src/control.ts +117 -0
- package/src/design/appearance.ts +16 -0
- package/src/design/caption.ts +31 -0
- package/src/design/components.ts +132 -316
- package/src/design/design.css +8 -0
- package/src/design/layout.ts +28 -0
- package/src/error-boundary.ts +18 -0
- package/src/host-key.ts +4 -0
- package/src/host.ts +42 -77
- package/src/index.ts +55 -25
- package/src/locale.ts +15 -37
- package/src/messages.ts +25 -10
- package/src/operations.ts +6 -22
- package/src/panels.ts +25 -57
- package/src/playback-control.ts +67 -0
- package/src/registries.ts +86 -0
- package/src/resident-worker.ts +84 -0
- package/src/runtime.ts +187 -0
- package/src/server.ts +27 -164
- package/src/session.ts +5 -29
- package/src/shared-state.ts +30 -0
- package/src/site.ts +14 -0
- package/src/start-report-panel.ts +8 -0
- package/src/step-clock.ts +49 -0
- package/src/step-scope.ts +38 -0
- package/src/store.ts +33 -0
- package/src/styles.d.ts +1 -0
- package/src/windows.ts +4 -2
- package/src/PresenterPanels.vue +0 -91
- package/src/StartReportPanel.vue +0 -45
- package/src/Toolbar.vue +0 -49
- package/src/boundary.ts +0 -56
- package/src/design/theme.ts +0 -32
- package/src/kernel.ts +0 -131
- package/src/key.ts +0 -6
- package/src/scope.ts +0 -29
- package/src/shared.ts +0 -85
- package/src/vue-shim.d.ts +0 -6
package/src/host.ts
CHANGED
|
@@ -1,82 +1,47 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
import { createElement, useContext, useEffect, useState } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { stepStateAt } from '@lincyaw/kernel'
|
|
4
|
+
import type { ServiceToken, StepRange, StepState } from '@lincyaw/kernel/contracts'
|
|
5
|
+
import { HOST, installed } from './host-key.ts'
|
|
6
|
+
import type { HostAdapter, SharedStateScope } from './contracts.ts'
|
|
7
|
+
import { useStepScope } from './step-scope.ts'
|
|
8
|
+
import { runtimeRevision } from './runtime.ts'
|
|
9
|
+
import { createStore, useStore } from './store.ts'
|
|
10
|
+
import type { Store } from './store.ts'
|
|
11
|
+
import { PmBoundary } from './error-boundary.ts'
|
|
12
|
+
|
|
13
|
+
export function installHost(adapter: HostAdapter): void { installed.adapter = adapter }
|
|
14
|
+
export function HostProvider({ adapter, children }: { adapter: HostAdapter, children?: ReactNode }) {
|
|
15
|
+
return createElement(HOST.Provider, { value: adapter }, children)
|
|
15
16
|
}
|
|
16
|
-
|
|
17
17
|
export function useHost(): HostAdapter {
|
|
18
|
-
const host =
|
|
19
|
-
if (!host) throw new Error('[presmium] no host installed
|
|
18
|
+
const host = useContext(HOST) ?? installed.adapter
|
|
19
|
+
if (!host) throw new Error('[presmium] no host installed')
|
|
20
20
|
return host
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
export function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
export function
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
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)
|
|
22
|
+
export function useDoc() { const host = useHost(); if (!host.useDoc) throw new Error("[presmium] this host has no document"); return host.useDoc() }
|
|
23
|
+
export function useStep(): number { return useHost().useStep() }
|
|
24
|
+
export function useStepState(range: StepRange): StepState { const step = useStep(); const scope = useStepScope(); return stepStateAt(range, scope.localStepAt(step)) }
|
|
25
|
+
export function useCursor() { return useHost().useCursor() }
|
|
26
|
+
export function useViewRole() { return useHost().useViewRole() }
|
|
27
|
+
export function usePresentationMode() { const host = useHost(); host.useViewRole(); return host.mode }
|
|
28
|
+
export function useService<T>(token: ServiceToken<T>): T | undefined { useStore(runtimeRevision); return useHost().getService(token) }
|
|
29
|
+
export function useStartReport() { useStore(runtimeRevision); return useHost().startReport() }
|
|
30
|
+
export function useStaticRender() { return useHost().useStaticRender() }
|
|
31
|
+
export function useSectionConfig(pluginId: string) { return useHost().useSectionConfig(pluginId) }
|
|
32
|
+
export interface SharedStateOptions { scope?: SharedStateScope }
|
|
33
|
+
export function useSharedState<T>(key: string, initial: T, options: SharedStateOptions = {}) { return useHost().useSharedState(key, initial, options.scope ?? 'local') }
|
|
34
|
+
export function usePrefersReducedMotion(): boolean {
|
|
35
|
+
const [reduced, setReduced] = useState(() => typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches)
|
|
36
|
+
useEffect(() => { const query = matchMedia('(prefers-reduced-motion: reduce)'); const update = () => setReduced(query.matches); update(); query.addEventListener('change', update); return () => query.removeEventListener('change', update) }, [])
|
|
37
|
+
return reduced
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const detachedShared = new Map<string, Store<unknown>>()
|
|
41
|
+
export function getSharedStateStore<T>(key: string, initial: T, options: SharedStateOptions = {}): Store<T> {
|
|
42
|
+
const scope = options.scope ?? 'local'
|
|
43
|
+
if (installed.adapter?.sharedStateStore) return installed.adapter.sharedStateStore(key, initial, scope)
|
|
44
|
+
const id = `${scope}:${key}`
|
|
45
|
+
let store = detachedShared.get(id); if (!store) { store = createStore<unknown>(initial); detachedShared.set(id, store) }
|
|
46
|
+
return store as Store<T>
|
|
82
47
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
/// <reference path="./styles.d.ts" />
|
|
1
2
|
import './design/design.css'
|
|
2
3
|
|
|
3
|
-
export {
|
|
4
|
+
export { useActions } from './actions.ts'
|
|
5
|
+
export type { ActionOptions, ComponentAction } from './actions.ts'
|
|
6
|
+
export { blockAt, blockElement, blockKey, codeLines, lineNumbers, linesText, splitBlockKey } from './blocks.ts'
|
|
4
7
|
export type { BlockPoint } from './blocks.ts'
|
|
5
|
-
export { componentErrors, PmBoundary } from './boundary.ts'
|
|
6
|
-
export type { ComponentError } from './boundary.ts'
|
|
8
|
+
export { componentErrors, componentErrorRevision, PmBoundary } from './error-boundary.ts'
|
|
9
|
+
export type { ComponentError } from './error-boundary.ts'
|
|
7
10
|
export {
|
|
8
11
|
PmBadge,
|
|
9
12
|
PmButton,
|
|
@@ -11,6 +14,7 @@ export {
|
|
|
11
14
|
PmDialog,
|
|
12
15
|
PmDrawer,
|
|
13
16
|
PmEmpty,
|
|
17
|
+
PmSkeleton,
|
|
14
18
|
PmField,
|
|
15
19
|
PmIcon,
|
|
16
20
|
PmIconButton,
|
|
@@ -19,17 +23,26 @@ export {
|
|
|
19
23
|
PmPopover,
|
|
20
24
|
PmSegmented,
|
|
21
25
|
PmTabs,
|
|
26
|
+
PmTabPanel,
|
|
27
|
+
PmTooltip,
|
|
22
28
|
PmToolbarEntry,
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
provideToolbarPlacement,
|
|
30
|
+
useToolbarPlacement,
|
|
25
31
|
} from './design/components.ts'
|
|
26
|
-
export type { ButtonVariant, Choice, MenuItem, PopoverPlacement, Tone } from './design/components.ts'
|
|
32
|
+
export type { ButtonVariant, Choice, ControlDensity, MenuItem, PopoverPlacement, Tone } from './design/components.ts'
|
|
33
|
+
export { PmColumns, PmStack, PmTabGroup } from './design/layout.ts'
|
|
34
|
+
export { PmCaption, PmCaptionProvider } from './design/caption.ts'
|
|
35
|
+
export type { CaptionProps } from './design/caption.ts'
|
|
36
|
+
export type { ContentLayoutProps, ColumnsLayoutProps, TabLayoutEntry } from './design/layout.ts'
|
|
27
37
|
export { iconMarkup, ICONS } from '@lincyaw/design'
|
|
28
38
|
export type { IconName } from '@lincyaw/design'
|
|
29
|
-
export {
|
|
30
|
-
export type {
|
|
39
|
+
export { useAppearance } from './design/appearance.ts'
|
|
40
|
+
export type { Appearance } from './design/appearance.ts'
|
|
31
41
|
export {
|
|
32
42
|
installHost,
|
|
43
|
+
HostProvider,
|
|
44
|
+
useStep,
|
|
45
|
+
useDoc,
|
|
33
46
|
useCursor,
|
|
34
47
|
useHost,
|
|
35
48
|
usePrefersReducedMotion,
|
|
@@ -37,48 +50,65 @@ export {
|
|
|
37
50
|
useSectionConfig,
|
|
38
51
|
useService,
|
|
39
52
|
useSharedState,
|
|
53
|
+
getSharedStateStore,
|
|
40
54
|
useStartReport,
|
|
41
55
|
useStaticRender,
|
|
42
56
|
useStepState,
|
|
43
57
|
useViewRole,
|
|
44
58
|
} from './host.ts'
|
|
45
59
|
export type { SharedStateOptions } from './host.ts'
|
|
46
|
-
export { pluginsOf, presmiumReport, presmiumService, registerPresmiumPlugin, reportPluginFailure,
|
|
47
|
-
export type { HostStart
|
|
60
|
+
export { pluginsOf, presmiumReport, presmiumService, registerPresmiumPlugin, reportPluginFailure, setHostActive, startHost, stopHost } from './runtime.ts'
|
|
61
|
+
export type { HostStart } from './runtime.ts'
|
|
48
62
|
export { currentLocale, useLocale, useMessages } from './locale.ts'
|
|
49
63
|
export type { Catalog } from './locale.ts'
|
|
50
64
|
export { PmApprovals } from './approvals.ts'
|
|
51
|
-
export {
|
|
52
|
-
export {
|
|
53
|
-
export {
|
|
54
|
-
export {
|
|
55
|
-
export {
|
|
56
|
-
export {
|
|
57
|
-
export
|
|
58
|
-
export {
|
|
59
|
-
export
|
|
60
|
-
export {
|
|
61
|
-
export {
|
|
62
|
-
export {
|
|
65
|
+
export { claimFirst } from './claim-first.ts'
|
|
66
|
+
export { createServerClient, pluginRoutePath, provideServer, useServer, serverClient } from './server.ts'
|
|
67
|
+
export { PmPresenterPanels } from './panels.ts'
|
|
68
|
+
export { useStepTime } from './step-clock.ts'
|
|
69
|
+
export type { StepTimeOptions } from './step-clock.ts'
|
|
70
|
+
export { StepScope, useBlockStepRange, useStepScope } from './step-scope.ts'
|
|
71
|
+
export { useSessionSnapshot } from './session.ts'
|
|
72
|
+
export type { StepScope as StepScopeValue } from './step-scope.ts'
|
|
73
|
+
export { createSharedStates } from './shared-state.ts'
|
|
74
|
+
export type { SharedStates } from './shared-state.ts'
|
|
75
|
+
export { PmToolbar } from './panels.ts'
|
|
76
|
+
export { NAVIGATION, OPERATIONS, PLAYBACK_CONTROL, PRODUCTION_PLAYBACK, STATUS, STEP_CLOCK, SYNC_TRANSPORTS, TIMED_CAPTIONS, TOOLBAR } from './contracts.ts'
|
|
77
|
+
export type { TimedCaptions } from './contracts.ts'
|
|
78
|
+
export { joinWindows } from './windows.ts'
|
|
79
|
+
export { setSiteBase, siteBase } from './site.ts'
|
|
63
80
|
export type { WindowChannel } from './windows.ts'
|
|
64
81
|
export type {
|
|
65
82
|
HostAdapter,
|
|
66
83
|
Navigation,
|
|
67
84
|
NavigationPosition,
|
|
68
85
|
Operations,
|
|
86
|
+
PlaybackControl,
|
|
87
|
+
PlaybackLease,
|
|
88
|
+
ProductionPlayback,
|
|
69
89
|
ReplayHandler,
|
|
70
90
|
ServerClient,
|
|
71
91
|
SharedStateScope,
|
|
72
92
|
StatusProvider,
|
|
73
93
|
StatusRegistry,
|
|
94
|
+
StepClock,
|
|
95
|
+
StepClockDriver,
|
|
74
96
|
SyncPublish,
|
|
75
97
|
SyncTransport,
|
|
76
98
|
SyncTransports,
|
|
77
99
|
Toolbar,
|
|
78
100
|
ToolbarItem,
|
|
79
|
-
|
|
101
|
+
ToolbarPlacement,
|
|
80
102
|
ToolbarSlot,
|
|
81
103
|
ViewRole,
|
|
82
|
-
} from './
|
|
83
|
-
export { localeOf } from '@lincyaw/kernel
|
|
104
|
+
} from './contracts.ts'
|
|
105
|
+
export { localeOf } from '@lincyaw/kernel'
|
|
84
106
|
export type { Locale } from '@lincyaw/kernel/contracts'
|
|
107
|
+
export { codeLinesBox } from './code-lines-box.ts'
|
|
108
|
+
export { createResidentWorker } from './resident-worker.ts'
|
|
109
|
+
export type { ResidentWorker, ResidentWorkerFailure, ResidentWorkerOptions, ResidentWorkerOutcome } from './resident-worker.ts'
|
|
110
|
+
|
|
111
|
+
export { createStore, useStore, observe } from './store.ts'
|
|
112
|
+
export type { Store } from './store.ts'
|
|
113
|
+
|
|
114
|
+
export { runtimeRevision } from "./runtime.ts"
|
package/src/locale.ts
CHANGED
|
@@ -1,42 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import { localeOf } from '@lincyaw/kernel'
|
|
2
3
|
import type { Locale } from '@lincyaw/kernel/contracts'
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import { HOST, installed } from './key.ts'
|
|
6
|
-
|
|
4
|
+
import { installed } from './host-key.ts'
|
|
5
|
+
import { createStore, useStore } from './store.ts'
|
|
7
6
|
export type Catalog<T> = Record<Locale, T>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
}
|
|
7
|
+
const language = createStore<string | undefined>(undefined)
|
|
8
|
+
let observer: MutationObserver | undefined
|
|
9
|
+
function observeLanguage() {
|
|
10
|
+
if (typeof document === 'undefined' || observer) return
|
|
11
|
+
language.set(document.documentElement.lang)
|
|
12
|
+
observer = new MutationObserver(() => language.set(document.documentElement.lang))
|
|
13
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] })
|
|
22
14
|
}
|
|
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
15
|
export function currentLocale(): Locale {
|
|
31
|
-
|
|
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])
|
|
16
|
+
const lang = typeof document === 'undefined' ? undefined : document.documentElement.lang
|
|
17
|
+
return localeOf(lang || installed.adapter?.lang) ?? localeOf(typeof navigator === 'undefined' ? undefined : navigator.language) ?? 'en'
|
|
42
18
|
}
|
|
19
|
+
export function useLocale(): Locale { useStore(language); useEffect(observeLanguage, []); return currentLocale() }
|
|
20
|
+
export function useMessages<T>(catalog: Catalog<T>): T { return catalog[useLocale()] }
|
package/src/messages.ts
CHANGED
|
@@ -8,44 +8,59 @@ export interface HostMessages {
|
|
|
8
8
|
noPlugins: string
|
|
9
9
|
status: Record<PluginStatus, string>
|
|
10
10
|
because: string
|
|
11
|
+
unnamedPlugin: string
|
|
11
12
|
componentError(component: string, message: string): string
|
|
12
13
|
problems(count: number): string
|
|
13
14
|
approvals(count: number): string
|
|
14
15
|
approvalsTitle: string
|
|
15
16
|
allow: string
|
|
16
17
|
deny: string
|
|
18
|
+
answerFailed: string
|
|
19
|
+
close: string
|
|
17
20
|
from(who: string): string
|
|
21
|
+
origins: Record<string, string>
|
|
22
|
+
otherOrigin: string
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export const HOST_MESSAGES: Catalog<HostMessages> = {
|
|
21
26
|
'zh-CN': {
|
|
22
27
|
panels: '课堂与工具',
|
|
23
28
|
startReport: '启动报告',
|
|
24
|
-
starting: '
|
|
29
|
+
starting: '正在启动…',
|
|
25
30
|
noPlugins: '没有注册插件。',
|
|
26
|
-
status: { started: '已启动', degraded: '
|
|
31
|
+
status: { started: '已启动', degraded: '部分可用', disabled: '已禁用', failed: '启动失败' },
|
|
27
32
|
because: ':',
|
|
33
|
+
unnamedPlugin: '其他插件',
|
|
28
34
|
componentError: (component, message) => `${component} 出错:${message}`,
|
|
29
35
|
problems: count => `${count} 个问题`,
|
|
30
36
|
approvals: count => `待确认 ${count}`,
|
|
31
37
|
approvalsTitle: '待确认的操作',
|
|
32
38
|
allow: '允许',
|
|
33
39
|
deny: '拒绝',
|
|
34
|
-
|
|
40
|
+
answerFailed: '无法发送答复。请重试。',
|
|
41
|
+
close: '关闭',
|
|
42
|
+
from: who => `来自${/^[A-Za-z0-9]/.test(who) ? ' ' : ''}${who}`,
|
|
43
|
+
origins: { 'presmium.mcp': '外部 AI 工具' },
|
|
44
|
+
otherOrigin: '其他程序',
|
|
35
45
|
},
|
|
36
46
|
'en': {
|
|
37
|
-
panels: '
|
|
38
|
-
startReport: '
|
|
39
|
-
starting: 'Starting
|
|
47
|
+
panels: 'Classroom and Tools',
|
|
48
|
+
startReport: 'Startup Report',
|
|
49
|
+
starting: 'Starting…',
|
|
40
50
|
noPlugins: 'No plugins registered.',
|
|
41
|
-
status: { started: '
|
|
51
|
+
status: { started: 'Started', degraded: 'Partly available', disabled: 'Disabled', failed: 'Failed to start' },
|
|
42
52
|
because: ': ',
|
|
53
|
+
unnamedPlugin: 'Other plugin',
|
|
43
54
|
componentError: (component, message) => `${component} failed: ${message}`,
|
|
44
55
|
problems: count => `${count} ${count === 1 ? 'problem' : 'problems'}`,
|
|
45
|
-
approvals: count => `Approvals ${count}`,
|
|
46
|
-
approvalsTitle: '
|
|
56
|
+
approvals: count => `Pending Approvals (${count})`,
|
|
57
|
+
approvalsTitle: 'Pending Approvals',
|
|
47
58
|
allow: 'Allow',
|
|
48
59
|
deny: 'Deny',
|
|
49
|
-
|
|
60
|
+
answerFailed: 'Couldn’t send the answer. Please try again.',
|
|
61
|
+
close: 'Close',
|
|
62
|
+
from: who => `From ${who}`,
|
|
63
|
+
origins: { 'presmium.mcp': 'an external AI tool' },
|
|
64
|
+
otherOrigin: 'another program',
|
|
50
65
|
},
|
|
51
66
|
}
|
package/src/operations.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { Operation, Unsubscribe } from '@lincyaw/kernel/contracts'
|
|
2
|
-
import {
|
|
3
|
-
import type { Operations, ReplayHandler
|
|
2
|
+
import { createStore } from './store.ts'
|
|
3
|
+
import type { Operations, ReplayHandler } from './contracts.ts'
|
|
4
4
|
|
|
5
5
|
export function createOperations(): Operations {
|
|
6
6
|
const listeners = new Set<(operation: Operation) => void>()
|
|
7
7
|
const handlers = new Map<string, ReplayHandler>()
|
|
8
|
-
const replaying =
|
|
8
|
+
const replaying = createStore(false)
|
|
9
9
|
let unsettled = 0
|
|
10
10
|
return {
|
|
11
11
|
report(operation) {
|
|
12
|
-
if (replaying.
|
|
12
|
+
if (replaying.getSnapshot()) return
|
|
13
13
|
for (const listener of [...listeners]) {
|
|
14
14
|
try {
|
|
15
15
|
listener(operation)
|
|
@@ -34,14 +34,14 @@ export function createOperations(): Operations {
|
|
|
34
34
|
const handler = handlers.get(operation.type)
|
|
35
35
|
if (!handler) return false
|
|
36
36
|
unsettled++
|
|
37
|
-
replaying.
|
|
37
|
+
replaying.set(true)
|
|
38
38
|
try {
|
|
39
39
|
handler.apply(operation.data, seeking)
|
|
40
40
|
} catch (error) {
|
|
41
41
|
console.error(`[presmium] the replay of ${operation.type} failed`, error)
|
|
42
42
|
} finally {
|
|
43
43
|
setTimeout(() => {
|
|
44
|
-
if (--unsettled === 0) replaying.
|
|
44
|
+
if (--unsettled === 0) replaying.set(false)
|
|
45
45
|
}, 0)
|
|
46
46
|
}
|
|
47
47
|
return true
|
|
@@ -58,19 +58,3 @@ export function createOperations(): Operations {
|
|
|
58
58
|
replaying,
|
|
59
59
|
}
|
|
60
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
CHANGED
|
@@ -1,60 +1,28 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { createElement as h, useEffect, useState } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import type { ComponentType } from 'react'
|
|
4
|
+
import { PRESENTER_PANELS } from '@lincyaw/kernel'
|
|
5
|
+
import type { PresenterPanel } from '@lincyaw/kernel'
|
|
6
|
+
import { TOOLBAR } from './contracts.ts'
|
|
7
|
+
import type { ToolbarItem, ToolbarPlacement } from './contracts.ts'
|
|
8
|
+
import { useService, useStartReport } from './host.ts'
|
|
9
|
+
import { useMessages } from './locale.ts'
|
|
10
|
+
import { componentErrors, componentErrorRevision } from './error-boundary.ts'
|
|
11
|
+
import { useStore } from './store.ts'
|
|
3
12
|
import { HOST_MESSAGES } from './messages.ts'
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
id:
|
|
9
|
-
order?: number
|
|
13
|
+
import { ToolbarPlacementContext } from './design/components.ts'
|
|
14
|
+
export function PmToolbar({ filter = (item: ToolbarItem) => !item.slot || item.slot === 'tools' || item.slot === 'settings', placement = 'bar', className }: { className?: string, filter?: (item: ToolbarItem) => boolean, placement?: ToolbarPlacement }) {
|
|
15
|
+
const service = useService(TOOLBAR); const [items, setItems] = useState<ToolbarItem[]>([])
|
|
16
|
+
useEffect(() => { const update = () => setItems(service?.list() ?? []); update(); return service?.subscribe(update) }, [service])
|
|
17
|
+
return h(ToolbarPlacementContext.Provider, { value: placement }, h('span', { className: `pm-toolbar${className ? ` ${className}` : ''}`, 'data-placement': placement }, items.filter(filter).map(item => h(item.component as ComponentType, { key: item.id, 'data-toolbar-item': item.id } as any))))
|
|
10
18
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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')
|
|
19
|
+
export function PmPresenterPanels({ to = 'body', docked = false }: { to?: string, docked?: boolean }) {
|
|
20
|
+
const t = useMessages(HOST_MESSAGES); const service = useService(PRESENTER_PANELS); const [panels, setPanels] = useState<PresenterPanel[]>([])
|
|
21
|
+
useEffect(() => { const update = () => setPanels(service?.list() ?? []); update(); return service?.subscribe(update) }, [service])
|
|
22
|
+
const [target, setTarget] = useState<Element | null>(null)
|
|
23
|
+
useEffect(() => { setTarget(document.querySelector(to)) }, [to])
|
|
24
|
+
const report = useStartReport(); useStore(componentErrorRevision)
|
|
25
|
+
const problems = (report?.plugins.filter(plugin => plugin.status === 'failed' || plugin.status === 'disabled').length ?? 0) + componentErrors().length
|
|
26
|
+
const content = service ? h('details', { className: `pm-presenter-panels${docked ? ' pm-presenter-panels--docked' : ''}` }, h('summary', {}, t.panels, problems ? h('span', { className: 'pm-presenter-panels__badge', title: t.problems(problems) }, problems) : null), panels.map(panel => h('section', { key: panel.id, 'data-panel': panel.id }, h('h3', {}, panel.title), h(panel.component as ComponentType)))) : null
|
|
27
|
+
return target ? createPortal(content, target) : null
|
|
60
28
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { PlaybackControl, PlaybackLease } from './contracts.ts'
|
|
2
|
+
|
|
3
|
+
interface Participant {
|
|
4
|
+
pause(): void
|
|
5
|
+
cancel(): void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Coordinates starts and cancellation without owning time, media or a second player. */
|
|
9
|
+
export function createPlaybackControl(): PlaybackControl {
|
|
10
|
+
const participants = new Set<Participant>()
|
|
11
|
+
let gate: { authorize(signal: AbortSignal): Promise<void> } | undefined
|
|
12
|
+
const pause = () => {
|
|
13
|
+
for (const participant of [...participants]) {
|
|
14
|
+
participant.cancel()
|
|
15
|
+
participant.pause()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
pause,
|
|
20
|
+
setGate(authorize) {
|
|
21
|
+
pause()
|
|
22
|
+
const installed = { authorize }
|
|
23
|
+
gate = installed
|
|
24
|
+
return () => {
|
|
25
|
+
if (gate !== installed) return
|
|
26
|
+
gate = undefined
|
|
27
|
+
pause()
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
register(stop) {
|
|
31
|
+
let active: AbortController | undefined
|
|
32
|
+
let disposed = false
|
|
33
|
+
const cancel = () => { active?.abort(); active = undefined }
|
|
34
|
+
const participant = { pause: stop, cancel }
|
|
35
|
+
participants.add(participant)
|
|
36
|
+
const lease: PlaybackLease = {
|
|
37
|
+
async requestStart() {
|
|
38
|
+
cancel()
|
|
39
|
+
if (disposed) return false
|
|
40
|
+
for (const other of [...participants]) if (other !== participant) {
|
|
41
|
+
other.cancel()
|
|
42
|
+
other.pause()
|
|
43
|
+
}
|
|
44
|
+
const request = new AbortController()
|
|
45
|
+
active = request
|
|
46
|
+
const requestedGate = gate
|
|
47
|
+
try {
|
|
48
|
+
await requestedGate?.authorize(request.signal)
|
|
49
|
+
return !disposed && active === request && !request.signal.aborted && gate === requestedGate
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (request.signal.aborted || error instanceof Error && error.name === 'AbortError') return false
|
|
52
|
+
throw error
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
cancel,
|
|
56
|
+
dispose() {
|
|
57
|
+
if (disposed) return
|
|
58
|
+
disposed = true
|
|
59
|
+
cancel()
|
|
60
|
+
participants.delete(participant)
|
|
61
|
+
stop()
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
return lease
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|