@tangle-network/ui 11.38.2 → 11.40.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/CHANGELOG.md +27 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/presentation.d.ts +1 -1
- package/dist/presentation.js +1 -0
- package/dist/primitives/avatar.d.ts +9 -1
- package/dist/primitives/avatar.js +6 -2
- package/dist/primitives/button.d.ts +2 -1
- package/dist/primitives/field.d.ts +27 -0
- package/dist/primitives/field.js +54 -0
- package/dist/primitives/info-tip.js +50 -0
- package/dist/primitives/page-shell.d.ts +19 -3
- package/dist/primitives/page-shell.js +80 -22
- package/dist/primitives/settings.d.ts +125 -0
- package/dist/primitives/settings.js +316 -0
- package/dist/primitives/switch.js +1 -1
- package/dist/primitives/tooltip.d.ts +4 -1
- package/dist/primitives/tooltip.js +16 -12
- package/dist/primitives.d.ts +4 -2
- package/dist/primitives.js +3 -1
- package/package.json +2 -2
- package/src/presentation.ts +2 -0
- package/src/primitives/avatar.tsx +10 -5
- package/src/primitives/button.tsx +2 -1
- package/src/primitives/field.tsx +63 -0
- package/src/primitives/index.ts +16 -0
- package/src/primitives/info-tip.tsx +40 -0
- package/src/primitives/page-shell.tsx +70 -8
- package/src/primitives/settings.stories.tsx +267 -0
- package/src/primitives/settings.tsx +404 -0
- package/src/primitives/switch.tsx +3 -0
- package/src/primitives/tooltip.tsx +14 -6
- package/src/styles/presentation.css +43 -2
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
1
4
|
import type { ReactNode } from "react";
|
|
2
5
|
import { cn } from "../lib/utils";
|
|
3
6
|
|
|
@@ -7,23 +10,82 @@ export interface PageShellProps {
|
|
|
7
10
|
navigation?: ReactNode;
|
|
8
11
|
article?: ReactNode;
|
|
9
12
|
outline?: ReactNode;
|
|
13
|
+
/** Spans the reading layout above navigation and article, such as the page's `PageHeader`. */
|
|
14
|
+
header?: ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* The reading layout's measure. `reading` (default) is the 42rem document
|
|
17
|
+
* column. `settings` is a 48rem column of cards whose navigation is a sticky
|
|
18
|
+
* 13rem column of rows when its frame is at least 60rem wide, and a sticky
|
|
19
|
+
* 56px tab strip under the header below that; the current link
|
|
20
|
+
* (`aria-current="page"`) is centred by scrolling the strip only. Put
|
|
21
|
+
* links (or a list of links) in `navigation`; an `svg` in a link shows only
|
|
22
|
+
* in the column, and an element with `data-count` sits at its end.
|
|
23
|
+
*/
|
|
24
|
+
measure?: "reading" | "settings";
|
|
10
25
|
navigationLabel?: string;
|
|
11
26
|
outlineLabel?: string;
|
|
12
27
|
className?: string;
|
|
13
28
|
}
|
|
14
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Centre the current link of a horizontally scrolling nav, so the links on
|
|
32
|
+
* both sides show that the strip scrolls. It moves the strip, never the page.
|
|
33
|
+
*/
|
|
34
|
+
function useCurrentLinkInView(ref: React.RefObject<HTMLElement | null>, enabled: boolean) {
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
const nav = ref.current;
|
|
37
|
+
if (!enabled || !nav) return;
|
|
38
|
+
const reveal = () => {
|
|
39
|
+
if (nav.scrollWidth <= nav.clientWidth) return;
|
|
40
|
+
const current = nav.querySelector<HTMLElement>('[aria-current="page"]');
|
|
41
|
+
if (!current) return;
|
|
42
|
+
const strip = nav.getBoundingClientRect();
|
|
43
|
+
const link = current.getBoundingClientRect();
|
|
44
|
+
nav.scrollLeft += link.left + link.width / 2 - (strip.left + strip.width / 2);
|
|
45
|
+
};
|
|
46
|
+
let live = true;
|
|
47
|
+
reveal();
|
|
48
|
+
// A web font swap widens the tabs without resizing the strip, so the
|
|
49
|
+
// resize observer alone would leave the current tab where it was.
|
|
50
|
+
void document.fonts?.ready.then(() => { if (live) reveal(); });
|
|
51
|
+
const resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(reveal);
|
|
52
|
+
resize?.observe(nav);
|
|
53
|
+
// Links that a caller replaces (sections loaded later, a count that
|
|
54
|
+
// changes) move the current tab as surely as a new aria-current does.
|
|
55
|
+
const links = new MutationObserver(reveal);
|
|
56
|
+
links.observe(nav, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ["aria-current"] });
|
|
57
|
+
return () => {
|
|
58
|
+
live = false;
|
|
59
|
+
resize?.disconnect();
|
|
60
|
+
links.disconnect();
|
|
61
|
+
};
|
|
62
|
+
}, [ref, enabled]);
|
|
63
|
+
}
|
|
64
|
+
|
|
15
65
|
/**
|
|
16
66
|
* Ported from sandbox-ui/src/primitives/page-shell.tsx. An optional bounded
|
|
17
67
|
* width, gutter and vertical rhythm, not an application shell or a card grid.
|
|
18
|
-
* The caller owns landmarks, routing, auth, navigation and data state.
|
|
19
|
-
* className
|
|
68
|
+
* The caller owns landmarks, routing, auth, navigation and data state.
|
|
69
|
+
* Without slots, className overrides the bound/gutters, or omit it for a
|
|
70
|
+
* workbench layout. With slots it goes on the reading grid. With
|
|
71
|
+
* `measure="settings"` it goes on the frame, the width the layout answers to,
|
|
72
|
+
* so a `max-w-*` there bounds the measure and a narrow frame gets the strip.
|
|
20
73
|
*/
|
|
21
|
-
export function PageShell({ children, className, navigation, article, outline, navigationLabel = "Sections", outlineLabel = "On this page" }: PageShellProps) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
74
|
+
export function PageShell({ children, className, navigation, article, outline, header, measure = "reading", navigationLabel = "Sections", outlineLabel = "On this page" }: PageShellProps) {
|
|
75
|
+
const navRef = React.useRef<HTMLElement>(null);
|
|
76
|
+
const settings = measure === "settings";
|
|
77
|
+
useCurrentLinkInView(navRef, settings && Boolean(navigation));
|
|
78
|
+
if (navigation || article || outline || header || settings) {
|
|
79
|
+
const shell = <div className={cn("tangle-reading-shell", !settings && className)} data-measure={settings ? "settings" : undefined}>
|
|
80
|
+
{header && <div className="tangle-reading-header">{header}</div>}
|
|
81
|
+
{navigation && <nav ref={navRef} aria-label={navigationLabel} className="tangle-reading-nav">{navigation}</nav>}
|
|
82
|
+
<article className="tangle-reading-article">{article ?? children}</article>
|
|
83
|
+
{outline && <aside aria-label={outlineLabel} className="tangle-reading-outline">{outline}</aside>}
|
|
84
|
+
</div>;
|
|
85
|
+
// The frame is the width container the settings layout answers to, so the
|
|
86
|
+
// navigation switches on the content area's width, not the viewport's.
|
|
87
|
+
return settings ? <div className={cn("tangle-settings-frame", className)}>{shell}</div> : shell;
|
|
88
|
+
}
|
|
27
89
|
return (
|
|
28
90
|
<div
|
|
29
91
|
className={cn(
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { Bell, Bot, Check, CreditCard, Inbox, Loader2, Settings, Users, Workflow } from "lucide-react";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
4
|
+
import { Avatar, AvatarFallback } from "./avatar";
|
|
5
|
+
import { Button } from "./button";
|
|
6
|
+
import { Card } from "./card";
|
|
7
|
+
import { Field, fieldMessageId } from "./field";
|
|
8
|
+
import { Input, Textarea } from "./input";
|
|
9
|
+
import { PageHeader } from "./page-header";
|
|
10
|
+
import { PageShell } from "./page-shell";
|
|
11
|
+
import { SegmentedControl } from "./segmented-control";
|
|
12
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
|
|
13
|
+
import { SettingsList, SettingsListItem, SettingsRow, SettingsSection } from "./settings";
|
|
14
|
+
import { Switch } from "./switch";
|
|
15
|
+
|
|
16
|
+
/** Example data only: no routing, persistence or fetching. Every state is drawn at once so one screenshot shows them all. */
|
|
17
|
+
|
|
18
|
+
const SECTIONS = [
|
|
19
|
+
{ id: "general", label: "General", icon: Settings },
|
|
20
|
+
{ id: "agent", label: "Agent", icon: Bot },
|
|
21
|
+
{ id: "notifications", label: "Notifications", icon: Bell },
|
|
22
|
+
{ id: "automations", label: "Automations", icon: Workflow },
|
|
23
|
+
{ id: "leads", label: "Inbound leads", icon: Inbox },
|
|
24
|
+
{ id: "team", label: "Team", icon: Users, count: 4 },
|
|
25
|
+
{ id: "billing", label: "Ad spend", icon: CreditCard },
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
function Saving() {
|
|
29
|
+
return <><Loader2 aria-hidden="true" className="size-4 animate-spin motion-reduce:animate-none" />Saving…</>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function Saved() {
|
|
33
|
+
return <span className="inline-flex items-center gap-1 text-[var(--surface-success-text)]"><Check aria-hidden="true" className="size-4" />Saved</span>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface Automation { name: string; meta: string; state: string; verb: string; ownerOnly?: boolean; error?: string }
|
|
37
|
+
|
|
38
|
+
const automations: Automation[] = [
|
|
39
|
+
{ name: "Weekly competitor scan", meta: "Mondays at 09:00", state: "Active", verb: "Pause" },
|
|
40
|
+
{ name: "Inbound lead triage", meta: "When a signed webhook arrives", state: "Paused", verb: "Resume", ownerOnly: true },
|
|
41
|
+
{ name: "Daily brief", meta: "Every day at 08:00 · Europe/Berlin", state: "Couldn't start", verb: "Try again", error: "Platform didn't respond. Nothing ran; try again." },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** Pause turns an automation off; Resume and Try again turn it back on. */
|
|
45
|
+
function runVerb(job: Automation): Automation {
|
|
46
|
+
return job.state === "Active" ? { ...job, state: "Paused", verb: "Resume" } : { ...job, state: "Active", verb: "Pause", error: undefined };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const members = [
|
|
50
|
+
{ name: "Priya Raman", email: "priya@fieldnote.example", role: "owner" },
|
|
51
|
+
{ name: "Jonas Weber", email: "jonas@fieldnote.example", role: "admin" },
|
|
52
|
+
{ name: "Ana Lima", email: "ana.lima.growth-operations-team@fieldnote-analytics.example", role: "editor" },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const roles = [
|
|
56
|
+
{ value: "owner", label: "Owner" },
|
|
57
|
+
{ value: "admin", label: "Can manage settings and people" },
|
|
58
|
+
{ value: "editor", label: "Can edit and chat" },
|
|
59
|
+
{ value: "viewer", label: "Can only view" },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
interface SettingsPageProps {
|
|
63
|
+
/** Only the first two sections render at first; the rest arrive 300ms later, as role-gated sections do. */
|
|
64
|
+
sectionsLoadLater?: boolean;
|
|
65
|
+
/** Leave out the section navigation. */
|
|
66
|
+
withoutNavigation?: boolean;
|
|
67
|
+
className?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function SettingsPage({ sectionsLoadLater = false, withoutNavigation = false, className }: SettingsPageProps) {
|
|
71
|
+
const [current, setCurrent] = useState("team");
|
|
72
|
+
const [sections, setSections] = useState<readonly (typeof SECTIONS)[number][]>(sectionsLoadLater ? SECTIONS.slice(0, 2) : SECTIONS);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!sectionsLoadLater) return;
|
|
75
|
+
const loaded = setTimeout(() => setSections(SECTIONS), 300);
|
|
76
|
+
return () => clearTimeout(loaded);
|
|
77
|
+
}, [sectionsLoadLater]);
|
|
78
|
+
const [name, setName] = useState("Fieldnote GTM");
|
|
79
|
+
const [digest, setDigest] = useState(true);
|
|
80
|
+
const [approval, setApproval] = useState("each");
|
|
81
|
+
const [jobs, setJobs] = useState(automations);
|
|
82
|
+
const [roleOf, setRoleOf] = useState<Record<string, string>>(Object.fromEntries(members.map((m) => [m.email, m.role])));
|
|
83
|
+
|
|
84
|
+
const navigation = withoutNavigation ? undefined : sections.map((section) => {
|
|
85
|
+
const Icon = section.icon;
|
|
86
|
+
return (
|
|
87
|
+
<a key={section.id} href={`#${section.id}`} aria-current={current === section.id ? "page" : undefined} onClick={() => setCurrent(section.id)}>
|
|
88
|
+
<Icon aria-hidden="true" />
|
|
89
|
+
<span>{section.label}</span>
|
|
90
|
+
{"count" in section && <span data-count="">{section.count}</span>}
|
|
91
|
+
</a>
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<PageShell
|
|
97
|
+
measure="settings"
|
|
98
|
+
className={className}
|
|
99
|
+
navigationLabel="Settings sections"
|
|
100
|
+
header={<PageHeader title="Project settings" description="Fieldnote GTM" />}
|
|
101
|
+
navigation={navigation}
|
|
102
|
+
article={<>
|
|
103
|
+
<SettingsSection id="general" title="General">
|
|
104
|
+
<SettingsRow label="Project name" htmlFor="project-name" status={<Saved />}>
|
|
105
|
+
<Input id="project-name" value={name} onChange={(event) => setName(event.target.value)} />
|
|
106
|
+
</SettingsRow>
|
|
107
|
+
<SettingsRow label="Timezone" htmlFor="timezone" description="Moves the digest hour; scheduled posts never move." info="Dates in chats, emails and exports use this zone.">
|
|
108
|
+
<Select defaultValue="berlin">
|
|
109
|
+
<SelectTrigger id="timezone" aria-describedby={fieldMessageId("timezone")}><SelectValue /></SelectTrigger>
|
|
110
|
+
<SelectContent>
|
|
111
|
+
<SelectItem value="berlin">Europe/Berlin (UTC+2)</SelectItem>
|
|
112
|
+
<SelectItem value="new-york">America/New_York (UTC−4)</SelectItem>
|
|
113
|
+
</SelectContent>
|
|
114
|
+
</Select>
|
|
115
|
+
</SettingsRow>
|
|
116
|
+
<SettingsRow label="Business brief" layout="compact">
|
|
117
|
+
<Button type="button" variant="outline">Edit brief</Button>
|
|
118
|
+
</SettingsRow>
|
|
119
|
+
<SettingsRow label="Custom instructions" htmlFor="instructions" layout="stacked" status={<Saving />} description="Applies to new chats and the next automation run.">
|
|
120
|
+
<Textarea id="instructions" defaultValue="Write in plain English. Never promise delivery dates." />
|
|
121
|
+
</SettingsRow>
|
|
122
|
+
</SettingsSection>
|
|
123
|
+
|
|
124
|
+
<SettingsSection id="notifications" title="Notifications" description="Daily digest and approval notices go to the owner.">
|
|
125
|
+
<SettingsRow label="Daily digest" htmlFor="digest" layout="compact">
|
|
126
|
+
<Switch id="digest" checked={digest} onCheckedChange={setDigest} />
|
|
127
|
+
</SettingsRow>
|
|
128
|
+
<SettingsRow label="Approval notices" htmlFor="approval-mode">
|
|
129
|
+
<SegmentedControl
|
|
130
|
+
id="approval-mode"
|
|
131
|
+
aria-label="Approval notices"
|
|
132
|
+
value={approval}
|
|
133
|
+
onValueChange={setApproval}
|
|
134
|
+
options={[{ value: "each", label: "Each request" }, { value: "daily", label: "Once a day" }, { value: "never", label: "Never" }]}
|
|
135
|
+
/>
|
|
136
|
+
</SettingsRow>
|
|
137
|
+
<SettingsRow label="Digest hour" htmlFor="digest-hour" error="Couldn't save the digest hour." onRetry={() => undefined}>
|
|
138
|
+
<Input id="digest-hour" defaultValue="08:00" />
|
|
139
|
+
</SettingsRow>
|
|
140
|
+
<SettingsRow label="Texting phone" htmlFor="texting-phone" disabledReason="Only Priya, the owner, can change the texting phone.">
|
|
141
|
+
<Input id="texting-phone" defaultValue="+1 (555) 010-0199" disabled />
|
|
142
|
+
</SettingsRow>
|
|
143
|
+
<SettingsRow label="Send today's digest" layout="compact">
|
|
144
|
+
<Button type="button" variant="outline">Send now</Button>
|
|
145
|
+
</SettingsRow>
|
|
146
|
+
</SettingsSection>
|
|
147
|
+
|
|
148
|
+
<SettingsSection id="automations" title="Automations" info="Automations run on the project's agent and are billed as compute." actions={<Button type="button" variant="outline">New automation</Button>}>
|
|
149
|
+
<SettingsList aria-label="Automations" columns={{ status: "8rem", action: "9rem" }}>
|
|
150
|
+
{jobs.map((job) => (
|
|
151
|
+
<SettingsListItem
|
|
152
|
+
key={job.name}
|
|
153
|
+
title={<a href={`#${job.name}`}>{job.name}</a>}
|
|
154
|
+
meta={job.meta}
|
|
155
|
+
status={job.error ? <span className="font-medium text-[var(--surface-danger-text)]">{job.state}</span> : job.state}
|
|
156
|
+
error={job.error}
|
|
157
|
+
action={{
|
|
158
|
+
label: job.verb,
|
|
159
|
+
onSelect: () => setJobs((all) => all.map((each) => (each.name === job.name ? runVerb(each) : each))),
|
|
160
|
+
disabledReason: job.ownerOnly ? "Only Priya, the owner, can resume it." : undefined,
|
|
161
|
+
}}
|
|
162
|
+
menu={[
|
|
163
|
+
{ label: "Duplicate", onSelect: () => undefined },
|
|
164
|
+
job.ownerOnly
|
|
165
|
+
? { label: "Delete", tone: "danger", onSelect: () => undefined, disabledReason: "Only Priya, the owner, can delete it." }
|
|
166
|
+
: { label: "Delete", tone: "danger", onSelect: () => undefined },
|
|
167
|
+
]}
|
|
168
|
+
/>
|
|
169
|
+
))}
|
|
170
|
+
</SettingsList>
|
|
171
|
+
</SettingsSection>
|
|
172
|
+
|
|
173
|
+
<SettingsSection id="team" title="Members" actions={<Button type="button">Invite</Button>}>
|
|
174
|
+
<SettingsList aria-label="Members" columns={{ leading: "32px", status: "18rem", menu: "36px" }}>
|
|
175
|
+
{members.map((member) => {
|
|
176
|
+
const roleId = `role-${member.email}`;
|
|
177
|
+
return (
|
|
178
|
+
<SettingsListItem
|
|
179
|
+
key={member.email}
|
|
180
|
+
leading={<Avatar size="sm"><AvatarFallback>{member.name.split(" ").map((part) => part[0]).join("")}</AvatarFallback></Avatar>}
|
|
181
|
+
title={member.name}
|
|
182
|
+
meta={member.email}
|
|
183
|
+
status={member.role === "owner" ? <span className="text-foreground">Owner</span> : (
|
|
184
|
+
<Select value={roleOf[member.email]} onValueChange={(role) => setRoleOf((all) => ({ ...all, [member.email]: role }))}>
|
|
185
|
+
<SelectTrigger id={roleId} aria-label={`Role for ${member.name}`}><SelectValue /></SelectTrigger>
|
|
186
|
+
<SelectContent>
|
|
187
|
+
{roles.filter((role) => role.value !== "owner").map((role) => <SelectItem key={role.value} value={role.value}>{role.label}</SelectItem>)}
|
|
188
|
+
</SelectContent>
|
|
189
|
+
</Select>
|
|
190
|
+
)}
|
|
191
|
+
menu={member.role === "owner" ? undefined : [{ label: "Remove from project", tone: "danger", onSelect: () => undefined }]}
|
|
192
|
+
/>
|
|
193
|
+
);
|
|
194
|
+
})}
|
|
195
|
+
</SettingsList>
|
|
196
|
+
</SettingsSection>
|
|
197
|
+
|
|
198
|
+
<SettingsSection
|
|
199
|
+
id="billing"
|
|
200
|
+
title="Ad spend caps"
|
|
201
|
+
description="Ads pause when a cap is reached."
|
|
202
|
+
footer={<>
|
|
203
|
+
<span className="text-muted-foreground">2 unsaved changes</span>
|
|
204
|
+
<span className="ml-auto flex gap-2">
|
|
205
|
+
<Button type="button" variant="ghost">Discard</Button>
|
|
206
|
+
<Button type="button">Save caps</Button>
|
|
207
|
+
</span>
|
|
208
|
+
</>}
|
|
209
|
+
>
|
|
210
|
+
<SettingsRow label="Daily cap" htmlFor="daily-cap"><Input id="daily-cap" defaultValue="$40" /></SettingsRow>
|
|
211
|
+
<SettingsRow label="Monthly cap" htmlFor="monthly-cap"><Input id="monthly-cap" defaultValue="$900" /></SettingsRow>
|
|
212
|
+
</SettingsSection>
|
|
213
|
+
|
|
214
|
+
<SettingsSection id="delete" tone="danger" title="Delete project" description="Deletes every chat, vault file and automation for everyone." actions={<Button type="button" variant="destructive-outline">Delete project…</Button>} />
|
|
215
|
+
</>}
|
|
216
|
+
/>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function FieldsExample() {
|
|
221
|
+
const [model, setModel] = useState("claude-sonnet");
|
|
222
|
+
return (
|
|
223
|
+
<div className="mx-auto max-w-xl p-6">
|
|
224
|
+
<Card className="space-y-4 p-4">
|
|
225
|
+
<Field label="Display name" htmlFor="display-name" hint="Use 40 characters or fewer.">
|
|
226
|
+
<Input id="display-name" defaultValue="Research agent" aria-describedby={fieldMessageId("display-name")} />
|
|
227
|
+
</Field>
|
|
228
|
+
<Field label="Default model" htmlFor="default-model" info="Replaces the profile's model for new chats." aside={<Button type="button" variant="ghost">Use default</Button>}
|
|
229
|
+
error={model.startsWith("claude") ? "This project's catalog refuses Anthropic models. Pick another." : undefined}>
|
|
230
|
+
<Select value={model} onValueChange={setModel}>
|
|
231
|
+
<SelectTrigger id="default-model" aria-invalid={model.startsWith("claude") || undefined} aria-describedby={fieldMessageId("default-model")}><SelectValue /></SelectTrigger>
|
|
232
|
+
<SelectContent>
|
|
233
|
+
<SelectItem value="claude-sonnet">claude-sonnet</SelectItem>
|
|
234
|
+
<SelectItem value="gpt-6.1-sol">gpt-6.1-sol</SelectItem>
|
|
235
|
+
</SelectContent>
|
|
236
|
+
</Select>
|
|
237
|
+
</Field>
|
|
238
|
+
<Field label="Webhook URL" htmlFor="webhook" status="Reachable: the last check returned 200.">
|
|
239
|
+
<Input id="webhook" defaultValue="https://hooks.fieldnote.example/gtm/leads" aria-describedby={fieldMessageId("webhook")} />
|
|
240
|
+
</Field>
|
|
241
|
+
</Card>
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const meta = {
|
|
247
|
+
title: "Primitives/Settings",
|
|
248
|
+
component: SettingsSection,
|
|
249
|
+
parameters: { layout: "fullscreen" },
|
|
250
|
+
} satisfies Meta<typeof SettingsSection>;
|
|
251
|
+
export default meta;
|
|
252
|
+
type Story = StoryObj<typeof SettingsSection>;
|
|
253
|
+
|
|
254
|
+
/** Every settings atom state on one page in PageShell measure="settings"; resize to see the nav switch at 60rem and rows stack at 34rem. */
|
|
255
|
+
export const Page: Story = { render: () => <SettingsPage /> };
|
|
256
|
+
|
|
257
|
+
/** The sections a role gates load after the page renders; on a phone the strip still centres the current tab when it arrives. */
|
|
258
|
+
export const SectionsLoadLater: Story = { render: () => <SettingsPage sectionsLoadLater /> };
|
|
259
|
+
|
|
260
|
+
/** A settings page with no section navigation is the 48rem column alone at every width. */
|
|
261
|
+
export const WithoutNavigation: Story = { render: () => <SettingsPage withoutNavigation /> };
|
|
262
|
+
|
|
263
|
+
/** className bounds the frame the layout answers to, so a 42rem page in a wide window gets the tab strip. */
|
|
264
|
+
export const Bounded: Story = { render: () => <SettingsPage className="mx-auto max-w-2xl" /> };
|
|
265
|
+
|
|
266
|
+
/** The promoted Field: hint, info tip with an aside action, a live error, and a status. */
|
|
267
|
+
export const Fields: Story = { render: () => <FieldsExample /> };
|