@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,566 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { expect, userEvent, waitFor, within } from "storybook/test";
|
|
4
|
+
import {
|
|
5
|
+
SchemaForm,
|
|
6
|
+
SchemaFormFields,
|
|
7
|
+
SchemaFormProvider,
|
|
8
|
+
SchemaFormRoot,
|
|
9
|
+
SchemaFormSubmit,
|
|
10
|
+
SchemaFormTestAction,
|
|
11
|
+
SchemaFormTitle,
|
|
12
|
+
} from "./schema-form";
|
|
13
|
+
import type { FormSpec, FormValues } from "./schema-form-spec";
|
|
14
|
+
import { fromJsonSchema } from "./from-json-schema";
|
|
15
|
+
|
|
16
|
+
const meta = {
|
|
17
|
+
title: "Forms/SchemaForm",
|
|
18
|
+
component: SchemaForm,
|
|
19
|
+
tags: ["autodocs"],
|
|
20
|
+
parameters: { layout: "padded" },
|
|
21
|
+
} satisfies Meta<typeof SchemaForm>;
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof meta>;
|
|
24
|
+
|
|
25
|
+
// A representative "connector settings" spec — the Onyx-style shape issue #22
|
|
26
|
+
// asks for: scalars, a repeating list, a repeating key/value list, a file
|
|
27
|
+
// upload, and two flavors of grouped alternative fields (mutually-exclusive
|
|
28
|
+
// tabs vs. always-visible advanced disclosures).
|
|
29
|
+
const connectorSpec: FormSpec = {
|
|
30
|
+
formName: "connector_settings",
|
|
31
|
+
title: "Connector settings",
|
|
32
|
+
description: "Configure how this connector syncs data into your workspace.",
|
|
33
|
+
submitLabel: "Save connector",
|
|
34
|
+
fields: [
|
|
35
|
+
{ type: "string", name: "name", label: "Connector name", required: true, minLength: 2 },
|
|
36
|
+
{
|
|
37
|
+
type: "string",
|
|
38
|
+
name: "baseUrl",
|
|
39
|
+
label: "Base URL",
|
|
40
|
+
format: "uri",
|
|
41
|
+
required: true,
|
|
42
|
+
description: "The root endpoint this connector reads from.",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: "integer",
|
|
46
|
+
name: "pollIntervalMinutes",
|
|
47
|
+
label: "Poll interval (minutes)",
|
|
48
|
+
min: 1,
|
|
49
|
+
max: 1440,
|
|
50
|
+
default: 15,
|
|
51
|
+
},
|
|
52
|
+
{ type: "boolean", name: "enabled", label: "Enabled", default: true },
|
|
53
|
+
{
|
|
54
|
+
type: "enum",
|
|
55
|
+
name: "environment",
|
|
56
|
+
label: "Environment",
|
|
57
|
+
required: true,
|
|
58
|
+
options: [
|
|
59
|
+
{ const: "dev", title: "Development" },
|
|
60
|
+
{ const: "staging", title: "Staging" },
|
|
61
|
+
{ const: "prod", title: "Production" },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
type: "multi-enum",
|
|
66
|
+
name: "tags",
|
|
67
|
+
label: "Tags",
|
|
68
|
+
options: ["Docs", "Code", "Tickets", "Chat"],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
type: "list",
|
|
72
|
+
name: "allowedDomains",
|
|
73
|
+
label: "Allowed domains",
|
|
74
|
+
itemPlaceholder: "example.com",
|
|
75
|
+
maxItems: 10,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: "key-value",
|
|
79
|
+
name: "headers",
|
|
80
|
+
label: "Request headers",
|
|
81
|
+
keyPlaceholder: "Header",
|
|
82
|
+
valuePlaceholder: "Value",
|
|
83
|
+
description: "Sent with every outbound request.",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: "file",
|
|
87
|
+
name: "credentialsFile",
|
|
88
|
+
label: "Service-account credentials",
|
|
89
|
+
accept: ".json",
|
|
90
|
+
maxSize: 1024 * 1024,
|
|
91
|
+
description: "A JSON key file, up to 1 MB.",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
type: "group",
|
|
95
|
+
name: "auth",
|
|
96
|
+
label: "Authentication",
|
|
97
|
+
variant: "tabs",
|
|
98
|
+
groups: [
|
|
99
|
+
{
|
|
100
|
+
key: "apiKey",
|
|
101
|
+
label: "API key",
|
|
102
|
+
fields: [{ type: "string", name: "apiKey", label: "API key", required: true }],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
key: "oauth",
|
|
106
|
+
label: "OAuth",
|
|
107
|
+
fields: [
|
|
108
|
+
{ type: "string", name: "clientId", label: "Client ID", required: true },
|
|
109
|
+
{ type: "string", name: "clientSecret", label: "Client secret", required: true },
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
type: "group",
|
|
116
|
+
name: "advanced",
|
|
117
|
+
label: "Advanced",
|
|
118
|
+
variant: "advanced",
|
|
119
|
+
groups: [
|
|
120
|
+
{
|
|
121
|
+
key: "rateLimiting",
|
|
122
|
+
label: "Rate limiting",
|
|
123
|
+
fields: [
|
|
124
|
+
{
|
|
125
|
+
type: "integer",
|
|
126
|
+
name: "requestsPerMinute",
|
|
127
|
+
label: "Requests per minute",
|
|
128
|
+
min: 1,
|
|
129
|
+
default: 60,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
key: "retry",
|
|
135
|
+
label: "Retry policy",
|
|
136
|
+
fields: [
|
|
137
|
+
{ type: "integer", name: "maxRetries", label: "Max retries", min: 0, default: 3 },
|
|
138
|
+
{ type: "number", name: "backoffSeconds", label: "Backoff (seconds)", min: 0 },
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/** First run: an uncontrolled form, nothing submitted yet. Every field type + both group variants render. */
|
|
147
|
+
export const Default: Story = {
|
|
148
|
+
args: {
|
|
149
|
+
spec: connectorSpec,
|
|
150
|
+
onSubmit: (state) => console.info("submit", state),
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** Controlled — the parent owns `values`; a live JSON preview reflects them as you type. */
|
|
155
|
+
export const Controlled: Story = {
|
|
156
|
+
render: function Controlled() {
|
|
157
|
+
const [values, setValues] = useState<FormValues>({ environment: "staging", enabled: true });
|
|
158
|
+
return (
|
|
159
|
+
<div className="flex flex-col gap-4">
|
|
160
|
+
<SchemaForm
|
|
161
|
+
spec={connectorSpec}
|
|
162
|
+
values={values}
|
|
163
|
+
onChange={setValues}
|
|
164
|
+
onSubmit={(state) => console.info("submit", state)}
|
|
165
|
+
/>
|
|
166
|
+
<pre className="overflow-x-auto rounded-md border border-border bg-surface-muted p-3 text-caption text-muted-foreground">
|
|
167
|
+
{JSON.stringify(values, null, 2)}
|
|
168
|
+
</pre>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Conditional visibility (`visibleWhen: { field, equals }`, issue #22) — the
|
|
176
|
+
* "client secret" field only renders (and only participates in validation)
|
|
177
|
+
* once "Auth method" is switched to OAuth; while hidden it can never block
|
|
178
|
+
* submit, even though it's `required`.
|
|
179
|
+
*/
|
|
180
|
+
export const ConditionalField: Story = {
|
|
181
|
+
args: {
|
|
182
|
+
spec: {
|
|
183
|
+
formName: "connector_auth",
|
|
184
|
+
title: "Authentication",
|
|
185
|
+
fields: [
|
|
186
|
+
{
|
|
187
|
+
type: "enum",
|
|
188
|
+
name: "authMethod",
|
|
189
|
+
label: "Auth method",
|
|
190
|
+
default: "apikey",
|
|
191
|
+
options: [
|
|
192
|
+
{ const: "apikey", title: "API key" },
|
|
193
|
+
{ const: "oauth", title: "OAuth" },
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
type: "string",
|
|
198
|
+
name: "clientSecret",
|
|
199
|
+
label: "Client secret",
|
|
200
|
+
required: true,
|
|
201
|
+
visibleWhen: { field: "authMethod", equals: "oauth" },
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
play: async ({ canvasElement }) => {
|
|
207
|
+
const canvas = within(canvasElement);
|
|
208
|
+
const body = within(canvasElement.ownerDocument.body);
|
|
209
|
+
|
|
210
|
+
// Hidden while the default "API key" method is active.
|
|
211
|
+
expect(canvas.queryByLabelText(/Client secret/)).not.toBeInTheDocument();
|
|
212
|
+
|
|
213
|
+
// Switch to OAuth through the real Select control.
|
|
214
|
+
await userEvent.click(canvas.getByRole("combobox", { name: /Auth method/ }));
|
|
215
|
+
await userEvent.click(await body.findByRole("option", { name: "OAuth" }));
|
|
216
|
+
|
|
217
|
+
// Now it renders...
|
|
218
|
+
await waitFor(() => {
|
|
219
|
+
expect(canvas.getByLabelText(/Client secret/)).toBeInTheDocument();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ...and, being required, blocks submit while empty.
|
|
223
|
+
await userEvent.click(canvas.getByRole("button", { name: "Submit" }));
|
|
224
|
+
await waitFor(() => {
|
|
225
|
+
expect(canvas.getByLabelText(/Client secret/)).toHaveFocus();
|
|
226
|
+
});
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Validation — submitting with missing required fields and a bad email-shaped
|
|
232
|
+
* URL surfaces inline errors and moves focus to the first invalid control.
|
|
233
|
+
*/
|
|
234
|
+
export const Validation: Story = {
|
|
235
|
+
args: {
|
|
236
|
+
spec: {
|
|
237
|
+
formName: "signup",
|
|
238
|
+
title: "Create your workspace",
|
|
239
|
+
fields: [
|
|
240
|
+
{ type: "string", name: "orgName", label: "Organization name", required: true },
|
|
241
|
+
{
|
|
242
|
+
type: "string",
|
|
243
|
+
name: "adminEmail",
|
|
244
|
+
label: "Admin email",
|
|
245
|
+
format: "email",
|
|
246
|
+
required: true,
|
|
247
|
+
},
|
|
248
|
+
{ type: "integer", name: "seats", label: "Seats", min: 1, max: 500 },
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
play: async ({ canvasElement }) => {
|
|
253
|
+
const canvas = within(canvasElement);
|
|
254
|
+
await userEvent.click(canvas.getByRole("button", { name: "Submit" }));
|
|
255
|
+
await waitFor(() => {
|
|
256
|
+
expect(canvas.getAllByText("This field is required.").length).toBeGreaterThan(0);
|
|
257
|
+
});
|
|
258
|
+
// Focus moved to the first invalid field (the a11y "don't leave the user hunting" contract).
|
|
259
|
+
await waitFor(() => {
|
|
260
|
+
expect(canvas.getByLabelText(/Organization name/)).toHaveFocus();
|
|
261
|
+
});
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/** Submitting — the request is in flight: controls are blocked, the submit button shows a spinner and stays focusable (`aria-disabled`, never native `disabled`). */
|
|
266
|
+
export const Submitting: Story = {
|
|
267
|
+
args: { spec: connectorSpec, submitting: true },
|
|
268
|
+
play: async ({ canvasElement }) => {
|
|
269
|
+
const canvas = within(canvasElement);
|
|
270
|
+
const button = canvas.getByRole("button", { name: "Submitting…" });
|
|
271
|
+
// Never natively disabled while transiently blocked — see interaction-guidelines.md.
|
|
272
|
+
expect(button).not.toBeDisabled();
|
|
273
|
+
expect(button).toHaveAttribute("aria-disabled", "true");
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/** Submitted — a terminal, inert record: controls are read-only, values stay visible, the submit button is replaced by a status badge. */
|
|
278
|
+
export const Submitted: Story = {
|
|
279
|
+
args: {
|
|
280
|
+
spec: connectorSpec,
|
|
281
|
+
submitted: true,
|
|
282
|
+
values: {
|
|
283
|
+
name: "Zendesk sync",
|
|
284
|
+
baseUrl: "https://api.zendesk.com",
|
|
285
|
+
pollIntervalMinutes: 30,
|
|
286
|
+
enabled: true,
|
|
287
|
+
environment: "prod",
|
|
288
|
+
tags: ["Tickets", "Chat"],
|
|
289
|
+
allowedDomains: ["zendesk.com"],
|
|
290
|
+
headers: [{ key: "Authorization", value: "Bearer •••", secret: true }],
|
|
291
|
+
auth: "apiKey",
|
|
292
|
+
apiKey: "sk_live_•••",
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/** A terminal, form-level submission error (e.g. the save request failed) rendered above the submit control. */
|
|
298
|
+
export const SubmissionError: Story = {
|
|
299
|
+
args: {
|
|
300
|
+
spec: connectorSpec,
|
|
301
|
+
error: "Couldn't save this connector. Check your network connection and try again.",
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* No fields yet (spec still loading) → a layout-shaped, announced skeleton —
|
|
307
|
+
* never a blank panel. Composed directly from the compound parts
|
|
308
|
+
* (`SchemaFormProvider` → `SchemaFormRoot` → `SchemaFormFields`) rather than
|
|
309
|
+
* the `SchemaForm` convenience wrapper, to also document that lower-level API.
|
|
310
|
+
*/
|
|
311
|
+
export const Loading: Story = {
|
|
312
|
+
render: () => (
|
|
313
|
+
<SchemaFormProvider
|
|
314
|
+
spec={{ formName: "connector_settings", title: "Connector settings", fields: [] }}
|
|
315
|
+
loading={true}
|
|
316
|
+
>
|
|
317
|
+
<SchemaFormRoot>
|
|
318
|
+
<SchemaFormTitle />
|
|
319
|
+
<SchemaFormFields />
|
|
320
|
+
</SchemaFormRoot>
|
|
321
|
+
</SchemaFormProvider>
|
|
322
|
+
),
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/** Empty (not loading) — a spec with zero fields renders a calm fallback, never a broken empty form. */
|
|
326
|
+
export const Empty: Story = {
|
|
327
|
+
args: {
|
|
328
|
+
spec: { formName: "empty", fields: [] },
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
/** Fallback — a malformed / non-object spec renders a typed fallback, never a throw. */
|
|
333
|
+
export const Fallback: Story = {
|
|
334
|
+
args: {
|
|
335
|
+
spec: "not a form spec",
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Overflowing content — long labels, a long description, and many enum
|
|
341
|
+
* options still wrap/scroll gracefully instead of breaking the layout.
|
|
342
|
+
*/
|
|
343
|
+
export const OverflowingContent: Story = {
|
|
344
|
+
args: {
|
|
345
|
+
spec: {
|
|
346
|
+
formName: "overflow",
|
|
347
|
+
title: "Migrate historical tickets from the legacy on-premises support desk",
|
|
348
|
+
description:
|
|
349
|
+
"This one-time migration copies every ticket, attachment, and internal note from the legacy system into the new workspace. Depending on volume this can take several hours; you can safely close this dialog once it starts.",
|
|
350
|
+
fields: [
|
|
351
|
+
{
|
|
352
|
+
type: "string",
|
|
353
|
+
name: "notes",
|
|
354
|
+
label: "Additional context for the migration team handling this request",
|
|
355
|
+
multiline: true,
|
|
356
|
+
description:
|
|
357
|
+
"Include any known data-quality issues, custom fields that must be mapped manually, or dates the migration must avoid.",
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
type: "enum",
|
|
361
|
+
name: "region",
|
|
362
|
+
label: "Data residency region for the migrated archive",
|
|
363
|
+
options: [
|
|
364
|
+
{ const: "us-east-1", title: "US East (N. Virginia) — primary production region" },
|
|
365
|
+
{ const: "us-west-2", title: "US West (Oregon) — disaster-recovery region" },
|
|
366
|
+
{ const: "eu-central-1", title: "EU Central (Frankfurt) — GDPR data-residency region" },
|
|
367
|
+
],
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/** A file field's designed states: a valid file, a rejected wrong-type file, and a rejected too-large file all render distinctly (never a silent drop). */
|
|
375
|
+
export const FileFieldStates: Story = {
|
|
376
|
+
args: {
|
|
377
|
+
spec: {
|
|
378
|
+
formName: "upload",
|
|
379
|
+
title: "Upload credentials",
|
|
380
|
+
fields: [
|
|
381
|
+
{
|
|
382
|
+
type: "file",
|
|
383
|
+
name: "credentialsFile",
|
|
384
|
+
label: "Service-account credentials",
|
|
385
|
+
accept: ".json",
|
|
386
|
+
multiple: true,
|
|
387
|
+
maxSize: 1024,
|
|
388
|
+
description:
|
|
389
|
+
"A JSON key file, up to 1 KB (kept small so this demo can show the too-large state).",
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
play: async ({ canvasElement }) => {
|
|
395
|
+
const canvas = within(canvasElement);
|
|
396
|
+
const input = canvasElement.querySelector('input[type="file"]') as HTMLInputElement;
|
|
397
|
+
|
|
398
|
+
const goodFile = new File(['{"type":"service_account"}'], "creds.json", {
|
|
399
|
+
type: "application/json",
|
|
400
|
+
});
|
|
401
|
+
const wrongTypeFile = new File(["binary"], "creds.exe", { type: "application/x-msdownload" });
|
|
402
|
+
const tooLargeFile = new File([new Uint8Array(2048)], "big.json", {
|
|
403
|
+
type: "application/json",
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// The good file and the too-large file both go through the real picker —
|
|
407
|
+
// `checkFileIssue` (not the native input) is what flags the too-large one,
|
|
408
|
+
// so it still renders with visible feedback instead of vanishing.
|
|
409
|
+
await userEvent.upload(input, [goodFile, tooLargeFile]);
|
|
410
|
+
await waitFor(() => {
|
|
411
|
+
expect(canvas.getByText("creds.json")).toBeInTheDocument();
|
|
412
|
+
expect(canvas.getByText("big.json")).toBeInTheDocument();
|
|
413
|
+
});
|
|
414
|
+
await waitFor(() => {
|
|
415
|
+
expect(canvas.getByText(/larger than the 1\.0 KB limit/)).toBeInTheDocument();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// The wrong-type file goes through a drag-and-drop instead of the native
|
|
419
|
+
// picker: real browsers can filter a programmatic `input.files` set
|
|
420
|
+
// against the field's `accept` hint (the same silent-drop failure mode
|
|
421
|
+
// `maxSize` had), but a drop's `DataTransfer` is never filtered that way —
|
|
422
|
+
// exactly like a real user dragging in a mismatched file — so it reaches
|
|
423
|
+
// `checkFileIssue` and renders with its own visible error.
|
|
424
|
+
const dropzone = canvasElement.querySelector(
|
|
425
|
+
'[data-slot="file-upload-dropzone"]',
|
|
426
|
+
) as HTMLElement;
|
|
427
|
+
const dataTransfer = new DataTransfer();
|
|
428
|
+
dataTransfer.items.add(wrongTypeFile);
|
|
429
|
+
dropzone.dispatchEvent(
|
|
430
|
+
new DragEvent("drop", { bubbles: true, cancelable: true, dataTransfer }),
|
|
431
|
+
);
|
|
432
|
+
await waitFor(() => {
|
|
433
|
+
expect(canvas.getByText("creds.exe")).toBeInTheDocument();
|
|
434
|
+
expect(canvas.getByText("This file type isn't accepted.")).toBeInTheDocument();
|
|
435
|
+
});
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
/** Disabled — the whole form is a deliberate, durable read-only surface (every control natively disabled, not just visually dimmed). */
|
|
440
|
+
export const Disabled: Story = {
|
|
441
|
+
args: { spec: connectorSpec, disabled: true },
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
/** Dark theme — verifies token-driven surfaces + focus rings in dark. Scoped via a `data-theme` decorator, not a `globals` override, so it doesn't re-theme the whole Docs page. */
|
|
445
|
+
export const DarkTheme: Story = {
|
|
446
|
+
args: { spec: connectorSpec },
|
|
447
|
+
decorators: [
|
|
448
|
+
(Story) => (
|
|
449
|
+
<div data-theme="dark" className="rounded-lg bg-background p-6 text-foreground">
|
|
450
|
+
<Story />
|
|
451
|
+
</div>
|
|
452
|
+
),
|
|
453
|
+
],
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const testActionSpec: FormSpec = {
|
|
457
|
+
formName: "connector_test",
|
|
458
|
+
title: "Connect a data source",
|
|
459
|
+
fields: [
|
|
460
|
+
{ type: "string", name: "host", label: "Host", required: true, default: "api.example.com" },
|
|
461
|
+
{ type: "string", name: "apiKey", label: "API key", required: true },
|
|
462
|
+
],
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* `SchemaFormTestAction` (issue #22 maintainer ruling, 2026-09-01) — a
|
|
467
|
+
* form-level "Test connection" affordance, composed alongside
|
|
468
|
+
* `SchemaFormSubmit` but entirely independent of it: it never reads or
|
|
469
|
+
* blocks field validity, and its own pending/success/failure state never
|
|
470
|
+
* gates `submit()`. Placed directly under `SchemaFormRoot`, not inside the
|
|
471
|
+
* default `SchemaForm` composition — it's an opt-in part a consumer places.
|
|
472
|
+
*/
|
|
473
|
+
export const TestConnectionAction: Story = {
|
|
474
|
+
render: function TestConnectionAction() {
|
|
475
|
+
return (
|
|
476
|
+
<SchemaFormProvider spec={testActionSpec} onSubmit={(state) => console.info("submit", state)}>
|
|
477
|
+
<SchemaFormRoot className="flex flex-col gap-6">
|
|
478
|
+
<SchemaFormTitle />
|
|
479
|
+
<SchemaFormFields />
|
|
480
|
+
<div className="flex items-center justify-between gap-4 border-t border-border pt-4">
|
|
481
|
+
<SchemaFormTestAction
|
|
482
|
+
onTest={async (values) => {
|
|
483
|
+
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
484
|
+
if (!values.apiKey) throw new Error("API key is required to test the connection");
|
|
485
|
+
}}
|
|
486
|
+
/>
|
|
487
|
+
<SchemaFormSubmit />
|
|
488
|
+
</div>
|
|
489
|
+
</SchemaFormRoot>
|
|
490
|
+
</SchemaFormProvider>
|
|
491
|
+
);
|
|
492
|
+
},
|
|
493
|
+
play: async ({ canvasElement }) => {
|
|
494
|
+
const canvas = within(canvasElement);
|
|
495
|
+
|
|
496
|
+
// Fill only the field the test action itself checks — the OTHER
|
|
497
|
+
// required field ("Host") is left with its default and never
|
|
498
|
+
// validated by this click, proving the test action doesn't run
|
|
499
|
+
// (or depend on) SchemaForm's own validation.
|
|
500
|
+
await userEvent.type(canvas.getByLabelText(/API key/), "sk-live-demo");
|
|
501
|
+
await userEvent.click(canvas.getByRole("button", { name: "Test connection" }));
|
|
502
|
+
|
|
503
|
+
await waitFor(() => {
|
|
504
|
+
expect(canvas.getByRole("button", { name: "Testing…" })).toBeInTheDocument();
|
|
505
|
+
});
|
|
506
|
+
await waitFor(
|
|
507
|
+
() => {
|
|
508
|
+
expect(canvas.getByText("Connected")).toBeInTheDocument();
|
|
509
|
+
},
|
|
510
|
+
{ timeout: 3000 },
|
|
511
|
+
);
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
// A connector manifest expressed as JSON Schema — the shape `fromJsonSchema()`
|
|
516
|
+
// (issue #22 maintainer ruling, 2026-09-01) targets: an OpenAPI-style request
|
|
517
|
+
// body using only the documented subset (string/enum/number/integer/boolean/
|
|
518
|
+
// array-of-string/object).
|
|
519
|
+
const connectorJsonSchema = {
|
|
520
|
+
type: "object",
|
|
521
|
+
title: "Connector settings",
|
|
522
|
+
description: "Configure how this connector syncs data into your workspace.",
|
|
523
|
+
properties: {
|
|
524
|
+
name: { type: "string", title: "Connector name", minLength: 1 },
|
|
525
|
+
environment: {
|
|
526
|
+
type: "string",
|
|
527
|
+
title: "Environment",
|
|
528
|
+
enum: ["production", "staging", "development"],
|
|
529
|
+
default: "staging",
|
|
530
|
+
},
|
|
531
|
+
syncIntervalMinutes: {
|
|
532
|
+
type: "integer",
|
|
533
|
+
title: "Sync interval (minutes)",
|
|
534
|
+
minimum: 5,
|
|
535
|
+
maximum: 1440,
|
|
536
|
+
default: 60,
|
|
537
|
+
},
|
|
538
|
+
enabled: { type: "boolean", title: "Enabled", default: true },
|
|
539
|
+
tags: { type: "array", title: "Tags", items: { type: "string" } },
|
|
540
|
+
advanced: {
|
|
541
|
+
type: "object",
|
|
542
|
+
title: "Advanced",
|
|
543
|
+
properties: {
|
|
544
|
+
timeoutSeconds: { type: "number", title: "Timeout (seconds)", minimum: 1, default: 30 },
|
|
545
|
+
retries: { type: "integer", title: "Retries", minimum: 0, maximum: 10, default: 3 },
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
required: ["name"],
|
|
550
|
+
} as const;
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* `fromJsonSchema()` (issue #22 maintainer ruling, 2026-09-01) — maps a
|
|
554
|
+
* documented JSON Schema subset (an OpenAPI request body or connector
|
|
555
|
+
* manifest already described that way) onto `FormSpec`, so `SchemaForm`
|
|
556
|
+
* renders it without hand-authoring the field list.
|
|
557
|
+
*/
|
|
558
|
+
export const FromJsonSchema: Story = {
|
|
559
|
+
render: function FromJsonSchema() {
|
|
560
|
+
const spec = fromJsonSchema(connectorJsonSchema, {
|
|
561
|
+
formName: "connector_from_json_schema",
|
|
562
|
+
submitLabel: "Save connector",
|
|
563
|
+
});
|
|
564
|
+
return <SchemaForm spec={spec} onSubmit={(state) => console.info("submit", state)} />;
|
|
565
|
+
},
|
|
566
|
+
};
|