@tai42/studio-sdk 25.0.1 → 26.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -26
- package/dist/components/action-menu.d.ts +30 -0
- package/dist/components/action-menu.d.ts.map +1 -0
- package/dist/components/action-menu.js +101 -0
- package/dist/components/action-menu.js.map +1 -0
- package/dist/components/agent-picker.d.ts +22 -0
- package/dist/components/agent-picker.d.ts.map +1 -0
- package/dist/components/agent-picker.js +17 -0
- package/dist/components/agent-picker.js.map +1 -0
- package/dist/components/channel-picker.d.ts +27 -0
- package/dist/components/channel-picker.d.ts.map +1 -0
- package/dist/components/channel-picker.js +26 -0
- package/dist/components/channel-picker.js.map +1 -0
- package/dist/components/components.css +226 -8
- package/dist/components/confirm-dialog.d.ts +3 -1
- package/dist/components/confirm-dialog.d.ts.map +1 -1
- package/dist/components/confirm-dialog.js +2 -2
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/execution-key-picker.d.ts +26 -10
- package/dist/components/execution-key-picker.d.ts.map +1 -1
- package/dist/components/execution-key-picker.js +134 -15
- package/dist/components/execution-key-picker.js.map +1 -1
- package/dist/components/extension-combo-builder.d.ts +7 -7
- package/dist/components/extension-combo-builder.d.ts.map +1 -1
- package/dist/components/extension-combo-builder.js +42 -16
- package/dist/components/extension-combo-builder.js.map +1 -1
- package/dist/components/extension-combo-parts.d.ts +13 -11
- package/dist/components/extension-combo-parts.d.ts.map +1 -1
- package/dist/components/extension-combo-parts.js +10 -19
- package/dist/components/extension-combo-parts.js.map +1 -1
- package/dist/components/extension-config-editor.d.ts +33 -0
- package/dist/components/extension-config-editor.d.ts.map +1 -0
- package/dist/components/extension-config-editor.js +190 -0
- package/dist/components/extension-config-editor.js.map +1 -0
- package/dist/components/extension-grouping.d.ts +4 -41
- package/dist/components/extension-grouping.d.ts.map +1 -1
- package/dist/components/extension-grouping.js +1 -48
- package/dist/components/extension-grouping.js.map +1 -1
- package/dist/components/extension-picker.d.ts +4 -3
- package/dist/components/extension-picker.d.ts.map +1 -1
- package/dist/components/extension-picker.js +6 -6
- package/dist/components/extension-picker.js.map +1 -1
- package/dist/components/fleet-report.d.ts +8 -3
- package/dist/components/fleet-report.d.ts.map +1 -1
- package/dist/components/fleet-report.js +12 -2
- package/dist/components/fleet-report.js.map +1 -1
- package/dist/components/holder-kinds.d.ts +18 -0
- package/dist/components/holder-kinds.d.ts.map +1 -0
- package/dist/components/holder-kinds.js +41 -0
- package/dist/components/holder-kinds.js.map +1 -0
- package/dist/components/holder-list.d.ts +58 -0
- package/dist/components/holder-list.d.ts.map +1 -0
- package/dist/components/holder-list.js +77 -0
- package/dist/components/holder-list.js.map +1 -0
- package/dist/components/icons/index.d.ts +1 -1
- package/dist/components/icons/index.d.ts.map +1 -1
- package/dist/components/icons/index.js +1 -1
- package/dist/components/icons/index.js.map +1 -1
- package/dist/components/icons/nav-map.d.ts.map +1 -1
- package/dist/components/icons/nav-map.js +1 -3
- package/dist/components/icons/nav-map.js.map +1 -1
- package/dist/components/icons/nav-marks.d.ts +0 -4
- package/dist/components/icons/nav-marks.d.ts.map +1 -1
- package/dist/components/icons/nav-marks.js +0 -4
- package/dist/components/icons/nav-marks.js.map +1 -1
- package/dist/components/named-list-picker.d.ts +39 -0
- package/dist/components/named-list-picker.d.ts.map +1 -0
- package/dist/components/named-list-picker.js +61 -0
- package/dist/components/named-list-picker.js.map +1 -0
- package/dist/components/named-list-test-utils.d.ts +23 -0
- package/dist/components/named-list-test-utils.d.ts.map +1 -0
- package/dist/components/named-list-test-utils.js +33 -0
- package/dist/components/named-list-test-utils.js.map +1 -0
- package/dist/components/recycle-roll.d.ts +23 -0
- package/dist/components/recycle-roll.d.ts.map +1 -0
- package/dist/components/recycle-roll.js +47 -0
- package/dist/components/recycle-roll.js.map +1 -0
- package/dist/components/stepper.d.ts +28 -0
- package/dist/components/stepper.d.ts.map +1 -0
- package/dist/components/stepper.js +33 -0
- package/dist/components/stepper.js.map +1 -0
- package/dist/components/subject-section.d.ts +2 -1
- package/dist/components/subject-section.d.ts.map +1 -1
- package/dist/components/subject-section.js +10 -5
- package/dist/components/subject-section.js.map +1 -1
- package/dist/components/tokens.css +31 -41
- package/dist/components/tokens.d.ts +3 -4
- package/dist/components/tokens.d.ts.map +1 -1
- package/dist/components/tokens.js +5 -13
- package/dist/components/tokens.js.map +1 -1
- package/dist/components/tool-picker-parts.d.ts +5 -1
- package/dist/components/tool-picker-parts.d.ts.map +1 -1
- package/dist/components/tool-picker-parts.js +8 -6
- package/dist/components/tool-picker-parts.js.map +1 -1
- package/dist/components/tool-picker.d.ts +77 -2
- package/dist/components/tool-picker.d.ts.map +1 -1
- package/dist/components/tool-picker.js +87 -29
- package/dist/components/tool-picker.js.map +1 -1
- package/dist/components/version-history-panel.d.ts +7 -1
- package/dist/components/version-history-panel.d.ts.map +1 -1
- package/dist/components/version-history-panel.js +2 -2
- package/dist/components/version-history-panel.js.map +1 -1
- package/dist/components/version-history-rows.d.ts +4 -2
- package/dist/components/version-history-rows.d.ts.map +1 -1
- package/dist/components/version-history-rows.js +14 -2
- package/dist/components/version-history-rows.js.map +1 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +7 -0
- package/dist/errors.js.map +1 -1
- package/dist/hooks/useApi.d.ts +524 -197
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/hooks/useCapabilities.d.ts +43 -10
- package/dist/hooks/useCapabilities.d.ts.map +1 -1
- package/dist/hooks/useCapabilities.js +39 -10
- package/dist/hooks/useCapabilities.js.map +1 -1
- package/dist/index.d.ts +25 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -6
- package/dist/index.js.map +1 -1
- package/dist/marketplace-install/env-var-fields.d.ts +17 -0
- package/dist/marketplace-install/env-var-fields.d.ts.map +1 -0
- package/dist/marketplace-install/env-var-fields.js +22 -0
- package/dist/marketplace-install/env-var-fields.js.map +1 -0
- package/dist/marketplace-install/index.d.ts +18 -0
- package/dist/marketplace-install/index.d.ts.map +1 -0
- package/dist/marketplace-install/index.js +8 -0
- package/dist/marketplace-install/index.js.map +1 -0
- package/dist/marketplace-install/install-env.d.ts +21 -0
- package/dist/marketplace-install/install-env.d.ts.map +1 -0
- package/dist/marketplace-install/install-env.js +22 -0
- package/dist/marketplace-install/install-env.js.map +1 -0
- package/dist/marketplace-install/install-mount.d.ts +42 -0
- package/dist/marketplace-install/install-mount.d.ts.map +1 -0
- package/dist/marketplace-install/install-mount.js +62 -0
- package/dist/marketplace-install/install-mount.js.map +1 -0
- package/dist/marketplace-install/install-report.d.ts +14 -0
- package/dist/marketplace-install/install-report.d.ts.map +1 -0
- package/dist/marketplace-install/install-report.js +19 -0
- package/dist/marketplace-install/install-report.js.map +1 -0
- package/dist/marketplace-install/install-shell.d.ts +34 -0
- package/dist/marketplace-install/install-shell.d.ts.map +1 -0
- package/dist/marketplace-install/install-shell.js +32 -0
- package/dist/marketplace-install/install-shell.js.map +1 -0
- package/dist/marketplace-install/mount-install.d.ts +41 -0
- package/dist/marketplace-install/mount-install.d.ts.map +1 -0
- package/dist/marketplace-install/mount-install.js +90 -0
- package/dist/marketplace-install/mount-install.js.map +1 -0
- package/dist/marketplace-install/plugin-detail.d.ts +17 -0
- package/dist/marketplace-install/plugin-detail.d.ts.map +1 -0
- package/dist/marketplace-install/plugin-detail.js +41 -0
- package/dist/marketplace-install/plugin-detail.js.map +1 -0
- package/dist/marketplace-install/plugin-install.d.ts +27 -0
- package/dist/marketplace-install/plugin-install.d.ts.map +1 -0
- package/dist/marketplace-install/plugin-install.js +89 -0
- package/dist/marketplace-install/plugin-install.js.map +1 -0
- package/dist/marketplace-install/route-items.d.ts +14 -0
- package/dist/marketplace-install/route-items.d.ts.map +1 -0
- package/dist/marketplace-install/route-items.js +11 -0
- package/dist/marketplace-install/route-items.js.map +1 -0
- package/dist/marketplace-install/use-debounced-value.d.ts +6 -0
- package/dist/marketplace-install/use-debounced-value.d.ts.map +1 -0
- package/dist/marketplace-install/use-debounced-value.js +18 -0
- package/dist/marketplace-install/use-debounced-value.js.map +1 -0
- package/dist/marketplace-install/use-install-action.d.ts +26 -0
- package/dist/marketplace-install/use-install-action.d.ts.map +1 -0
- package/dist/marketplace-install/use-install-action.js +35 -0
- package/dist/marketplace-install/use-install-action.js.map +1 -0
- package/dist/marketplace-install/use-install-preview.d.ts +33 -0
- package/dist/marketplace-install/use-install-preview.d.ts.map +1 -0
- package/dist/marketplace-install/use-install-preview.js +44 -0
- package/dist/marketplace-install/use-install-preview.js.map +1 -0
- package/dist/marketplace-install/use-mount-install.d.ts +80 -0
- package/dist/marketplace-install/use-mount-install.d.ts.map +1 -0
- package/dist/marketplace-install/use-mount-install.js +75 -0
- package/dist/marketplace-install/use-mount-install.js.map +1 -0
- package/dist/navigation/index.d.ts +1 -1
- package/dist/navigation/index.d.ts.map +1 -1
- package/dist/navigation/index.js.map +1 -1
- package/dist/navigation/types.d.ts +23 -7
- package/dist/navigation/types.d.ts.map +1 -1
- package/dist/plugin/registry.d.ts +0 -8
- package/dist/plugin/registry.d.ts.map +1 -1
- package/dist/plugin/registry.js +25 -1
- package/dist/plugin/registry.js.map +1 -1
- package/dist/plugin/types.d.ts +36 -12
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/version.d.ts +6 -4
- package/dist/plugin/version.d.ts.map +1 -1
- package/dist/plugin/version.js +6 -4
- package/dist/plugin/version.js.map +1 -1
- package/dist/query-keys.d.ts +45 -4
- package/dist/query-keys.d.ts.map +1 -1
- package/dist/query-keys.js +52 -4
- package/dist/query-keys.js.map +1 -1
- package/dist/schema-form/json-schema-meta.d.ts +3 -0
- package/dist/schema-form/json-schema-meta.d.ts.map +1 -0
- package/dist/schema-form/json-schema-meta.js +3 -0
- package/dist/schema-form/json-schema-meta.js.map +1 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +2 -0
- package/dist/testing.js.map +1 -1
- package/dist/token-rules.d.ts +26 -0
- package/dist/token-rules.d.ts.map +1 -0
- package/dist/token-rules.js +64 -0
- package/dist/token-rules.js.map +1 -0
- package/package.json +2 -2
- package/src/components/action-menu.tsx +160 -0
- package/src/components/agent-picker.tsx +53 -0
- package/src/components/channel-picker.tsx +68 -0
- package/src/components/components.css +226 -8
- package/src/components/confirm-dialog.tsx +4 -1
- package/src/components/execution-key-picker.tsx +252 -36
- package/src/components/extension-combo-builder.tsx +61 -29
- package/src/components/extension-combo-parts.tsx +28 -38
- package/src/components/extension-config-editor.tsx +307 -0
- package/src/components/extension-grouping.ts +4 -73
- package/src/components/extension-picker.tsx +13 -13
- package/src/components/fleet-report.tsx +38 -4
- package/src/components/holder-kinds.ts +60 -0
- package/src/components/holder-list.tsx +163 -0
- package/src/components/icons/index.ts +0 -2
- package/src/components/icons/nav-map.ts +0 -4
- package/src/components/icons/nav-marks.tsx +0 -15
- package/src/components/named-list-picker.tsx +182 -0
- package/src/components/named-list-test-utils.tsx +47 -0
- package/src/components/recycle-roll.tsx +120 -0
- package/src/components/stepper.tsx +111 -0
- package/src/components/subject-section.tsx +10 -5
- package/src/components/tokens.css +31 -41
- package/src/components/tokens.ts +5 -13
- package/src/components/tool-picker-parts.tsx +17 -6
- package/src/components/tool-picker.tsx +215 -41
- package/src/components/version-history-panel.tsx +8 -0
- package/src/components/version-history-rows.tsx +14 -2
- package/src/errors.ts +10 -0
- package/src/hooks/useCapabilities.ts +59 -10
- package/src/index.ts +54 -14
- package/src/marketplace-install/env-var-fields.tsx +59 -0
- package/src/marketplace-install/index.ts +17 -0
- package/src/marketplace-install/install-env.ts +39 -0
- package/src/marketplace-install/install-mount.ts +96 -0
- package/src/marketplace-install/install-report.tsx +54 -0
- package/src/marketplace-install/install-shell.tsx +99 -0
- package/src/marketplace-install/mount-install.tsx +290 -0
- package/src/marketplace-install/plugin-detail.ts +60 -0
- package/src/marketplace-install/plugin-install.tsx +221 -0
- package/src/marketplace-install/route-items.ts +20 -0
- package/src/marketplace-install/use-debounced-value.ts +18 -0
- package/src/marketplace-install/use-install-action.ts +60 -0
- package/src/marketplace-install/use-install-preview.ts +74 -0
- package/src/marketplace-install/use-mount-install.ts +116 -0
- package/src/navigation/index.ts +1 -0
- package/src/navigation/types.ts +27 -7
- package/src/plugin/registry.ts +19 -1
- package/src/plugin/types.ts +40 -12
- package/src/plugin/version.ts +6 -4
- package/src/query-keys.ts +71 -4
- package/src/schema-form/json-schema-meta.ts +2 -0
- package/src/testing.ts +2 -0
- package/src/token-rules.ts +66 -0
|
@@ -27,14 +27,19 @@ export interface SubjectTargetOption {
|
|
|
27
27
|
/** The message a partially-filled subject is refused with (all three fields or none). */
|
|
28
28
|
export const SUBJECT_INCOMPLETE_MESSAGE = 'A subject needs a target, a kind, and a key.';
|
|
29
29
|
|
|
30
|
-
/** Map the conversation routes read to the target select's options
|
|
30
|
+
/** Map the conversation routes read to the target select's options: one option per
|
|
31
|
+
* distinct target, in first-seen order, since several routes may share a target. */
|
|
31
32
|
export function toSubjectTargetOptions(
|
|
32
33
|
routes: readonly { readonly target_kind: string; readonly target_name: string }[],
|
|
33
34
|
): SubjectTargetOption[] {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
const byValue = new Map<string, SubjectTargetOption>();
|
|
36
|
+
for (const route of routes) {
|
|
37
|
+
const value = `${route.target_kind}:${route.target_name}`;
|
|
38
|
+
if (!byValue.has(value)) {
|
|
39
|
+
byValue.set(value, { value, label: `${route.target_kind} · ${route.target_name}` });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return [...byValue.values()];
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
/**
|
|
@@ -147,11 +147,14 @@
|
|
|
147
147
|
/* -- Semantic ----------------------------------------------------------
|
|
148
148
|
The `-text` members are the ONLY ones legible as text, dots, or icons;
|
|
149
149
|
the `-fill` members are for fills and charts and carry `on-fill` ink
|
|
150
|
-
labels in both themes.
|
|
151
|
-
|
|
150
|
+
labels in both themes. A badge label is a `-text` member over its own
|
|
151
|
+
tint, and that tint can sit on a row that is tinted too (a hovered or
|
|
152
|
+
highlighted row, a diff row), so the values are set to clear 4.5:1 there:
|
|
153
|
+
`badge-contrast.test.tsx` computes every such stack in both themes. */
|
|
154
|
+
--tai-color-ok-text: #065f46;
|
|
152
155
|
--tai-dark-color-ok-text: #34d399;
|
|
153
156
|
--tai-color-err-text: #b91c1c;
|
|
154
|
-
--tai-dark-color-err-text: #
|
|
157
|
+
--tai-dark-color-err-text: #fca5a5;
|
|
155
158
|
--tai-color-warn-text: #92400e;
|
|
156
159
|
--tai-dark-color-warn-text: #fbbf24;
|
|
157
160
|
--tai-color-ok-fill: #10b981;
|
|
@@ -196,30 +199,22 @@
|
|
|
196
199
|
--tai-color-syntax-number: var(--tai-color-accent);
|
|
197
200
|
--tai-color-syntax-bool: var(--tai-color-warn-text);
|
|
198
201
|
|
|
199
|
-
/* --
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
--tai-color-
|
|
213
|
-
--tai-color-
|
|
214
|
-
--tai-color-
|
|
215
|
-
--tai-dark-color-danger: #dc2626;
|
|
216
|
-
--tai-color-danger-text: #ffffff;
|
|
217
|
-
--tai-dark-color-danger-text: #ffffff;
|
|
218
|
-
--tai-color-danger-hover: #7f1d1d;
|
|
219
|
-
--tai-dark-color-danger-hover: #b91c1c;
|
|
220
|
-
--tai-color-danger-surface: var(--tai-color-err-tint);
|
|
221
|
-
--tai-color-success: var(--tai-color-ok-text);
|
|
222
|
-
--tai-color-warning: var(--tai-color-warn-text);
|
|
202
|
+
/* -- Destructive emphasis ---------------------------------------------
|
|
203
|
+
A FILL for the destructive button only, never text: as text the dark
|
|
204
|
+
fill fails contrast on the dark grounds, so error text takes `err-text`.
|
|
205
|
+
It runs deeper than the accent on purpose: a destructive fill sharing the
|
|
206
|
+
crimson brand ramp reads as the primary action, so this is its own deep
|
|
207
|
+
red, clearly darker and less pink than the accent in BOTH themes, unlike
|
|
208
|
+
the light-fill dark-ink convention the accent follows. `on-destructive` is
|
|
209
|
+
the white label on that fill in both themes, and the hover is a shade
|
|
210
|
+
deeper still, so a destructive button is instantly told apart from a
|
|
211
|
+
primary one. */
|
|
212
|
+
--tai-color-destructive: #991b1b;
|
|
213
|
+
--tai-dark-color-destructive: #dc2626;
|
|
214
|
+
--tai-color-destructive-hover: #7f1d1d;
|
|
215
|
+
--tai-dark-color-destructive-hover: #b91c1c;
|
|
216
|
+
--tai-color-on-destructive: #ffffff;
|
|
217
|
+
--tai-dark-color-on-destructive: #ffffff;
|
|
223
218
|
|
|
224
219
|
/* -- Spacing (n x 4; there is deliberately no space-7) ------------------ */
|
|
225
220
|
--tai-space-1: 0.25rem; /* 4px */
|
|
@@ -272,11 +267,6 @@
|
|
|
272
267
|
--tai-dark-shadow-overlay-color: rgb(0 0 0 / 0.6);
|
|
273
268
|
--tai-shadow-lift: 0 12px 32px var(--tai-shadow-lift-color);
|
|
274
269
|
--tai-shadow-overlay: 0 24px 48px var(--tai-shadow-overlay-color);
|
|
275
|
-
/* Legacy slots, re-pointed at the new elevation pair. Both names are kept
|
|
276
|
-
and both changed MEANING: they were resting shadows (`0 1px 2px` /
|
|
277
|
-
`0 4px 12px`) and now alias the lift and overlay geometry. */
|
|
278
|
-
--tai-shadow-sm: var(--tai-shadow-lift);
|
|
279
|
-
--tai-shadow-md: var(--tai-shadow-overlay);
|
|
280
270
|
|
|
281
271
|
/* -- Stacking ------------------------------------------------------------
|
|
282
272
|
`popover` and `tooltip` sit ABOVE the modal layers on purpose. Both are
|
|
@@ -284,9 +274,9 @@
|
|
|
284
274
|
be opened from whatever layer is on top — so a listbox or a tip raised
|
|
285
275
|
from inside a dialog has to clear that dialog's scrim to be usable.
|
|
286
276
|
`dropdown` is the rung reserved for an in-flow menu surface — one that
|
|
287
|
-
stacks against the page rather than being portalled.
|
|
288
|
-
|
|
289
|
-
rung to sit on instead of inventing a number. */
|
|
277
|
+
stacks against the page rather than being portalled. `ActionMenu`'s list
|
|
278
|
+
sits on it, and it is part of the published scale so a plugin's own menu
|
|
279
|
+
has a rung to sit on instead of inventing a number. */
|
|
290
280
|
--tai-z-sticky: 10;
|
|
291
281
|
--tai-z-dropdown: 20;
|
|
292
282
|
--tai-z-overlay: 30;
|
|
@@ -335,9 +325,9 @@
|
|
|
335
325
|
--tai-color-scrim: var(--tai-dark-color-scrim);
|
|
336
326
|
--tai-color-prose-link: var(--tai-dark-color-prose-link);
|
|
337
327
|
--tai-color-prose-link-hover: var(--tai-dark-color-prose-link-hover);
|
|
338
|
-
--tai-color-
|
|
339
|
-
--tai-color-
|
|
340
|
-
--tai-color-
|
|
328
|
+
--tai-color-destructive: var(--tai-dark-color-destructive);
|
|
329
|
+
--tai-color-destructive-hover: var(--tai-dark-color-destructive-hover);
|
|
330
|
+
--tai-color-on-destructive: var(--tai-dark-color-on-destructive);
|
|
341
331
|
--tai-shadow-lift-color: var(--tai-dark-shadow-lift-color);
|
|
342
332
|
--tai-shadow-overlay-color: var(--tai-dark-shadow-overlay-color);
|
|
343
333
|
}
|
|
@@ -382,9 +372,9 @@
|
|
|
382
372
|
--tai-color-scrim: var(--tai-dark-color-scrim);
|
|
383
373
|
--tai-color-prose-link: var(--tai-dark-color-prose-link);
|
|
384
374
|
--tai-color-prose-link-hover: var(--tai-dark-color-prose-link-hover);
|
|
385
|
-
--tai-color-
|
|
386
|
-
--tai-color-
|
|
387
|
-
--tai-color-
|
|
375
|
+
--tai-color-destructive: var(--tai-dark-color-destructive);
|
|
376
|
+
--tai-color-destructive-hover: var(--tai-dark-color-destructive-hover);
|
|
377
|
+
--tai-color-on-destructive: var(--tai-dark-color-on-destructive);
|
|
388
378
|
--tai-shadow-lift-color: var(--tai-dark-shadow-lift-color);
|
|
389
379
|
--tai-shadow-overlay-color: var(--tai-dark-shadow-overlay-color);
|
|
390
380
|
}
|
package/src/components/tokens.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The documented CSS-variable token names — the Studio-plugin styling API. A
|
|
3
|
-
* snapshot test pins this list so the contract can't drift silently.
|
|
4
|
-
*
|
|
5
|
-
* grows additively.
|
|
3
|
+
* snapshot test pins this list so the contract can't drift silently. A rename
|
|
4
|
+
* or a removal bumps the SDK compatibility version; a new name does not.
|
|
6
5
|
*
|
|
7
6
|
* The `--tai-dark-*` half of each themed pair is deliberately NOT here. Those
|
|
8
7
|
* names are the theming mechanism's own storage — the two blocks in `tokens.css`
|
|
@@ -49,14 +48,9 @@ export const TOKEN_NAMES = [
|
|
|
49
48
|
'--tai-color-syntax-string',
|
|
50
49
|
'--tai-color-syntax-number',
|
|
51
50
|
'--tai-color-syntax-bool',
|
|
52
|
-
'--tai-color-
|
|
53
|
-
'--tai-color-
|
|
54
|
-
'--tai-color-
|
|
55
|
-
'--tai-color-danger-text',
|
|
56
|
-
'--tai-color-danger-hover',
|
|
57
|
-
'--tai-color-danger-surface',
|
|
58
|
-
'--tai-color-success',
|
|
59
|
-
'--tai-color-warning',
|
|
51
|
+
'--tai-color-destructive',
|
|
52
|
+
'--tai-color-destructive-hover',
|
|
53
|
+
'--tai-color-on-destructive',
|
|
60
54
|
'--tai-space-1',
|
|
61
55
|
'--tai-space-2',
|
|
62
56
|
'--tai-space-3',
|
|
@@ -89,8 +83,6 @@ export const TOKEN_NAMES = [
|
|
|
89
83
|
'--tai-shadow-overlay-color',
|
|
90
84
|
'--tai-shadow-lift',
|
|
91
85
|
'--tai-shadow-overlay',
|
|
92
|
-
'--tai-shadow-sm',
|
|
93
|
-
'--tai-shadow-md',
|
|
94
86
|
'--tai-z-sticky',
|
|
95
87
|
'--tai-z-dropdown',
|
|
96
88
|
'--tai-z-overlay',
|
|
@@ -8,7 +8,7 @@ import type { ReactNode } from 'react';
|
|
|
8
8
|
import { toolDisplayLabel } from '../hooks/useToolDisplayNames';
|
|
9
9
|
import { Badge } from './badge';
|
|
10
10
|
import { Field } from './field';
|
|
11
|
-
import { Select, type SelectGroup } from './select';
|
|
11
|
+
import { Select, type SelectGroup, type SelectOption } from './select';
|
|
12
12
|
import { Tooltip } from './tooltip';
|
|
13
13
|
|
|
14
14
|
/** The shared informational note every badge surface carries: declared, never enforced. */
|
|
@@ -80,6 +80,9 @@ interface ToolSelectProps {
|
|
|
80
80
|
readonly disabled: boolean | undefined;
|
|
81
81
|
readonly displayNames: Readonly<Record<string, string>> | undefined;
|
|
82
82
|
readonly agentToolNames: ReadonlySet<string> | undefined;
|
|
83
|
+
/** An option drawn ahead of the list's own (a stored value the list does not carry). */
|
|
84
|
+
readonly leading: SelectOption | undefined;
|
|
85
|
+
readonly ariaDescribedBy: string | undefined;
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
/** The tool select: grouped by tag when `tagsByTool` is present, else a flat list.
|
|
@@ -94,32 +97,40 @@ export function ToolSelect({
|
|
|
94
97
|
disabled,
|
|
95
98
|
displayNames,
|
|
96
99
|
agentToolNames,
|
|
100
|
+
leading,
|
|
101
|
+
ariaDescribedBy,
|
|
97
102
|
}: ToolSelectProps): ReactNode {
|
|
98
103
|
if (tagsByTool !== undefined) {
|
|
104
|
+
const groups = buildGroups(filtered, tagsByTool, displayNames, agentToolNames);
|
|
99
105
|
return (
|
|
100
106
|
<Select
|
|
101
|
-
groups={
|
|
107
|
+
groups={
|
|
108
|
+
leading === undefined ? groups : [{ label: leading.label, options: [leading] }, ...groups]
|
|
109
|
+
}
|
|
102
110
|
value={value ?? ''}
|
|
103
111
|
onValueChange={onChange}
|
|
104
112
|
placeholder={placeholder}
|
|
105
113
|
aria-label={ariaLabel}
|
|
114
|
+
aria-describedby={ariaDescribedBy}
|
|
106
115
|
disabled={disabled}
|
|
107
116
|
/>
|
|
108
117
|
);
|
|
109
118
|
}
|
|
119
|
+
const options = filtered.map((name) => ({
|
|
120
|
+
value: name,
|
|
121
|
+
label: optionLabel(name, displayNames, agentToolNames),
|
|
122
|
+
}));
|
|
110
123
|
return (
|
|
111
124
|
<Select
|
|
112
125
|
// Keep Radix controlled even when the caller's value is null: `''` matches no
|
|
113
126
|
// item (placeholder still shows) and avoids the uncontrolled→controlled warning
|
|
114
127
|
// a subsequent selection would otherwise trigger.
|
|
115
|
-
options={
|
|
116
|
-
value: name,
|
|
117
|
-
label: optionLabel(name, displayNames, agentToolNames),
|
|
118
|
-
}))}
|
|
128
|
+
options={leading === undefined ? options : [leading, ...options]}
|
|
119
129
|
value={value ?? ''}
|
|
120
130
|
onValueChange={onChange}
|
|
121
131
|
placeholder={placeholder}
|
|
122
132
|
aria-label={ariaLabel}
|
|
133
|
+
aria-describedby={ariaDescribedBy}
|
|
123
134
|
disabled={disabled}
|
|
124
135
|
/>
|
|
125
136
|
);
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `ToolPicker` — a controlled single-select over the
|
|
2
|
+
* `ToolPicker` — a controlled single-select over the tool names a host offers, built
|
|
3
3
|
* on the DS `Select` so features share one picker instead of each re-implementing
|
|
4
4
|
* a listbox (the same reason `SchemaForm` lives in the SDK). It is
|
|
5
|
-
* PRESENTATIONAL: the
|
|
6
|
-
* selected value; this component fetches nothing.
|
|
5
|
+
* PRESENTATIONAL: the host owns the tool list read and hands it in as `query`, and
|
|
6
|
+
* owns the selected value; this component fetches nothing.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* The picker owns its `Field` (with `label`) and draws the read's states in it: pending
|
|
9
|
+
* (a disabled `Select` reading "Loading tools…"), error (`ErrorState` with Retry, or the
|
|
10
|
+
* host's `errorNote` line where the same failure is drawn with Retry elsewhere), refused
|
|
11
|
+
* (a 403 with `forbiddenNote` set), and loaded with nothing to offer (the host's
|
|
12
|
+
* `emptyNote`, then its `emptyAction`). The host's `error` shows, and the `Field` claims
|
|
13
|
+
* its control id, exactly while a `Select` renders.
|
|
14
|
+
*
|
|
15
|
+
* Optional extensions, each inert unless supplied:
|
|
10
16
|
* - `excludeNames` removes names from the options (e.g. a preset cannot be
|
|
11
17
|
* another preset's base, so the preset create flow excludes existing preset
|
|
12
18
|
* names). The server stays the authority — a slipped-through name is rejected
|
|
13
19
|
* loudly there — this is only a UI convenience.
|
|
20
|
+
* - `unlistedLabel` flags a stored value the loaded list does not carry: it is the
|
|
21
|
+
* first option, labelled `<value> (<unlistedLabel>)`, with a warning line under the
|
|
22
|
+
* control. It never blocks: the server judges the save.
|
|
14
23
|
* - `tagsByTool` (from GET /api/tools/tags) turns on tag GROUPING + a tag FILTER:
|
|
15
24
|
* the options are grouped under their tag and a filter control narrows them to
|
|
16
25
|
* a chosen tag.
|
|
@@ -31,26 +40,60 @@
|
|
|
31
40
|
* SAFETY: a tool name, a tag, a display name, AND a badge are server-supplied, so
|
|
32
41
|
* every one renders as TEXT (React escapes it) — never an HTML sink. Pinned by a test.
|
|
33
42
|
*/
|
|
34
|
-
import { useState } from 'react';
|
|
43
|
+
import { type ReactNode, useId, useState } from 'react';
|
|
35
44
|
|
|
45
|
+
import { errorMessage, isForbiddenError } from '../errors';
|
|
46
|
+
import { Badge } from './badge';
|
|
36
47
|
import { Field } from './field';
|
|
48
|
+
import { ErrorState } from './primitives';
|
|
49
|
+
import type { SelectOption } from './select';
|
|
37
50
|
import { ALL_TAGS, SelectedToolBadges, TagFilterField, ToolSelect } from './tool-picker-parts';
|
|
38
51
|
|
|
39
52
|
export { BADGES_NOTE } from './tool-picker-parts';
|
|
40
53
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
const MUTED_LINE = { margin: 0, color: 'var(--tai-color-text-muted)' } as const;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The subset of a tool list read this picker reads: `data` is the names the host
|
|
58
|
+
* offers. A caller's TanStack `UseQueryResult<readonly string[]>` satisfies it
|
|
59
|
+
* structurally, so the SDK names no query library.
|
|
60
|
+
*/
|
|
61
|
+
export interface ToolListQuery {
|
|
62
|
+
readonly data: readonly string[] | undefined;
|
|
63
|
+
readonly isPending: boolean;
|
|
64
|
+
readonly isError: boolean;
|
|
65
|
+
readonly isSuccess: boolean;
|
|
66
|
+
readonly error: unknown;
|
|
67
|
+
readonly refetch: () => void;
|
|
68
|
+
}
|
|
45
69
|
|
|
46
70
|
export interface ToolPickerProps {
|
|
47
|
-
|
|
71
|
+
/** The tool list read the host owns, `data` projected to the names it offers. */
|
|
72
|
+
readonly query: ToolListQuery;
|
|
48
73
|
readonly value: string | null;
|
|
49
74
|
readonly onChange: (toolName: string) => void;
|
|
75
|
+
/** The empty state's sentence, drawn when the loaded list offers no option (after `excludeNames`). */
|
|
76
|
+
readonly emptyNote: string;
|
|
77
|
+
/** Drawn after `emptyNote` in the empty state (e.g. a link to where tools come from). */
|
|
78
|
+
readonly emptyAction?: ReactNode;
|
|
79
|
+
/**
|
|
80
|
+
* Drawn in place of the `ErrorState` and the control when the read fails, with no
|
|
81
|
+
* Retry: for a host that already draws the same read's failure with Retry elsewhere.
|
|
82
|
+
*/
|
|
83
|
+
readonly errorNote?: string;
|
|
84
|
+
/** Drawn on a 403 of the read in place of the error state (the no-grant state). */
|
|
85
|
+
readonly forbiddenNote?: string;
|
|
86
|
+
/** Flags a stored value the loaded list does not carry (see the module docs). */
|
|
87
|
+
readonly unlistedLabel?: string;
|
|
50
88
|
readonly disabled?: boolean;
|
|
89
|
+
/** Shown once the list is loaded; a pending read reads "Loading tools…". */
|
|
51
90
|
readonly placeholder?: string;
|
|
52
91
|
readonly idPrefix?: string;
|
|
53
92
|
readonly label?: string;
|
|
93
|
+
/** Handed to the picker's `Field` (with `label`). */
|
|
94
|
+
readonly description?: string;
|
|
95
|
+
/** The host's field error, shown under the control while a `Select` renders (with `label`). */
|
|
96
|
+
readonly error?: string;
|
|
54
97
|
/**
|
|
55
98
|
* Accessible name for the tool listbox when `label` renders none and no
|
|
56
99
|
* enclosing `Field` names it. `label` wins where both are given: it names the
|
|
@@ -79,14 +122,112 @@ export interface ToolPickerProps {
|
|
|
79
122
|
readonly badgesByTool?: Readonly<Record<string, readonly string[]>>;
|
|
80
123
|
}
|
|
81
124
|
|
|
125
|
+
/** Which of the picker's states the read and the stored value put it in. */
|
|
126
|
+
type PickerPhase = 'refused' | 'error' | 'unlisted' | 'empty' | 'select';
|
|
127
|
+
|
|
128
|
+
interface PickerState {
|
|
129
|
+
readonly phase: PickerPhase;
|
|
130
|
+
readonly available: readonly string[];
|
|
131
|
+
/** The stored value, or `null` for none. */
|
|
132
|
+
readonly stored: string | null;
|
|
133
|
+
readonly pending: boolean;
|
|
134
|
+
/** The loaded list lacks the stored value and an `unlistedLabel` asks for the flag. */
|
|
135
|
+
readonly unlisted: boolean;
|
|
136
|
+
readonly empty: boolean;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function phaseOf(
|
|
140
|
+
refused: boolean,
|
|
141
|
+
failed: boolean,
|
|
142
|
+
unlisted: boolean,
|
|
143
|
+
empty: boolean,
|
|
144
|
+
): PickerPhase {
|
|
145
|
+
if (refused) return 'refused';
|
|
146
|
+
if (failed) return 'error';
|
|
147
|
+
if (unlisted) return 'unlisted';
|
|
148
|
+
return empty ? 'empty' : 'select';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function pickerState(
|
|
152
|
+
query: ToolListQuery,
|
|
153
|
+
value: string | null,
|
|
154
|
+
excludeNames: readonly string[] | undefined,
|
|
155
|
+
forbiddenNote: string | undefined,
|
|
156
|
+
unlistedLabel: string | undefined,
|
|
157
|
+
): PickerState {
|
|
158
|
+
const excluded = new Set(excludeNames ?? []);
|
|
159
|
+
const available = (query.data ?? []).filter((name) => !excluded.has(name));
|
|
160
|
+
const stored = value !== null && value !== '' ? value : null;
|
|
161
|
+
const loaded = query.isSuccess && !query.isError;
|
|
162
|
+
const unlisted =
|
|
163
|
+
unlistedLabel !== undefined && loaded && stored !== null && !available.includes(stored);
|
|
164
|
+
const empty = loaded && available.length === 0;
|
|
165
|
+
const refused = forbiddenNote !== undefined && query.isError && isForbiddenError(query.error);
|
|
166
|
+
const phase = phaseOf(refused, query.isError, unlisted, empty);
|
|
167
|
+
return { phase, available, stored, pending: !loaded && !query.isError, unlisted, empty };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The distinct tags across `names`, sorted, feeding the filter control. */
|
|
171
|
+
function tagsAcross(
|
|
172
|
+
names: readonly string[],
|
|
173
|
+
tagsByTool: Readonly<Record<string, readonly string[]>>,
|
|
174
|
+
): string[] {
|
|
175
|
+
return [...new Set(names.flatMap((name) => tagsByTool[name] ?? []))].sort((a, b) =>
|
|
176
|
+
a.localeCompare(b),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The tag filter's choices, and the available names the chosen tag keeps. */
|
|
181
|
+
function tagView(
|
|
182
|
+
state: PickerState,
|
|
183
|
+
tagsByTool: Readonly<Record<string, readonly string[]>> | undefined,
|
|
184
|
+
tagFilter: string,
|
|
185
|
+
): { readonly allTags: readonly string[]; readonly filtered: readonly string[] } {
|
|
186
|
+
if (tagsByTool === undefined) return { allTags: [], filtered: state.available };
|
|
187
|
+
const listed = !state.pending && state.phase !== 'error' && state.phase !== 'refused';
|
|
188
|
+
return {
|
|
189
|
+
allTags: listed ? tagsAcross(state.available, tagsByTool) : [],
|
|
190
|
+
filtered:
|
|
191
|
+
tagFilter === ALL_TAGS
|
|
192
|
+
? state.available
|
|
193
|
+
: state.available.filter((name) => (tagsByTool[name] ?? []).includes(tagFilter)),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function MutedLine({ text }: { readonly text: string }): ReactNode {
|
|
198
|
+
return (
|
|
199
|
+
<p role="status" style={MUTED_LINE}>
|
|
200
|
+
{text}
|
|
201
|
+
</p>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function ReadError({ query }: { readonly query: ToolListQuery }): ReactNode {
|
|
206
|
+
return (
|
|
207
|
+
<ErrorState
|
|
208
|
+
message={errorMessage(query.error)}
|
|
209
|
+
onRetry={() => {
|
|
210
|
+
query.refetch();
|
|
211
|
+
}}
|
|
212
|
+
/>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
82
216
|
export function ToolPicker({
|
|
83
|
-
|
|
217
|
+
query,
|
|
84
218
|
value,
|
|
85
219
|
onChange,
|
|
220
|
+
emptyNote,
|
|
221
|
+
emptyAction,
|
|
222
|
+
errorNote,
|
|
223
|
+
forbiddenNote,
|
|
224
|
+
unlistedLabel,
|
|
86
225
|
disabled,
|
|
87
226
|
placeholder = 'Select a tool…',
|
|
88
227
|
idPrefix = 'tool-picker',
|
|
89
228
|
label,
|
|
229
|
+
description,
|
|
230
|
+
error,
|
|
90
231
|
'aria-label': ariaLabel,
|
|
91
232
|
excludeNames,
|
|
92
233
|
tagsByTool,
|
|
@@ -95,47 +236,69 @@ export function ToolPicker({
|
|
|
95
236
|
badgesByTool,
|
|
96
237
|
}: ToolPickerProps) {
|
|
97
238
|
const [tagFilter, setTagFilter] = useState<string>(ALL_TAGS);
|
|
239
|
+
const unlistedId = useId();
|
|
240
|
+
const state = pickerState(query, value, excludeNames, forbiddenNote, unlistedLabel);
|
|
241
|
+
const { phase, stored } = state;
|
|
98
242
|
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// A settled, empty option set (not a load in progress) is a "nothing to pick"
|
|
103
|
-
// state: the trigger reads the empty copy and cannot open onto an empty popup. The
|
|
104
|
-
// caller's `disabled` still marks a loading picker, which keeps its own placeholder.
|
|
105
|
-
const noneAvailable = available.length === 0;
|
|
106
|
-
const pickerDisabled = disabled === true || noneAvailable;
|
|
107
|
-
const pickerPlaceholder = noneAvailable && disabled !== true ? NO_TOOLS_AVAILABLE : placeholder;
|
|
108
|
-
|
|
109
|
-
// Distinct tags across the available tools, sorted, feeding the filter control.
|
|
110
|
-
const allTags =
|
|
111
|
-
tagsByTool === undefined
|
|
112
|
-
? []
|
|
113
|
-
: [...new Set(available.flatMap((name) => tagsByTool[name] ?? []))].sort((a, b) =>
|
|
114
|
-
a.localeCompare(b),
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
const filtered =
|
|
118
|
-
tagsByTool !== undefined && tagFilter !== ALL_TAGS
|
|
119
|
-
? available.filter((name) => (tagsByTool[name] ?? []).includes(tagFilter))
|
|
120
|
-
: available;
|
|
121
|
-
|
|
122
|
-
const toolSelect = (
|
|
243
|
+
const { allTags, filtered } = tagView(state, tagsByTool, tagFilter);
|
|
244
|
+
|
|
245
|
+
const select = (options: readonly string[], leading: SelectOption | undefined): ReactNode => (
|
|
123
246
|
<ToolSelect
|
|
124
247
|
tagsByTool={tagsByTool}
|
|
125
|
-
filtered={
|
|
248
|
+
filtered={options}
|
|
126
249
|
value={value}
|
|
127
250
|
onChange={onChange}
|
|
128
|
-
placeholder={
|
|
251
|
+
placeholder={state.pending ? 'Loading tools…' : placeholder}
|
|
129
252
|
ariaLabel={ariaLabel}
|
|
130
|
-
|
|
253
|
+
ariaDescribedBy={state.unlisted ? unlistedId : undefined}
|
|
254
|
+
disabled={disabled === true || state.pending}
|
|
131
255
|
displayNames={displayNames}
|
|
132
256
|
agentToolNames={agentToolNames}
|
|
257
|
+
leading={leading}
|
|
133
258
|
/>
|
|
134
259
|
);
|
|
260
|
+
const emptyState = (
|
|
261
|
+
<>
|
|
262
|
+
<MutedLine text={emptyNote} />
|
|
263
|
+
{emptyAction}
|
|
264
|
+
</>
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// Only a rendered `Select` claims the field's control id; with none the host's
|
|
268
|
+
// error is hidden and the note in its place says why the field is empty.
|
|
269
|
+
const selectRenders =
|
|
270
|
+
phase === 'unlisted' || phase === 'select' || (phase === 'refused' && stored !== null);
|
|
271
|
+
const contentByPhase: Record<PickerPhase, () => ReactNode> = {
|
|
272
|
+
// A stored value stays as the sole, unflagged option: without the list the form claims nothing about it.
|
|
273
|
+
refused: () => (
|
|
274
|
+
<>
|
|
275
|
+
{stored !== null ? select([], { value: stored, label: stored }) : null}
|
|
276
|
+
<MutedLine text={forbiddenNote ?? ''} />
|
|
277
|
+
</>
|
|
278
|
+
),
|
|
279
|
+
error: () =>
|
|
280
|
+
errorNote !== undefined ? <MutedLine text={errorNote} /> : <ReadError query={query} />,
|
|
281
|
+
unlisted: () => (
|
|
282
|
+
<>
|
|
283
|
+
{select(filtered, {
|
|
284
|
+
value: stored ?? '',
|
|
285
|
+
label: `${stored ?? ''} (${unlistedLabel ?? ''})`,
|
|
286
|
+
})}
|
|
287
|
+
{state.empty ? emptyState : null}
|
|
288
|
+
<p id={unlistedId} className="tai-field-hint" style={{ margin: 0 }}>
|
|
289
|
+
<Badge variant="warning">{unlistedLabel}</Badge> {stored} is {unlistedLabel}. Pick
|
|
290
|
+
another, or saving is refused.
|
|
291
|
+
</p>
|
|
292
|
+
</>
|
|
293
|
+
),
|
|
294
|
+
empty: () => emptyState,
|
|
295
|
+
select: () => select(filtered, undefined),
|
|
296
|
+
};
|
|
297
|
+
const content = contentByPhase[phase]();
|
|
135
298
|
|
|
136
299
|
return (
|
|
137
300
|
<div data-testid={idPrefix} className="tai-stack tai-stack-2">
|
|
138
|
-
{
|
|
301
|
+
{allTags.length > 0 ? (
|
|
139
302
|
<TagFilterField
|
|
140
303
|
allTags={allTags}
|
|
141
304
|
tagFilter={tagFilter}
|
|
@@ -143,7 +306,18 @@ export function ToolPicker({
|
|
|
143
306
|
disabled={disabled}
|
|
144
307
|
/>
|
|
145
308
|
) : null}
|
|
146
|
-
{label !== undefined ?
|
|
309
|
+
{label !== undefined ? (
|
|
310
|
+
<Field
|
|
311
|
+
label={label}
|
|
312
|
+
description={description}
|
|
313
|
+
error={selectRenders ? error : undefined}
|
|
314
|
+
group={!selectRenders}
|
|
315
|
+
>
|
|
316
|
+
{content}
|
|
317
|
+
</Field>
|
|
318
|
+
) : (
|
|
319
|
+
content
|
|
320
|
+
)}
|
|
147
321
|
<SelectedToolBadges value={value} badgesByTool={badgesByTool} idPrefix={idPrefix} />
|
|
148
322
|
</div>
|
|
149
323
|
);
|
|
@@ -82,6 +82,12 @@ export interface VersionHistoryPanelProps {
|
|
|
82
82
|
* state. Omitted ⇒ no edit affordance (tags stay read-only).
|
|
83
83
|
*/
|
|
84
84
|
readonly onEditTags?: (version: number, tags: string[]) => Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* A version to point the reader at (a deep link names it): its row is marked
|
|
87
|
+
* `aria-current="true"` on the accent tint and scrolled into view. Omitted ⇒ no
|
|
88
|
+
* row is marked.
|
|
89
|
+
*/
|
|
90
|
+
readonly highlightVersion?: number;
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
/** The initially-selected version: the current one, else the first listed. */
|
|
@@ -168,6 +174,7 @@ export function VersionHistoryPanel({
|
|
|
168
174
|
readOnly = false,
|
|
169
175
|
rollbackConfirmDescription,
|
|
170
176
|
onEditTags,
|
|
177
|
+
highlightVersion,
|
|
171
178
|
}: VersionHistoryPanelProps): ReactNode {
|
|
172
179
|
const [selected, setSelected] = useState<number | null>(() => initialSelection(versions));
|
|
173
180
|
const [confirming, setConfirming] = useState<number | null>(null);
|
|
@@ -246,6 +253,7 @@ export function VersionHistoryPanel({
|
|
|
246
253
|
compareArmed={compareFrom === entry.version}
|
|
247
254
|
readOnly={readOnly}
|
|
248
255
|
canEditTags={onEditTags !== undefined}
|
|
256
|
+
highlighted={entry.version === highlightVersion}
|
|
249
257
|
onView={view}
|
|
250
258
|
onCompare={compare}
|
|
251
259
|
onEditTags={setEditingTags}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* the two-version compare dialog (`CompareDialog`), and the rollback confirm
|
|
5
5
|
* dialog (`RollbackConfirmDialog`). All generic and kind-agnostic.
|
|
6
6
|
*/
|
|
7
|
-
import type
|
|
7
|
+
import { type ReactNode, useEffect, useRef } from 'react';
|
|
8
8
|
|
|
9
9
|
import { Dialog } from './dialog';
|
|
10
10
|
import { CheckCircleIcon, PendingIcon } from './icons';
|
|
@@ -25,6 +25,8 @@ interface VersionRowProps {
|
|
|
25
25
|
readonly readOnly: boolean;
|
|
26
26
|
/** An edit-tags affordance is available (the consumer supplied `onEditTags`). */
|
|
27
27
|
readonly canEditTags: boolean;
|
|
28
|
+
/** The row a deep link points at: marked current on the accent tint, scrolled to. */
|
|
29
|
+
readonly highlighted: boolean;
|
|
28
30
|
readonly onView: (version: number) => void;
|
|
29
31
|
readonly onCompare: (version: number) => void;
|
|
30
32
|
readonly onEditTags: (version: number) => void;
|
|
@@ -40,13 +42,23 @@ export function VersionRow({
|
|
|
40
42
|
compareArmed,
|
|
41
43
|
readOnly,
|
|
42
44
|
canEditTags,
|
|
45
|
+
highlighted,
|
|
43
46
|
onView,
|
|
44
47
|
onCompare,
|
|
45
48
|
onEditTags,
|
|
46
49
|
onRollback,
|
|
47
50
|
}: VersionRowProps): ReactNode {
|
|
51
|
+
const rowRef = useRef<HTMLTableRowElement>(null);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (highlighted) rowRef.current?.scrollIntoView({ block: 'nearest' });
|
|
54
|
+
}, [highlighted]);
|
|
48
55
|
return (
|
|
49
|
-
<TR
|
|
56
|
+
<TR
|
|
57
|
+
ref={rowRef}
|
|
58
|
+
data-testid={`version-row-${String(entry.version)}`}
|
|
59
|
+
aria-current={highlighted ? 'true' : undefined}
|
|
60
|
+
style={highlighted ? { background: 'var(--tai-color-accent-tint)' } : undefined}
|
|
61
|
+
>
|
|
50
62
|
<TD className="tai-table-id" numeric>
|
|
51
63
|
{entry.version}
|
|
52
64
|
</TD>
|
package/src/errors.ts
CHANGED
|
@@ -10,3 +10,13 @@ export function errorMessage(error: unknown): string {
|
|
|
10
10
|
if (typeof error === 'string') return error;
|
|
11
11
|
return 'An unexpected error occurred.';
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* True when `error` is a refusal for want of a grant: an object whose `status` is
|
|
16
|
+
* `403`. Duck-typed, so it reads an `ApiError` and any error shaped like one.
|
|
17
|
+
*/
|
|
18
|
+
export function isForbiddenError(error: unknown): boolean {
|
|
19
|
+
return (
|
|
20
|
+
typeof error === 'object' && error !== null && (error as { status?: unknown }).status === 403
|
|
21
|
+
);
|
|
22
|
+
}
|