@elabs-ai/components-ui 4.0.0 → 4.1.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 +10 -8
- package/dist/chunk-JKPPTL63.js +48 -0
- package/dist/chunk-JKPPTL63.js.map +1 -0
- package/dist/form.d.ts +28 -0
- package/dist/form.js +123 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +2485 -167
- package/dist/index.js +10716 -2706
- package/dist/index.js.map +1 -1
- package/package.json +21 -7
- package/src/_peer-deps.test.ts +59 -0
- package/src/blocks-comparison-table.stories.tsx +2 -2
- package/src/components/accordion/accordion.tsx +1 -1
- package/src/components/advanced-group/advanced-group.tsx +1 -1
- package/src/components/alert/alert.tsx +4 -1
- package/src/components/app-shell/app-shell.stories.tsx +44 -2
- package/src/components/app-shell/app-shell.tsx +35 -2
- package/src/components/app-sidebar/app-sidebar.stories.tsx +9 -1
- package/src/components/attribution-panel/attribution-panel.tsx +1 -1
- package/src/components/attribution-panel/attributions.generated.ts +81 -77
- package/src/components/badge/badge.stories.tsx +8 -0
- package/src/components/badge/badge.tsx +1 -1
- package/src/components/bento-grid/bento-grid.test.tsx +1 -1
- package/src/components/bento-grid/bento-grid.tsx +1 -1
- package/src/components/breadcrumb/breadcrumb.stories.tsx +38 -0
- package/src/components/breadcrumb/breadcrumb.test.tsx +24 -1
- package/src/components/breadcrumb/breadcrumb.tsx +7 -2
- package/src/components/button/button.stories.tsx +116 -8
- package/src/components/button/button.tsx +1 -1
- package/src/components/card/card.stories.tsx +11 -6
- package/src/components/card/card.tsx +1 -1
- package/src/components/change-review/change-review.stories.tsx +145 -1
- package/src/components/change-review/change-review.test.tsx +143 -0
- package/src/components/change-review/change-review.tsx +185 -4
- package/src/components/checkbox/checkbox.tsx +1 -1
- package/src/components/color-picker/color-picker.tsx +6 -5
- package/src/components/command/command.stories.tsx +16 -6
- package/src/components/command/command.test.tsx +73 -0
- package/src/components/command/command.tsx +30 -7
- package/src/components/command/index.ts +1 -0
- package/src/components/command-trigger/command-trigger.stories.tsx +39 -0
- package/src/components/command-trigger/command-trigger.test.tsx +18 -0
- package/src/components/command-trigger/command-trigger.tsx +70 -0
- package/src/components/command-trigger/index.ts +1 -0
- package/src/components/context-rail/context-rail.stories.tsx +285 -0
- package/src/components/context-rail/context-rail.test.tsx +263 -0
- package/src/components/context-rail/context-rail.tsx +678 -0
- package/src/components/context-rail/index.ts +1 -0
- package/src/components/copyable-value/copyable-value.tsx +1 -1
- package/src/components/dialog/dialog.tsx +2 -2
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +1 -1
- package/src/components/empty-state/empty-state.test.tsx +26 -0
- package/src/components/empty-state/empty-state.tsx +6 -1
- package/src/components/expand-dialog/expand-dialog.tsx +1 -2
- package/src/components/field/field-context.ts +59 -0
- package/src/components/field/field.stories.tsx +142 -0
- package/src/components/field/field.test.tsx +415 -0
- package/src/components/field/field.tsx +340 -0
- package/src/components/field/index.ts +9 -0
- package/src/components/field-row/field-row.stories.tsx +85 -1
- package/src/components/field-row/field-row.test.tsx +145 -0
- package/src/components/field-row/field-row.tsx +48 -6
- package/src/components/file-upload/file-upload.stories.tsx +16 -0
- package/src/components/file-upload/file-upload.test.tsx +7 -2
- package/src/components/file-upload/file-upload.tsx +15 -4
- package/src/components/form/form.tsx +5 -2
- package/src/components/icon-button/icon-button.test.tsx +1 -1
- package/src/components/icon-button/icon-button.tsx +2 -2
- package/src/components/input/input.stories.tsx +121 -0
- package/src/components/input/input.test.tsx +13 -0
- package/src/components/input/input.tsx +2 -2
- package/src/components/input-group/input-group.stories.tsx +74 -0
- package/src/components/input-group/input-group.tsx +13 -4
- package/src/components/input-otp/input-otp.tsx +5 -1
- package/src/components/keyboard-shortcuts/index.ts +6 -0
- package/src/components/keyboard-shortcuts/keyboard-shortcuts.stories.tsx +177 -0
- package/src/components/keyboard-shortcuts/keyboard-shortcuts.test.tsx +73 -0
- package/src/components/keyboard-shortcuts/keyboard-shortcuts.tsx +203 -0
- package/src/components/link-preview/link-preview.tsx +1 -1
- package/src/components/locale-provider/index.ts +6 -1
- package/src/components/locale-provider/locale-provider.stories.tsx +89 -1
- package/src/components/locale-provider/locale-provider.test.tsx +257 -1
- package/src/components/locale-provider/locale-provider.tsx +63 -8
- package/src/components/locale-provider/messages.ts +501 -2
- package/src/components/match-highlight/match-highlight.stories.tsx +1 -1
- package/src/components/mention-input/mention-input-mirror.stories.tsx +9 -1
- package/src/components/mention-input/mention-input.tsx +8 -38
- package/src/components/mention-input/mention-value.ts +8 -2
- package/src/components/metric-card/metric-card.stories.tsx +5 -3
- package/src/components/metric-card/metric-card.tsx +21 -2
- package/src/components/model-picker/model-picker.stories.tsx +63 -3
- package/src/components/model-picker/model-picker.test.tsx +63 -0
- package/src/components/model-picker/model-picker.tsx +62 -36
- package/src/components/nav-main/nav-main.tsx +1 -1
- package/src/components/nav-notifications/nav-notifications.stories.tsx +110 -0
- package/src/components/nav-notifications/nav-notifications.tsx +57 -5
- package/src/components/navigation-menu/navigation-menu.stories.tsx +22 -0
- package/src/components/navigation-menu/navigation-menu.test.tsx +174 -0
- package/src/components/navigation-menu/navigation-menu.tsx +189 -12
- package/src/components/page-shell/page-shell.stories.tsx +163 -0
- package/src/components/page-shell/page-shell.test.tsx +229 -0
- package/src/components/page-shell/page-shell.tsx +108 -25
- package/src/components/pagination/pagination.test.tsx +90 -0
- package/src/components/pagination/pagination.tsx +9 -5
- package/src/components/radio-group/radio-group.tsx +1 -1
- package/src/components/rating/rating.tsx +1 -1
- package/src/components/resizable/resizable.stories.tsx +114 -2
- package/src/components/resizable/resizable.tsx +1 -1
- package/src/components/revision-timeline/revision-timeline.stories.tsx +8 -1
- package/src/components/revision-timeline/revision-timeline.test.tsx +26 -0
- package/src/components/revision-timeline/revision-timeline.tsx +46 -3
- package/src/components/schema-form/from-json-schema.test.ts +390 -0
- package/src/components/schema-form/from-json-schema.ts +375 -0
- package/src/components/schema-form/index.ts +81 -0
- package/src/components/schema-form/schema-form-spec.ts +698 -0
- package/src/components/schema-form/schema-form.stories.tsx +566 -0
- package/src/components/schema-form/schema-form.test.tsx +900 -0
- package/src/components/schema-form/schema-form.tsx +1545 -0
- package/src/components/section-header/section-header.stories.tsx +37 -0
- package/src/components/section-header/section-header.tsx +12 -1
- package/src/components/select/select.tsx +6 -2
- package/src/components/sheet/sheet.tsx +1 -1
- package/src/components/side-dock/index.ts +1 -0
- package/src/components/side-dock/side-dock.stories.tsx +137 -0
- package/src/components/side-dock/side-dock.test.tsx +273 -0
- package/src/components/side-dock/side-dock.tsx +502 -0
- package/src/components/sidebar/index.ts +1 -0
- package/src/components/sidebar/sidebar-frame.test.tsx +162 -0
- package/src/components/sidebar/sidebar.stories.tsx +421 -1
- package/src/components/sidebar/sidebar.test.tsx +36 -1
- package/src/components/sidebar/sidebar.tsx +245 -33
- package/src/components/skip-link/index.ts +1 -0
- package/src/components/skip-link/skip-link.stories.tsx +106 -0
- package/src/components/skip-link/skip-link.test.tsx +20 -0
- package/src/components/skip-link/skip-link.tsx +34 -0
- package/src/components/slider/slider.tsx +1 -1
- package/src/components/split-panel/index.ts +6 -1
- package/src/components/split-panel/split-panel.stories.tsx +18 -2
- package/src/components/split-panel/split-panel.test.tsx +20 -0
- package/src/components/split-panel/split-panel.tsx +28 -8
- package/src/components/state-panel/state-panel.stories.tsx +52 -0
- package/src/components/state-panel/state-panel.test.tsx +125 -0
- package/src/components/state-panel/state-panel.tsx +96 -17
- package/src/components/status-badge/status-badge.stories.tsx +8 -0
- package/src/components/switch/switch.tsx +1 -1
- package/src/components/table/table.stories.tsx +62 -0
- package/src/components/table/table.test.tsx +142 -1
- package/src/components/table/table.tsx +60 -1
- package/src/components/tabs/tabs.tsx +8 -2
- package/src/components/tag-input/tag-input.tsx +3 -2
- package/src/components/team-switcher/team-switcher.tsx +34 -5
- package/src/components/textarea/textarea.tsx +1 -1
- package/src/components/theme-switcher/theme-switcher.tsx +6 -2
- package/src/components/theme-switcher/use-theme-transition.ts +2 -2
- package/src/components/timeline/timeline.stories.tsx +27 -0
- package/src/components/timeline/timeline.tsx +11 -1
- package/src/components/toggle/toggle.tsx +1 -1
- package/src/components/toolbar/toolbar.stories.tsx +10 -2
- package/src/components/tree/tree.stories.tsx +2 -2
- package/src/components/tree/tree.tsx +14 -9
- package/src/components/typography/prose.tsx +1 -1
- package/src/components/typography/typography.stories.tsx +228 -0
- package/src/components/view-toolbar/view-toolbar.stories.tsx +41 -1
- package/src/components/view-toolbar/view-toolbar.test.tsx +33 -0
- package/src/components/view-toolbar/view-toolbar.tsx +37 -4
- package/src/components/virtual-select/virtual-select.test.tsx +1 -1
- package/src/components/wizard/wizard.tsx +1 -1
- package/src/components/workspace-picker/index.ts +3 -0
- package/src/components/workspace-picker/workspace-picker-state.ts +50 -0
- package/src/components/workspace-picker/workspace-picker.stories.tsx +126 -0
- package/src/components/workspace-picker/workspace-picker.test.tsx +207 -0
- package/src/components/workspace-picker/workspace-picker.tsx +172 -0
- package/src/illustrations/empty-list-illustration.tsx +26 -0
- package/src/illustrations/error-illustration.tsx +60 -0
- package/src/illustrations/first-run-illustration.tsx +27 -0
- package/src/illustrations/illustration-base.tsx +108 -0
- package/src/illustrations/illustrations.stories.tsx +94 -0
- package/src/illustrations/illustrations.test.tsx +98 -0
- package/src/illustrations/index.ts +16 -0
- package/src/illustrations/no-access-illustration.tsx +45 -0
- package/src/illustrations/no-results-illustration.tsx +46 -0
- package/src/illustrations/offline-illustration.tsx +37 -0
- package/src/illustrations/success-illustration.tsx +40 -0
- package/src/index.ts +75 -2
- package/src/lib/agent-event-model.test.ts +12 -0
- package/src/lib/agent-event-model.ts +42 -0
- package/src/lib/approval-option.test.ts +17 -0
- package/src/lib/approval-option.ts +53 -0
- package/src/lib/check-result.ts +33 -0
- package/src/lib/diff-line.test.ts +36 -0
- package/src/lib/diff-line.ts +63 -0
- package/src/lib/diff-rows.test.ts +76 -0
- package/src/lib/diff-rows.ts +92 -0
- package/src/lib/format-duration.test.ts +22 -0
- package/src/lib/format-duration.ts +30 -0
- package/src/lib/has-renderable-content.test.ts +49 -0
- package/src/lib/has-renderable-content.ts +32 -0
- package/src/lib/operating-mode.test.ts +36 -0
- package/src/lib/operating-mode.ts +81 -0
- package/src/lib/optional-peer.ts +59 -0
- package/src/lib/session-launch.ts +37 -0
- package/src/lib/slash-command.test.ts +44 -0
- package/src/lib/slash-command.ts +45 -0
- package/src/lib/trigger-query.test.ts +94 -0
- package/src/lib/trigger-query.ts +88 -0
- package/src/lib/use-mobile.ts +12 -6
- package/src/motion.stories.tsx +6 -0
- package/src/templates-object-detail-hub.stories.tsx +2 -2
- package/src/templates-screen-states.stories.tsx +5 -5
- package/src/templates-settings.stories.tsx +1 -1
- package/src/blocks/sidebar-02/app-sidebar.tsx +0 -160
- package/src/blocks/sidebar-02/logo.tsx +0 -18
- package/src/blocks/sidebar-02/nav-main.tsx +0 -13
- package/src/blocks/sidebar-02/nav-notifications.tsx +0 -7
- package/src/blocks/sidebar-02/sidebar-02.stories.tsx +0 -23
- package/src/blocks/sidebar-02/team-switcher.tsx +0 -7
- package/src/blocks/sidebar-04/app-sidebar.tsx +0 -236
- package/src/blocks/sidebar-04/mail-context.tsx +0 -29
- package/src/blocks/sidebar-04/nav-user.tsx +0 -6
- package/src/blocks/sidebar-04/sidebar-04.stories.tsx +0 -150
- package/src/blocks/sidebar-05/app-sidebar.tsx +0 -375
- package/src/blocks/sidebar-05/sidebar-05.stories.tsx +0 -18
- package/src/blocks/sidebar-05/team-switcher.tsx +0 -6
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import type { ComponentType } from "react";
|
|
3
|
+
import {
|
|
4
|
+
EmptyListIllustration,
|
|
5
|
+
NoResultsIllustration,
|
|
6
|
+
NoAccessIllustration,
|
|
7
|
+
ErrorIllustration,
|
|
8
|
+
OfflineIllustration,
|
|
9
|
+
SuccessIllustration,
|
|
10
|
+
FirstRunIllustration,
|
|
11
|
+
type IllustrationProps,
|
|
12
|
+
} from "./index";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Demo wrapper: illustrations are pure `currentColor` line art plus one
|
|
16
|
+
* meaning-bearing accent (`--primary-text` by default; `--success-text` on
|
|
17
|
+
* `SuccessIllustration`, `--destructive` on `ErrorIllustration`), so a story
|
|
18
|
+
* renders them inside a `text-muted-foreground` container — the same ambient
|
|
19
|
+
* color `StatePanel` applies via its icon/illustration slot. `StatePanel`
|
|
20
|
+
* retints the accent to `--destructive` for any illustration placed in a
|
|
21
|
+
* `kind="error"` slot via the `--illustration-accent` custom property (see
|
|
22
|
+
* `illustration-base.tsx`) — see `States/StatePanel`'s "Error (with
|
|
23
|
+
* illustration, #24 P0-2)" story for that pairing.
|
|
24
|
+
*/
|
|
25
|
+
function Demo(props: IllustrationProps & { as: ComponentType<IllustrationProps> }) {
|
|
26
|
+
const { as: Illustration, ...rest } = props;
|
|
27
|
+
return (
|
|
28
|
+
<div className="text-muted-foreground">
|
|
29
|
+
<Illustration {...rest} />
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const meta = {
|
|
35
|
+
title: "States/Illustrations",
|
|
36
|
+
component: Demo,
|
|
37
|
+
tags: ["autodocs"],
|
|
38
|
+
argTypes: {
|
|
39
|
+
size: {
|
|
40
|
+
control: "text",
|
|
41
|
+
description: "Rendered width/height (any CSS length). Legible ~4rem–10rem.",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
} satisfies Meta<typeof Demo>;
|
|
45
|
+
export default meta;
|
|
46
|
+
type Story = StoryObj<typeof meta>;
|
|
47
|
+
|
|
48
|
+
export const EmptyList: Story = {
|
|
49
|
+
args: { as: EmptyListIllustration },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const NoResults: Story = {
|
|
53
|
+
args: { as: NoResultsIllustration },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const NoAccess: Story = {
|
|
57
|
+
args: { as: NoAccessIllustration },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const Error: Story = {
|
|
61
|
+
args: { as: ErrorIllustration },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const Offline: Story = {
|
|
65
|
+
args: { as: OfflineIllustration },
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const Success: Story = {
|
|
69
|
+
args: { as: SuccessIllustration },
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const FirstRun: Story = {
|
|
73
|
+
args: { as: FirstRunIllustration },
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const CustomSize: Story = {
|
|
77
|
+
name: "Custom size (10rem)",
|
|
78
|
+
args: { as: EmptyListIllustration, size: "10rem" },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const AllStates: Story = {
|
|
82
|
+
name: "All seven, side by side",
|
|
83
|
+
render: () => (
|
|
84
|
+
<div className="flex flex-wrap gap-8 text-muted-foreground">
|
|
85
|
+
<EmptyListIllustration />
|
|
86
|
+
<NoResultsIllustration />
|
|
87
|
+
<NoAccessIllustration />
|
|
88
|
+
<ErrorIllustration />
|
|
89
|
+
<OfflineIllustration />
|
|
90
|
+
<SuccessIllustration />
|
|
91
|
+
<FirstRunIllustration />
|
|
92
|
+
</div>
|
|
93
|
+
),
|
|
94
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { render } from "@testing-library/react";
|
|
4
|
+
import {
|
|
5
|
+
EmptyListIllustration,
|
|
6
|
+
NoResultsIllustration,
|
|
7
|
+
NoAccessIllustration,
|
|
8
|
+
ErrorIllustration,
|
|
9
|
+
OfflineIllustration,
|
|
10
|
+
SuccessIllustration,
|
|
11
|
+
FirstRunIllustration,
|
|
12
|
+
ILLUSTRATION_ACCENT_VAR,
|
|
13
|
+
illustrationAccent,
|
|
14
|
+
} from "./index";
|
|
15
|
+
|
|
16
|
+
// No literal hex anywhere in a rendered attribute — only `currentColor` or a
|
|
17
|
+
// `var(--…)` token reference (inline style or attribute) may carry color.
|
|
18
|
+
const RAW_HEX_RE = /#[0-9a-fA-F]{3,6}\b/;
|
|
19
|
+
|
|
20
|
+
const ILLUSTRATIONS = [
|
|
21
|
+
["EmptyListIllustration", EmptyListIllustration, "empty-list-illustration"],
|
|
22
|
+
["NoResultsIllustration", NoResultsIllustration, "no-results-illustration"],
|
|
23
|
+
["NoAccessIllustration", NoAccessIllustration, "no-access-illustration"],
|
|
24
|
+
["ErrorIllustration", ErrorIllustration, "error-illustration"],
|
|
25
|
+
["OfflineIllustration", OfflineIllustration, "offline-illustration"],
|
|
26
|
+
["SuccessIllustration", SuccessIllustration, "success-illustration"],
|
|
27
|
+
["FirstRunIllustration", FirstRunIllustration, "first-run-illustration"],
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
describe("state illustrations", () => {
|
|
31
|
+
it.each(ILLUSTRATIONS)(
|
|
32
|
+
"%s renders a decorative, token-driven <svg>",
|
|
33
|
+
(_name, Component, slot) => {
|
|
34
|
+
const { container } = render(<Component />);
|
|
35
|
+
const svg = container.querySelector("svg");
|
|
36
|
+
expect(svg).not.toBeNull();
|
|
37
|
+
expect(svg?.tagName.toLowerCase()).toBe("svg");
|
|
38
|
+
expect(svg).toHaveAttribute("aria-hidden", "true");
|
|
39
|
+
expect(svg).toHaveAttribute("role", "presentation");
|
|
40
|
+
expect(svg).toHaveAttribute("data-slot", slot);
|
|
41
|
+
|
|
42
|
+
// No raw hex anywhere in the tree's attributes (element attrs + style attr).
|
|
43
|
+
const offenders: string[] = [];
|
|
44
|
+
for (const el of Array.from(container.querySelectorAll("*"))) {
|
|
45
|
+
for (const attr of Array.from(el.attributes)) {
|
|
46
|
+
if (RAW_HEX_RE.test(attr.value)) {
|
|
47
|
+
offenders.push(`${el.tagName}[${attr.name}]="${attr.value}"`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
expect(offenders).toEqual([]);
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
it("accepts a custom `size` and applies it as width/height", () => {
|
|
56
|
+
const { container } = render(<EmptyListIllustration size="10rem" />);
|
|
57
|
+
const svg = container.querySelector("svg");
|
|
58
|
+
expect(svg).toHaveAttribute("width", "10rem");
|
|
59
|
+
expect(svg).toHaveAttribute("height", "10rem");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("defaults to a rem-based size within the legible 64px–160px range", () => {
|
|
63
|
+
const { container } = render(<NoResultsIllustration />);
|
|
64
|
+
const svg = container.querySelector("svg");
|
|
65
|
+
const width = svg?.getAttribute("width") ?? "";
|
|
66
|
+
expect(width).toMatch(/rem$/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("merges a caller className without dropping the base classes", () => {
|
|
70
|
+
const { container } = render(<SuccessIllustration className="text-primary" />);
|
|
71
|
+
const svg = container.querySelector("svg");
|
|
72
|
+
expect(svg?.getAttribute("class")).toContain("text-primary");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// #12/#53 review (P2): `ILLUSTRATION_ACCENT_VAR` is the mechanism `StatePanel`
|
|
76
|
+
// uses internally to safely retint an illustration's meaning-bearing accent
|
|
77
|
+
// (see illustration-base.tsx). A consumer rendering an illustration OUTSIDE
|
|
78
|
+
// `StatePanel` needs the same seam without duplicating the private
|
|
79
|
+
// "--illustration-accent" string, so the barrel must export it.
|
|
80
|
+
it("exports ILLUSTRATION_ACCENT_VAR — the public retint seam for consumers outside StatePanel", () => {
|
|
81
|
+
expect(ILLUSTRATION_ACCENT_VAR).toBe("--illustration-accent");
|
|
82
|
+
// A consumer can set it inline exactly the way StatePanel does internally.
|
|
83
|
+
const { container } = render(
|
|
84
|
+
<div style={{ [ILLUSTRATION_ACCENT_VAR]: "var(--success-text)" } as CSSProperties}>
|
|
85
|
+
<SuccessIllustration />
|
|
86
|
+
</div>,
|
|
87
|
+
);
|
|
88
|
+
const wrapper = container.firstChild as HTMLElement;
|
|
89
|
+
expect(wrapper.style.getPropertyValue(ILLUSTRATION_ACCENT_VAR)).toBe("var(--success-text)");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("exports illustrationAccent — the public helper for building an illustration's fallback stroke/fill color", () => {
|
|
93
|
+
const fallback = "var(--success-text)";
|
|
94
|
+
const result = illustrationAccent(fallback);
|
|
95
|
+
// illustrationAccent(fallback) returns var(--illustration-accent, fallback)
|
|
96
|
+
expect(result).toBe(`var(${ILLUSTRATION_ACCENT_VAR}, ${fallback})`);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type { IllustrationProps } from "./illustration-base";
|
|
2
|
+
// The custom-property name a consumer sets to safely retint an illustration's
|
|
3
|
+
// meaning-bearing accent (see `StatePanel`'s own usage). Previously only the
|
|
4
|
+
// illustration components + `IllustrationProps` were exported here, so a
|
|
5
|
+
// consumer rendering an illustration OUTSIDE `StatePanel` had no way to reach
|
|
6
|
+
// this seam without duplicating the private string (#12/#53 review, P2).
|
|
7
|
+
// Relates to the wider "illustration customization" tracking issue #46 — this
|
|
8
|
+
// change exports the existing constant, it does not itself resolve #46.
|
|
9
|
+
export { ILLUSTRATION_ACCENT_VAR, illustrationAccent } from "./illustration-base";
|
|
10
|
+
export { EmptyListIllustration } from "./empty-list-illustration";
|
|
11
|
+
export { NoResultsIllustration } from "./no-results-illustration";
|
|
12
|
+
export { NoAccessIllustration } from "./no-access-illustration";
|
|
13
|
+
export { ErrorIllustration } from "./error-illustration";
|
|
14
|
+
export { OfflineIllustration } from "./offline-illustration";
|
|
15
|
+
export { SuccessIllustration } from "./success-illustration";
|
|
16
|
+
export { FirstRunIllustration } from "./first-run-illustration";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
illustrationSvgProps,
|
|
4
|
+
illustrationAccent,
|
|
5
|
+
type IllustrationProps,
|
|
6
|
+
} from "./illustration-base";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Permission / access-denied state. A line-art padlock with a `--primary-text`
|
|
10
|
+
* keyhole — reads as "restricted", not "broken". The keyhole is meaning-bearing
|
|
11
|
+
* (per the accent-semantics convention in `illustration-base.tsx`, #48
|
|
12
|
+
* finding 1) — it's what distinguishes a padlock from a generic rounded box.
|
|
13
|
+
*/
|
|
14
|
+
export const NoAccessIllustration = forwardRef<SVGSVGElement, IllustrationProps>(
|
|
15
|
+
function NoAccessIllustration({ size, className, ...props }, ref) {
|
|
16
|
+
return (
|
|
17
|
+
<svg
|
|
18
|
+
ref={ref}
|
|
19
|
+
{...illustrationSvgProps(size, className)}
|
|
20
|
+
data-slot="no-access-illustration"
|
|
21
|
+
{...props}
|
|
22
|
+
>
|
|
23
|
+
<path d="M50,72 V52 A30,20 0 0 1 110,52 V72" />
|
|
24
|
+
<rect x="36" y="72" width="88" height="48" rx="10" />
|
|
25
|
+
<circle
|
|
26
|
+
cx="80"
|
|
27
|
+
cy="92"
|
|
28
|
+
r="6"
|
|
29
|
+
style={{ stroke: illustrationAccent("var(--primary-text)") }}
|
|
30
|
+
strokeWidth={3}
|
|
31
|
+
/>
|
|
32
|
+
<rect
|
|
33
|
+
x="77"
|
|
34
|
+
y="98"
|
|
35
|
+
width="6"
|
|
36
|
+
height="12"
|
|
37
|
+
rx="2"
|
|
38
|
+
style={{ stroke: illustrationAccent("var(--primary-text)") }}
|
|
39
|
+
strokeWidth={3}
|
|
40
|
+
/>
|
|
41
|
+
</svg>
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
NoAccessIllustration.displayName = "NoAccessIllustration";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
illustrationSvgProps,
|
|
4
|
+
illustrationAccent,
|
|
5
|
+
type IllustrationProps,
|
|
6
|
+
} from "./illustration-base";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Search / filter with zero matches. A line-art magnifying glass with a
|
|
10
|
+
* `--primary-text` "×" inside the lens standing in for "nothing matched".
|
|
11
|
+
* The "×" is meaning-bearing (per the accent-semantics convention in
|
|
12
|
+
* `illustration-base.tsx`, #48 finding 1) — a bare magnifying glass alone
|
|
13
|
+
* reads as "search", not "no results".
|
|
14
|
+
*/
|
|
15
|
+
export const NoResultsIllustration = forwardRef<SVGSVGElement, IllustrationProps>(
|
|
16
|
+
function NoResultsIllustration({ size, className, ...props }, ref) {
|
|
17
|
+
return (
|
|
18
|
+
<svg
|
|
19
|
+
ref={ref}
|
|
20
|
+
{...illustrationSvgProps(size, className)}
|
|
21
|
+
data-slot="no-results-illustration"
|
|
22
|
+
{...props}
|
|
23
|
+
>
|
|
24
|
+
<circle cx="68" cy="66" r="30" />
|
|
25
|
+
<line x1="89.2" y1="87.2" x2="124" y2="122" />
|
|
26
|
+
<line
|
|
27
|
+
x1="56"
|
|
28
|
+
y1="54"
|
|
29
|
+
x2="80"
|
|
30
|
+
y2="78"
|
|
31
|
+
style={{ stroke: illustrationAccent("var(--primary-text)") }}
|
|
32
|
+
strokeWidth={3}
|
|
33
|
+
/>
|
|
34
|
+
<line
|
|
35
|
+
x1="80"
|
|
36
|
+
y1="54"
|
|
37
|
+
x2="56"
|
|
38
|
+
y2="78"
|
|
39
|
+
style={{ stroke: illustrationAccent("var(--primary-text)") }}
|
|
40
|
+
strokeWidth={3}
|
|
41
|
+
/>
|
|
42
|
+
</svg>
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
NoResultsIllustration.displayName = "NoResultsIllustration";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import { illustrationSvgProps, type IllustrationProps } from "./illustration-base";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Disconnected / no-network state. A line-art cloud crossed by a slash — the
|
|
6
|
+
* universal "disabled/no signal" idiom — reads as "offline" on its own
|
|
7
|
+
* silhouette. No accent mark, per the accent-semantics convention in
|
|
8
|
+
* `illustration-base.tsx` (#48 finding 1).
|
|
9
|
+
*
|
|
10
|
+
* Redrawn for #24 fix round 1 (P1-4) — the previous cloud path's closing curve
|
|
11
|
+
* left a visible hook/spur, and the dashed tether + falling node read as a
|
|
12
|
+
* raindrop rather than a severed connection while pulling the ink centre 11.5
|
|
13
|
+
* units below the shared stage centre.
|
|
14
|
+
*
|
|
15
|
+
* Redrawn again for #48 finding 4 — the two small rounded-rect "chain link"
|
|
16
|
+
* fragments below the slash read as loose debris/dashes rather than a
|
|
17
|
+
* "broken link" gesture. Removed rather than redrawn: the cloud+slash
|
|
18
|
+
* silhouette already fully conveys "offline" without them (the same
|
|
19
|
+
* reasoning that dropped the decorative accents from `EmptyListIllustration`/
|
|
20
|
+
* `FirstRunIllustration`).
|
|
21
|
+
*/
|
|
22
|
+
export const OfflineIllustration = forwardRef<SVGSVGElement, IllustrationProps>(
|
|
23
|
+
function OfflineIllustration({ size, className, ...props }, ref) {
|
|
24
|
+
return (
|
|
25
|
+
<svg
|
|
26
|
+
ref={ref}
|
|
27
|
+
{...illustrationSvgProps(size, className)}
|
|
28
|
+
data-slot="offline-illustration"
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
<path d="M44.9,100.4 C44.9,83.6 56.4,72.4 67.9,75.2 C70.2,58.4 86.3,50 97.8,61.2 C111.6,55.6 125.4,69.6 120.8,86.4 C127.7,92 125.4,106 116.2,106 L54.1,106 C49.5,106 44.9,104.6 44.9,100.4 Z" />
|
|
32
|
+
<line x1="36" y1="112" x2="124" y2="36" strokeWidth={4} />
|
|
33
|
+
</svg>
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
OfflineIllustration.displayName = "OfflineIllustration";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
illustrationSvgProps,
|
|
4
|
+
illustrationAccent,
|
|
5
|
+
type IllustrationProps,
|
|
6
|
+
} from "./illustration-base";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Completed / positive-outcome state. A line-art ring with a `--success-text`
|
|
10
|
+
* checkmark — a ring + check needs no rays. The checkmark is meaning-bearing
|
|
11
|
+
* (per the accent-semantics convention in `illustration-base.tsx`, #48
|
|
12
|
+
* finding 1) — a bare ring alone carries no state at all.
|
|
13
|
+
*
|
|
14
|
+
* Redrawn for #24 fix round 1 (P0-1, P1-4) — the checkmark previously used
|
|
15
|
+
* `--primary` (re-creating the "success chips read as primary" confusion #334
|
|
16
|
+
* removed from the tokens) at `strokeWidth={6}` (off the shared accent-weight
|
|
17
|
+
* rule), and the three radiating ticks straddled the now-removed dashed stage
|
|
18
|
+
* (two crossed it, one didn't), reading as a clock/sun rather than a
|
|
19
|
+
* celebration.
|
|
20
|
+
*/
|
|
21
|
+
export const SuccessIllustration = forwardRef<SVGSVGElement, IllustrationProps>(
|
|
22
|
+
function SuccessIllustration({ size, className, ...props }, ref) {
|
|
23
|
+
return (
|
|
24
|
+
<svg
|
|
25
|
+
ref={ref}
|
|
26
|
+
{...illustrationSvgProps(size, className)}
|
|
27
|
+
data-slot="success-illustration"
|
|
28
|
+
{...props}
|
|
29
|
+
>
|
|
30
|
+
<circle cx="80" cy="78" r="44" />
|
|
31
|
+
<path
|
|
32
|
+
d="M59,81 L75,96 L103,60"
|
|
33
|
+
style={{ stroke: illustrationAccent("var(--success-text)") }}
|
|
34
|
+
strokeWidth={3}
|
|
35
|
+
/>
|
|
36
|
+
</svg>
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
SuccessIllustration.displayName = "SuccessIllustration";
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* components require their own peer libs (declared in package.json), e.g.
|
|
10
10
|
* Drawer (vaul), Command (cmdk), Toast (sonner), Calendar (react-day-picker),
|
|
11
11
|
* Carousel (embla-carousel-react), Resizable (react-resizable-panels),
|
|
12
|
-
* Form (react-hook-form + zod)
|
|
12
|
+
* InputOTP (input-otp). Form (react-hook-form + zod) is NOT on this barrel —
|
|
13
|
+
* it lives on the "@elabs-ai/components-ui/form" subpath (issue #26).
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
// Utilities
|
|
@@ -64,6 +65,63 @@ export {
|
|
|
64
65
|
type StreamdownTranslationKey,
|
|
65
66
|
type StreamdownTranslationMap,
|
|
66
67
|
} from "./lib/streamdown-translations";
|
|
68
|
+
// A verification verdict (linter/type-check/test run/policy hook) — shared
|
|
69
|
+
// vocabulary between `ChangeReview` (this package) and `ai`'s `AgentEvent`
|
|
70
|
+
// (docs/decisions/2026-09-01-brainless-adoption-architecture.md § 6). `ai`
|
|
71
|
+
// imports this rather than declaring its own.
|
|
72
|
+
export { type CheckResult, type CheckSummary } from "./lib/check-result";
|
|
73
|
+
// One elapsed-time formatter for every "how long did this take" surface —
|
|
74
|
+
// `TurnStatus` (ai), `ChangeReview`'s check rows, `AgentEvent`'s hook
|
|
75
|
+
// durations (§ 6 / § 6.1 of the doc above).
|
|
76
|
+
export { formatElapsed } from "./lib/format-duration";
|
|
77
|
+
// Optional-peer detection, promoted from `@elabs-ai/components-ai` (§ 2 of the
|
|
78
|
+
// doc above) — `ui` is upstream of every layer-2 package, so this is the one
|
|
79
|
+
// legal home for a helper shared by DAG siblings.
|
|
80
|
+
export { isModuleNotFoundMessage, isOptionalPeerMissing } from "./lib/optional-peer";
|
|
81
|
+
// The generic trigger-driven-query algorithm behind `MentionInput`'s popup,
|
|
82
|
+
// generalised (§ 5 of the doc above) so a second trigger surface (a
|
|
83
|
+
// slash-command palette) reuses it instead of re-implementing it.
|
|
84
|
+
export {
|
|
85
|
+
findTriggerQuery,
|
|
86
|
+
replaceTriggerRun,
|
|
87
|
+
type FindTriggerQueryOptions,
|
|
88
|
+
type TriggerBoundary,
|
|
89
|
+
type TriggerQuery,
|
|
90
|
+
} from "./lib/trigger-query";
|
|
91
|
+
// The T0 promotion wave (issue #117 — see
|
|
92
|
+
// docs/decisions/2026-09-01-brainless-adoption-architecture.md § 4/§ 10(b)):
|
|
93
|
+
// headless models/helpers behind several `@elabs-ai/components-ai` composer
|
|
94
|
+
// and transcript surfaces, promoted here (never duplicated) because
|
|
95
|
+
// `@elabs-ai/components-ai` and `@elabs-ai/components-terminal` are layer-2
|
|
96
|
+
// DAG siblings that may not import each other — `ui` is upstream of both.
|
|
97
|
+
export { defaultSlashCommandFilter, stepIndex, type SlashCommand } from "./lib/slash-command";
|
|
98
|
+
export {
|
|
99
|
+
APPROVAL_SCOPE_DESCRIPTION_KEYS,
|
|
100
|
+
type ApprovalOption,
|
|
101
|
+
type ApprovalScope,
|
|
102
|
+
} from "./lib/approval-option";
|
|
103
|
+
export {
|
|
104
|
+
diffLineAccessibleLabel,
|
|
105
|
+
diffLineMarker,
|
|
106
|
+
type DiffLine,
|
|
107
|
+
type DiffLineType,
|
|
108
|
+
} from "./lib/diff-line";
|
|
109
|
+
export { collapseDiffRows, useDiffRows, type DiffRow } from "./lib/diff-rows";
|
|
110
|
+
export { effortRungForIndex, type EffortLevel, type OperatingMode } from "./lib/operating-mode";
|
|
111
|
+
export {
|
|
112
|
+
agentEventOutcomeStatus,
|
|
113
|
+
type AgentEventOutcome,
|
|
114
|
+
type AgentEventPhase,
|
|
115
|
+
} from "./lib/agent-event-model";
|
|
116
|
+
export {
|
|
117
|
+
type SessionCapability,
|
|
118
|
+
type SessionQuickAction,
|
|
119
|
+
type SessionWhatsNewItem,
|
|
120
|
+
} from "./lib/session-launch";
|
|
121
|
+
// Empty/error/success/etc. state illustrations for StatePanel's `illustration`
|
|
122
|
+
// slot (#24) — token-driven line art, kept alongside the utilities above
|
|
123
|
+
// because it lives outside the folder-per-component `./components/` tree.
|
|
124
|
+
export * from "./illustrations";
|
|
67
125
|
|
|
68
126
|
// Components
|
|
69
127
|
export * from "./components/accordion";
|
|
@@ -91,8 +149,10 @@ export * from "./components/collapsible-panel";
|
|
|
91
149
|
export * from "./components/color-picker";
|
|
92
150
|
export * from "./components/combobox";
|
|
93
151
|
export * from "./components/command";
|
|
152
|
+
export * from "./components/command-trigger";
|
|
94
153
|
export * from "./components/confirm-dialog";
|
|
95
154
|
export * from "./components/context-menu";
|
|
155
|
+
export * from "./components/context-rail";
|
|
96
156
|
export * from "./components/copyable-value";
|
|
97
157
|
export * from "./components/date-picker";
|
|
98
158
|
export * from "./components/date-range-picker";
|
|
@@ -103,15 +163,24 @@ export * from "./components/dropdown-menu";
|
|
|
103
163
|
export * from "./components/empty-state";
|
|
104
164
|
export * from "./components/error-state";
|
|
105
165
|
export * from "./components/expand-dialog";
|
|
166
|
+
export * from "./components/field";
|
|
106
167
|
export * from "./components/field-row";
|
|
107
168
|
export * from "./components/file-upload";
|
|
108
|
-
|
|
169
|
+
// `Form`/`FormField`/… are NOT re-exported from the main barrel — they are the
|
|
170
|
+
// ONLY module in this package that imports `react-hook-form`/`@hookform/resolvers`
|
|
171
|
+
// (peer, optional per issue #26). Re-exporting them here would pull that static
|
|
172
|
+
// import into every consumer's bundled `dist/index.js` (bundlers must resolve a
|
|
173
|
+
// top-level ESM import to build the module graph, even if the binding is never
|
|
174
|
+
// used), forcing react-hook-form on every consumer regardless of whether they
|
|
175
|
+
// use Form. Import the RHF-bound family from the dedicated subpath instead:
|
|
176
|
+
// `import { Form, FormField, … } from "@elabs-ai/components-ui/form"`.
|
|
109
177
|
export * from "./components/hover-card";
|
|
110
178
|
export * from "./components/icon-button";
|
|
111
179
|
export * from "./components/input";
|
|
112
180
|
export * from "./components/input-group";
|
|
113
181
|
export * from "./components/input-otp";
|
|
114
182
|
export * from "./components/kbd";
|
|
183
|
+
export * from "./components/keyboard-shortcuts";
|
|
115
184
|
export * from "./components/key-value-editor";
|
|
116
185
|
export * from "./components/label";
|
|
117
186
|
export * from "./components/link-preview";
|
|
@@ -136,14 +205,17 @@ export * from "./components/radio-group";
|
|
|
136
205
|
export * from "./components/rating";
|
|
137
206
|
export * from "./components/resizable";
|
|
138
207
|
export * from "./components/reveal";
|
|
208
|
+
export * from "./components/schema-form";
|
|
139
209
|
export * from "./components/scroll-area";
|
|
140
210
|
export * from "./components/section-header";
|
|
141
211
|
export * from "./components/segmented-field";
|
|
142
212
|
export * from "./components/select";
|
|
143
213
|
export * from "./components/separator";
|
|
144
214
|
export * from "./components/sheet";
|
|
215
|
+
export * from "./components/side-dock";
|
|
145
216
|
export * from "./components/sidebar";
|
|
146
217
|
export * from "./components/skeleton";
|
|
218
|
+
export * from "./components/skip-link";
|
|
147
219
|
export * from "./components/slider";
|
|
148
220
|
export * from "./components/slider-number";
|
|
149
221
|
export * from "./components/sonner";
|
|
@@ -171,4 +243,5 @@ export * from "./components/tree-select";
|
|
|
171
243
|
export * from "./components/typography";
|
|
172
244
|
export * from "./components/view-toolbar";
|
|
173
245
|
export * from "./components/virtual-select";
|
|
246
|
+
export * from "./components/workspace-picker";
|
|
174
247
|
export * from "./components/wizard";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { agentEventOutcomeStatus, type AgentEventOutcome } from "./agent-event-model";
|
|
3
|
+
|
|
4
|
+
describe("agentEventOutcomeStatus", () => {
|
|
5
|
+
it.each([
|
|
6
|
+
["ok", "complete"],
|
|
7
|
+
["blocked", "denied"],
|
|
8
|
+
["failed", "failed"],
|
|
9
|
+
] as [AgentEventOutcome, string][])("maps %s to the %s status", (outcome, expected) => {
|
|
10
|
+
expect(agentEventOutcomeStatus(outcome)).toBe(expected);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lifecycle/hook-event model behind `AgentEvent` (`@elabs-ai/components-ai`,
|
|
3
|
+
* #109) and the terminal CLI look-alike family's own event line (issue #117).
|
|
4
|
+
*
|
|
5
|
+
* Promoted here (not duplicated) because `@elabs-ai/components-ai` and
|
|
6
|
+
* `@elabs-ai/components-terminal` are layer-2 DAG SIBLINGS and may not import
|
|
7
|
+
* each other — `ui` is upstream of both
|
|
8
|
+
* (docs/decisions/2026-09-01-brainless-adoption-architecture.md § 4).
|
|
9
|
+
* `agentEventOutcomeStatus` maps onto the existing closed 7-state `Status`
|
|
10
|
+
* (`components/status-badge/status-badge.tsx`), which already lives in
|
|
11
|
+
* `ui` — no eighth status is introduced.
|
|
12
|
+
*/
|
|
13
|
+
import type { Status } from "../components/status-badge/status-badge";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* When an event fired relative to the action it gates. A separate, WIDER
|
|
17
|
+
* vocabulary than `CheckResult.phase` (`"before" | "after"`, per-check): this
|
|
18
|
+
* one also admits `"lifecycle"` for events with no gated action (a turn's
|
|
19
|
+
* `user_prompt_submit`/`stop`). The two `phase` concepts answer different
|
|
20
|
+
* questions — "when did this event fire" vs "when did this check run" — and
|
|
21
|
+
* are never unified (§ 6.1 of the architecture decision above).
|
|
22
|
+
*/
|
|
23
|
+
export type AgentEventPhase = "before" | "after" | "lifecycle";
|
|
24
|
+
|
|
25
|
+
/** An event's verdict. Maps onto the existing closed `Status` — see `agentEventOutcomeStatus`. */
|
|
26
|
+
export type AgentEventOutcome = "ok" | "blocked" | "failed";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `outcome` → the canonical 7-state `Status`. No new status value is added
|
|
30
|
+
* (issue #109 acceptance criteria); `blocked` maps to `denied` — a runtime
|
|
31
|
+
* hook refusing an action is the same shape as a human denying one.
|
|
32
|
+
*/
|
|
33
|
+
export function agentEventOutcomeStatus(outcome: AgentEventOutcome): Status {
|
|
34
|
+
switch (outcome) {
|
|
35
|
+
case "ok":
|
|
36
|
+
return "complete";
|
|
37
|
+
case "blocked":
|
|
38
|
+
return "denied";
|
|
39
|
+
case "failed":
|
|
40
|
+
return "failed";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { APPROVAL_SCOPE_DESCRIPTION_KEYS, type ApprovalScope } from "./approval-option";
|
|
3
|
+
|
|
4
|
+
describe("APPROVAL_SCOPE_DESCRIPTION_KEYS", () => {
|
|
5
|
+
it("has one message key per scope", () => {
|
|
6
|
+
const scopes: ApprovalScope[] = ["once", "session", "always", "deny"];
|
|
7
|
+
for (const scope of scopes) {
|
|
8
|
+
expect(typeof APPROVAL_SCOPE_DESCRIPTION_KEYS[scope]).toBe("string");
|
|
9
|
+
expect(APPROVAL_SCOPE_DESCRIPTION_KEYS[scope].length).toBeGreaterThan(0);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("keys are distinct per scope", () => {
|
|
14
|
+
const values = Object.values(APPROVAL_SCOPE_DESCRIPTION_KEYS);
|
|
15
|
+
expect(new Set(values).size).toBe(values.length);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The N-option, scoped-approval model behind `ApprovalCardOptions`
|
|
3
|
+
* (`@elabs-ai/components-ai`'s `confirmation.tsx`, #103) and the terminal CLI
|
|
4
|
+
* look-alike family's own permission row (issue #117) — a real coding-agent
|
|
5
|
+
* permission prompt is rarely a plain yes/no; it asks an N-option question
|
|
6
|
+
* whose options encode SCOPE ("Yes", "Yes, and don't ask again this
|
|
7
|
+
* session", "No, and tell the agent what to do instead").
|
|
8
|
+
*
|
|
9
|
+
* Promoted here (not duplicated) because `@elabs-ai/components-ai` and
|
|
10
|
+
* `@elabs-ai/components-terminal` are layer-2 DAG SIBLINGS and may not import
|
|
11
|
+
* each other — `ui` is upstream of both
|
|
12
|
+
* (docs/decisions/2026-09-01-brainless-adoption-architecture.md § 4). The
|
|
13
|
+
* SYMBOLS have zero SDK coupling; only their previous host module
|
|
14
|
+
* (`confirmation.tsx`) imports `ai`'s `ToolUIPart`, so only the vocabulary —
|
|
15
|
+
* never the component — moves.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* How far a chosen option's permission reaches. Deliberately a closed,
|
|
20
|
+
* app-agnostic vocabulary (not a free string) so a consumer can derive an
|
|
21
|
+
* accessible fallback description per option (see
|
|
22
|
+
* `APPROVAL_SCOPE_DESCRIPTION_KEYS` below) — the one thing every option of a
|
|
23
|
+
* given scope always means, regardless of the agent-specific label wording.
|
|
24
|
+
*/
|
|
25
|
+
export type ApprovalScope = "once" | "session" | "always" | "deny";
|
|
26
|
+
|
|
27
|
+
export interface ApprovalOption {
|
|
28
|
+
id: string;
|
|
29
|
+
label: string;
|
|
30
|
+
/**
|
|
31
|
+
* What choosing this option actually does. Optional: when omitted, the
|
|
32
|
+
* consumer renders a scope-derived sentence instead, so scope always
|
|
33
|
+
* reaches assistive tech through real text — never only through colour or
|
|
34
|
+
* a `data-*` attribute (WCAG 1.4.1).
|
|
35
|
+
*/
|
|
36
|
+
description?: string;
|
|
37
|
+
scope: ApprovalScope;
|
|
38
|
+
/** Optional key hint shown beside the option, e.g. "1". */
|
|
39
|
+
keyHint?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The locale-message KEY (not the translated text — `ui`'s locale catalogue,
|
|
44
|
+
* `components/locale-provider/messages.ts`, holds the English fallback and
|
|
45
|
+
* every other locale's translation) for the scope-derived sentence used only
|
|
46
|
+
* when `option.description` is omitted.
|
|
47
|
+
*/
|
|
48
|
+
export const APPROVAL_SCOPE_DESCRIPTION_KEYS: Record<ApprovalScope, string> = {
|
|
49
|
+
once: "ai.approvalCard.scopeOnceDescription",
|
|
50
|
+
session: "ai.approvalCard.scopeSessionDescription",
|
|
51
|
+
always: "ai.approvalCard.scopeAlwaysDescription",
|
|
52
|
+
deny: "ai.approvalCard.scopeDenyDescription",
|
|
53
|
+
};
|