@tangle-network/ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/active-sessions-store-CeOmXgv5.d.ts +85 -0
- package/dist/artifact-pane-DvJyPWV4.d.ts +24 -0
- package/dist/auth.d.ts +74 -0
- package/dist/auth.js +15 -0
- package/dist/button-CMQuQEW_.d.ts +17 -0
- package/dist/chat.d.ts +232 -0
- package/dist/chat.js +30 -0
- package/dist/chunk-2NFQRQOD.js +1009 -0
- package/dist/chunk-2VH6PUXD.js +186 -0
- package/dist/chunk-34A66VBG.js +214 -0
- package/dist/chunk-3OI2QKFD.js +0 -0
- package/dist/chunk-4CLN43XT.js +45 -0
- package/dist/chunk-54SQQMMM.js +156 -0
- package/dist/chunk-5Z5ZYMOJ.js +0 -0
- package/dist/chunk-66BNMOVT.js +167 -0
- package/dist/chunk-6BGQA4BQ.js +0 -0
- package/dist/chunk-7UO2ZMRQ.js +133 -0
- package/dist/chunk-BX6AQMUS.js +183 -0
- package/dist/chunk-CD53GZOM.js +59 -0
- package/dist/chunk-CSAIKY36.js +54 -0
- package/dist/chunk-EEE55AVS.js +1201 -0
- package/dist/chunk-GYPQXTJU.js +230 -0
- package/dist/chunk-HFL6R6IF.js +37 -0
- package/dist/chunk-HJKCSXCH.js +737 -0
- package/dist/chunk-LISXUB4D.js +1222 -0
- package/dist/chunk-LQS34IGP.js +0 -0
- package/dist/chunk-MKTSMWVD.js +109 -0
- package/dist/chunk-NKDZ7GZE.js +192 -0
- package/dist/chunk-OEX7NZE3.js +321 -0
- package/dist/chunk-Q56BYXQF.js +61 -0
- package/dist/chunk-Q7EIIWTC.js +0 -0
- package/dist/chunk-REJESC5U.js +117 -0
- package/dist/chunk-RQGKSCEZ.js +0 -0
- package/dist/chunk-RQHJBTEU.js +10 -0
- package/dist/chunk-TMFOPHHN.js +299 -0
- package/dist/chunk-XGKULLYE.js +40 -0
- package/dist/chunk-XIHMJ7ZQ.js +614 -0
- package/dist/chunk-YJ2G3XO5.js +1048 -0
- package/dist/chunk-YNN4O57I.js +754 -0
- package/dist/code-block-DjXf8eOG.d.ts +19 -0
- package/dist/document-editor-pane-A5LT5H4N.js +12 -0
- package/dist/document-editor-pane-DyDEX_Zm.d.ts +124 -0
- package/dist/editor.d.ts +120 -0
- package/dist/editor.js +34 -0
- package/dist/files.d.ts +175 -0
- package/dist/files.js +20 -0
- package/dist/hooks.d.ts +56 -0
- package/dist/hooks.js +41 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +446 -0
- package/dist/markdown.d.ts +15 -0
- package/dist/markdown.js +14 -0
- package/dist/message-BHWbxBtT.d.ts +15 -0
- package/dist/openui.d.ts +115 -0
- package/dist/openui.js +12 -0
- package/dist/parts-dj7AcUg0.d.ts +36 -0
- package/dist/primitives.d.ts +332 -0
- package/dist/primitives.js +191 -0
- package/dist/run-PfLmDAox.d.ts +41 -0
- package/dist/run.d.ts +69 -0
- package/dist/run.js +36 -0
- package/dist/sdk-hooks.d.ts +285 -0
- package/dist/sdk-hooks.js +31 -0
- package/dist/stores.d.ts +17 -0
- package/dist/stores.js +76 -0
- package/dist/tool-call-feed-Bs3MyQMT.d.ts +68 -0
- package/dist/tool-display-z4JcDmMQ.d.ts +32 -0
- package/dist/tool-previews.d.ts +48 -0
- package/dist/tool-previews.js +21 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +45 -0
- package/dist/utils.js +32 -0
- package/package.json +193 -0
- package/src/auth/auth.tsx +228 -0
- package/src/auth/index.ts +13 -0
- package/src/auth/login-layout.tsx +46 -0
- package/src/chat/agent-timeline.stories.tsx +429 -0
- package/src/chat/agent-timeline.tsx +360 -0
- package/src/chat/chat-container.tsx +486 -0
- package/src/chat/chat-input.stories.tsx +142 -0
- package/src/chat/chat-input.tsx +389 -0
- package/src/chat/chat-message.stories.tsx +237 -0
- package/src/chat/chat-message.tsx +129 -0
- package/src/chat/index.ts +18 -0
- package/src/chat/message-list.stories.tsx +336 -0
- package/src/chat/message-list.tsx +79 -0
- package/src/chat/thinking-indicator.stories.tsx +56 -0
- package/src/chat/thinking-indicator.tsx +30 -0
- package/src/chat/user-message.stories.tsx +92 -0
- package/src/chat/user-message.tsx +43 -0
- package/src/editor/document-editor-pane.tsx +351 -0
- package/src/editor/editor-provider.tsx +428 -0
- package/src/editor/editor-toolbar.tsx +130 -0
- package/src/editor/index.ts +31 -0
- package/src/editor/markdown-conversion.ts +21 -0
- package/src/editor/markdown-document-editor.tsx +137 -0
- package/src/editor/tiptap-editor.tsx +331 -0
- package/src/editor/use-editor.ts +221 -0
- package/src/files/file-artifact-pane.tsx +183 -0
- package/src/files/file-preview.tsx +342 -0
- package/src/files/file-tabs.tsx +71 -0
- package/src/files/file-tree.tsx +258 -0
- package/src/files/index.ts +17 -0
- package/src/files/rich-file-tree.stories.tsx +104 -0
- package/src/files/rich-file-tree.test.tsx +42 -0
- package/src/files/rich-file-tree.tsx +232 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/use-auth.ts +153 -0
- package/src/hooks/use-auto-scroll.ts +59 -0
- package/src/hooks/use-dropdown-menu.ts +40 -0
- package/src/hooks/use-live-time.test.tsx +40 -0
- package/src/hooks/use-live-time.ts +27 -0
- package/src/hooks/use-realtime-session.ts +319 -0
- package/src/hooks/use-run-collapse-state.ts +25 -0
- package/src/hooks/use-run-groups.ts +111 -0
- package/src/hooks/use-sdk-session.ts +575 -0
- package/src/hooks/use-sse-stream.ts +475 -0
- package/src/hooks/use-tool-call-stream.ts +96 -0
- package/src/index.ts +14 -0
- package/src/lib/utils.ts +6 -0
- package/src/markdown/code-block.tsx +198 -0
- package/src/markdown/index.ts +2 -0
- package/src/markdown/markdown.stories.tsx +190 -0
- package/src/markdown/markdown.tsx +62 -0
- package/src/openui/index.ts +20 -0
- package/src/openui/openui-artifact-renderer.tsx +542 -0
- package/src/primitives/artifact-pane.tsx +91 -0
- package/src/primitives/avatar.stories.tsx +95 -0
- package/src/primitives/avatar.tsx +47 -0
- package/src/primitives/badge.stories.tsx +57 -0
- package/src/primitives/badge.tsx +97 -0
- package/src/primitives/button.stories.tsx +48 -0
- package/src/primitives/button.tsx +115 -0
- package/src/primitives/card.stories.tsx +53 -0
- package/src/primitives/card.tsx +98 -0
- package/src/primitives/code-block.stories.tsx +115 -0
- package/src/primitives/code-block.tsx +22 -0
- package/src/primitives/design-tokens.stories.tsx +162 -0
- package/src/primitives/dialog.stories.tsx +176 -0
- package/src/primitives/dialog.tsx +137 -0
- package/src/primitives/drop-zone.stories.tsx +123 -0
- package/src/primitives/drop-zone.tsx +131 -0
- package/src/primitives/dropdown-menu.stories.tsx +122 -0
- package/src/primitives/dropdown-menu.tsx +214 -0
- package/src/primitives/empty-state.stories.tsx +81 -0
- package/src/primitives/empty-state.tsx +40 -0
- package/src/primitives/index.ts +118 -0
- package/src/primitives/input.stories.tsx +113 -0
- package/src/primitives/input.tsx +136 -0
- package/src/primitives/label.stories.tsx +84 -0
- package/src/primitives/label.tsx +24 -0
- package/src/primitives/progress.stories.tsx +93 -0
- package/src/primitives/progress.tsx +50 -0
- package/src/primitives/segmented-control.test.tsx +328 -0
- package/src/primitives/segmented-control.tsx +154 -0
- package/src/primitives/select.stories.tsx +164 -0
- package/src/primitives/select.tsx +158 -0
- package/src/primitives/sidebar-drop-zone.stories.tsx +100 -0
- package/src/primitives/sidebar-drop-zone.tsx +149 -0
- package/src/primitives/skeleton.stories.tsx +79 -0
- package/src/primitives/skeleton.tsx +55 -0
- package/src/primitives/stat-card.stories.tsx +137 -0
- package/src/primitives/stat-card.tsx +97 -0
- package/src/primitives/switch.stories.tsx +85 -0
- package/src/primitives/switch.tsx +28 -0
- package/src/primitives/table.stories.tsx +170 -0
- package/src/primitives/table.tsx +116 -0
- package/src/primitives/tabs.stories.tsx +180 -0
- package/src/primitives/tabs.tsx +71 -0
- package/src/primitives/terminal-display.stories.tsx +191 -0
- package/src/primitives/terminal-display.tsx +189 -0
- package/src/primitives/theme-toggle.stories.tsx +32 -0
- package/src/primitives/theme-toggle.tsx +96 -0
- package/src/primitives/toast.stories.tsx +155 -0
- package/src/primitives/toast.tsx +190 -0
- package/src/primitives/upload-progress.stories.tsx +120 -0
- package/src/primitives/upload-progress.tsx +110 -0
- package/src/run/expanded-tool-detail.stories.tsx +182 -0
- package/src/run/expanded-tool-detail.tsx +186 -0
- package/src/run/index.ts +13 -0
- package/src/run/inline-thinking-item.stories.tsx +136 -0
- package/src/run/inline-thinking-item.tsx +120 -0
- package/src/run/inline-tool-item.stories.tsx +222 -0
- package/src/run/inline-tool-item.tsx +190 -0
- package/src/run/run-group.stories.tsx +322 -0
- package/src/run/run-group.tsx +569 -0
- package/src/run/run-item-primitives.tsx +17 -0
- package/src/run/tool-call-feed.stories.tsx +294 -0
- package/src/run/tool-call-feed.tsx +192 -0
- package/src/run/tool-call-step.stories.tsx +198 -0
- package/src/run/tool-call-step.tsx +240 -0
- package/src/sdk-hooks.ts +38 -0
- package/src/stores/active-sessions-store.ts +455 -0
- package/src/stores/chat-store.ts +43 -0
- package/src/stores/index.ts +2 -0
- package/src/tool-previews/command-preview.tsx +116 -0
- package/src/tool-previews/diff-preview.tsx +85 -0
- package/src/tool-previews/glob-results-preview.tsx +98 -0
- package/src/tool-previews/grep-results-preview.tsx +157 -0
- package/src/tool-previews/index.ts +22 -0
- package/src/tool-previews/preview-primitives.tsx +84 -0
- package/src/tool-previews/question-preview.tsx +101 -0
- package/src/tool-previews/web-search-preview.tsx +117 -0
- package/src/tool-previews/write-file-preview.tsx +80 -0
- package/src/types/branding.ts +11 -0
- package/src/types/index.ts +5 -0
- package/src/types/message.ts +13 -0
- package/src/types/parts.ts +51 -0
- package/src/types/run.ts +56 -0
- package/src/types/tool-display.ts +41 -0
- package/src/utils/copy-text.ts +30 -0
- package/src/utils/format.test.ts +43 -0
- package/src/utils/format.ts +56 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/time-ago.ts +9 -0
- package/src/utils/tool-display.ts +238 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as SelectPrimitive from "@radix-ui/react-select";
|
|
4
|
+
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
|
|
8
|
+
const Select = SelectPrimitive.Root;
|
|
9
|
+
|
|
10
|
+
const SelectGroup = SelectPrimitive.Group;
|
|
11
|
+
|
|
12
|
+
const SelectValue = SelectPrimitive.Value;
|
|
13
|
+
|
|
14
|
+
const SelectTrigger = React.forwardRef<
|
|
15
|
+
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
16
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
|
17
|
+
>(({ className, children, ...props }, ref) => (
|
|
18
|
+
<SelectPrimitive.Trigger
|
|
19
|
+
ref={ref}
|
|
20
|
+
className={cn(
|
|
21
|
+
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-border bg-card px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
|
22
|
+
className,
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
{children}
|
|
27
|
+
<SelectPrimitive.Icon asChild>
|
|
28
|
+
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
29
|
+
</SelectPrimitive.Icon>
|
|
30
|
+
</SelectPrimitive.Trigger>
|
|
31
|
+
));
|
|
32
|
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
|
33
|
+
|
|
34
|
+
const SelectScrollUpButton = React.forwardRef<
|
|
35
|
+
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
|
36
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
|
37
|
+
>(({ className, ...props }, ref) => (
|
|
38
|
+
<SelectPrimitive.ScrollUpButton
|
|
39
|
+
ref={ref}
|
|
40
|
+
className={cn(
|
|
41
|
+
"flex cursor-default items-center justify-center py-1",
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
<ChevronUp className="h-4 w-4" />
|
|
47
|
+
</SelectPrimitive.ScrollUpButton>
|
|
48
|
+
));
|
|
49
|
+
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
|
50
|
+
|
|
51
|
+
const SelectScrollDownButton = React.forwardRef<
|
|
52
|
+
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
|
53
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
|
54
|
+
>(({ className, ...props }, ref) => (
|
|
55
|
+
<SelectPrimitive.ScrollDownButton
|
|
56
|
+
ref={ref}
|
|
57
|
+
className={cn(
|
|
58
|
+
"flex cursor-default items-center justify-center py-1",
|
|
59
|
+
className,
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
>
|
|
63
|
+
<ChevronDown className="h-4 w-4" />
|
|
64
|
+
</SelectPrimitive.ScrollDownButton>
|
|
65
|
+
));
|
|
66
|
+
SelectScrollDownButton.displayName =
|
|
67
|
+
SelectPrimitive.ScrollDownButton.displayName;
|
|
68
|
+
|
|
69
|
+
const SelectContent = React.forwardRef<
|
|
70
|
+
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
71
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
72
|
+
>(({ className, children, position = "popper", ...props }, ref) => (
|
|
73
|
+
<SelectPrimitive.Portal>
|
|
74
|
+
<SelectPrimitive.Content
|
|
75
|
+
ref={ref}
|
|
76
|
+
className={cn(
|
|
77
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-border bg-card text-foreground shadow-[var(--shadow-card)] data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
78
|
+
position === "popper" &&
|
|
79
|
+
"data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=bottom]:translate-y-1 data-[side=top]:-translate-y-1",
|
|
80
|
+
className,
|
|
81
|
+
)}
|
|
82
|
+
position={position}
|
|
83
|
+
{...props}
|
|
84
|
+
>
|
|
85
|
+
<SelectScrollUpButton />
|
|
86
|
+
<SelectPrimitive.Viewport
|
|
87
|
+
className={cn(
|
|
88
|
+
"p-1",
|
|
89
|
+
position === "popper" &&
|
|
90
|
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
93
|
+
{children}
|
|
94
|
+
</SelectPrimitive.Viewport>
|
|
95
|
+
<SelectScrollDownButton />
|
|
96
|
+
</SelectPrimitive.Content>
|
|
97
|
+
</SelectPrimitive.Portal>
|
|
98
|
+
));
|
|
99
|
+
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
|
100
|
+
|
|
101
|
+
const SelectLabel = React.forwardRef<
|
|
102
|
+
React.ElementRef<typeof SelectPrimitive.Label>,
|
|
103
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
|
104
|
+
>(({ className, ...props }, ref) => (
|
|
105
|
+
<SelectPrimitive.Label
|
|
106
|
+
ref={ref}
|
|
107
|
+
className={cn("px-2 py-1.5 font-semibold text-sm", className)}
|
|
108
|
+
{...props}
|
|
109
|
+
/>
|
|
110
|
+
));
|
|
111
|
+
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
|
112
|
+
|
|
113
|
+
const SelectItem = React.forwardRef<
|
|
114
|
+
React.ElementRef<typeof SelectPrimitive.Item>,
|
|
115
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
|
116
|
+
>(({ className, children, ...props }, ref) => (
|
|
117
|
+
<SelectPrimitive.Item
|
|
118
|
+
ref={ref}
|
|
119
|
+
className={cn(
|
|
120
|
+
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pr-8 pl-2 text-sm outline-none focus:bg-muted/50 focus:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
121
|
+
className,
|
|
122
|
+
)}
|
|
123
|
+
{...props}
|
|
124
|
+
>
|
|
125
|
+
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
126
|
+
<SelectPrimitive.ItemIndicator>
|
|
127
|
+
<Check className="h-4 w-4" />
|
|
128
|
+
</SelectPrimitive.ItemIndicator>
|
|
129
|
+
</span>
|
|
130
|
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
131
|
+
</SelectPrimitive.Item>
|
|
132
|
+
));
|
|
133
|
+
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
|
134
|
+
|
|
135
|
+
const SelectSeparator = React.forwardRef<
|
|
136
|
+
React.ElementRef<typeof SelectPrimitive.Separator>,
|
|
137
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
|
138
|
+
>(({ className, ...props }, ref) => (
|
|
139
|
+
<SelectPrimitive.Separator
|
|
140
|
+
ref={ref}
|
|
141
|
+
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
142
|
+
{...props}
|
|
143
|
+
/>
|
|
144
|
+
));
|
|
145
|
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
|
146
|
+
|
|
147
|
+
export {
|
|
148
|
+
Select,
|
|
149
|
+
SelectContent,
|
|
150
|
+
SelectGroup,
|
|
151
|
+
SelectItem,
|
|
152
|
+
SelectLabel,
|
|
153
|
+
SelectScrollDownButton,
|
|
154
|
+
SelectScrollUpButton,
|
|
155
|
+
SelectSeparator,
|
|
156
|
+
SelectTrigger,
|
|
157
|
+
SelectValue,
|
|
158
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
import { SidebarDropZone } from './sidebar-drop-zone'
|
|
3
|
+
import { FolderOpen } from 'lucide-react'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof SidebarDropZone> = {
|
|
6
|
+
title: 'Primitives/SidebarDropZone',
|
|
7
|
+
component: SidebarDropZone,
|
|
8
|
+
parameters: { layout: 'centered', backgrounds: { default: 'dark' } },
|
|
9
|
+
args: {
|
|
10
|
+
onDrop: (files) => console.log('dropped', files),
|
|
11
|
+
},
|
|
12
|
+
decorators: [
|
|
13
|
+
(Story) => (
|
|
14
|
+
<div className="w-64 p-4 rounded-xl border border-border bg-background space-y-3">
|
|
15
|
+
<div className="text-xs text-muted-foreground font-mono uppercase tracking-widest">Workspace Files</div>
|
|
16
|
+
<div className="space-y-1 text-sm text-foreground">
|
|
17
|
+
<div className="px-2 py-1 rounded hover:bg-accent">index.ts</div>
|
|
18
|
+
<div className="px-2 py-1 rounded hover:bg-accent">utils.ts</div>
|
|
19
|
+
<div className="px-2 py-1 rounded hover:bg-accent">config.json</div>
|
|
20
|
+
</div>
|
|
21
|
+
<Story />
|
|
22
|
+
</div>
|
|
23
|
+
),
|
|
24
|
+
],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default meta
|
|
28
|
+
type Story = StoryObj<typeof SidebarDropZone>
|
|
29
|
+
|
|
30
|
+
export const Persistent: Story = {
|
|
31
|
+
name: 'Persistent (always visible)',
|
|
32
|
+
args: {
|
|
33
|
+
persistent: true,
|
|
34
|
+
title: 'Drop files here',
|
|
35
|
+
description: 'Uploads to workspace',
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const PersistentNoDescription: Story = {
|
|
40
|
+
name: 'Persistent — title only',
|
|
41
|
+
args: {
|
|
42
|
+
persistent: true,
|
|
43
|
+
title: 'Upload to workspace',
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Static drag-over appearance — mirrors what the component renders when dragOver=true */
|
|
48
|
+
export const DragOverPreview: Story = {
|
|
49
|
+
name: 'Drag-over state (static preview)',
|
|
50
|
+
render: () => (
|
|
51
|
+
<div
|
|
52
|
+
className="rounded-lg border-2 border-dashed p-4 transition-all duration-150"
|
|
53
|
+
style={{
|
|
54
|
+
borderColor: 'hsl(var(--ring, 217 91% 60%))',
|
|
55
|
+
backgroundColor: 'hsl(var(--primary, 217 91% 60%) / 0.08)',
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
<div className="flex flex-col items-center gap-2 text-center">
|
|
59
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-500/15 text-blue-400">
|
|
60
|
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
|
61
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
62
|
+
<polyline points="17 8 12 3 7 8" />
|
|
63
|
+
<line x1="12" y1="3" x2="12" y2="15" />
|
|
64
|
+
</svg>
|
|
65
|
+
</div>
|
|
66
|
+
<p className="text-xs font-medium text-white">Drop files here</p>
|
|
67
|
+
<p className="text-[10px] text-zinc-400">Uploads to workspace</p>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const CustomIcon: Story = {
|
|
74
|
+
name: 'Custom Icon',
|
|
75
|
+
args: {
|
|
76
|
+
persistent: true,
|
|
77
|
+
title: 'Add folder',
|
|
78
|
+
description: 'Drop a folder to sync',
|
|
79
|
+
icon: <FolderOpen className="h-4 w-4" />,
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const WithAcceptFilter: Story = {
|
|
84
|
+
name: 'Accept filter (.pdf, .csv)',
|
|
85
|
+
args: {
|
|
86
|
+
persistent: true,
|
|
87
|
+
accept: '.pdf,.csv',
|
|
88
|
+
title: 'Drop CSV or PDF',
|
|
89
|
+
description: 'Only structured data files',
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const Disabled: Story = {
|
|
94
|
+
name: 'Disabled',
|
|
95
|
+
args: {
|
|
96
|
+
persistent: true,
|
|
97
|
+
disabled: true,
|
|
98
|
+
title: 'Uploads disabled',
|
|
99
|
+
},
|
|
100
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SidebarDropZone — compact scoped drop zone for sidebar panels and narrow containers.
|
|
3
|
+
*
|
|
4
|
+
* Unlike the full-window DropZone, this renders as an inline element
|
|
5
|
+
* that can be placed inside sidebar panels, file trees, or any container.
|
|
6
|
+
* Shows a subtle drop target when files are dragged over it.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* <SidebarPanelContent>
|
|
10
|
+
* <FileTree ... />
|
|
11
|
+
* <SidebarDropZone onDrop={uploadToWorkspace} title="Upload to workspace" />
|
|
12
|
+
* </SidebarPanelContent>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useCallback, useRef, useState, type DragEvent, type ReactNode } from "react";
|
|
16
|
+
import { Upload } from "lucide-react";
|
|
17
|
+
import { cn } from "../lib/utils";
|
|
18
|
+
|
|
19
|
+
export interface SidebarDropZoneProps {
|
|
20
|
+
/** Called with dropped files */
|
|
21
|
+
onDrop: (files: File[]) => void;
|
|
22
|
+
/** Accepted file types (e.g. ".pdf,.csv") */
|
|
23
|
+
accept?: string;
|
|
24
|
+
/** Whether drop zone is active */
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
/** Title shown in the drop zone */
|
|
27
|
+
title?: string;
|
|
28
|
+
/** Description shown below the title */
|
|
29
|
+
description?: string;
|
|
30
|
+
/** Custom icon (replaces default upload icon) */
|
|
31
|
+
icon?: ReactNode;
|
|
32
|
+
/** Always visible (not just on drag). Useful as a persistent upload area. */
|
|
33
|
+
persistent?: boolean;
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function SidebarDropZone({
|
|
38
|
+
onDrop,
|
|
39
|
+
accept,
|
|
40
|
+
disabled,
|
|
41
|
+
title = "Drop files here",
|
|
42
|
+
description,
|
|
43
|
+
icon,
|
|
44
|
+
persistent = false,
|
|
45
|
+
className,
|
|
46
|
+
}: SidebarDropZoneProps) {
|
|
47
|
+
const [dragOver, setDragOver] = useState(false);
|
|
48
|
+
const counter = useRef(0);
|
|
49
|
+
|
|
50
|
+
const isAccepted = useCallback(
|
|
51
|
+
(file: File) => {
|
|
52
|
+
if (!accept) return true;
|
|
53
|
+
const extensions = accept.split(",").map((ext) => ext.trim().toLowerCase());
|
|
54
|
+
const fileName = file.name.toLowerCase();
|
|
55
|
+
return extensions.some((ext) => fileName.endsWith(ext));
|
|
56
|
+
},
|
|
57
|
+
[accept],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const handleDragEnter = useCallback(
|
|
61
|
+
(e: DragEvent) => {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
e.stopPropagation();
|
|
64
|
+
if (disabled) return;
|
|
65
|
+
counter.current++;
|
|
66
|
+
if (e.dataTransfer?.types.includes("Files")) setDragOver(true);
|
|
67
|
+
},
|
|
68
|
+
[disabled],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const handleDragLeave = useCallback((e: DragEvent) => {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
e.stopPropagation();
|
|
74
|
+
counter.current--;
|
|
75
|
+
if (counter.current === 0) setDragOver(false);
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const handleDragOver = useCallback(
|
|
79
|
+
(e: DragEvent) => {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
e.stopPropagation();
|
|
82
|
+
if (!disabled) e.dataTransfer.dropEffect = "copy";
|
|
83
|
+
},
|
|
84
|
+
[disabled],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const handleDrop = useCallback(
|
|
88
|
+
(e: DragEvent) => {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
e.stopPropagation();
|
|
91
|
+
counter.current = 0;
|
|
92
|
+
setDragOver(false);
|
|
93
|
+
if (disabled) return;
|
|
94
|
+
|
|
95
|
+
const allFiles = Array.from(e.dataTransfer?.files || []);
|
|
96
|
+
const accepted = accept ? allFiles.filter(isAccepted) : allFiles;
|
|
97
|
+
if (accepted.length > 0) onDrop(accepted);
|
|
98
|
+
},
|
|
99
|
+
[disabled, accept, isAccepted, onDrop],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const isVisible = persistent || dragOver;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div
|
|
106
|
+
onDragEnter={handleDragEnter}
|
|
107
|
+
onDragLeave={handleDragLeave}
|
|
108
|
+
onDragOver={handleDragOver}
|
|
109
|
+
onDrop={handleDrop}
|
|
110
|
+
className={cn(
|
|
111
|
+
"rounded-lg border-2 border-dashed transition-all duration-150",
|
|
112
|
+
isVisible ? "p-4" : "p-0 border-transparent",
|
|
113
|
+
dragOver
|
|
114
|
+
? "border-[var(--brand-cool,hsl(var(--ring)))] bg-[var(--accent-surface-soft)]"
|
|
115
|
+
: persistent
|
|
116
|
+
? "border-[var(--border-subtle,hsl(var(--border)))] bg-transparent hover:border-[var(--border-default,hsl(var(--border)))] hover:bg-[var(--bg-hover,hsl(var(--accent)))]"
|
|
117
|
+
: "",
|
|
118
|
+
disabled && "opacity-50 pointer-events-none",
|
|
119
|
+
className,
|
|
120
|
+
)}
|
|
121
|
+
>
|
|
122
|
+
{isVisible && (
|
|
123
|
+
<div className="flex flex-col items-center gap-2 text-center">
|
|
124
|
+
<div className={cn(
|
|
125
|
+
"flex h-8 w-8 items-center justify-center rounded-lg transition-colors",
|
|
126
|
+
dragOver
|
|
127
|
+
? "bg-[var(--brand-cool,hsl(var(--primary)))]/15 text-[var(--brand-cool,hsl(var(--primary)))]"
|
|
128
|
+
: "text-[var(--text-muted,hsl(var(--muted-foreground)))]",
|
|
129
|
+
)}>
|
|
130
|
+
{icon ?? <Upload className="h-4 w-4" />}
|
|
131
|
+
</div>
|
|
132
|
+
<p className={cn(
|
|
133
|
+
"text-xs font-medium",
|
|
134
|
+
dragOver
|
|
135
|
+
? "text-[var(--text-primary,hsl(var(--foreground)))]"
|
|
136
|
+
: "text-[var(--text-muted,hsl(var(--muted-foreground)))]",
|
|
137
|
+
)}>
|
|
138
|
+
{title}
|
|
139
|
+
</p>
|
|
140
|
+
{description && (
|
|
141
|
+
<p className="text-[10px] text-[var(--text-muted,hsl(var(--muted-foreground)))]">
|
|
142
|
+
{description}
|
|
143
|
+
</p>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
import { Skeleton, SkeletonCard, SkeletonTable } from './skeleton'
|
|
3
|
+
|
|
4
|
+
const meta: Meta = {
|
|
5
|
+
title: 'Primitives/Skeleton',
|
|
6
|
+
parameters: { layout: 'centered', backgrounds: { default: 'dark' } },
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default meta
|
|
10
|
+
type Story = StoryObj
|
|
11
|
+
|
|
12
|
+
export const Base: Story = {
|
|
13
|
+
render: () => <Skeleton className="h-4 w-48" />,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const Card: Story = {
|
|
17
|
+
render: () => <SkeletonCard className="w-72" />,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const Table: Story = {
|
|
21
|
+
render: () => (
|
|
22
|
+
<div className="w-[520px] rounded-xl border border-border bg-card p-4">
|
|
23
|
+
<SkeletonTable rows={4} />
|
|
24
|
+
</div>
|
|
25
|
+
),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const AvatarRow: Story = {
|
|
29
|
+
name: 'Avatar + text row',
|
|
30
|
+
render: () => (
|
|
31
|
+
<div className="flex items-center gap-3 w-72">
|
|
32
|
+
<Skeleton className="h-10 w-10 rounded-full shrink-0" />
|
|
33
|
+
<div className="flex flex-col gap-2 flex-1">
|
|
34
|
+
<Skeleton className="h-4 w-3/4" />
|
|
35
|
+
<Skeleton className="h-3 w-1/2" />
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const Overview: Story = {
|
|
42
|
+
name: 'Overview',
|
|
43
|
+
render: () => (
|
|
44
|
+
<div className="flex flex-col gap-8 p-6 w-[560px]">
|
|
45
|
+
<div className="text-muted-foreground text-xs font-mono uppercase tracking-widest">Inline elements</div>
|
|
46
|
+
<div className="flex flex-col gap-2">
|
|
47
|
+
<Skeleton className="h-5 w-3/4" />
|
|
48
|
+
<Skeleton className="h-4 w-full" />
|
|
49
|
+
<Skeleton className="h-4 w-5/6" />
|
|
50
|
+
<Skeleton className="h-4 w-2/3" />
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div className="text-muted-foreground text-xs font-mono uppercase tracking-widest">Team list</div>
|
|
54
|
+
<div className="flex flex-col gap-3">
|
|
55
|
+
{[1, 2, 3].map((i) => (
|
|
56
|
+
<div key={i} className="flex items-center gap-3">
|
|
57
|
+
<Skeleton className="h-9 w-9 rounded-full shrink-0" />
|
|
58
|
+
<div className="flex flex-col gap-1.5 flex-1">
|
|
59
|
+
<Skeleton className="h-4 w-40" />
|
|
60
|
+
<Skeleton className="h-3 w-28" />
|
|
61
|
+
</div>
|
|
62
|
+
<Skeleton className="h-6 w-16 rounded-full" />
|
|
63
|
+
</div>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div className="text-muted-foreground text-xs font-mono uppercase tracking-widest">Cards</div>
|
|
68
|
+
<div className="flex gap-4">
|
|
69
|
+
<SkeletonCard className="flex-1" />
|
|
70
|
+
<SkeletonCard className="flex-1" />
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div className="text-muted-foreground text-xs font-mono uppercase tracking-widest">Table</div>
|
|
74
|
+
<div className="rounded-xl border border-border bg-card p-4">
|
|
75
|
+
<SkeletonTable rows={3} />
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
),
|
|
79
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { cn } from "../lib/utils";
|
|
2
|
+
|
|
3
|
+
function Skeleton({
|
|
4
|
+
className,
|
|
5
|
+
...props
|
|
6
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
7
|
+
return (
|
|
8
|
+
<div
|
|
9
|
+
className={cn("animate-pulse rounded-lg bg-muted/50", className)}
|
|
10
|
+
{...props}
|
|
11
|
+
/>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function SkeletonCard({ className }: { className?: string }) {
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
className={cn(
|
|
19
|
+
"space-y-4 rounded-xl border border-border bg-card p-6",
|
|
20
|
+
className,
|
|
21
|
+
)}
|
|
22
|
+
>
|
|
23
|
+
<Skeleton className="h-4 w-3/4" />
|
|
24
|
+
<Skeleton className="h-4 w-1/2" />
|
|
25
|
+
<div className="space-y-2 pt-4">
|
|
26
|
+
<Skeleton className="h-3 w-full" />
|
|
27
|
+
<Skeleton className="h-3 w-5/6" />
|
|
28
|
+
<Skeleton className="h-3 w-4/6" />
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function SkeletonTable({ rows = 5 }: { rows?: number }) {
|
|
35
|
+
return (
|
|
36
|
+
<div className="space-y-3">
|
|
37
|
+
<div className="flex gap-4 border-border border-b pb-2">
|
|
38
|
+
<Skeleton className="h-4 w-1/4" />
|
|
39
|
+
<Skeleton className="h-4 w-1/4" />
|
|
40
|
+
<Skeleton className="h-4 w-1/4" />
|
|
41
|
+
<Skeleton className="h-4 w-1/4" />
|
|
42
|
+
</div>
|
|
43
|
+
{Array.from({ length: rows }).map((_, i) => (
|
|
44
|
+
<div key={i} className="flex gap-4">
|
|
45
|
+
<Skeleton className="h-4 w-1/4" />
|
|
46
|
+
<Skeleton className="h-4 w-1/4" />
|
|
47
|
+
<Skeleton className="h-4 w-1/4" />
|
|
48
|
+
<Skeleton className="h-4 w-1/4" />
|
|
49
|
+
</div>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { Skeleton, SkeletonCard, SkeletonTable };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
import { Activity, Cpu, DollarSign, HardDrive, Terminal, Users, Zap } from 'lucide-react'
|
|
3
|
+
import { StatCard } from './stat-card'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof StatCard> = {
|
|
6
|
+
title: 'Primitives/StatCard',
|
|
7
|
+
component: StatCard,
|
|
8
|
+
parameters: { layout: 'centered', backgrounds: { default: 'dark' } },
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof StatCard>
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
args: {
|
|
16
|
+
title: 'Active Sessions',
|
|
17
|
+
value: 24,
|
|
18
|
+
subtitle: 'across 3 regions',
|
|
19
|
+
icon: <Terminal className="h-5 w-5" />,
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const SandboxVariant: Story = {
|
|
24
|
+
name: 'Sandbox Variant',
|
|
25
|
+
args: {
|
|
26
|
+
variant: 'sandbox',
|
|
27
|
+
title: 'Active Sessions',
|
|
28
|
+
value: 24,
|
|
29
|
+
subtitle: 'across 3 regions',
|
|
30
|
+
icon: <Terminal className="h-5 w-5" />,
|
|
31
|
+
trend: { value: 12, label: 'vs last hour' },
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const CpuUsage: Story = {
|
|
36
|
+
name: 'CPU Usage',
|
|
37
|
+
args: {
|
|
38
|
+
title: 'Avg CPU Usage',
|
|
39
|
+
value: '68%',
|
|
40
|
+
subtitle: 'p95: 94%',
|
|
41
|
+
icon: <Cpu className="h-5 w-5" />,
|
|
42
|
+
trend: { value: -4, label: 'vs yesterday' },
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const MemoryUsage: Story = {
|
|
47
|
+
name: 'Memory Usage',
|
|
48
|
+
args: {
|
|
49
|
+
title: 'Memory Allocated',
|
|
50
|
+
value: '12.4 GB',
|
|
51
|
+
subtitle: 'of 32 GB pool',
|
|
52
|
+
icon: <HardDrive className="h-5 w-5" />,
|
|
53
|
+
trend: { value: 8, label: 'vs last hour' },
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const MonthlyCost: Story = {
|
|
58
|
+
name: 'Monthly Cost',
|
|
59
|
+
args: {
|
|
60
|
+
title: 'Estimated Cost',
|
|
61
|
+
value: '$142.80',
|
|
62
|
+
subtitle: 'current billing cycle',
|
|
63
|
+
icon: <DollarSign className="h-5 w-5" />,
|
|
64
|
+
trend: { value: 0, label: 'vs last month' },
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const TrendPositive: Story = {
|
|
69
|
+
name: 'Trend — Positive',
|
|
70
|
+
args: {
|
|
71
|
+
title: 'Sessions Today',
|
|
72
|
+
value: 1284,
|
|
73
|
+
icon: <Activity className="h-5 w-5" />,
|
|
74
|
+
trend: { value: 23, label: 'vs yesterday' },
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const TrendNegative: Story = {
|
|
79
|
+
name: 'Trend — Negative',
|
|
80
|
+
args: {
|
|
81
|
+
title: 'Error Rate',
|
|
82
|
+
value: '1.2%',
|
|
83
|
+
icon: <Zap className="h-5 w-5" />,
|
|
84
|
+
trend: { value: -18, label: 'vs last week' },
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const InfraGrid: Story = {
|
|
89
|
+
name: 'Infrastructure Grid',
|
|
90
|
+
render: () => (
|
|
91
|
+
<div className="grid grid-cols-2 gap-4 w-[640px]">
|
|
92
|
+
<StatCard
|
|
93
|
+
title="Active Sessions"
|
|
94
|
+
value={24}
|
|
95
|
+
subtitle="across 3 regions"
|
|
96
|
+
icon={<Terminal className="h-5 w-5" />}
|
|
97
|
+
trend={{ value: 12, label: 'vs last hour' }}
|
|
98
|
+
/>
|
|
99
|
+
<StatCard
|
|
100
|
+
title="CPU Usage"
|
|
101
|
+
value="68%"
|
|
102
|
+
subtitle="p95: 94%"
|
|
103
|
+
icon={<Cpu className="h-5 w-5" />}
|
|
104
|
+
trend={{ value: -4, label: 'vs yesterday' }}
|
|
105
|
+
/>
|
|
106
|
+
<StatCard
|
|
107
|
+
title="Memory Allocated"
|
|
108
|
+
value="12.4 GB"
|
|
109
|
+
subtitle="of 32 GB pool"
|
|
110
|
+
icon={<HardDrive className="h-5 w-5" />}
|
|
111
|
+
trend={{ value: 8, label: 'vs last hour' }}
|
|
112
|
+
/>
|
|
113
|
+
<StatCard
|
|
114
|
+
variant="sandbox"
|
|
115
|
+
title="Estimated Cost"
|
|
116
|
+
value="$142.80"
|
|
117
|
+
subtitle="current billing cycle"
|
|
118
|
+
icon={<DollarSign className="h-5 w-5" />}
|
|
119
|
+
trend={{ value: 3, label: 'vs last month' }}
|
|
120
|
+
/>
|
|
121
|
+
<StatCard
|
|
122
|
+
title="Unique Users"
|
|
123
|
+
value={312}
|
|
124
|
+
subtitle="last 30 days"
|
|
125
|
+
icon={<Users className="h-5 w-5" />}
|
|
126
|
+
trend={{ value: 19, label: 'MoM' }}
|
|
127
|
+
/>
|
|
128
|
+
<StatCard
|
|
129
|
+
title="Avg Startup Time"
|
|
130
|
+
value="840ms"
|
|
131
|
+
subtitle="p99: 1.4s"
|
|
132
|
+
icon={<Zap className="h-5 w-5" />}
|
|
133
|
+
trend={{ value: -11, label: 'vs last week' }}
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
),
|
|
137
|
+
}
|