Skip to content

Commit d2c0822

Browse files
authored
polish website with animation (#701)
1 parent b9994c2 commit d2c0822

File tree

7 files changed

+377
-199
lines changed

7 files changed

+377
-199
lines changed

docs/app/page.tsx

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react'
2+
import { TerminalAnimation } from './terminal-animation'
23

34
export default function Page() {
45
return (
@@ -36,17 +37,27 @@ function TerminalBody() {
3637
"name": "coffee",
3738
"type": "module",
3839
"main": "./dist/index.js",
39-
"scripts": { "build": "bunchee" }
40+
"scripts": {
41+
"build": "bunchee"
42+
}
4043
}`}
4144
</CodeBlock>
4245
<BlockSpacer />
43-
<Prompt caret>npm run build</Prompt>
44-
<CodeBlock>{`Exports File Size
45-
. dist/index.js 5.6 kB`}</CodeBlock>
46+
<TerminalAnimation
47+
text="npm run build"
48+
logs={`Exports File Size\n. dist/index.js 5.6 kB`}
49+
/>
4650
<BlockSpacer />
47-
<Comment># Features</Comment>
48-
<Comment> - Zero config, smart externals, and TS declarations</Comment>
49-
<Comment> - Works great for monorepos</Comment>
51+
<MarkdownTitle title="# Why bunchee?" />
52+
<Comment> - Zero config - package.json as config</Comment>
53+
<Comment> - Auto-generates TypeScript declarations</Comment>
54+
<Comment> - Supports ESM, CJS, or dual packages</Comment>
55+
<Comment> - Tree-shakeable and monorepo friendly</Comment>
56+
<BlockSpacer />
57+
<MarkdownTitle title="# Perfect for" />
58+
<Comment> - npm packages and component libraries</Comment>
59+
<Comment> - Node.js tools, CLI apps, and utilities</Comment>
60+
<Comment> - Monorepo workspaces with shared packages</Comment>
5061
<BlockSpacer />
5162
<div className="my-2 h-px bg-white/10" />
5263
<BlockSpacer />
@@ -103,28 +114,47 @@ function Output({ children }: { children: React.ReactNode }) {
103114
}
104115

105116
function Comment({ children }: { children: React.ReactNode }) {
106-
return <div className="pl-6 text-sm text-black/90">{children}</div>
117+
return <div className="pl-2 text-sm text-black/80">{children}</div>
118+
}
119+
120+
function MarkdownTitle({ title }: { title: string }) {
121+
const match = title.match(/^(#+)\s+(.+)$/)
122+
if (match) {
123+
const [, hashes, titleText] = match
124+
return (
125+
<div className="pl-2 text-sm">
126+
<span className="text-black/40">{hashes} </span>
127+
<span className="text-black/90 font-bold">{titleText}</span>
128+
</div>
129+
)
130+
}
131+
return (
132+
<div className="pl-2 text-sm">
133+
<span className="text-black/40"># </span>
134+
<span className="text-black/90 font-bold">{title}</span>
135+
</div>
136+
)
107137
}
108138

109139
function CodeBlock({ children }: { children: React.ReactNode }) {
110140
return (
111-
<pre className="ml-4 mt-2 rounded-md bg-[#f5e6d4] text-[12px] leading-relaxed text-black/80">
112-
<code className="px-3 py-2 block">{children}</code>
141+
<pre className="mt-2 w-full block rounded-md bg-[#f5e6d4] text-[12px] leading-relaxed text-black/80">
142+
<code className="px-3 py-2 block w-full select-none">{children}</code>
113143
</pre>
114144
)
115145
}
116146

117147
function TerminalLearn() {
118148
return (
119149
<div>
120-
<Comment># Learn</Comment>
121-
<Comment>## Entry & Convention</Comment>
150+
<MarkdownTitle title="# Learn" />
151+
<MarkdownTitle title="## Entry & Convention" />
122152
<Output>Files in src/ folder match export names in package.json:</Output>
123153
<CodeBlock>
124154
{`+--------------------------+---------------------+\n| File | Export Name |\n+--------------------------+---------------------+\n| src/index.ts | "." (default) |\n| src/lite.ts | "./lite" |\n| src/react/index.ts | "./react" |\n+--------------------------+---------------------+`}
125155
</CodeBlock>
126156
<BlockSpacer />
127-
<Comment>## Directives</Comment>
157+
<MarkdownTitle title="## Directives" />
128158
<Output>
129159
{`Bunchee can manage multiple directives such as "use client", "use server", or "use cache" and automatically split your code into different chunks and preserve the directives properly.`}
130160
</Output>

docs/app/terminal-animation.tsx

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
'use client'
2+
import React, { useEffect, useState } from 'react'
3+
4+
export function TerminalAnimation({
5+
text,
6+
logs,
7+
spinner = 'ora',
8+
}: {
9+
text: string
10+
logs: string
11+
spinner?: 'ora' | 'line' | 'dots' | 'blocks'
12+
}) {
13+
const command = text
14+
15+
const [typedLength, setTypedLength] = useState(0)
16+
const [phase, setPhase] = useState<
17+
'typing' | 'waitingEnter' | 'spinning' | 'showingLogs'
18+
>('typing')
19+
const [reveal, setReveal] = useState(false)
20+
const [revealedCount, setRevealedCount] = useState(0)
21+
const spinnerFramesMap: Record<string, string[]> = {
22+
ora: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
23+
line: ['|', '/', '-', '\\'],
24+
dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
25+
blocks: ['▖', '▘', '▝', '▗'],
26+
}
27+
const spinFrames = spinnerFramesMap[spinner] ?? spinnerFramesMap.ora
28+
const [spinIndex, setSpinIndex] = useState(0)
29+
30+
const resetAnimation = () => {
31+
setTypedLength(0)
32+
setPhase('typing')
33+
setReveal(false)
34+
setRevealedCount(0)
35+
setSpinIndex(0)
36+
}
37+
38+
useEffect(() => {
39+
if (phase !== 'typing') return
40+
const id = setInterval(() => {
41+
setTypedLength((n) => {
42+
const next = n + 1
43+
if (next >= command.length) {
44+
clearInterval(id)
45+
setPhase('waitingEnter')
46+
return command.length
47+
}
48+
return next
49+
})
50+
}, 25)
51+
return () => clearInterval(id)
52+
}, [phase, command.length])
53+
54+
useEffect(() => {
55+
if (phase === 'waitingEnter') {
56+
const id = setTimeout(() => setPhase('spinning'), 500)
57+
return () => clearTimeout(id)
58+
}
59+
if (phase === 'spinning') {
60+
setRevealedCount(0)
61+
const timeoutId = setTimeout(() => setPhase('showingLogs'), 1200)
62+
const spinId = setInterval(() => {
63+
setSpinIndex((i) => (i + 1) % spinFrames.length)
64+
}, 80)
65+
return () => {
66+
clearTimeout(timeoutId)
67+
clearInterval(spinId)
68+
}
69+
}
70+
if (phase === 'showingLogs') {
71+
const logLines = logs.split('\n')
72+
const timer = setInterval(() => {
73+
setRevealedCount((n) => {
74+
const next = n + 1
75+
if (next >= logLines.length) {
76+
clearInterval(timer)
77+
setReveal(true)
78+
return logLines.length
79+
}
80+
return next
81+
})
82+
}, 180)
83+
return () => clearInterval(timer)
84+
}
85+
}, [phase, logs, spinFrames.length])
86+
87+
return (
88+
<div>
89+
<Prompt caret>
90+
{command.slice(0, typedLength)}
91+
{phase === 'waitingEnter' && (
92+
<span className="ml-2 text-xs text-black/50"></span>
93+
)}
94+
</Prompt>
95+
<div
96+
className={`transition-all duration-300 ease-out ${
97+
reveal
98+
? 'opacity-100 translate-y-0 scale-100'
99+
: 'opacity-100 translate-y-0 scale-100'
100+
}`}
101+
onDoubleClick={resetAnimation}
102+
>
103+
<CodeBlock>
104+
{logs.split('\n').map((line, idx) => {
105+
const isVisible =
106+
phase === 'spinning'
107+
? idx === 0
108+
: idx <
109+
(phase === 'showingLogs' ? Math.max(1, revealedCount) : 0)
110+
const content =
111+
phase === 'spinning' && idx === 0
112+
? `Building ${spinFrames[spinIndex]}`
113+
: line
114+
return (
115+
<div key={idx} className={isVisible ? '' : 'opacity-0'}>
116+
{content}
117+
</div>
118+
)
119+
})}
120+
</CodeBlock>
121+
</div>
122+
</div>
123+
)
124+
}
125+
126+
function Prompt({
127+
children,
128+
className = '',
129+
caret = false,
130+
}: {
131+
children: React.ReactNode
132+
className?: string
133+
caret?: boolean
134+
}) {
135+
return (
136+
<div className={`flex text-sm ${className}`}>
137+
<span className="text-black/40 mr-2">{`➜`}</span>
138+
<span className="text-[#000]">~/project</span>
139+
<span className="ml-2 text-black">$</span>
140+
<span className="ml-2 inline-flex items-center">
141+
<span className="text-black/60">{children}</span>
142+
{caret && (
143+
<span className="ml-1 inline-block h-4 w-2 translate-y-[1px] bg-[#000]/70 align-middle caret" />
144+
)}
145+
</span>
146+
</div>
147+
)
148+
}
149+
150+
function CodeBlock({ children }: { children: React.ReactNode }) {
151+
return (
152+
<pre className="mt-2 w-full block rounded-md bg-[#f5e6d4] text-[12px] leading-relaxed text-black/80">
153+
<code className="px-3 py-2 block w-full select-none">{children}</code>
154+
</pre>
155+
)
156+
}

docs/next-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3-
/// <reference path="./.next/types/routes.d.ts" />
3+
import './.next/dev/types/routes.d.ts'
44

55
// NOTE: This file should not be edited
66
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

docs/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "bunchee-docs",
3+
"private": true,
4+
"type": "module"
5+
}

docs/tsconfig.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,19 @@
1212
"moduleResolution": "node",
1313
"resolveJsonModule": true,
1414
"isolatedModules": true,
15-
"jsx": "preserve",
15+
"jsx": "react-jsx",
1616
"plugins": [
1717
{
1818
"name": "next"
1919
}
2020
]
2121
},
22-
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
22+
"include": [
23+
"next-env.d.ts",
24+
".next/types/**/*.ts",
25+
"**/*.ts",
26+
"**/*.tsx",
27+
".next/dev/types/**/*.ts"
28+
],
2329
"exclude": ["node_modules"]
2430
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
"cross-env": "^7.0.3",
103103
"husky": "^9.0.11",
104104
"lint-staged": "^15.2.2",
105-
"next": "canary",
105+
"next": "16.0.0-canary.1",
106106
"picocolors": "^1.0.0",
107107
"postcss": "^8.5.4",
108108
"prettier": "3.4.2",

0 commit comments

Comments
 (0)