@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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ConversationWatchesReport } from "@sentry/junior/api/schema";
|
|
2
|
+
import {
|
|
3
|
+
DASHBOARD_QA_CONVERSATION_ID,
|
|
4
|
+
readMockConversationDetail,
|
|
5
|
+
} from "./fixtures";
|
|
6
|
+
|
|
7
|
+
const HOUR_MS = 60 * 60_000;
|
|
8
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
9
|
+
|
|
10
|
+
// Watch times are relative to the real clock, not the fixed fixture time, so
|
|
11
|
+
// time-left labels stay readable.
|
|
12
|
+
function iso(offsetMs: number): string {
|
|
13
|
+
return new Date(Date.now() + offsetMs).toISOString();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Return the active Watches of one mock Conversation. The dashboard QA
|
|
18
|
+
* Conversation has one Watch of each common shape: a pull request with a
|
|
19
|
+
* match filter, a deployment, a timer, a redacted Watch, and one that a child
|
|
20
|
+
* agent created.
|
|
21
|
+
*/
|
|
22
|
+
export function readMockConversationWatches(
|
|
23
|
+
conversationId: string,
|
|
24
|
+
): ConversationWatchesReport | undefined {
|
|
25
|
+
if (!readMockConversationDetail(conversationId)) return undefined;
|
|
26
|
+
if (conversationId !== DASHBOARD_QA_CONVERSATION_ID) {
|
|
27
|
+
return { conversationId, watches: [] };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
conversationId,
|
|
31
|
+
watches: [
|
|
32
|
+
{
|
|
33
|
+
createdAt: iso(-3 * HOUR_MS),
|
|
34
|
+
events: [
|
|
35
|
+
"pull_request.checks.failed",
|
|
36
|
+
"pull_request.review.changes_requested",
|
|
37
|
+
"pull_request.merged",
|
|
38
|
+
],
|
|
39
|
+
expiresAt: iso(13 * DAY_MS),
|
|
40
|
+
id: "watch-pr",
|
|
41
|
+
intent:
|
|
42
|
+
"Fix failing checks and address requested changes on the dashboard PR.",
|
|
43
|
+
label: "getsentry/junior#2383",
|
|
44
|
+
namespace: "github",
|
|
45
|
+
resourceType: "pull_request",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
createdAt: iso(-2 * HOUR_MS),
|
|
49
|
+
events: ["deployment.failed", "deployment.succeeded"],
|
|
50
|
+
expiresAt: iso(20 * HOUR_MS),
|
|
51
|
+
id: "watch-deploy",
|
|
52
|
+
intent: "Report the preview URL, or the build error if it fails.",
|
|
53
|
+
label: "junior-dashboard preview",
|
|
54
|
+
match: { target: "preview" },
|
|
55
|
+
namespace: "vercel",
|
|
56
|
+
resourceType: "deployment",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
createdAt: iso(-30 * 60_000),
|
|
60
|
+
events: ["timer.fired"],
|
|
61
|
+
expiresAt: iso(2 * HOUR_MS + 5 * 60_000),
|
|
62
|
+
firesAt: iso(2 * HOUR_MS),
|
|
63
|
+
id: "watch-timer",
|
|
64
|
+
intent: "Check whether CI recovered after the retry.",
|
|
65
|
+
label: "Recheck CI",
|
|
66
|
+
namespace: "junior",
|
|
67
|
+
resourceType: "timer",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
childAgent: {
|
|
71
|
+
conversationId: "junior:internal:dashboard-qa:advisor-review",
|
|
72
|
+
name: "reviewer",
|
|
73
|
+
},
|
|
74
|
+
createdAt: iso(-20 * 60_000),
|
|
75
|
+
events: ["pull_request.review_comment.created"],
|
|
76
|
+
expiresAt: iso(6 * DAY_MS),
|
|
77
|
+
id: "watch-child",
|
|
78
|
+
intent: "Reply to new review comments on the PR.",
|
|
79
|
+
label: "getsentry/junior#2351",
|
|
80
|
+
namespace: "github",
|
|
81
|
+
resourceType: "pull_request",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
createdAt: iso(-10 * 60_000),
|
|
85
|
+
events: ["message.posted"],
|
|
86
|
+
expiresAt: iso(3 * DAY_MS),
|
|
87
|
+
id: "watch-redacted",
|
|
88
|
+
namespace: "slack",
|
|
89
|
+
redacted: true,
|
|
90
|
+
resourceType: "channel",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
package/src/shell.ts
CHANGED
|
@@ -117,9 +117,8 @@ function dashboardStartUrl(basePath: string): string {
|
|
|
117
117
|
/** List the browser page paths served by the authenticated dashboard shell. */
|
|
118
118
|
export function dashboardPagePaths(
|
|
119
119
|
basePath: string,
|
|
120
|
-
options: { componentGallery?: boolean } = {},
|
|
121
120
|
): Array<{ nested?: boolean; path: string }> {
|
|
122
|
-
|
|
121
|
+
return [
|
|
123
122
|
{ path: basePath },
|
|
124
123
|
{
|
|
125
124
|
path: basePath === "/" ? "/admin" : `${basePath}/admin`,
|
|
@@ -165,13 +164,6 @@ export function dashboardPagePaths(
|
|
|
165
164
|
path: basePath === "/" ? "/plugins" : `${basePath}/plugins`,
|
|
166
165
|
},
|
|
167
166
|
];
|
|
168
|
-
if (options.componentGallery) {
|
|
169
|
-
paths.push({
|
|
170
|
-
nested: true,
|
|
171
|
-
path: basePath === "/" ? "/dev" : `${basePath}/dev`,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
return paths;
|
|
175
167
|
}
|
|
176
168
|
|
|
177
169
|
/** Serve the installable web app manifest for the dashboard shell. */
|
|
@@ -286,6 +278,9 @@ export function renderDashboard(basePath: string, agentName: string): Response {
|
|
|
286
278
|
"</pre></section></main>";
|
|
287
279
|
};
|
|
288
280
|
window.addEventListener("error", function (event) {
|
|
281
|
+
// Browsers report a ResizeObserver loop as an error event with no
|
|
282
|
+
// error object. Layout continues, so it must not replace the page.
|
|
283
|
+
if (!event.error && /^ResizeObserver loop/.test(event.message)) return;
|
|
289
284
|
window.__JUNIOR_DASHBOARD_SHOW_ERROR__(event.error || event.message);
|
|
290
285
|
});
|
|
291
286
|
window.addEventListener("unhandledrejection", function (event) {
|
package/src/tailwind.css
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@300;400;500;600;700&display=swap");
|
|
2
2
|
@import "tailwindcss";
|
|
3
|
+
@import "tw-animate-css";
|
|
4
|
+
@import "shadcn/tailwind.css";
|
|
3
5
|
|
|
4
6
|
/*
|
|
5
7
|
* Dashboard design tokens. components/README.md explains each role.
|
|
@@ -214,6 +216,81 @@
|
|
|
214
216
|
--color-violet-400: oklch(54.1% 0.281 293.009);
|
|
215
217
|
}
|
|
216
218
|
|
|
219
|
+
/* shadcn components use the dashboard tokens in both themes. */
|
|
220
|
+
@layer base {
|
|
221
|
+
:root {
|
|
222
|
+
--radius: 0.5rem;
|
|
223
|
+
--background: var(--color-dashboard-bg);
|
|
224
|
+
--foreground: var(--color-dashboard-text);
|
|
225
|
+
--card: var(--color-dashboard-surface-panel);
|
|
226
|
+
--card-foreground: var(--color-dashboard-text);
|
|
227
|
+
--popover: var(--color-dashboard-bg-elevated);
|
|
228
|
+
--popover-foreground: var(--color-dashboard-text);
|
|
229
|
+
--primary: var(--color-dashboard-text-solid);
|
|
230
|
+
--primary-foreground: var(--color-dashboard-text-inverse);
|
|
231
|
+
--secondary: var(--color-dashboard-surface-raised);
|
|
232
|
+
--secondary-foreground: var(--color-dashboard-text);
|
|
233
|
+
--muted: var(--color-dashboard-surface-raised);
|
|
234
|
+
--muted-foreground: var(--color-dashboard-text-muted);
|
|
235
|
+
--accent: var(--color-dashboard-surface-hover);
|
|
236
|
+
--accent-foreground: var(--color-dashboard-text);
|
|
237
|
+
--destructive: var(--color-rose-300);
|
|
238
|
+
--border: var(--color-dashboard-border);
|
|
239
|
+
--input: var(--color-dashboard-border-emphasis);
|
|
240
|
+
--ring: var(--color-dashboard-focus);
|
|
241
|
+
--chart-1: var(--color-violet-400);
|
|
242
|
+
--chart-2: var(--color-sky-400);
|
|
243
|
+
--chart-3: var(--color-emerald-400);
|
|
244
|
+
--chart-4: var(--color-amber-400);
|
|
245
|
+
--chart-5: var(--color-rose-400);
|
|
246
|
+
--sidebar: var(--color-dashboard-surface-panel);
|
|
247
|
+
--sidebar-foreground: var(--color-dashboard-text);
|
|
248
|
+
--sidebar-primary: var(--primary);
|
|
249
|
+
--sidebar-primary-foreground: var(--color-dashboard-text-inverse);
|
|
250
|
+
--sidebar-accent: var(--color-dashboard-surface-hover);
|
|
251
|
+
--sidebar-accent-foreground: var(--color-dashboard-text);
|
|
252
|
+
--sidebar-border: var(--color-dashboard-border);
|
|
253
|
+
--sidebar-ring: var(--color-dashboard-focus);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@theme inline {
|
|
258
|
+
--color-background: var(--background);
|
|
259
|
+
--color-foreground: var(--foreground);
|
|
260
|
+
--color-card: var(--card);
|
|
261
|
+
--color-card-foreground: var(--card-foreground);
|
|
262
|
+
--color-popover: var(--popover);
|
|
263
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
264
|
+
--color-primary: var(--primary);
|
|
265
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
266
|
+
--color-secondary: var(--secondary);
|
|
267
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
268
|
+
--color-muted: var(--muted);
|
|
269
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
270
|
+
--color-accent: var(--accent);
|
|
271
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
272
|
+
--color-destructive: var(--destructive);
|
|
273
|
+
--color-border: var(--border);
|
|
274
|
+
--color-input: var(--input);
|
|
275
|
+
--color-ring: var(--ring);
|
|
276
|
+
--color-chart-1: var(--chart-1);
|
|
277
|
+
--color-chart-2: var(--chart-2);
|
|
278
|
+
--color-chart-3: var(--chart-3);
|
|
279
|
+
--color-chart-4: var(--chart-4);
|
|
280
|
+
--color-chart-5: var(--chart-5);
|
|
281
|
+
--color-sidebar: var(--sidebar);
|
|
282
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
283
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
284
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
285
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
286
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
287
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
288
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* The dashboard defaults to dark and selects light with data-theme. */
|
|
292
|
+
@custom-variant dark (&:where(:root:not([data-theme="light"]), :root:not([data-theme="light"]) *));
|
|
293
|
+
|
|
217
294
|
/* Style one-off values that the token and palette mirror cannot cover. */
|
|
218
295
|
@custom-variant light (&:where([data-theme="light"], [data-theme="light"] *));
|
|
219
296
|
|
|
@@ -261,16 +338,6 @@
|
|
|
261
338
|
}
|
|
262
339
|
}
|
|
263
340
|
|
|
264
|
-
@keyframes junior-text-shimmer {
|
|
265
|
-
0% {
|
|
266
|
-
background-position: 200% 50%;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
100% {
|
|
270
|
-
background-position: -200% 50%;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
341
|
@keyframes junior-active-indicator {
|
|
275
342
|
0%,
|
|
276
343
|
100% {
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import type { ButtonHTMLAttributes, Ref } from "react";
|
|
2
|
-
import { Link, type LinkProps } from "react-router";
|
|
3
|
-
|
|
4
|
-
import { cn } from "../styles";
|
|
5
|
-
|
|
6
|
-
type ButtonSize = "default" | "icon";
|
|
7
|
-
type ButtonTone = "default" | "danger" | "primary";
|
|
8
|
-
type ToggleButtonVariant = "pill" | "segment" | "text";
|
|
9
|
-
|
|
10
|
-
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
11
|
-
ref?: Ref<HTMLButtonElement>;
|
|
12
|
-
size?: ButtonSize;
|
|
13
|
-
tone?: ButtonTone;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export type ToggleButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
17
|
-
pressed: boolean;
|
|
18
|
-
variant: ToggleButtonVariant;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const buttonClassName =
|
|
22
|
-
"inline-flex h-9 max-w-full items-center justify-center gap-2 rounded-md border border-dashboard-border-emphasis bg-dashboard-surface-raised px-3 font-sans text-sm font-medium leading-none text-dashboard-text transition-colors hover:border-dashboard-border-heavy hover:bg-dashboard-surface-hover hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dashboard-focus";
|
|
23
|
-
|
|
24
|
-
const dangerButtonClassName =
|
|
25
|
-
"inline-flex h-9 max-w-full items-center justify-center gap-2 rounded-md border border-dashboard-border-strong bg-transparent px-3 font-sans text-sm font-medium leading-none text-dashboard-text-muted transition-colors hover:border-rose-300/40 hover:text-rose-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rose-300/55";
|
|
26
|
-
|
|
27
|
-
/** Render the dashboard's standard bordered command button surface. */
|
|
28
|
-
export function Button({
|
|
29
|
-
className,
|
|
30
|
-
size = "default",
|
|
31
|
-
tone = "default",
|
|
32
|
-
type = "button",
|
|
33
|
-
...props
|
|
34
|
-
}: ButtonProps) {
|
|
35
|
-
const isDanger = tone === "danger";
|
|
36
|
-
return (
|
|
37
|
-
<button
|
|
38
|
-
{...props}
|
|
39
|
-
className={cn(
|
|
40
|
-
"font-sans transition-colors disabled:cursor-not-allowed disabled:opacity-50",
|
|
41
|
-
size === "icon"
|
|
42
|
-
? cn(
|
|
43
|
-
"grid size-9 place-items-center rounded-md border-0 bg-transparent p-0 focus-visible:outline-2 focus-visible:outline-offset-1 disabled:hover:!bg-transparent",
|
|
44
|
-
isDanger
|
|
45
|
-
? "text-dashboard-text-muted hover:!bg-rose-300/10 hover:!text-rose-300 focus-visible:outline-rose-300/55 disabled:hover:!text-dashboard-text-muted"
|
|
46
|
-
: "text-dashboard-text-muted hover:!bg-dashboard-fill-strong hover:!text-dashboard-text focus-visible:outline-dashboard-focus disabled:hover:!text-dashboard-text-muted",
|
|
47
|
-
)
|
|
48
|
-
: tone === "primary"
|
|
49
|
-
? "inline-flex h-9 items-center justify-center gap-2 rounded-md border border-dashboard-text-solid bg-dashboard-text-solid px-3 font-sans text-sm font-medium leading-none text-dashboard-text-inverse hover:border-dashboard-text-subtle hover:bg-dashboard-text-subtle disabled:hover:border-dashboard-text-solid disabled:hover:bg-dashboard-text-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dashboard-focus"
|
|
50
|
-
: isDanger
|
|
51
|
-
? cn(
|
|
52
|
-
dangerButtonClassName,
|
|
53
|
-
"disabled:hover:border-dashboard-border-strong disabled:hover:text-dashboard-text-muted",
|
|
54
|
-
)
|
|
55
|
-
: cn(
|
|
56
|
-
buttonClassName,
|
|
57
|
-
"disabled:hover:border-dashboard-border-emphasis disabled:hover:bg-dashboard-surface-raised disabled:hover:text-dashboard-text",
|
|
58
|
-
),
|
|
59
|
-
props.disabled ? "" : "cursor-pointer",
|
|
60
|
-
className,
|
|
61
|
-
)}
|
|
62
|
-
type={type}
|
|
63
|
-
/>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Render a dashboard navigation link with the standard button surface. */
|
|
68
|
-
export function ButtonLink({ className, ...props }: LinkProps) {
|
|
69
|
-
return (
|
|
70
|
-
<Link
|
|
71
|
-
{...props}
|
|
72
|
-
className={cn(buttonClassName, "no-underline", className)}
|
|
73
|
-
/>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Render a dashboard toggle button with a consistent pressed-state contract. */
|
|
78
|
-
export function ToggleButton({
|
|
79
|
-
className,
|
|
80
|
-
pressed,
|
|
81
|
-
type = "button",
|
|
82
|
-
variant,
|
|
83
|
-
...props
|
|
84
|
-
}: ToggleButtonProps) {
|
|
85
|
-
return (
|
|
86
|
-
<button
|
|
87
|
-
{...props}
|
|
88
|
-
aria-pressed={pressed}
|
|
89
|
-
className={cn(
|
|
90
|
-
toggleButtonBase[variant],
|
|
91
|
-
pressed ? toggleButtonPressed[variant] : toggleButtonIdle[variant],
|
|
92
|
-
className,
|
|
93
|
-
)}
|
|
94
|
-
type={type}
|
|
95
|
-
/>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const toggleButtonBase: Record<ToggleButtonVariant, string> = {
|
|
100
|
-
pill: "cursor-pointer rounded-md border px-2 py-1 font-sans text-xs font-medium leading-tight transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dashboard-focus",
|
|
101
|
-
segment:
|
|
102
|
-
"inline-flex h-7 min-w-10 cursor-pointer items-center justify-center rounded-md border px-2 font-sans text-xs font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dashboard-focus",
|
|
103
|
-
text: "cursor-pointer rounded-sm border-0 bg-transparent px-1.5 py-1 font-sans font-medium underline-offset-4 transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dashboard-focus",
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const toggleButtonPressed: Record<ToggleButtonVariant, string> = {
|
|
107
|
-
pill: "border-dashboard-border-heavy bg-dashboard-text-solid text-dashboard-text-inverse",
|
|
108
|
-
segment:
|
|
109
|
-
"border-dashboard-border-interactive bg-dashboard-surface-active text-dashboard-text",
|
|
110
|
-
text: "text-dashboard-text underline decoration-dashboard-text-solid",
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const toggleButtonIdle: Record<ToggleButtonVariant, string> = {
|
|
114
|
-
pill: "border-dashboard-border-strong bg-dashboard-surface-raised text-dashboard-text-muted hover:border-dashboard-border-interactive hover:bg-dashboard-surface-hover hover:text-dashboard-text",
|
|
115
|
-
segment:
|
|
116
|
-
"border-transparent bg-transparent text-dashboard-text-muted hover:bg-dashboard-fill-hover hover:text-dashboard-text",
|
|
117
|
-
text: "text-dashboard-text-muted hover:text-dashboard-text",
|
|
118
|
-
};
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
import { cn } from "../styles";
|
|
4
|
-
|
|
5
|
-
export type FieldSize = "compact" | "default";
|
|
6
|
-
|
|
7
|
-
const labelClass: Record<FieldSize, string> = {
|
|
8
|
-
compact: "font-sans font-medium text-xs text-dashboard-text-muted",
|
|
9
|
-
default: "text-sm font-semibold text-dashboard-text",
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const stackClass: Record<FieldSize, string> = {
|
|
13
|
-
compact: "grid gap-1.5",
|
|
14
|
-
default: "grid gap-2",
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
/** Label a control with optional help text using the shared form stack. */
|
|
18
|
-
export function Field(props: {
|
|
19
|
-
children: ReactNode;
|
|
20
|
-
className?: string;
|
|
21
|
-
help?: ReactNode;
|
|
22
|
-
error?: string;
|
|
23
|
-
htmlFor?: string;
|
|
24
|
-
label: ReactNode;
|
|
25
|
-
size?: FieldSize;
|
|
26
|
-
}) {
|
|
27
|
-
const size = props.size ?? "default";
|
|
28
|
-
return (
|
|
29
|
-
<div className={cn(stackClass[size], props.className)}>
|
|
30
|
-
<label className={labelClass[size]} htmlFor={props.htmlFor}>
|
|
31
|
-
{props.label}
|
|
32
|
-
</label>
|
|
33
|
-
{props.children}
|
|
34
|
-
{props.error ? (
|
|
35
|
-
<p
|
|
36
|
-
id={props.htmlFor ? `${props.htmlFor}-error` : undefined}
|
|
37
|
-
role="alert"
|
|
38
|
-
className="m-0 text-xs leading-relaxed text-rose-300"
|
|
39
|
-
>
|
|
40
|
-
{props.error}
|
|
41
|
-
</p>
|
|
42
|
-
) : null}
|
|
43
|
-
{props.help ? (
|
|
44
|
-
<p
|
|
45
|
-
id={props.htmlFor ? `${props.htmlFor}-help` : undefined}
|
|
46
|
-
className="m-0 text-xs leading-relaxed text-dashboard-text-muted sm:text-sm"
|
|
47
|
-
>
|
|
48
|
-
{props.help}
|
|
49
|
-
</p>
|
|
50
|
-
) : null}
|
|
51
|
-
</div>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { useId, type ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
import { cn } from "../styles";
|
|
4
|
-
|
|
5
|
-
/** Style the border and fill of the segmented pill track. */
|
|
6
|
-
const segmentedTabsFrameClass =
|
|
7
|
-
"rounded-lg border border-dashboard-border bg-dashboard-surface-panel";
|
|
8
|
-
|
|
9
|
-
/** Style the spacing inside the segmented pill track. */
|
|
10
|
-
const segmentedTabsInsetClass = "gap-1 p-1";
|
|
11
|
-
|
|
12
|
-
/** Style one tab or link in the segmented pill track. */
|
|
13
|
-
export function segmentedTabClass(selected: boolean): string {
|
|
14
|
-
return cn(
|
|
15
|
-
"min-h-9 cursor-pointer rounded-md border-0 px-1.5 py-2 font-sans text-xs font-medium transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus",
|
|
16
|
-
selected
|
|
17
|
-
? "bg-dashboard-fill-strong text-dashboard-text"
|
|
18
|
-
: "bg-transparent text-dashboard-text-muted hover:bg-dashboard-fill-faint hover:text-dashboard-text",
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Switch between related panels. Tabs share the row equally on wide screens. */
|
|
23
|
-
export function SegmentedTabs<const Value extends string>(props: {
|
|
24
|
-
children: ReactNode;
|
|
25
|
-
label: string;
|
|
26
|
-
/** Fill the frame with fixed tabs and padded, scrolling content. */
|
|
27
|
-
scroll?: boolean;
|
|
28
|
-
/** `count` is the number of entries in the panel. Zero and unknown counts are hidden. */
|
|
29
|
-
items: readonly { count?: number; label: string; value: Value }[];
|
|
30
|
-
onChange(value: NoInfer<Value>): void;
|
|
31
|
-
value: NoInfer<Value>;
|
|
32
|
-
}) {
|
|
33
|
-
const id = useId();
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<div
|
|
37
|
-
className={cn(
|
|
38
|
-
"grid min-w-0",
|
|
39
|
-
props.scroll
|
|
40
|
-
? "h-full min-h-0 grid-rows-[auto_minmax(0,1fr)]"
|
|
41
|
-
: "gap-4",
|
|
42
|
-
)}
|
|
43
|
-
>
|
|
44
|
-
<div
|
|
45
|
-
aria-label={props.label}
|
|
46
|
-
className={cn(
|
|
47
|
-
"flex min-w-0",
|
|
48
|
-
segmentedTabsFrameClass,
|
|
49
|
-
segmentedTabsInsetClass,
|
|
50
|
-
props.scroll && "m-4",
|
|
51
|
-
)}
|
|
52
|
-
role="tablist"
|
|
53
|
-
>
|
|
54
|
-
{props.items.map((item, index) => (
|
|
55
|
-
<button
|
|
56
|
-
aria-controls={`${id}-panel`}
|
|
57
|
-
aria-selected={props.value === item.value}
|
|
58
|
-
className={cn(
|
|
59
|
-
"inline-flex min-w-0 flex-auto items-center justify-center gap-1.5 sm:flex-1",
|
|
60
|
-
segmentedTabClass(props.value === item.value),
|
|
61
|
-
)}
|
|
62
|
-
id={`${id}-${item.value}`}
|
|
63
|
-
key={item.value}
|
|
64
|
-
onClick={() => props.onChange(item.value)}
|
|
65
|
-
onKeyDown={(event) => {
|
|
66
|
-
let nextIndex: number;
|
|
67
|
-
const count = props.items.length;
|
|
68
|
-
switch (event.key) {
|
|
69
|
-
case "ArrowLeft":
|
|
70
|
-
nextIndex = (index + count - 1) % count;
|
|
71
|
-
break;
|
|
72
|
-
case "ArrowRight":
|
|
73
|
-
nextIndex = (index + 1) % count;
|
|
74
|
-
break;
|
|
75
|
-
case "Home":
|
|
76
|
-
nextIndex = 0;
|
|
77
|
-
break;
|
|
78
|
-
case "End":
|
|
79
|
-
nextIndex = count - 1;
|
|
80
|
-
break;
|
|
81
|
-
default:
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
event.preventDefault();
|
|
85
|
-
props.onChange(props.items[nextIndex]!.value);
|
|
86
|
-
event.currentTarget.parentElement
|
|
87
|
-
?.querySelectorAll<HTMLButtonElement>('[role="tab"]')
|
|
88
|
-
[nextIndex]?.focus();
|
|
89
|
-
}}
|
|
90
|
-
role="tab"
|
|
91
|
-
tabIndex={props.value === item.value ? 0 : -1}
|
|
92
|
-
type="button"
|
|
93
|
-
>
|
|
94
|
-
<span className="truncate">{item.label}</span>
|
|
95
|
-
{item.count ? (
|
|
96
|
-
<span className="min-w-5 shrink-0 rounded-full bg-dashboard-fill-strong px-1.5 text-center text-xs font-semibold leading-5 tabular-nums">
|
|
97
|
-
{item.count.toLocaleString("en-US")}
|
|
98
|
-
</span>
|
|
99
|
-
) : null}
|
|
100
|
-
</button>
|
|
101
|
-
))}
|
|
102
|
-
</div>
|
|
103
|
-
<div
|
|
104
|
-
aria-labelledby={`${id}-${props.value}`}
|
|
105
|
-
className={cn(
|
|
106
|
-
"min-w-0 focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus",
|
|
107
|
-
props.scroll
|
|
108
|
-
? "min-h-0 overflow-y-auto overscroll-contain px-4 pb-[max(1rem,env(safe-area-inset-bottom))] focus-visible:-outline-offset-2"
|
|
109
|
-
: "rounded-md",
|
|
110
|
-
)}
|
|
111
|
-
id={`${id}-panel`}
|
|
112
|
-
role="tabpanel"
|
|
113
|
-
tabIndex={0}
|
|
114
|
-
>
|
|
115
|
-
{props.children}
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
);
|
|
119
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { SelectHTMLAttributes } from "react";
|
|
2
|
-
import { ChevronDown } from "lucide-react";
|
|
3
|
-
import { cn } from "../styles";
|
|
4
|
-
|
|
5
|
-
const selectClassName = {
|
|
6
|
-
form: "min-h-11 rounded-md border border-dashboard-border-emphasis bg-dashboard-ink pl-3 py-2 text-base text-dashboard-text focus:border-dashboard-focus focus:outline-none aria-invalid:border-rose-300 sm:min-h-10 sm:text-sm",
|
|
7
|
-
filter:
|
|
8
|
-
"h-9 rounded-lg border border-dashboard-border-emphasis bg-dashboard-control pl-2 text-sm text-dashboard-text transition-colors hover:border-dashboard-border-interactive focus:border-dashboard-focus focus:outline-none focus:ring-1 focus:ring-dashboard-focus/25",
|
|
9
|
-
sort: "h-full bg-dashboard-control pl-2 font-sans text-xs text-dashboard-text-muted outline-none",
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
/** Keep native choices and position their arrow consistently across the dashboard. */
|
|
13
|
-
export function Select({
|
|
14
|
-
className,
|
|
15
|
-
variant = "form",
|
|
16
|
-
...props
|
|
17
|
-
}: SelectHTMLAttributes<HTMLSelectElement> & {
|
|
18
|
-
variant?: keyof typeof selectClassName;
|
|
19
|
-
}) {
|
|
20
|
-
const showArrow = !props.multiple && (props.size ?? 1) <= 1;
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<span
|
|
24
|
-
className={cn(
|
|
25
|
-
"relative block w-full min-w-0",
|
|
26
|
-
variant === "sort" && "h-full",
|
|
27
|
-
)}
|
|
28
|
-
>
|
|
29
|
-
<select
|
|
30
|
-
{...props}
|
|
31
|
-
className={cn(
|
|
32
|
-
"block w-full min-w-0 disabled:cursor-not-allowed disabled:opacity-60 [color-scheme:var(--dashboard-color-scheme,dark)]",
|
|
33
|
-
selectClassName[variant],
|
|
34
|
-
showArrow
|
|
35
|
-
? "appearance-none pr-9 forced-colors:appearance-auto"
|
|
36
|
-
: "pr-3",
|
|
37
|
-
className,
|
|
38
|
-
)}
|
|
39
|
-
/>
|
|
40
|
-
{showArrow ? (
|
|
41
|
-
<ChevronDown
|
|
42
|
-
aria-hidden="true"
|
|
43
|
-
className={cn(
|
|
44
|
-
"pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-dashboard-text-muted forced-colors:hidden",
|
|
45
|
-
props.disabled && "opacity-60",
|
|
46
|
-
)}
|
|
47
|
-
size={16}
|
|
48
|
-
/>
|
|
49
|
-
) : null}
|
|
50
|
-
</span>
|
|
51
|
-
);
|
|
52
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2023 shadcn
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|