nextworks 0.2.0-alpha.2 → 0.2.0-alpha.21
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/README.md +280 -228
- package/dist/cli_manifests/blocks_manifest.json +193 -194
- package/dist/commands/blocks.d.ts +3 -0
- package/dist/commands/blocks.d.ts.map +1 -1
- package/dist/commands/blocks.js +128 -19
- package/dist/commands/blocks.js.map +1 -1
- package/dist/commands/doctor.d.ts +136 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +696 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/remove-blocks.d.ts +4 -1
- package/dist/commands/remove-blocks.d.ts.map +1 -1
- package/dist/commands/remove-blocks.js +24 -6
- package/dist/commands/remove-blocks.js.map +1 -1
- package/dist/index.js +125 -4
- package/dist/index.js.map +1 -1
- package/dist/kits/blocks/.nextworks/docs/BLOCKS_QUICKSTART.md +104 -194
- package/dist/kits/blocks/.nextworks/docs/BLOCKS_README.md +115 -75
- package/dist/kits/blocks/.nextworks/docs/THEME_GUIDE.md +269 -251
- package/dist/kits/blocks/app/templates/digitalagency/PresetThemeVars.tsx +80 -80
- package/dist/kits/blocks/app/templates/digitalagency/README.md +42 -36
- package/dist/kits/blocks/app/templates/digitalagency/components/About.tsx +71 -99
- package/dist/kits/blocks/app/templates/digitalagency/components/CTA.tsx +40 -74
- package/dist/kits/blocks/app/templates/digitalagency/components/Contact.tsx +123 -227
- package/dist/kits/blocks/app/templates/digitalagency/components/Footer.tsx +89 -89
- package/dist/kits/blocks/app/templates/digitalagency/components/Hero.tsx +83 -90
- package/dist/kits/blocks/app/templates/digitalagency/components/Navbar.tsx +121 -168
- package/dist/kits/blocks/app/templates/digitalagency/components/NetworkPattern.tsx +288 -297
- package/dist/kits/blocks/app/templates/digitalagency/components/Portfolio.tsx +157 -157
- package/dist/kits/blocks/app/templates/digitalagency/components/Pricing.tsx +114 -114
- package/dist/kits/blocks/app/templates/digitalagency/components/Process.tsx +59 -59
- package/dist/kits/blocks/app/templates/digitalagency/components/Services.tsx +55 -55
- package/dist/kits/blocks/app/templates/digitalagency/components/Team.tsx +28 -28
- package/dist/kits/blocks/app/templates/digitalagency/components/Testimonials.tsx +65 -65
- package/dist/kits/blocks/app/templates/digitalagency/page.tsx +38 -38
- package/dist/kits/blocks/app/templates/gallery/PresetThemeVars.tsx +130 -85
- package/dist/kits/blocks/app/templates/gallery/page.tsx +357 -303
- package/dist/kits/blocks/app/templates/productlaunch/PresetThemeVars.tsx +75 -74
- package/dist/kits/blocks/app/templates/productlaunch/README.md +62 -55
- package/dist/kits/blocks/app/templates/productlaunch/components/About.tsx +84 -178
- package/dist/kits/blocks/app/templates/productlaunch/components/CTA.tsx +50 -93
- package/dist/kits/blocks/app/templates/productlaunch/components/Contact.tsx +231 -231
- package/dist/kits/blocks/app/templates/productlaunch/components/FAQ.tsx +86 -93
- package/dist/kits/blocks/app/templates/productlaunch/components/Features.tsx +83 -84
- package/dist/kits/blocks/app/templates/productlaunch/components/Footer.tsx +132 -132
- package/dist/kits/blocks/app/templates/productlaunch/components/Hero.tsx +88 -89
- package/dist/kits/blocks/app/templates/productlaunch/components/Navbar.tsx +116 -162
- package/dist/kits/blocks/app/templates/productlaunch/components/Pricing.tsx +106 -106
- package/dist/kits/blocks/app/templates/productlaunch/components/ProcessTimeline.tsx +110 -110
- package/dist/kits/blocks/app/templates/productlaunch/components/ServicesGrid.tsx +68 -68
- package/dist/kits/blocks/app/templates/productlaunch/components/Team.tsx +104 -104
- package/dist/kits/blocks/app/templates/productlaunch/components/Testimonials.tsx +90 -89
- package/dist/kits/blocks/app/templates/productlaunch/components/TrustBadges.tsx +76 -76
- package/dist/kits/blocks/app/templates/productlaunch/page.tsx +43 -43
- package/dist/kits/blocks/app/templates/saasdashboard/PresetThemeVars.tsx +80 -80
- package/dist/kits/blocks/app/templates/saasdashboard/README.md +44 -38
- package/dist/kits/blocks/app/templates/saasdashboard/components/Contact.tsx +129 -176
- package/dist/kits/blocks/app/templates/saasdashboard/components/Dashboard.tsx +293 -293
- package/dist/kits/blocks/app/templates/saasdashboard/components/FAQ.tsx +55 -55
- package/dist/kits/blocks/app/templates/saasdashboard/components/Features.tsx +90 -91
- package/dist/kits/blocks/app/templates/saasdashboard/components/Footer.tsx +77 -77
- package/dist/kits/blocks/app/templates/saasdashboard/components/Hero.tsx +104 -105
- package/dist/kits/blocks/app/templates/saasdashboard/components/Hero_mask.tsx +126 -127
- package/dist/kits/blocks/app/templates/saasdashboard/components/Navbar.tsx +117 -159
- package/dist/kits/blocks/app/templates/saasdashboard/components/Pricing.tsx +90 -90
- package/dist/kits/blocks/app/templates/saasdashboard/components/SmoothScroll.tsx +96 -97
- package/dist/kits/blocks/app/templates/saasdashboard/components/Testimonials.tsx +72 -72
- package/dist/kits/blocks/app/templates/saasdashboard/components/TrustBadges.tsx +53 -53
- package/dist/kits/blocks/app/templates/saasdashboard/page.tsx +39 -39
- package/dist/kits/blocks/components/enhanced-theme-provider.tsx +195 -183
- package/dist/kits/blocks/components/providers/BlocksAppProviders.tsx +27 -27
- package/dist/kits/blocks/components/sections/About.tsx +291 -291
- package/dist/kits/blocks/components/sections/CTA.tsx +257 -258
- package/dist/kits/blocks/components/sections/CommandShowcase.tsx +517 -0
- package/dist/kits/blocks/components/sections/Contact.tsx +267 -267
- package/dist/kits/blocks/components/sections/FAQ.tsx +214 -225
- package/dist/kits/blocks/components/sections/FeaturedProjectShowcase.tsx +687 -0
- package/dist/kits/blocks/components/sections/Features.tsx +268 -270
- package/dist/kits/blocks/components/sections/Footer.tsx +302 -302
- package/dist/kits/blocks/components/sections/HeroMotion.tsx +308 -308
- package/dist/kits/blocks/components/sections/HeroOverlay.tsx +358 -358
- package/dist/kits/blocks/components/sections/HeroProductDemo.tsx +246 -0
- package/dist/kits/blocks/components/sections/HeroSplit.tsx +352 -352
- package/dist/kits/blocks/components/sections/HeroWithVideo.tsx +495 -0
- package/dist/kits/blocks/components/sections/Navbar.tsx +352 -353
- package/dist/kits/blocks/components/sections/PortfolioSimple.tsx +549 -550
- package/dist/kits/blocks/components/sections/Pricing.tsx +264 -264
- package/dist/kits/blocks/components/sections/ProcessTimeline.tsx +325 -325
- package/dist/kits/blocks/components/sections/ProjectDeepDive.tsx +805 -0
- package/dist/kits/blocks/components/sections/SelectedWorkRail.tsx +485 -0
- package/dist/kits/blocks/components/sections/ServicesGrid.tsx +210 -210
- package/dist/kits/blocks/components/sections/Team.tsx +309 -309
- package/dist/kits/blocks/components/sections/Testimonials.tsx +158 -158
- package/dist/kits/blocks/components/sections/TrustBadges.tsx +162 -162
- package/dist/kits/blocks/components/sections/product-demo/ApprovalInboxPanel.tsx +128 -0
- package/dist/kits/blocks/components/sections/product-demo/DemoStage.tsx +519 -0
- package/dist/kits/blocks/components/sections/product-demo/DemoWindow.tsx +140 -0
- package/dist/kits/blocks/components/sections/product-demo/KnowledgePanel.tsx +130 -0
- package/dist/kits/blocks/components/sections/product-demo/RunConsolePanel.tsx +430 -0
- package/dist/kits/blocks/components/sections/product-demo/TaskListPanel.tsx +302 -0
- package/dist/kits/blocks/components/sections/product-demo/WorkflowStudioPanel.tsx +744 -0
- package/dist/kits/blocks/components/sections/product-demo/types.ts +262 -0
- package/dist/kits/blocks/components/theme-provider.tsx +1 -34
- package/dist/kits/blocks/components/ui/alert-dialog.tsx +134 -134
- package/dist/kits/blocks/components/ui/brand-node.tsx +121 -121
- package/dist/kits/blocks/components/ui/button.tsx +122 -122
- package/dist/kits/blocks/components/ui/card.tsx +95 -95
- package/dist/kits/blocks/components/ui/checkbox.tsx +30 -30
- package/dist/kits/blocks/components/ui/cta-button.tsx +125 -125
- package/dist/kits/blocks/components/ui/dropdown-menu.tsx +201 -201
- package/dist/kits/blocks/components/ui/feature-card.tsx +91 -91
- package/dist/kits/blocks/components/ui/input.tsx +27 -27
- package/dist/kits/blocks/components/ui/label.tsx +29 -29
- package/dist/kits/blocks/components/ui/pricing-card.tsx +120 -120
- package/dist/kits/blocks/components/ui/select.tsx +25 -25
- package/dist/kits/blocks/components/ui/skeleton.tsx +13 -13
- package/dist/kits/blocks/components/ui/table.tsx +98 -98
- package/dist/kits/blocks/components/ui/testimonial-card.tsx +108 -108
- package/dist/kits/blocks/components/ui/textarea.tsx +26 -26
- package/dist/kits/blocks/components/ui/theme-selector.tsx +243 -247
- package/dist/kits/blocks/components/ui/theme-toggle.tsx +74 -74
- package/dist/kits/blocks/components/ui/toaster.tsx +7 -7
- package/dist/kits/blocks/lib/themes.ts +400 -400
- package/dist/kits/blocks/lib/utils.ts +6 -9
- package/dist/kits/blocks/package-deps.json +40 -28
- package/dist/kits/blocks/tsconfig.json +11 -0
- package/dist/kits/blocks/tsconfig.tsbuildinfo +1 -0
- package/dist/utils/file-operations.d.ts +3 -1
- package/dist/utils/file-operations.d.ts.map +1 -1
- package/dist/utils/file-operations.js +84 -12
- package/dist/utils/file-operations.js.map +1 -1
- package/dist/utils/installation-tracker.d.ts +2 -2
- package/dist/utils/installation-tracker.d.ts.map +1 -1
- package/dist/utils/installation-tracker.js +11 -11
- package/dist/utils/installation-tracker.js.map +1 -1
- package/dist/utils/package-manager.d.ts +6 -0
- package/dist/utils/package-manager.d.ts.map +1 -0
- package/dist/utils/package-manager.js +58 -0
- package/dist/utils/package-manager.js.map +1 -0
- package/dist/utils/yarn-pnp.d.ts +6 -0
- package/dist/utils/yarn-pnp.d.ts.map +1 -0
- package/dist/utils/yarn-pnp.js +39 -0
- package/dist/utils/yarn-pnp.js.map +1 -0
- package/package.json +5 -2
- package/dist/kits/blocks/notes/THEME_GUIDE.md +0 -29
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
import type { ProductDemoStatusTone, ProductDemoWindowMeta } from "./types";
|
|
6
|
+
|
|
7
|
+
export interface DemoWindowProps {
|
|
8
|
+
window: ProductDemoWindowMeta;
|
|
9
|
+
className?: string;
|
|
10
|
+
contentClassName?: string;
|
|
11
|
+
chromeClassName?: string;
|
|
12
|
+
bodyClassName?: string;
|
|
13
|
+
active?: boolean;
|
|
14
|
+
dimmed?: boolean;
|
|
15
|
+
enableMotion?: boolean;
|
|
16
|
+
showControls?: boolean;
|
|
17
|
+
showResizeHandle?: boolean;
|
|
18
|
+
showHeader?: boolean;
|
|
19
|
+
children?: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const STATUS_TONE_CLASSES: Record<ProductDemoStatusTone, string> = {
|
|
23
|
+
neutral:
|
|
24
|
+
"border-[var(--demo-border)] bg-[var(--demo-panel-bg)] text-[var(--demo-muted-fg)]",
|
|
25
|
+
info: "border-[var(--demo-info-border)] bg-[var(--demo-info-soft-bg)] text-[var(--demo-info)]",
|
|
26
|
+
success:
|
|
27
|
+
"border-[var(--demo-success-border)] bg-[var(--demo-success-soft-bg)] text-[var(--demo-success)]",
|
|
28
|
+
warning:
|
|
29
|
+
"border-[var(--demo-warning-border)] bg-[var(--demo-warning-soft-bg)] text-[var(--demo-warning)]",
|
|
30
|
+
danger:
|
|
31
|
+
"border-[var(--demo-danger-border)] bg-[var(--demo-danger-soft-bg)] text-[var(--demo-danger)]",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function DemoWindow({
|
|
35
|
+
window,
|
|
36
|
+
className,
|
|
37
|
+
contentClassName,
|
|
38
|
+
chromeClassName,
|
|
39
|
+
bodyClassName,
|
|
40
|
+
active = false,
|
|
41
|
+
dimmed = false,
|
|
42
|
+
enableMotion = true,
|
|
43
|
+
showControls = true,
|
|
44
|
+
showResizeHandle = true,
|
|
45
|
+
showHeader = true,
|
|
46
|
+
children,
|
|
47
|
+
}: DemoWindowProps) {
|
|
48
|
+
const statusTone = window.status?.tone ?? "neutral";
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<section
|
|
52
|
+
className={cn(
|
|
53
|
+
"group relative flex h-full min-h-[14rem] flex-col overflow-hidden border border-[var(--demo-border)] bg-[var(--demo-shell-bg)] text-[var(--demo-fg)] shadow-none [font-synthesis:none] antialiased",
|
|
54
|
+
|
|
55
|
+
"before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:h-px before:bg-[linear-gradient(90deg,transparent,rgba(255,255,255,0.68),transparent)] before:opacity-70 dark:before:bg-[linear-gradient(90deg,transparent,rgba(255,255,255,0.16),transparent)] dark:before:opacity-100",
|
|
56
|
+
"after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-8 after:bg-[linear-gradient(180deg,transparent,rgba(15,23,42,0.03))] dark:after:bg-[linear-gradient(180deg,transparent,rgba(255,255,255,0.02))]",
|
|
57
|
+
enableMotion &&
|
|
58
|
+
"transition-[opacity,border-color,background-color] duration-500 ease-out motion-reduce:transition-none",
|
|
59
|
+
active && "border-[var(--demo-border-strong)]",
|
|
60
|
+
|
|
61
|
+
dimmed && "opacity-90",
|
|
62
|
+
className,
|
|
63
|
+
)}
|
|
64
|
+
data-product-demo-window={window.key}
|
|
65
|
+
data-active={active ? "true" : "false"}
|
|
66
|
+
aria-label={window.title}
|
|
67
|
+
>
|
|
68
|
+
{showHeader ? (
|
|
69
|
+
<header
|
|
70
|
+
className={cn(
|
|
71
|
+
"relative flex min-h-[3.25rem] items-center justify-between gap-3 border-b border-[var(--demo-border)] bg-[var(--demo-shell-muted-bg)] px-4 py-2.5 [font-synthesis:none] antialiased sm:px-5",
|
|
72
|
+
|
|
73
|
+
chromeClassName,
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
77
|
+
{showControls && (
|
|
78
|
+
<div className="flex items-center gap-1.5 opacity-75 transition-opacity duration-200 group-hover:opacity-100">
|
|
79
|
+
<span className="h-2.5 w-2.5 rounded-full bg-rose-400/90" />
|
|
80
|
+
<span className="h-2.5 w-2.5 rounded-full bg-amber-400/90" />
|
|
81
|
+
<span className="h-2.5 w-2.5 rounded-full bg-emerald-400/90" />
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
<div className="min-w-0 flex items-center gap-2 overflow-hidden">
|
|
86
|
+
<div className="flex min-w-0 items-center gap-2 whitespace-nowrap">
|
|
87
|
+
<h3 className="shrink-0 text-sm font-semibold tracking-[-0.02em] text-[var(--demo-fg)] sm:text-[0.95rem]">
|
|
88
|
+
{window.title}
|
|
89
|
+
</h3>
|
|
90
|
+
{window.badge && (
|
|
91
|
+
<span className="rounded-full border border-[var(--demo-border)] bg-[var(--demo-panel-bg)] px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.16em] text-[var(--demo-muted-fg)]">
|
|
92
|
+
{window.badge}
|
|
93
|
+
</span>
|
|
94
|
+
)}
|
|
95
|
+
{window.subtitle && (
|
|
96
|
+
<p className="min-w-0 truncate text-[11px] leading-5 tracking-[0.005em] text-[var(--demo-muted-fg)] sm:text-[0.8rem]">
|
|
97
|
+
{window.subtitle}
|
|
98
|
+
</p>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{window.status?.label && (
|
|
105
|
+
<span
|
|
106
|
+
className={cn(
|
|
107
|
+
"shrink-0 rounded-full border px-2.5 py-1 text-[10px] font-medium uppercase tracking-[0.16em]",
|
|
108
|
+
STATUS_TONE_CLASSES[statusTone],
|
|
109
|
+
"hidden sm:inline-flex",
|
|
110
|
+
)}
|
|
111
|
+
>
|
|
112
|
+
{window.status.label}
|
|
113
|
+
</span>
|
|
114
|
+
)}
|
|
115
|
+
</header>
|
|
116
|
+
) : null}
|
|
117
|
+
|
|
118
|
+
<div
|
|
119
|
+
className={cn(
|
|
120
|
+
"relative flex-1 min-h-0 px-4 py-4 sm:px-5 sm:py-5",
|
|
121
|
+
|
|
122
|
+
bodyClassName,
|
|
123
|
+
)}
|
|
124
|
+
>
|
|
125
|
+
<div className={cn("h-full", contentClassName)}>{children}</div>
|
|
126
|
+
|
|
127
|
+
{showResizeHandle && (
|
|
128
|
+
<div
|
|
129
|
+
aria-hidden="true"
|
|
130
|
+
className="pointer-events-none absolute bottom-3 right-3 h-4 w-4 opacity-0 transition-opacity duration-200 group-hover:opacity-70"
|
|
131
|
+
>
|
|
132
|
+
<span className="absolute bottom-0 right-0 h-px w-3 rotate-45 bg-[var(--demo-border-strong)]" />
|
|
133
|
+
<span className="absolute bottom-1 right-0 h-px w-2 rotate-45 bg-[var(--demo-border)]" />
|
|
134
|
+
<span className="absolute bottom-0 right-1 h-px w-2 rotate-45 bg-[var(--demo-border)]" />
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
</section>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
import type {
|
|
4
|
+
ProductDemoKnowledgePanelState,
|
|
5
|
+
ProductDemoStatusTone,
|
|
6
|
+
} from "./types";
|
|
7
|
+
|
|
8
|
+
export interface KnowledgePanelProps {
|
|
9
|
+
state: ProductDemoKnowledgePanelState;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const STATUS_TONE_CLASSES: Record<ProductDemoStatusTone, string> = {
|
|
13
|
+
neutral:
|
|
14
|
+
"border-[var(--demo-border)] bg-[var(--demo-panel-bg)] text-[var(--demo-muted-fg)]",
|
|
15
|
+
info: "border-[var(--demo-info-border)] bg-[var(--demo-info-soft-bg)] text-[var(--demo-info)]",
|
|
16
|
+
success:
|
|
17
|
+
"border-[var(--demo-success-border)] bg-[var(--demo-success-soft-bg)] text-[var(--demo-success)]",
|
|
18
|
+
warning:
|
|
19
|
+
"border-[var(--demo-warning-border)] bg-[var(--demo-warning-soft-bg)] text-[var(--demo-warning)]",
|
|
20
|
+
danger:
|
|
21
|
+
"border-[var(--demo-danger-border)] bg-[var(--demo-danger-soft-bg)] text-[var(--demo-danger)]",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function getStatusClass(tone: ProductDemoStatusTone = "neutral") {
|
|
25
|
+
return STATUS_TONE_CLASSES[tone];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function KnowledgePanel({ state }: KnowledgePanelProps) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="flex h-full flex-col gap-4 text-[var(--demo-fg)]">
|
|
31
|
+
<div className="space-y-1.5">
|
|
32
|
+
{state.title && (
|
|
33
|
+
<h4 className="text-sm font-semibold text-[var(--demo-fg)]">
|
|
34
|
+
{state.title}
|
|
35
|
+
</h4>
|
|
36
|
+
)}
|
|
37
|
+
{state.subtitle && (
|
|
38
|
+
<p className="text-xs leading-relaxed text-[var(--demo-muted-fg)]">
|
|
39
|
+
{state.subtitle}
|
|
40
|
+
</p>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{state.query && (
|
|
45
|
+
<div className="rounded-xl border border-[var(--demo-info-border)] bg-[var(--demo-info-soft-bg)] px-3 py-2 text-xs text-[var(--demo-info)]">
|
|
46
|
+
{state.query}
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
|
|
50
|
+
{state.summary && (
|
|
51
|
+
<p className="text-xs leading-relaxed text-[var(--demo-muted-fg)]">
|
|
52
|
+
{state.summary}
|
|
53
|
+
</p>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
{state.sources?.length ? (
|
|
57
|
+
<div className="flex flex-wrap gap-2">
|
|
58
|
+
{state.sources.map((source) => (
|
|
59
|
+
<span
|
|
60
|
+
key={source.id}
|
|
61
|
+
className={cn(
|
|
62
|
+
"rounded-full border px-2.5 py-1 text-[10px] font-medium uppercase tracking-[0.14em]",
|
|
63
|
+
getStatusClass(source.status),
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
{source.label}
|
|
67
|
+
{source.kind ? ` • ${source.kind}` : ""}
|
|
68
|
+
</span>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
) : null}
|
|
72
|
+
|
|
73
|
+
<div className="space-y-3">
|
|
74
|
+
{state.snippets.map((snippet) => {
|
|
75
|
+
const isActive =
|
|
76
|
+
snippet.id === state.activeSnippetId || snippet.highlighted;
|
|
77
|
+
const source = state.sources?.find(
|
|
78
|
+
(item) => item.id === snippet.sourceId,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
key={snippet.id}
|
|
84
|
+
className={cn(
|
|
85
|
+
"rounded-2xl border border-[var(--demo-border)] bg-[var(--demo-panel-bg)] p-3",
|
|
86
|
+
isActive &&
|
|
87
|
+
"border-[var(--demo-info-border)] bg-[var(--demo-info-soft-bg)] shadow-sm",
|
|
88
|
+
)}
|
|
89
|
+
>
|
|
90
|
+
<div className="flex items-start justify-between gap-3">
|
|
91
|
+
<div>
|
|
92
|
+
<div className="text-sm font-semibold text-[var(--demo-fg)]">
|
|
93
|
+
{snippet.title}
|
|
94
|
+
</div>
|
|
95
|
+
{(snippet.excerptLabel || source?.label) && (
|
|
96
|
+
<div className="mt-1 text-[11px] text-[var(--demo-muted-fg)]">
|
|
97
|
+
{[snippet.excerptLabel, source?.label]
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.join(" • ")}
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
{snippet.confidence && (
|
|
104
|
+
<span className="rounded-full border border-[var(--demo-border)] bg-[var(--demo-panel-muted-bg)] px-2 py-1 text-[10px] font-medium uppercase tracking-[0.14em] text-[var(--demo-muted-fg)]">
|
|
105
|
+
{snippet.confidence}
|
|
106
|
+
</span>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
<p className="mt-2 text-xs leading-relaxed text-[var(--demo-muted-fg)]">
|
|
110
|
+
{snippet.content}
|
|
111
|
+
</p>
|
|
112
|
+
{snippet.tags?.length ? (
|
|
113
|
+
<div className="mt-3 flex flex-wrap gap-2">
|
|
114
|
+
{snippet.tags.map((tag) => (
|
|
115
|
+
<span
|
|
116
|
+
key={tag}
|
|
117
|
+
className="rounded-full border border-[var(--demo-border)] bg-[var(--demo-panel-bg)] px-2 py-0.5 text-[10px] text-[var(--demo-muted-fg)]"
|
|
118
|
+
>
|
|
119
|
+
{tag}
|
|
120
|
+
</span>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
) : null}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
})}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
import type { ProductDemoRunConsoleState } from "./types";
|
|
4
|
+
|
|
5
|
+
export interface RunConsolePanelProps {
|
|
6
|
+
state: ProductDemoRunConsoleState;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function RunConsolePanel({ state }: RunConsolePanelProps) {
|
|
10
|
+
const scrollViewportRef = React.useRef<HTMLDivElement | null>(null);
|
|
11
|
+
const scrollbarTrackRef = React.useRef<HTMLDivElement | null>(null);
|
|
12
|
+
const dragStateRef = React.useRef<{
|
|
13
|
+
pointerId: number;
|
|
14
|
+
startY: number;
|
|
15
|
+
startScrollTop: number;
|
|
16
|
+
scrollRatio: number;
|
|
17
|
+
pointerOffsetY: number;
|
|
18
|
+
} | null>(null);
|
|
19
|
+
const playbackMs = state.playbackMs ?? 1800;
|
|
20
|
+
const playbackStepEntryIndices = state.playbackStepEntryIndices ?? [];
|
|
21
|
+
const playbackStepVisibleLineCounts =
|
|
22
|
+
state.playbackStepVisibleLineCounts ?? [];
|
|
23
|
+
const [activeIndex, setActiveIndex] = React.useState(
|
|
24
|
+
Math.max(
|
|
25
|
+
0,
|
|
26
|
+
state.entries.findIndex(
|
|
27
|
+
(entry) => entry.id === state.activeEntryId || entry.highlighted,
|
|
28
|
+
),
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
React.useEffect(() => {
|
|
33
|
+
const preferredIndex = state.entries.findIndex(
|
|
34
|
+
(entry) => entry.id === state.activeEntryId || entry.highlighted,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (typeof state.playbackStep === "number") {
|
|
38
|
+
const mappedIndex = playbackStepEntryIndices[state.playbackStep - 1];
|
|
39
|
+
const syncedIndex = Math.min(
|
|
40
|
+
Math.max(
|
|
41
|
+
typeof mappedIndex === "number"
|
|
42
|
+
? mappedIndex
|
|
43
|
+
: state.playbackStep - 2,
|
|
44
|
+
0,
|
|
45
|
+
),
|
|
46
|
+
Math.max(state.entries.length - 1, 0),
|
|
47
|
+
);
|
|
48
|
+
setActiveIndex(syncedIndex);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setActiveIndex(Math.max(0, preferredIndex));
|
|
53
|
+
|
|
54
|
+
if (state.entries.length <= 1) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const interval = window.setInterval(() => {
|
|
59
|
+
setActiveIndex((current) => (current + 1) % state.entries.length);
|
|
60
|
+
}, playbackMs);
|
|
61
|
+
|
|
62
|
+
return () => window.clearInterval(interval);
|
|
63
|
+
}, [
|
|
64
|
+
playbackMs,
|
|
65
|
+
playbackStepEntryIndices,
|
|
66
|
+
state.activeEntryId,
|
|
67
|
+
state.entries,
|
|
68
|
+
state.title,
|
|
69
|
+
state.playbackStep,
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const fallbackCodeEntry =
|
|
73
|
+
state.entries.find((entry) => (entry.code?.length ?? 0) > 0) ??
|
|
74
|
+
state.entries[0];
|
|
75
|
+
const activeEntry = state.entries[activeIndex] ?? state.entries[0];
|
|
76
|
+
const displayEntry =
|
|
77
|
+
(activeEntry?.code?.length ?? 0) > 0 ? activeEntry : fallbackCodeEntry;
|
|
78
|
+
const activeCode = displayEntry?.code ?? [];
|
|
79
|
+
const startLine = Number(
|
|
80
|
+
displayEntry?.lineNumber ?? activeEntry?.lineNumber ?? 24,
|
|
81
|
+
);
|
|
82
|
+
const activeLineCount = Math.max(1, activeCode.length || 1);
|
|
83
|
+
const [visibleLineCount, setVisibleLineCount] = React.useState(
|
|
84
|
+
Math.max(1, Math.min(2, activeCode.length || 1)),
|
|
85
|
+
);
|
|
86
|
+
const [scrollMetrics, setScrollMetrics] = React.useState({
|
|
87
|
+
scrollTop: 0,
|
|
88
|
+
scrollHeight: 1,
|
|
89
|
+
clientHeight: 1,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
React.useEffect(() => {
|
|
93
|
+
if (typeof state.playbackStep === "number") {
|
|
94
|
+
const mappedVisibleCount =
|
|
95
|
+
playbackStepVisibleLineCounts[state.playbackStep - 1];
|
|
96
|
+
const revealFromStep = Math.max(state.playbackStep - 1, 1);
|
|
97
|
+
const syncedVisibleCount = Math.min(
|
|
98
|
+
activeCode.length || 1,
|
|
99
|
+
Math.max(
|
|
100
|
+
1,
|
|
101
|
+
typeof mappedVisibleCount === "number"
|
|
102
|
+
? mappedVisibleCount
|
|
103
|
+
: revealFromStep * 2,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
setVisibleLineCount(syncedVisibleCount);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
setVisibleLineCount(Math.max(1, Math.min(2, activeCode.length || 1)));
|
|
111
|
+
|
|
112
|
+
if (activeCode.length <= 2) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const interval = window.setInterval(
|
|
117
|
+
() => {
|
|
118
|
+
setVisibleLineCount((current) => {
|
|
119
|
+
if (current >= activeCode.length) {
|
|
120
|
+
return Math.max(1, Math.min(2, activeCode.length));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return current + 1;
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
Math.max(
|
|
127
|
+
520,
|
|
128
|
+
Math.round(playbackMs / Math.max(activeCode.length - 1, 1)),
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return () => window.clearInterval(interval);
|
|
133
|
+
}, [
|
|
134
|
+
activeCode,
|
|
135
|
+
playbackMs,
|
|
136
|
+
playbackStepVisibleLineCounts,
|
|
137
|
+
activeEntry?.id,
|
|
138
|
+
state.playbackStep,
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
React.useEffect(() => {
|
|
142
|
+
const viewport = scrollViewportRef.current;
|
|
143
|
+
|
|
144
|
+
if (!viewport) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const updateScrollMetrics = () => {
|
|
149
|
+
setScrollMetrics({
|
|
150
|
+
scrollTop: viewport.scrollTop,
|
|
151
|
+
scrollHeight: viewport.scrollHeight,
|
|
152
|
+
clientHeight: viewport.clientHeight,
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
updateScrollMetrics();
|
|
157
|
+
|
|
158
|
+
viewport.addEventListener("scroll", updateScrollMetrics, {
|
|
159
|
+
passive: true,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
163
|
+
updateScrollMetrics();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
resizeObserver.observe(viewport);
|
|
167
|
+
|
|
168
|
+
if (viewport.firstElementChild instanceof HTMLElement) {
|
|
169
|
+
resizeObserver.observe(viewport.firstElementChild);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
window.addEventListener("resize", updateScrollMetrics);
|
|
173
|
+
|
|
174
|
+
return () => {
|
|
175
|
+
viewport.removeEventListener("scroll", updateScrollMetrics);
|
|
176
|
+
resizeObserver.disconnect();
|
|
177
|
+
window.removeEventListener("resize", updateScrollMetrics);
|
|
178
|
+
};
|
|
179
|
+
}, [visibleLineCount, activeEntry?.id, activeCode.length]);
|
|
180
|
+
|
|
181
|
+
const visibleCode = activeCode.slice(0, visibleLineCount);
|
|
182
|
+
const hasOverflow =
|
|
183
|
+
scrollMetrics.scrollHeight > scrollMetrics.clientHeight + 1;
|
|
184
|
+
const thumbHeight = hasOverflow
|
|
185
|
+
? Math.max(
|
|
186
|
+
36,
|
|
187
|
+
(scrollMetrics.clientHeight / scrollMetrics.scrollHeight) *
|
|
188
|
+
scrollMetrics.clientHeight,
|
|
189
|
+
)
|
|
190
|
+
: 0;
|
|
191
|
+
const maxThumbOffset = Math.max(scrollMetrics.clientHeight - thumbHeight, 0);
|
|
192
|
+
const maxScrollTop = Math.max(
|
|
193
|
+
scrollMetrics.scrollHeight - scrollMetrics.clientHeight,
|
|
194
|
+
1,
|
|
195
|
+
);
|
|
196
|
+
const thumbOffset = hasOverflow
|
|
197
|
+
? (scrollMetrics.scrollTop / maxScrollTop) * maxThumbOffset
|
|
198
|
+
: 0;
|
|
199
|
+
|
|
200
|
+
const updateScrollTopFromPointer = React.useCallback(
|
|
201
|
+
(clientY: number) => {
|
|
202
|
+
const viewport = scrollViewportRef.current;
|
|
203
|
+
const track = scrollbarTrackRef.current;
|
|
204
|
+
|
|
205
|
+
if (!viewport || !track || !hasOverflow) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const trackRect = track.getBoundingClientRect();
|
|
210
|
+
const nextThumbTop = Math.min(
|
|
211
|
+
Math.max(
|
|
212
|
+
clientY - trackRect.top - dragStateRef.current!.pointerOffsetY,
|
|
213
|
+
0,
|
|
214
|
+
),
|
|
215
|
+
maxThumbOffset,
|
|
216
|
+
);
|
|
217
|
+
const nextScrollTop =
|
|
218
|
+
maxThumbOffset > 0 ? (nextThumbTop / maxThumbOffset) * maxScrollTop : 0;
|
|
219
|
+
|
|
220
|
+
viewport.scrollTop = nextScrollTop;
|
|
221
|
+
},
|
|
222
|
+
[hasOverflow, maxScrollTop, maxThumbOffset],
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const handleScrollbarPointerDown = React.useCallback(
|
|
226
|
+
(event: React.PointerEvent<HTMLDivElement>) => {
|
|
227
|
+
const viewport = scrollViewportRef.current;
|
|
228
|
+
const track = scrollbarTrackRef.current;
|
|
229
|
+
|
|
230
|
+
if (!viewport || !track || !hasOverflow) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const trackRect = track.getBoundingClientRect();
|
|
235
|
+
const targetElement = event.target as HTMLElement | null;
|
|
236
|
+
const clickedThumb = targetElement?.dataset.scrollbarThumb === "true";
|
|
237
|
+
const pointerOffsetY = clickedThumb
|
|
238
|
+
? event.clientY - trackRect.top - thumbOffset
|
|
239
|
+
: thumbHeight / 2;
|
|
240
|
+
|
|
241
|
+
dragStateRef.current = {
|
|
242
|
+
pointerId: event.pointerId,
|
|
243
|
+
startY: event.clientY,
|
|
244
|
+
startScrollTop: viewport.scrollTop,
|
|
245
|
+
scrollRatio: maxThumbOffset > 0 ? maxScrollTop / maxThumbOffset : 0,
|
|
246
|
+
pointerOffsetY,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
if (!clickedThumb) {
|
|
250
|
+
updateScrollTopFromPointer(event.clientY);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
(event.currentTarget as HTMLDivElement).setPointerCapture(
|
|
254
|
+
event.pointerId,
|
|
255
|
+
);
|
|
256
|
+
event.preventDefault();
|
|
257
|
+
},
|
|
258
|
+
[
|
|
259
|
+
hasOverflow,
|
|
260
|
+
maxScrollTop,
|
|
261
|
+
maxThumbOffset,
|
|
262
|
+
thumbHeight,
|
|
263
|
+
thumbOffset,
|
|
264
|
+
updateScrollTopFromPointer,
|
|
265
|
+
],
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const handleScrollbarPointerMove = React.useCallback(
|
|
269
|
+
(event: React.PointerEvent<HTMLDivElement>) => {
|
|
270
|
+
const dragState = dragStateRef.current;
|
|
271
|
+
|
|
272
|
+
if (!dragState || dragState.pointerId !== event.pointerId) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const viewport = scrollViewportRef.current;
|
|
277
|
+
const track = scrollbarTrackRef.current;
|
|
278
|
+
|
|
279
|
+
if (!viewport || !track || !hasOverflow) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const deltaY = event.clientY - dragState.startY;
|
|
284
|
+
const nextScrollTop = Math.min(
|
|
285
|
+
Math.max(dragState.startScrollTop + deltaY * dragState.scrollRatio, 0),
|
|
286
|
+
maxScrollTop,
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
viewport.scrollTop = nextScrollTop;
|
|
290
|
+
event.preventDefault();
|
|
291
|
+
},
|
|
292
|
+
[hasOverflow, maxScrollTop],
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const handleScrollbarPointerUp = React.useCallback(
|
|
296
|
+
(event: React.PointerEvent<HTMLDivElement>) => {
|
|
297
|
+
const dragState = dragStateRef.current;
|
|
298
|
+
|
|
299
|
+
if (dragState?.pointerId === event.pointerId) {
|
|
300
|
+
dragStateRef.current = null;
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
[],
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<div className="flex h-full min-h-0 flex-col text-[var(--demo-fg)] [font-synthesis:none] antialiased">
|
|
308
|
+
<div className="flex min-h-0 flex-1 overflow-hidden rounded-none border border-[var(--demo-border)] bg-[var(--demo-code-bg)] shadow-none">
|
|
309
|
+
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col">
|
|
310
|
+
<div
|
|
311
|
+
ref={scrollViewportRef}
|
|
312
|
+
className="grid min-h-0 flex-1 grid-cols-[3.5rem_minmax(0,1fr)] overflow-y-auto bg-[var(--demo-code-bg)] pr-5 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden lg:overflow-hidden lg:pr-0"
|
|
313
|
+
>
|
|
314
|
+
<div className="border-r border-[var(--demo-border)] bg-[var(--demo-code-gutter-bg)] px-2 py-3 font-mono text-[11px] leading-7 text-[var(--demo-subtle-fg)]">
|
|
315
|
+
{visibleCode.map((line, index) => {
|
|
316
|
+
const isAdded = line.trimStart().startsWith("+");
|
|
317
|
+
const isRemoved = line.trimStart().startsWith("-");
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<div
|
|
321
|
+
key={`${startLine + index}`}
|
|
322
|
+
className={cn(
|
|
323
|
+
"text-right",
|
|
324
|
+
isAdded && "text-[var(--demo-info)]",
|
|
325
|
+
isRemoved && "text-[var(--demo-danger)]",
|
|
326
|
+
)}
|
|
327
|
+
>
|
|
328
|
+
{startLine + index}
|
|
329
|
+
</div>
|
|
330
|
+
);
|
|
331
|
+
})}
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
<div className="relative min-h-full bg-[var(--demo-code-bg)] px-3 py-3 font-mono text-[12px] leading-7 text-[var(--demo-fg)]">
|
|
335
|
+
<div>
|
|
336
|
+
{visibleCode.map((line, index) => {
|
|
337
|
+
const isAdded = line.trimStart().startsWith("+");
|
|
338
|
+
const isRemoved = line.trimStart().startsWith("-");
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<div
|
|
342
|
+
key={`${line}-${index}`}
|
|
343
|
+
className={cn(
|
|
344
|
+
"flex border-l border-transparent pl-3 transition-colors duration-300",
|
|
345
|
+
isAdded &&
|
|
346
|
+
"border-[var(--demo-info-border)] bg-[var(--demo-info-soft-bg)] text-[var(--demo-fg)]",
|
|
347
|
+
isRemoved &&
|
|
348
|
+
"border-[var(--demo-danger-border)] bg-[var(--demo-danger-soft-bg)] text-[var(--demo-fg)]",
|
|
349
|
+
!isAdded && !isRemoved && "text-[var(--demo-muted-fg)]",
|
|
350
|
+
|
|
351
|
+
displayEntry?.highlighted &&
|
|
352
|
+
index === Math.min(1, visibleCode.length - 1) &&
|
|
353
|
+
"animate-pulse",
|
|
354
|
+
)}
|
|
355
|
+
>
|
|
356
|
+
<span
|
|
357
|
+
className={cn(
|
|
358
|
+
"mr-3 w-3 shrink-0 text-center text-[11px]",
|
|
359
|
+
isAdded
|
|
360
|
+
? "text-[var(--demo-info)]"
|
|
361
|
+
: isRemoved
|
|
362
|
+
? "text-[var(--demo-danger)]"
|
|
363
|
+
: "text-[var(--demo-subtle-fg)]",
|
|
364
|
+
)}
|
|
365
|
+
>
|
|
366
|
+
{isAdded ? "+" : isRemoved ? "-" : " "}
|
|
367
|
+
</span>
|
|
368
|
+
<span className="min-w-0 flex-1 whitespace-pre-wrap break-words">
|
|
369
|
+
{isAdded || isRemoved
|
|
370
|
+
? line.slice(1).trimStart()
|
|
371
|
+
: line}
|
|
372
|
+
</span>
|
|
373
|
+
</div>
|
|
374
|
+
);
|
|
375
|
+
})}
|
|
376
|
+
|
|
377
|
+
{displayEntry?.highlighted &&
|
|
378
|
+
visibleLineCount < activeLineCount ? (
|
|
379
|
+
<div className="mt-2 flex items-center gap-2 pl-3 text-[11px] text-[var(--demo-subtle-fg)]">
|
|
380
|
+
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[var(--demo-accent)]" />
|
|
381
|
+
Applying change...
|
|
382
|
+
</div>
|
|
383
|
+
) : null}
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
{hasOverflow ? (
|
|
389
|
+
<div
|
|
390
|
+
ref={scrollbarTrackRef}
|
|
391
|
+
aria-hidden="true"
|
|
392
|
+
onPointerDown={handleScrollbarPointerDown}
|
|
393
|
+
onPointerMove={handleScrollbarPointerMove}
|
|
394
|
+
onPointerUp={handleScrollbarPointerUp}
|
|
395
|
+
onPointerCancel={handleScrollbarPointerUp}
|
|
396
|
+
className="absolute inset-y-3 right-1.5 w-[10px] cursor-pointer overflow-hidden rounded-full bg-[var(--demo-scroll-track)] lg:hidden"
|
|
397
|
+
>
|
|
398
|
+
<div
|
|
399
|
+
data-scrollbar-thumb="true"
|
|
400
|
+
className="absolute inset-x-1 rounded-full bg-[var(--demo-scroll-thumb)] shadow-[inset_0_1px_0_rgba(255,255,255,0.35)] dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.08)] will-change-transform"
|
|
401
|
+
style={{
|
|
402
|
+
height: `${thumbHeight}px`,
|
|
403
|
+
transform: `translateY(${thumbOffset}px)`,
|
|
404
|
+
}}
|
|
405
|
+
/>
|
|
406
|
+
</div>
|
|
407
|
+
) : null}
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
{state.metrics?.length ? (
|
|
411
|
+
<div className="hidden w-[6.75rem] shrink-0 border-l border-[var(--demo-border)] bg-[var(--demo-panel-muted-bg)] p-2 lg:flex lg:flex-col lg:gap-2">
|
|
412
|
+
{state.metrics.map((metric) => (
|
|
413
|
+
<div
|
|
414
|
+
key={metric.id}
|
|
415
|
+
className="rounded-md border border-[var(--demo-border)] bg-[var(--demo-panel-bg)] px-2 py-2 text-center"
|
|
416
|
+
>
|
|
417
|
+
<div className="text-[10px] uppercase tracking-[0.16em] text-[var(--demo-subtle-fg)]">
|
|
418
|
+
{metric.label}
|
|
419
|
+
</div>
|
|
420
|
+
<div className="mt-1 font-mono text-[12px] text-[var(--demo-fg)]">
|
|
421
|
+
{metric.value}
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
))}
|
|
425
|
+
</div>
|
|
426
|
+
) : null}
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
);
|
|
430
|
+
}
|