@sentry/junior-dashboard 0.263.0 → 0.265.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/LICENSE +201 -0
- package/README.md +2 -5
- package/dist/app.js +119 -35
- package/dist/client.js +148 -135
- package/dist/index.d.ts +0 -1
- package/dist/index.js +119 -35
- package/dist/tailwind.css +1 -1
- package/package.json +9 -4
- package/src/api/schema.ts +0 -1
- package/src/app.ts +5 -20
- package/src/client/App.tsx +29 -31
- package/src/client/api.ts +7 -0
- package/src/client/components/BottomSheet.tsx +26 -18
- package/src/client/components/CopyButton.tsx +83 -0
- package/src/client/components/DetailsPanel.tsx +133 -0
- package/src/client/components/Drawer.tsx +34 -33
- package/src/client/components/FilterBar.tsx +16 -31
- package/src/client/components/Metric.tsx +33 -23
- package/src/client/components/PageContentSkeleton.tsx +1 -1
- package/src/client/components/Pagination.tsx +1 -1
- package/src/client/components/ParticipantAvatarStack.tsx +40 -36
- package/src/client/components/ProfileMenu.tsx +27 -93
- package/src/client/components/README.md +101 -64
- package/src/client/components/SegmentedNav.tsx +1 -1
- package/src/client/components/ShimmerText.tsx +2 -6
- package/src/client/components/ThemeSwitcher.tsx +19 -15
- package/src/client/components/charts/ActivityChart.tsx +173 -288
- package/src/client/components/charts/CacheWrites.tsx +63 -50
- package/src/client/components/charts/InputCacheChart.tsx +137 -131
- package/src/client/components/charts/README.md +4 -4
- package/src/client/components/charts/SystemMetricCharts.tsx +68 -95
- package/src/client/components/charts/WorkspaceUsageChart.tsx +42 -62
- package/src/client/components/controls/DirectorySortSelect.tsx +3 -3
- package/src/client/components/controls/TimeRangeSelector.tsx +10 -15
- package/src/client/components/layout/DashboardHeader.tsx +44 -90
- package/src/client/components/layout/SecondaryNavigation.tsx +2 -1
- package/src/client/components/layout/VisualViewportShell.tsx +2 -2
- package/src/client/components/ui/accordion.tsx +77 -0
- package/src/client/components/ui/alert-dialog.tsx +89 -0
- package/src/client/components/ui/button.tsx +85 -0
- package/src/client/components/ui/chart.tsx +74 -0
- package/src/client/components/ui/checkbox.tsx +26 -0
- package/src/client/components/ui/collapsible.tsx +22 -0
- package/src/client/components/ui/field.tsx +49 -0
- package/src/client/components/ui/input.tsx +35 -0
- package/src/client/components/ui/native-select.tsx +57 -0
- package/src/client/components/ui/popover.tsx +67 -0
- package/src/client/components/ui/radio-group.tsx +32 -0
- package/src/client/components/ui/sheet.tsx +151 -0
- package/src/client/components/ui/skeleton.tsx +19 -0
- package/src/client/components/ui/table.tsx +80 -0
- package/src/client/components/ui/tabs.tsx +75 -0
- package/src/client/components/ui/textarea.tsx +17 -0
- package/src/client/components/ui/toast.tsx +215 -0
- package/src/client/components/ui/toggle-group.tsx +36 -0
- package/src/client/components/ui/toggle.tsx +37 -0
- package/src/client/components/ui/tooltip.tsx +168 -0
- package/src/client/components/ui/trace.tsx +96 -0
- package/src/client/components/ui/useSheet.ts +74 -0
- package/src/client/conversations/ChatLayout.tsx +7 -3
- package/src/client/conversations/ConversationBrief.tsx +17 -6
- package/src/client/conversations/ConversationChildAgents.tsx +87 -58
- package/src/client/conversations/ConversationComposer.tsx +26 -12
- package/src/client/conversations/ConversationDetails.tsx +198 -95
- package/src/client/conversations/ConversationEventDetails.tsx +141 -0
- package/src/client/conversations/ConversationEventLog.tsx +150 -129
- package/src/client/conversations/ConversationHeader.tsx +8 -3
- package/src/client/conversations/ConversationHeaderActions.tsx +90 -82
- package/src/client/conversations/ConversationHomeList.tsx +1 -1
- package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
- package/src/client/conversations/ConversationMeta.tsx +111 -92
- package/src/client/conversations/ConversationPage.tsx +241 -110
- package/src/client/conversations/ConversationSidebar.tsx +48 -29
- package/src/client/conversations/ConversationUsageBreakdown.tsx +16 -3
- package/src/client/conversations/ConversationWatches.tsx +165 -0
- package/src/client/conversations/ConversationWorkspace.tsx +12 -22
- package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
- package/src/client/conversations/DetailsList.tsx +124 -0
- package/src/client/conversations/EventDetails.tsx +42 -8
- package/src/client/conversations/ForkConversationButton.tsx +27 -16
- package/src/client/conversations/ObjectCard.tsx +20 -8
- package/src/client/conversations/PendingMailboxStack.tsx +52 -33
- package/src/client/conversations/README.md +93 -19
- package/src/client/conversations/ShareConversationButton.tsx +28 -3
- package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
- package/src/client/conversations/ToolFrame.tsx +36 -52
- package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
- package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
- package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
- package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
- package/src/client/conversations/TranscriptLoading.tsx +1 -1
- package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
- package/src/client/conversations/TranscriptMessageContextView.tsx +8 -2
- package/src/client/conversations/TranscriptMessageView.tsx +6 -1
- package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
- package/src/client/conversations/TranscriptSubagentView.tsx +3 -3
- package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
- package/src/client/conversations/TranscriptToolView.tsx +1 -1
- package/src/client/conversations/TranscriptTurnContextView.tsx +99 -27
- package/src/client/conversations/TranscriptView.tsx +63 -27
- package/src/client/conversations/eventLog.ts +57 -0
- package/src/client/conversations/eventTime.ts +60 -0
- package/src/client/conversations/queries.ts +29 -0
- package/src/client/conversations/transcriptBottomPinning.ts +34 -0
- package/src/client/format.ts +5 -0
- package/src/client/lib/utils.ts +1 -0
- package/src/client/pages/ConnectionsPage.tsx +2 -2
- package/src/client/pages/PersonalTokensPage.tsx +4 -4
- package/src/client/pages/SettingsPage.tsx +9 -10
- package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
- package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
- package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
- package/src/client/pages/automations/AutomationDetails.tsx +193 -0
- package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
- package/src/client/pages/automations/AutomationEditor.tsx +41 -29
- package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
- package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
- package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
- package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
- package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
- package/src/client/pages/automations/AutomationFilters.tsx +189 -74
- package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
- package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
- package/src/client/pages/automations/AutomationRow.tsx +12 -138
- package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
- package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
- package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
- package/src/client/pages/automations/AutomationStatus.tsx +201 -0
- package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
- package/src/client/pages/automations/AutomationsPage.tsx +80 -68
- package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
- package/src/client/pages/automations/README.md +57 -0
- package/src/client/pages/automations/TriggerFilters.tsx +22 -0
- package/src/client/pages/automations/automationFormat.ts +37 -23
- package/src/client/pages/code/CodeActivityChart.tsx +59 -101
- package/src/client/pages/code/CodePage.tsx +52 -55
- package/src/client/pages/code/RepositoryPage.tsx +6 -8
- package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
- package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
- package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
- package/src/client/pages/memory/MemoryDetails.tsx +218 -0
- package/src/client/pages/memory/MemoryPage.tsx +168 -112
- package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
- package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
- package/src/client/pages/memory/memoryRecord.ts +1 -1
- package/src/client/pages/people/ContributionGrid.tsx +23 -16
- package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
- package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
- package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
- package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
- package/src/client/pages/system/GuardianActivity.tsx +1 -2
- package/src/client/pages/system/PluginBarChart.tsx +106 -87
- package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
- package/src/client/pages/system/README.md +23 -0
- package/src/client/pages/system/SystemNavigation.tsx +5 -1
- package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
- package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
- package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
- package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
- package/src/client/pages/system/WorkspaceList.tsx +20 -30
- package/src/client/pages/system/WorkspacesPage.tsx +117 -119
- package/src/client/pages/system/sandboxSize.ts +8 -0
- package/src/client/pages/system/workspaceQueries.ts +53 -0
- package/src/client/pages/user/PluginUserPage.tsx +51 -2
- package/src/client/pages/user/pluginUserPageData.ts +1 -6
- package/src/client/styles.ts +4 -0
- package/src/client.tsx +7 -1
- package/src/mock-reporting/automation-collection.ts +4 -2
- package/src/mock-reporting/fixtures.ts +23 -7
- package/src/mock-reporting/routes.ts +12 -0
- package/src/mock-reporting/watches.ts +94 -0
- package/src/shell.ts +4 -9
- package/src/tailwind.css +77 -10
- package/src/client/components/Button.tsx +0 -118
- package/src/client/components/Field.tsx +0 -53
- package/src/client/components/SegmentedTabs.tsx +0 -119
- package/src/client/components/Select.tsx +0 -52
- package/src/client/components/Sheet.LICENSE +0 -21
- package/src/client/components/Sheet.tsx +0 -141
- package/src/client/components/Skeleton.tsx +0 -17
- package/src/client/components/TextInput.tsx +0 -58
- package/src/client/components/Tooltip.tsx +0 -216
- package/src/client/components/charts/useChartLayout.ts +0 -27
- package/src/client/conversations/ConversationDetailsFrame.tsx +0 -91
- package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
- package/src/client/pages/dev/ChartFixtures.tsx +0 -325
- package/src/client/pages/dev/ComponentsPage.tsx +0 -899
- package/src/client/pages/dev/ConversationFixture.tsx +0 -231
- package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
- package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
- package/src/client/pages/dev/SelectFixtures.tsx +0 -62
- package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
- package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
- package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
- package/src/client/pages/system/SnapshotSummary.tsx +0 -78
|
@@ -3,6 +3,7 @@ import type { ReactNode } from "react";
|
|
|
3
3
|
import { cn } from "../styles";
|
|
4
4
|
import { Card } from "./layout/Card";
|
|
5
5
|
import { SearchInput } from "./SearchInput";
|
|
6
|
+
import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
|
|
6
7
|
|
|
7
8
|
/** Lay out labeled filter groups and an optional search field. */
|
|
8
9
|
export function FilterBar(props: {
|
|
@@ -69,43 +70,27 @@ export function FilterTabList(props: {
|
|
|
69
70
|
value: string;
|
|
70
71
|
}) {
|
|
71
72
|
return (
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
aria-selected={selected}
|
|
82
|
-
className={cn(
|
|
83
|
-
"relative flex min-w-0 cursor-pointer items-center justify-between gap-2 border-0 bg-transparent px-3 py-2.5 font-sans font-medium text-xs transition-colors after:absolute after:inset-x-2 after:bottom-0 after:h-px sm:shrink-0 sm:justify-start",
|
|
84
|
-
selected
|
|
85
|
-
? "text-dashboard-accent-text after:bg-dashboard-accent"
|
|
86
|
-
: "text-dashboard-text-muted after:bg-transparent hover:text-dashboard-text",
|
|
87
|
-
)}
|
|
73
|
+
<Tabs onValueChange={props.onChange} value={props.value}>
|
|
74
|
+
<TabsList
|
|
75
|
+
aria-label={props.ariaLabel}
|
|
76
|
+
className="grid grid-cols-3 sm:flex sm:overflow-x-auto"
|
|
77
|
+
variant="line"
|
|
78
|
+
>
|
|
79
|
+
{props.items.map((item) => (
|
|
80
|
+
<TabsTrigger
|
|
81
|
+
className="group/tab justify-between sm:shrink-0 sm:justify-start"
|
|
88
82
|
key={item.value || "all"}
|
|
89
|
-
|
|
90
|
-
role="tab"
|
|
91
|
-
type="button"
|
|
83
|
+
value={item.value}
|
|
92
84
|
>
|
|
93
85
|
{item.label}
|
|
94
86
|
{item.count !== undefined ? (
|
|
95
|
-
<span
|
|
96
|
-
className={cn(
|
|
97
|
-
"rounded-sm border px-1.5 py-0.5 text-xs",
|
|
98
|
-
selected
|
|
99
|
-
? "border-dashboard-accent/20 bg-dashboard-accent/10 text-dashboard-accent-text"
|
|
100
|
-
: "border-dashboard-border bg-dashboard-fill-soft text-dashboard-text-muted",
|
|
101
|
-
)}
|
|
102
|
-
>
|
|
87
|
+
<span className="rounded-sm border border-dashboard-border bg-dashboard-fill-soft px-1.5 py-0.5 text-xs text-dashboard-text-muted group-data-active/tab:border-dashboard-accent/20 group-data-active/tab:bg-dashboard-accent/10 group-data-active/tab:text-dashboard-accent-text">
|
|
103
88
|
{item.count.toLocaleString("en-US")}
|
|
104
89
|
</span>
|
|
105
90
|
) : null}
|
|
106
|
-
</
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
</
|
|
91
|
+
</TabsTrigger>
|
|
92
|
+
))}
|
|
93
|
+
</TabsList>
|
|
94
|
+
</Tabs>
|
|
110
95
|
);
|
|
111
96
|
}
|
|
@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
|
|
|
2
2
|
|
|
3
3
|
import { cn } from "../styles";
|
|
4
4
|
import { ShimmerText } from "./ShimmerText";
|
|
5
|
-
import { Tooltip } from "./
|
|
5
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
|
6
6
|
|
|
7
7
|
export type MetricTooltipLine = {
|
|
8
8
|
label?: string;
|
|
@@ -85,34 +85,44 @@ export function MetricValue(props: {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
return (
|
|
88
|
-
<Tooltip
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
88
|
+
<Tooltip>
|
|
89
|
+
<TooltipTrigger
|
|
90
|
+
render={
|
|
91
|
+
<span
|
|
92
|
+
className={cn(
|
|
93
|
+
"inline-flex border-b border-dotted border-dashboard-border-interactive outline-none transition-colors hover:border-dashboard-border-heavy focus-visible:border-dashboard-border-heavy",
|
|
94
|
+
props.className,
|
|
95
|
+
)}
|
|
96
|
+
tabIndex={0}
|
|
97
|
+
>
|
|
98
|
+
{props.children}
|
|
99
|
+
</span>
|
|
100
|
+
}
|
|
101
|
+
/>
|
|
102
|
+
<TooltipContent
|
|
103
|
+
align={
|
|
104
|
+
props.align === "left"
|
|
105
|
+
? "start"
|
|
106
|
+
: props.align === "right"
|
|
107
|
+
? "end"
|
|
108
|
+
: "center"
|
|
109
|
+
}
|
|
110
|
+
className={cn(
|
|
111
|
+
"w-[calc(100vw-2rem)] max-w-80 rounded-lg bg-dashboard-bg text-left font-normal shadow-xl shadow-black/35",
|
|
112
|
+
props.tooltipCard ? "p-3 leading-snug" : "px-3 py-2 leading-relaxed",
|
|
113
|
+
)}
|
|
114
|
+
side={props.tooltipPlacement === "above" ? "top" : "bottom"}
|
|
115
|
+
>
|
|
116
|
+
{props.tooltipCard ? (
|
|
117
|
+
<span className="block max-h-[min(30rem,var(--available-height,30rem))] overflow-y-auto">
|
|
97
118
|
{props.tooltipCard}
|
|
98
119
|
</span>
|
|
99
120
|
) : tooltip ? (
|
|
100
121
|
<span className="block max-h-72 overflow-y-auto">
|
|
101
122
|
<TooltipLines lines={tooltip} />
|
|
102
123
|
</span>
|
|
103
|
-
) : null
|
|
104
|
-
|
|
105
|
-
placement={props.tooltipPlacement ?? "below"}
|
|
106
|
-
>
|
|
107
|
-
<span
|
|
108
|
-
className={cn(
|
|
109
|
-
"inline-flex border-b border-dotted border-dashboard-border-interactive outline-none transition-colors hover:border-dashboard-border-heavy focus-visible:border-dashboard-border-heavy",
|
|
110
|
-
props.className,
|
|
111
|
-
)}
|
|
112
|
-
tabIndex={0}
|
|
113
|
-
>
|
|
114
|
-
{props.children}
|
|
115
|
-
</span>
|
|
124
|
+
) : null}
|
|
125
|
+
</TooltipContent>
|
|
116
126
|
</Tooltip>
|
|
117
127
|
);
|
|
118
128
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
2
2
|
|
|
3
3
|
import { cn } from "../styles";
|
|
4
|
-
import { Button } from "./
|
|
4
|
+
import { Button } from "./ui/button";
|
|
5
5
|
|
|
6
6
|
/** Render a centered load-more control for cursor or progressive lists. */
|
|
7
7
|
export function LoadMorePagination(props: {
|
|
@@ -4,7 +4,7 @@ import { actorLabel } from "../format";
|
|
|
4
4
|
import { cn } from "../styles";
|
|
5
5
|
import type { Conversation } from "../types";
|
|
6
6
|
import { ActorAvatar } from "./ActorAvatar";
|
|
7
|
-
import { Tooltip } from "./
|
|
7
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
|
8
8
|
|
|
9
9
|
const MAX_VISIBLE_PARTICIPANTS = 3;
|
|
10
10
|
|
|
@@ -57,22 +57,25 @@ function Avatar(props: {
|
|
|
57
57
|
}) {
|
|
58
58
|
const label = participantDescription(props.participant);
|
|
59
59
|
return (
|
|
60
|
-
<Tooltip
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
60
|
+
<Tooltip>
|
|
61
|
+
<TooltipTrigger
|
|
62
|
+
render={
|
|
63
|
+
<span
|
|
64
|
+
aria-label={label}
|
|
65
|
+
className="relative -ml-1 inline-flex rounded-full first:ml-0 pointer-events-none focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus [@media(hover:hover)_and_(pointer:fine)]:pointer-events-auto"
|
|
66
|
+
onKeyDown={(event) => event.stopPropagation()}
|
|
67
|
+
tabIndex={0}
|
|
68
|
+
>
|
|
69
|
+
<ActorAvatar
|
|
70
|
+
name={participantName(props.participant)}
|
|
71
|
+
size={props.size}
|
|
72
|
+
/>
|
|
73
|
+
</span>
|
|
74
|
+
}
|
|
75
|
+
/>
|
|
76
|
+
<TooltipContent>
|
|
77
|
+
<ParticipantTooltipContent participant={props.participant} />
|
|
78
|
+
</TooltipContent>
|
|
76
79
|
</Tooltip>
|
|
77
80
|
);
|
|
78
81
|
}
|
|
@@ -105,8 +108,25 @@ export function ParticipantAvatarStack(props: ParticipantAvatarStackProps) {
|
|
|
105
108
|
/>
|
|
106
109
|
))}
|
|
107
110
|
{hidden.length > 0 ? (
|
|
108
|
-
<Tooltip
|
|
109
|
-
|
|
111
|
+
<Tooltip>
|
|
112
|
+
<TooltipTrigger
|
|
113
|
+
render={
|
|
114
|
+
<span
|
|
115
|
+
aria-label={`${hidden.length} more participants: ${hidden
|
|
116
|
+
.map(participantDescription)
|
|
117
|
+
.join("; ")}`}
|
|
118
|
+
className={cn(
|
|
119
|
+
"relative -ml-1 inline-grid shrink-0 pointer-events-none [@media(hover:hover)_and_(pointer:fine)]:pointer-events-auto place-items-center rounded-full border-2 border-dashboard-bg bg-dashboard-surface-active font-sans font-semibold tabular-nums leading-none text-dashboard-text-muted focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus",
|
|
120
|
+
props.size === "list" ? "size-6 text-2xs" : "size-7 text-xs",
|
|
121
|
+
)}
|
|
122
|
+
onKeyDown={(event) => event.stopPropagation()}
|
|
123
|
+
tabIndex={0}
|
|
124
|
+
>
|
|
125
|
+
+{hidden.length}
|
|
126
|
+
</span>
|
|
127
|
+
}
|
|
128
|
+
/>
|
|
129
|
+
<TooltipContent>
|
|
110
130
|
<span className="grid gap-1 font-sans">
|
|
111
131
|
{hidden.map((participant, index) => (
|
|
112
132
|
<span key={participantKey(participant, index)}>
|
|
@@ -114,23 +134,7 @@ export function ParticipantAvatarStack(props: ParticipantAvatarStackProps) {
|
|
|
114
134
|
</span>
|
|
115
135
|
))}
|
|
116
136
|
</span>
|
|
117
|
-
|
|
118
|
-
focusable
|
|
119
|
-
triggerClassName="-ml-1 pointer-events-none [@media(hover:hover)_and_(pointer:fine)]:pointer-events-auto"
|
|
120
|
-
>
|
|
121
|
-
<span
|
|
122
|
-
aria-label={`${hidden.length} more participants: ${hidden
|
|
123
|
-
.map(participantDescription)
|
|
124
|
-
.join("; ")}`}
|
|
125
|
-
className={cn(
|
|
126
|
-
"relative inline-grid shrink-0 place-items-center rounded-full border-2 border-dashboard-bg bg-dashboard-surface-active font-sans font-semibold tabular-nums leading-none text-dashboard-text-muted focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus",
|
|
127
|
-
props.size === "list" ? "size-6 text-2xs" : "size-7 text-xs",
|
|
128
|
-
)}
|
|
129
|
-
onKeyDown={(event) => event.stopPropagation()}
|
|
130
|
-
tabIndex={0}
|
|
131
|
-
>
|
|
132
|
-
+{hidden.length}
|
|
133
|
-
</span>
|
|
137
|
+
</TooltipContent>
|
|
134
138
|
</Tooltip>
|
|
135
139
|
) : null}
|
|
136
140
|
</span>
|
|
@@ -8,12 +8,7 @@ import {
|
|
|
8
8
|
ShieldCheck,
|
|
9
9
|
UserRound,
|
|
10
10
|
} from "lucide-react";
|
|
11
|
-
import {
|
|
12
|
-
useEffect,
|
|
13
|
-
useRef,
|
|
14
|
-
useState,
|
|
15
|
-
type PointerEvent as ReactPointerEvent,
|
|
16
|
-
} from "react";
|
|
11
|
+
import { useState } from "react";
|
|
17
12
|
import { Link } from "react-router";
|
|
18
13
|
import type { PersonalSpendReport } from "@sentry/junior/api/schema";
|
|
19
14
|
import type { PluginUserPageLink } from "@sentry/junior-plugin-api";
|
|
@@ -23,6 +18,7 @@ import { pluginUserPagePath } from "../pages/user/PluginUserPage";
|
|
|
23
18
|
import { cn, dashboardInteractiveTextClass } from "../styles";
|
|
24
19
|
import type { Identity } from "../types";
|
|
25
20
|
import { ThemeSwitcher } from "./ThemeSwitcher";
|
|
21
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
|
26
22
|
|
|
27
23
|
type ProfileMenuProps = {
|
|
28
24
|
identity: Identity;
|
|
@@ -65,14 +61,6 @@ export function ProfileMenu({
|
|
|
65
61
|
variant = "popover",
|
|
66
62
|
}: ProfileMenuProps) {
|
|
67
63
|
const [open, setOpen] = useState(false);
|
|
68
|
-
const rootRef = useRef<HTMLDivElement>(null);
|
|
69
|
-
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
70
|
-
const openTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
|
71
|
-
undefined,
|
|
72
|
-
);
|
|
73
|
-
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
|
74
|
-
undefined,
|
|
75
|
-
);
|
|
76
64
|
const email = identity.user.email!;
|
|
77
65
|
const name = identity.user.name?.trim() || email;
|
|
78
66
|
const sevenDaySpend = spend
|
|
@@ -85,54 +73,6 @@ export function ProfileMenu({
|
|
|
85
73
|
(page) => page.navigation === "profile",
|
|
86
74
|
);
|
|
87
75
|
|
|
88
|
-
function clearHoverTimers() {
|
|
89
|
-
if (openTimerRef.current) clearTimeout(openTimerRef.current);
|
|
90
|
-
if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
|
|
91
|
-
openTimerRef.current = undefined;
|
|
92
|
-
closeTimerRef.current = undefined;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function openOnHover(event: ReactPointerEvent<HTMLDivElement>) {
|
|
96
|
-
if (event.pointerType !== "mouse") return;
|
|
97
|
-
clearHoverTimers();
|
|
98
|
-
openTimerRef.current = setTimeout(() => setOpen(true), HOVER_OPEN_DELAY_MS);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function closeOnHover(event: ReactPointerEvent<HTMLDivElement>) {
|
|
102
|
-
if (event.pointerType !== "mouse") return;
|
|
103
|
-
clearHoverTimers();
|
|
104
|
-
closeTimerRef.current = setTimeout(
|
|
105
|
-
() => setOpen(false),
|
|
106
|
-
HOVER_CLOSE_DELAY_MS,
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
useEffect(() => clearHoverTimers, []);
|
|
111
|
-
|
|
112
|
-
useEffect(() => {
|
|
113
|
-
if (variant !== "popover" || !open) return;
|
|
114
|
-
|
|
115
|
-
function closeOnOutsideClick(event: PointerEvent) {
|
|
116
|
-
if (!rootRef.current?.contains(event.target as Node)) {
|
|
117
|
-
setOpen(false);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function closeOnEscape(event: KeyboardEvent) {
|
|
122
|
-
if (event.key === "Escape") {
|
|
123
|
-
setOpen(false);
|
|
124
|
-
triggerRef.current?.focus();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
document.addEventListener("pointerdown", closeOnOutsideClick);
|
|
129
|
-
document.addEventListener("keydown", closeOnEscape);
|
|
130
|
-
return () => {
|
|
131
|
-
document.removeEventListener("pointerdown", closeOnOutsideClick);
|
|
132
|
-
document.removeEventListener("keydown", closeOnEscape);
|
|
133
|
-
};
|
|
134
|
-
}, [open, variant]);
|
|
135
|
-
|
|
136
76
|
const avatar = (
|
|
137
77
|
<span
|
|
138
78
|
aria-hidden="true"
|
|
@@ -335,32 +275,28 @@ export function ProfileMenu({
|
|
|
335
275
|
}
|
|
336
276
|
|
|
337
277
|
return (
|
|
338
|
-
<
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
278
|
+
<Popover
|
|
279
|
+
onOpenChange={(nextOpen, details) => {
|
|
280
|
+
// A mouse click always keeps the menu open, also after hover opened it.
|
|
281
|
+
// A keyboard press (detail 0) still toggles it.
|
|
282
|
+
const mouseClick =
|
|
283
|
+
details.event instanceof MouseEvent && details.event.detail > 0;
|
|
284
|
+
if (!nextOpen && details.reason === "trigger-press" && mouseClick) {
|
|
285
|
+
return;
|
|
343
286
|
}
|
|
287
|
+
setOpen(nextOpen);
|
|
344
288
|
}}
|
|
345
|
-
|
|
346
|
-
onPointerLeave={closeOnHover}
|
|
347
|
-
ref={rootRef}
|
|
289
|
+
open={open}
|
|
348
290
|
>
|
|
349
|
-
<
|
|
350
|
-
aria-controls="profile-popover"
|
|
351
|
-
aria-expanded={open}
|
|
352
|
-
aria-haspopup="true"
|
|
291
|
+
<PopoverTrigger
|
|
353
292
|
aria-label={`${open ? "Close" : "Open"} profile menu for ${name}. Personal model spend: 7 days ${sevenDaySpend}, 30 days ${thirtyDaySpend}.`}
|
|
354
293
|
className={cn(
|
|
355
294
|
"group flex h-10 cursor-pointer items-center gap-2 rounded-lg border-0 bg-transparent px-1.5 text-dashboard-text transition-colors hover:bg-dashboard-fill-hover hover:text-dashboard-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus/70",
|
|
356
295
|
open && "bg-dashboard-fill-hover text-dashboard-text",
|
|
357
296
|
)}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}}
|
|
362
|
-
ref={triggerRef}
|
|
363
|
-
type="button"
|
|
297
|
+
closeDelay={HOVER_CLOSE_DELAY_MS}
|
|
298
|
+
delay={HOVER_OPEN_DELAY_MS}
|
|
299
|
+
openOnHover
|
|
364
300
|
>
|
|
365
301
|
<span className="hidden items-center gap-2 md:flex">
|
|
366
302
|
<span className="flex items-baseline gap-1 whitespace-nowrap tabular-nums">
|
|
@@ -399,18 +335,16 @@ export function ProfileMenu({
|
|
|
399
335
|
size={14}
|
|
400
336
|
strokeWidth={2}
|
|
401
337
|
/>
|
|
402
|
-
</
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
) : null}
|
|
414
|
-
</div>
|
|
338
|
+
</PopoverTrigger>
|
|
339
|
+
<PopoverContent
|
|
340
|
+
align="end"
|
|
341
|
+
className="w-64 rounded-xl border-0 bg-dashboard-surface-raised/95 p-1.5 shadow-2xl shadow-black/75 backdrop-blur-xl"
|
|
342
|
+
id="profile-popover"
|
|
343
|
+
>
|
|
344
|
+
{identityBlock}
|
|
345
|
+
<ThemeSwitcher className="border-b border-dashboard-border-strong px-2.5 py-2" />
|
|
346
|
+
{menuLinks(() => setOpen(false))}
|
|
347
|
+
</PopoverContent>
|
|
348
|
+
</Popover>
|
|
415
349
|
);
|
|
416
350
|
}
|
|
@@ -4,6 +4,74 @@ Shared dashboard components live here. Feature pages compose these surfaces
|
|
|
4
4
|
instead of copying Tailwind blocks. See `policies/frontend-components.md` for
|
|
5
5
|
the policy. `pnpm dashboard-style:check` enforces the token rules below.
|
|
6
6
|
|
|
7
|
+
## shadcn/ui
|
|
8
|
+
|
|
9
|
+
`packages/junior-dashboard/components.json` configures shadcn/ui for this package.
|
|
10
|
+
It uses Base UI (`base-nova`), Tailwind 4, and Lucide. Add components from the
|
|
11
|
+
repository root:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pnpm --filter @sentry/junior-dashboard exec shadcn add <component>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The CLI writes components to `src/client/components/ui/`. Its `@dashboard/*`
|
|
18
|
+
alias points to `src/client/*`. The existing `@/*` alias still points to core.
|
|
19
|
+
The `ui/` files join classes with the merging `cn` from
|
|
20
|
+
`src/client/lib/utils.ts`, so a caller's `className` wins over a base class.
|
|
21
|
+
|
|
22
|
+
The shadcn color tokens in `src/tailwind.css` use the dashboard tokens in both
|
|
23
|
+
themes. The `dark:` variant follows `data-theme`. Keep the existing Tailwind
|
|
24
|
+
radius scale. Do not apply the CLI's global border or body resets. Check each
|
|
25
|
+
added component against the rules below before use.
|
|
26
|
+
|
|
27
|
+
Each `ui/` file is shadcn source that uses dashboard tokens. Remove exports
|
|
28
|
+
that no app code uses. Use `ui/` components directly. Do not add an app
|
|
29
|
+
component that only renames a `ui/` component or its props. Change the `ui/`
|
|
30
|
+
file when every caller needs the same style or variant. App components compose
|
|
31
|
+
`ui/` source only when they add structure or behavior:
|
|
32
|
+
|
|
33
|
+
| App component | shadcn source |
|
|
34
|
+
| ------------------------------ | ----------------- |
|
|
35
|
+
| `Drawer`, `BottomSheet` | `ui/sheet` |
|
|
36
|
+
| `layout/DashboardHeader` (nav) | `ui/sheet` |
|
|
37
|
+
| `ProfileMenu` (desktop) | `ui/popover` |
|
|
38
|
+
| `TimeRangeSelector` | `ui/toggle-group` |
|
|
39
|
+
| `ThemeSwitcher` | `ui/toggle-group` |
|
|
40
|
+
| `TranscriptViewToggle` | `ui/toggle-group` |
|
|
41
|
+
| `FilterTabList` | `ui/tabs` |
|
|
42
|
+
| `charts/ActivityChartTooltip` | `ui/tooltip` |
|
|
43
|
+
| `conversations/ToolFrame` | `ui/trace` |
|
|
44
|
+
| `TranscriptReasoningView` | `ui/trace` |
|
|
45
|
+
| Recharts activity charts | `ui/chart` |
|
|
46
|
+
|
|
47
|
+
Rules for adopted components:
|
|
48
|
+
|
|
49
|
+
- Use `ui/trace` for transcript activity rows. It composes `ui/collapsible`.
|
|
50
|
+
A closed tool row unmounts its panel, so it does not run Shiki. Reasoning
|
|
51
|
+
sets `hiddenUntilFound`, so browser find reaches closed text. Transcript
|
|
52
|
+
search holds rows open and turns off the row button. Put only inline
|
|
53
|
+
elements, such as `span`, inside the row button.
|
|
54
|
+
- Use `ui/alert-dialog` for every confirmation. Do not use `window.confirm`.
|
|
55
|
+
Keep the confirmed item in state after close, so the closing dialog keeps its
|
|
56
|
+
text.
|
|
57
|
+
- `ui/toast` has no global notification manager. Features call
|
|
58
|
+
`useToastManager` inside the mounted `Toaster` provider.
|
|
59
|
+
- Put a checkbox or radio inside its `<label>`. Give each `RadioGroup` an
|
|
60
|
+
`aria-label`.
|
|
61
|
+
- Give an icon-only control its label with `TooltipContent size="compact"`.
|
|
62
|
+
Do not use the native `title` attribute. Tooltips open on mouse hover and
|
|
63
|
+
keyboard focus only, so a tap still runs the control.
|
|
64
|
+
- When a `ui/button` tooltip trigger can be disabled, set
|
|
65
|
+
`focusableWhenDisabled`. A native disabled button can drop hover and focus,
|
|
66
|
+
so Firefox closes the tooltip and hides pending or result labels.
|
|
67
|
+
`ui/button` styles `data-disabled`, so both disabled modes look the same.
|
|
68
|
+
- Build a form field from `ui/field`. Give `FieldError` the id
|
|
69
|
+
`<control id>-error` and `FieldDescription` the id `<control id>-help`. The
|
|
70
|
+
control sets `aria-invalid` and `aria-describedby`.
|
|
71
|
+
- For a link that looks like a button, put `buttonVariants()` on the router
|
|
72
|
+
`Link`. A form page has one `Button variant="primary"` for its main save
|
|
73
|
+
action.
|
|
74
|
+
|
|
7
75
|
## Foundations
|
|
8
76
|
|
|
9
77
|
Tokens live in the `@theme` block of `src/tailwind.css`. Keep every color in a
|
|
@@ -16,9 +84,9 @@ token so each theme can redefine it.
|
|
|
16
84
|
paint from the saved choice or the system color scheme. `ThemeSwitcher` saves
|
|
17
85
|
the choice for this browser. Use the `light:` variant only for one-off values
|
|
18
86
|
that no token covers, such as provider brand colors.
|
|
19
|
-
- **Charts.** Use `var(--color-*)` for series colors
|
|
20
|
-
|
|
21
|
-
|
|
87
|
+
- **Charts.** Use `var(--color-*)` for series colors, not hex values, so they
|
|
88
|
+
follow the theme. Name series colors in the `ChartContainer` config and fill
|
|
89
|
+
each series with `var(--color-<key>)`.
|
|
22
90
|
- **Surfaces.** Use the surface ladder, darkest to lightest: `bg`,
|
|
23
91
|
`bg-elevated` (drawers), `surface-panel` (sidebar, cards, bubbles),
|
|
24
92
|
`surface-raised` (buttons, tooltips), `control`, `surface-hover`,
|
|
@@ -51,82 +119,53 @@ token so each theme can redefine it.
|
|
|
51
119
|
use `rounded-lg`. The composer, menus, and transcript object cards use
|
|
52
120
|
`rounded-xl`. Avatars and count badges use `rounded-full`.
|
|
53
121
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
`src/client/styles.ts` holds the few class strings that many components share.
|
|
122
|
+
Shared class strings live in `src/client/styles.ts`.
|
|
57
123
|
|
|
58
|
-
|
|
59
|
-
- `dashboardContainerClass` sets the page width.
|
|
60
|
-
- `dashboardInlineLinkClass` styles a neutral underlined link inside prose.
|
|
61
|
-
- `dashboardInteractiveTextClass` styles muted text that brightens on hover.
|
|
62
|
-
- `dashboardComposerSurfaceClass` styles the composer surface.
|
|
124
|
+
## App components
|
|
63
125
|
|
|
64
|
-
## Components
|
|
65
|
-
|
|
66
|
-
### Primitives
|
|
67
|
-
|
|
68
|
-
- **Actions.** `Button` (`default`, `primary`, `danger`, and `size="icon"`) and
|
|
69
|
-
`ButtonLink`. Form pages use `Button tone="primary"` for their one main save
|
|
70
|
-
action. Use `ToggleButton` variants (`pill`, `segment`, `text`) for pressed
|
|
71
|
-
states, not one-off styles.
|
|
72
|
-
- **Forms.** `Field` (`default`, `compact`), `TextInput`, `TextArea`, `Select`,
|
|
73
|
-
and `SearchInput`. Use `TextInput size="comfortable"` for larger mobile
|
|
74
|
-
controls. Use `TextArea prose` for instructions, not code. `Field error`
|
|
75
|
-
renders an error at `<htmlFor>-error`. The owning control must set
|
|
76
|
-
`aria-invalid` and `aria-describedby`. `Select` owns the native control and its
|
|
77
|
-
inset arrow. Use its `filter` and `sort` variants for compact controls.
|
|
78
126
|
- **Status.** Use `StatusDot` for dense table or list markers. Use `StatusChip`
|
|
79
|
-
when the label must stay visible. `StatusChip` is a
|
|
80
|
-
sentence-case text, with no border or fill. It is not a control.
|
|
127
|
+
when the label must stay visible. `StatusChip` is not a control.
|
|
81
128
|
- **Messages.** Use `Notice` for compact transient messages and `FormNotice`
|
|
82
129
|
for wrapping form recovery text.
|
|
83
|
-
- **
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
`
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
Octicons paths. Do not replace it with a provider logo or a status-only icon.
|
|
96
|
-
- **Loading.** Use `Skeleton` for placeholder leaves and `PageContentSkeleton`
|
|
130
|
+
- **Disclosure.** Use `ui/collapsible` for one show-or-hide section. Put a
|
|
131
|
+
`ChevronRight` with `dashboardDisclosureChevronClass` in a `group` trigger.
|
|
132
|
+
Set `hiddenUntilFound` on the content when browser find must reach closed
|
|
133
|
+
text. Closed content then keeps its box, so put margins and padding on a
|
|
134
|
+
child. Use `ui/accordion` for a set of sections with headings.
|
|
135
|
+
- **Details.** `DetailsPanel` docks an aside at 1280px and above, and uses a
|
|
136
|
+
`BottomSheet` below that width. It owns Escape and the scroll frame for
|
|
137
|
+
docked panels.
|
|
138
|
+
- **Identity.** `ObjectIcon` owns the native Ticket, Code change, Automation,
|
|
139
|
+
Deployment, and Item icons. Do not replace it with a provider logo or a
|
|
140
|
+
status-only icon.
|
|
141
|
+
- **Loading.** Use `ui/skeleton` for placeholder leaves and `PageContentSkeleton`
|
|
97
142
|
variants for page bodies. Use `PageRouteLoading` when a page keeps its real
|
|
98
143
|
header above a body skeleton. Use a feature-owned loading component for
|
|
99
144
|
distinct geometry, such as conversations.
|
|
100
145
|
|
|
101
|
-
|
|
146
|
+
## Navigation
|
|
102
147
|
|
|
103
148
|
The dashboard has three navigation levels. Each has its own look, so the
|
|
104
149
|
levels stay distinct.
|
|
105
150
|
|
|
106
151
|
- **App navigation** in `layout/DashboardHeader` uses pills.
|
|
107
152
|
- **Page links** use `SegmentedNav`, through `layout/SecondaryNavigation`. They
|
|
108
|
-
are underline tabs that sit on the header border.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
selected value, items, change handler, and panel content. It owns tab and
|
|
112
|
-
panel labels and keyboard focus. Pass `scroll` to keep the tabs fixed while
|
|
113
|
-
the panel content scrolls. That mode fills its frame and owns the content
|
|
114
|
-
padding, so its scrollbar stays at the frame edge. `BottomSheet` content owns
|
|
115
|
-
its own padding.
|
|
153
|
+
are underline tabs that sit on the header border.
|
|
154
|
+
- **Panel switches** use `ui/tabs`. Give `TabsList` an `aria-label`. Set
|
|
155
|
+
`activateOnFocus` so arrow keys select the tab.
|
|
116
156
|
|
|
117
|
-
Keep collection filters in `FilterTabList`.
|
|
157
|
+
Keep collection filters in `FilterTabList`. It uses `TabsList variant="line"`
|
|
158
|
+
and adds the count badges.
|
|
118
159
|
|
|
119
|
-
|
|
160
|
+
## Patterns
|
|
120
161
|
|
|
121
162
|
- **Layout.** `layout/` holds the shell chrome, page headers, cards, and
|
|
122
163
|
section titles. `DashboardChrome` owns the sticky header block and its one
|
|
123
164
|
bottom border.
|
|
124
|
-
- **
|
|
125
|
-
|
|
126
|
-
`charts/ActivityChart`
|
|
127
|
-
|
|
128
|
-
day or hour charts compose those helpers. They do not format UTC bucket keys
|
|
129
|
-
inline.
|
|
165
|
+
- **Charts.** Put `role="img"` and the chart `aria-label` on the Recharts
|
|
166
|
+
chart, so the tooltip live region stays outside the image. Day and hour
|
|
167
|
+
charts compose the `charts/ActivityChart` axis, average line, and tooltip
|
|
168
|
+
helpers. They do not format UTC bucket keys inline.
|
|
130
169
|
- **Conversation mobile geometry** is a hard ownership edge:
|
|
131
170
|
- `layout/VisualViewportShell` owns the fixed shell, visualViewport CSS
|
|
132
171
|
variables, and body lock.
|
|
@@ -138,16 +177,14 @@ Keep collection filters in `FilterTabList`.
|
|
|
138
177
|
dock, and must not use `ComposerDock`. Do not add viewport height, offset, or
|
|
139
178
|
bottom safe-area math in page modules.
|
|
140
179
|
|
|
141
|
-
### Features
|
|
142
|
-
|
|
143
180
|
Feature surfaces stay under `conversations/` or `pages/` until two real callers
|
|
144
|
-
share them. Transcript
|
|
145
|
-
|
|
181
|
+
share them. See [Transcript components](../conversations/README.md) for
|
|
182
|
+
transcript ownership.
|
|
146
183
|
|
|
147
184
|
## Rules
|
|
148
185
|
|
|
149
186
|
- Prefer a named component on the second real use.
|
|
150
187
|
- Keep Tailwind classes on the owning component.
|
|
151
|
-
-
|
|
152
|
-
|
|
188
|
+
- Check new or changed shared components on a real page with mock data and in
|
|
189
|
+
the dashboard E2E screenshots.
|
|
153
190
|
- One-off page glue may stay inline when extraction only adds indirection.
|