@morscherlab/mint-sdk 1.2.0-beta.4 → 1.2.0-beta.7

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 (137) hide show
  1. package/dist/{EmptyState-p90quq8Z.js → EmptyState-NBqf2_8z.js} +2 -2
  2. package/dist/{EmptyState-p90quq8Z.js.map → EmptyState-NBqf2_8z.js.map} +1 -1
  3. package/dist/{ExperimentPopover-tLwJ7XF4.js → ExperimentPopover-CfOdh5Rm.js} +3 -3
  4. package/dist/{ExperimentPopover-tLwJ7XF4.js.map → ExperimentPopover-CfOdh5Rm.js.map} +1 -1
  5. package/dist/{ExperimentSelectorModal-B6P2msT4.js → ExperimentSelectorModal-DvIagrxY.js} +4 -4
  6. package/dist/{ExperimentSelectorModal-B6P2msT4.js.map → ExperimentSelectorModal-DvIagrxY.js.map} +1 -1
  7. package/dist/{SettingsModal-CkNM8qAI.js → SettingsModal-W3TYfnP9.js} +4 -7
  8. package/dist/SettingsModal-W3TYfnP9.js.map +1 -0
  9. package/dist/__tests__/components/PluginWorkspaceView.internal.test.d.ts +1 -0
  10. package/dist/__tests__/components/SecretInput.test.d.ts +1 -0
  11. package/dist/__tests__/components/WellEditPopupInternal.test.d.ts +1 -0
  12. package/dist/components/AppAvatarMenu.vue.d.ts +4 -0
  13. package/dist/components/AppSidebar.vue.d.ts +1 -1
  14. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +1 -1
  15. package/dist/components/DropdownButton.vue.d.ts +2 -0
  16. package/dist/components/FitPanel.vue.d.ts +1 -1
  17. package/dist/components/PluginWorkspaceView.props.d.ts +29 -1
  18. package/dist/components/PluginWorkspaceView.vue.d.ts +5 -5
  19. package/dist/components/SecretInput.vue.d.ts +28 -0
  20. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -1
  21. package/dist/components/index.d.ts +9 -0
  22. package/dist/components/index.js +8 -8
  23. package/dist/components/internal/WellEditPopupInternal.vue.d.ts +3 -1
  24. package/dist/components/internal/WorkspaceControlsInternal.vue.d.ts +23 -0
  25. package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +70 -0
  26. package/dist/components/internal/WorkspaceSidebarInternal.vue.d.ts +43 -0
  27. package/dist/{components-MGNL6tRn.js → components-B80gV0P6.js} +575 -561
  28. package/dist/components-B80gV0P6.js.map +1 -0
  29. package/dist/composables/flattenWorkspaceBindings.d.ts +61 -0
  30. package/dist/composables/index.d.ts +5 -0
  31. package/dist/composables/index.js +9 -9
  32. package/dist/composables/pluginWorkspaceModel.d.ts +164 -0
  33. package/dist/composables/useAsync.d.ts +5 -1
  34. package/dist/composables/useBioTemplateWorkspace.d.ts +4 -20
  35. package/dist/composables/useCommandHistory.d.ts +5 -1
  36. package/dist/composables/useFocusTrap.d.ts +21 -0
  37. package/dist/composables/useMenuKeyboard.d.ts +29 -0
  38. package/dist/composables/useOptimisticMutation.d.ts +5 -1
  39. package/dist/composables/usePluginWorkspace.d.ts +51 -0
  40. package/dist/composables/useResourceCrud.d.ts +5 -1
  41. package/dist/composables/useRovingFocus.d.ts +24 -0
  42. package/dist/composables/useWellPainting.d.ts +5 -1
  43. package/dist/composables/useWellPlateAdapter.d.ts +5 -1
  44. package/dist/composables/useWellPlateValidation.d.ts +5 -1
  45. package/dist/composables/workspaceRecipes.d.ts +188 -0
  46. package/dist/{composables-CKxOcvgh.js → composables-C8mmD5ta.js} +65 -17
  47. package/dist/composables-C8mmD5ta.js.map +1 -0
  48. package/dist/{experiment-utils-CLnzPPq5.js → experiment-utils-DV3DrvE_.js} +70 -51
  49. package/dist/experiment-utils-DV3DrvE_.js.map +1 -0
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.js +13 -13
  52. package/dist/install.js +7 -7
  53. package/dist/styles.css +100 -49
  54. package/dist/templates/builderReadUtils.d.ts +1 -1
  55. package/dist/templates/index.js +4 -4
  56. package/dist/{templates-BTJ9C9Kb.js → templates-CK72aKO1.js} +14 -23
  57. package/dist/templates-CK72aKO1.js.map +1 -0
  58. package/dist/types/form-builder.d.ts +1 -1
  59. package/dist/{useControlSchema-CXhxTul7.js → useControlSchema-_OVhavKq.js} +280 -63
  60. package/dist/useControlSchema-_OVhavKq.js.map +1 -0
  61. package/dist/{useDropdownState-DSb3BC3N.js → useDropdownState-D5G4UvWd.js} +2 -2
  62. package/dist/{useDropdownState-DSb3BC3N.js.map → useDropdownState-D5G4UvWd.js.map} +1 -1
  63. package/dist/{useExperimentSelector-D0pjj9WH.js → useExperimentSelector-DSEckf_g.js} +3 -3
  64. package/dist/{useExperimentSelector-D0pjj9WH.js.map → useExperimentSelector-DSEckf_g.js.map} +1 -1
  65. package/dist/{useFormBuilder-CuOFB3aP.js → useFormBuilder-Bd714C7y.js} +2 -2
  66. package/dist/{useFormBuilder-CuOFB3aP.js.map → useFormBuilder-Bd714C7y.js.map} +1 -1
  67. package/dist/{useProtocolTemplates-BWhQIWV5.js → useProtocolTemplates-Dq5Pzwyd.js} +481 -46
  68. package/dist/useProtocolTemplates-Dq5Pzwyd.js.map +1 -0
  69. package/dist/utils/pluginSecrets.d.ts +53 -0
  70. package/dist/utils/records.d.ts +4 -0
  71. package/package.json +1 -1
  72. package/src/__tests__/components/AppAvatarMenu.test.ts +123 -0
  73. package/src/__tests__/components/BaseTabs.test.ts +67 -0
  74. package/src/__tests__/components/DropdownButton.test.ts +134 -0
  75. package/src/__tests__/components/PluginWorkspaceView.internal.test.ts +230 -0
  76. package/src/__tests__/components/SecretInput.test.ts +87 -0
  77. package/src/__tests__/components/WellEditPopupInternal.test.ts +108 -0
  78. package/src/__tests__/composables/usePluginClient.test.ts +140 -0
  79. package/src/__tests__/docs/frontendDocsCatalog.test.ts +22 -1
  80. package/src/components/AppAvatarMenu.vue +34 -3
  81. package/src/components/BaseModal.vue +8 -56
  82. package/src/components/BaseTabs.vue +26 -1
  83. package/src/components/BioTemplateExperimentWorkspaceView.vue +10 -21
  84. package/src/components/BioTemplatePackWorkspaceView.vue +11 -15
  85. package/src/components/BioTemplatePresetWorkspaceView.vue +8 -13
  86. package/src/components/ComponentBindingRenderer.vue +1 -3
  87. package/src/components/ControlWorkspaceView.vue +11 -22
  88. package/src/components/DoseDesignWorkspaceView.vue +6 -25
  89. package/src/components/DropdownButton.vue +32 -2
  90. package/src/components/PluginWorkspaceView.props.ts +38 -0
  91. package/src/components/PluginWorkspaceView.vue +53 -224
  92. package/src/components/SecretInput.story.vue +79 -0
  93. package/src/components/SecretInput.vue +150 -0
  94. package/src/components/index.ts +9 -0
  95. package/src/components/internal/WellEditPopupInternal.vue +20 -1
  96. package/src/components/internal/WorkspaceControlsInternal.vue +20 -0
  97. package/src/components/internal/WorkspaceShellInternal.vue +125 -0
  98. package/src/components/internal/WorkspaceSidebarInternal.vue +82 -0
  99. package/src/composables/experimentDesignData.ts +1 -3
  100. package/src/composables/flattenWorkspaceBindings.ts +104 -0
  101. package/src/composables/formBuilderRegistry.ts +5 -0
  102. package/src/composables/index.ts +26 -0
  103. package/src/composables/pluginWorkspaceModel.ts +182 -0
  104. package/src/composables/useAsync.ts +5 -1
  105. package/src/composables/useBioTemplatePackWorkspace.ts +15 -9
  106. package/src/composables/useBioTemplatePresetWorkspace.ts +14 -15
  107. package/src/composables/useBioTemplateWorkspace.ts +18 -43
  108. package/src/composables/useCommandHistory.ts +5 -1
  109. package/src/composables/useFocusTrap.ts +91 -0
  110. package/src/composables/useMenuKeyboard.ts +90 -0
  111. package/src/composables/useOptimisticMutation.ts +5 -1
  112. package/src/composables/usePluginClient.ts +42 -4
  113. package/src/composables/usePluginWorkspace.ts +208 -0
  114. package/src/composables/useResourceCrud.ts +5 -1
  115. package/src/composables/useRovingFocus.ts +66 -0
  116. package/src/composables/useWellPainting.ts +5 -1
  117. package/src/composables/useWellPlateAdapter.ts +5 -1
  118. package/src/composables/useWellPlateValidation.ts +5 -1
  119. package/src/composables/workspaceRecipes.ts +267 -0
  120. package/src/index.ts +16 -0
  121. package/src/styles/components/secret-input.css +60 -0
  122. package/src/styles/index.css +1 -0
  123. package/src/templates/builderReadUtils.ts +1 -3
  124. package/src/templates/componentTargetResolvers.ts +1 -3
  125. package/src/templates/controlSchemaTargets.ts +1 -3
  126. package/src/types/form-builder.ts +1 -0
  127. package/src/utils/formModelSync.ts +1 -3
  128. package/src/utils/pluginIcon.ts +1 -3
  129. package/src/utils/pluginSecrets.ts +102 -0
  130. package/src/utils/records.ts +9 -0
  131. package/dist/SettingsModal-CkNM8qAI.js.map +0 -1
  132. package/dist/components-MGNL6tRn.js.map +0 -1
  133. package/dist/composables-CKxOcvgh.js.map +0 -1
  134. package/dist/experiment-utils-CLnzPPq5.js.map +0 -1
  135. package/dist/templates-BTJ9C9Kb.js.map +0 -1
  136. package/dist/useControlSchema-CXhxTul7.js.map +0 -1
  137. package/dist/useProtocolTemplates-BWhQIWV5.js.map +0 -1
@@ -34,6 +34,9 @@ import {
34
34
  type BioTemplateRendererBinding,
35
35
  type UseBioTemplateWorkspaceReturn,
36
36
  } from './useBioTemplateWorkspace'
37
+ import {
38
+ liftWorkspaceComponentBindings,
39
+ } from './flattenWorkspaceBindings'
37
40
  import type {
38
41
  BioTemplateComponentBinding,
39
42
  BioTemplateComponentBindingsById,
@@ -138,14 +141,17 @@ export function useBioTemplatePresetWorkspace(
138
141
  const collection = templateCollection.templateCollection
139
142
  const workspace = computed(() => useBioTemplateWorkspace(collection.value, resolvedControlOptions))
140
143
  const renderer = computed<BioTemplateRendererBinding>(() => ({ target: collection.value }))
141
- const componentBindings = computed(() => workspace.value.componentBindings)
142
- const componentBindingsById = computed(() => workspace.value.componentBindingsById)
143
- const componentImports = computed(() => workspace.value.componentImports)
144
- const componentProps = computed(() => workspace.value.componentProps)
145
- const componentPropsById = computed(() => workspace.value.componentPropsById)
146
- const componentPropsByComponent = computed(() => workspace.value.componentPropsByComponent)
147
- const componentSnippets = computed(() => workspace.value.componentSnippets)
148
- const componentUsage = computed(() => workspace.value.componentUsage)
144
+ const {
145
+ componentBindings,
146
+ componentBindingsById,
147
+ componentImports,
148
+ componentProps,
149
+ componentPropsById,
150
+ componentPropsByComponent,
151
+ componentSnippets,
152
+ componentUsage,
153
+ getComponentProps,
154
+ } = liftWorkspaceComponentBindings(workspace)
149
155
  const form = reactive({
150
156
  ...controls.form,
151
157
  modelValue: controlValues.value,
@@ -179,13 +185,6 @@ export function useBioTemplatePresetWorkspace(
179
185
  ...topBarSettings,
180
186
  }))
181
187
 
182
- function getComponentProps(
183
- component: string,
184
- options?: Parameters<UseBioTemplateWorkspaceReturn['getComponentProps']>[1],
185
- ) {
186
- return workspace.value.getComponentProps(component, options)
187
- }
188
-
189
188
  const bindings: BioTemplatePresetWorkspaceBindings = {
190
189
  renderer,
191
190
  form,
@@ -21,16 +21,12 @@ import {
21
21
  } from './useBioTemplateControls'
22
22
  import {
23
23
  useBioTemplateComponents,
24
- type BioTemplateComponentBinding,
25
- type BioTemplateComponentBindingsById,
26
- type BioTemplateComponentImport,
27
- type BioTemplateComponentPropsByComponent,
28
- type BioTemplateComponentPropsBinding,
29
- type BioTemplateComponentPropsById,
30
- type BioTemplateComponentSnippet,
31
- type BioTemplateComponentUsage,
32
24
  type UseBioTemplateComponentsReturn,
33
25
  } from './useBioTemplateComponents'
26
+ import {
27
+ flattenWorkspaceComponentBindings,
28
+ type BioTemplateWorkspaceComponentBindings,
29
+ } from './flattenWorkspaceBindings'
34
30
 
35
31
  export type BioTemplateWorkspaceTarget = BioTemplateControlTarget
36
32
 
@@ -38,24 +34,16 @@ export interface BioTemplateRendererBinding {
38
34
  target: BioTemplateEnvelope<unknown> | TemplateCollectionEnvelope
39
35
  }
40
36
 
41
- export interface BioTemplateWorkspaceBindings {
37
+ export interface BioTemplateWorkspaceBindings extends BioTemplateWorkspaceComponentBindings {
42
38
  renderer: BioTemplateRendererBinding | null
43
39
  form: ControlWorkspaceFormBinding
44
40
  sidebar: ControlWorkspaceSidebarBinding
45
41
  topBar: UseControlWorkspaceReturn<BioTemplateControlSchema>['bindings']['topBar']
46
42
  topBarSettings: ControlWorkspaceTopBarSettingsBinding
47
43
  pillNav: ControlWorkspacePillNavBinding
48
- componentBindings: BioTemplateComponentBinding[]
49
- componentBindingsById: BioTemplateComponentBindingsById
50
- componentProps: BioTemplateComponentPropsBinding[]
51
- componentPropsById: BioTemplateComponentPropsById
52
- componentPropsByComponent: BioTemplateComponentPropsByComponent
53
- componentSnippets: BioTemplateComponentSnippet[]
54
- componentUsage: BioTemplateComponentUsage | null
55
- getComponentProps: UseBioTemplateComponentsReturn['getComponentProps']
56
44
  }
57
45
 
58
- export interface UseBioTemplateWorkspaceReturn {
46
+ export interface UseBioTemplateWorkspaceReturn extends BioTemplateWorkspaceComponentBindings {
59
47
  target: BioTemplateWorkspaceTarget
60
48
  controls: UseControlSchemaReturn<BioTemplateControlSchema>
61
49
  workspace: UseControlWorkspaceReturn<BioTemplateControlSchema>
@@ -70,15 +58,6 @@ export interface UseBioTemplateWorkspaceReturn {
70
58
  topBarSettings: ControlWorkspaceTopBarSettingsBinding
71
59
  bindings: BioTemplateWorkspaceBindings
72
60
  renderer: BioTemplateRendererBinding | null
73
- componentBindings: BioTemplateComponentBinding[]
74
- componentBindingsById: BioTemplateComponentBindingsById
75
- componentImports: BioTemplateComponentImport[]
76
- componentProps: BioTemplateComponentPropsBinding[]
77
- componentPropsById: BioTemplateComponentPropsById
78
- componentPropsByComponent: BioTemplateComponentPropsByComponent
79
- componentSnippets: BioTemplateComponentSnippet[]
80
- componentUsage: BioTemplateComponentUsage | null
81
- getComponentProps: UseBioTemplateComponentsReturn['getComponentProps']
82
61
  }
83
62
 
84
63
  /** One-stop wiring for BioTemplateRenderer, FormBuilder, AppSidebar, and template-aware SDK components. */
@@ -90,21 +69,25 @@ export function useBioTemplateWorkspace(
90
69
  const workspace = useControlWorkspace(controls.controls, options)
91
70
  const components = useBioTemplateComponents(target)
92
71
  const renderer = isTemplateObjectTarget(target) ? { target } : null
93
- const bindings: BioTemplateWorkspaceBindings = {
94
- renderer,
95
- form: workspace.bindings.form,
96
- sidebar: workspace.bindings.sidebar,
97
- topBar: workspace.bindings.topBar,
98
- topBarSettings: workspace.bindings.topBarSettings,
99
- pillNav: workspace.bindings.pillNav,
72
+ const componentBindings = flattenWorkspaceComponentBindings({
100
73
  componentBindings: components.bindings,
101
74
  componentBindingsById: components.componentBindingsById,
75
+ componentImports: components.imports,
102
76
  componentProps: components.componentProps,
103
77
  componentPropsById: components.componentPropsById,
104
78
  componentPropsByComponent: components.componentPropsByComponent,
105
79
  componentSnippets: components.snippets,
106
80
  componentUsage: components.usage,
107
81
  getComponentProps: components.getComponentProps,
82
+ })
83
+ const bindings: BioTemplateWorkspaceBindings = {
84
+ renderer,
85
+ form: workspace.bindings.form,
86
+ sidebar: workspace.bindings.sidebar,
87
+ topBar: workspace.bindings.topBar,
88
+ topBarSettings: workspace.bindings.topBarSettings,
89
+ pillNav: workspace.bindings.pillNav,
90
+ ...componentBindings,
108
91
  }
109
92
 
110
93
  return {
@@ -122,15 +105,7 @@ export function useBioTemplateWorkspace(
122
105
  topBarSettings: workspace.topBarSettings,
123
106
  bindings,
124
107
  renderer,
125
- componentBindings: components.bindings,
126
- componentBindingsById: components.componentBindingsById,
127
- componentImports: components.imports,
128
- componentProps: components.componentProps,
129
- componentPropsById: components.componentPropsById,
130
- componentPropsByComponent: components.componentPropsByComponent,
131
- componentSnippets: components.snippets,
132
- componentUsage: components.usage,
133
- getComponentProps: components.getComponentProps,
108
+ ...componentBindings,
134
109
  }
135
110
  }
136
111
 
@@ -31,7 +31,11 @@ export interface UseCommandHistoryReturn<TCommand extends CommandHistoryCommand
31
31
  clear: () => void
32
32
  }
33
33
 
34
- /** Generic command-stack state for plugin editors with undo/redo controls. */
34
+ /**
35
+ * Generic command-stack state for plugin editors with undo/redo controls.
36
+ *
37
+ * @deprecated Unused by the MINT platform and scheduled for removal in MINT 1.3; keep command stacks plugin-local.
38
+ */
35
39
  export function useCommandHistory<TCommand extends CommandHistoryCommand = CommandHistoryCommand>(
36
40
  options: UseCommandHistoryOptions = {},
37
41
  ): UseCommandHistoryReturn<TCommand> {
@@ -0,0 +1,91 @@
1
+ import { nextTick, watch, type Ref } from 'vue'
2
+ import { useEventListener } from './useEventListener'
3
+
4
+ export interface UseFocusTrapOptions {
5
+ /** Ref to the trap's container element. Focusable elements are queried within it. */
6
+ containerRef: Ref<HTMLElement | null>
7
+ /** Reactive flag — the trap activates when this becomes `true` and releases when it becomes `false`. */
8
+ active: Ref<boolean>
9
+ /** Restore focus to the element that was focused before activation, when the trap releases. Default `true`. */
10
+ restoreFocus?: boolean
11
+ }
12
+
13
+ export interface UseFocusTrapReturn {
14
+ /** Focusable descendants of the container, in DOM order, excluding roving-tabindex (`tabindex="-1"`) elements. */
15
+ getFocusableElements: () => HTMLElement[]
16
+ }
17
+
18
+ const FOCUSABLE_SELECTOR = [
19
+ 'a[href]',
20
+ 'button:not([disabled])',
21
+ 'input:not([disabled])',
22
+ 'select:not([disabled])',
23
+ 'textarea:not([disabled])',
24
+ '[tabindex]:not([tabindex="-1"])',
25
+ ].join(', ')
26
+
27
+ /**
28
+ * Traps Tab focus within a container while `active` is true: Tab wraps from the last
29
+ * focusable element back to the first (and Shift+Tab from the first to the last), the
30
+ * first focusable element (or the container itself, if none) receives focus when the
31
+ * trap activates, and focus returns to whatever was focused beforehand when it releases.
32
+ * Used by BaseModal and WellEditPopupInternal.
33
+ */
34
+ export function useFocusTrap(options: UseFocusTrapOptions): UseFocusTrapReturn {
35
+ const { containerRef, active } = options
36
+ const restoreFocus = options.restoreFocus ?? true
37
+ let previouslyFocused: HTMLElement | null = null
38
+
39
+ function getFocusableElements(): HTMLElement[] {
40
+ if (!containerRef.value) return []
41
+ // Roving tabindex (inactive tabs/rail items) stays out of the Tab cycle. Checking
42
+ // the attribute directly (rather than the `.tabIndex` IDL property) avoids relying
43
+ // on a browser/engine's default-tabindex computation for freshly-patched elements.
44
+ return Array.from(containerRef.value.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))
45
+ .filter((el) => el.getAttribute('tabindex') !== '-1')
46
+ }
47
+
48
+ function handleKeydown(event: KeyboardEvent) {
49
+ if (event.key !== 'Tab' || !active.value || !containerRef.value) return
50
+
51
+ const focusable = getFocusableElements()
52
+ if (focusable.length === 0) {
53
+ event.preventDefault()
54
+ return
55
+ }
56
+
57
+ const first = focusable[0]
58
+ const last = focusable[focusable.length - 1]
59
+
60
+ if (event.shiftKey && document.activeElement === first) {
61
+ event.preventDefault()
62
+ last.focus()
63
+ } else if (!event.shiftKey && document.activeElement === last) {
64
+ event.preventDefault()
65
+ first.focus()
66
+ }
67
+ }
68
+
69
+ // `flush: 'sync'` so releasing the trap from `onBeforeUnmount` restores focus
70
+ // before the component's reactive scope is torn down — a batched (default
71
+ // 'pre') watcher would be scheduled but never flushed once that scope stops.
72
+ watch(active, async (isActive) => {
73
+ if (isActive) {
74
+ previouslyFocused = document.activeElement as HTMLElement | null
75
+ await nextTick()
76
+ const focusable = getFocusableElements()
77
+ if (focusable.length > 0) {
78
+ focusable[0].focus()
79
+ } else {
80
+ containerRef.value?.focus()
81
+ }
82
+ } else {
83
+ if (restoreFocus) previouslyFocused?.focus()
84
+ previouslyFocused = null
85
+ }
86
+ }, { flush: 'sync' })
87
+
88
+ useEventListener(() => document, 'keydown', handleKeydown)
89
+
90
+ return { getFocusableElements }
91
+ }
@@ -0,0 +1,90 @@
1
+ import { nextTick } from 'vue'
2
+
3
+ export interface UseMenuKeyboardOptions {
4
+ /** Returns the currently navigable items inside the open panel, in DOM order. */
5
+ items: () => HTMLElement[]
6
+ /** True while the panel is open. */
7
+ isOpen: () => boolean
8
+ /** Opens the panel. */
9
+ open: () => void
10
+ /** Closes the panel. When `returnFocus` is true, focus should return to the trigger. */
11
+ close: (returnFocus?: boolean) => void
12
+ }
13
+
14
+ export interface UseMenuKeyboardReturn {
15
+ /**
16
+ * Bind to the trigger's `@keydown`. ArrowDown/Enter/Space open the panel (when
17
+ * closed) and focus its first item; ArrowUp opens it focused on the last item.
18
+ */
19
+ handleTriggerKeydown: (event: KeyboardEvent) => void
20
+ /**
21
+ * Bind to the open panel's `@keydown`. ArrowUp/ArrowDown cycle focus with
22
+ * wraparound, Home/End jump to the first/last item, Escape closes and returns
23
+ * focus to the trigger, and Tab closes without moving focus.
24
+ */
25
+ handlePanelKeydown: (event: KeyboardEvent) => void
26
+ }
27
+
28
+ /**
29
+ * Shared keyboard contract for menu- and listbox-style disclosure panels (the
30
+ * WAI-ARIA menu button pattern). Used by AppAvatarMenu and DropdownButton; ActionMenu
31
+ * implements the same contract inline.
32
+ */
33
+ export function useMenuKeyboard(options: UseMenuKeyboardOptions): UseMenuKeyboardReturn {
34
+ function focusItem(position: 'first' | 'last') {
35
+ void nextTick(() => {
36
+ const items = options.items()
37
+ const target = position === 'first' ? items[0] : items[items.length - 1]
38
+ target?.focus()
39
+ })
40
+ }
41
+
42
+ function handleTriggerKeydown(event: KeyboardEvent) {
43
+ if (options.isOpen()) return
44
+
45
+ if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {
46
+ event.preventDefault()
47
+ options.open()
48
+ focusItem('first')
49
+ return
50
+ }
51
+ if (event.key === 'ArrowUp') {
52
+ event.preventDefault()
53
+ options.open()
54
+ focusItem('last')
55
+ }
56
+ }
57
+
58
+ function handlePanelKeydown(event: KeyboardEvent) {
59
+ const items = options.items()
60
+ const currentIndex = items.indexOf(document.activeElement as HTMLElement)
61
+
62
+ if (event.key === 'Escape') {
63
+ event.preventDefault()
64
+ options.close(true)
65
+ return
66
+ }
67
+ if (event.key === 'Tab') {
68
+ options.close()
69
+ return
70
+ }
71
+ if (event.key === 'Home') {
72
+ event.preventDefault()
73
+ items[0]?.focus()
74
+ return
75
+ }
76
+ if (event.key === 'End') {
77
+ event.preventDefault()
78
+ items[items.length - 1]?.focus()
79
+ return
80
+ }
81
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return
82
+
83
+ event.preventDefault()
84
+ const direction = event.key === 'ArrowDown' ? 1 : -1
85
+ const nextIndex = (Math.max(currentIndex, 0) + direction + items.length) % items.length
86
+ items[nextIndex]?.focus()
87
+ }
88
+
89
+ return { handleTriggerKeydown, handlePanelKeydown }
90
+ }
@@ -32,7 +32,11 @@ export interface UseOptimisticMutationReturn<TVariables, TData, TSnapshot = unkn
32
32
  reset: () => void
33
33
  }
34
34
 
35
- /** Run async mutations with optional optimistic local updates and rollback. */
35
+ /**
36
+ * Run async mutations with optional optimistic local updates and rollback.
37
+ *
38
+ * @deprecated Unused by the MINT platform and scheduled for removal in MINT 1.3; use `useRequestSyncState()` instead.
39
+ */
36
40
  export function useOptimisticMutation<TVariables, TData, TSnapshot = unknown>(
37
41
  options: UseOptimisticMutationOptions<TVariables, TData, TSnapshot>,
38
42
  ): UseOptimisticMutationReturn<TVariables, TData, TSnapshot> {
@@ -15,6 +15,7 @@ import {
15
15
  import { usePlatformContext } from './usePlatformContext'
16
16
  import { useRequestSyncState } from './useRequestSyncState'
17
17
  import { useAuthStore } from '../stores/auth'
18
+ import { PLUGIN_SECRET_FORMAT_KEY } from '../utils/pluginSecrets'
18
19
  import {
19
20
  currentExperimentFromContext,
20
21
  getInjectedPlatformContext,
@@ -545,6 +546,26 @@ export function usePluginClient<TClient>(client: TClient): TClient {
545
546
 
546
547
  const PLATFORM_OWNED_SETTINGS_KEY = 'allowed_experiment_types'
547
548
 
549
+ /**
550
+ * Secret settings travel as `{ "$secret": "<id>" }` references, so an untouched
551
+ * secret round-trips without ever exposing its value. Declaring the format on
552
+ * every write keeps a `null` unambiguous: it clears the stored secret instead of
553
+ * meaning "unchanged", which is what the deprecated placeholder form meant.
554
+ */
555
+ function withSecretFormat(
556
+ payload: Record<string, unknown>,
557
+ secretFormat: string | null,
558
+ ): Record<string, unknown> {
559
+ if (!secretFormat) return payload
560
+ return { ...payload, [PLUGIN_SECRET_FORMAT_KEY]: secretFormat }
561
+ }
562
+
563
+ function responseSecretFormat(response: unknown): string | null {
564
+ if (!response || typeof response !== 'object') return null
565
+ const format = (response as Record<string, unknown>)[PLUGIN_SECRET_FORMAT_KEY]
566
+ return typeof format === 'string' && format ? format : null
567
+ }
568
+
548
569
  function cloneRecord(value: Record<string, unknown>): Record<string, unknown> {
549
570
  return JSON.parse(JSON.stringify(value)) as Record<string, unknown>
550
571
  }
@@ -618,6 +639,7 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
618
639
  const values = ref({}) as Ref<PluginSettingsValues<TSettings>>
619
640
  const savedValues = shallowRef<Record<string, unknown>>({})
620
641
  const revision = ref<string | null>(null)
642
+ const secretFormat = ref<string | null>(null)
621
643
  const request = useRequestSyncState('Plugin settings request failed.')
622
644
  const isLoading = ref(false)
623
645
  const isSaving = ref(false)
@@ -650,11 +672,16 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
650
672
  values.value = cloneRecord(nextValues) as PluginSettingsValues<TSettings>
651
673
  }
652
674
 
653
- function applyLoaded(nextValues: Record<string, unknown>, nextRevision: string | null): void {
675
+ function applyLoaded(
676
+ nextValues: Record<string, unknown>,
677
+ nextRevision: string | null,
678
+ nextSecretFormat: string | null = secretFormat.value,
679
+ ): void {
654
680
  const cloned = cloneRecord(nextValues)
655
681
  values.value = cloned as PluginSettingsValues<TSettings>
656
682
  savedValues.value = cloneRecord(cloned)
657
683
  revision.value = nextRevision
684
+ secretFormat.value = nextSecretFormat
658
685
  }
659
686
 
660
687
  async function load(): Promise<void> {
@@ -674,6 +701,7 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
674
701
  applyLoaded(
675
702
  responseSettingsPayload(response),
676
703
  responseSettingsRevision(response),
704
+ responseSecretFormat(response),
677
705
  )
678
706
  } catch {
679
707
  // Error state is handled by request.run().
@@ -707,7 +735,12 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
707
735
  revision?: string
708
736
  }>(
709
737
  `/plugins/${encodeURIComponent(resolvedPluginName.value)}/config`,
710
- { config: topLevelSettingsPatch(payload, typedKeys) },
738
+ {
739
+ config: withSecretFormat(
740
+ topLevelSettingsPatch(payload, typedKeys),
741
+ secretFormat.value,
742
+ ),
743
+ },
711
744
  ),
712
745
  { success: 'save', errorMessage: 'Failed to save plugin settings' },
713
746
  )
@@ -730,7 +763,11 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
730
763
  }
731
764
 
732
765
  if (!response) return true
733
- applyLoaded(responseSettingsPayload(response), nextRevision)
766
+ applyLoaded(
767
+ responseSettingsPayload(response),
768
+ nextRevision,
769
+ responseSecretFormat(response) ?? secretFormat.value,
770
+ )
734
771
  return true
735
772
  } else {
736
773
  const ifMatch = revision.value
@@ -741,13 +778,14 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
741
778
  settings: Record<string, unknown>
742
779
  mode?: string
743
780
  revision?: string
744
- }>('/settings', payload, ifMatch),
781
+ }>('/settings', withSecretFormat(payload, secretFormat.value), ifMatch),
745
782
  { success: 'save', errorMessage: 'Failed to save plugin settings' },
746
783
  )
747
784
  }
748
785
  applyLoaded(
749
786
  responseSettingsPayload(response),
750
787
  responseSettingsRevision(response) ?? revision.value,
788
+ responseSecretFormat(response) ?? secretFormat.value,
751
789
  )
752
790
  return true
753
791
  } catch {