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
package/src/client-copy.js
CHANGED
|
@@ -106,6 +106,9 @@ const CHROME_COPY = Object.freeze({
|
|
|
106
106
|
'bindings.openHint': 'PTC Plus · 全局绑定与设置',
|
|
107
107
|
'bindings.draftHint': 'PTC Plus 插件 · 绑定草稿待处理,点击查看',
|
|
108
108
|
'bindings.quickHeading': '全局用户绑定 · 对所有会话生效',
|
|
109
|
+
'bindings.quickEnabled': '已启用({count})',
|
|
110
|
+
'bindings.quickDisabled': '停用({count})',
|
|
111
|
+
'bindings.quickNoEnabled': '暂无启用绑定',
|
|
109
112
|
'bindings.quickDrafts': '待处理草稿',
|
|
110
113
|
'bindings.quickLoading': '正在读取绑定…',
|
|
111
114
|
'bindings.quickSaving': '正在保存…',
|
|
@@ -118,6 +121,8 @@ const CHROME_COPY = Object.freeze({
|
|
|
118
121
|
'bindings.reviewExpand': '展开绑定草稿',
|
|
119
122
|
'bindings.reviewCollapse': '折叠绑定草稿',
|
|
120
123
|
'bindings.reviewClose': '关闭绑定草稿面板',
|
|
124
|
+
'bindings.reviewEnlarge': '放大查看绑定草稿',
|
|
125
|
+
'bindings.reviewRestore': '缩小显示绑定草稿',
|
|
121
126
|
'bindings.reviewOpen': '打开草稿',
|
|
122
127
|
'bindings.reviewMenuLabel': '绑定草稿({count})',
|
|
123
128
|
'bindings.reviewLoading': '正在确认草稿操作资格…',
|
|
@@ -277,6 +282,9 @@ const CHROME_COPY = Object.freeze({
|
|
|
277
282
|
'bindings.openHint': 'PTC Plus · Global bindings and settings',
|
|
278
283
|
'bindings.draftHint': 'PTC Plus plugin · Binding draft pending; click to review',
|
|
279
284
|
'bindings.quickHeading': 'Global bindings · Applies to all sessions',
|
|
285
|
+
'bindings.quickEnabled': 'Enabled ({count})',
|
|
286
|
+
'bindings.quickDisabled': 'Disabled ({count})',
|
|
287
|
+
'bindings.quickNoEnabled': 'No enabled bindings',
|
|
280
288
|
'bindings.quickDrafts': 'Pending drafts',
|
|
281
289
|
'bindings.quickLoading': 'Loading bindings…',
|
|
282
290
|
'bindings.quickSaving': 'Saving…',
|
|
@@ -289,6 +297,8 @@ const CHROME_COPY = Object.freeze({
|
|
|
289
297
|
'bindings.reviewExpand': 'Expand binding draft',
|
|
290
298
|
'bindings.reviewCollapse': 'Collapse binding draft',
|
|
291
299
|
'bindings.reviewClose': 'Close binding draft panel',
|
|
300
|
+
'bindings.reviewEnlarge': 'Enlarge binding draft',
|
|
301
|
+
'bindings.reviewRestore': 'Shrink binding draft',
|
|
292
302
|
'bindings.reviewOpen': 'Open draft',
|
|
293
303
|
'bindings.reviewMenuLabel': 'Binding drafts ({count})',
|
|
294
304
|
'bindings.reviewLoading': 'Confirming draft actions…',
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { CONFIG_DEFAULTS } from '../internal/config-spec.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Settings snapshot to feature eligibility. Each feature names the settings that
|
|
3
5
|
* gate exactly one contribution, so a new gated surface reads one table instead
|
|
@@ -18,15 +20,20 @@ const FEATURE_SETTINGS = Object.freeze({
|
|
|
18
20
|
})
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
|
-
* Pure settings projection: a feature is eligible
|
|
22
|
-
*
|
|
23
|
-
*
|
|
23
|
+
* Pure settings projection: a feature is eligible unless the settings say
|
|
24
|
+
* otherwise. Only a readable snapshot can turn a feature off — an unreadable or
|
|
25
|
+
* not-yet-arrived settings transport falls back to the defaults the plugin ships
|
|
26
|
+
* in `CONFIG_FIELDS`, so a transport outage degrades the settings surface
|
|
27
|
+
* instead of removing every contribution the plugin owns. Writability gates
|
|
28
|
+
* setting writes, not contribution presence.
|
|
24
29
|
*/
|
|
25
30
|
export function featureEnabled(snapshot, feature) {
|
|
26
31
|
const rule = FEATURE_SETTINGS[feature]
|
|
27
32
|
if (rule === undefined) throw new Error(`Unknown client feature: ${feature}`)
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
const value = snapshot?.status === 'ready' && snapshot.value !== undefined
|
|
34
|
+
? snapshot.value
|
|
35
|
+
: CONFIG_DEFAULTS
|
|
36
|
+
if (value.enabled === false) return false
|
|
30
37
|
return rule.every(key => value[key] !== false)
|
|
31
38
|
}
|
|
32
39
|
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
import { SETTINGS_NAMESPACE } from '../internal/config-spec.js'
|
|
2
|
+
import { ForwardRef, Lazy, Memo, isValidElementType } from 'react-is'
|
|
3
|
+
|
|
4
|
+
/** Host exports are components, including React's wrapped object types. */
|
|
5
|
+
export function isHostComponent(component) {
|
|
6
|
+
if (typeof component === 'function') return true
|
|
7
|
+
if (typeof component !== 'object' || component === null) return false
|
|
8
|
+
const seen = new Set()
|
|
9
|
+
while (isValidElementType(component)) {
|
|
10
|
+
if (typeof component !== 'object' || component === null) return true
|
|
11
|
+
if (seen.has(component)) return false
|
|
12
|
+
seen.add(component)
|
|
13
|
+
// react-is also admits server references and wrappers with missing payloads.
|
|
14
|
+
if (component.$$typeof === ForwardRef) return typeof component.render === 'function'
|
|
15
|
+
if (component.$$typeof === Lazy) return typeof component._init === 'function'
|
|
16
|
+
&& typeof component._payload === 'object' && component._payload !== null
|
|
17
|
+
if (component.$$typeof !== Memo) return false
|
|
18
|
+
if (component.compare != null && typeof component.compare !== 'function') return false
|
|
19
|
+
component = component.type
|
|
20
|
+
}
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
2
23
|
|
|
3
24
|
const HOST_ICON_EXPORTS = Object.freeze({
|
|
4
25
|
check: ['IconCheckOutlineRegular', 'IconCheckOutline14'],
|
|
@@ -28,7 +49,7 @@ ABSENT_HOST_ICON.absentHostIcon = true
|
|
|
28
49
|
|
|
29
50
|
/** Whether a normalized icon entry is a glyph the host actually published. */
|
|
30
51
|
export function isHostIconComponent(icon) {
|
|
31
|
-
return
|
|
52
|
+
return isHostComponent(icon) && icon.absentHostIcon !== true
|
|
32
53
|
}
|
|
33
54
|
|
|
34
55
|
/**
|
|
@@ -42,7 +63,7 @@ export function hostIconComponents(primitives) {
|
|
|
42
63
|
return Object.fromEntries(Object.entries(HOST_ICON_EXPORTS).map(([name, candidates]) => [
|
|
43
64
|
name,
|
|
44
65
|
candidates.map(candidate => primitives?.[candidate])
|
|
45
|
-
.find(
|
|
66
|
+
.find(isHostComponent) ?? ABSENT_HOST_ICON,
|
|
46
67
|
]))
|
|
47
68
|
}
|
|
48
69
|
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { isHostComponent } from './client-host-compat.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the Harness Client components this plugin renders against the
|
|
5
|
+
* installed generation.
|
|
6
|
+
*
|
|
7
|
+
* The plugin may load a generation that renamed, removed, or reshaped a
|
|
8
|
+
* primitive; a missing component would otherwise throw while rendering and take
|
|
9
|
+
* the whole slot entry down with it. Each component therefore resolves to the
|
|
10
|
+
* shipped primitive when that generation provides it and to a plugin-local
|
|
11
|
+
* fallback otherwise, so a primitive change degrades one control instead of
|
|
12
|
+
* blanking the entry that renders it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** One menu row: separators and labels paint; every other row selects by id. */
|
|
16
|
+
function menuEntry(React, item, onSelect) {
|
|
17
|
+
const h = React.createElement
|
|
18
|
+
const content = item.label ?? item.text ?? null
|
|
19
|
+
if (item.type === 'separator') {
|
|
20
|
+
return h('div', { key: item.id, className: 'ptcPlusFallbackMenuSeparator', role: 'separator' })
|
|
21
|
+
}
|
|
22
|
+
if (item.type === 'label') {
|
|
23
|
+
return h('div', { key: item.id, className: 'ptcPlusFallbackMenuLabel' }, content)
|
|
24
|
+
}
|
|
25
|
+
return h('button', {
|
|
26
|
+
key: item.id, type: 'button', role: 'menuitem', className: 'ptcPlusFallbackMenuItem',
|
|
27
|
+
disabled: item.disabled === true,
|
|
28
|
+
onClick: () => onSelect?.(item.id),
|
|
29
|
+
}, content)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function useCompositionGuard(React, open) {
|
|
33
|
+
const composing = React.useRef(false)
|
|
34
|
+
const ended = React.useRef(false)
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
composing.current = false
|
|
37
|
+
ended.current = false
|
|
38
|
+
if (!open) return undefined
|
|
39
|
+
const start = () => { composing.current = true }
|
|
40
|
+
const end = () => { composing.current = false; ended.current = true }
|
|
41
|
+
const release = () => { ended.current = false }
|
|
42
|
+
const blur = () => { composing.current = false; ended.current = false }
|
|
43
|
+
document.addEventListener('compositionstart', start, true)
|
|
44
|
+
document.addEventListener('compositionend', end, true)
|
|
45
|
+
document.addEventListener('keyup', release, true)
|
|
46
|
+
window.addEventListener('blur', blur)
|
|
47
|
+
return () => {
|
|
48
|
+
document.removeEventListener('compositionstart', start, true)
|
|
49
|
+
document.removeEventListener('compositionend', end, true)
|
|
50
|
+
document.removeEventListener('keyup', release, true)
|
|
51
|
+
window.removeEventListener('blur', blur)
|
|
52
|
+
composing.current = false
|
|
53
|
+
ended.current = false
|
|
54
|
+
}
|
|
55
|
+
}, [open])
|
|
56
|
+
return event => {
|
|
57
|
+
const guarded = composing.current || ended.current || event.isComposing === true || event.keyCode === 229
|
|
58
|
+
ended.current = false
|
|
59
|
+
return guarded
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Menu fallback: the anchor, the item list, the pinned footer and the children region. */
|
|
64
|
+
function createFallbackMenu(React, createPortal = content => content) {
|
|
65
|
+
const h = React.createElement
|
|
66
|
+
return function FallbackMenu({ open, items = [], children, footer, className, listClassName, autoFocus, anchor,
|
|
67
|
+
onClose, onSelect, portal = false, side = 'bottom', align = 'start', getAnchorRect, closeOnPointerLeave = false }) {
|
|
68
|
+
const root = React.useRef(null)
|
|
69
|
+
const list = React.useRef(null)
|
|
70
|
+
const closeTimer = React.useRef(null)
|
|
71
|
+
const close = React.useRef(onClose)
|
|
72
|
+
close.current = onClose
|
|
73
|
+
const isComposing = useCompositionGuard(React, open)
|
|
74
|
+
const cancelClose = () => { clearTimeout(closeTimer.current); closeTimer.current = null }
|
|
75
|
+
const scheduleClose = () => {
|
|
76
|
+
if (!closeOnPointerLeave) return
|
|
77
|
+
cancelClose()
|
|
78
|
+
closeTimer.current = setTimeout(() => close.current?.(), 180)
|
|
79
|
+
}
|
|
80
|
+
React.useEffect(() => cancelClose, [open])
|
|
81
|
+
React.useLayoutEffect(() => {
|
|
82
|
+
if (!open) return undefined
|
|
83
|
+
const place = () => {
|
|
84
|
+
const rect = getAnchorRect?.() ?? root.current?.getBoundingClientRect()
|
|
85
|
+
const surface = list.current
|
|
86
|
+
if (!rect || !surface) return
|
|
87
|
+
const viewport = window.visualViewport
|
|
88
|
+
const viewportTop = viewport?.offsetTop ?? 0
|
|
89
|
+
const viewportLeft = viewport?.offsetLeft ?? 0
|
|
90
|
+
const viewportHeight = viewport?.height ?? window.innerHeight
|
|
91
|
+
const viewportWidth = viewport?.width ?? window.innerWidth
|
|
92
|
+
const available = Math.max(0, side === 'top' ? rect.top - viewportTop - 12
|
|
93
|
+
: viewportTop + viewportHeight - rect.bottom - 12)
|
|
94
|
+
surface.style.maxHeight = `${available}px`
|
|
95
|
+
const measured = surface.getBoundingClientRect()
|
|
96
|
+
const left = align === 'end' ? rect.right - measured.width : rect.left
|
|
97
|
+
surface.style.left = `${Math.max(viewportLeft + 8, Math.min(left, viewportLeft + viewportWidth - measured.width - 8))}px`
|
|
98
|
+
surface.style.top = `${side === 'top' ? Math.max(viewportTop + 8, rect.top - measured.height - 4) : rect.bottom + 4}px`
|
|
99
|
+
}
|
|
100
|
+
place()
|
|
101
|
+
window.addEventListener('resize', place)
|
|
102
|
+
window.addEventListener('scroll', place, true)
|
|
103
|
+
window.visualViewport?.addEventListener('resize', place)
|
|
104
|
+
window.visualViewport?.addEventListener('scroll', place)
|
|
105
|
+
return () => {
|
|
106
|
+
window.removeEventListener('resize', place)
|
|
107
|
+
window.removeEventListener('scroll', place, true)
|
|
108
|
+
window.visualViewport?.removeEventListener('resize', place)
|
|
109
|
+
window.visualViewport?.removeEventListener('scroll', place)
|
|
110
|
+
}
|
|
111
|
+
}, [open, side, align, getAnchorRect, children, items, footer])
|
|
112
|
+
React.useEffect(() => {
|
|
113
|
+
if (!open) return undefined
|
|
114
|
+
if (autoFocus) list.current?.querySelector('button:not(:disabled)')?.focus()
|
|
115
|
+
const onKeyDown = event => {
|
|
116
|
+
if (event.defaultPrevented || isComposing(event)) return
|
|
117
|
+
if (event.key === 'Escape') {
|
|
118
|
+
event.preventDefault()
|
|
119
|
+
event.stopPropagation()
|
|
120
|
+
if (!event.repeat) close.current?.()
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
if (!root.current?.contains(event.target) && !list.current?.contains(event.target)) return
|
|
124
|
+
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return
|
|
125
|
+
const controls = [...list.current.querySelectorAll('button:not(:disabled)')]
|
|
126
|
+
if (controls.length === 0) return
|
|
127
|
+
const current = controls.indexOf(document.activeElement)
|
|
128
|
+
const next = event.key === 'Home' ? 0 : event.key === 'End' ? controls.length - 1
|
|
129
|
+
: (current + (event.key === 'ArrowDown' ? 1 : -1) + controls.length) % controls.length
|
|
130
|
+
event.preventDefault()
|
|
131
|
+
controls[next].focus()
|
|
132
|
+
}
|
|
133
|
+
const onPointerDown = event => {
|
|
134
|
+
if (root.current?.contains(event.target) || list.current?.contains(event.target)) return
|
|
135
|
+
close.current?.()
|
|
136
|
+
}
|
|
137
|
+
document.addEventListener('keydown', onKeyDown)
|
|
138
|
+
document.addEventListener('pointerdown', onPointerDown, true)
|
|
139
|
+
return () => {
|
|
140
|
+
document.removeEventListener('keydown', onKeyDown)
|
|
141
|
+
document.removeEventListener('pointerdown', onPointerDown, true)
|
|
142
|
+
}
|
|
143
|
+
}, [open, autoFocus])
|
|
144
|
+
const rows = list => list.map(item => menuEntry(React, item, onSelect))
|
|
145
|
+
const surface = open ? h('div', { ref: list, className: ['ptcPlusFallbackMenuList', listClassName].filter(Boolean).join(' '),
|
|
146
|
+
role: 'menu', onPointerEnter: cancelClose, onPointerLeave: scheduleClose }, ...rows(items),
|
|
147
|
+
...(Array.isArray(footer) ? rows(footer) : []),
|
|
148
|
+
children ?? null) : null
|
|
149
|
+
return h('span', { ref: root, className: ['ptcPlusFallbackMenu', className].filter(Boolean).join(' '),
|
|
150
|
+
onPointerEnter: cancelClose, onPointerLeave: scheduleClose }, anchor ?? null,
|
|
151
|
+
portal && surface !== null ? createPortal(surface, document.body) : surface)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Modal fallback: headless content or a titled dialog that closes on Escape and backdrop. */
|
|
156
|
+
function createFallbackModal(React, createPortal = content => content) {
|
|
157
|
+
const h = React.createElement
|
|
158
|
+
const modalStack = []
|
|
159
|
+
return function FallbackModal({ open, onClose, title, closeLabel, description, className, contentClassName, headless, children }) {
|
|
160
|
+
const root = React.useRef(null)
|
|
161
|
+
const close = React.useRef(onClose)
|
|
162
|
+
close.current = onClose
|
|
163
|
+
const isComposing = useCompositionGuard(React, open)
|
|
164
|
+
const focusable = 'button:not(:disabled),input:not(:disabled),textarea:not(:disabled),select:not(:disabled),[href],[tabindex]:not([tabindex="-1"])'
|
|
165
|
+
React.useLayoutEffect(() => {
|
|
166
|
+
if (!open) return undefined
|
|
167
|
+
const previous = document.activeElement
|
|
168
|
+
const surface = root.current
|
|
169
|
+
const target = surface?.querySelector('[data-modal-autofocus]:not(:disabled)') ?? surface?.querySelector(focusable) ?? surface
|
|
170
|
+
target?.focus({ preventScroll: true })
|
|
171
|
+
modalStack.push(surface)
|
|
172
|
+
const onDocumentKeyDown = event => {
|
|
173
|
+
if (modalStack.at(-1) === surface) onKeyDown(event)
|
|
174
|
+
}
|
|
175
|
+
document.addEventListener('keydown', onDocumentKeyDown)
|
|
176
|
+
return () => {
|
|
177
|
+
document.removeEventListener('keydown', onDocumentKeyDown)
|
|
178
|
+
modalStack.splice(modalStack.indexOf(surface), 1)
|
|
179
|
+
if (document.activeElement === document.body || surface?.contains(document.activeElement)) previous?.focus?.({ preventScroll: true })
|
|
180
|
+
}
|
|
181
|
+
}, [open])
|
|
182
|
+
const onKeyDown = event => {
|
|
183
|
+
if (event.defaultPrevented || isComposing(event)) return
|
|
184
|
+
if (event.key === 'Escape') {
|
|
185
|
+
event.preventDefault()
|
|
186
|
+
event.stopPropagation()
|
|
187
|
+
if (!event.repeat) close.current?.()
|
|
188
|
+
} else if (event.key === 'Tab') {
|
|
189
|
+
const controls = [...root.current.querySelectorAll(focusable)]
|
|
190
|
+
.filter(control => control.getClientRects().length > 0)
|
|
191
|
+
const target = event.shiftKey ? controls.at(-1) : controls[0]
|
|
192
|
+
const boundary = event.shiftKey ? controls[0] : controls.at(-1)
|
|
193
|
+
if (controls.length === 0 || document.activeElement === boundary || !root.current.contains(document.activeElement)) {
|
|
194
|
+
event.preventDefault()
|
|
195
|
+
;(target ?? root.current).focus({ preventScroll: true })
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (!open) return null
|
|
200
|
+
const body = h('div', { className: contentClassName ?? 'ptcPlusFallbackModalBody' }, children)
|
|
201
|
+
const surface = ['ptcPlusFallbackModal', className].filter(Boolean).join(' ')
|
|
202
|
+
return createPortal(h('div', {
|
|
203
|
+
className: 'ptcPlusFallbackModalBackdrop',
|
|
204
|
+
onPointerDown: event => { if (event.target === event.currentTarget) onClose?.() },
|
|
205
|
+
}, h('div', { ref: root, tabIndex: -1, className: surface, role: 'dialog', 'aria-modal': 'true', 'aria-label': title },
|
|
206
|
+
headless === true ? null : h('div', { className: 'ptcPlusFallbackModalHead' },
|
|
207
|
+
h('h2', null, title),
|
|
208
|
+
h('button', { type: 'button', className: 'ptcPlusDialogClose', 'aria-label': closeLabel ?? title, onClick: () => onClose?.() },
|
|
209
|
+
closeLabel ?? '\u00d7')),
|
|
210
|
+
headless === true || description === undefined ? null : h('p', { className: 'ptcPlusDetail' }, description),
|
|
211
|
+
headless === true ? children : body)), document.body)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Resolve every component the plugin renders.
|
|
217
|
+
* @param primitives - the installed generation's Client primitives.
|
|
218
|
+
* @param React - the browser React instance.
|
|
219
|
+
* @returns the component set the views consume.
|
|
220
|
+
*/
|
|
221
|
+
export function resolvePrimitives(primitives, React, createPortal) {
|
|
222
|
+
return Object.freeze({
|
|
223
|
+
Button: primitives.Button,
|
|
224
|
+
CodeBlock: primitives.CodeBlock,
|
|
225
|
+
DisclosureRow: primitives.DisclosureRow,
|
|
226
|
+
Menu: isHostComponent(primitives.Menu) ? primitives.Menu : createFallbackMenu(React, createPortal),
|
|
227
|
+
MenuFallback: createFallbackMenu(React, createPortal),
|
|
228
|
+
Modal: isHostComponent(primitives.Modal) ? primitives.Modal : createFallbackModal(React, createPortal),
|
|
229
|
+
Toast: primitives.Toast,
|
|
230
|
+
Tooltip: primitives.Tooltip,
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Adapt host action styling, or own it completely when the primitive is absent. */
|
|
235
|
+
export function createActionButton(React, Button) {
|
|
236
|
+
const h = React.createElement
|
|
237
|
+
return function ActionButton({ className = '', 'data-kind': kind, ...props }) {
|
|
238
|
+
const classes = className.split(/\s+/).filter(name => name !== '' && name !== 'ptcPlusButton')
|
|
239
|
+
return isHostComponent(Button)
|
|
240
|
+
? h(Button, {
|
|
241
|
+
type: 'button', ...props, size: 'sm',
|
|
242
|
+
variant: kind === 'primary' ? 'primary' : kind === 'ghost' ? 'ghost' : 'outline',
|
|
243
|
+
className: classes.join(' '),
|
|
244
|
+
})
|
|
245
|
+
: h('button', { type: 'button', ...props, className: ['ptcPlusButton', ...classes].join(' '), 'data-kind': kind })
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export { createFallbackMenu, createFallbackModal }
|
package/src/client-repl-view.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isHostIconComponent } from './client-host-compat.js'
|
|
1
|
+
import { isHostComponent, isHostIconComponent } from './client-host-compat.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* REPL surface views: the conversation tab (session bindings + global workbench)
|
|
@@ -23,7 +23,7 @@ export function createReplView(React, deps) {
|
|
|
23
23
|
h('strong', null, entry.name),
|
|
24
24
|
h('span', null, t('memory.location', entry.definition))),
|
|
25
25
|
h('div', { className: 'ptcPlusObservationLabel' }, t('console.definition')),
|
|
26
|
-
|
|
26
|
+
isHostComponent(CodeBlock)
|
|
27
27
|
? h(CodeBlock, { code: entry.definition.source, lang: 'typescript', className: 'ptcPlusObservationCode',
|
|
28
28
|
copyLabel: t('tool.copy'), copiedLabel: t('tool.copied') })
|
|
29
29
|
: h('pre', { className: 'ptcPlusObservationCode' }, entry.definition.source),
|
|
@@ -275,7 +275,7 @@ export function createReplView(React, deps) {
|
|
|
275
275
|
line: binding.definition.line,
|
|
276
276
|
column: binding.definition.column,
|
|
277
277
|
})),
|
|
278
|
-
|
|
278
|
+
isHostComponent(CodeBlock)
|
|
279
279
|
? h(CodeBlock, {
|
|
280
280
|
code: binding.definition.source,
|
|
281
281
|
lang: 'typescript',
|
|
@@ -10,13 +10,13 @@ import { featureEnabled } from './client-feature-gates.js'
|
|
|
10
10
|
export function createPtcSettingsView(React, deps) {
|
|
11
11
|
const { ActionButton, BindingsDialog, Modal, useWorkbenchController, icons } = deps
|
|
12
12
|
const h = React.createElement
|
|
13
|
-
const focusableSelector = 'button:not(:disabled),input:not(:disabled),textarea:not(:disabled),select:not(:disabled),[href],[tabindex]:not([tabindex="-1"]),[contenteditable=true],summary'
|
|
14
13
|
|
|
15
14
|
function fieldInput(field, value, disabled, onChange, label) {
|
|
16
15
|
if (field.type === 'boolean') {
|
|
17
16
|
return h('input', {
|
|
18
17
|
type: 'checkbox', role: 'switch', className: 'ptcPlusCheck', checked: value === true,
|
|
19
18
|
disabled, 'aria-label': label,
|
|
19
|
+
'data-modal-autofocus': field.key === 'enabled' && !disabled ? true : undefined,
|
|
20
20
|
onChange: event => onChange(field, event.target.checked),
|
|
21
21
|
})
|
|
22
22
|
}
|
|
@@ -174,34 +174,7 @@ export function createPtcSettingsView(React, deps) {
|
|
|
174
174
|
|
|
175
175
|
function PTCPlusSettingsDialog(props) {
|
|
176
176
|
const { t, onClose } = props
|
|
177
|
-
const body = React.useRef(null)
|
|
178
177
|
const [bindingsOpen, setBindingsOpen] = React.useState(false)
|
|
179
|
-
React.useEffect(() => {
|
|
180
|
-
const dialog = body.current?.closest('[role=dialog]')
|
|
181
|
-
const target = body.current?.querySelector(focusableSelector)
|
|
182
|
-
?? dialog?.querySelector(focusableSelector)
|
|
183
|
-
target?.focus({ preventScroll: true })
|
|
184
|
-
}, [])
|
|
185
|
-
React.useEffect(() => {
|
|
186
|
-
const trapFocus = event => {
|
|
187
|
-
if (event.key !== 'Tab' || bindingsOpen) return
|
|
188
|
-
const dialog = body.current?.closest('[role=dialog]')
|
|
189
|
-
if (dialog === undefined || dialog === null) return
|
|
190
|
-
const controls = [...dialog.querySelectorAll(focusableSelector)]
|
|
191
|
-
.filter(element => !element.closest('[hidden], [inert], [aria-hidden="true"]'))
|
|
192
|
-
const first = controls[0]
|
|
193
|
-
const last = controls.at(-1)
|
|
194
|
-
const active = document.activeElement
|
|
195
|
-
if (!dialog.contains(active)
|
|
196
|
-
|| (event.shiftKey && active === first)
|
|
197
|
-
|| (!event.shiftKey && active === last)) {
|
|
198
|
-
event.preventDefault()
|
|
199
|
-
;(event.shiftKey ? last : first)?.focus({ preventScroll: true })
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
document.addEventListener('keydown', trapFocus)
|
|
203
|
-
return () => document.removeEventListener('keydown', trapFocus)
|
|
204
|
-
}, [bindingsOpen])
|
|
205
178
|
const close = () => {
|
|
206
179
|
if (!bindingsOpen) onClose()
|
|
207
180
|
}
|
|
@@ -213,7 +186,7 @@ export function createPtcSettingsView(React, deps) {
|
|
|
213
186
|
description: t('card.description'),
|
|
214
187
|
className: 'ptcPlusSettingsModal',
|
|
215
188
|
contentClassName: 'ptcPlusSettingsDialogContent',
|
|
216
|
-
}, h('div', { className: 'ptcPlusSettingsDialog'
|
|
189
|
+
}, h('div', { className: 'ptcPlusSettingsDialog' },
|
|
217
190
|
h(PTCPlusSettingsCard, { ...props, view: 'dialog', onBindingsOpenChange: setBindingsOpen })))
|
|
218
191
|
}
|
|
219
192
|
|