dsh-ptc-plus 0.4.8 → 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.
Files changed (62) hide show
  1. package/README.en.md +11 -3
  2. package/README.md +11 -3
  3. package/client.js +1151 -529
  4. package/cordis.patch.yml +12 -2
  5. package/docs/README.md +1 -0
  6. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +19 -0
  7. package/docs/adr/0019-plugin-settings-and-kill-switch.md +13 -2
  8. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +1 -1
  9. package/docs/adr/0023-global-user-bindings.md +1 -1
  10. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +82 -18
  11. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +3 -3
  12. package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +27 -0
  13. package/docs/adr/0033-gate-client-visual-usability-in-a-browser.md +23 -0
  14. package/docs/architecture.md +3 -3
  15. package/docs/binding-review-placement-design.md +29 -21
  16. package/docs/client-ui.md +20 -8
  17. package/docs/client-visual-contract.md +48 -0
  18. package/docs/durability-design.md +12 -0
  19. package/docs/installation.md +27 -6
  20. package/docs/runtime-reference.md +1 -1
  21. package/docs/user-bindings.en.md +2 -2
  22. package/docs/user-bindings.md +3 -3
  23. package/docs/verification-optimization-work.md +26 -0
  24. package/docs/verification.md +2 -0
  25. package/index.js +28 -7
  26. package/internal/compiler-platform.js +5 -3
  27. package/internal/compiler-service.js +2 -9
  28. package/internal/cordis-tools-owner.js +22 -19
  29. package/internal/direct-surface-owner.js +2 -0
  30. package/internal/edit-transport-owner.js +8 -5
  31. package/internal/execution-provider-owner.js +157 -0
  32. package/internal/execution-seam-compat.js +69 -113
  33. package/internal/isolated-worker.js +12 -8
  34. package/internal/recovery-tips.js +50 -21
  35. package/internal/runtime-bridge-owner.js +88 -57
  36. package/internal/runtime-contexts.js +13 -13
  37. package/internal/session-events.js +40 -4
  38. package/internal/session-journal-recovery.js +678 -181
  39. package/internal/session-log-view.js +189 -43
  40. package/internal/session-runtime.js +58 -10
  41. package/internal/settings-compat.js +12 -2
  42. package/internal/user-bindings-owner.js +6 -5
  43. package/locale/en.json +6 -0
  44. package/locale/zh.json +6 -0
  45. package/package.json +13 -14
  46. package/src/client-authoring-view.js +222 -141
  47. package/src/client-binding-review.js +2 -0
  48. package/src/client-code-editor.js +6 -0
  49. package/src/client-copy.js +10 -0
  50. package/src/client-feature-gates.js +12 -5
  51. package/src/client-host-compat.js +23 -2
  52. package/src/client-primitives.js +249 -0
  53. package/src/client-repl-view.js +3 -3
  54. package/src/client-settings-view.js +2 -29
  55. package/src/client-styles.js +92 -33
  56. package/src/client-tool-view.js +8 -3
  57. package/src/client-workbench.js +3 -3
  58. package/src/client.js +72 -46
  59. package/assets/ptc-plus-bindings-en.png +0 -0
  60. package/assets/ptc-plus-bindings-zh.png +0 -0
  61. package/assets/ptc-plus-repair-en.png +0 -0
  62. package/assets/ptc-plus-repair-zh.png +0 -0
@@ -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 only from a ready snapshot
22
- * with the master switch on. These features default to on unless the user
23
- * turned them off. Writability gates setting writes, not contribution presence.
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
- if (snapshot?.status !== 'ready' || snapshot.value?.enabled !== true) return false
29
- const value = snapshot.value
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 typeof icon === 'function' && icon.absentHostIcon !== true
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(value => typeof value === 'function') ?? ABSENT_HOST_ICON,
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 }
@@ -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
- typeof CodeBlock === 'function'
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
- typeof CodeBlock === 'function'
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', ref: body },
189
+ }, h('div', { className: 'ptcPlusSettingsDialog' },
217
190
  h(PTCPlusSettingsCard, { ...props, view: 'dialog', onBindingsOpenChange: setBindingsOpen })))
218
191
  }
219
192