dsh-ptc-plus 0.4.3 → 0.4.10
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.en.md +15 -3
- package/README.md +15 -3
- package/client.js +1157 -533
- package/cordis.patch.yml +12 -2
- package/docs/README.md +2 -0
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +60 -4
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +5 -1
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +14 -3
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
- package/docs/adr/0023-global-user-bindings.md +1 -1
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +82 -18
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +3 -3
- package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +27 -0
- package/docs/adr/0033-gate-client-visual-usability-in-a-browser.md +23 -0
- package/docs/architecture.md +6 -4
- package/docs/binding-review-placement-design.md +29 -21
- package/docs/client-ui.md +20 -8
- package/docs/client-visual-contract.md +48 -0
- package/docs/durability-design.md +12 -0
- package/docs/installation.md +48 -9
- package/docs/publishing.md +4 -4
- package/docs/release-notes.md +109 -0
- package/docs/runtime-reference.md +8 -2
- package/docs/user-bindings.en.md +2 -2
- package/docs/user-bindings.md +3 -3
- package/docs/verification-optimization-work.md +26 -0
- package/docs/verification.md +7 -1
- package/index.js +28 -7
- package/internal/compiler-platform.js +5 -3
- package/internal/compiler-service.js +2 -9
- package/internal/cordis-tools-owner.js +75 -16
- package/internal/direct-surface-owner.js +333 -7
- package/internal/edit-transport-owner.js +8 -5
- package/internal/execution-provider-owner.js +157 -0
- package/internal/execution-seam-compat.js +69 -113
- package/internal/isolated-worker.js +12 -8
- package/internal/recovery-tips.js +50 -21
- package/internal/rejected-cell-editor.js +11 -21
- package/internal/runtime-bridge-owner.js +88 -57
- package/internal/runtime-contexts.js +13 -13
- package/internal/session-events.js +40 -4
- package/internal/session-journal-recovery.js +678 -181
- package/internal/session-log-view.js +189 -43
- package/internal/session-runtime.js +58 -10
- package/internal/settings-compat.js +12 -2
- package/internal/user-bindings-owner.js +6 -5
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +18 -13
- package/src/client-authoring-view.js +222 -141
- package/src/client-binding-review.js +2 -0
- package/src/client-code-editor.js +6 -0
- package/src/client-copy.js +10 -0
- package/src/client-feature-gates.js +12 -5
- package/src/client-host-compat.js +23 -2
- package/src/client-primitives.js +249 -0
- package/src/client-repl-view.js +3 -3
- package/src/client-settings-view.js +2 -29
- package/src/client-styles.js +92 -33
- package/src/client-tool-view.js +8 -3
- package/src/client-workbench.js +9 -7
- package/src/client.js +72 -46
- package/assets/ptc-plus-bindings-en.png +0 -0
- package/assets/ptc-plus-bindings-zh.png +0 -0
- package/assets/ptc-plus-repair-en.png +0 -0
- package/assets/ptc-plus-repair-zh.png +0 -0
|
@@ -3,6 +3,7 @@ import { bindingModelPreferences } from '../internal/user-binding-model-context.
|
|
|
3
3
|
import { featureEnabled } from './client-feature-gates.js'
|
|
4
4
|
import {
|
|
5
5
|
MENU_CHILDREN_PROBE_SENTINEL,
|
|
6
|
+
isHostComponent,
|
|
6
7
|
isHostIconComponent,
|
|
7
8
|
readMenuChildrenProbe,
|
|
8
9
|
} from './client-host-compat.js'
|
|
@@ -22,7 +23,7 @@ const HOVER_DWELL_MS = 150
|
|
|
22
23
|
*/
|
|
23
24
|
export function createAuthoringView(React, deps) {
|
|
24
25
|
const {
|
|
25
|
-
ActionButton, IconButton, Menu, Toast, Tooltip, CodeBlock, BindingsDialog, PTCPlusSettingsDialog,
|
|
26
|
+
ActionButton, IconButton, Menu, MenuFallback, Toast, Tooltip, CodeBlock, Modal, createPortal, BindingsDialog, PTCPlusSettingsDialog,
|
|
26
27
|
useWorkbenchController, useBindingReview, catalogOwner, callUserBindings, subscribeReset,
|
|
27
28
|
settingsCardSeat, updateSetting, menuChildren,
|
|
28
29
|
icons: {
|
|
@@ -32,6 +33,24 @@ export function createAuthoringView(React, deps) {
|
|
|
32
33
|
} = deps
|
|
33
34
|
const h = React.createElement
|
|
34
35
|
|
|
36
|
+
function EnlargeIcon() {
|
|
37
|
+
return h('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true },
|
|
38
|
+
h('path', { d: 'M6 2H2v4m8-4h4v4M2 10v4h4m8-4v4h-4', stroke: 'currentColor', strokeWidth: 1.5,
|
|
39
|
+
strokeLinecap: 'round', strokeLinejoin: 'round' }))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function RestoreIcon() {
|
|
43
|
+
return h('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true },
|
|
44
|
+
h('path', { d: 'M2 6h4V2m4 0v4h4M6 14v-4H2m12 0h-4v4', stroke: 'currentColor', strokeWidth: 1.5,
|
|
45
|
+
strokeLinecap: 'round', strokeLinejoin: 'round' }))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ReviewCloseIcon() {
|
|
49
|
+
return h('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true },
|
|
50
|
+
h('path', { d: 'm4 4 8 8m0-8-8 8', stroke: 'currentColor', strokeWidth: 1.5,
|
|
51
|
+
strokeLinecap: 'round' }))
|
|
52
|
+
}
|
|
53
|
+
|
|
35
54
|
/**
|
|
36
55
|
* Where the settings card lives on the installed generation.
|
|
37
56
|
*
|
|
@@ -56,10 +75,10 @@ export function createAuthoringView(React, deps) {
|
|
|
56
75
|
anchorRef.current = element
|
|
57
76
|
review.access = element
|
|
58
77
|
}, [review])
|
|
59
|
-
const
|
|
60
|
-
const manageItemRef = React.useRef(null)
|
|
78
|
+
const menuBodyRef = React.useRef(null)
|
|
61
79
|
const reloadItemRef = React.useRef(null)
|
|
62
|
-
const
|
|
80
|
+
const disabledGroupRef = React.useRef(null)
|
|
81
|
+
const [disabledOpen, setDisabledOpen] = React.useState(false)
|
|
63
82
|
const focused = React.useRef(false)
|
|
64
83
|
const dialogReturnFocus = React.useRef(null)
|
|
65
84
|
const hoverTimer = React.useRef(undefined)
|
|
@@ -68,8 +87,8 @@ export function createAuthoringView(React, deps) {
|
|
|
68
87
|
const [settingsOpen, setSettingsOpen] = React.useState(false)
|
|
69
88
|
// The composer entry's four actions live in the host Menu's `children`
|
|
70
89
|
// region. Preceding Clients accept that prop and mount nothing, so the entry
|
|
71
|
-
// probes the installed primitive once and
|
|
72
|
-
//
|
|
90
|
+
// probes the installed primitive once and uses its own Menu fallback when
|
|
91
|
+
// that public region is absent.
|
|
73
92
|
const probeRef = React.useRef(null)
|
|
74
93
|
const [probing, setProbing] = React.useState(() => menuChildren.supported() === undefined)
|
|
75
94
|
const menuChildrenRegion = menuChildren.supported() !== false
|
|
@@ -128,7 +147,10 @@ export function createAuthoringView(React, deps) {
|
|
|
128
147
|
React.useEffect(() => cancelHoverOpen, [cancelHoverOpen])
|
|
129
148
|
const showMenu = mode => {
|
|
130
149
|
cancelHoverOpen()
|
|
131
|
-
if (!menuOpen
|
|
150
|
+
if (!menuOpen) {
|
|
151
|
+
setDisabledOpen(false)
|
|
152
|
+
if (quickAccess) void refreshCatalog()
|
|
153
|
+
}
|
|
132
154
|
setMenu({ key: view.candidateKey, mode, step: 'root' })
|
|
133
155
|
}
|
|
134
156
|
// Touch has no hover: its tap reaches the same menu through the click path.
|
|
@@ -140,11 +162,24 @@ export function createAuthoringView(React, deps) {
|
|
|
140
162
|
showMenu('hover')
|
|
141
163
|
}, HOVER_DWELL_MS)
|
|
142
164
|
}
|
|
143
|
-
const menuElement = () =>
|
|
144
|
-
?? firstItemRef.current?.closest('[role=menu]')
|
|
165
|
+
const menuElement = () => menuBodyRef.current
|
|
145
166
|
const captureCatalogFocus = () => {
|
|
146
167
|
catalogFocus.current = menuElement()?.contains(document.activeElement) ? document.activeElement : null
|
|
147
168
|
}
|
|
169
|
+
const focusMenuItem = next => {
|
|
170
|
+
next?.focus({ preventScroll: true })
|
|
171
|
+
const scroll = menuElement()?.querySelector('.ptcPlusBindingMenuScroll')
|
|
172
|
+
if (!scroll?.contains(next)) return
|
|
173
|
+
const viewport = scroll.getBoundingClientRect()
|
|
174
|
+
const item = next.getBoundingClientRect()
|
|
175
|
+
// Reveal only inside the owned catalog. Browser focus scrolling can also
|
|
176
|
+
// scroll the outer menu or transcript and move the fixed actions.
|
|
177
|
+
const offset = item.height > scroll.clientHeight
|
|
178
|
+
? item.top + item.height / 2 - (viewport.top + viewport.height / 2)
|
|
179
|
+
: item.top < viewport.top ? item.top - viewport.top
|
|
180
|
+
: item.bottom > viewport.bottom ? item.bottom - viewport.bottom : 0
|
|
181
|
+
scroll.scrollTop += offset
|
|
182
|
+
}
|
|
148
183
|
// The list must sit next to the entry that opened it. The published Menu reads
|
|
149
184
|
// this rect on open and on every scroll/resize, and re-runs placement whenever
|
|
150
185
|
// the prop identity changes, so measuring the trigger each render also keeps an
|
|
@@ -190,19 +225,22 @@ export function createAuthoringView(React, deps) {
|
|
|
190
225
|
if (!menuOpen) { catalogFocus.current = null; return }
|
|
191
226
|
if (catalogStatus === 'writing' || catalogStatus === 'loading') return
|
|
192
227
|
const target = catalogFocus.current
|
|
193
|
-
|
|
194
|
-
if (!target || document.activeElement !== document.body) return
|
|
228
|
+
if (!target || (document.activeElement !== document.body && document.activeElement !== target)) return
|
|
195
229
|
const root = menuElement()
|
|
230
|
+
const bindingId = target.dataset.bindingId
|
|
231
|
+
const currentRow = bindingId === undefined ? null
|
|
232
|
+
: [...(root?.querySelectorAll('[data-binding-id]') ?? [])].find(row => row.dataset.bindingId === bindingId)
|
|
196
233
|
const next = root?.contains(target) && !target.disabled ? target
|
|
197
|
-
:
|
|
198
|
-
|
|
234
|
+
: currentRow ?? (bindingId !== undefined && catalog?.entries.some(entry => entry.id === bindingId && !entry.enabled)
|
|
235
|
+
? disabledGroupRef.current : null) ?? reloadItemRef.current ?? root?.querySelector('button:not(:disabled)')
|
|
236
|
+
focusMenuItem(next)
|
|
199
237
|
}, [catalogStatus, menuOpen])
|
|
200
238
|
React.useLayoutEffect(() => {
|
|
201
239
|
if (!menuOpen || menu.mode === 'hover') return
|
|
202
240
|
// The public Menu commits its measured position before keyboard focus enters it.
|
|
203
241
|
let cancelled = false
|
|
204
242
|
queueMicrotask(() => {
|
|
205
|
-
if (!cancelled && anchorRef.current?.getClientRects().length) menuElement()?.querySelector('button:not(:disabled)')
|
|
243
|
+
if (!cancelled && anchorRef.current?.getClientRects().length) focusMenuItem(menuElement()?.querySelector('button:not(:disabled)'))
|
|
206
244
|
})
|
|
207
245
|
return () => { cancelled = true }
|
|
208
246
|
}, [menuOpen, menu?.mode, menu?.step])
|
|
@@ -217,7 +255,7 @@ export function createAuthoringView(React, deps) {
|
|
|
217
255
|
setProbing(false)
|
|
218
256
|
}, [probing, menuChildren])
|
|
219
257
|
React.useEffect(() => {
|
|
220
|
-
if (toast === null ||
|
|
258
|
+
if (toast === null || isHostComponent(Toast)) return undefined
|
|
221
259
|
const timer = setTimeout(() => setToast(null), 2_500)
|
|
222
260
|
return () => clearTimeout(timer)
|
|
223
261
|
}, [toast])
|
|
@@ -241,7 +279,7 @@ export function createAuthoringView(React, deps) {
|
|
|
241
279
|
const starButton = h('button', {
|
|
242
280
|
type: 'button', className: 'ptcPlusAuthorButton', 'aria-label': label,
|
|
243
281
|
// Older UI-kit lines ship no Tooltip primitive; the native title carries the hint there.
|
|
244
|
-
title:
|
|
282
|
+
title: isHostComponent(Tooltip) || menuOpen || managing || settingsOpen ? undefined : hint,
|
|
245
283
|
'aria-haspopup': 'menu', 'aria-expanded': menuOpen,
|
|
246
284
|
onPointerEnter: hoverMenu,
|
|
247
285
|
onPointerLeave: cancelHoverOpen,
|
|
@@ -261,15 +299,31 @@ export function createAuthoringView(React, deps) {
|
|
|
261
299
|
// and drops a duplicate, while disabling rows would dim every row and add a
|
|
262
300
|
// pending row for the duration of the request: both read as the popover
|
|
263
301
|
// flickering when the user toggles an entry.
|
|
302
|
+
const catalogRow = entry => ({ id: `global:${entry.id}`, bindingId: entry.id,
|
|
303
|
+
disabled: catalog === null,
|
|
304
|
+
label: h('span', { className: 'ptcPlusBindingQuickRow', 'data-enabled': entry.enabled },
|
|
305
|
+
h('span', { className: 'ptcPlusBindingQuickName' }, h('strong', { title: entry.name }, entry.name),
|
|
306
|
+
h('span', { className: 'ptcPlusBindingQuickState' }, t(entry.enabled ? 'bindings.enabled' : 'bindings.disabledEntry'))),
|
|
307
|
+
h('span', { className: 'ptcPlusBindingQuickPurpose', title: entry.purpose }, entry.purpose)),
|
|
308
|
+
})
|
|
309
|
+
const enabledEntries = (catalog?.entries ?? []).filter(entry => entry.enabled)
|
|
310
|
+
const disabledEntries = (catalog?.entries ?? []).filter(entry => !entry.enabled)
|
|
264
311
|
const catalogItems = !quickAccess ? [] : [
|
|
265
312
|
{ id: 'global-heading', type: 'label', text: t('bindings.quickHeading') },
|
|
266
|
-
...(catalog
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
313
|
+
...(catalog === null || catalog.entries.length === 0 ? [] : [
|
|
314
|
+
{ id: 'enabled-heading', type: 'label', text: t('bindings.quickEnabled', { count: enabledEntries.length }) },
|
|
315
|
+
...enabledEntries.map(catalogRow),
|
|
316
|
+
...(enabledEntries.length === 0 ? [{ id: 'no-enabled', type: 'label', text: t('bindings.quickNoEnabled') }] : []),
|
|
317
|
+
...(disabledEntries.length === 0 ? [] : [
|
|
318
|
+
{ id: 'disabled-group', label: h(React.Fragment, null,
|
|
319
|
+
h('svg', { className: 'ptcPlusMenuGroupChevron', 'aria-hidden': true,
|
|
320
|
+
width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none', 'data-expanded': disabledOpen },
|
|
321
|
+
h('path', { d: 'm5 3 4 4-4 4', stroke: 'currentColor', strokeWidth: 1.5,
|
|
322
|
+
strokeLinecap: 'round', strokeLinejoin: 'round' })),
|
|
323
|
+
h('span', null, t('bindings.quickDisabled', { count: disabledEntries.length }))) },
|
|
324
|
+
...(disabledOpen ? disabledEntries.map(catalogRow) : []),
|
|
325
|
+
]),
|
|
326
|
+
]),
|
|
273
327
|
...(catalog !== null && catalog.entries.length === 0
|
|
274
328
|
? [{ id: 'empty', type: 'label', text: t('memory.globalEmpty') }] : []),
|
|
275
329
|
...(catalog === null && catalogStatus !== 'error'
|
|
@@ -289,12 +343,10 @@ export function createAuthoringView(React, deps) {
|
|
|
289
343
|
h('span', { className: 'ptcPlusBindingQuickState' }, t(entry.enabled ? 'bindings.enabled' : 'bindings.disabledEntry'))),
|
|
290
344
|
h('span', { className: 'ptcPlusBindingQuickPurpose', title: entry.purpose }, entry.purpose)),
|
|
291
345
|
})),
|
|
292
|
-
// The back row also anchors menu lookup, so the step still owns its list
|
|
293
|
-
// when the catalog is momentarily empty.
|
|
294
|
-
{ id: 'edit-back', label: h('span', { className: 'ptcPlusBindingMenuAction', ref: firstItemRef }, t('bindings.back')) },
|
|
295
346
|
]
|
|
296
347
|
const selectMenuItem = id => {
|
|
297
348
|
if (!anchorRef.current?.getClientRects().length) return
|
|
349
|
+
if (id === 'disabled-group') { setDisabledOpen(open => !open); return }
|
|
298
350
|
if (id === 'edit') {
|
|
299
351
|
setMenu(current => current === null ? current : { ...current, step: 'edit' })
|
|
300
352
|
return
|
|
@@ -329,14 +381,14 @@ export function createAuthoringView(React, deps) {
|
|
|
329
381
|
}
|
|
330
382
|
// One copy of each action label serves the grid the plugin renders itself,
|
|
331
383
|
// in whichever carrier the installed Menu publishes for it.
|
|
332
|
-
const actionCopy = (id, copy
|
|
333
|
-
|
|
384
|
+
const actionCopy = (id, copy) => h('span', {
|
|
385
|
+
className: 'ptcPlusBindingMenuAction',
|
|
334
386
|
title: id === 'settings' ? settingsPath(t) : undefined,
|
|
335
387
|
}, t(copy))
|
|
336
|
-
const actionButton = (id, copy
|
|
388
|
+
const actionButton = (id, copy) => h('button', {
|
|
337
389
|
type: 'button', role: 'menuitem', className: 'ptcPlusMenuButton',
|
|
338
390
|
onClick: () => selectMenuItem(id),
|
|
339
|
-
}, actionCopy(id, copy
|
|
391
|
+
}, actionCopy(id, copy))
|
|
340
392
|
const actionSections = [
|
|
341
393
|
canAuthor ? h('div', { key: 'authoring', className: 'ptcPlusMenuAuthoring', role: 'group', 'aria-label': t('bindings.authorGroup') },
|
|
342
394
|
h('div', { className: 'ptcPlusMenuGroupLabel' }, t('bindings.authorGroup')),
|
|
@@ -344,49 +396,58 @@ export function createAuthoringView(React, deps) {
|
|
|
344
396
|
actionButton('new', 'bindings.authorNewDraft'),
|
|
345
397
|
(catalog?.entries?.length ?? 0) > 0 ? actionButton('edit', 'bindings.authorEdit') : null)) : null,
|
|
346
398
|
h('div', { key: 'utilities', className: 'ptcPlusMenuUtilities' },
|
|
347
|
-
quickAccess ? actionButton('manage', 'bindings.manage'
|
|
348
|
-
actionButton('settings', 'settings.menuEntry'
|
|
399
|
+
quickAccess ? actionButton('manage', 'bindings.manage') : null,
|
|
400
|
+
actionButton('settings', 'settings.menuEntry')),
|
|
401
|
+
]
|
|
402
|
+
const menuEntries = menu?.step === 'edit' ? editItems : [
|
|
403
|
+
...(hasDraft ? [{ id: 'draft-heading', type: 'label', text: t('bindings.quickDrafts') }, {
|
|
404
|
+
id: view.candidateKey,
|
|
405
|
+
label: h('span', { className: 'ptcPlusDraftMenuItem' },
|
|
406
|
+
h('strong', null, view.candidate.entry.name), h('span', null, t(bindingReviewStatus(view)))),
|
|
407
|
+
}, ...(quickAccess ? [{ id: 'draft-separator', type: 'separator' }] : [])] : []),
|
|
408
|
+
...catalogItems,
|
|
409
|
+
...(quickAccess && catalogError !== null ? [{ id: 'reload', label: t('bindings.reload') }] : []),
|
|
349
410
|
]
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
411
|
+
const menuContent = h('div', { className: 'ptcPlusBindingMenuContent', ref: menuBodyRef,
|
|
412
|
+
// A reopened menu can acquire foreground focus while an earlier write is
|
|
413
|
+
// pending. Recovery follows the live menu, not the initiating operation.
|
|
414
|
+
onFocusCapture: event => { catalogFocus.current = event.target },
|
|
415
|
+
onBlurCapture: event => {
|
|
416
|
+
if (!event.currentTarget.contains(event.relatedTarget)) catalogFocus.current = null
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
h('div', { className: 'ptcPlusBindingMenuScroll' }, ...menuEntries.map(entry => entry.type === 'separator'
|
|
420
|
+
? h('div', { key: entry.id, role: 'separator', className: 'ptcPlusOwnedMenuSeparator' })
|
|
421
|
+
: entry.type === 'label'
|
|
422
|
+
? h('div', { key: entry.id, className: 'ptcPlusMenuGroupLabel' }, entry.text)
|
|
423
|
+
: h('button', {
|
|
424
|
+
key: entry.id, type: 'button', role: 'menuitem', className: 'ptcPlusOwnedMenuRow',
|
|
425
|
+
disabled: entry.disabled, 'data-binding-id': entry.bindingId,
|
|
426
|
+
ref: entry.id === 'reload' ? reloadItemRef : entry.id === 'disabled-group' ? disabledGroupRef : undefined,
|
|
427
|
+
'aria-expanded': entry.id === 'disabled-group' ? disabledOpen : undefined,
|
|
428
|
+
onClick: () => selectMenuItem(entry.id),
|
|
429
|
+
}, entry.label))),
|
|
430
|
+
h('div', { className: 'ptcPlusMenuActions' }, menu?.step === 'edit'
|
|
431
|
+
? h('div', { className: 'ptcPlusMenuUtilities' }, actionButton('edit-back', 'bindings.back')) : actionSections))
|
|
363
432
|
return h('span', {
|
|
364
433
|
className: 'ptcPlusComposerBindingAnchor', tabIndex: -1,
|
|
365
434
|
onFocusCapture: () => { focused.current = true }, onBlurCapture: () => { focused.current = false },
|
|
366
435
|
'data-text': isHostIconComponent(IconSparkle16) ? undefined : true,
|
|
367
436
|
ref: attachAnchor,
|
|
368
437
|
},
|
|
369
|
-
!hasDraft && !quickAccess ? null : h(Menu, {
|
|
438
|
+
!hasDraft && !quickAccess ? null : h(menuChildrenRegion ? Menu : MenuFallback, {
|
|
370
439
|
className: 'ptcPlusAuthorButtonShell', open: menuOpen,
|
|
371
|
-
anchor:
|
|
440
|
+
anchor: isHostComponent(Tooltip)
|
|
372
441
|
? h(Tooltip, { label: hint, delayMs: 700, disabled: menuOpen || managing || settingsOpen }, starButton) : starButton,
|
|
373
442
|
portal: true, side: 'top', dense: true,
|
|
443
|
+
listClassName: 'ptcPlusBindingMenu',
|
|
374
444
|
// A hovered menu leaves with the pointer once it leaves the trigger and the
|
|
375
445
|
// list for the published grace, which re-entering either cancels; click and
|
|
376
446
|
// keyboard menus stay until dismissed.
|
|
377
447
|
closeOnPointerLeave: menu !== null && menu.mode === 'hover',
|
|
378
448
|
getAnchorRect: menuAnchorRect,
|
|
379
449
|
onClose: hideMenu,
|
|
380
|
-
items:
|
|
381
|
-
label: h('span', { className: 'ptcPlusDraftMenuItem', ref: firstItemRef },
|
|
382
|
-
h('strong', null, view.candidate.entry.name),
|
|
383
|
-
h('span', null, t(bindingReviewStatus(view)))) },
|
|
384
|
-
...(quickAccess ? [{ id: 'draft-separator', type: 'separator' }] : [])] : []), ...catalogItems,
|
|
385
|
-
...(quickAccess && catalogError !== null
|
|
386
|
-
? [{ id: 'reload', label: h('span', { ref: reloadItemRef }, t('bindings.reload')) }] : []),
|
|
387
|
-
],
|
|
388
|
-
children: actionsInRegion,
|
|
389
|
-
...(actionsPinned.length === 0 ? {} : { footer: actionsPinned }),
|
|
450
|
+
items: [], children: menuContent,
|
|
390
451
|
onSelect: selectMenuItem,
|
|
391
452
|
}),
|
|
392
453
|
// The probe mounts the host Menu's own `children` region inside a hidden
|
|
@@ -413,7 +474,7 @@ export function createAuthoringView(React, deps) {
|
|
|
413
474
|
?.focus({ preventScroll: true }))
|
|
414
475
|
},
|
|
415
476
|
}) : null,
|
|
416
|
-
toast === null ? null :
|
|
477
|
+
toast === null ? null : isHostComponent(Toast)
|
|
417
478
|
? h(Toast, {
|
|
418
479
|
key: toast.sequence,
|
|
419
480
|
text: toast.text,
|
|
@@ -432,7 +493,7 @@ export function createAuthoringView(React, deps) {
|
|
|
432
493
|
showName ? h('strong', null, candidate.entry.name) : null,
|
|
433
494
|
h('span', { className: 'ptcPlusBindingMeta' }, `${candidate.entry.scope} - ${candidate.entry.symbols.join(', ')}`),
|
|
434
495
|
candidate.entry.purpose ? h('p', { className: 'ptcPlusMessage' }, candidate.entry.purpose) : null,
|
|
435
|
-
|
|
496
|
+
isHostComponent(CodeBlock)
|
|
436
497
|
? h(CodeBlock, { code: candidate.entry.source, lang: 'typescript', className: 'ptcPlusBindingCommandCode',
|
|
437
498
|
copyLabel: t('tool.copy'), copiedLabel: t('tool.copied') })
|
|
438
499
|
: h('pre', { className: 'ptcPlusBindingCommandSource' }, candidate.entry.source),
|
|
@@ -450,22 +511,20 @@ export function createAuthoringView(React, deps) {
|
|
|
450
511
|
preferences.instructions || t('bindings.noInstructions'))))))
|
|
451
512
|
}
|
|
452
513
|
|
|
453
|
-
// Native editable semantics are enough to restore focus; no Host class, store or editor internals are used.
|
|
454
514
|
function focusComposer(anchor) {
|
|
455
515
|
if (!anchor || anchor.getClientRects().length === 0) return
|
|
456
|
-
for (let parent = anchor.parentElement; parent; parent = parent.parentElement) {
|
|
457
|
-
const editable = [...parent.querySelectorAll('textarea, [contenteditable="true"]')]
|
|
458
|
-
.find(element => element.getClientRects().length > 0 && !element.disabled)
|
|
459
|
-
if (editable) { editable.focus({ preventScroll: true }); return }
|
|
460
|
-
}
|
|
461
516
|
anchor.focus({ preventScroll: true })
|
|
462
517
|
}
|
|
463
518
|
|
|
464
|
-
function openBindingReview(review, candidate = review.getSnapshot().candidate
|
|
465
|
-
|
|
519
|
+
function openBindingReview(review, candidate = review.getSnapshot().candidate,
|
|
520
|
+
visibility = 'enlarged') {
|
|
521
|
+
if (!review.display(visibility, candidate)) return
|
|
466
522
|
requestAnimationFrame(() => {
|
|
467
|
-
|
|
468
|
-
|
|
523
|
+
const current = review.getSnapshot()
|
|
524
|
+
if (review.isCurrentCandidate(candidate) && current.reachable && current.visibility === 'expanded'
|
|
525
|
+
&& review.panel?.getClientRects().length) {
|
|
526
|
+
if (review.previewAvailable === false) focusBindingReviewAccess(review)
|
|
527
|
+
else review.panel.focus({ preventScroll: true })
|
|
469
528
|
}
|
|
470
529
|
})
|
|
471
530
|
}
|
|
@@ -477,60 +536,6 @@ export function createAuthoringView(React, deps) {
|
|
|
477
536
|
else focusComposer(anchor)
|
|
478
537
|
}
|
|
479
538
|
|
|
480
|
-
function fitBindingReview(panel) {
|
|
481
|
-
const body = panel?.querySelector('.ptcPlusBindingDockBody')
|
|
482
|
-
const anchor = panel?.parentElement
|
|
483
|
-
if (!anchor || typeof ResizeObserver !== 'function') return undefined
|
|
484
|
-
const ancestors = []
|
|
485
|
-
let seat = panel
|
|
486
|
-
let viewport
|
|
487
|
-
for (let parent = panel.parentElement; parent; parent = parent.parentElement) {
|
|
488
|
-
ancestors.push(parent)
|
|
489
|
-
if (/auto|scroll|hidden|clip/.test(getComputedStyle(parent).overflowY)) {
|
|
490
|
-
viewport = parent
|
|
491
|
-
break
|
|
492
|
-
}
|
|
493
|
-
seat = parent
|
|
494
|
-
}
|
|
495
|
-
if (!viewport) return undefined
|
|
496
|
-
let frame
|
|
497
|
-
const update = () => {
|
|
498
|
-
if (!panel.getClientRects().length) return
|
|
499
|
-
const top = Math.max(viewport.getBoundingClientRect().top + viewport.clientTop,
|
|
500
|
-
window.visualViewport?.offsetTop ?? 0)
|
|
501
|
-
// Float above the complete composer stack without changing its height or covering other docks.
|
|
502
|
-
const offset = anchor.getBoundingClientRect().bottom - seat.getBoundingClientRect().top + 8
|
|
503
|
-
panel.style.setProperty('--ptc-plus-review-offset', `${Math.max(8, Math.ceil(offset))}px`)
|
|
504
|
-
const room = Math.max(0, seat.getBoundingClientRect().top - top - 16)
|
|
505
|
-
panel.style.setProperty('--ptc-plus-review-height', `${Math.floor(room)}px`)
|
|
506
|
-
if (body) {
|
|
507
|
-
const chrome = panel.scrollHeight - body.offsetHeight + 2
|
|
508
|
-
const available = Math.max(0, room - chrome)
|
|
509
|
-
// In short viewports one scroller keeps source and actions reachable together.
|
|
510
|
-
panel.dataset.scroll = available < 80 ? 'panel' : 'body'
|
|
511
|
-
body.style.setProperty('--ptc-plus-review-space', `${Math.floor(available)}px`)
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
const schedule = () => {
|
|
515
|
-
cancelAnimationFrame(frame)
|
|
516
|
-
frame = requestAnimationFrame(update)
|
|
517
|
-
}
|
|
518
|
-
const observer = new ResizeObserver(schedule)
|
|
519
|
-
for (const element of [panel, ...panel.children, ...ancestors]) observer.observe(element)
|
|
520
|
-
window.visualViewport?.addEventListener('resize', schedule)
|
|
521
|
-
window.visualViewport?.addEventListener('scroll', schedule)
|
|
522
|
-
update()
|
|
523
|
-
return () => {
|
|
524
|
-
observer.disconnect()
|
|
525
|
-
cancelAnimationFrame(frame)
|
|
526
|
-
window.visualViewport?.removeEventListener('resize', schedule)
|
|
527
|
-
window.visualViewport?.removeEventListener('scroll', schedule)
|
|
528
|
-
panel.style.removeProperty('--ptc-plus-review-offset')
|
|
529
|
-
panel.style.removeProperty('--ptc-plus-review-height')
|
|
530
|
-
delete panel.dataset.scroll
|
|
531
|
-
body?.style.removeProperty('--ptc-plus-review-space')
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
539
|
|
|
535
540
|
function BindingReviewDock({ sessionId, useProjection, t }) {
|
|
536
541
|
const raw = useProjection('ptcPlusBindingDraft')
|
|
@@ -549,10 +554,75 @@ export function createAuthoringView(React, deps) {
|
|
|
549
554
|
React.useLayoutEffect(() => { review.sync(projection) }, [review, projection])
|
|
550
555
|
const title = React.useId()
|
|
551
556
|
const content = React.useId()
|
|
552
|
-
const
|
|
557
|
+
const anchor = React.useRef(null)
|
|
558
|
+
const layer = React.useRef(null)
|
|
559
|
+
const [compact, setCompact] = React.useState(false)
|
|
560
|
+
const enlarged = view.visibility === 'enlarged'
|
|
561
|
+
const expanded = enlarged || view.visibility === 'expanded' && !compact
|
|
553
562
|
const candidateKey = view.candidateKey
|
|
554
|
-
React.useLayoutEffect(() =>
|
|
555
|
-
|
|
563
|
+
React.useLayoutEffect(() => {
|
|
564
|
+
if (enlarged || !view.reachable || view.visibility === 'hidden' || view.candidate === null) return undefined
|
|
565
|
+
let frame
|
|
566
|
+
let previous
|
|
567
|
+
// A dock can move when input or sibling cards grow without a scroll/resize.
|
|
568
|
+
// Track only our own anchor while visible; no Host node owns this placement.
|
|
569
|
+
const place = () => {
|
|
570
|
+
const rect = anchor.current?.getBoundingClientRect()
|
|
571
|
+
const surface = layer.current
|
|
572
|
+
if (rect && surface) {
|
|
573
|
+
const viewport = window.visualViewport
|
|
574
|
+
const leftEdge = (viewport?.offsetLeft ?? 0) + 8
|
|
575
|
+
const topEdge = (viewport?.offsetTop ?? 0) + 8
|
|
576
|
+
const rightEdge = leftEdge + (viewport?.width ?? window.innerWidth) - 16
|
|
577
|
+
const bottomEdge = topEdge + (viewport?.height ?? window.innerHeight) - 16
|
|
578
|
+
const width = Math.min(rect.width, rightEdge - leftEdge)
|
|
579
|
+
const left = Math.max(leftEdge, Math.min(rect.left, rightEdge - width))
|
|
580
|
+
if (surface.style.width !== `${width}px`) surface.style.width = `${width}px`
|
|
581
|
+
const panel = surface.querySelector('.ptcPlusBindingDock')
|
|
582
|
+
const head = panel?.querySelector('.ptcPlusBindingDockHead')
|
|
583
|
+
const actions = panel?.querySelector('.ptcPlusBindingDockActions')
|
|
584
|
+
const body = panel?.querySelector('.ptcPlusBindingDockBody')
|
|
585
|
+
const panelStyle = panel && getComputedStyle(panel)
|
|
586
|
+
const border = (parseFloat(panelStyle?.borderTopWidth) || 0) + (parseFloat(panelStyle?.borderBottomWidth) || 0)
|
|
587
|
+
const chrome = (head?.getBoundingClientRect().height ?? 0) + border
|
|
588
|
+
const bottom = Math.max(topEdge, Math.min(Math.max(topEdge + chrome, rect.top - 8), rect.top, bottomEdge))
|
|
589
|
+
const height = bottom - topEdge
|
|
590
|
+
const geometry = [width, left, bottom, height, chrome].join(',')
|
|
591
|
+
if (geometry !== previous) {
|
|
592
|
+
previous = geometry
|
|
593
|
+
surface.style.left = `${left}px`
|
|
594
|
+
surface.style.bottom = `${window.innerHeight - bottom}px`
|
|
595
|
+
surface.style.setProperty('--ptc-plus-review-space', `${height}px`)
|
|
596
|
+
}
|
|
597
|
+
if (rect.width > 0) {
|
|
598
|
+
const available = surface.getBoundingClientRect().height >= chrome && bottom <= rect.top
|
|
599
|
+
review.previewAvailable = available
|
|
600
|
+
if (surface.dataset.available !== String(available)) {
|
|
601
|
+
surface.dataset.available = String(available)
|
|
602
|
+
if (available) {
|
|
603
|
+
surface.removeAttribute('inert')
|
|
604
|
+
surface.removeAttribute('aria-hidden')
|
|
605
|
+
} else {
|
|
606
|
+
surface.setAttribute('inert', '')
|
|
607
|
+
surface.setAttribute('aria-hidden', 'true')
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
// CSS owns the viewport cap. Full chrome stays measurable in compact
|
|
612
|
+
// mode so hiding it cannot make the layout classification oscillate.
|
|
613
|
+
if (head && actions && body && rect.width > 0) {
|
|
614
|
+
const minimumBody = parseFloat(getComputedStyle(body).minBlockSize)
|
|
615
|
+
const required = chrome + actions.getBoundingClientRect().height + minimumBody
|
|
616
|
+
setCompact(surface.getBoundingClientRect().height < required)
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
frame = requestAnimationFrame(place)
|
|
620
|
+
}
|
|
621
|
+
place()
|
|
622
|
+
return () => {
|
|
623
|
+
cancelAnimationFrame(frame)
|
|
624
|
+
}
|
|
625
|
+
}, [candidateKey, enlarged, view.reachable, view.visibility, view.candidate === null])
|
|
556
626
|
const close = () => {
|
|
557
627
|
review.display('hidden')
|
|
558
628
|
requestAnimationFrame(() => focusBindingReviewAccess(review))
|
|
@@ -562,44 +632,55 @@ export function createAuthoringView(React, deps) {
|
|
|
562
632
|
if (review.panel?.contains(document.activeElement)) review.panel.focus({ preventScroll: true })
|
|
563
633
|
void review.act(operation, activate)
|
|
564
634
|
}
|
|
635
|
+
const restorePreview = () => openBindingReview(review, view.candidate, 'expanded')
|
|
565
636
|
if (view.candidate === null || view.visibility === 'hidden') return null
|
|
566
|
-
|
|
567
|
-
h('section', { key: candidateKey, className: 'ptcPlusBindingDock', 'aria-labelledby': title, tabIndex: -1,
|
|
637
|
+
const panel = h('section', { key: candidateKey, className: 'ptcPlusBindingDock', 'aria-labelledby': title, tabIndex: -1,
|
|
568
638
|
ref: element => { review.panel = element },
|
|
569
639
|
onFocusCapture: () => { focused.current = true },
|
|
570
640
|
onBlurCapture: event => {
|
|
571
641
|
// Blink blurs the removed panel before layout effects; completion still owns that lost focus.
|
|
572
642
|
if (event.relatedTarget || review.getSnapshot().action === null) focused.current = false
|
|
573
643
|
},
|
|
574
|
-
'aria-busy': view.busy },
|
|
644
|
+
'aria-busy': view.busy, 'data-compact': !enlarged && compact },
|
|
575
645
|
h('div', { className: 'ptcPlusBindingDockHead' },
|
|
576
|
-
h('button', { type: 'button', className: 'ptcPlusBindingDockToggle',
|
|
577
|
-
'aria-label': t(expanded ? 'bindings.reviewCollapse' : 'bindings.reviewExpand'),
|
|
578
|
-
'aria-expanded': expanded, 'aria-controls': expanded ? content : undefined,
|
|
579
|
-
onClick: () => review.display(expanded ? 'collapsed' : 'expanded') },
|
|
646
|
+
h(enlarged ? 'div' : 'button', { type: enlarged ? undefined : 'button', className: 'ptcPlusBindingDockToggle',
|
|
647
|
+
'aria-label': enlarged ? undefined : t(expanded ? 'bindings.reviewCollapse' : 'bindings.reviewExpand'),
|
|
648
|
+
'aria-expanded': enlarged ? undefined : expanded, 'aria-controls': expanded && !enlarged ? content : undefined,
|
|
649
|
+
onClick: enlarged ? undefined : () => review.display(compact ? 'enlarged' : expanded ? 'collapsed' : 'expanded') },
|
|
580
650
|
h('span', { className: 'ptcPlusBindingDockSymbol', 'aria-hidden': true }, '</>'),
|
|
581
651
|
h('span', { className: 'ptcPlusBindingDockHeading' },
|
|
582
652
|
h('strong', { id: title, title: view.candidate.entry.name }, view.candidate.entry.name),
|
|
583
653
|
h('span', { role: 'status', title: t(bindingReviewStatus(view)) },
|
|
584
654
|
`${t('bindings.reviewTitle')} · ${t(bindingReviewStatus(view))}`)),
|
|
585
|
-
h('span', { className: 'ptcPlusBindingDockChevron', 'aria-hidden': true },
|
|
655
|
+
enlarged ? null : h('span', { className: 'ptcPlusBindingDockChevron', 'aria-hidden': true },
|
|
586
656
|
h(IconChevronDownOutline14, { size: 16 }))),
|
|
587
|
-
h(
|
|
588
|
-
|
|
657
|
+
h('div', { className: 'ptcPlusBindingDockControls' },
|
|
658
|
+
h(IconButton, { icon: enlarged ? RestoreIcon : EnlargeIcon,
|
|
659
|
+
label: t(enlarged ? 'bindings.reviewRestore' : 'bindings.reviewEnlarge'),
|
|
660
|
+
onClick: enlarged ? restorePreview : () => review.display('enlarged', view.candidate) }),
|
|
661
|
+
h(IconButton, { icon: isHostIconComponent(IconCloseOutline16) ? IconCloseOutline16 : ReviewCloseIcon,
|
|
662
|
+
label: t('bindings.reviewClose'), onClick: close }))),
|
|
663
|
+
!enlarged && view.visibility !== 'expanded' ? null : h(React.Fragment, null,
|
|
589
664
|
h('div', { className: 'ptcPlusBindingDockBody', id: content, tabIndex: 0,
|
|
665
|
+
inert: !expanded ? '' : undefined, 'aria-hidden': !expanded ? true : undefined,
|
|
590
666
|
role: 'region', 'aria-label': t('bindings.source') },
|
|
667
|
+
view.message === null ? null : h('p', { className: 'ptcPlusMessage ptcPlusDanger', role: 'status' }, t(view.message)),
|
|
668
|
+
view.message === null ? null : h(ActionButton, { disabled: view.busy,
|
|
669
|
+
onClick: () => review.reset() }, t('bindings.reviewRetry')),
|
|
591
670
|
h(BindingCandidateContent, { candidate: view.candidate, t, showName: false })),
|
|
592
|
-
|
|
593
|
-
|
|
671
|
+
h('div', { className: 'ptcPlusBindingDockActions',
|
|
672
|
+
inert: !expanded ? '' : undefined, 'aria-hidden': !expanded ? true : undefined },
|
|
594
673
|
h(ActionButton, { className: 'ptcPlusBindingDockDiscard', 'data-kind': 'ghost',
|
|
595
674
|
disabled: !view.writable || view.busy,
|
|
596
675
|
onClick: () => act('discard-draft') }, t('bindings.draftDiscard')),
|
|
597
676
|
h(ActionButton, { disabled: !view.writable || view.busy,
|
|
598
677
|
onClick: () => act('save-draft', false) }, t('bindings.draftSave')),
|
|
599
678
|
h(ActionButton, { 'data-kind': 'primary', disabled: !view.writable || view.busy,
|
|
600
|
-
onClick: () => act('save-draft', true) }, t('bindings.draftSaveEnable'))
|
|
601
|
-
|
|
602
|
-
|
|
679
|
+
onClick: () => act('save-draft', true) }, t('bindings.draftSaveEnable')))))
|
|
680
|
+
return h('div', { className: 'ptcPlusBindingDockAnchor', ref: anchor }, enlarged
|
|
681
|
+
? h(Modal, { open: view.reachable, title: `${view.candidate.entry.name} · ${t('bindings.reviewTitle')}`,
|
|
682
|
+
headless: true, className: 'ptcPlusBindingReviewModal', onClose: restorePreview }, panel)
|
|
683
|
+
: !view.reachable ? null : createPortal(h('div', { className: 'ptcPlusBindingPreviewLayer', ref: layer }, panel), document.body))
|
|
603
684
|
}
|
|
604
685
|
|
|
605
686
|
function BindingCommandCard({ node, sessionId, t, useProjection }) {
|
|
@@ -191,6 +191,8 @@ export function createBindingReviews(call) {
|
|
|
191
191
|
}
|
|
192
192
|
const review = {
|
|
193
193
|
getSnapshot: () => snapshot,
|
|
194
|
+
isCurrentCandidate: candidate => !disposed && attached && snapshot.action === null
|
|
195
|
+
&& sameCandidate(snapshot.candidate, candidate),
|
|
194
196
|
subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener) },
|
|
195
197
|
sync,
|
|
196
198
|
attach() {
|
|
@@ -4,6 +4,7 @@ import { EditorView, keymap } from '@codemirror/view'
|
|
|
4
4
|
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'
|
|
5
5
|
import { javascript } from '@codemirror/lang-javascript'
|
|
6
6
|
import { tags } from '@lezer/highlight'
|
|
7
|
+
import { adoptUnownedStyles, captureUnownedStyles } from './client-styles.js'
|
|
7
8
|
|
|
8
9
|
const editorTheme = EditorView.theme({
|
|
9
10
|
'&': { backgroundColor: 'var(--dsw-alias-bg-layer-3)', color: 'var(--dsw-alias-label-primary)', fontSize: '12px' },
|
|
@@ -40,6 +41,10 @@ export function createTypeScriptEditor(React) {
|
|
|
40
41
|
const accessExtensions = () => [EditorState.readOnly.of(disabled), EditorView.editable.of(!disabled)]
|
|
41
42
|
|
|
42
43
|
React.useLayoutEffect(() => {
|
|
44
|
+
// style-mod mounts one global sheet per document and cannot tag it, so
|
|
45
|
+
// the plugin that mounts the editor claims the sheet this mount added
|
|
46
|
+
// before any other client plugin can take it over.
|
|
47
|
+
const unowned = captureUnownedStyles()
|
|
43
48
|
const editor = new EditorView({
|
|
44
49
|
parent: parent.current,
|
|
45
50
|
state: EditorState.create({ doc: value, extensions: [
|
|
@@ -63,6 +68,7 @@ export function createTypeScriptEditor(React) {
|
|
|
63
68
|
}),
|
|
64
69
|
] }),
|
|
65
70
|
})
|
|
71
|
+
adoptUnownedStyles(unowned)
|
|
66
72
|
view.current = editor
|
|
67
73
|
return () => { view.current = null; editor.destroy() }
|
|
68
74
|
}, [documentId])
|