dsh-ptc-plus 0.4.8 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +61 -123
- package/README.md +61 -123
- package/assets/dsh-ptc-plus-banner-en.webp +0 -0
- package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
- package/assets/ptc-plus-repl-workspace-en.png +0 -0
- package/assets/ptc-plus-repl-workspace-zh.png +0 -0
- package/assets/ptc-plus-settings-en.png +0 -0
- package/assets/ptc-plus-settings-zh.png +0 -0
- package/client.cjs +42336 -0
- package/cordis.patch.yml +13 -2
- package/docs/README.md +7 -2
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +31 -179
- package/docs/adr/0008-define-agent-operating-contract.md +5 -1
- package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -2
- package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +2 -2
- 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/0022-stage-npm-releases-with-oidc.md +4 -2
- package/docs/adr/0023-global-user-bindings.md +3 -1
- package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +2 -0
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +101 -35
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +3 -3
- package/docs/adr/0031-close-semantic-obligations-by-owner.md +1 -1
- 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/adr/0034-keep-program-values-separate-from-output-projections.md +29 -0
- package/docs/adr/0035-run-coverage-only-before-a-new-release.md +27 -0
- package/docs/adr/0036-separate-focused-development-from-delivery-verification.md +29 -0
- package/docs/adr/0037-keep-tool-hover-previews-passive.md +52 -0
- package/docs/adr/0038-freeze-session-capability-plans.md +71 -0
- package/docs/adr/0039-verify-documentation-without-runtime-suites.md +29 -0
- package/docs/adr/0040-separate-capability-configurations-from-session-observations.md +39 -0
- package/docs/adr/0041-record-cell-execution-directories.md +65 -0
- package/docs/adr/0042-persist-provider-availability-and-handback.md +41 -0
- package/docs/adr/0043-compose-capability-and-context-policies.md +37 -0
- package/docs/adr/README.md +6 -0
- package/docs/architecture.md +25 -27
- package/docs/binding-review-placement-design.md +29 -21
- package/docs/capability-planning-components.md +93 -0
- package/docs/capability-projection.md +7 -0
- package/docs/client-ui.md +88 -29
- package/docs/client-visual-contract.md +65 -0
- package/docs/context-management.md +22 -0
- package/docs/context-optimization-plan.md +353 -0
- package/docs/context-public-interface-coverage.md +25 -0
- package/docs/durability-design.md +31 -14
- package/docs/evaluation.md +41 -6
- package/docs/evaluations/command-goat-deepseek-v4.1-flash-20261006.md +126 -0
- package/docs/evaluations/command-goat-deepseek-v4.1-flash.md +281 -0
- package/docs/installation.md +50 -8
- package/docs/program-data-plane.md +5 -3
- package/docs/publishing.md +5 -5
- package/docs/release-coverage.md +73 -0
- package/docs/release-notes.md +1 -1
- package/docs/review-procedure.md +56 -0
- package/docs/runtime-reference.md +48 -16
- package/docs/semantic-validation.md +14 -2
- package/docs/stateful-binding-updates-proposal.md +2 -2
- package/docs/user-bindings.en.md +2 -2
- package/docs/user-bindings.md +3 -3
- package/docs/value-wire.md +11 -1
- package/docs/verification-optimization-work.md +52 -15
- package/docs/verification.md +30 -86
- package/index.js +147 -23
- package/internal/binding-toggle-receipt.js +24 -0
- package/internal/capability-command-receipt.js +29 -0
- package/internal/capability-environment.js +1247 -0
- package/internal/capability-plan.js +285 -0
- package/internal/capability-policy.js +96 -0
- package/internal/capability-statistics-store.js +180 -0
- package/internal/capability-statistics.js +210 -0
- package/internal/compiler-platform.js +5 -3
- package/internal/compiler-service.js +2 -9
- package/internal/config-spec.js +79 -9
- package/internal/cordis-tools-owner.js +22 -19
- package/internal/direct-surface-owner.js +29 -29
- package/internal/dynamic-environment-runtime.js +9 -9
- package/internal/edit-transport-owner.js +183 -139
- package/internal/execution-provider-owner.js +160 -0
- package/internal/execution-seam-compat.js +76 -120
- package/internal/isolated-worker.js +12 -8
- package/internal/kernel-worker.js +104 -32
- package/internal/native-root-dynamic.js +12 -13
- package/internal/program-bindings.js +190 -0
- package/internal/recovery-tips.js +53 -23
- package/internal/rejected-cell-editor.js +8 -8
- package/internal/repl-observation-interest.js +11 -3
- package/internal/rpc-contract.js +1 -0
- package/internal/runtime-bridge-owner.js +351 -82
- package/internal/runtime-config.js +58 -0
- package/internal/runtime-contexts.js +85 -16
- package/internal/runtime-intrinsics.js +1 -0
- package/internal/runtime-messages.js +41 -0
- package/internal/sdk-projection.js +10 -82
- package/internal/session-cell-executor.js +42 -19
- package/internal/session-events.js +74 -4
- package/internal/session-journal-recovery.js +904 -184
- package/internal/session-journal-schema.js +6 -3
- package/internal/session-journal.js +31 -2
- package/internal/session-log-view.js +189 -43
- package/internal/session-runtime.js +175 -23
- package/internal/settings-compat.js +23 -6
- package/internal/stateful-module-compiler.js +3 -2
- package/internal/stateful-module-runtime.js +7 -3
- package/internal/stateful-root-runtime.js +9 -4
- package/internal/user-binding-authoring.js +2 -7
- package/internal/user-bindings-owner.js +47 -7
- package/internal/validated-parse-repair.js +1 -1
- package/internal/value-wire.js +68 -21
- package/internal/worker-cwd-virtualization.js +20 -18
- package/internal/worker-realm-surfaces.js +3 -3
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +55 -38
- package/src/client-authoring-view.js +286 -168
- package/src/client-binding-review.js +4 -1
- package/src/client-binding-toggle.js +18 -0
- package/src/client-capabilities-view.js +1341 -0
- package/src/client-capability-sort.js +29 -0
- package/src/client-code-editor.js +6 -0
- package/src/client-copy.js +881 -8
- package/src/client-feature-gates.js +15 -5
- package/src/client-host-compat.js +23 -2
- package/src/client-indicator-view.js +4 -6
- package/src/client-primitives.js +280 -0
- package/src/client-repl-view.js +23 -16
- package/src/client-settings-view.js +412 -69
- package/src/client-styles.js +193 -41
- package/src/client-tool-hover.js +185 -0
- package/src/client-tool-view.js +45 -11
- package/src/client-transport.js +44 -9
- package/src/client-workbench.js +9 -10
- package/src/client.js +148 -68
- 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
- package/client.js +0 -31857
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { bindingDraftProjection, bindingReviewStatus } from './client-binding-review.js'
|
|
2
2
|
import { bindingModelPreferences } from '../internal/user-binding-model-context.js'
|
|
3
|
+
import { capabilityCommandText } from './client-capabilities-view.js'
|
|
3
4
|
import { featureEnabled } from './client-feature-gates.js'
|
|
4
5
|
import {
|
|
5
6
|
MENU_CHILDREN_PROBE_SENTINEL,
|
|
7
|
+
isHostComponent,
|
|
6
8
|
isHostIconComponent,
|
|
7
9
|
readMenuChildrenProbe,
|
|
8
10
|
} from './client-host-compat.js'
|
|
@@ -22,9 +24,9 @@ const HOVER_DWELL_MS = 150
|
|
|
22
24
|
*/
|
|
23
25
|
export function createAuthoringView(React, deps) {
|
|
24
26
|
const {
|
|
25
|
-
ActionButton, IconButton, Menu, Toast, Tooltip, CodeBlock, BindingsDialog, PTCPlusSettingsDialog,
|
|
27
|
+
ActionButton, IconButton, Menu, MenuFallback, Toast, Tooltip, CodeBlock, Modal, createPortal, BindingsDialog, PTCPlusSettingsDialog,
|
|
26
28
|
useWorkbenchController, useBindingReview, catalogOwner, callUserBindings, subscribeReset,
|
|
27
|
-
settingsCardSeat, updateSetting, menuChildren,
|
|
29
|
+
settingsCardSeat, updateSetting, updateCapabilityConfiguration, menuChildren, SessionCapabilityControls,
|
|
28
30
|
icons: {
|
|
29
31
|
sparkle: IconSparkle16, chevron: IconChevronDownOutline14,
|
|
30
32
|
close: IconCloseOutline16, check: IconCheckOutline14,
|
|
@@ -32,6 +34,24 @@ export function createAuthoringView(React, deps) {
|
|
|
32
34
|
} = deps
|
|
33
35
|
const h = React.createElement
|
|
34
36
|
|
|
37
|
+
function EnlargeIcon() {
|
|
38
|
+
return h('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true },
|
|
39
|
+
h('path', { d: 'M6 2H2v4m8-4h4v4M2 10v4h4m8-4v4h-4', stroke: 'currentColor', strokeWidth: 1.5,
|
|
40
|
+
strokeLinecap: 'round', strokeLinejoin: 'round' }))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function RestoreIcon() {
|
|
44
|
+
return h('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true },
|
|
45
|
+
h('path', { d: 'M2 6h4V2m4 0v4h4M6 14v-4H2m12 0h-4v4', stroke: 'currentColor', strokeWidth: 1.5,
|
|
46
|
+
strokeLinecap: 'round', strokeLinejoin: 'round' }))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ReviewCloseIcon() {
|
|
50
|
+
return h('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true },
|
|
51
|
+
h('path', { d: 'm4 4 8 8m0-8-8 8', stroke: 'currentColor', strokeWidth: 1.5,
|
|
52
|
+
strokeLinecap: 'round' }))
|
|
53
|
+
}
|
|
54
|
+
|
|
35
55
|
/**
|
|
36
56
|
* Where the settings card lives on the installed generation.
|
|
37
57
|
*
|
|
@@ -45,10 +65,11 @@ export function createAuthoringView(React, deps) {
|
|
|
45
65
|
}[settingsCardSeat?.()] ?? 'settings.menuEntry')
|
|
46
66
|
|
|
47
67
|
function BindingAuthorButton({
|
|
48
|
-
sessionId, t, useInput, inputActions, usePtcSettings, useBindingCommand,
|
|
68
|
+
sessionId, t, useInput, inputActions, usePtcSettings, useBindingCommand, useCapabilityCommand,
|
|
49
69
|
}) {
|
|
50
70
|
const input = useInput?.(snapshot => snapshot)
|
|
51
71
|
const available = useBindingCommand(snapshot => snapshot)
|
|
72
|
+
const capabilityAvailable = useCapabilityCommand?.(snapshot => snapshot) === true
|
|
52
73
|
const settings = usePtcSettings(snapshot => snapshot)
|
|
53
74
|
const [review, view] = useBindingReview(sessionId)
|
|
54
75
|
const anchorRef = React.useRef(null)
|
|
@@ -56,20 +77,21 @@ export function createAuthoringView(React, deps) {
|
|
|
56
77
|
anchorRef.current = element
|
|
57
78
|
review.access = element
|
|
58
79
|
}, [review])
|
|
59
|
-
const
|
|
60
|
-
const manageItemRef = React.useRef(null)
|
|
80
|
+
const menuBodyRef = React.useRef(null)
|
|
61
81
|
const reloadItemRef = React.useRef(null)
|
|
62
|
-
const
|
|
82
|
+
const disabledGroupRef = React.useRef(null)
|
|
83
|
+
const [disabledOpen, setDisabledOpen] = React.useState(false)
|
|
63
84
|
const focused = React.useRef(false)
|
|
64
85
|
const dialogReturnFocus = React.useRef(null)
|
|
65
86
|
const hoverTimer = React.useRef(undefined)
|
|
66
87
|
const [menu, setMenu] = React.useState(null)
|
|
67
88
|
const [managing, setManaging] = React.useState(false)
|
|
89
|
+
const [capabilitiesOpen, setCapabilitiesOpen] = React.useState(false)
|
|
68
90
|
const [settingsOpen, setSettingsOpen] = React.useState(false)
|
|
69
91
|
// The composer entry's four actions live in the host Menu's `children`
|
|
70
92
|
// region. Preceding Clients accept that prop and mount nothing, so the entry
|
|
71
|
-
// probes the installed primitive once and
|
|
72
|
-
//
|
|
93
|
+
// probes the installed primitive once and uses its own Menu fallback when
|
|
94
|
+
// that public region is absent.
|
|
73
95
|
const probeRef = React.useRef(null)
|
|
74
96
|
const [probing, setProbing] = React.useState(() => menuChildren.supported() === undefined)
|
|
75
97
|
const menuChildrenRegion = menuChildren.supported() !== false
|
|
@@ -86,16 +108,25 @@ export function createAuthoringView(React, deps) {
|
|
|
86
108
|
const catalogFocus = React.useRef(null)
|
|
87
109
|
const toastSequence = React.useRef(0)
|
|
88
110
|
const [toast, setToast] = React.useState(null)
|
|
89
|
-
const hasDraft = view.mounted && view.candidate !== null && view.action === null
|
|
111
|
+
const hasDraft = featureEnabled(settings, 'bindings') && view.mounted && view.candidate !== null && view.action === null
|
|
90
112
|
const quickAccess = featureEnabled(settings, 'authorButton')
|
|
91
|
-
const
|
|
92
|
-
const
|
|
113
|
+
const pluginEnabled = featureEnabled(settings, 'plugin') && (hasDraft || featureEnabled(settings, 'ptcMenu'))
|
|
114
|
+
const canPrefill = typeof useInput === 'function' && typeof inputActions?.setDraft === 'function'
|
|
115
|
+
const canAuthor = quickAccess && available && canPrefill
|
|
116
|
+
const canRefresh = pluginEnabled && settings.status === 'ready' && settings.writable === true
|
|
117
|
+
&& capabilityAvailable && canPrefill
|
|
118
|
+
const menuOpen = pluginEnabled && view.reachable && !managing && !capabilitiesOpen && !settingsOpen
|
|
93
119
|
&& menu !== null && menu.key === view.candidateKey
|
|
94
120
|
// The dialog owns the management session; losing the input surface only
|
|
95
121
|
// suspends reads, so a draft survives a temporary Host takeover.
|
|
96
122
|
const workbench = useWorkbenchController({
|
|
97
123
|
enabled: quickAccess && managing, active: view.reachable, callUserBindings,
|
|
98
124
|
})
|
|
125
|
+
React.useEffect(() => {
|
|
126
|
+
if (!quickAccess) {
|
|
127
|
+
setManaging(false)
|
|
128
|
+
}
|
|
129
|
+
}, [quickAccess])
|
|
99
130
|
const refreshCatalog = React.useCallback(async (reload = false) => {
|
|
100
131
|
if (reload) captureCatalogFocus()
|
|
101
132
|
await catalogSource.read({ reload })
|
|
@@ -105,6 +136,7 @@ export function createAuthoringView(React, deps) {
|
|
|
105
136
|
catalogSource.reset()
|
|
106
137
|
setMenu(null)
|
|
107
138
|
setManaging(false)
|
|
139
|
+
setCapabilitiesOpen(false)
|
|
108
140
|
setSettingsOpen(false)
|
|
109
141
|
}
|
|
110
142
|
const unsubscribe = subscribeReset(reset)
|
|
@@ -116,9 +148,14 @@ export function createAuthoringView(React, deps) {
|
|
|
116
148
|
const toggleBinding = async entry => {
|
|
117
149
|
if (!catalogReady || !quickAccess) return
|
|
118
150
|
captureCatalogFocus()
|
|
119
|
-
await catalogSource.write(() => callUserBindings(entry.enabled ? 'disable' : 'enable', {
|
|
151
|
+
const next = await catalogSource.write(() => callUserBindings(entry.enabled ? 'disable' : 'enable', {
|
|
120
152
|
id: entry.id, expectedRevision: catalog.revision,
|
|
121
153
|
}))
|
|
154
|
+
const saved = next?.entries.find(item => item.id === entry.id)
|
|
155
|
+
if (saved) {
|
|
156
|
+
toastSequence.current++
|
|
157
|
+
setToast({ sequence: toastSequence.current, key: saved.enabled ? 'bindings.toggleEnabled' : 'bindings.toggleDisabled', params: { name: saved.name } })
|
|
158
|
+
}
|
|
122
159
|
}
|
|
123
160
|
const cancelHoverOpen = React.useCallback(() => {
|
|
124
161
|
if (hoverTimer.current === undefined) return
|
|
@@ -128,7 +165,10 @@ export function createAuthoringView(React, deps) {
|
|
|
128
165
|
React.useEffect(() => cancelHoverOpen, [cancelHoverOpen])
|
|
129
166
|
const showMenu = mode => {
|
|
130
167
|
cancelHoverOpen()
|
|
131
|
-
if (!menuOpen
|
|
168
|
+
if (!menuOpen) {
|
|
169
|
+
setDisabledOpen(false)
|
|
170
|
+
if (quickAccess) void refreshCatalog()
|
|
171
|
+
}
|
|
132
172
|
setMenu({ key: view.candidateKey, mode, step: 'root' })
|
|
133
173
|
}
|
|
134
174
|
// Touch has no hover: its tap reaches the same menu through the click path.
|
|
@@ -140,11 +180,24 @@ export function createAuthoringView(React, deps) {
|
|
|
140
180
|
showMenu('hover')
|
|
141
181
|
}, HOVER_DWELL_MS)
|
|
142
182
|
}
|
|
143
|
-
const menuElement = () =>
|
|
144
|
-
?? firstItemRef.current?.closest('[role=menu]')
|
|
183
|
+
const menuElement = () => menuBodyRef.current
|
|
145
184
|
const captureCatalogFocus = () => {
|
|
146
185
|
catalogFocus.current = menuElement()?.contains(document.activeElement) ? document.activeElement : null
|
|
147
186
|
}
|
|
187
|
+
const focusMenuItem = next => {
|
|
188
|
+
next?.focus({ preventScroll: true })
|
|
189
|
+
const scroll = menuElement()?.querySelector('.ptcPlusBindingMenuScroll')
|
|
190
|
+
if (!scroll?.contains(next)) return
|
|
191
|
+
const viewport = scroll.getBoundingClientRect()
|
|
192
|
+
const item = next.getBoundingClientRect()
|
|
193
|
+
// Reveal only inside the owned catalog. Browser focus scrolling can also
|
|
194
|
+
// scroll the outer menu or transcript and move the fixed actions.
|
|
195
|
+
const offset = item.height > scroll.clientHeight
|
|
196
|
+
? item.top + item.height / 2 - (viewport.top + viewport.height / 2)
|
|
197
|
+
: item.top < viewport.top ? item.top - viewport.top
|
|
198
|
+
: item.bottom > viewport.bottom ? item.bottom - viewport.bottom : 0
|
|
199
|
+
scroll.scrollTop += offset
|
|
200
|
+
}
|
|
148
201
|
// The list must sit next to the entry that opened it. The published Menu reads
|
|
149
202
|
// this rect on open and on every scroll/resize, and re-runs placement whenever
|
|
150
203
|
// the prop identity changes, so measuring the trigger each render also keeps an
|
|
@@ -190,19 +243,22 @@ export function createAuthoringView(React, deps) {
|
|
|
190
243
|
if (!menuOpen) { catalogFocus.current = null; return }
|
|
191
244
|
if (catalogStatus === 'writing' || catalogStatus === 'loading') return
|
|
192
245
|
const target = catalogFocus.current
|
|
193
|
-
|
|
194
|
-
if (!target || document.activeElement !== document.body) return
|
|
246
|
+
if (!target || (document.activeElement !== document.body && document.activeElement !== target)) return
|
|
195
247
|
const root = menuElement()
|
|
248
|
+
const bindingId = target.dataset.bindingId
|
|
249
|
+
const currentRow = bindingId === undefined ? null
|
|
250
|
+
: [...(root?.querySelectorAll('[data-binding-id]') ?? [])].find(row => row.dataset.bindingId === bindingId)
|
|
196
251
|
const next = root?.contains(target) && !target.disabled ? target
|
|
197
|
-
:
|
|
198
|
-
|
|
252
|
+
: currentRow ?? (bindingId !== undefined && catalog?.entries.some(entry => entry.id === bindingId && !entry.enabled)
|
|
253
|
+
? disabledGroupRef.current : null) ?? reloadItemRef.current ?? root?.querySelector('button:not(:disabled)')
|
|
254
|
+
focusMenuItem(next)
|
|
199
255
|
}, [catalogStatus, menuOpen])
|
|
200
256
|
React.useLayoutEffect(() => {
|
|
201
257
|
if (!menuOpen || menu.mode === 'hover') return
|
|
202
258
|
// The public Menu commits its measured position before keyboard focus enters it.
|
|
203
259
|
let cancelled = false
|
|
204
260
|
queueMicrotask(() => {
|
|
205
|
-
if (!cancelled && anchorRef.current?.getClientRects().length) menuElement()?.querySelector('button:not(:disabled)')
|
|
261
|
+
if (!cancelled && anchorRef.current?.getClientRects().length) focusMenuItem(menuElement()?.querySelector('button:not(:disabled)'))
|
|
206
262
|
})
|
|
207
263
|
return () => { cancelled = true }
|
|
208
264
|
}, [menuOpen, menu?.mode, menu?.step])
|
|
@@ -217,18 +273,17 @@ export function createAuthoringView(React, deps) {
|
|
|
217
273
|
setProbing(false)
|
|
218
274
|
}, [probing, menuChildren])
|
|
219
275
|
React.useEffect(() => {
|
|
220
|
-
if (toast === null ||
|
|
276
|
+
if (toast === null || isHostComponent(Toast)) return undefined
|
|
221
277
|
const timer = setTimeout(() => setToast(null), 2_500)
|
|
222
278
|
return () => clearTimeout(timer)
|
|
223
279
|
}, [toast])
|
|
224
280
|
const label = hasDraft ? t('bindings.reviewMenuLabel', { count: 1 })
|
|
225
|
-
+ (view.message ? ` · ${t('bindings.reviewAttention')}` : '') : t('bindings.open')
|
|
226
|
-
const hint = t(hasDraft ? 'bindings.draftHint' : 'bindings.openHint')
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
const prefillAuthoring = value => {
|
|
281
|
+
+ (view.message ? ` · ${t('bindings.reviewAttention')}` : '') : t(quickAccess ? 'bindings.open' : 'settings.toolsMenu')
|
|
282
|
+
const hint = t(hasDraft ? 'bindings.draftHint' : quickAccess ? 'bindings.openHint' : 'settings.toolsMenu')
|
|
283
|
+
// Command shortcuts share the composer guard; submission remains the user's action.
|
|
284
|
+
const prefillCommand = (value, allowed) => {
|
|
230
285
|
hideMenu()
|
|
231
|
-
if (!
|
|
286
|
+
if (!allowed) return
|
|
232
287
|
if (typeof input?.draft === 'string' && input.draft.trim() !== '') {
|
|
233
288
|
toastSequence.current += 1
|
|
234
289
|
setToast({ sequence: toastSequence.current, text: t('bindings.composerBusy') })
|
|
@@ -236,12 +291,12 @@ export function createAuthoringView(React, deps) {
|
|
|
236
291
|
}
|
|
237
292
|
inputActions.setDraft(value)
|
|
238
293
|
}
|
|
239
|
-
const openAuthoring = () =>
|
|
240
|
-
const openAuthoringEdit = entry =>
|
|
294
|
+
const openAuthoring = () => prefillCommand('/binding new ', canAuthor)
|
|
295
|
+
const openAuthoringEdit = entry => prefillCommand(`/binding edit ${entry.id} `, canAuthor)
|
|
241
296
|
const starButton = h('button', {
|
|
242
297
|
type: 'button', className: 'ptcPlusAuthorButton', 'aria-label': label,
|
|
243
298
|
// Older UI-kit lines ship no Tooltip primitive; the native title carries the hint there.
|
|
244
|
-
title:
|
|
299
|
+
title: isHostComponent(Tooltip) || menuOpen || managing || settingsOpen ? undefined : hint,
|
|
245
300
|
'aria-haspopup': 'menu', 'aria-expanded': menuOpen,
|
|
246
301
|
onPointerEnter: hoverMenu,
|
|
247
302
|
onPointerLeave: cancelHoverOpen,
|
|
@@ -261,15 +316,30 @@ export function createAuthoringView(React, deps) {
|
|
|
261
316
|
// and drops a duplicate, while disabling rows would dim every row and add a
|
|
262
317
|
// pending row for the duration of the request: both read as the popover
|
|
263
318
|
// flickering when the user toggles an entry.
|
|
319
|
+
const catalogRow = entry => ({ id: `global:${entry.id}`, bindingId: entry.id,
|
|
320
|
+
disabled: catalog === null,
|
|
321
|
+
label: h('span', { className: 'ptcPlusBindingQuickRow', 'data-enabled': entry.enabled },
|
|
322
|
+
h('span', { className: 'ptcPlusBindingQuickName' }, h('strong', { title: entry.name }, entry.name),
|
|
323
|
+
h('span', { className: 'ptcPlusBindingQuickState' }, t(entry.enabled ? 'bindings.enabled' : 'bindings.disabledEntry'))),
|
|
324
|
+
h('span', { className: 'ptcPlusBindingQuickPurpose', title: entry.purpose }, entry.purpose)),
|
|
325
|
+
})
|
|
326
|
+
const enabledEntries = (catalog?.entries ?? []).filter(entry => entry.enabled)
|
|
327
|
+
const disabledEntries = (catalog?.entries ?? []).filter(entry => !entry.enabled)
|
|
264
328
|
const catalogItems = !quickAccess ? [] : [
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
329
|
+
...(catalog === null || catalog.entries.length === 0 ? [] : [
|
|
330
|
+
{ id: 'enabled-heading', type: 'label', text: t('bindings.quickEnabled', { count: enabledEntries.length }) },
|
|
331
|
+
...enabledEntries.map(catalogRow),
|
|
332
|
+
...(enabledEntries.length === 0 ? [{ id: 'no-enabled', type: 'label', text: t('bindings.quickNoEnabled') }] : []),
|
|
333
|
+
...(disabledEntries.length === 0 ? [] : [
|
|
334
|
+
{ id: 'disabled-group', label: h(React.Fragment, null,
|
|
335
|
+
h('svg', { className: 'ptcPlusMenuGroupChevron', 'aria-hidden': true,
|
|
336
|
+
width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none', 'data-expanded': disabledOpen },
|
|
337
|
+
h('path', { d: 'm5 3 4 4-4 4', stroke: 'currentColor', strokeWidth: 1.5,
|
|
338
|
+
strokeLinecap: 'round', strokeLinejoin: 'round' })),
|
|
339
|
+
h('span', null, t('bindings.quickDisabled', { count: disabledEntries.length }))) },
|
|
340
|
+
...(disabledOpen ? disabledEntries.map(catalogRow) : []),
|
|
341
|
+
]),
|
|
342
|
+
]),
|
|
273
343
|
...(catalog !== null && catalog.entries.length === 0
|
|
274
344
|
? [{ id: 'empty', type: 'label', text: t('memory.globalEmpty') }] : []),
|
|
275
345
|
...(catalog === null && catalogStatus !== 'error'
|
|
@@ -281,20 +351,11 @@ export function createAuthoringView(React, deps) {
|
|
|
281
351
|
// submenu here would lose the bounded, scrollable catalog this step needs.
|
|
282
352
|
const editItems = [
|
|
283
353
|
{ id: 'edit-heading', type: 'label', text: t('bindings.quickEditHeading') },
|
|
284
|
-
...(catalog?.entries ?? []).map(entry => ({
|
|
285
|
-
id: `edit:${entry.id}`,
|
|
286
|
-
disabled: catalog === null,
|
|
287
|
-
label: h('span', { className: 'ptcPlusBindingQuickRow' },
|
|
288
|
-
h('span', { className: 'ptcPlusBindingQuickName' }, h('strong', { title: entry.name }, entry.name),
|
|
289
|
-
h('span', { className: 'ptcPlusBindingQuickState' }, t(entry.enabled ? 'bindings.enabled' : 'bindings.disabledEntry'))),
|
|
290
|
-
h('span', { className: 'ptcPlusBindingQuickPurpose', title: entry.purpose }, entry.purpose)),
|
|
291
|
-
})),
|
|
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')) },
|
|
354
|
+
...(catalog?.entries ?? []).map(entry => ({ ...catalogRow(entry), id: `edit:${entry.id}` })),
|
|
295
355
|
]
|
|
296
356
|
const selectMenuItem = id => {
|
|
297
357
|
if (!anchorRef.current?.getClientRects().length) return
|
|
358
|
+
if (id === 'disabled-group') { setDisabledOpen(open => !open); return }
|
|
298
359
|
if (id === 'edit') {
|
|
299
360
|
setMenu(current => current === null ? current : { ...current, step: 'edit' })
|
|
300
361
|
return
|
|
@@ -319,6 +380,8 @@ export function createAuthoringView(React, deps) {
|
|
|
319
380
|
setSettingsOpen(true)
|
|
320
381
|
return
|
|
321
382
|
}
|
|
383
|
+
if (id === 'capabilities') { hideMenu(); setCapabilitiesOpen(true); return }
|
|
384
|
+
if (id === 'refresh') { prefillCommand('/capabilities refresh', canRefresh); return }
|
|
322
385
|
hideMenu()
|
|
323
386
|
if (id === 'new') openAuthoring()
|
|
324
387
|
else if (id === 'manage') {
|
|
@@ -329,14 +392,29 @@ export function createAuthoringView(React, deps) {
|
|
|
329
392
|
}
|
|
330
393
|
// One copy of each action label serves the grid the plugin renders itself,
|
|
331
394
|
// in whichever carrier the installed Menu publishes for it.
|
|
332
|
-
const
|
|
333
|
-
|
|
395
|
+
const actionPaths = {
|
|
396
|
+
new: 'M8 3v10M3 8h10',
|
|
397
|
+
edit: 'm10 3 3 3M3 13l3-.7 7-7a1.4 1.4 0 0 0-3-3l-7 7L3 13Z',
|
|
398
|
+
refresh: 'M13 6a5 5 0 0 0-9-2L2 6m0-3v3h3M3 10a5 5 0 0 0 9 2l2-2m0 3v-3h-3',
|
|
399
|
+
manage: 'M2 4h5l1.5 2H14v7H2V4Z',
|
|
400
|
+
capabilities: 'M3 4h10M3 8h10M3 12h10M6 2v4M10 6v4M6 10v4',
|
|
401
|
+
settings: 'M6 2h4l.5 2 1.5 1.5 2 .5v4l-2 .5-1.5 1.5-.5 2H6l-.5-2L4 10.5 2 10V6l2-.5L5.5 4 6 2ZM10 8a2 2 0 1 0-4 0 2 2 0 0 0 4 0Z',
|
|
402
|
+
'edit-back': 'm7 3-5 5 5 5M2 8h12',
|
|
403
|
+
}
|
|
404
|
+
const actionCopy = (id, copy) => h(React.Fragment, null,
|
|
405
|
+
h('svg', { className: 'ptcPlusMenuActionIcon', width: 16, height: 16, viewBox: '0 0 16 16',
|
|
406
|
+
fill: 'none', 'aria-hidden': true }, h('path', { d: actionPaths[id], stroke: 'currentColor',
|
|
407
|
+
strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round' })), h('span', {
|
|
408
|
+
className: 'ptcPlusBindingMenuAction',
|
|
334
409
|
title: id === 'settings' ? settingsPath(t) : undefined,
|
|
335
|
-
}, t(copy))
|
|
336
|
-
const actionButton = (id, copy
|
|
410
|
+
}, t(copy)))
|
|
411
|
+
const actionButton = (id, copy) => h('button', {
|
|
337
412
|
type: 'button', role: 'menuitem', className: 'ptcPlusMenuButton',
|
|
413
|
+
disabled: id === 'refresh' && !canRefresh,
|
|
414
|
+
title: id === 'refresh' && !capabilityAvailable ? t('capabilities.refreshUnavailable')
|
|
415
|
+
: id === 'settings' ? settingsPath(t) : t(copy),
|
|
338
416
|
onClick: () => selectMenuItem(id),
|
|
339
|
-
}, actionCopy(id, copy
|
|
417
|
+
}, actionCopy(id, copy))
|
|
340
418
|
const actionSections = [
|
|
341
419
|
canAuthor ? h('div', { key: 'authoring', className: 'ptcPlusMenuAuthoring', role: 'group', 'aria-label': t('bindings.authorGroup') },
|
|
342
420
|
h('div', { className: 'ptcPlusMenuGroupLabel' }, t('bindings.authorGroup')),
|
|
@@ -344,49 +422,61 @@ export function createAuthoringView(React, deps) {
|
|
|
344
422
|
actionButton('new', 'bindings.authorNewDraft'),
|
|
345
423
|
(catalog?.entries?.length ?? 0) > 0 ? actionButton('edit', 'bindings.authorEdit') : null)) : null,
|
|
346
424
|
h('div', { key: 'utilities', className: 'ptcPlusMenuUtilities' },
|
|
347
|
-
|
|
348
|
-
actionButton('
|
|
425
|
+
actionButton('refresh', 'capabilities.applySession'),
|
|
426
|
+
quickAccess ? actionButton('manage', 'bindings.manage') : null,
|
|
427
|
+
SessionCapabilityControls ? actionButton('capabilities', 'capabilities.quick.title') : null,
|
|
428
|
+
actionButton('settings', 'settings.menuEntry')),
|
|
429
|
+
]
|
|
430
|
+
const menuEntries = menu?.step === 'edit' ? editItems : [
|
|
431
|
+
...(hasDraft ? [{ id: 'draft-heading', type: 'label', text: t('bindings.quickDrafts') }, {
|
|
432
|
+
id: view.candidateKey,
|
|
433
|
+
label: h('span', { className: 'ptcPlusDraftMenuItem' },
|
|
434
|
+
h('strong', null, view.candidate.entry.name), h('span', null, t(bindingReviewStatus(view)))),
|
|
435
|
+
}, ...(quickAccess ? [{ id: 'draft-separator', type: 'separator' }] : [])] : []),
|
|
436
|
+
...catalogItems,
|
|
437
|
+
...(quickAccess && catalogError !== null ? [{ id: 'reload', label: t('bindings.reload') }] : []),
|
|
349
438
|
]
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
439
|
+
const menuContent = h('div', { className: 'ptcPlusBindingMenuContent', ref: menuBodyRef,
|
|
440
|
+
// A reopened menu can acquire foreground focus while an earlier write is
|
|
441
|
+
// pending. Recovery follows the live menu, not the initiating operation.
|
|
442
|
+
onFocusCapture: event => { catalogFocus.current = event.target },
|
|
443
|
+
onBlurCapture: event => {
|
|
444
|
+
if (!event.currentTarget.contains(event.relatedTarget)) catalogFocus.current = null
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
h('div', { className: 'ptcPlusBindingMenuScroll' }, ...menuEntries.map(entry => entry.type === 'separator'
|
|
448
|
+
? h('div', { key: entry.id, role: 'separator', className: 'ptcPlusOwnedMenuSeparator' })
|
|
449
|
+
: entry.type === 'label'
|
|
450
|
+
? h('div', { key: entry.id, className: 'ptcPlusMenuGroupLabel' }, entry.text)
|
|
451
|
+
: h('button', {
|
|
452
|
+
key: entry.id, type: 'button', role: 'menuitem',
|
|
453
|
+
className: entry.id === 'disabled-group' ? 'ptcPlusOwnedMenuRow ptcPlusMenuDisclosure' : 'ptcPlusOwnedMenuRow',
|
|
454
|
+
disabled: entry.disabled, 'data-binding-id': entry.bindingId,
|
|
455
|
+
ref: entry.id === 'reload' ? reloadItemRef : entry.id === 'disabled-group' ? disabledGroupRef : undefined,
|
|
456
|
+
'aria-expanded': entry.id === 'disabled-group' ? disabledOpen : undefined,
|
|
457
|
+
onClick: () => selectMenuItem(entry.id),
|
|
458
|
+
}, entry.label))),
|
|
459
|
+
h('div', { className: 'ptcPlusMenuActions' }, menu?.step === 'edit'
|
|
460
|
+
? h('div', { className: 'ptcPlusMenuUtilities' }, actionButton('edit-back', 'bindings.back')) : actionSections))
|
|
363
461
|
return h('span', {
|
|
364
462
|
className: 'ptcPlusComposerBindingAnchor', tabIndex: -1,
|
|
365
463
|
onFocusCapture: () => { focused.current = true }, onBlurCapture: () => { focused.current = false },
|
|
366
464
|
'data-text': isHostIconComponent(IconSparkle16) ? undefined : true,
|
|
367
465
|
ref: attachAnchor,
|
|
368
466
|
},
|
|
369
|
-
!
|
|
467
|
+
!pluginEnabled ? null : h(menuChildrenRegion ? Menu : MenuFallback, {
|
|
370
468
|
className: 'ptcPlusAuthorButtonShell', open: menuOpen,
|
|
371
|
-
anchor:
|
|
469
|
+
anchor: isHostComponent(Tooltip)
|
|
372
470
|
? h(Tooltip, { label: hint, delayMs: 700, disabled: menuOpen || managing || settingsOpen }, starButton) : starButton,
|
|
373
471
|
portal: true, side: 'top', dense: true,
|
|
472
|
+
listClassName: 'ptcPlusBindingMenu',
|
|
374
473
|
// A hovered menu leaves with the pointer once it leaves the trigger and the
|
|
375
474
|
// list for the published grace, which re-entering either cancels; click and
|
|
376
475
|
// keyboard menus stay until dismissed.
|
|
377
476
|
closeOnPointerLeave: menu !== null && menu.mode === 'hover',
|
|
378
477
|
getAnchorRect: menuAnchorRect,
|
|
379
478
|
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 }),
|
|
479
|
+
items: [], children: menuContent,
|
|
390
480
|
onSelect: selectMenuItem,
|
|
391
481
|
}),
|
|
392
482
|
// The probe mounts the host Menu's own `children` region inside a hidden
|
|
@@ -404,25 +494,33 @@ export function createAuthoringView(React, deps) {
|
|
|
404
494
|
controller: workbench, t, onClose: () => setManaging(false), returnFocusRef: dialogReturnFocus,
|
|
405
495
|
getReturnFocus: () => anchorRef.current?.querySelector('.ptcPlusAuthorButton'),
|
|
406
496
|
}) : null,
|
|
497
|
+
SessionCapabilityControls && capabilitiesOpen && view.reachable
|
|
498
|
+
? h(SessionCapabilityControls, { sessionId, open: true, t,
|
|
499
|
+
enabled: pluginEnabled && settings.status === 'ready' && settings.writable === true,
|
|
500
|
+
onClose: () => {
|
|
501
|
+
setCapabilitiesOpen(false)
|
|
502
|
+
queueMicrotask(() => anchorRef.current?.querySelector('.ptcPlusAuthorButton')
|
|
503
|
+
?.focus({ preventScroll: true }))
|
|
504
|
+
} }) : null,
|
|
407
505
|
settingsOpen && view.reachable
|
|
408
506
|
? h(PTCPlusSettingsDialog, {
|
|
409
|
-
t, usePtcSettings, updateSetting, callUserBindings,
|
|
507
|
+
t, usePtcSettings, updateSetting, updateCapabilityConfiguration, callUserBindings,
|
|
410
508
|
onClose: () => {
|
|
411
509
|
setSettingsOpen(false)
|
|
412
510
|
queueMicrotask(() => anchorRef.current?.querySelector('.ptcPlusAuthorButton')
|
|
413
511
|
?.focus({ preventScroll: true }))
|
|
414
512
|
},
|
|
415
513
|
}) : null,
|
|
416
|
-
toast === null ? null :
|
|
514
|
+
toast === null ? null : isHostComponent(Toast)
|
|
417
515
|
? h(Toast, {
|
|
418
516
|
key: toast.sequence,
|
|
419
|
-
text: toast.text,
|
|
517
|
+
text: toast.key ? t(toast.key, toast.params) : capabilityCommandText(t, toast.text),
|
|
420
518
|
anchor: anchorRef.current,
|
|
421
519
|
onDone: () => setToast(null),
|
|
422
520
|
})
|
|
423
521
|
: h('span', {
|
|
424
522
|
key: toast.sequence, className: 'ptcPlusComposerNotice', role: 'status',
|
|
425
|
-
}, toast.text))
|
|
523
|
+
}, toast.key ? t(toast.key, toast.params) : capabilityCommandText(t, toast.text)))
|
|
426
524
|
}
|
|
427
525
|
|
|
428
526
|
function BindingCandidateContent({ candidate, t, showName = true }) {
|
|
@@ -432,7 +530,7 @@ export function createAuthoringView(React, deps) {
|
|
|
432
530
|
showName ? h('strong', null, candidate.entry.name) : null,
|
|
433
531
|
h('span', { className: 'ptcPlusBindingMeta' }, `${candidate.entry.scope} - ${candidate.entry.symbols.join(', ')}`),
|
|
434
532
|
candidate.entry.purpose ? h('p', { className: 'ptcPlusMessage' }, candidate.entry.purpose) : null,
|
|
435
|
-
|
|
533
|
+
isHostComponent(CodeBlock)
|
|
436
534
|
? h(CodeBlock, { code: candidate.entry.source, lang: 'typescript', className: 'ptcPlusBindingCommandCode',
|
|
437
535
|
copyLabel: t('tool.copy'), copiedLabel: t('tool.copied') })
|
|
438
536
|
: h('pre', { className: 'ptcPlusBindingCommandSource' }, candidate.entry.source),
|
|
@@ -450,22 +548,20 @@ export function createAuthoringView(React, deps) {
|
|
|
450
548
|
preferences.instructions || t('bindings.noInstructions'))))))
|
|
451
549
|
}
|
|
452
550
|
|
|
453
|
-
// Native editable semantics are enough to restore focus; no Host class, store or editor internals are used.
|
|
454
551
|
function focusComposer(anchor) {
|
|
455
552
|
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
553
|
anchor.focus({ preventScroll: true })
|
|
462
554
|
}
|
|
463
555
|
|
|
464
|
-
function openBindingReview(review, candidate = review.getSnapshot().candidate
|
|
465
|
-
|
|
556
|
+
function openBindingReview(review, candidate = review.getSnapshot().candidate,
|
|
557
|
+
visibility = 'enlarged') {
|
|
558
|
+
if (!review.display(visibility, candidate)) return
|
|
466
559
|
requestAnimationFrame(() => {
|
|
467
|
-
|
|
468
|
-
|
|
560
|
+
const current = review.getSnapshot()
|
|
561
|
+
if (review.isCurrentCandidate(candidate) && current.reachable && current.visibility === 'expanded'
|
|
562
|
+
&& review.panel?.getClientRects().length) {
|
|
563
|
+
if (review.previewAvailable === false) focusBindingReviewAccess(review)
|
|
564
|
+
else review.panel.focus({ preventScroll: true })
|
|
469
565
|
}
|
|
470
566
|
})
|
|
471
567
|
}
|
|
@@ -477,60 +573,6 @@ export function createAuthoringView(React, deps) {
|
|
|
477
573
|
else focusComposer(anchor)
|
|
478
574
|
}
|
|
479
575
|
|
|
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
576
|
|
|
535
577
|
function BindingReviewDock({ sessionId, useProjection, t }) {
|
|
536
578
|
const raw = useProjection('ptcPlusBindingDraft')
|
|
@@ -549,10 +591,75 @@ export function createAuthoringView(React, deps) {
|
|
|
549
591
|
React.useLayoutEffect(() => { review.sync(projection) }, [review, projection])
|
|
550
592
|
const title = React.useId()
|
|
551
593
|
const content = React.useId()
|
|
552
|
-
const
|
|
594
|
+
const anchor = React.useRef(null)
|
|
595
|
+
const layer = React.useRef(null)
|
|
596
|
+
const [compact, setCompact] = React.useState(false)
|
|
597
|
+
const enlarged = view.visibility === 'enlarged'
|
|
598
|
+
const expanded = enlarged || view.visibility === 'expanded' && !compact
|
|
553
599
|
const candidateKey = view.candidateKey
|
|
554
|
-
React.useLayoutEffect(() =>
|
|
555
|
-
|
|
600
|
+
React.useLayoutEffect(() => {
|
|
601
|
+
if (enlarged || !view.reachable || view.visibility === 'hidden' || view.candidate === null) return undefined
|
|
602
|
+
let frame
|
|
603
|
+
let previous
|
|
604
|
+
// A dock can move when input or sibling cards grow without a scroll/resize.
|
|
605
|
+
// Track only our own anchor while visible; no Host node owns this placement.
|
|
606
|
+
const place = () => {
|
|
607
|
+
const rect = anchor.current?.getBoundingClientRect()
|
|
608
|
+
const surface = layer.current
|
|
609
|
+
if (rect && surface) {
|
|
610
|
+
const viewport = window.visualViewport
|
|
611
|
+
const leftEdge = (viewport?.offsetLeft ?? 0) + 8
|
|
612
|
+
const topEdge = (viewport?.offsetTop ?? 0) + 8
|
|
613
|
+
const rightEdge = leftEdge + (viewport?.width ?? window.innerWidth) - 16
|
|
614
|
+
const bottomEdge = topEdge + (viewport?.height ?? window.innerHeight) - 16
|
|
615
|
+
const width = Math.min(rect.width, rightEdge - leftEdge)
|
|
616
|
+
const left = Math.max(leftEdge, Math.min(rect.left, rightEdge - width))
|
|
617
|
+
if (surface.style.width !== `${width}px`) surface.style.width = `${width}px`
|
|
618
|
+
const panel = surface.querySelector('.ptcPlusBindingDock')
|
|
619
|
+
const head = panel?.querySelector('.ptcPlusBindingDockHead')
|
|
620
|
+
const actions = panel?.querySelector('.ptcPlusBindingDockActions')
|
|
621
|
+
const body = panel?.querySelector('.ptcPlusBindingDockBody')
|
|
622
|
+
const panelStyle = panel && getComputedStyle(panel)
|
|
623
|
+
const border = (parseFloat(panelStyle?.borderTopWidth) || 0) + (parseFloat(panelStyle?.borderBottomWidth) || 0)
|
|
624
|
+
const chrome = (head?.getBoundingClientRect().height ?? 0) + border
|
|
625
|
+
const bottom = Math.max(topEdge, Math.min(Math.max(topEdge + chrome, rect.top - 8), rect.top, bottomEdge))
|
|
626
|
+
const height = bottom - topEdge
|
|
627
|
+
const geometry = [width, left, bottom, height, chrome].join(',')
|
|
628
|
+
if (geometry !== previous) {
|
|
629
|
+
previous = geometry
|
|
630
|
+
surface.style.left = `${left}px`
|
|
631
|
+
surface.style.bottom = `${window.innerHeight - bottom}px`
|
|
632
|
+
surface.style.setProperty('--ptc-plus-review-space', `${height}px`)
|
|
633
|
+
}
|
|
634
|
+
if (rect.width > 0) {
|
|
635
|
+
const available = surface.getBoundingClientRect().height >= chrome && bottom <= rect.top
|
|
636
|
+
review.previewAvailable = available
|
|
637
|
+
if (surface.dataset.available !== String(available)) {
|
|
638
|
+
surface.dataset.available = String(available)
|
|
639
|
+
if (available) {
|
|
640
|
+
surface.removeAttribute('inert')
|
|
641
|
+
surface.removeAttribute('aria-hidden')
|
|
642
|
+
} else {
|
|
643
|
+
surface.setAttribute('inert', '')
|
|
644
|
+
surface.setAttribute('aria-hidden', 'true')
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
// CSS owns the viewport cap. Full chrome stays measurable in compact
|
|
649
|
+
// mode so hiding it cannot make the layout classification oscillate.
|
|
650
|
+
if (head && actions && body && rect.width > 0) {
|
|
651
|
+
const minimumBody = parseFloat(getComputedStyle(body).minBlockSize)
|
|
652
|
+
const required = chrome + actions.getBoundingClientRect().height + minimumBody
|
|
653
|
+
setCompact(surface.getBoundingClientRect().height < required)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
frame = requestAnimationFrame(place)
|
|
657
|
+
}
|
|
658
|
+
place()
|
|
659
|
+
return () => {
|
|
660
|
+
cancelAnimationFrame(frame)
|
|
661
|
+
}
|
|
662
|
+
}, [candidateKey, enlarged, view.reachable, view.visibility, view.candidate === null])
|
|
556
663
|
const close = () => {
|
|
557
664
|
review.display('hidden')
|
|
558
665
|
requestAnimationFrame(() => focusBindingReviewAccess(review))
|
|
@@ -562,44 +669,55 @@ export function createAuthoringView(React, deps) {
|
|
|
562
669
|
if (review.panel?.contains(document.activeElement)) review.panel.focus({ preventScroll: true })
|
|
563
670
|
void review.act(operation, activate)
|
|
564
671
|
}
|
|
672
|
+
const restorePreview = () => openBindingReview(review, view.candidate, 'expanded')
|
|
565
673
|
if (view.candidate === null || view.visibility === 'hidden') return null
|
|
566
|
-
|
|
567
|
-
h('section', { key: candidateKey, className: 'ptcPlusBindingDock', 'aria-labelledby': title, tabIndex: -1,
|
|
674
|
+
const panel = h('section', { key: candidateKey, className: 'ptcPlusBindingDock', 'aria-labelledby': title, tabIndex: -1,
|
|
568
675
|
ref: element => { review.panel = element },
|
|
569
676
|
onFocusCapture: () => { focused.current = true },
|
|
570
677
|
onBlurCapture: event => {
|
|
571
678
|
// Blink blurs the removed panel before layout effects; completion still owns that lost focus.
|
|
572
679
|
if (event.relatedTarget || review.getSnapshot().action === null) focused.current = false
|
|
573
680
|
},
|
|
574
|
-
'aria-busy': view.busy },
|
|
681
|
+
'aria-busy': view.busy, 'data-compact': !enlarged && compact },
|
|
575
682
|
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') },
|
|
683
|
+
h(enlarged ? 'div' : 'button', { type: enlarged ? undefined : 'button', className: 'ptcPlusBindingDockToggle',
|
|
684
|
+
'aria-label': enlarged ? undefined : t(expanded ? 'bindings.reviewCollapse' : 'bindings.reviewExpand'),
|
|
685
|
+
'aria-expanded': enlarged ? undefined : expanded, 'aria-controls': expanded && !enlarged ? content : undefined,
|
|
686
|
+
onClick: enlarged ? undefined : () => review.display(compact ? 'enlarged' : expanded ? 'collapsed' : 'expanded') },
|
|
580
687
|
h('span', { className: 'ptcPlusBindingDockSymbol', 'aria-hidden': true }, '</>'),
|
|
581
688
|
h('span', { className: 'ptcPlusBindingDockHeading' },
|
|
582
689
|
h('strong', { id: title, title: view.candidate.entry.name }, view.candidate.entry.name),
|
|
583
690
|
h('span', { role: 'status', title: t(bindingReviewStatus(view)) },
|
|
584
691
|
`${t('bindings.reviewTitle')} · ${t(bindingReviewStatus(view))}`)),
|
|
585
|
-
h('span', { className: 'ptcPlusBindingDockChevron', 'aria-hidden': true },
|
|
692
|
+
enlarged ? null : h('span', { className: 'ptcPlusBindingDockChevron', 'aria-hidden': true },
|
|
586
693
|
h(IconChevronDownOutline14, { size: 16 }))),
|
|
587
|
-
h(
|
|
588
|
-
|
|
694
|
+
h('div', { className: 'ptcPlusBindingDockControls' },
|
|
695
|
+
h(IconButton, { icon: enlarged ? RestoreIcon : EnlargeIcon,
|
|
696
|
+
label: t(enlarged ? 'bindings.reviewRestore' : 'bindings.reviewEnlarge'),
|
|
697
|
+
onClick: enlarged ? restorePreview : () => review.display('enlarged', view.candidate) }),
|
|
698
|
+
h(IconButton, { icon: isHostIconComponent(IconCloseOutline16) ? IconCloseOutline16 : ReviewCloseIcon,
|
|
699
|
+
label: t('bindings.reviewClose'), onClick: close }))),
|
|
700
|
+
!enlarged && view.visibility !== 'expanded' ? null : h(React.Fragment, null,
|
|
589
701
|
h('div', { className: 'ptcPlusBindingDockBody', id: content, tabIndex: 0,
|
|
702
|
+
inert: !expanded ? '' : undefined, 'aria-hidden': !expanded ? true : undefined,
|
|
590
703
|
role: 'region', 'aria-label': t('bindings.source') },
|
|
704
|
+
view.message === null ? null : h('p', { className: 'ptcPlusMessage ptcPlusDanger', role: 'status' }, t(view.message)),
|
|
705
|
+
view.message === null ? null : h(ActionButton, { disabled: view.busy,
|
|
706
|
+
onClick: () => review.reset() }, t('bindings.reviewRetry')),
|
|
591
707
|
h(BindingCandidateContent, { candidate: view.candidate, t, showName: false })),
|
|
592
|
-
|
|
593
|
-
|
|
708
|
+
h('div', { className: 'ptcPlusBindingDockActions',
|
|
709
|
+
inert: !expanded ? '' : undefined, 'aria-hidden': !expanded ? true : undefined },
|
|
594
710
|
h(ActionButton, { className: 'ptcPlusBindingDockDiscard', 'data-kind': 'ghost',
|
|
595
711
|
disabled: !view.writable || view.busy,
|
|
596
712
|
onClick: () => act('discard-draft') }, t('bindings.draftDiscard')),
|
|
597
713
|
h(ActionButton, { disabled: !view.writable || view.busy,
|
|
598
714
|
onClick: () => act('save-draft', false) }, t('bindings.draftSave')),
|
|
599
715
|
h(ActionButton, { 'data-kind': 'primary', disabled: !view.writable || view.busy,
|
|
600
|
-
onClick: () => act('save-draft', true) }, t('bindings.draftSaveEnable'))
|
|
601
|
-
|
|
602
|
-
|
|
716
|
+
onClick: () => act('save-draft', true) }, t('bindings.draftSaveEnable')))))
|
|
717
|
+
return h('div', { className: 'ptcPlusBindingDockAnchor', ref: anchor }, enlarged
|
|
718
|
+
? h(Modal, { open: view.reachable, title: `${view.candidate.entry.name} · ${t('bindings.reviewTitle')}`,
|
|
719
|
+
headless: true, className: 'ptcPlusBindingReviewModal', onClose: restorePreview }, panel)
|
|
720
|
+
: !view.reachable ? null : createPortal(h('div', { className: 'ptcPlusBindingPreviewLayer', ref: layer }, panel), document.body))
|
|
603
721
|
}
|
|
604
722
|
|
|
605
723
|
function BindingCommandCard({ node, sessionId, t, useProjection }) {
|