@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.
Files changed (46) hide show
  1. package/README.md +14 -15
  2. package/package.json +16 -7
  3. package/src/actions.ts +22 -0
  4. package/src/approvals.ts +9 -55
  5. package/src/blocks.ts +9 -10
  6. package/src/claim-first.ts +13 -0
  7. package/src/code-lines-box.ts +11 -0
  8. package/src/{types.ts → contracts.ts} +62 -12
  9. package/src/control.ts +117 -0
  10. package/src/design/appearance.ts +16 -0
  11. package/src/design/caption.ts +31 -0
  12. package/src/design/components.ts +132 -316
  13. package/src/design/design.css +8 -0
  14. package/src/design/layout.ts +28 -0
  15. package/src/error-boundary.ts +18 -0
  16. package/src/host-key.ts +4 -0
  17. package/src/host.ts +42 -77
  18. package/src/index.ts +55 -25
  19. package/src/locale.ts +15 -37
  20. package/src/messages.ts +25 -10
  21. package/src/operations.ts +6 -22
  22. package/src/panels.ts +25 -57
  23. package/src/playback-control.ts +67 -0
  24. package/src/registries.ts +86 -0
  25. package/src/resident-worker.ts +84 -0
  26. package/src/runtime.ts +187 -0
  27. package/src/server.ts +27 -164
  28. package/src/session.ts +5 -29
  29. package/src/shared-state.ts +30 -0
  30. package/src/site.ts +14 -0
  31. package/src/start-report-panel.ts +8 -0
  32. package/src/step-clock.ts +49 -0
  33. package/src/step-scope.ts +38 -0
  34. package/src/store.ts +33 -0
  35. package/src/styles.d.ts +1 -0
  36. package/src/windows.ts +4 -2
  37. package/src/PresenterPanels.vue +0 -91
  38. package/src/StartReportPanel.vue +0 -45
  39. package/src/Toolbar.vue +0 -49
  40. package/src/boundary.ts +0 -56
  41. package/src/design/theme.ts +0 -32
  42. package/src/kernel.ts +0 -131
  43. package/src/key.ts +0 -6
  44. package/src/scope.ts +0 -29
  45. package/src/shared.ts +0 -85
  46. package/src/vue-shim.d.ts +0 -6
@@ -1,327 +1,143 @@
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'
1
+ import { createContext, createElement as h, forwardRef, useContext, useEffect, useId, useRef } from 'react'
2
+ import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from 'react'
3
+ import * as Dialog from '@radix-ui/react-dialog'
4
+ import * as Popover from '@radix-ui/react-popover'
5
+ import * as Dropdown from '@radix-ui/react-dropdown-menu'
6
+ import * as Tabs from '@radix-ui/react-tabs'
7
+ import * as Tooltip from '@radix-ui/react-tooltip'
8
+ import * as RadioGroup from '@radix-ui/react-radio-group'
9
+ import { useLocale } from '../locale.ts'
10
+ import { HOST_MESSAGES } from '../messages.ts'
5
11
  import { ICONS } from '@lincyaw/design'
6
12
  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
-
13
+ import type { ToolbarPlacement } from '../contracts.ts'
53
14
  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
-
15
+ export type ControlDensity = 'compact' | 'regular' | 'touch'
88
16
  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
17
+ export type PopoverPlacement = 'below-end' | 'below-start' | 'above-end' | 'above-start' | 'above' | 'below'
18
+ export interface Choice { value: string, label: string, icon?: IconName, disabled?: boolean }
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(' ')
21
+ export function PmIcon({ name, size: _size, strokeWidth = 1.7, ...attrs }: { name: IconName, size?: number | string, strokeWidth?: number } & HTMLAttributes<SVGElement>) {
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 })))
128
23
  }
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
- },
24
+ type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & { variant?: ButtonVariant, small?: boolean, icon?: IconName, density?: ControlDensity, loading?: boolean }
25
+ export const PmButton = forwardRef<HTMLButtonElement, ButtonProps>(function PmButton({ variant = 'default', small, density = small ? 'compact' : 'regular', loading, icon, className, children, disabled, ...rest }, ref) {
26
+ return h('button', { ...rest, ref, type: rest.type ?? 'button', 'data-density': density, 'aria-busy': loading || undefined, disabled: disabled || loading || rest['aria-disabled'] === true || rest['aria-disabled'] === 'true', className: cls('pm-ui-button', variant !== 'default' && `pm-ui-button--${variant}`, className) },
27
+ h('span', { className: cls('pm-ui-button__content', !icon && loading !== undefined && 'pm-ui-button__content--loading-text') },
28
+ icon || loading !== undefined ? h('span', { className: 'pm-ui-button__icon', 'aria-hidden': true }, loading ? h(PmIcon, { name: 'loading', className: 'pm-ui-loading' }) : icon ? h(PmIcon, { name: icon }) : null) : null,
29
+ children))
166
30
  })
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 }
31
+ export function PmTooltip({ label, shortcut, children }: { label: string, shortcut?: string, children: ReactNode }) {
32
+ return h(Tooltip.Provider, { delayDuration: 300, skipDelayDuration: 0, children:
33
+ h(Tooltip.Root, {}, h(Tooltip.Trigger, { asChild: true }, children), h(Tooltip.Portal, {},
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))) })
172
35
  }
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
- },
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 }))
38
+ return h(PmTooltip, { label: hint ?? label, shortcut, children: button })
201
39
  })
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
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) }
41
+ export function PmCard({ variant = 'default', className, children, ...rest }: HTMLAttributes<HTMLDivElement> & { variant?: string }) { return h('div', { ...rest, className: cls('pm-ui-card', variant !== 'default' && `pm-ui-card--${variant}`, className) }, children) }
42
+ export function PmField({ label, inline, group, children }: { label: string, inline?: boolean, group?: boolean, children?: ReactNode }) { return h(group ? 'div' : 'label', { className: cls('pm-ui-field', inline && 'pm-ui-field--inline'), ...(group ? { role: 'group', 'aria-label': label } : {}) }, h('span', { className: 'pm-ui-field__label' }, label), children) }
43
+ interface OverlayProps {
44
+ open: boolean
247
45
  label: string
248
- icon?: IconName
249
- danger?: boolean
250
- disabled?: boolean
46
+ closeLabel?: string
47
+ name?: string
48
+ children?: ReactNode
49
+ trigger?: ReactNode
50
+ actions?: ReactNode
51
+ description?: string
52
+ footer?: ReactNode
53
+ busy?: boolean
54
+ onOpenChange?(open: boolean): void
55
+ onClose?(): void
56
+ onOpenAutoFocus?(event: Event): void
57
+ onCloseAutoFocus?(event: Event): void
251
58
  }
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
- })
59
+ function DialogSurface({ open, label, closeLabel, name, children, actions, trigger, description, footer, busy, onOpenChange, onClose, onOpenAutoFocus, onCloseAutoFocus, drawer = false }: OverlayProps & { drawer?: boolean }) {
60
+ const kind = drawer ? 'pm-ui-drawer' : 'pm-ui-dialog'
61
+ const locale = useLocale()
62
+ const id = useId()
63
+ const focusTarget = useRef<HTMLElement | null>(typeof document === 'undefined' ? null : document.activeElement as HTMLElement)
64
+ useEffect(() => {
65
+ if (open) return
66
+ const remember = (event: FocusEvent) => {
67
+ if (event.target instanceof HTMLElement && event.target !== document.body && event.target.closest('[data-pm-dialog]')?.getAttribute('data-pm-dialog') !== id) focusTarget.current = event.target
325
68
  }
326
- },
327
- })
69
+ document.addEventListener('focusin', remember, true)
70
+ return () => document.removeEventListener('focusin', remember, true)
71
+ }, [id, open])
72
+ return h(Dialog.Root, { open, modal: true, onOpenChange: next => { if (!next && busy) return; onOpenChange?.(next); if (!next) onClose?.() } },
73
+ trigger ? h(Dialog.Trigger, { asChild: true }, trigger) : null,
74
+ h(Dialog.Portal, {}, h(Dialog.Overlay, { className: 'pm-ui-dialog-overlay' }),
75
+ h(Dialog.Content, {
76
+ className: cls(kind, 'pm-ui', name && `${name}__panel`), ...{ 'data-density': 'regular', 'data-pm-dialog': id }, 'aria-busy': busy || undefined,
77
+ ...description ? {} : { 'aria-describedby': undefined },
78
+ onInteractOutside: event => event.preventDefault(),
79
+ onEscapeKeyDown: event => { event.stopPropagation(); if (busy) event.preventDefault() },
80
+ onOpenAutoFocus: event => {
81
+ onOpenAutoFocus?.(event)
82
+ if (event.defaultPrevented || !(event.target instanceof HTMLElement)) return
83
+ const panel = event.target
84
+ if (panel.contains(document.activeElement)) { event.preventDefault(); return }
85
+ const target = [...panel.querySelectorAll<HTMLElement>(`.${kind}__body :is(button, input, textarea, select, a[href], [tabindex]), .pm-ui-dialog__footer :is(button, input, textarea, select, a[href], [tabindex])`)].find(element => element.tabIndex >= 0 && !element.closest('[inert], [hidden]') && !element.matches(':disabled, [aria-disabled="true"]') && element.getClientRects().length && getComputedStyle(element).visibility === 'visible')
86
+ if (target) { event.preventDefault(); target.focus() }
87
+ },
88
+ onCloseAutoFocus: event => {
89
+ onCloseAutoFocus?.(event)
90
+ if (event.defaultPrevented) return
91
+ event.preventDefault()
92
+ const available = (element: HTMLElement | null): element is HTMLElement => Boolean(element?.isConnected && element !== document.body && !element.closest(`[data-pm-dialog="${id}"], [inert], [hidden]`) && !element.matches(':disabled, [aria-disabled="true"]') && element.getClientRects().length && getComputedStyle(element).visibility === 'visible')
93
+ const target = available(focusTarget.current) ? focusTarget.current : [...document.querySelectorAll<HTMLElement>('[role="toolbar"] button, main button')].find(available)
94
+ target?.focus()
95
+ },
96
+ }, h('div', { className: `${kind}__header` },
97
+ h(Dialog.Title, { className: `${kind}__title` }, label), actions,
98
+ h(Dialog.Close, { asChild: true }, h(PmIconButton, { icon: 'close', label: closeLabel ?? HOST_MESSAGES[locale].close, small: true, disabled: busy }))),
99
+ description ? h(Dialog.Description, { className: 'pm-ui-dialog__description' }, description) : null,
100
+ h('div', { className: `${kind}__body` }, children),
101
+ footer ? h('div', { className: 'pm-ui-dialog__footer' }, footer) : null)))
102
+ }
103
+ export function PmDialog(props: OverlayProps) { return h(DialogSurface, props) }
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 })
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
+ 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
+ return h(Tabs.Root, { value: value ?? modelValue, onValueChange, activationMode: 'manual', className: 'pm-ui-tabs-root', ...{ 'data-density': density } },
119
+ h(Tabs.List, { className: 'pm-ui-tabs', 'aria-label': label }, tabs.map(tab => {
120
+ const trigger = h(Tabs.Trigger, { key: tab.value, value: tab.value, className: 'pm-ui-tab', disabled: tab.disabled, 'aria-label': tab.label, ...{ 'data-icon-only': iconOnly || undefined } }, tab.icon ? h(PmIcon, { name: tab.icon }) : null, iconOnly ? null : tab.label)
121
+ return iconOnly ? h(PmTooltip, { key: tab.value, label: tab.label, children: trigger }) : trigger
122
+ })), children)
123
+ }
124
+ 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
+ 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
+ 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
+ /** Loading in the shape of the content: `document` headings and paragraphs, `list` rows, `preview` a slide-shaped area. */
132
+ export function PmSkeleton({ label, shape = 'document' }: { label: string, shape?: 'document' | 'list' | 'preview' }) {
133
+ const bar = (kind?: string, key?: number) => h('span', { key, className: 'pm-ui-skeleton__bar', 'data-kind': kind })
134
+ const paragraph = (lines: number, key: number) => h('span', { key, className: 'pm-ui-skeleton__group' }, Array.from({ length: lines }, (_, index) => bar(undefined, index)))
135
+ const bars = shape === 'list' ? Array.from({ length: 5 }, (_, index) => bar('row', index))
136
+ : shape === 'preview' ? [bar('media', 0), paragraph(2, 1)]
137
+ : [bar('heading', 0), paragraph(4, 1), paragraph(3, 2), bar('heading', 3), paragraph(4, 4)]
138
+ return h('div', { className: 'pm-ui-skeleton', 'data-shape': shape, role: 'status', 'aria-busy': 'true' }, h('span', { className: 'pm-ui-sr-only' }, label), h('span', { className: 'pm-ui-skeleton', 'aria-hidden': 'true' }, bars))
139
+ }
140
+ export const ToolbarPlacementContext = createContext<ToolbarPlacement>('bar')
141
+ export const provideToolbarPlacement = ToolbarPlacementContext.Provider
142
+ export function useToolbarPlacement() { return useContext(ToolbarPlacementContext) }
143
+ export function PmToolbarEntry({ title, label = title ?? '', name, icon = 'more', open = false, onOpenChange, children }: { title?: string, name?: string, label?: string, closeLabel?: string, icon?: IconName, open?: boolean, onOpenChange?(open: boolean): void, children?: ReactNode }) { const where = useToolbarPlacement(); return h(PmPopover, { open, onOpenChange, name, label, placement: where === 'status' ? 'above-start' : 'below-end', trigger: h(PmIconButton, { icon, label, small: true, className: name && `${name}__toggle`, 'aria-expanded': open }) }, children) }
@@ -1 +1,9 @@
1
1
  @import '@lincyaw/design/design.css';
2
+
3
+ .pm-toolbar { display: inline-flex; align-items: center; gap: var(--pm-space-2); }
4
+ .pm-toolbar[data-placement='status'] { gap: var(--pm-space-4); }
5
+ .pm-presenter-panels { position: fixed; right: var(--pm-space-2); bottom: var(--pm-overlay-bar, var(--pm-space-12)); z-index: var(--pm-z-overlay-view); max-width: var(--pm-aside-width); max-height: 50vh; overflow: auto; padding: var(--pm-space-1) var(--pm-space-3); border: var(--pm-size-hairline) solid var(--pm-color-line); border-radius: var(--pm-radius-small); background: var(--pm-color-surface); color: var(--pm-color-ink); font-size: var(--pm-text-sm); box-shadow: var(--pm-shadow-float); }
6
+ .pm-presenter-panels--docked { position: static; max-width: none; max-height: none; margin-top: var(--pm-space-4); box-shadow: none; }
7
+ .pm-presenter-panels summary { cursor: pointer; font-weight: 600; }
8
+ .pm-presenter-panels__badge { display: inline-block; padding: 0 var(--pm-space-1); border-radius: var(--pm-radius-pill); background: var(--pm-color-danger); color: var(--pm-color-surface); text-align: center; }
9
+ .pm-presenter-panels h3 { margin: var(--pm-space-2) 0 var(--pm-space-1); font-size: var(--pm-text-md); }
@@ -0,0 +1,28 @@
1
+ import { Children, createElement as h, useState } from 'react'
2
+ import type { CSSProperties, ReactNode } from 'react'
3
+ import { PmTabs, PmTabPanel } from './components.ts'
4
+
5
+ export interface ContentLayoutProps { title?: string, children?: ReactNode }
6
+ export interface ColumnsLayoutProps extends ContentLayoutProps { columnWidths?: number[] }
7
+ const heading = (title?: string) => title ? h('h3', { className: 'pm-layout__title' }, title) : null
8
+
9
+ /** Authored column proportions are content geometry; narrow surfaces stack the same children. */
10
+ export function PmColumns({ title, columnWidths, children }: ColumnsLayoutProps) {
11
+ const items = Children.toArray(children)
12
+ const columns = items.map((_, index) => `minmax(0, ${Number.isFinite(columnWidths?.[index]) && columnWidths![index]! > 0 ? columnWidths![index] : 1}fr)`).join(' ')
13
+ const style: CSSProperties & { '--pm-layout-columns': string } = { '--pm-layout-columns': columns || 'minmax(0, 1fr)' }
14
+ return h('div', { className: 'pm-layout-columns', style }, heading(title), items.map((item, index) => h('div', { className: 'pm-layout__item', key: index }, item)))
15
+ }
16
+ export function PmStack({ title, children }: ContentLayoutProps) {
17
+ return h('div', { className: 'pm-layout-stack' }, heading(title), children)
18
+ }
19
+ export interface TabLayoutEntry { id: string, title: string, content: ReactNode, visible?: boolean }
20
+ export function PmTabGroup({ title, label, tabs, staticView = false }: { title?: string, label: string, tabs: TabLayoutEntry[], staticView?: boolean }) {
21
+ const [chosen, setChosen] = useState<string>()
22
+ if (staticView) return h('div', { className: 'pm-layout-stack', 'data-layout-static': 'tabs' }, heading(title), tabs.map(tab => h('section', { key: tab.id, className: 'pm-layout-stack' }, h('h4', { className: 'pm-layout__title' }, tab.title), tab.content)))
23
+ const visible = tabs.filter(tab => tab.visible !== false)
24
+ const active = visible.find(tab => tab.id === chosen)?.id ?? visible[0]?.id ?? ''
25
+ return h('div', { className: 'pm-layout-stack' }, heading(title), h(PmTabs, {
26
+ label: title || label, value: active, tabs: visible.map(tab => ({ value: tab.id, label: tab.title })), onValueChange: setChosen,
27
+ }, tabs.map(tab => h(PmTabPanel, { key: tab.id, value: tab.id, forceMount: true, hidden: tab.id !== active || tab.visible === false }, tab.content))))
28
+ }
@@ -0,0 +1,18 @@
1
+ import { Component, createElement as h } from 'react'
2
+ import type { ReactNode, ErrorInfo } from 'react'
3
+ import { errorMessage } from '@lincyaw/kernel'
4
+ import { createStore } from './store.ts'
5
+ import { HOST_MESSAGES } from './messages.ts'
6
+ import { currentLocale } from './locale.ts'
7
+ export interface ComponentError { component: string, message: string }
8
+ export const componentErrorRevision = createStore(0)
9
+ const errors = new Map<object, ComponentError>()
10
+ export function componentErrors(): readonly ComponentError[] { return [...errors.values()] }
11
+ export class PmBoundary extends Component<{ name?: string, resetKey?: unknown, children?: ReactNode }, { error?: ComponentError }> {
12
+ state: { error?: ComponentError } = {}
13
+ static getDerivedStateFromError(error: unknown) { return { error: { component: 'component', message: errorMessage(error) } } }
14
+ componentDidCatch(error: unknown, _info: ErrorInfo) { const problem = { component: this.props.name ?? 'component', message: errorMessage(error) }; errors.set(this, problem); componentErrorRevision.set(n => n + 1); this.setState({ error: problem }); console.error(`[presmium] ${problem.component} failed`, error) }
15
+ componentDidUpdate(before: Readonly<{ name?: string, resetKey?: unknown }>) { if (before.resetKey !== this.props.resetKey && this.state.error) { errors.delete(this); componentErrorRevision.set(n => n + 1); this.setState({ error: undefined }) } }
16
+ componentWillUnmount() { if (errors.delete(this)) componentErrorRevision.set(n => n + 1) }
17
+ render() { const error = this.state.error; return error ? h('div', { className: 'pm-boundary-error', role: 'alert', 'data-pm-error': error.component }, HOST_MESSAGES[currentLocale()].componentError(error.component, error.message)) : this.props.children }
18
+ }
@@ -0,0 +1,4 @@
1
+ import { createContext } from 'react'
2
+ import type { HostAdapter } from './contracts.ts'
3
+ export const HOST = createContext<HostAdapter | undefined>(undefined)
4
+ export const installed: { adapter?: HostAdapter } = {}