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.
Files changed (145) hide show
  1. package/README.md +280 -228
  2. package/dist/cli_manifests/blocks_manifest.json +193 -194
  3. package/dist/commands/blocks.d.ts +3 -0
  4. package/dist/commands/blocks.d.ts.map +1 -1
  5. package/dist/commands/blocks.js +128 -19
  6. package/dist/commands/blocks.js.map +1 -1
  7. package/dist/commands/doctor.d.ts +136 -0
  8. package/dist/commands/doctor.d.ts.map +1 -0
  9. package/dist/commands/doctor.js +696 -0
  10. package/dist/commands/doctor.js.map +1 -0
  11. package/dist/commands/remove-blocks.d.ts +4 -1
  12. package/dist/commands/remove-blocks.d.ts.map +1 -1
  13. package/dist/commands/remove-blocks.js +24 -6
  14. package/dist/commands/remove-blocks.js.map +1 -1
  15. package/dist/index.js +125 -4
  16. package/dist/index.js.map +1 -1
  17. package/dist/kits/blocks/.nextworks/docs/BLOCKS_QUICKSTART.md +104 -194
  18. package/dist/kits/blocks/.nextworks/docs/BLOCKS_README.md +115 -75
  19. package/dist/kits/blocks/.nextworks/docs/THEME_GUIDE.md +269 -251
  20. package/dist/kits/blocks/app/templates/digitalagency/PresetThemeVars.tsx +80 -80
  21. package/dist/kits/blocks/app/templates/digitalagency/README.md +42 -36
  22. package/dist/kits/blocks/app/templates/digitalagency/components/About.tsx +71 -99
  23. package/dist/kits/blocks/app/templates/digitalagency/components/CTA.tsx +40 -74
  24. package/dist/kits/blocks/app/templates/digitalagency/components/Contact.tsx +123 -227
  25. package/dist/kits/blocks/app/templates/digitalagency/components/Footer.tsx +89 -89
  26. package/dist/kits/blocks/app/templates/digitalagency/components/Hero.tsx +83 -90
  27. package/dist/kits/blocks/app/templates/digitalagency/components/Navbar.tsx +121 -168
  28. package/dist/kits/blocks/app/templates/digitalagency/components/NetworkPattern.tsx +288 -297
  29. package/dist/kits/blocks/app/templates/digitalagency/components/Portfolio.tsx +157 -157
  30. package/dist/kits/blocks/app/templates/digitalagency/components/Pricing.tsx +114 -114
  31. package/dist/kits/blocks/app/templates/digitalagency/components/Process.tsx +59 -59
  32. package/dist/kits/blocks/app/templates/digitalagency/components/Services.tsx +55 -55
  33. package/dist/kits/blocks/app/templates/digitalagency/components/Team.tsx +28 -28
  34. package/dist/kits/blocks/app/templates/digitalagency/components/Testimonials.tsx +65 -65
  35. package/dist/kits/blocks/app/templates/digitalagency/page.tsx +38 -38
  36. package/dist/kits/blocks/app/templates/gallery/PresetThemeVars.tsx +130 -85
  37. package/dist/kits/blocks/app/templates/gallery/page.tsx +357 -303
  38. package/dist/kits/blocks/app/templates/productlaunch/PresetThemeVars.tsx +75 -74
  39. package/dist/kits/blocks/app/templates/productlaunch/README.md +62 -55
  40. package/dist/kits/blocks/app/templates/productlaunch/components/About.tsx +84 -178
  41. package/dist/kits/blocks/app/templates/productlaunch/components/CTA.tsx +50 -93
  42. package/dist/kits/blocks/app/templates/productlaunch/components/Contact.tsx +231 -231
  43. package/dist/kits/blocks/app/templates/productlaunch/components/FAQ.tsx +86 -93
  44. package/dist/kits/blocks/app/templates/productlaunch/components/Features.tsx +83 -84
  45. package/dist/kits/blocks/app/templates/productlaunch/components/Footer.tsx +132 -132
  46. package/dist/kits/blocks/app/templates/productlaunch/components/Hero.tsx +88 -89
  47. package/dist/kits/blocks/app/templates/productlaunch/components/Navbar.tsx +116 -162
  48. package/dist/kits/blocks/app/templates/productlaunch/components/Pricing.tsx +106 -106
  49. package/dist/kits/blocks/app/templates/productlaunch/components/ProcessTimeline.tsx +110 -110
  50. package/dist/kits/blocks/app/templates/productlaunch/components/ServicesGrid.tsx +68 -68
  51. package/dist/kits/blocks/app/templates/productlaunch/components/Team.tsx +104 -104
  52. package/dist/kits/blocks/app/templates/productlaunch/components/Testimonials.tsx +90 -89
  53. package/dist/kits/blocks/app/templates/productlaunch/components/TrustBadges.tsx +76 -76
  54. package/dist/kits/blocks/app/templates/productlaunch/page.tsx +43 -43
  55. package/dist/kits/blocks/app/templates/saasdashboard/PresetThemeVars.tsx +80 -80
  56. package/dist/kits/blocks/app/templates/saasdashboard/README.md +44 -38
  57. package/dist/kits/blocks/app/templates/saasdashboard/components/Contact.tsx +129 -176
  58. package/dist/kits/blocks/app/templates/saasdashboard/components/Dashboard.tsx +293 -293
  59. package/dist/kits/blocks/app/templates/saasdashboard/components/FAQ.tsx +55 -55
  60. package/dist/kits/blocks/app/templates/saasdashboard/components/Features.tsx +90 -91
  61. package/dist/kits/blocks/app/templates/saasdashboard/components/Footer.tsx +77 -77
  62. package/dist/kits/blocks/app/templates/saasdashboard/components/Hero.tsx +104 -105
  63. package/dist/kits/blocks/app/templates/saasdashboard/components/Hero_mask.tsx +126 -127
  64. package/dist/kits/blocks/app/templates/saasdashboard/components/Navbar.tsx +117 -159
  65. package/dist/kits/blocks/app/templates/saasdashboard/components/Pricing.tsx +90 -90
  66. package/dist/kits/blocks/app/templates/saasdashboard/components/SmoothScroll.tsx +96 -97
  67. package/dist/kits/blocks/app/templates/saasdashboard/components/Testimonials.tsx +72 -72
  68. package/dist/kits/blocks/app/templates/saasdashboard/components/TrustBadges.tsx +53 -53
  69. package/dist/kits/blocks/app/templates/saasdashboard/page.tsx +39 -39
  70. package/dist/kits/blocks/components/enhanced-theme-provider.tsx +195 -183
  71. package/dist/kits/blocks/components/providers/BlocksAppProviders.tsx +27 -27
  72. package/dist/kits/blocks/components/sections/About.tsx +291 -291
  73. package/dist/kits/blocks/components/sections/CTA.tsx +257 -258
  74. package/dist/kits/blocks/components/sections/CommandShowcase.tsx +517 -0
  75. package/dist/kits/blocks/components/sections/Contact.tsx +267 -267
  76. package/dist/kits/blocks/components/sections/FAQ.tsx +214 -225
  77. package/dist/kits/blocks/components/sections/FeaturedProjectShowcase.tsx +687 -0
  78. package/dist/kits/blocks/components/sections/Features.tsx +268 -270
  79. package/dist/kits/blocks/components/sections/Footer.tsx +302 -302
  80. package/dist/kits/blocks/components/sections/HeroMotion.tsx +308 -308
  81. package/dist/kits/blocks/components/sections/HeroOverlay.tsx +358 -358
  82. package/dist/kits/blocks/components/sections/HeroProductDemo.tsx +246 -0
  83. package/dist/kits/blocks/components/sections/HeroSplit.tsx +352 -352
  84. package/dist/kits/blocks/components/sections/HeroWithVideo.tsx +495 -0
  85. package/dist/kits/blocks/components/sections/Navbar.tsx +352 -353
  86. package/dist/kits/blocks/components/sections/PortfolioSimple.tsx +549 -550
  87. package/dist/kits/blocks/components/sections/Pricing.tsx +264 -264
  88. package/dist/kits/blocks/components/sections/ProcessTimeline.tsx +325 -325
  89. package/dist/kits/blocks/components/sections/ProjectDeepDive.tsx +805 -0
  90. package/dist/kits/blocks/components/sections/SelectedWorkRail.tsx +485 -0
  91. package/dist/kits/blocks/components/sections/ServicesGrid.tsx +210 -210
  92. package/dist/kits/blocks/components/sections/Team.tsx +309 -309
  93. package/dist/kits/blocks/components/sections/Testimonials.tsx +158 -158
  94. package/dist/kits/blocks/components/sections/TrustBadges.tsx +162 -162
  95. package/dist/kits/blocks/components/sections/product-demo/ApprovalInboxPanel.tsx +128 -0
  96. package/dist/kits/blocks/components/sections/product-demo/DemoStage.tsx +519 -0
  97. package/dist/kits/blocks/components/sections/product-demo/DemoWindow.tsx +140 -0
  98. package/dist/kits/blocks/components/sections/product-demo/KnowledgePanel.tsx +130 -0
  99. package/dist/kits/blocks/components/sections/product-demo/RunConsolePanel.tsx +430 -0
  100. package/dist/kits/blocks/components/sections/product-demo/TaskListPanel.tsx +302 -0
  101. package/dist/kits/blocks/components/sections/product-demo/WorkflowStudioPanel.tsx +744 -0
  102. package/dist/kits/blocks/components/sections/product-demo/types.ts +262 -0
  103. package/dist/kits/blocks/components/theme-provider.tsx +1 -34
  104. package/dist/kits/blocks/components/ui/alert-dialog.tsx +134 -134
  105. package/dist/kits/blocks/components/ui/brand-node.tsx +121 -121
  106. package/dist/kits/blocks/components/ui/button.tsx +122 -122
  107. package/dist/kits/blocks/components/ui/card.tsx +95 -95
  108. package/dist/kits/blocks/components/ui/checkbox.tsx +30 -30
  109. package/dist/kits/blocks/components/ui/cta-button.tsx +125 -125
  110. package/dist/kits/blocks/components/ui/dropdown-menu.tsx +201 -201
  111. package/dist/kits/blocks/components/ui/feature-card.tsx +91 -91
  112. package/dist/kits/blocks/components/ui/input.tsx +27 -27
  113. package/dist/kits/blocks/components/ui/label.tsx +29 -29
  114. package/dist/kits/blocks/components/ui/pricing-card.tsx +120 -120
  115. package/dist/kits/blocks/components/ui/select.tsx +25 -25
  116. package/dist/kits/blocks/components/ui/skeleton.tsx +13 -13
  117. package/dist/kits/blocks/components/ui/table.tsx +98 -98
  118. package/dist/kits/blocks/components/ui/testimonial-card.tsx +108 -108
  119. package/dist/kits/blocks/components/ui/textarea.tsx +26 -26
  120. package/dist/kits/blocks/components/ui/theme-selector.tsx +243 -247
  121. package/dist/kits/blocks/components/ui/theme-toggle.tsx +74 -74
  122. package/dist/kits/blocks/components/ui/toaster.tsx +7 -7
  123. package/dist/kits/blocks/lib/themes.ts +400 -400
  124. package/dist/kits/blocks/lib/utils.ts +6 -9
  125. package/dist/kits/blocks/package-deps.json +40 -28
  126. package/dist/kits/blocks/tsconfig.json +11 -0
  127. package/dist/kits/blocks/tsconfig.tsbuildinfo +1 -0
  128. package/dist/utils/file-operations.d.ts +3 -1
  129. package/dist/utils/file-operations.d.ts.map +1 -1
  130. package/dist/utils/file-operations.js +84 -12
  131. package/dist/utils/file-operations.js.map +1 -1
  132. package/dist/utils/installation-tracker.d.ts +2 -2
  133. package/dist/utils/installation-tracker.d.ts.map +1 -1
  134. package/dist/utils/installation-tracker.js +11 -11
  135. package/dist/utils/installation-tracker.js.map +1 -1
  136. package/dist/utils/package-manager.d.ts +6 -0
  137. package/dist/utils/package-manager.d.ts.map +1 -0
  138. package/dist/utils/package-manager.js +58 -0
  139. package/dist/utils/package-manager.js.map +1 -0
  140. package/dist/utils/yarn-pnp.d.ts +6 -0
  141. package/dist/utils/yarn-pnp.d.ts.map +1 -0
  142. package/dist/utils/yarn-pnp.js +39 -0
  143. package/dist/utils/yarn-pnp.js.map +1 -0
  144. package/package.json +5 -2
  145. 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
+ }