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

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 (136) 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/components/AppAvatarMenu.vue +34 -3
  80. package/src/components/BaseModal.vue +8 -56
  81. package/src/components/BaseTabs.vue +26 -1
  82. package/src/components/BioTemplateExperimentWorkspaceView.vue +10 -21
  83. package/src/components/BioTemplatePackWorkspaceView.vue +11 -15
  84. package/src/components/BioTemplatePresetWorkspaceView.vue +8 -13
  85. package/src/components/ComponentBindingRenderer.vue +1 -3
  86. package/src/components/ControlWorkspaceView.vue +11 -22
  87. package/src/components/DoseDesignWorkspaceView.vue +6 -25
  88. package/src/components/DropdownButton.vue +32 -2
  89. package/src/components/PluginWorkspaceView.props.ts +38 -0
  90. package/src/components/PluginWorkspaceView.vue +53 -224
  91. package/src/components/SecretInput.story.vue +79 -0
  92. package/src/components/SecretInput.vue +150 -0
  93. package/src/components/index.ts +9 -0
  94. package/src/components/internal/WellEditPopupInternal.vue +20 -1
  95. package/src/components/internal/WorkspaceControlsInternal.vue +20 -0
  96. package/src/components/internal/WorkspaceShellInternal.vue +125 -0
  97. package/src/components/internal/WorkspaceSidebarInternal.vue +82 -0
  98. package/src/composables/experimentDesignData.ts +1 -3
  99. package/src/composables/flattenWorkspaceBindings.ts +104 -0
  100. package/src/composables/formBuilderRegistry.ts +5 -0
  101. package/src/composables/index.ts +26 -0
  102. package/src/composables/pluginWorkspaceModel.ts +182 -0
  103. package/src/composables/useAsync.ts +5 -1
  104. package/src/composables/useBioTemplatePackWorkspace.ts +15 -9
  105. package/src/composables/useBioTemplatePresetWorkspace.ts +14 -15
  106. package/src/composables/useBioTemplateWorkspace.ts +18 -43
  107. package/src/composables/useCommandHistory.ts +5 -1
  108. package/src/composables/useFocusTrap.ts +91 -0
  109. package/src/composables/useMenuKeyboard.ts +90 -0
  110. package/src/composables/useOptimisticMutation.ts +5 -1
  111. package/src/composables/usePluginClient.ts +42 -4
  112. package/src/composables/usePluginWorkspace.ts +208 -0
  113. package/src/composables/useResourceCrud.ts +5 -1
  114. package/src/composables/useRovingFocus.ts +66 -0
  115. package/src/composables/useWellPainting.ts +5 -1
  116. package/src/composables/useWellPlateAdapter.ts +5 -1
  117. package/src/composables/useWellPlateValidation.ts +5 -1
  118. package/src/composables/workspaceRecipes.ts +267 -0
  119. package/src/index.ts +16 -0
  120. package/src/styles/components/secret-input.css +60 -0
  121. package/src/styles/index.css +1 -0
  122. package/src/templates/builderReadUtils.ts +1 -3
  123. package/src/templates/componentTargetResolvers.ts +1 -3
  124. package/src/templates/controlSchemaTargets.ts +1 -3
  125. package/src/types/form-builder.ts +1 -0
  126. package/src/utils/formModelSync.ts +1 -3
  127. package/src/utils/pluginIcon.ts +1 -3
  128. package/src/utils/pluginSecrets.ts +102 -0
  129. package/src/utils/records.ts +9 -0
  130. package/dist/SettingsModal-CkNM8qAI.js.map +0 -1
  131. package/dist/components-MGNL6tRn.js.map +0 -1
  132. package/dist/composables-CKxOcvgh.js.map +0 -1
  133. package/dist/experiment-utils-CLnzPPq5.js.map +0 -1
  134. package/dist/templates-BTJ9C9Kb.js.map +0 -1
  135. package/dist/useControlSchema-CXhxTul7.js.map +0 -1
  136. package/dist/useProtocolTemplates-BWhQIWV5.js.map +0 -1
@@ -0,0 +1,267 @@
1
+ import type { TopBarVariant } from '../types'
2
+ import type { FormEnhancements } from '../types/form-builder'
3
+ import type {
4
+ ControlModel,
5
+ ControlModelBinding,
6
+ ControlSchema,
7
+ ControlWorkspaceOptions,
8
+ UseControlWorkspaceReturn,
9
+ } from './useControlSchema'
10
+ import { mergeControlWorkspaceOptions, resolveControlModel } from './useControlSchema'
11
+
12
+ /**
13
+ * Merges `initialValues`/external `modelValue`/`values` on top of a base
14
+ * `ControlWorkspaceOptions.initialValues`, matching the layering FormBuilder expects: base
15
+ * defaults, then caller-supplied initial values, then live external values win last. Shared by
16
+ * {@link controlWorkspaceProps} and {@link bioTemplatePresetWorkspaceProps} so both "generate a
17
+ * workspace from options, initialValues, and v-model" views merge options identically.
18
+ */
19
+ export function resolveControlOptionsWithExternalValues<TOptions extends ControlWorkspaceOptions>(
20
+ base: TOptions,
21
+ initialValues: Record<string, unknown> | undefined,
22
+ externalValues: Record<string, unknown> | undefined,
23
+ ): TOptions {
24
+ if (initialValues === undefined && externalValues === undefined) return base
25
+
26
+ return {
27
+ ...base,
28
+ initialValues: {
29
+ ...(base.initialValues ?? {}),
30
+ ...(initialValues ?? {}),
31
+ ...(externalValues ?? {}),
32
+ },
33
+ }
34
+ }
35
+
36
+ export interface ControlWorkspaceModelProps {
37
+ model?: ControlModel | ControlModelBinding
38
+ controls?: ControlSchema
39
+ controlOptions?: ControlWorkspaceOptions
40
+ initialValues?: Record<string, unknown>
41
+ modelValue?: Record<string, unknown>
42
+ values?: Record<string, unknown>
43
+ }
44
+
45
+ export interface ResolvedControlWorkspaceModel {
46
+ /** `modelValue` if provided, else `values` — the single external v-model source of truth. */
47
+ externalValues: Record<string, unknown> | undefined
48
+ /** `model` resolved through `resolveControlModel()`, or undefined for a raw `controls` schema. */
49
+ resolvedModel: ControlModelBinding | undefined
50
+ /** `controls` if provided, else the resolved model's controls, else `{}`. */
51
+ resolvedControls: ControlSchema
52
+ /** `controlOptions` merged over the resolved model's own options, with initialValues/external values layered on top. */
53
+ resolvedControlOptions: ControlWorkspaceOptions
54
+ }
55
+
56
+ /**
57
+ * Resolves ControlWorkspaceView's `model`/`controls`/`controlOptions`/`initialValues`/
58
+ * `modelValue`/`values` props into the schema + options pair `useControlWorkspace()` needs.
59
+ * Pure and framework-free so callers wrap each field in their own `computed()`.
60
+ *
61
+ * @example Recommended
62
+ * ```ts
63
+ * const resolved = computed(() => controlWorkspaceProps(props))
64
+ * const resolvedControls = computed(() => resolved.value.resolvedControls)
65
+ * ```
66
+ */
67
+ export function controlWorkspaceProps(props: ControlWorkspaceModelProps): ResolvedControlWorkspaceModel {
68
+ const externalValues = props.modelValue ?? props.values
69
+ const resolvedModel = resolveControlModel(props.model)
70
+ const resolvedControls: ControlSchema = props.controls ?? resolvedModel?.controls ?? {}
71
+ const baseControlOptions = mergeControlWorkspaceOptions(resolvedModel?.controlOptions, props.controlOptions)
72
+ const resolvedControlOptions = resolveControlOptionsWithExternalValues(
73
+ baseControlOptions,
74
+ props.initialValues,
75
+ externalValues,
76
+ )
77
+
78
+ return { externalValues, resolvedModel, resolvedControls, resolvedControlOptions }
79
+ }
80
+
81
+ export interface DoseDesignWorkspacePassthroughProps {
82
+ workspace?: UseControlWorkspaceReturn<ControlSchema>
83
+ controlOptions?: ControlWorkspaceOptions
84
+ initialValues?: Record<string, unknown>
85
+ modelValue?: Record<string, unknown>
86
+ values?: Record<string, unknown>
87
+ title?: string
88
+ subtitle?: string
89
+ topBarVariant?: TopBarVariant
90
+ sidebarWidth?: string
91
+ sidebarVariant?: 'default' | 'analysis'
92
+ responsiveSidebar?: boolean
93
+ sidebarPosition?: 'left' | 'right'
94
+ sidebarTitle?: string
95
+ sidebarSubtitle?: string
96
+ sidebarBadge?: string | number
97
+ floating?: boolean
98
+ dense?: boolean
99
+ showSettings?: boolean
100
+ showFormActions?: boolean
101
+ formEnhancements?: FormEnhancements<Record<string, unknown>>
102
+ formLoading?: boolean
103
+ formDisabled?: boolean
104
+ formReadonly?: boolean
105
+ formSize?: 'sm' | 'md' | 'lg'
106
+ }
107
+
108
+ /**
109
+ * Maps DoseDesignWorkspaceView's props straight through to the identically-named
110
+ * ControlWorkspaceView props it wraps — the ~25-key pass-through list that used to be spelled
111
+ * out attribute-by-attribute in the template. `model` is the caller's already-resolved dose
112
+ * design model, not the raw (possibly absent) `props.model`.
113
+ *
114
+ * @example Recommended
115
+ * ```vue
116
+ * <ControlWorkspaceView v-bind="doseDesignWorkspaceProps(props, resolvedModel)" />
117
+ * ```
118
+ */
119
+ export function doseDesignWorkspaceProps(
120
+ props: DoseDesignWorkspacePassthroughProps,
121
+ model: ControlModel | ControlModelBinding,
122
+ ) {
123
+ return {
124
+ model,
125
+ workspace: props.workspace,
126
+ controlOptions: props.controlOptions,
127
+ initialValues: props.initialValues,
128
+ modelValue: props.modelValue,
129
+ values: props.values,
130
+ title: props.title,
131
+ subtitle: props.subtitle,
132
+ topBarVariant: props.topBarVariant,
133
+ sidebarWidth: props.sidebarWidth,
134
+ sidebarVariant: props.sidebarVariant,
135
+ responsiveSidebar: props.responsiveSidebar,
136
+ sidebarPosition: props.sidebarPosition,
137
+ sidebarTitle: props.sidebarTitle,
138
+ sidebarSubtitle: props.sidebarSubtitle,
139
+ sidebarBadge: props.sidebarBadge,
140
+ floating: props.floating,
141
+ dense: props.dense,
142
+ showSettings: props.showSettings,
143
+ showFormActions: props.showFormActions,
144
+ formEnhancements: props.formEnhancements,
145
+ formLoading: props.formLoading,
146
+ formDisabled: props.formDisabled,
147
+ formReadonly: props.formReadonly,
148
+ formSize: props.formSize,
149
+ }
150
+ }
151
+
152
+ export interface BioTemplateExperimentWorkspaceStatusProps {
153
+ status?: {
154
+ loading?: boolean
155
+ saving?: boolean
156
+ error?: string | null
157
+ hasExperiment?: boolean
158
+ currentExperimentId?: number
159
+ lastLoadedAt?: Date | null
160
+ lastSavedAt?: Date | null
161
+ }
162
+ loading?: boolean
163
+ error?: string | null
164
+ hasExperiment?: boolean
165
+ currentExperimentId?: number
166
+ lastLoadedAt?: Date | null
167
+ lastSavedAt?: Date | null
168
+ }
169
+
170
+ export interface ResolvedBioTemplateExperimentStatus {
171
+ loading: boolean
172
+ error: string | null | undefined
173
+ hasExperiment: boolean
174
+ currentExperimentId: number | undefined
175
+ lastLoadedAt: Date | null | undefined
176
+ lastSavedAt: Date | null | undefined
177
+ }
178
+
179
+ /**
180
+ * Merges BioTemplateExperimentWorkspaceView's grouped `status` prop over its individual
181
+ * `loading`/`error`/`hasExperiment`/... props: `status` wins whenever the corresponding field
182
+ * on it is set, otherwise the flat prop (or its default) is used. Centralizes the six
183
+ * independent fallback rules generated plugin pages rely on when they pass a request/experiment
184
+ * `status` object instead of wiring each field by hand.
185
+ */
186
+ export function bioTemplateExperimentWorkspaceProps(
187
+ props: BioTemplateExperimentWorkspaceStatusProps,
188
+ ): ResolvedBioTemplateExperimentStatus {
189
+ const status = props.status
190
+ return {
191
+ loading: status?.loading !== undefined || status?.saving !== undefined
192
+ ? Boolean(status?.loading) || Boolean(status?.saving)
193
+ : Boolean(props.loading),
194
+ error: status?.error !== undefined ? status.error : props.error,
195
+ hasExperiment: status?.hasExperiment ?? props.hasExperiment ?? false,
196
+ currentExperimentId: status?.currentExperimentId ?? props.currentExperimentId,
197
+ lastLoadedAt: status?.lastLoadedAt !== undefined ? status.lastLoadedAt : props.lastLoadedAt,
198
+ lastSavedAt: status?.lastSavedAt !== undefined ? status.lastSavedAt : props.lastSavedAt,
199
+ }
200
+ }
201
+
202
+ export interface BioTemplatePackWorkspacePassthroughProps {
203
+ message?: string
204
+ dense?: boolean
205
+ readonly?: boolean
206
+ showStatus?: boolean
207
+ showTemplateSummary?: boolean
208
+ loadLabel?: string
209
+ resetLabel?: string
210
+ saveLabel?: string
211
+ }
212
+
213
+ /**
214
+ * Assembles the prop object BioTemplatePackWorkspaceView binds onto the
215
+ * BioTemplateExperimentWorkspaceView it wraps: its own display props passed straight through,
216
+ * plus the pack-derived `target`/`label`/`status`/`actions` computed by the caller.
217
+ */
218
+ export function bioTemplatePackWorkspaceProps<TTarget, TStatus, TActions>(
219
+ props: BioTemplatePackWorkspacePassthroughProps,
220
+ derived: {
221
+ target: TTarget
222
+ label: string
223
+ status: TStatus
224
+ actions: TActions
225
+ },
226
+ ) {
227
+ return {
228
+ target: derived.target,
229
+ kind: 'collection' as const,
230
+ label: derived.label,
231
+ message: props.message,
232
+ status: derived.status,
233
+ actions: derived.actions,
234
+ dense: props.dense,
235
+ readonly: props.readonly,
236
+ showStatus: props.showStatus,
237
+ showTemplateSummary: props.showTemplateSummary,
238
+ loadLabel: props.loadLabel,
239
+ resetLabel: props.resetLabel,
240
+ saveLabel: props.saveLabel,
241
+ }
242
+ }
243
+
244
+ export interface BioTemplatePresetWorkspaceOptionsProps<TOptions extends ControlWorkspaceOptions> {
245
+ workspaceOptions?: TOptions
246
+ initialValues?: Record<string, unknown>
247
+ modelValue?: Record<string, unknown>
248
+ values?: Record<string, unknown>
249
+ }
250
+
251
+ /**
252
+ * Resolves BioTemplatePresetWorkspaceView's `workspaceOptions`/`initialValues`/`modelValue`/
253
+ * `values` props into the single options object `useBioTemplatePresetWorkspace()` needs,
254
+ * reusing the same base/initialValues/external-values layering as {@link controlWorkspaceProps}.
255
+ */
256
+ export function bioTemplatePresetWorkspaceProps<TOptions extends ControlWorkspaceOptions>(
257
+ props: BioTemplatePresetWorkspaceOptionsProps<TOptions>,
258
+ ): { externalValues: Record<string, unknown> | undefined; resolvedWorkspaceOptions: TOptions } {
259
+ const externalValues = props.modelValue ?? props.values
260
+ const resolvedWorkspaceOptions = resolveControlOptionsWithExternalValues(
261
+ (props.workspaceOptions ?? {}) as TOptions,
262
+ props.initialValues,
263
+ externalValues,
264
+ )
265
+
266
+ return { externalValues, resolvedWorkspaceOptions }
267
+ }
package/src/index.ts CHANGED
@@ -41,6 +41,22 @@ export {
41
41
  deriveShade,
42
42
  type Hsl,
43
43
  } from './utils/color'
44
+ export {
45
+ PLUGIN_SECRET_FORMAT_KEY,
46
+ PLUGIN_SECRET_FORMAT_REF,
47
+ PLUGIN_SECRET_REF_KEY,
48
+ clearPluginSecret,
49
+ isPluginSecretLocked,
50
+ isPluginSecretRef,
51
+ keepPluginSecret,
52
+ pluginSecretId,
53
+ pluginSecretLabel,
54
+ pluginSecretState,
55
+ setPluginSecret,
56
+ usesPluginSecretReferences,
57
+ type PluginSecretRef,
58
+ type PluginSecretState,
59
+ } from './utils/pluginSecrets'
44
60
  export {
45
61
  LCMS_DEFAULT_CONTROL_POSITIONS,
46
62
  createLcmsControlWellEditData,
@@ -0,0 +1,60 @@
1
+ /* SecretInput — plugin secret settings field with a set / not set / environment badge. */
2
+
3
+ .mint-secret-input {
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: 0.5rem;
7
+ }
8
+
9
+ .mint-secret-input__status {
10
+ display: flex;
11
+ align-items: center;
12
+ gap: 0.5rem;
13
+ flex-wrap: wrap;
14
+ }
15
+
16
+ .mint-secret-input__badge {
17
+ display: inline-flex;
18
+ align-items: center;
19
+ border-radius: 999px;
20
+ padding: 0.125rem 0.5rem;
21
+ font-size: 0.75rem;
22
+ font-weight: 500;
23
+ line-height: 1.4;
24
+ border: 1px solid transparent;
25
+ background-color: var(--bg-tertiary);
26
+ color: var(--text-secondary);
27
+ }
28
+
29
+ .mint-secret-input__badge--set {
30
+ background-color: color-mix(in srgb, var(--mint-success) 12%, transparent);
31
+ border-color: color-mix(in srgb, var(--mint-success) 35%, transparent);
32
+ color: var(--mint-success);
33
+ }
34
+
35
+ .mint-secret-input__badge--environment {
36
+ background-color: color-mix(in srgb, var(--mint-info) 12%, transparent);
37
+ border-color: color-mix(in srgb, var(--mint-info) 35%, transparent);
38
+ color: var(--mint-info);
39
+ }
40
+
41
+ .mint-secret-input__badge--pending {
42
+ background-color: color-mix(in srgb, var(--mint-warning) 12%, transparent);
43
+ border-color: color-mix(in srgb, var(--mint-warning) 35%, transparent);
44
+ color: var(--mint-warning);
45
+ }
46
+
47
+ .mint-secret-input__hint {
48
+ font-size: 0.75rem;
49
+ color: var(--text-muted);
50
+ }
51
+
52
+ .mint-secret-input__actions {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: 0.5rem;
56
+ }
57
+
58
+ .mint-secret-input--error .mint-secret-input__badge {
59
+ border-color: color-mix(in srgb, var(--mint-error) 45%, transparent);
60
+ }
@@ -30,6 +30,7 @@
30
30
  @import './components/radio-group.css';
31
31
  @import './components/sample-legend.css';
32
32
  @import './components/section-card.css';
33
+ @import './components/secret-input.css';
33
34
  @import './components/select.css';
34
35
  @import './components/skeleton.css';
35
36
  @import './components/slider.css';
@@ -1,6 +1,4 @@
1
- export function isRecord(value: unknown): value is Record<string, unknown> {
2
- return typeof value === 'object' && value !== null && !Array.isArray(value)
3
- }
1
+ export { isRecord } from '../utils/records'
4
2
 
5
3
  export function readStringList(value: unknown, fallback: string[]): string[] {
6
4
  if (Array.isArray(value)) {
@@ -1,3 +1,4 @@
1
+ import { isObjectLike as isRecord } from '../utils/records'
1
2
  import { extractTemplateCollection } from './templateEnvelopes'
2
3
  import { getBioTemplateInfo } from './catalog'
3
4
  import { getBioTemplatePackInfo } from './packs'
@@ -64,6 +65,3 @@ function collectionPackName(value: unknown): string | undefined {
64
65
  return typeof value.metadata.pack === 'string' ? value.metadata.pack : undefined
65
66
  }
66
67
 
67
- function isRecord(value: unknown): value is Record<string, unknown> {
68
- return typeof value === 'object' && value !== null
69
- }
@@ -1,3 +1,4 @@
1
+ import { isObjectLike as isRecord } from '../utils/records'
1
2
  import {
2
3
  getBioTemplateInfo,
3
4
  } from './catalog'
@@ -82,6 +83,3 @@ function collectionPackName(value: unknown): string | undefined {
82
83
  return typeof value.metadata.pack === 'string' ? value.metadata.pack : undefined
83
84
  }
84
85
 
85
- function isRecord(value: unknown): value is Record<string, unknown> {
86
- return typeof value === 'object' && value !== null
87
- }
@@ -11,6 +11,7 @@ export type FormFieldType =
11
11
  | 'text'
12
12
  | 'email'
13
13
  | 'password'
14
+ | 'secret'
14
15
  | 'number'
15
16
  | 'tel'
16
17
  | 'url'
@@ -1,4 +1,5 @@
1
1
  import type { FormSchema } from '../types/form-builder'
2
+ import { isRecord } from './records'
2
3
 
3
4
  export function recordValuesEqualForKeys(
4
5
  left: Record<string, unknown>,
@@ -47,6 +48,3 @@ function valuesEqual(left: unknown, right: unknown): boolean {
47
48
  return false
48
49
  }
49
50
 
50
- function isRecord(value: unknown): value is Record<string, unknown> {
51
- return typeof value === 'object' && value !== null && !Array.isArray(value)
52
- }
@@ -1,3 +1,4 @@
1
+ import { isRecord } from './records'
1
2
  export type PluginIconFormat = 'path' | 'data-url' | 'asset-url' | 'structured' | 'fallback'
2
3
 
3
4
  export interface StructuredPluginIconStop {
@@ -177,9 +178,6 @@ function parseOffset(value: unknown): string | undefined {
177
178
  return undefined
178
179
  }
179
180
 
180
- function isRecord(value: unknown): value is Record<string, unknown> {
181
- return typeof value === 'object' && value !== null && !Array.isArray(value)
182
- }
183
181
 
184
182
  function isSafeColor(value: unknown): value is string {
185
183
  return typeof value === 'string' && COLOR_REGEX.test(value)
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Stable references for plugin secret settings.
3
+ *
4
+ * A settings response never carries a secret's value. Every secret leaf is a
5
+ * marker instead:
6
+ *
7
+ * - `{ "$secret": "sec_…", "set": true }` — a stored secret, referenced by a
8
+ * stable opaque id. Send the marker back unchanged to keep the stored value.
9
+ * - `null` — nothing stored at that position.
10
+ * - `{ "$secret": null, "locked": true }` — the effective value comes from the
11
+ * environment and is never written back.
12
+ *
13
+ * On save each secret leaf accepts the reference, a plain string (a new value),
14
+ * or `null` (clear). Payloads carry `secret_format: "ref"` so a `null` reads as
15
+ * a deliberate clear rather than the deprecated "unchanged" placeholder.
16
+ */
17
+
18
+ /** Reserved marker key that references a stored secret. */
19
+ export const PLUGIN_SECRET_REF_KEY = '$secret'
20
+
21
+ /** Reserved payload key declaring which secret wire format a client speaks. */
22
+ export const PLUGIN_SECRET_FORMAT_KEY = 'secret_format'
23
+
24
+ /** Wire format published and understood by this SDK. */
25
+ export const PLUGIN_SECRET_FORMAT_REF = 'ref'
26
+
27
+ /** One `$secret` marker as published by a settings response. */
28
+ export interface PluginSecretRef {
29
+ $secret: string | null
30
+ set?: boolean
31
+ locked?: boolean
32
+ }
33
+
34
+ /** What a settings form should tell the user about one secret field. */
35
+ export type PluginSecretState = 'set' | 'unset' | 'environment'
36
+
37
+ const MARKER_KEYS = new Set([PLUGIN_SECRET_REF_KEY, 'set', 'locked'])
38
+
39
+ /** Return whether one settings value is a `$secret` reference marker. */
40
+ export function isPluginSecretRef(value: unknown): value is PluginSecretRef {
41
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false
42
+ const record = value as Record<string, unknown>
43
+ if (!(PLUGIN_SECRET_REF_KEY in record)) return false
44
+ return Object.keys(record).every(key => MARKER_KEYS.has(key))
45
+ }
46
+
47
+ /** Return the stored secret id behind a marker, or `null` when nothing is stored. */
48
+ export function pluginSecretId(value: unknown): string | null {
49
+ if (!isPluginSecretRef(value)) return null
50
+ return typeof value.$secret === 'string' ? value.$secret : null
51
+ }
52
+
53
+ /** Classify one secret field for display: stored, unset, or environment-provided. */
54
+ export function pluginSecretState(value: unknown): PluginSecretState {
55
+ if (isPluginSecretRef(value)) {
56
+ if (value.locked) return 'environment'
57
+ return typeof value.$secret === 'string' ? 'set' : 'unset'
58
+ }
59
+ return typeof value === 'string' && value.length > 0 ? 'set' : 'unset'
60
+ }
61
+
62
+ /** Human label for one secret field's state. */
63
+ export function pluginSecretLabel(value: unknown): string {
64
+ const state = pluginSecretState(value)
65
+ if (state === 'environment') return 'Provided by environment'
66
+ return state === 'set' ? 'Set' : 'Not set'
67
+ }
68
+
69
+ /** Whether a secret field is read-only because the environment supplies it. */
70
+ export function isPluginSecretLocked(value: unknown): boolean {
71
+ return isPluginSecretRef(value) && value.locked === true
72
+ }
73
+
74
+ /**
75
+ * Value to submit for an untouched secret field: the reference, unchanged.
76
+ *
77
+ * A locked marker round-trips too, which is how the platform keeps a stored
78
+ * value that an environment variable currently hides.
79
+ */
80
+ export function keepPluginSecret(current: unknown): PluginSecretRef | null {
81
+ if (isPluginSecretRef(current)) return { ...current }
82
+ return null
83
+ }
84
+
85
+ /** Value to submit when the user typed a new secret. */
86
+ export function setPluginSecret(value: string): string {
87
+ return value
88
+ }
89
+
90
+ /** Value to submit when the user cleared a secret. */
91
+ export function clearPluginSecret(): null {
92
+ return null
93
+ }
94
+
95
+ /** Whether a settings response declares the `$secret` reference wire format. */
96
+ export function usesPluginSecretReferences(response: unknown): boolean {
97
+ if (!response || typeof response !== 'object') return false
98
+ return (
99
+ (response as Record<string, unknown>)[PLUGIN_SECRET_FORMAT_KEY]
100
+ === PLUGIN_SECRET_FORMAT_REF
101
+ )
102
+ }
@@ -0,0 +1,9 @@
1
+ /** Narrow an unknown value to a plain object (arrays and null excluded). */
2
+ export function isRecord(value: unknown): value is Record<string, unknown> {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
4
+ }
5
+
6
+ /** Narrow an unknown value to any non-null object, arrays included. */
7
+ export function isObjectLike(value: unknown): value is Record<string, unknown> {
8
+ return typeof value === 'object' && value !== null
9
+ }