@sentry/junior-dashboard 0.233.0 → 0.235.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.
Files changed (59) hide show
  1. package/dist/app.js +178 -29
  2. package/dist/client.js +53 -50
  3. package/dist/index.js +178 -29
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +3 -3
  6. package/src/assets/junior-avatar.png +0 -0
  7. package/src/assets/junior-favicon.png +0 -0
  8. package/src/assets.ts +1 -0
  9. package/src/client/App.tsx +372 -345
  10. package/src/client/api.ts +60 -1
  11. package/src/client/components/ActorAvatar.tsx +37 -0
  12. package/src/client/components/Button.tsx +12 -10
  13. package/src/client/components/Field.tsx +14 -1
  14. package/src/client/components/FormNotice.tsx +40 -0
  15. package/src/client/components/ParticipantAvatarStack.tsx +6 -14
  16. package/src/client/components/README.md +19 -6
  17. package/src/client/components/SegmentedNav.tsx +167 -0
  18. package/src/client/components/SegmentedTabs.tsx +24 -5
  19. package/src/client/components/Select.tsx +18 -0
  20. package/src/client/components/StatusChip.tsx +15 -12
  21. package/src/client/components/TextInput.tsx +25 -5
  22. package/src/client/components/layout/SecondaryNavigation.tsx +40 -83
  23. package/src/client/http.ts +31 -3
  24. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +78 -68
  25. package/src/client/pages/automations/AutomationEditPage.tsx +171 -0
  26. package/src/client/pages/automations/AutomationEditor.tsx +397 -0
  27. package/src/client/pages/automations/AutomationEditorLoading.tsx +18 -0
  28. package/src/client/pages/automations/AutomationEventFields.tsx +264 -0
  29. package/src/client/pages/automations/AutomationFilters.tsx +179 -0
  30. package/src/client/pages/automations/AutomationFormSection.tsx +22 -0
  31. package/src/client/pages/automations/AutomationLifecycle.tsx +146 -0
  32. package/src/client/pages/automations/AutomationOutcomeFields.tsx +177 -0
  33. package/src/client/pages/automations/AutomationRow.tsx +343 -0
  34. package/src/client/pages/automations/AutomationScheduleFields.tsx +328 -0
  35. package/src/client/pages/automations/AutomationsPage.tsx +186 -439
  36. package/src/client/pages/automations/AutomationsPageLayout.tsx +19 -7
  37. package/src/client/pages/automations/README.md +19 -0
  38. package/src/client/pages/automations/automationDraft.ts +110 -0
  39. package/src/client/pages/automations/automationFormat.ts +43 -0
  40. package/src/client/pages/automations/automationOutcomes.ts +23 -0
  41. package/src/client/pages/code/CodeChangeRow.tsx +41 -0
  42. package/src/client/pages/code/CodePage.tsx +18 -93
  43. package/src/client/pages/code/CodeSummaryCards.tsx +53 -0
  44. package/src/client/pages/code/RepositoryPage.tsx +227 -0
  45. package/src/client/pages/code/RepositorySections.tsx +108 -0
  46. package/src/client/pages/code/RepositoryTabs.tsx +146 -0
  47. package/src/client/pages/code/codeRepositoryRoutes.ts +18 -0
  48. package/src/client/pages/dev/ComponentsPage.tsx +55 -26
  49. package/src/client/pages/dev/SegmentedFixtures.tsx +58 -0
  50. package/src/client/pages/memory/MemoryPageLayout.tsx +12 -13
  51. package/src/client/pages/system/SystemNavigation.tsx +6 -2
  52. package/src/client/pages/system/SystemPageLayout.tsx +4 -6
  53. package/src/client/searchParams.ts +4 -2
  54. package/src/client.tsx +7 -4
  55. package/src/mock-reporting/automation-collection.ts +101 -0
  56. package/src/mock-reporting/code-repository.ts +77 -0
  57. package/src/mock-reporting/fixtures.ts +9 -3
  58. package/src/mock-reporting/routes.ts +39 -17
  59. package/src/shell.ts +21 -5
@@ -0,0 +1,227 @@
1
+ import { useState } from "react";
2
+ import type { CodeRepositoryReport } from "@sentry/junior/api/schema";
3
+ import { ArrowLeft, ExternalLink } from "lucide-react";
4
+ import { Link, useParams } from "react-router";
5
+
6
+ import {
7
+ useCodeRepositoryConversationsData,
8
+ useCodeRepositoryData,
9
+ } from "../../api";
10
+ import { ButtonLink } from "../../components/Button";
11
+ import { EmptyTelemetry } from "../../components/EmptyTelemetry";
12
+ import { PageRouteLoading } from "../../components/PageRouteLoading";
13
+ import {
14
+ timeRangeBucketUnit,
15
+ TimeRangeSelector,
16
+ type TimeRangeDays,
17
+ } from "../../components/controls/TimeRangeSelector";
18
+ import { PageHeader } from "../../components/layout/PageHeader";
19
+ import { PageLayout } from "../../components/layout/PageLayout";
20
+ import { SecondaryNavigation } from "../../components/layout/SecondaryNavigation";
21
+ import { buildConversations } from "../../format";
22
+ import { CodeActivityChart } from "./CodeActivityChart";
23
+ import { CodeChangeRow } from "./CodeChangeRow";
24
+ import { CodeSummaryCards } from "./CodeSummaryCards";
25
+ import {
26
+ codeRepositoryPath,
27
+ type CodeRepositoryTab,
28
+ } from "./codeRepositoryRoutes";
29
+ import {
30
+ RecentConversationRow,
31
+ RepositorySection,
32
+ repositoryActivity,
33
+ } from "./RepositorySections";
34
+ import {
35
+ RepositoryChangesTab,
36
+ RepositoryConversationsTab,
37
+ } from "./RepositoryTabs";
38
+
39
+ const RECENT_ROWS = 5;
40
+ const DESCRIPTION = "Conversations and code changes from Junior.";
41
+
42
+ /** Inline link style used by `ConversationSummary` location links. */
43
+ const inlineLinkClass =
44
+ "font-semibold text-dashboard-text underline decoration-white/20 underline-offset-2 transition-colors hover:decoration-white/60";
45
+
46
+ /** Render one repository tab under the shared header and tab bar. */
47
+ export function RepositoryPage(props: { tab: CodeRepositoryTab }) {
48
+ const params = useParams();
49
+ const repositoryId = params.repositoryId;
50
+ const [range, setRange] = useState<TimeRangeDays>(30);
51
+ const query = useCodeRepositoryData(repositoryId);
52
+ if (!query.data && !query.error) {
53
+ return (
54
+ <PageRouteLoading
55
+ description={DESCRIPTION}
56
+ label="Loading repository"
57
+ title="Repository"
58
+ variant="stats"
59
+ />
60
+ );
61
+ }
62
+ const data = query.data;
63
+ if (!data) {
64
+ return (
65
+ <PageLayout>
66
+ <BackToCode />
67
+ <EmptyTelemetry>
68
+ This repository is unavailable. It may not have any code changes from
69
+ Junior.
70
+ </EmptyTelemetry>
71
+ </PageLayout>
72
+ );
73
+ }
74
+ const repository = data.repository;
75
+ return (
76
+ <SecondaryNavigation
77
+ ariaLabel="Repository navigation"
78
+ items={[
79
+ {
80
+ end: true,
81
+ label: "Overview",
82
+ to: codeRepositoryPath(repository.id),
83
+ },
84
+ {
85
+ label: "Conversations",
86
+ to: codeRepositoryPath(repository.id, "conversations"),
87
+ },
88
+ {
89
+ label: "Changes",
90
+ to: codeRepositoryPath(repository.id, "changes"),
91
+ },
92
+ ]}
93
+ >
94
+ <PageLayout>
95
+ <BackToCode />
96
+ <PageHeader
97
+ actions={
98
+ <div className="flex flex-wrap items-center gap-2">
99
+ <TimeRangeSelector onChange={setRange} value={range} />
100
+ {repository.url ? (
101
+ <ButtonLink
102
+ rel="noopener noreferrer"
103
+ target="_blank"
104
+ to={repository.url}
105
+ >
106
+ Open repository
107
+ <ExternalLink
108
+ aria-hidden="true"
109
+ className="size-3.5 shrink-0"
110
+ />
111
+ </ButtonLink>
112
+ ) : null}
113
+ </div>
114
+ }
115
+ description={<RepositoryDescription data={data} />}
116
+ title={repository.name}
117
+ />
118
+ {props.tab === "overview" ? (
119
+ <RepositoryOverview data={data} range={range} />
120
+ ) : null}
121
+ {props.tab === "conversations" ? (
122
+ <RepositoryConversationsTab repositoryId={repository.id} />
123
+ ) : null}
124
+ {props.tab === "changes" ? (
125
+ <RepositoryChangesTab data={data} range={range} />
126
+ ) : null}
127
+ </PageLayout>
128
+ </SecondaryNavigation>
129
+ );
130
+ }
131
+
132
+ function BackToCode() {
133
+ return (
134
+ <Link
135
+ className="flex w-fit items-center gap-2 font-display text-sm font-medium text-dashboard-text-muted no-underline transition-colors hover:text-dashboard-text"
136
+ to="/code"
137
+ >
138
+ <ArrowLeft aria-hidden="true" size={15} strokeWidth={1.8} />
139
+ Back to code
140
+ </Link>
141
+ );
142
+ }
143
+
144
+ /** Page description, then each Workspace that includes the repository. */
145
+ function RepositoryDescription(props: { data: CodeRepositoryReport }) {
146
+ const workspaces = props.data.workspaces;
147
+ if (workspaces.length === 0) return DESCRIPTION;
148
+ return (
149
+ <>
150
+ {DESCRIPTION} / Workspaces{" "}
151
+ {workspaces.map((workspace, index) => (
152
+ <span key={workspace.id}>
153
+ {index > 0 ? ", " : null}
154
+ <Link
155
+ className={inlineLinkClass}
156
+ to={`/system/workspaces/${encodeURIComponent(workspace.id)}`}
157
+ >
158
+ {workspace.name}
159
+ </Link>
160
+ </span>
161
+ ))}
162
+ </>
163
+ );
164
+ }
165
+
166
+ function RepositoryOverview(props: {
167
+ data: CodeRepositoryReport;
168
+ range: TimeRangeDays;
169
+ }) {
170
+ const data = props.data;
171
+ const conversationsQuery = useCodeRepositoryConversationsData(
172
+ data.repository.id,
173
+ );
174
+ const conversations = buildConversations(
175
+ conversationsQuery.data?.conversations ?? [],
176
+ ).slice(0, RECENT_ROWS);
177
+ const changes = data.changes.slice(0, RECENT_ROWS);
178
+ return (
179
+ <>
180
+ <CodeSummaryCards summary={data.summary} />
181
+ <div className="grid gap-3 lg:grid-cols-2">
182
+ <RepositorySection
183
+ title="Recent conversations"
184
+ viewAllTo={codeRepositoryPath(data.repository.id, "conversations")}
185
+ >
186
+ {conversations.length > 0 ? (
187
+ conversations.map((conversation) => (
188
+ <RecentConversationRow
189
+ conversation={conversation}
190
+ key={conversation.id}
191
+ />
192
+ ))
193
+ ) : (
194
+ <div className="p-4">
195
+ <EmptyTelemetry>
196
+ {conversationsQuery.error
197
+ ? "Conversations are unavailable. Try refreshing the dashboard."
198
+ : conversationsQuery.data
199
+ ? "No conversations are linked to this repository yet."
200
+ : "Loading conversations…"}
201
+ </EmptyTelemetry>
202
+ </div>
203
+ )}
204
+ </RepositorySection>
205
+ <RepositorySection
206
+ title="Recent changes"
207
+ viewAllTo={codeRepositoryPath(data.repository.id, "changes")}
208
+ >
209
+ {changes.length > 0 ? (
210
+ changes.map((change) => (
211
+ <CodeChangeRow change={change} key={change.id} />
212
+ ))
213
+ ) : (
214
+ <div className="p-4">
215
+ <EmptyTelemetry>No code changes are recorded yet.</EmptyTelemetry>
216
+ </div>
217
+ )}
218
+ </RepositorySection>
219
+ </div>
220
+ <CodeActivityChart
221
+ bucketUnit={timeRangeBucketUnit(props.range)}
222
+ days={repositoryActivity(data, props.range)}
223
+ range={props.range}
224
+ />
225
+ </>
226
+ );
227
+ }
@@ -0,0 +1,108 @@
1
+ import type { CodeRepositoryReport } from "@sentry/junior/api/schema";
2
+ import type { ReactNode } from "react";
3
+ import { Link } from "react-router";
4
+
5
+ import {
6
+ selectTimeSeries,
7
+ type TimeRangeDays,
8
+ } from "../../components/controls/TimeRangeSelector";
9
+ import { Card } from "../../components/layout/Card";
10
+ import { SectionHeader } from "../../components/layout/SectionHeader";
11
+ import { SectionTitle } from "../../components/layout/SectionTitle";
12
+ import { ConversationSidebarAnnotations } from "../../conversations/ConversationMeta";
13
+ import { ConversationListStatusIcon } from "../../conversations/ConversationListStatusIcon";
14
+ import { conversationPath } from "../../conversations/conversationRoutes";
15
+ import {
16
+ conversationDisplayTitle,
17
+ formatRelativeTime,
18
+ slackLocationLabel,
19
+ visualStatusForConversation,
20
+ } from "../../format";
21
+ import type { Conversation } from "../../types";
22
+
23
+ /** Titled repository card with an optional "View all" link to its tab. */
24
+ export function RepositorySection(props: {
25
+ children: ReactNode;
26
+ title: string;
27
+ viewAllTo?: string;
28
+ }) {
29
+ return (
30
+ <Card as="section" className="mb-0" variant="section">
31
+ <SectionHeader
32
+ actions={
33
+ props.viewAllTo ? (
34
+ <Link
35
+ className="text-sm text-dashboard-text underline underline-offset-2"
36
+ to={props.viewAllTo}
37
+ >
38
+ View all
39
+ </Link>
40
+ ) : undefined
41
+ }
42
+ >
43
+ <SectionTitle>{props.title}</SectionTitle>
44
+ </SectionHeader>
45
+ {props.children}
46
+ </Card>
47
+ );
48
+ }
49
+
50
+ /**
51
+ * Two-line conversation row built from the sidebar row parts: status icon and
52
+ * title, then plugin annotations or the location.
53
+ */
54
+ export function RecentConversationRow(props: { conversation: Conversation }) {
55
+ const conversation = props.conversation;
56
+ const title = conversationDisplayTitle(conversation);
57
+ const location = slackLocationLabel(conversation, { includeId: false });
58
+ const hasAnnotations = Boolean(conversation.sidebarAnnotations?.length);
59
+ return (
60
+ <div className="relative min-w-0 border-b border-dashboard-border-subtle px-4 py-3 transition-colors last:border-b-0 hover:bg-dashboard-fill-soft">
61
+ <Link
62
+ aria-label={`Open ${title}`}
63
+ className="absolute inset-0 z-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-dashboard-focus"
64
+ to={conversationPath(conversation.id)}
65
+ />
66
+ <div className="pointer-events-none relative z-[1] grid min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-x-2">
67
+ <span className="mt-1.5 grid size-3 shrink-0 place-items-center">
68
+ <ConversationListStatusIcon
69
+ finishedSinceSeen={false}
70
+ isPrivate={conversation.visibility === "private"}
71
+ status={visualStatusForConversation(conversation)}
72
+ />
73
+ </span>
74
+ <div className="min-w-0 truncate font-display text-sm text-dashboard-text">
75
+ {title}
76
+ </div>
77
+ <span className="whitespace-nowrap font-mono text-xs text-dashboard-text-muted">
78
+ {formatRelativeTime(conversation.lastSeenAt)}
79
+ </span>
80
+ {/* Keep the meta line at text height so annotation chips do not make
81
+ this row taller than a code change row beside it. */}
82
+ <div className="col-start-2 col-end-4 mt-1 flex h-4 min-w-0 items-center gap-1.5 font-mono text-xs text-dashboard-text-muted">
83
+ {hasAnnotations ? (
84
+ <ConversationSidebarAnnotations
85
+ annotations={conversation.sidebarAnnotations}
86
+ />
87
+ ) : location ? (
88
+ <span className="truncate">{location}</span>
89
+ ) : null}
90
+ </div>
91
+ </div>
92
+ </div>
93
+ );
94
+ }
95
+
96
+ /** Chart buckets for the selected range from a repository report. */
97
+ export function repositoryActivity(
98
+ data: CodeRepositoryReport,
99
+ range: TimeRangeDays,
100
+ ) {
101
+ return selectTimeSeries({
102
+ days: data.activityDays,
103
+ hours: data.activityHours,
104
+ sixHours: data.activitySixHours,
105
+ range,
106
+ emptySixHour: (date) => ({ closed: 0, created: 0, date, merged: 0 }),
107
+ });
108
+ }
@@ -0,0 +1,146 @@
1
+ import { useDeferredValue, useEffect, useState } from "react";
2
+ import type { CodeRepositoryReport } from "@sentry/junior/api/schema";
3
+
4
+ import { useCodeRepositoryConversationsData } from "../../api";
5
+ import { EmptyTelemetry } from "../../components/EmptyTelemetry";
6
+ import { FilterTabList } from "../../components/FilterBar";
7
+ import {
8
+ PagePagination,
9
+ pageCount,
10
+ pageItems,
11
+ } from "../../components/Pagination";
12
+ import { SearchInput } from "../../components/SearchInput";
13
+ import {
14
+ timeRangeBucketUnit,
15
+ type TimeRangeDays,
16
+ } from "../../components/controls/TimeRangeSelector";
17
+ import { Card } from "../../components/layout/Card";
18
+ import { ConversationHomeList } from "../../conversations/ConversationHomeList";
19
+ import { buildConversations, getDashboardTimeZone } from "../../format";
20
+ import { CodeActivityChart } from "./CodeActivityChart";
21
+ import { CodeChangeRow } from "./CodeChangeRow";
22
+ import { repositoryActivity } from "./RepositorySections";
23
+
24
+ const PAGE_SIZE = 25;
25
+ const noFinishedConversations = new Set<string>();
26
+
27
+ /** Conversations linked to the repository, in the home list layout. */
28
+ export function RepositoryConversationsTab(props: { repositoryId: string }) {
29
+ const [query, setQuery] = useState("");
30
+ const [page, setPage] = useState(1);
31
+ const search = useDeferredValue(query.trim());
32
+ const feed = useCodeRepositoryConversationsData(props.repositoryId, search);
33
+ const conversations = buildConversations(feed.data?.conversations ?? []);
34
+ useEffect(() => {
35
+ setPage(1);
36
+ }, [search]);
37
+ return (
38
+ <section aria-label="Conversations" className="grid gap-3">
39
+ <SearchInput
40
+ className="w-full sm:ml-auto sm:w-72"
41
+ label="Search conversations in this repository"
42
+ onChange={setQuery}
43
+ placeholder="Search conversations…"
44
+ value={query}
45
+ />
46
+ {feed.error ? (
47
+ <EmptyTelemetry>
48
+ Conversations are unavailable. Try refreshing the dashboard.
49
+ </EmptyTelemetry>
50
+ ) : (
51
+ <ConversationHomeList
52
+ conversations={pageItems(conversations, page, PAGE_SIZE)}
53
+ emptyLabel={
54
+ search
55
+ ? "No conversations match this search."
56
+ : "No conversations are linked to this repository yet."
57
+ }
58
+ finishedConversationIds={noFinishedConversations}
59
+ loading={!feed.data}
60
+ timeZone={getDashboardTimeZone()}
61
+ />
62
+ )}
63
+ <PagePagination
64
+ className="pt-1"
65
+ onPageChange={setPage}
66
+ page={page}
67
+ pageCount={pageCount(conversations.length, PAGE_SIZE)}
68
+ pageSize={PAGE_SIZE}
69
+ total={conversations.length}
70
+ />
71
+ </section>
72
+ );
73
+ }
74
+
75
+ /** Recent code changes in the repository, filtered by state and search. */
76
+ export function RepositoryChangesTab(props: {
77
+ data: CodeRepositoryReport;
78
+ range: TimeRangeDays;
79
+ }) {
80
+ const [state, setState] = useState("");
81
+ const [query, setQuery] = useState("");
82
+ const [page, setPage] = useState(1);
83
+ const all = props.data.changes;
84
+ const needle = query.trim().toLowerCase();
85
+ const changes = all.filter(
86
+ (change) =>
87
+ (!state || change.state === state) &&
88
+ (!needle ||
89
+ change.title?.toLowerCase().includes(needle) ||
90
+ `#${change.number}`.includes(needle)),
91
+ );
92
+ const count = (value: string) =>
93
+ all.filter((change) => change.state === value).length;
94
+ useEffect(() => {
95
+ setPage(1);
96
+ }, [state, needle]);
97
+ return (
98
+ <>
99
+ <CodeActivityChart
100
+ bucketUnit={timeRangeBucketUnit(props.range)}
101
+ days={repositoryActivity(props.data, props.range)}
102
+ range={props.range}
103
+ />
104
+ <section aria-label="Changes" className="grid gap-4">
105
+ <div className="flex flex-wrap items-end justify-between gap-4">
106
+ <FilterTabList
107
+ ariaLabel="Change state"
108
+ items={[
109
+ { count: all.length, label: "All", value: "" },
110
+ { count: count("open"), label: "Open", value: "open" },
111
+ { count: count("merged"), label: "Merged", value: "merged" },
112
+ { count: count("closed"), label: "Closed", value: "closed" },
113
+ ]}
114
+ onChange={setState}
115
+ value={state}
116
+ />
117
+ <SearchInput
118
+ className="w-full max-w-md sm:w-auto"
119
+ label="Search changes"
120
+ onChange={setQuery}
121
+ placeholder="Title or number"
122
+ value={query}
123
+ />
124
+ </div>
125
+ <Card>
126
+ {changes.length === 0 ? (
127
+ <div className="p-4">
128
+ <EmptyTelemetry>No changes match these filters.</EmptyTelemetry>
129
+ </div>
130
+ ) : (
131
+ pageItems(changes, page, PAGE_SIZE).map((change) => (
132
+ <CodeChangeRow change={change} key={change.id} />
133
+ ))
134
+ )}
135
+ </Card>
136
+ <PagePagination
137
+ onPageChange={setPage}
138
+ page={page}
139
+ pageCount={pageCount(changes.length, PAGE_SIZE)}
140
+ pageSize={PAGE_SIZE}
141
+ total={changes.length}
142
+ />
143
+ </section>
144
+ </>
145
+ );
146
+ }
@@ -0,0 +1,18 @@
1
+ /** Repository page tabs. Overview is the bare repository path. */
2
+ export type CodeRepositoryTab = "overview" | "conversations" | "changes";
3
+
4
+ /** Tab order in the repository page navigation. */
5
+ export const codeRepositoryTabs: readonly CodeRepositoryTab[] = [
6
+ "overview",
7
+ "conversations",
8
+ "changes",
9
+ ];
10
+
11
+ /** Build the dashboard path for one repository page tab. */
12
+ export function codeRepositoryPath(
13
+ repositoryId: string,
14
+ tab: CodeRepositoryTab = "overview",
15
+ ): string {
16
+ const base = `/code/${encodeURIComponent(repositoryId)}`;
17
+ return tab === "overview" ? base : `${base}/${tab}`;
18
+ }
@@ -1,5 +1,8 @@
1
+ import { FormNotice } from "../../components/FormNotice";
2
+ import { Select } from "../../components/Select";
1
3
  import { ObjectFixtures } from "./ObjectFixtures";
2
4
  import { ChartFixtures } from "./ChartFixtures";
5
+ import { SegmentedNavFixture, SegmentedTabsFixture } from "./SegmentedFixtures";
3
6
  import {
4
7
  ConversationFixture,
5
8
  MessageAttachmentsFixture,
@@ -33,11 +36,11 @@ import { DashboardHeader } from "../../components/layout/DashboardHeader";
33
36
  import { PageHeader } from "../../components/layout/PageHeader";
34
37
  import { MetricList, MetricValue } from "../../components/Metric";
35
38
  import { PageContentSkeleton } from "../../components/PageContentSkeleton";
39
+ import { ActorAvatar } from "../../components/ActorAvatar";
36
40
  import { ParticipantAvatarStack } from "../../components/ParticipantAvatarStack";
37
41
  import { StatCard } from "../../components/metrics/StatCard";
38
42
  import { StatusChip } from "../../components/StatusChip";
39
43
  import { StatusDot } from "../../components/StatusDot";
40
- import { SegmentedTabs } from "../../components/SegmentedTabs";
41
44
  import { TextArea, TextInput } from "../../components/TextInput";
42
45
  import { TranscriptMarkdown } from "../../conversations/TranscriptMarkdown";
43
46
  import { TranscriptText } from "../../conversations/TranscriptText";
@@ -300,7 +303,6 @@ function FoundationsGalleryPage() {
300
303
  const [navigationOpen, setNavigationOpen] = useState(false);
301
304
  const [drawerOpen, setDrawerOpen] = useState(false);
302
305
  const [nestedDrawerOpen, setNestedDrawerOpen] = useState(false);
303
- const [tab, setTab] = useState<"details" | "memories" | "usage">("details");
304
306
 
305
307
  return (
306
308
  <GalleryShell
@@ -308,6 +310,34 @@ function FoundationsGalleryPage() {
308
310
  sectionId="foundations"
309
311
  title="Foundations"
310
312
  >
313
+ <Fixture title="Editor controls">
314
+ <Field
315
+ label="Instruction"
316
+ htmlFor="gallery-instruction"
317
+ error="Enter an instruction."
318
+ >
319
+ <TextArea
320
+ prose
321
+ id="gallery-instruction"
322
+ aria-invalid
323
+ aria-describedby="gallery-instruction-error"
324
+ />
325
+ </Field>
326
+ <Field label="Repeat" htmlFor="gallery-repeat">
327
+ <TextInput
328
+ size="comfortable"
329
+ aria-label="Resource identifier"
330
+ defaultValue="getsentry/junior#42"
331
+ />
332
+ <Select id="gallery-repeat">
333
+ <option>Every week</option>
334
+ </Select>
335
+ </Field>
336
+ <Button tone="primary">Save changes</Button>
337
+ <FormNotice title="This automation changed while you were editing.">
338
+ Your edits are still here. Review the latest version before saving.
339
+ </FormNotice>
340
+ </Fixture>
311
341
  <Fixture title="Conversation navigation">
312
342
  <DashboardChromeProvider>
313
343
  <DashboardHeader
@@ -323,26 +353,10 @@ function FoundationsGalleryPage() {
323
353
  </DashboardChromeProvider>
324
354
  </Fixture>
325
355
  <Fixture title="Segmented tabs">
326
- <div className="max-w-md">
327
- <SegmentedTabs
328
- items={[
329
- { label: "Details", value: "details" },
330
- { label: "Memories", value: "memories" },
331
- { label: "Usage", value: "usage" },
332
- ]}
333
- label="Conversation panels"
334
- onChange={setTab}
335
- value={tab}
336
- >
337
- <p className="m-0 text-sm leading-relaxed text-dashboard-text-muted">
338
- {tab === "details"
339
- ? "Conversation summary and linked work."
340
- : tab === "memories"
341
- ? "What Junior learned from this conversation."
342
- : "Time, tokens, and cost for this conversation."}
343
- </p>
344
- </SegmentedTabs>
345
- </div>
356
+ <SegmentedTabsFixture />
357
+ </Fixture>
358
+ <Fixture title="Segmented page links">
359
+ <SegmentedNavFixture />
346
360
  </Fixture>
347
361
  <Fixture title="Narrow details drawer">
348
362
  <Button onClick={() => setDrawerOpen(true)}>Open details</Button>
@@ -563,19 +577,34 @@ function FoundationsGalleryPage() {
563
577
  />
564
578
  </div>
565
579
  </Fixture>
566
- <Fixture title="Status chips">
567
- <div className="flex flex-wrap items-center gap-2">
580
+ <Fixture title="Status labels">
581
+ <div className="flex flex-wrap items-center gap-x-5 gap-y-3">
568
582
  <StatusChip tone="neutral">private</StatusChip>
569
583
  <StatusChip tone="success">completed</StatusChip>
570
584
  <StatusChip tone="danger">failed</StatusChip>
571
585
  <StatusChip tone="warning">blocked</StatusChip>
572
586
  <StatusChip tone="info">preference</StatusChip>
573
587
  <StatusChip tone="accent">knowledge</StatusChip>
574
- <StatusChip size="compact" tone="success">
575
- public
588
+ <StatusChip size="compact" tone="warning">
589
+ Trigger unavailable
576
590
  </StatusChip>
577
591
  </div>
578
592
  </Fixture>
593
+ <Fixture title="Actor avatars">
594
+ <div className="flex items-center gap-4">
595
+ <ActorAvatar
596
+ name="Ada Lovelace"
597
+ imageUrl="/_junior/dashboard/avatar.png"
598
+ size="detail"
599
+ />
600
+ <ActorAvatar name="Grace Hopper" size="detail" />
601
+ <ActorAvatar
602
+ name="Alan Turing"
603
+ imageUrl="/missing-avatar.png"
604
+ size="list"
605
+ />
606
+ </div>
607
+ </Fixture>
579
608
  <Fixture title="Participant avatars">
580
609
  <div className="flex flex-wrap items-center gap-8">
581
610
  <ParticipantAvatarStack
@@ -0,0 +1,58 @@
1
+ import { useState } from "react";
2
+
3
+ import { SegmentedNav } from "../../components/SegmentedNav";
4
+ import { SegmentedTabs } from "../../components/SegmentedTabs";
5
+
6
+ /** Show panel tabs switching the panel below them. */
7
+ export function SegmentedTabsFixture() {
8
+ const [tab, setTab] = useState<"details" | "memories" | "usage">("details");
9
+ return (
10
+ <div className="max-w-md">
11
+ <SegmentedTabs
12
+ items={[
13
+ { label: "Details", value: "details" },
14
+ { label: "Memories", value: "memories" },
15
+ { label: "Usage", value: "usage" },
16
+ ]}
17
+ label="Conversation panels"
18
+ onChange={setTab}
19
+ value={tab}
20
+ >
21
+ <p className="m-0 text-sm leading-relaxed text-dashboard-text-muted">
22
+ {tab === "details"
23
+ ? "Conversation summary and linked work."
24
+ : tab === "memories"
25
+ ? "What Junior learned from this conversation."
26
+ : "Time, tokens, and cost for this conversation."}
27
+ </p>
28
+ </SegmentedTabs>
29
+ </div>
30
+ );
31
+ }
32
+
33
+ const PAGE_LINKS = [
34
+ "Overview",
35
+ "People",
36
+ "Locations",
37
+ "Workspaces",
38
+ "Plugins",
39
+ ].map((label) => ({
40
+ isActive: () => label === "Overview",
41
+ label,
42
+ to: `#${label.toLowerCase()}`,
43
+ }));
44
+
45
+ /** Show page links at label width, filling a row, and overflowing a row. */
46
+ export function SegmentedNavFixture() {
47
+ return (
48
+ <div className="grid gap-4">
49
+ <SegmentedNav ariaLabel="Label width" items={PAGE_LINKS} />
50
+ <div className="max-w-md">
51
+ <SegmentedNav ariaLabel="Fill" fill items={PAGE_LINKS.slice(0, 2)} />
52
+ </div>
53
+ <div className="max-w-xs">
54
+ <SegmentedNav ariaLabel="Overflow" fill items={PAGE_LINKS} />
55
+ </div>
56
+ </div>
57
+ );
58
+ }