nextworks 0.2.0-alpha.13 → 0.2.0-alpha.15

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 (43) hide show
  1. package/README.md +3 -1
  2. package/dist/cli_manifests/blocks_manifest.json +5 -0
  3. package/dist/kits/blocks/.nextworks/docs/BLOCKS_QUICKSTART.md +2 -0
  4. package/dist/kits/blocks/.nextworks/docs/BLOCKS_README.md +2 -0
  5. package/dist/kits/blocks/app/templates/aiworkflow/PresetThemeVars.tsx +1 -58
  6. package/dist/kits/blocks/app/templates/aiworkflow/README.md +2 -0
  7. package/dist/kits/blocks/app/templates/aiworkflow/components/CTA.tsx +9 -9
  8. package/dist/kits/blocks/app/templates/aiworkflow/components/Contact.tsx +12 -13
  9. package/dist/kits/blocks/app/templates/aiworkflow/components/FAQ.tsx +22 -19
  10. package/dist/kits/blocks/app/templates/aiworkflow/components/FeatureMockups.tsx +562 -0
  11. package/dist/kits/blocks/app/templates/aiworkflow/components/Features.tsx +18 -16
  12. package/dist/kits/blocks/app/templates/aiworkflow/components/Footer.tsx +13 -9
  13. package/dist/kits/blocks/app/templates/aiworkflow/components/Hero.tsx +883 -636
  14. package/dist/kits/blocks/app/templates/aiworkflow/components/Navbar.tsx +14 -15
  15. package/dist/kits/blocks/app/templates/aiworkflow/components/Pricing.tsx +27 -22
  16. package/dist/kits/blocks/app/templates/aiworkflow/components/ProcessTimeline.tsx +20 -21
  17. package/dist/kits/blocks/app/templates/aiworkflow/components/Testimonials.tsx +17 -13
  18. package/dist/kits/blocks/app/templates/aiworkflow/components/TrustBadges.tsx +15 -12
  19. package/dist/kits/blocks/app/templates/aiworkflow/themes/animation.tsx +151 -0
  20. package/dist/kits/blocks/app/templates/aiworkflow/themes/default.tsx +158 -0
  21. package/dist/kits/blocks/app/templates/aiworkflow/themes/test.tsx +163 -0
  22. package/dist/kits/blocks/app/templates/gallery/PresetThemeVars.tsx +46 -0
  23. package/dist/kits/blocks/app/templates/gallery/page.tsx +550 -161
  24. package/dist/kits/blocks/components/sections/HeroProductDemo.tsx +74 -64
  25. package/dist/kits/blocks/components/sections/Navbar.tsx +2 -0
  26. package/dist/kits/blocks/components/sections/product-demo/ApprovalInboxPanel.tsx +16 -13
  27. package/dist/kits/blocks/components/sections/product-demo/DemoStage.tsx +283 -162
  28. package/dist/kits/blocks/components/sections/product-demo/DemoWindow.tsx +65 -53
  29. package/dist/kits/blocks/components/sections/product-demo/KnowledgePanel.tsx +20 -17
  30. package/dist/kits/blocks/components/sections/product-demo/RunConsolePanel.tsx +208 -127
  31. package/dist/kits/blocks/components/sections/product-demo/TaskListPanel.tsx +95 -0
  32. package/dist/kits/blocks/components/sections/product-demo/WorkflowStudioPanel.tsx +714 -161
  33. package/dist/kits/blocks/components/sections/product-demo/types.ts +69 -0
  34. package/dist/kits/blocks/components/ui/theme-selector.tsx +1 -1
  35. package/dist/kits/blocks/package-deps.json +3 -3
  36. package/dist/kits/blocks/public/placeholders/aiworkflow/live.svg +92 -0
  37. package/dist/kits/blocks/public/placeholders/aiworkflow/review.svg +80 -0
  38. package/dist/kits/blocks/public/placeholders/aiworkflow/task.svg +71 -0
  39. package/dist/kits/blocks/tsconfig.json +13 -0
  40. package/dist/utils/file-operations.d.ts.map +1 -1
  41. package/dist/utils/file-operations.js +6 -1
  42. package/dist/utils/file-operations.js.map +1 -1
  43. package/package.json +1 -1
@@ -15,17 +15,20 @@ export interface DemoWindowProps {
15
15
  enableMotion?: boolean;
16
16
  showControls?: boolean;
17
17
  showResizeHandle?: boolean;
18
+ showHeader?: boolean;
18
19
  children?: React.ReactNode;
19
20
  }
20
21
 
21
22
  const STATUS_TONE_CLASSES: Record<ProductDemoStatusTone, string> = {
22
- neutral: "border-border/60 bg-muted/60 text-muted-foreground",
23
- info: "border-sky-500/30 bg-sky-500/10 text-sky-600 dark:text-sky-300",
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)]",
24
26
  success:
25
- "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-300",
27
+ "border-[var(--demo-success-border)] bg-[var(--demo-success-soft-bg)] text-[var(--demo-success)]",
26
28
  warning:
27
- "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300",
28
- danger: "border-rose-500/30 bg-rose-500/10 text-rose-600 dark:text-rose-300",
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)]",
29
32
  };
30
33
 
31
34
  export function DemoWindow({
@@ -39,6 +42,7 @@ export function DemoWindow({
39
42
  enableMotion = true,
40
43
  showControls = true,
41
44
  showResizeHandle = true,
45
+ showHeader = true,
42
46
  children,
43
47
  }: DemoWindowProps) {
44
48
  const statusTone = window.status?.tone ?? "neutral";
@@ -46,11 +50,14 @@ export function DemoWindow({
46
50
  return (
47
51
  <section
48
52
  className={cn(
49
- "group relative flex h-full min-h-[14rem] flex-col overflow-hidden rounded-[1.5rem] border border-border/60 bg-card/92 shadow-[0_18px_48px_-24px_rgba(15,23,42,0.45)] backdrop-blur-xl",
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))]",
50
57
  enableMotion &&
51
- "transition-[transform,opacity,box-shadow,border-color] duration-500 ease-out motion-reduce:transition-none",
52
- active &&
53
- "border-primary/45 shadow-[0_24px_60px_-26px_rgba(59,130,246,0.45)]",
58
+ "transition-[opacity,border-color,background-color] duration-500 ease-out motion-reduce:transition-none",
59
+ active && "border-[var(--demo-border-strong)]",
60
+
54
61
  dimmed && "opacity-90",
55
62
  className,
56
63
  )}
@@ -58,55 +65,60 @@ export function DemoWindow({
58
65
  data-active={active ? "true" : "false"}
59
66
  aria-label={window.title}
60
67
  >
61
- <header
62
- className={cn(
63
- "relative flex items-start justify-between gap-3 border-b border-border/50 px-4 py-3 sm:px-5",
64
- chromeClassName,
65
- )}
66
- >
67
- <div className="flex items-start gap-3">
68
- {showControls && (
69
- <div className="mt-1 flex items-center gap-1.5 opacity-75 transition-opacity duration-200 group-hover:opacity-100">
70
- <span className="h-2.5 w-2.5 rounded-full bg-rose-400/90" />
71
- <span className="h-2.5 w-2.5 rounded-full bg-amber-400/90" />
72
- <span className="h-2.5 w-2.5 rounded-full bg-emerald-400/90" />
73
- </div>
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
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
+ )}
75
84
 
76
- <div className="min-w-0">
77
- <div className="flex flex-wrap items-center gap-2">
78
- <h3 className="truncate text-sm font-semibold text-card-foreground sm:text-[0.95rem]">
79
- {window.title}
80
- </h3>
81
- {window.badge && (
82
- <span className="rounded-full border border-border/60 bg-muted/70 px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.16em] text-muted-foreground">
83
- {window.badge}
84
- </span>
85
- )}
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>
86
101
  </div>
87
- {window.subtitle && (
88
- <p className="mt-1 truncate text-xs text-muted-foreground sm:text-[0.8rem]">
89
- {window.subtitle}
90
- </p>
91
- )}
92
102
  </div>
93
- </div>
94
103
 
95
- {window.status?.label && (
96
- <span
97
- className={cn(
98
- "shrink-0 rounded-full border px-2.5 py-1 text-[10px] font-medium uppercase tracking-[0.16em]",
99
- STATUS_TONE_CLASSES[statusTone],
100
- )}
101
- >
102
- {window.status.label}
103
- </span>
104
- )}
105
- </header>
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}
106
117
 
107
118
  <div
108
119
  className={cn(
109
- "relative flex-1 px-4 py-4 sm:px-5 sm:py-5",
120
+ "relative flex-1 min-h-0 px-4 py-4 sm:px-5 sm:py-5",
121
+
110
122
  bodyClassName,
111
123
  )}
112
124
  >
@@ -117,9 +129,9 @@ export function DemoWindow({
117
129
  aria-hidden="true"
118
130
  className="pointer-events-none absolute bottom-3 right-3 h-4 w-4 opacity-0 transition-opacity duration-200 group-hover:opacity-70"
119
131
  >
120
- <span className="absolute bottom-0 right-0 h-px w-3 rotate-45 bg-border/80" />
121
- <span className="absolute bottom-1 right-0 h-px w-2 rotate-45 bg-border/70" />
122
- <span className="absolute bottom-0 right-1 h-px w-2 rotate-45 bg-border/60" />
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)]" />
123
135
  </div>
124
136
  )}
125
137
  </div>
@@ -10,13 +10,15 @@ export interface KnowledgePanelProps {
10
10
  }
11
11
 
12
12
  const STATUS_TONE_CLASSES: Record<ProductDemoStatusTone, string> = {
13
- neutral: "border-border/60 bg-muted/55 text-muted-foreground",
14
- info: "border-sky-500/30 bg-sky-500/10 text-sky-600 dark:text-sky-300",
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)]",
15
16
  success:
16
- "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-300",
17
+ "border-[var(--demo-success-border)] bg-[var(--demo-success-soft-bg)] text-[var(--demo-success)]",
17
18
  warning:
18
- "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300",
19
- danger: "border-rose-500/30 bg-rose-500/10 text-rose-600 dark:text-rose-300",
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)]",
20
22
  };
21
23
 
22
24
  function getStatusClass(tone: ProductDemoStatusTone = "neutral") {
@@ -25,28 +27,28 @@ function getStatusClass(tone: ProductDemoStatusTone = "neutral") {
25
27
 
26
28
  export function KnowledgePanel({ state }: KnowledgePanelProps) {
27
29
  return (
28
- <div className="flex h-full flex-col gap-4">
30
+ <div className="flex h-full flex-col gap-4 text-[var(--demo-fg)]">
29
31
  <div className="space-y-1.5">
30
32
  {state.title && (
31
- <h4 className="text-sm font-semibold text-card-foreground">
33
+ <h4 className="text-sm font-semibold text-[var(--demo-fg)]">
32
34
  {state.title}
33
35
  </h4>
34
36
  )}
35
37
  {state.subtitle && (
36
- <p className="text-xs leading-relaxed text-muted-foreground">
38
+ <p className="text-xs leading-relaxed text-[var(--demo-muted-fg)]">
37
39
  {state.subtitle}
38
40
  </p>
39
41
  )}
40
42
  </div>
41
43
 
42
44
  {state.query && (
43
- <div className="rounded-xl border border-primary/25 bg-primary/8 px-3 py-2 text-xs text-primary">
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)]">
44
46
  {state.query}
45
47
  </div>
46
48
  )}
47
49
 
48
50
  {state.summary && (
49
- <p className="text-xs leading-relaxed text-muted-foreground">
51
+ <p className="text-xs leading-relaxed text-[var(--demo-muted-fg)]">
50
52
  {state.summary}
51
53
  </p>
52
54
  )}
@@ -80,17 +82,18 @@ export function KnowledgePanel({ state }: KnowledgePanelProps) {
80
82
  <div
81
83
  key={snippet.id}
82
84
  className={cn(
83
- "rounded-2xl border border-border/60 bg-background/80 p-3",
84
- isActive && "border-primary/45 bg-primary/6 shadow-sm",
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",
85
88
  )}
86
89
  >
87
90
  <div className="flex items-start justify-between gap-3">
88
91
  <div>
89
- <div className="text-sm font-semibold text-card-foreground">
92
+ <div className="text-sm font-semibold text-[var(--demo-fg)]">
90
93
  {snippet.title}
91
94
  </div>
92
95
  {(snippet.excerptLabel || source?.label) && (
93
- <div className="mt-1 text-[11px] text-muted-foreground">
96
+ <div className="mt-1 text-[11px] text-[var(--demo-muted-fg)]">
94
97
  {[snippet.excerptLabel, source?.label]
95
98
  .filter(Boolean)
96
99
  .join(" • ")}
@@ -98,12 +101,12 @@ export function KnowledgePanel({ state }: KnowledgePanelProps) {
98
101
  )}
99
102
  </div>
100
103
  {snippet.confidence && (
101
- <span className="rounded-full border border-border/60 bg-muted/60 px-2 py-1 text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
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)]">
102
105
  {snippet.confidence}
103
106
  </span>
104
107
  )}
105
108
  </div>
106
- <p className="mt-2 text-xs leading-relaxed text-muted-foreground">
109
+ <p className="mt-2 text-xs leading-relaxed text-[var(--demo-muted-fg)]">
107
110
  {snippet.content}
108
111
  </p>
109
112
  {snippet.tags?.length ? (
@@ -111,7 +114,7 @@ export function KnowledgePanel({ state }: KnowledgePanelProps) {
111
114
  {snippet.tags.map((tag) => (
112
115
  <span
113
116
  key={tag}
114
- className="rounded-full border border-border/60 bg-background/70 px-2 py-0.5 text-[10px] text-muted-foreground"
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)]"
115
118
  >
116
119
  {tag}
117
120
  </span>
@@ -1,149 +1,230 @@
1
1
  import React from "react";
2
2
  import { cn } from "@/lib/utils";
3
- import type {
4
- ProductDemoRunConsoleState,
5
- ProductDemoStatusTone,
6
- } from "./types";
3
+ import type { ProductDemoRunConsoleState } from "./types";
7
4
 
8
5
  export interface RunConsolePanelProps {
9
6
  state: ProductDemoRunConsoleState;
10
7
  }
11
8
 
12
- const STATUS_TONE_CLASSES: Record<ProductDemoStatusTone, string> = {
13
- neutral: "border-border/60 bg-muted/55 text-muted-foreground",
14
- info: "border-sky-500/30 bg-sky-500/10 text-sky-600 dark:text-sky-300",
15
- success:
16
- "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-300",
17
- warning:
18
- "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300",
19
- danger: "border-rose-500/30 bg-rose-500/10 text-rose-600 dark:text-rose-300",
20
- };
21
-
22
- function getStatusClass(tone: ProductDemoStatusTone = "neutral") {
23
- return STATUS_TONE_CLASSES[tone];
24
- }
9
+ export function RunConsolePanel({ state }: RunConsolePanelProps) {
10
+ const playbackMs = state.playbackMs ?? 1800;
11
+ const playbackStepEntryIndices = state.playbackStepEntryIndices ?? [];
12
+ const playbackStepVisibleLineCounts =
13
+ state.playbackStepVisibleLineCounts ?? [];
14
+ const [activeIndex, setActiveIndex] = React.useState(
15
+ Math.max(
16
+ 0,
17
+ state.entries.findIndex(
18
+ (entry) => entry.id === state.activeEntryId || entry.highlighted,
19
+ ),
20
+ ),
21
+ );
25
22
 
26
- function getProgressPercent(value: number | undefined) {
27
- if (typeof value !== "number" || Number.isNaN(value)) {
28
- return undefined;
29
- }
23
+ React.useEffect(() => {
24
+ const preferredIndex = state.entries.findIndex(
25
+ (entry) => entry.id === state.activeEntryId || entry.highlighted,
26
+ );
30
27
 
31
- return Math.min(100, Math.max(0, value));
32
- }
28
+ if (typeof state.playbackStep === "number") {
29
+ const mappedIndex = playbackStepEntryIndices[state.playbackStep - 1];
30
+ const syncedIndex = Math.min(
31
+ Math.max(
32
+ typeof mappedIndex === "number"
33
+ ? mappedIndex
34
+ : state.playbackStep - 2,
35
+ 0,
36
+ ),
37
+ Math.max(state.entries.length - 1, 0),
38
+ );
39
+ setActiveIndex(syncedIndex);
40
+ return;
41
+ }
33
42
 
34
- export function RunConsolePanel({ state }: RunConsolePanelProps) {
35
- const progressPercent = getProgressPercent(state.progressPercent);
43
+ setActiveIndex(Math.max(0, preferredIndex));
44
+
45
+ if (state.entries.length <= 1) {
46
+ return;
47
+ }
48
+
49
+ const interval = window.setInterval(() => {
50
+ setActiveIndex((current) => (current + 1) % state.entries.length);
51
+ }, playbackMs);
52
+
53
+ return () => window.clearInterval(interval);
54
+ }, [
55
+ playbackMs,
56
+ playbackStepEntryIndices,
57
+ state.activeEntryId,
58
+ state.entries,
59
+ state.title,
60
+ state.playbackStep,
61
+ ]);
62
+
63
+ const fallbackCodeEntry =
64
+ state.entries.find((entry) => (entry.code?.length ?? 0) > 0) ??
65
+ state.entries[0];
66
+ const activeEntry = state.entries[activeIndex] ?? state.entries[0];
67
+ const displayEntry =
68
+ (activeEntry?.code?.length ?? 0) > 0 ? activeEntry : fallbackCodeEntry;
69
+ const activeCode = displayEntry?.code ?? [];
70
+ const startLine = Number(
71
+ displayEntry?.lineNumber ?? activeEntry?.lineNumber ?? 24,
72
+ );
73
+ const activeLineCount = Math.max(1, activeCode.length || 1);
74
+ const [visibleLineCount, setVisibleLineCount] = React.useState(
75
+ Math.max(1, Math.min(2, activeCode.length || 1)),
76
+ );
77
+
78
+ React.useEffect(() => {
79
+ if (typeof state.playbackStep === "number") {
80
+ const mappedVisibleCount =
81
+ playbackStepVisibleLineCounts[state.playbackStep - 1];
82
+ const revealFromStep = Math.max(state.playbackStep - 1, 1);
83
+ const syncedVisibleCount = Math.min(
84
+ activeCode.length || 1,
85
+ Math.max(
86
+ 1,
87
+ typeof mappedVisibleCount === "number"
88
+ ? mappedVisibleCount
89
+ : revealFromStep * 2,
90
+ ),
91
+ );
92
+ setVisibleLineCount(syncedVisibleCount);
93
+ return;
94
+ }
95
+
96
+ setVisibleLineCount(Math.max(1, Math.min(2, activeCode.length || 1)));
97
+
98
+ if (activeCode.length <= 2) {
99
+ return;
100
+ }
101
+
102
+ const interval = window.setInterval(
103
+ () => {
104
+ setVisibleLineCount((current) => {
105
+ if (current >= activeCode.length) {
106
+ return Math.max(1, Math.min(2, activeCode.length));
107
+ }
108
+
109
+ return current + 1;
110
+ });
111
+ },
112
+ Math.max(
113
+ 520,
114
+ Math.round(playbackMs / Math.max(activeCode.length - 1, 1)),
115
+ ),
116
+ );
117
+
118
+ return () => window.clearInterval(interval);
119
+ }, [
120
+ activeCode,
121
+ playbackMs,
122
+ playbackStepVisibleLineCounts,
123
+ activeEntry?.id,
124
+ state.playbackStep,
125
+ ]);
126
+
127
+ const visibleCode = activeCode.slice(0, visibleLineCount);
36
128
 
37
129
  return (
38
- <div className="flex h-full flex-col gap-4">
39
- <div className="flex flex-wrap items-start justify-between gap-3">
40
- <div className="space-y-1.5">
41
- {state.title && (
42
- <h4 className="text-sm font-semibold text-card-foreground">
43
- {state.title}
44
- </h4>
45
- )}
46
- {state.subtitle && (
47
- <p className="text-xs leading-relaxed text-muted-foreground">
48
- {state.subtitle}
49
- </p>
50
- )}
51
- </div>
130
+ <div className="flex h-full min-h-0 flex-col text-[var(--demo-fg)] [font-synthesis:none] antialiased">
131
+ <div className="flex min-h-0 flex-1 overflow-hidden rounded-none border border-[var(--demo-border)] bg-[var(--demo-code-bg)] shadow-none">
132
+ <div className="flex min-h-0 min-w-0 flex-1 flex-col">
133
+ <div className="grid min-h-0 flex-1 grid-cols-[3.5rem_minmax(0,1fr)] bg-[var(--demo-code-bg)]">
134
+ <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)]">
135
+ {visibleCode.map((line, index) => {
136
+ const isAdded = line.trimStart().startsWith("+");
137
+ const isRemoved = line.trimStart().startsWith("-");
52
138
 
53
- {(state.statusLabel || state.progressLabel) && (
54
- <div className="space-y-1 text-right">
55
- {state.statusLabel && (
56
- <div className="text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground">
57
- {state.statusLabel}
58
- </div>
59
- )}
60
- {state.progressLabel && (
61
- <div className="text-sm font-semibold text-card-foreground">
62
- {state.progressLabel}
63
- </div>
64
- )}
65
- </div>
66
- )}
67
- </div>
139
+ return (
140
+ <div
141
+ key={`${startLine + index}`}
142
+ className={cn(
143
+ "text-right",
144
+ isAdded && "text-[var(--demo-info)]",
145
+ isRemoved && "text-[var(--demo-danger)]",
146
+ )}
147
+ >
148
+ {startLine + index}
149
+ </div>
150
+ );
151
+ })}
152
+ </div>
68
153
 
69
- {typeof progressPercent === "number" ? (
70
- <div className="space-y-2">
71
- <div className="h-2 overflow-hidden rounded-full bg-muted/70">
72
- <div
73
- className="h-full rounded-full bg-primary transition-[width] duration-500"
74
- style={{ width: `${progressPercent}%` }}
75
- />
76
- </div>
77
- <div className="text-[11px] text-muted-foreground">
78
- {progressPercent}% complete
79
- </div>
80
- </div>
81
- ) : null}
82
-
83
- {state.metrics?.length ? (
84
- <div className="grid grid-cols-2 gap-2">
85
- {state.metrics.map((metric) => (
86
- <div
87
- key={metric.id}
88
- className={cn(
89
- "rounded-xl border px-3 py-2",
90
- getStatusClass(metric.tone),
91
- )}
92
- >
93
- <div className="text-[10px] font-medium uppercase tracking-[0.14em] opacity-80">
94
- {metric.label}
154
+ <div className="relative flex h-full min-h-0 flex-col overflow-hidden bg-[var(--demo-code-bg)] px-3 py-3 font-mono text-[12px] leading-7 text-[var(--demo-fg)]">
155
+ <div>
156
+ {visibleCode.map((line, index) => {
157
+ const isAdded = line.trimStart().startsWith("+");
158
+ const isRemoved = line.trimStart().startsWith("-");
159
+
160
+ return (
161
+ <div
162
+ key={`${line}-${index}`}
163
+ className={cn(
164
+ "flex border-l border-transparent pl-3 transition-colors duration-300",
165
+ isAdded &&
166
+ "border-[var(--demo-info-border)] bg-[var(--demo-info-soft-bg)] text-[var(--demo-fg)]",
167
+ isRemoved &&
168
+ "border-[var(--demo-danger-border)] bg-[var(--demo-danger-soft-bg)] text-[var(--demo-fg)]",
169
+ !isAdded && !isRemoved && "text-[var(--demo-muted-fg)]",
170
+
171
+ displayEntry?.highlighted &&
172
+ index === Math.min(1, visibleCode.length - 1) &&
173
+ "animate-pulse",
174
+ )}
175
+ >
176
+ <span
177
+ className={cn(
178
+ "mr-3 w-3 shrink-0 text-center text-[11px]",
179
+ isAdded
180
+ ? "text-[var(--demo-info)]"
181
+ : isRemoved
182
+ ? "text-[var(--demo-danger)]"
183
+ : "text-[var(--demo-subtle-fg)]",
184
+ )}
185
+ >
186
+ {isAdded ? "+" : isRemoved ? "-" : " "}
187
+ </span>
188
+ <span className="min-w-0 flex-1 whitespace-pre-wrap break-words">
189
+ {isAdded || isRemoved
190
+ ? line.slice(1).trimStart()
191
+ : line}
192
+ </span>
193
+ </div>
194
+ );
195
+ })}
196
+
197
+ {displayEntry?.highlighted &&
198
+ visibleLineCount < activeLineCount ? (
199
+ <div className="mt-2 flex items-center gap-2 pl-3 text-[11px] text-[var(--demo-subtle-fg)]">
200
+ <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[var(--demo-accent)]" />
201
+ Applying change...
202
+ </div>
203
+ ) : null}
95
204
  </div>
96
- <div className="mt-1 text-sm font-semibold">{metric.value}</div>
205
+
206
+ <div className="flex-1" />
97
207
  </div>
98
- ))}
208
+ </div>
99
209
  </div>
100
- ) : null}
101
-
102
- <div className="space-y-2">
103
- {state.entries.map((entry) => {
104
- const isActive =
105
- entry.id === state.activeEntryId || entry.highlighted;
106
-
107
- return (
108
- <div
109
- key={entry.id}
110
- className={cn(
111
- "rounded-xl border border-border/60 bg-background/70 px-3 py-2.5",
112
- isActive && "border-primary/40 bg-primary/6",
113
- )}
114
- >
115
- <div className="flex items-start justify-between gap-3">
116
- <div className="min-w-0 flex-1">
117
- <div className="text-sm text-card-foreground">
118
- {entry.message}
119
- </div>
120
- {(entry.source || entry.timestamp) && (
121
- <div className="mt-1 text-[11px] text-muted-foreground">
122
- {[entry.source, entry.timestamp]
123
- .filter(Boolean)
124
- .join(" • ")}
125
- </div>
126
- )}
127
- {entry.detail && (
128
- <p className="mt-2 text-xs leading-relaxed text-muted-foreground">
129
- {entry.detail}
130
- </p>
131
- )}
210
+
211
+ {state.metrics?.length ? (
212
+ <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">
213
+ {state.metrics.map((metric) => (
214
+ <div
215
+ key={metric.id}
216
+ className="rounded-md border border-[var(--demo-border)] bg-[var(--demo-panel-bg)] px-2 py-2 text-center"
217
+ >
218
+ <div className="text-[10px] uppercase tracking-[0.16em] text-[var(--demo-subtle-fg)]">
219
+ {metric.label}
220
+ </div>
221
+ <div className="mt-1 font-mono text-[12px] text-[var(--demo-fg)]">
222
+ {metric.value}
132
223
  </div>
133
- {entry.status && (
134
- <span
135
- className={cn(
136
- "rounded-full border px-2 py-1 text-[10px] font-medium uppercase tracking-[0.14em]",
137
- getStatusClass(entry.status),
138
- )}
139
- >
140
- {entry.status}
141
- </span>
142
- )}
143
224
  </div>
144
- </div>
145
- );
146
- })}
225
+ ))}
226
+ </div>
227
+ ) : null}
147
228
  </div>
148
229
  </div>
149
230
  );