add modal to show code
This commit is contained in:
20
packages/website/app/modules/dom/portal.tsx
Normal file
20
packages/website/app/modules/dom/portal.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { ReactNode } from "react"
|
||||
import { useEffect, useRef } from "react"
|
||||
import { createPortal } from "react-dom"
|
||||
|
||||
export function Portal({ children }: { children: ReactNode }) {
|
||||
const containerRef = useRef<Element>()
|
||||
|
||||
if (!containerRef.current && typeof document !== "undefined") {
|
||||
containerRef.current = document.createElement("react-portal")
|
||||
document.body.append(containerRef.current)
|
||||
}
|
||||
|
||||
useEffect(() => () => containerRef.current!.remove(), [])
|
||||
|
||||
return containerRef.current ? (
|
||||
createPortal(children, containerRef.current)
|
||||
) : (
|
||||
<>{children}</>
|
||||
)
|
||||
}
|
||||
197
packages/website/app/modules/landing/landing-animation.tsx
Normal file
197
packages/website/app/modules/landing/landing-animation.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
import clsx from "clsx"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import blobComfyUrl from "~/assets/blob-comfy.png"
|
||||
import cursorIbeamUrl from "~/assets/cursor-ibeam.png"
|
||||
import cursorUrl from "~/assets/cursor.png"
|
||||
|
||||
const defaultState = {
|
||||
chatInputText: "",
|
||||
chatInputCursorVisible: true,
|
||||
messageVisible: false,
|
||||
count: 0,
|
||||
cursorLeft: "25%",
|
||||
cursorBottom: "-15px",
|
||||
}
|
||||
|
||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
const animationFrame = () =>
|
||||
new Promise((resolve) => requestAnimationFrame(resolve))
|
||||
|
||||
export function LandingAnimation() {
|
||||
const [state, setState] = useState(defaultState)
|
||||
const chatInputRef = useRef<HTMLDivElement>(null)
|
||||
const addRef = useRef<HTMLDivElement>(null)
|
||||
const deleteRef = useRef<HTMLDivElement>(null)
|
||||
const cursorRef = useRef<HTMLImageElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const animateClick = (element: HTMLElement) =>
|
||||
element.animate(
|
||||
[{ transform: `translateY(2px)` }, { transform: `translateY(0px)` }],
|
||||
300,
|
||||
)
|
||||
|
||||
let running = true
|
||||
|
||||
void (async () => {
|
||||
await delay(1000)
|
||||
|
||||
while (running) {
|
||||
setState(defaultState)
|
||||
await delay(1000)
|
||||
|
||||
for (const letter of "/counter") {
|
||||
setState((state) => ({
|
||||
...state,
|
||||
chatInputText: state.chatInputText + letter,
|
||||
}))
|
||||
await delay(100)
|
||||
}
|
||||
|
||||
await delay(1000)
|
||||
|
||||
setState((state) => ({
|
||||
...state,
|
||||
messageVisible: true,
|
||||
chatInputText: "",
|
||||
}))
|
||||
await delay(1000)
|
||||
|
||||
setState((state) => ({
|
||||
...state,
|
||||
cursorLeft: "70px",
|
||||
cursorBottom: "40px",
|
||||
}))
|
||||
await delay(1500)
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
setState((state) => ({
|
||||
...state,
|
||||
count: state.count + 1,
|
||||
chatInputCursorVisible: false,
|
||||
}))
|
||||
animateClick(addRef.current!)
|
||||
await delay(700)
|
||||
}
|
||||
|
||||
await delay(500)
|
||||
|
||||
setState((state) => ({
|
||||
...state,
|
||||
cursorLeft: "140px",
|
||||
}))
|
||||
await delay(1000)
|
||||
|
||||
animateClick(deleteRef.current!)
|
||||
setState((state) => ({ ...state, messageVisible: false }))
|
||||
await delay(1000)
|
||||
|
||||
setState(() => ({
|
||||
...defaultState,
|
||||
chatInputCursorVisible: false,
|
||||
}))
|
||||
await delay(500)
|
||||
}
|
||||
})()
|
||||
|
||||
return () => {
|
||||
running = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let running = true
|
||||
|
||||
void (async () => {
|
||||
while (running) {
|
||||
// check if the cursor is in the input
|
||||
const cursorRect = cursorRef.current!.getBoundingClientRect()
|
||||
const chatInputRect = chatInputRef.current!.getBoundingClientRect()
|
||||
|
||||
const isOverInput =
|
||||
cursorRef.current &&
|
||||
chatInputRef.current &&
|
||||
cursorRect.top + cursorRect.height / 2 > chatInputRect.top
|
||||
|
||||
cursorRef.current!.src = isOverInput ? cursorIbeamUrl : cursorUrl
|
||||
|
||||
await animationFrame()
|
||||
}
|
||||
})()
|
||||
|
||||
return () => {
|
||||
running = false
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className="grid gap-2 relative pointer-events-none select-none"
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-slate-800 p-4 rounded-lg shadow transition",
|
||||
state.messageVisible ? "opacity-100" : "opacity-0 -translate-y-2",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 p-2 rounded-full bg-no-repeat bg-contain bg-black/25">
|
||||
<img
|
||||
src={blobComfyUrl}
|
||||
alt=""
|
||||
className="object-contain scale-90 w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold">comfybot</p>
|
||||
<p>this button was clicked {state.count} times</p>
|
||||
<div className="mt-2 flex flex-row gap-3">
|
||||
<div
|
||||
ref={addRef}
|
||||
className="bg-emerald-700 text-white py-1.5 px-3 text-sm rounded"
|
||||
>
|
||||
+1
|
||||
</div>
|
||||
<div
|
||||
ref={deleteRef}
|
||||
className="bg-red-700 text-white py-1.5 px-3 text-sm rounded"
|
||||
>
|
||||
🗑 delete
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="bg-slate-700 pb-2 pt-1.5 px-4 rounded-lg shadow"
|
||||
ref={chatInputRef}
|
||||
>
|
||||
<span
|
||||
className={clsx(
|
||||
"text-sm after:content-[attr(data-after)] after:relative after:-top-px after:-left-[2px]",
|
||||
state.chatInputCursorVisible
|
||||
? "after:opacity-100"
|
||||
: "after:opacity-0",
|
||||
)}
|
||||
data-after="|"
|
||||
>
|
||||
{state.chatInputText || (
|
||||
<span className="opacity-50 block absolute translate-y-1">
|
||||
Message #showing-off-reacord
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<img
|
||||
src={cursorUrl}
|
||||
alt=""
|
||||
className="transition-all duration-500 absolute scale-75"
|
||||
style={{ left: state.cursorLeft, bottom: state.cursorBottom }}
|
||||
ref={cursorRef}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
26
packages/website/app/modules/landing/landing-code.mdx
Normal file
26
packages/website/app/modules/landing/landing-code.mdx
Normal file
@@ -0,0 +1,26 @@
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import * as React from "react"
|
||||
import { Button, useInstance } from "reacord"
|
||||
|
||||
function Counter() {
|
||||
const [count, setCount] = React.useState(0)
|
||||
const instance = useInstance()
|
||||
return (
|
||||
<>
|
||||
this button was clicked {count} times
|
||||
<Button
|
||||
label="+1"
|
||||
style="success"
|
||||
onClick={() => setCount(count + 1)}
|
||||
/>
|
||||
<Button
|
||||
label="delete"
|
||||
emoji="🗑"
|
||||
style="danger"
|
||||
onClick={() => instance.destroy()}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -1,19 +0,0 @@
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import * as React from "react"
|
||||
import { Embed, Button } from "reacord"
|
||||
|
||||
function Counter() {
|
||||
const [count, setCount] = React.useState(0)
|
||||
return (
|
||||
<>
|
||||
<Embed title="Counter">
|
||||
This button has been clicked {count} times.
|
||||
</Embed>
|
||||
<Button onClick={() => setCount(count + 1)}>
|
||||
+1
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
80
packages/website/app/modules/ui/modal.tsx
Normal file
80
packages/website/app/modules/ui/modal.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { XIcon } from "@heroicons/react/outline"
|
||||
import clsx from "clsx"
|
||||
import type { ReactNode } from "react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { FocusOn } from "react-focus-on"
|
||||
import { Portal } from "~/modules/dom/portal"
|
||||
|
||||
export function Modal({
|
||||
children,
|
||||
visible,
|
||||
onClose,
|
||||
}: {
|
||||
children: ReactNode
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
}) {
|
||||
const closeButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
// trying to immediately focus doesn't work for whatever reason
|
||||
// neither did requestAnimationFrame
|
||||
setTimeout(() => {
|
||||
closeButtonRef.current?.focus()
|
||||
}, 50)
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-black/70 fixed inset-0 transition-all flex flex-col p-4",
|
||||
visible ? "opacity-100 visible" : "opacity-0 invisible",
|
||||
)}
|
||||
>
|
||||
<FocusOn
|
||||
className={clsx(
|
||||
"m-auto flex flex-col gap-2 w-full max-h-full max-w-screen-sm overflow-y-auto transition",
|
||||
visible ? "translate-y-0" : "translate-y-3",
|
||||
)}
|
||||
enabled={visible}
|
||||
onClickOutside={onClose}
|
||||
onEscapeKey={onClose}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="self-end"
|
||||
onClick={onClose}
|
||||
ref={closeButtonRef}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<XIcon aria-hidden className="w-6 text-white" />
|
||||
</button>
|
||||
<div className={clsx("bg-slate-700 rounded-md shadow p-4")}>
|
||||
{children}
|
||||
</div>
|
||||
</FocusOn>
|
||||
</div>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export function ControlledModal({
|
||||
children,
|
||||
button,
|
||||
}: {
|
||||
children: ReactNode
|
||||
button: (buttonProps: { onClick: () => void }) => void
|
||||
}) {
|
||||
const [visible, setVisible] = useState(false)
|
||||
return (
|
||||
<>
|
||||
{button({ onClick: () => setVisible(true) })}
|
||||
<Modal visible={visible} onClose={() => setVisible(false)}>
|
||||
{children}
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user