Skip to content

Commit f76782e

Browse files
committed
feat: Add animated code block to shikiComponent
1 parent a78ec7e commit f76782e

File tree

1 file changed

+169
-14
lines changed

1 file changed

+169
-14
lines changed
Lines changed: 169 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,41 @@
11
import type { Command, CommandLatest } from "@cursorless/common";
2+
import { useState, useEffect, useRef } from "react";
3+
import clsx from "clsx";
24

35
export function ShikiComponent({
46
data,
7+
debug,
58
}: {
69
data: {
7-
before: string;
8-
during: string;
9-
after: string;
10+
before: { html: string; data: string[] };
11+
during: { html: string; data: string[] };
12+
after: { html: string; data: string[] };
1013
command: CommandLatest | Command;
1114
};
15+
debug?: boolean;
1216
}) {
1317
const { spokenForm } = data.command;
1418
const { before, during, after } = data;
1519
return (
16-
<div className="mx-16 overflow-auto p-4">
17-
<div className="p-8">
20+
<div className="mx-16 overflow-auto p-4 px-10">
21+
<div className="">
1822
<h2 className="dark:text-stone-100">{spokenForm}</h2>
1923

20-
<div className="m-2 border">
21-
<Before content={before} />
22-
<div className="command">{spokenForm}</div>
23-
<During content={during} />
24-
<After content={after} />
25-
</div>
24+
{debug && (
25+
<div className="my-4 outline">
26+
<Before content={before.html} />
27+
<div className="command">{spokenForm}</div>
28+
<During content={during.html} />
29+
<After content={after.html} />
30+
</div>
31+
)}
32+
<Carousel>
33+
<Before content={before.html} />
34+
<During content={during.html} />
35+
<After content={after.html} />
36+
</Carousel>
2637
</div>
38+
2739
<details>
2840
<summary>JSON</summary>
2941
<pre className="max-w-xl overflow-auto">
@@ -35,13 +47,21 @@ export function ShikiComponent({
3547
}
3648

3749
const Before = ({ content }: { content: string }) => {
38-
return <div className="p-4" dangerouslySetInnerHTML={{ __html: content }} />;
50+
return (
51+
<div
52+
className="rounded-md py-4"
53+
dangerouslySetInnerHTML={{ __html: content }}
54+
/>
55+
);
3956
};
4057

4158
const During = ({ content }: { content: string }) => {
4259
if (content) {
4360
return (
44-
<div className="p-4" dangerouslySetInnerHTML={{ __html: content }} />
61+
<div
62+
className="rounded-md py-4"
63+
dangerouslySetInnerHTML={{ __html: content }}
64+
/>
4565
);
4666
}
4767
return <></>;
@@ -50,8 +70,143 @@ const During = ({ content }: { content: string }) => {
5070
const After = ({ content }: { content: string }) => {
5171
return (
5272
<div
53-
className="flex flex-col gap-y-4 p-4"
73+
className="rounded-md py-4"
5474
dangerouslySetInnerHTML={{ __html: content }}
5575
/>
5676
);
5777
};
78+
79+
const STEP_DURATIONS = [1500, 500, 2000]; // milliseconds
80+
81+
function Carousel({ children }: { children: React.ReactNode[] }) {
82+
const [activeIndex, setActiveIndex] = useState(0);
83+
84+
const handleNext = () => {
85+
setActiveIndex((prevIndex) => (prevIndex + 1) % children.length);
86+
};
87+
88+
const handlePrev = () => {
89+
setActiveIndex((prevIndex) =>
90+
prevIndex === 0 ? children.length - 1 : prevIndex - 1,
91+
);
92+
};
93+
94+
const handleIndicatorClick = (index: number) => {
95+
setActiveIndex(index);
96+
};
97+
98+
const timeoutRef = useRef<any>(null);
99+
100+
useEffect(() => {
101+
const duration = STEP_DURATIONS[activeIndex] || 3000;
102+
103+
timeoutRef.current = setTimeout(() => {
104+
setActiveIndex((prevIndex) => (prevIndex + 1) % children.length);
105+
}, duration);
106+
107+
return () => clearTimeout(timeoutRef.current);
108+
}, [activeIndex, children.length]);
109+
110+
const CarouselItem = ({
111+
child,
112+
isActive,
113+
}: {
114+
child: React.ReactNode;
115+
isActive: boolean;
116+
}) => {
117+
return (
118+
<div
119+
className={clsx("transform transition-all duration-700 ease-in-out", {
120+
"block scale-100 opacity-100": isActive,
121+
"hidden scale-95 opacity-0": !isActive,
122+
})}
123+
data-carousel-item={isActive ? "active" : undefined}
124+
>
125+
{child}
126+
</div>
127+
);
128+
};
129+
130+
return (
131+
<div
132+
id="default-carousel"
133+
className="relative w-full"
134+
data-carousel="slide"
135+
>
136+
{/* <!-- Carousel wrapper --> */}
137+
<div className="relative h-56 overflow-hidden rounded-lg bg-gray-900 md:h-96">
138+
<div className="absolute right-2 top-2 z-30 font-bold text-red-500">
139+
{activeIndex + 1}/{children.length}
140+
</div>
141+
{children.map((child, index) => (
142+
<CarouselItem
143+
key={index}
144+
child={child}
145+
isActive={index === activeIndex}
146+
/>
147+
))}
148+
</div>
149+
{/* <!-- Slider indicators --> */}
150+
<div className="absolute bottom-5 left-1/2 z-30 flex -translate-x-1/2 space-x-3 rtl:space-x-reverse">
151+
{children.map((_, index) => (
152+
<button
153+
key={index}
154+
type="button"
155+
className={clsx("h-3 w-3 rounded-full", {
156+
"bg-blue-500": index === activeIndex,
157+
"bg-gray-300": index !== activeIndex,
158+
})}
159+
aria-current={index === activeIndex}
160+
aria-label={`Slide ${index + 1}`}
161+
onClick={() => handleIndicatorClick(index)}
162+
></button>
163+
))}
164+
</div>
165+
{/* <!-- Slider controls --> */}
166+
<SliderButton additionalClasses="-start-12 top-0" callback={handlePrev} />
167+
<SliderButton
168+
additionalClasses="rotate-180 -end-12 top-0"
169+
callback={handleNext}
170+
/>
171+
</div>
172+
);
173+
}
174+
175+
const SliderButton = ({
176+
additionalClasses,
177+
callback,
178+
}: {
179+
additionalClasses?: string;
180+
callback: React.MouseEventHandler;
181+
}) => {
182+
return (
183+
<button
184+
type="button"
185+
className={clsx(
186+
"group absolute z-30 flex h-full cursor-pointer items-center justify-center px-4 focus:outline-none",
187+
additionalClasses,
188+
)}
189+
onClick={callback}
190+
data-carousel-next
191+
>
192+
<span className="inline-flex h-10 w-10 items-center justify-center rounded-full border-2 border-gray-300 bg-white text-base group-hover:bg-gray-100 group-focus:outline-none group-focus:ring-4 group-focus:ring-white dark:bg-gray-800 dark:text-white dark:group-hover:bg-gray-700 dark:group-focus:ring-white">
193+
<svg
194+
className="h-4 w-4 rtl:rotate-180"
195+
aria-hidden="true"
196+
xmlns="http://www.w3.org/2000/svg"
197+
fill="none"
198+
viewBox="0 0 6 10"
199+
>
200+
<path
201+
stroke="currentColor"
202+
stroke-linecap="round"
203+
stroke-linejoin="round"
204+
stroke-width="2"
205+
d="M5 1 1 5l4 4"
206+
/>
207+
</svg>
208+
<span className="sr-only">Next</span>
209+
</span>
210+
</button>
211+
);
212+
};

0 commit comments

Comments
 (0)