@lincyaw/host 1.0.0-rc.12 → 1.0.0-rc.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lincyaw/host",
3
- "version": "1.0.0-rc.12",
3
+ "version": "1.0.0-rc.14",
4
4
  "description": "Presmium's host interface for plugin components in every form: step state, services, interface text, and the design system (tokens, components, fonts)",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -16,6 +16,9 @@
16
16
  "design-system"
17
17
  ],
18
18
  "type": "module",
19
+ "sideEffects": [
20
+ "*.css"
21
+ ],
19
22
  "engines": {
20
23
  "node": ">=22.19"
21
24
  },
@@ -41,8 +44,8 @@
41
44
  "@radix-ui/react-tooltip": "^1.2.8",
42
45
  "react": "18.3.1",
43
46
  "react-dom": "18.3.1",
44
- "@lincyaw/design": "1.0.0-rc.12",
45
- "@lincyaw/kernel": "1.0.0-rc.12"
47
+ "@lincyaw/design": "1.0.0-rc.14",
48
+ "@lincyaw/kernel": "1.0.0-rc.14"
46
49
  },
47
50
  "devDependencies": {
48
51
  "@types/react": "18.3.28",
package/src/contracts.ts CHANGED
@@ -143,6 +143,16 @@ export interface TimedCaptions {
143
143
  }
144
144
  export const TIMED_CAPTIONS = defineService<TimedCaptions>('presmium.timedCaptions')
145
145
 
146
+ /**
147
+ * A component that draws asynchronously (a scene loading its code) holds the page as not yet drawn until it has: the
148
+ * viewer tells an embedding page when the current slide is drawn (`notifyEmbedder`).
149
+ */
150
+ export interface PageReadiness {
151
+ readonly pending: Store<number>
152
+ hold(): Unsubscribe
153
+ }
154
+ export const PAGE_READINESS = defineService<PageReadiness>('presmium.pageReadiness')
155
+
146
156
  export interface StepClockDriver {
147
157
  readonly elapsed: Store<number | undefined>
148
158
  readonly recording?: boolean
@@ -2,10 +2,8 @@ import { createContext, createElement as h, forwardRef, useContext, useEffect, u
2
2
  import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from 'react'
3
3
  import * as Dialog from '@radix-ui/react-dialog'
4
4
  import * as Popover from '@radix-ui/react-popover'
5
- import * as Dropdown from '@radix-ui/react-dropdown-menu'
6
5
  import * as Tabs from '@radix-ui/react-tabs'
7
6
  import * as Tooltip from '@radix-ui/react-tooltip'
8
- import * as RadioGroup from '@radix-ui/react-radio-group'
9
7
  import { useLocale } from '../locale.ts'
10
8
  import { HOST_MESSAGES } from '../messages.ts'
11
9
  import { ICONS } from '@lincyaw/design'
@@ -13,11 +11,13 @@ import type { IconName } from '@lincyaw/design'
13
11
  import type { ToolbarPlacement } from '../contracts.ts'
14
12
  export type ButtonVariant = 'default' | 'primary' | 'accent' | 'ghost' | 'danger'
15
13
  export type ControlDensity = 'compact' | 'regular' | 'touch'
14
+ /** `overlay`: one large round control laid over media, such as the Play that starts a lecture the browser would not autoplay. */
15
+ export type IconButtonVariant = 'default' | 'overlay'
16
16
  export type Tone = 'neutral' | 'accent' | 'ok' | 'warn' | 'danger' | 'outline'
17
17
  export type PopoverPlacement = 'below-end' | 'below-start' | 'above-end' | 'above-start' | 'above' | 'below'
18
18
  export interface Choice { value: string, label: string, icon?: IconName, disabled?: boolean }
19
19
  export interface MenuItem { id: string, label: string, icon?: IconName, danger?: boolean, disabled?: boolean, shortcut?: string, checked?: boolean, separator?: boolean }
20
- const cls = (...values: (string | false | undefined)[]) => values.filter(Boolean).join(' ')
20
+ export const cls = (...values: (string | false | undefined)[]) => values.filter(Boolean).join(' ')
21
21
  export function PmIcon({ name, size: _size, strokeWidth = 1.7, ...attrs }: { name: IconName, size?: number | string, strokeWidth?: number } & HTMLAttributes<SVGElement>) {
22
22
  return h('svg', { ...attrs, className: cls('pm-ui-icon', attrs.className), viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true, focusable: false }, ICONS[name]?.map(([tag, attributes], key) => h(tag, { ...attributes, key })))
23
23
  }
@@ -33,8 +33,8 @@ export function PmTooltip({ label, shortcut, children }: { label: string, shortc
33
33
  h(Tooltip.Root, {}, h(Tooltip.Trigger, { asChild: true }, children), h(Tooltip.Portal, {},
34
34
  h(Tooltip.Content, { className: 'pm-ui-tooltip pm-ui', side: 'top', sideOffset: 6, collisionPadding: 8 }, label, shortcut ? h('kbd', { className: 'pm-ui-shortcut' }, shortcut) : null))) })
35
35
  }
36
- export const PmIconButton = forwardRef<HTMLButtonElement, ButtonProps & { icon: IconName, label: string, hint?: string, shortcut?: string }>(function PmIconButton({ icon, label, hint, shortcut, small, density = small ? 'compact' : 'regular', loading, className, disabled, ...rest }, ref) {
37
- const button = h('button', { ...rest, ref, type: 'button', 'data-density': density, 'aria-label': label, 'aria-busy': loading || undefined, disabled: disabled || loading || rest['aria-disabled'] === true || rest['aria-disabled'] === 'true', className: cls('pm-ui-icon-button', className) }, h(PmIcon, { name: loading ? 'loading' : icon, className: loading ? 'pm-ui-loading' : undefined }))
36
+ export const PmIconButton = forwardRef<HTMLButtonElement, Omit<ButtonProps, 'variant'> & { variant?: IconButtonVariant, icon: IconName, label: string, hint?: string, shortcut?: string }>(function PmIconButton({ variant = 'default', icon, label, hint, shortcut, small, density = small ? 'compact' : 'regular', loading, className, disabled, ...rest }, ref) {
37
+ const button = h('button', { ...rest, ref, type: 'button', 'data-density': density, 'aria-label': label, 'aria-busy': loading || undefined, disabled: disabled || loading || rest['aria-disabled'] === true || rest['aria-disabled'] === 'true', className: cls('pm-ui-icon-button', variant !== 'default' && `pm-ui-icon-button--${variant}`, className) }, h(PmIcon, { name: loading ? 'loading' : icon, className: loading ? 'pm-ui-loading' : undefined }))
38
38
  return h(PmTooltip, { label: hint ?? label, shortcut, children: button })
39
39
  })
40
40
  export function PmBadge({ tone = 'neutral', pill, children, className, ...rest }: HTMLAttributes<HTMLSpanElement> & { tone?: Tone, pill?: boolean }) { return h('span', { ...rest, className: cls('pm-ui-badge', tone !== 'neutral' && `pm-ui-badge--${tone}`, pill && 'pm-ui-badge--pill', className) }, children) }
@@ -102,18 +102,8 @@ function DialogSurface({ open, label, closeLabel, name, children, actions, trigg
102
102
  }
103
103
  export function PmDialog(props: OverlayProps) { return h(DialogSurface, props) }
104
104
  export function PmDrawer(props: OverlayProps) { return h(DialogSurface, { ...props, drawer: true }) }
105
- const placement = (value: PopoverPlacement) => ({ side: value.startsWith('above') ? 'top' as const : 'bottom' as const, align: value.endsWith('start') ? 'start' as const : 'end' as const })
105
+ export const placement = (value: PopoverPlacement) => ({ side: value.startsWith('above') ? 'top' as const : 'bottom' as const, align: value.endsWith('start') ? 'start' as const : 'end' as const })
106
106
  export function PmPopover({ open, label, name, children, trigger, placement: place = 'below-end', onOpenChange }: OverlayProps & { placement?: PopoverPlacement }) { return h(Popover.Root, { open, onOpenChange }, trigger ? h(Popover.Trigger, { asChild: true }, trigger) : null, h(Popover.Portal, {}, h(Popover.Content, { ...placement(place), className: cls('pm-ui-popover pm-ui-floating pm-ui', name), 'aria-label': label, onEscapeKeyDown: event => event.stopPropagation() }, children))) }
107
- export function PmMenu({ label, items, icon = 'more', placement: place = 'below-end', onSelect }: { label: string, items: MenuItem[], icon?: IconName, placement?: PopoverPlacement, onSelect?(id: string): void }) {
108
- return h(Dropdown.Root, {},
109
- h(Dropdown.Trigger, { asChild: true }, h(PmIconButton, { icon, label, small: true })),
110
- h(Dropdown.Portal, {}, h(Dropdown.Content, { ...placement(place), className: 'pm-ui-menu pm-ui-floating pm-ui', ...{ 'data-density': 'compact' }, 'aria-label': label, onEscapeKeyDown: event => event.stopPropagation() },
111
- items.map(item => h(Dropdown.Group, { key: item.id }, item.separator ? h(Dropdown.Separator, { className: 'pm-ui-menu__separator' }) : null,
112
- item.checked === undefined ? h(Dropdown.Item, { disabled: item.disabled, textValue: item.label, className: cls('pm-ui-menu__item', item.danger && 'pm-ui-menu__item--danger'), onSelect: () => onSelect?.(item.id) },
113
- item.icon ? h(PmIcon, { name: item.icon }) : null, item.label, item.shortcut ? h('kbd', { className: 'pm-ui-shortcut' }, item.shortcut) : null)
114
- : h(Dropdown.CheckboxItem, { checked: item.checked, disabled: item.disabled, textValue: item.label, className: cls('pm-ui-menu__item', item.danger && 'pm-ui-menu__item--danger'), onSelect: () => onSelect?.(item.id) },
115
- h(Dropdown.ItemIndicator, {}, h(PmIcon, { name: 'check' })), item.label))))))
116
- }
117
107
  export function PmTabs({ value, modelValue, tabs, label, children, onValueChange, iconOnly, density = 'compact' }: { value?: string, modelValue?: string, tabs: Choice[], label: string, children?: ReactNode, iconOnly?: boolean, density?: ControlDensity, onValueChange?(value: string): void }) {
118
108
  return h(Tabs.Root, { value: value ?? modelValue, onValueChange, activationMode: 'manual', className: 'pm-ui-tabs-root', ...{ 'data-density': density } },
119
109
  h(Tabs.List, { className: 'pm-ui-tabs', 'aria-label': label }, tabs.map(tab => {
@@ -122,10 +112,6 @@ export function PmTabs({ value, modelValue, tabs, label, children, onValueChange
122
112
  })), children)
123
113
  }
124
114
  export function PmTabPanel({ value, children, forceMount, hidden }: { value: string, children?: ReactNode, forceMount?: true, hidden?: boolean }) { return h(Tabs.Content, { value, forceMount, hidden, className: 'pm-ui-tab-panel' }, children) }
125
- export function PmSegmented({ value, modelValue, choices, items, label, onValueChange, density = 'compact', disabled }: { value?: string, modelValue?: string, choices?: Choice[], items?: Choice[], label: string, density?: ControlDensity, disabled?: boolean, onValueChange?(value: string): void }) {
126
- return h(RadioGroup.Root, { className: 'pm-ui-segmented', 'aria-label': label, value: value ?? modelValue, onValueChange, orientation: 'horizontal', ...{ 'data-density': density }, disabled },
127
- (choices ?? items ?? []).map(item => h(RadioGroup.Item, { key: item.value, value: item.value, className: 'pm-ui-segmented__option', disabled: item.disabled }, item.icon ? h(PmIcon, { name: item.icon }) : null, item.label)))
128
- }
129
115
  export function PmNotice({ tone = 'neutral', children, className, ...rest }: HTMLAttributes<HTMLDivElement> & { tone?: Tone }) { return h('div', { ...rest, className: cls('pm-ui-notice', tone !== 'neutral' && `pm-ui-notice--${tone}`, className), role: tone === 'danger' ? 'alert' : 'status' }, children) }
130
116
  export function PmEmpty({ title, icon, action, children }: { title?: string, icon?: IconName, action?: ReactNode, children?: ReactNode }) { return h('div', { className: 'pm-ui-empty' }, icon ? h(PmIcon, { name: icon }) : null, title ? h('p', { className: 'pm-ui-empty__title' }, title) : null, children, action) }
131
117
  /** Loading in the shape of the content: `document` headings and paragraphs, `list` rows, `preview` a slide-shaped area. */
@@ -0,0 +1,18 @@
1
+ import { createElement as h } from 'react'
2
+ import * as Dropdown from '@radix-ui/react-dropdown-menu'
3
+ import type { IconName } from '@lincyaw/design'
4
+ import { cls, placement, PmIcon, PmIconButton } from './components.ts'
5
+ import type { MenuItem, PopoverPlacement } from './components.ts'
6
+
7
+ // A module of its own, as `segmented.ts` is: a page whose code shows no menu loads none of Radix's menus.
8
+
9
+ export function PmMenu({ label, items, icon = 'more', placement: place = 'below-end', onSelect }: { label: string, items: MenuItem[], icon?: IconName, placement?: PopoverPlacement, onSelect?(id: string): void }) {
10
+ return h(Dropdown.Root, {},
11
+ h(Dropdown.Trigger, { asChild: true }, h(PmIconButton, { icon, label, small: true })),
12
+ h(Dropdown.Portal, {}, h(Dropdown.Content, { ...placement(place), className: 'pm-ui-menu pm-ui-floating pm-ui', ...{ 'data-density': 'compact' }, 'aria-label': label, onEscapeKeyDown: event => event.stopPropagation() },
13
+ items.map(item => h(Dropdown.Group, { key: item.id }, item.separator ? h(Dropdown.Separator, { className: 'pm-ui-menu__separator' }) : null,
14
+ item.checked === undefined ? h(Dropdown.Item, { disabled: item.disabled, textValue: item.label, className: cls('pm-ui-menu__item', item.danger && 'pm-ui-menu__item--danger'), onSelect: () => onSelect?.(item.id) },
15
+ item.icon ? h(PmIcon, { name: item.icon }) : null, item.label, item.shortcut ? h('kbd', { className: 'pm-ui-shortcut' }, item.shortcut) : null)
16
+ : h(Dropdown.CheckboxItem, { checked: item.checked, disabled: item.disabled, textValue: item.label, className: cls('pm-ui-menu__item', item.danger && 'pm-ui-menu__item--danger'), onSelect: () => onSelect?.(item.id) },
17
+ h(Dropdown.ItemIndicator, {}, h(PmIcon, { name: 'check' })), item.label))))))
18
+ }
@@ -0,0 +1,11 @@
1
+ import { createElement as h } from 'react'
2
+ import * as RadioGroup from '@radix-ui/react-radio-group'
3
+ import { PmIcon } from './components.ts'
4
+ import type { Choice, ControlDensity } from './components.ts'
5
+
6
+ // A module of its own, as `menu.ts` is: a page whose code shows no segmented control loads none of Radix's radio group.
7
+
8
+ export function PmSegmented({ value, modelValue, choices, items, label, onValueChange, density = 'compact', disabled }: { value?: string, modelValue?: string, choices?: Choice[], items?: Choice[], label: string, density?: ControlDensity, disabled?: boolean, onValueChange?(value: string): void }) {
9
+ return h(RadioGroup.Root, { className: 'pm-ui-segmented', 'aria-label': label, value: value ?? modelValue, onValueChange, orientation: 'horizontal', ...{ 'data-density': density }, disabled },
10
+ (choices ?? items ?? []).map(item => h(RadioGroup.Item, { key: item.value, value: item.value, className: 'pm-ui-segmented__option', disabled: item.disabled }, item.icon ? h(PmIcon, { name: item.icon }) : null, item.label)))
11
+ }
package/src/index.ts CHANGED
@@ -18,10 +18,8 @@ export {
18
18
  PmField,
19
19
  PmIcon,
20
20
  PmIconButton,
21
- PmMenu,
22
21
  PmNotice,
23
22
  PmPopover,
24
- PmSegmented,
25
23
  PmTabs,
26
24
  PmTabPanel,
27
25
  PmTooltip,
@@ -29,7 +27,9 @@ export {
29
27
  provideToolbarPlacement,
30
28
  useToolbarPlacement,
31
29
  } from './design/components.ts'
32
- export type { ButtonVariant, Choice, ControlDensity, MenuItem, PopoverPlacement, Tone } from './design/components.ts'
30
+ export type { ButtonVariant, Choice, ControlDensity, IconButtonVariant, MenuItem, PopoverPlacement, Tone } from './design/components.ts'
31
+ export { PmMenu } from './design/menu.ts'
32
+ export { PmSegmented } from './design/segmented.ts'
33
33
  export { PmColumns, PmStack, PmTabGroup } from './design/layout.ts'
34
34
  export { PmCaption, PmCaptionProvider } from './design/caption.ts'
35
35
  export type { CaptionProps } from './design/caption.ts'
@@ -73,8 +73,8 @@ export type { StepScope as StepScopeValue } from './step-scope.ts'
73
73
  export { createSharedStates } from './shared-state.ts'
74
74
  export type { SharedStates } from './shared-state.ts'
75
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'
76
+ export { NAVIGATION, OPERATIONS, PAGE_READINESS, PLAYBACK_CONTROL, PRODUCTION_PLAYBACK, STATUS, STEP_CLOCK, SYNC_TRANSPORTS, TIMED_CAPTIONS, TOOLBAR } from './contracts.ts'
77
+ export type { PageReadiness, TimedCaptions } from './contracts.ts'
78
78
  export { joinWindows } from './windows.ts'
79
79
  export { setSiteBase, siteBase } from './site.ts'
80
80
  export type { WindowChannel } from './windows.ts'
package/src/registries.ts CHANGED
@@ -2,7 +2,7 @@ import type { PresenterPanel, PresenterPanels, Unsubscribe } from '@lincyaw/kern
2
2
  import { currentLocale } from './locale.ts'
3
3
  import { HOST_MESSAGES } from './messages.ts'
4
4
  import StartReportPanel from './start-report-panel.ts'
5
- import type { StatusProvider, StatusRegistry, TimedCaptions, Toolbar, ToolbarItem } from './contracts.ts'
5
+ import type { PageReadiness, StatusProvider, StatusRegistry, TimedCaptions, Toolbar, ToolbarItem } from './contracts.ts'
6
6
  import { createStore } from './store.ts'
7
7
 
8
8
  export function createTimedCaptions(): TimedCaptions {
@@ -14,6 +14,15 @@ export function createTimedCaptions(): TimedCaptions {
14
14
  } }
15
15
  }
16
16
 
17
+ export function createPageReadiness(): PageReadiness {
18
+ const holders = new Set<symbol>(), pending = createStore(0)
19
+ return { pending, hold() {
20
+ const holder = Symbol()
21
+ holders.add(holder); pending.set(holders.size)
22
+ return () => { if (holders.delete(holder)) pending.set(holders.size) }
23
+ } }
24
+ }
25
+
17
26
  interface Item {
18
27
  id: string
19
28
  order?: number
package/src/runtime.ts CHANGED
@@ -3,7 +3,7 @@ import type { EnvironmentSpec, Logger, PluginDefinition, PluginReport, Presentat
3
3
  import { createStore } from './store.ts'
4
4
  import { connectActions } from './actions.ts'
5
5
  import { PmApprovals } from './approvals.ts'
6
- import { NAVIGATION, OPERATIONS, PLAYBACK_CONTROL, STATUS, STEP_CLOCK, SYNC_TRANSPORTS, TIMED_CAPTIONS, TOOLBAR } from './contracts.ts'
6
+ import { NAVIGATION, OPERATIONS, PAGE_READINESS, PLAYBACK_CONTROL, STATUS, STEP_CLOCK, SYNC_TRANSPORTS, TIMED_CAPTIONS, TOOLBAR } from './contracts.ts'
7
7
  import type { Navigation, ServerClient, SyncTransports } from './contracts.ts'
8
8
  import { connectControl } from './control.ts'
9
9
  import { componentErrors } from './error-boundary.ts'
@@ -11,7 +11,7 @@ import { currentLocale } from './locale.ts'
11
11
  import { HOST_MESSAGES } from './messages.ts'
12
12
  import { createOperations } from './operations.ts'
13
13
  import { createPlaybackControl } from './playback-control.ts'
14
- import { createPresenterPanels, createStatusRegistry, createTimedCaptions, createToolbar } from './registries.ts'
14
+ import { createPageReadiness, createPresenterPanels, createStatusRegistry, createTimedCaptions, createToolbar } from './registries.ts'
15
15
  import { provideServer } from './server.ts'
16
16
  import { createStepClock } from './step-clock.ts'
17
17
 
@@ -156,6 +156,7 @@ export function startHost(options: HostStart): Promise<StartReport> {
156
156
  kernel.provide(OPERATIONS, operations)
157
157
  kernel.provide(PLAYBACK_CONTROL, createPlaybackControl())
158
158
  kernel.provide(TIMED_CAPTIONS, createTimedCaptions())
159
+ kernel.provide(PAGE_READINESS, createPageReadiness())
159
160
  const stepClock = createStepClock(options.navigation)
160
161
  release(stepClock.stop)
161
162
  kernel.provide(STEP_CLOCK, stepClock.clock)