diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..bba0fcf
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,54 @@
+name: Deploy playground demo # 工作流的名称
+
+on: # 触发条件
+ push:
+ branches:
+ - main # 当 main 分支收到推送时触发
+ workflow_dispatch: # 允许手动触发
+
+permissions: # GitHub token 的权限设置
+ contents: read # 读取仓库内容
+ pages: write # 写入 GitHub Pages
+ id-token: write # 写入身份令牌
+
+concurrency: # 并发控制
+ group: pages # 同一时间只允许一个部署任务运行
+ cancel-in-progress: true # 如果有新的部署,取消正在进行的部署
+
+jobs:
+ deploy:
+ environment: # 环境配置
+ name: github-pages # 环境名称
+ url: ${{ steps.deployment.outputs.page_url }} # 部署后的 URL
+
+ runs-on: ubuntu-latest # 运行环境
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ with:
+ run_install: false
+ - uses: actions/setup-node@v4
+ with:
+ node-version: lts/*
+ cache: pnpm
+
+ - run: pnpm i
+ - run: pnpm playground:build
+
+ # 添加缓存验证步骤
+ - name: Verify build
+ run: |
+ if [ ! -d "playground/dist" ]; then
+ echo "Playground build failed - dist directory not found"
+ exit 1
+ fi
+
+ - uses: actions/configure-pages@v5
+ with:
+ enablement: true
+ - uses: actions/upload-pages-artifact@v3
+ with:
+ path: playground/dist # 指定要部署的目录
+ - uses: actions/deploy-pages@v4
+ id: deployment
diff --git a/README.md b/README.md
index 09d5557..5ed29b4 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,7 @@ const timer = pausableTimers(() => {
timer.pause()
timer.resume()
timer.clear()
-timer.reset()
+timer.restart()
timer.isPaused()
timers.getRemainingTime()
timer.isCompleted() // only for timeout mode
diff --git a/package.json b/package.json
index 67fc530..305b2bc 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,9 @@
"scripts": {
"build": "unbuild",
"dev": "unbuild --stub",
+ "playground:dev": "vite playground",
+ "playground:build": "vite build playground",
+ "playground:preview": "vite preview playground",
"lint": "eslint .",
"prepublishOnly": "automd && nr build",
"release": "bumpp && pnpm publish",
diff --git a/playground/index.html b/playground/index.html
new file mode 100644
index 0000000..d472260
--- /dev/null
+++ b/playground/index.html
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/index.ts b/playground/index.ts
deleted file mode 100644
index ffd5f97..0000000
--- a/playground/index.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-/* eslint-disable antfu/no-top-level-await */
-/* eslint-disable no-console */
-import { pausableTimers } from '../src/index'
-
-const timer = pausableTimers(() => {
- console.timeEnd('timeout timer')
-
- console.time('timeout timer')
-}, 5000, {
- mode: 'interval',
-})
-
-// remain 3s
-await delay(2000)
-timer.pause()
-timer.resume()
-
-// remain 1s
-await delay(2000)
-timer.pause()
-timer.resume()
-
-console.time('timeout timer')
-
-function delay(ms: number): Promise {
- return new Promise(resolve => setTimeout(resolve, ms))
-}
diff --git a/playground/main.js b/playground/main.js
new file mode 100644
index 0000000..104b0ef
--- /dev/null
+++ b/playground/main.js
@@ -0,0 +1,180 @@
+import pkg from '../package.json'
+import { pausableTimers } from '../src/index'
+
+// 设置页面标题和项目信息
+document.title = `${pkg.name} | Playground`
+document.querySelector('.project-info h1').textContent = pkg.name
+document.querySelector('.version').textContent = `v${pkg.version}`
+
+const elements = {
+ progressBar: document.querySelector('.progress-bar'),
+ statusText: document.querySelector('.status-text'),
+ timeInput: document.querySelector('#timeInput'),
+ modeSelect: document.querySelector('#modeSelect'),
+ startBtn: document.querySelector('#startBtn'),
+ pauseBtn: document.querySelector('#pauseBtn'),
+ clearBtn: document.querySelector('#clearBtn'),
+ restartBtn: document.querySelector('#restartBtn'),
+}
+
+let timer = null
+let animationFrameId = null
+let cycleCount = 0
+
+function updateProgress(remaining, total) {
+ const progress = (remaining / total) * 100
+ const progressBar = elements.progressBar
+
+ // 完全移除过渡效果
+ progressBar.style.transition = 'none'
+ progressBar.style.width = `${progress}%`
+
+ if (remaining === total) {
+ // 当需要重置到 100% 时,保持 none
+ return
+ }
+
+ // 确保 DOM 更新后再添加过渡效果
+ requestAnimationFrame(() => {
+ progressBar.style.transition = 'width 0.1s linear'
+ })
+}
+
+function updateButtons(isRunning) {
+ elements.startBtn.disabled = isRunning
+ elements.pauseBtn.disabled = !isRunning
+ elements.clearBtn.disabled = !isRunning
+ elements.restartBtn.disabled = !isRunning
+ elements.timeInput.disabled = isRunning
+ elements.modeSelect.disabled = isRunning
+}
+
+function animate(total) {
+ if (!timer)
+ return
+
+ const remaining = timer.getRemainingTime()
+ updateProgress(remaining, total)
+
+ // 根据模式显示不同的状态信息
+ if (elements.modeSelect.value === 'interval') {
+ elements.statusText.textContent = `Cycle ${cycleCount} - Remaining: ${remaining}ms`
+ }
+ else {
+ elements.statusText.textContent = `Remaining: ${remaining}ms`
+ }
+
+ if (timer.isCompleted()) {
+ elements.statusText.textContent = 'Completed!'
+ updateButtons(false)
+ return
+ }
+
+ animationFrameId = requestAnimationFrame(() => animate(total))
+}
+
+function startTimer() {
+ const delay = Number.parseInt(elements.timeInput.value)
+ // 添加输入值检查
+ if (delay < 100) {
+ elements.statusText.textContent = 'Please enter a value greater than 100ms'
+ return
+ }
+
+ const mode = elements.modeSelect.value
+
+ // 先清理现有的计时器和动画
+ if (timer) {
+ timer.clear()
+ }
+ if (animationFrameId) {
+ cancelAnimationFrame(animationFrameId)
+ animationFrameId = null
+ }
+
+ elements.statusText.textContent = ''
+ timer = pausableTimers(() => {
+ if (mode === 'timeout') {
+ cancelAnimationFrame(animationFrameId)
+ updateProgress(0, delay)
+ elements.statusText.textContent = 'Completed!'
+ updateButtons(false)
+ }
+ else {
+ // 在 interval 模式下,先取消动画帧
+ cancelAnimationFrame(animationFrameId)
+ // 立即重置进度条到 100%(无动画)
+ updateProgress(delay, delay)
+ // 在 interval 模式下添加循环次数显示
+ cycleCount++
+ elements.statusText.textContent = `Cycle ${cycleCount}`
+ // 立即开始新的倒计时(有动画)
+ requestAnimationFrame(() => animate(delay))
+ }
+ }, delay, { mode })
+
+ updateButtons(true)
+ // 启动时也应用相同的逻辑
+ updateProgress(delay, delay)
+ requestAnimationFrame(() => animate(delay))
+}
+
+// Event Listeners
+elements.startBtn.addEventListener('click', startTimer)
+
+elements.pauseBtn.addEventListener('click', () => {
+ if (!timer)
+ return
+ if (timer.isPaused()) {
+ timer.resume()
+ elements.pauseBtn.textContent = 'Pause'
+ animate(Number.parseInt(elements.timeInput.value))
+ }
+ else {
+ timer.pause()
+ elements.pauseBtn.textContent = 'Resume'
+ cancelAnimationFrame(animationFrameId)
+ }
+})
+
+elements.clearBtn.addEventListener('click', () => {
+ if (!timer)
+ return
+ timer.clear()
+ // 确保动画帧被取消
+ if (animationFrameId) {
+ cancelAnimationFrame(animationFrameId)
+ animationFrameId = null
+ }
+ updateProgress(100, 100)
+ elements.statusText.textContent = ''
+ updateButtons(false)
+ elements.pauseBtn.textContent = 'Pause'
+ cycleCount = 0
+})
+
+elements.restartBtn.addEventListener('click', () => {
+ if (!timer)
+ return
+ // 先清理现有的动画帧
+ if (animationFrameId) {
+ cancelAnimationFrame(animationFrameId)
+ animationFrameId = null
+ }
+ timer.restart()
+ elements.pauseBtn.textContent = 'Pause'
+ updateProgress(Number.parseInt(elements.timeInput.value), Number.parseInt(elements.timeInput.value))
+ animate(Number.parseInt(elements.timeInput.value))
+ cycleCount = 0
+})
+
+// 添加输入框验证
+elements.timeInput.addEventListener('input', (e) => {
+ const value = Number.parseInt(e.target.value)
+ if (value < 100) {
+ e.target.setCustomValidity('Please enter a value greater than 100ms')
+ }
+ else {
+ e.target.setCustomValidity('')
+ }
+})
diff --git a/playground/style.css b/playground/style.css
new file mode 100644
index 0000000..1261a0d
--- /dev/null
+++ b/playground/style.css
@@ -0,0 +1,155 @@
+:root {
+ --primary: #F9B208;
+ --primary-light: #FFD93D;
+ --primary-dark: #F48B29;
+ --background: #FCF5ED;
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background-color: var(--background);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 100vh;
+ padding: 20px;
+}
+
+.container {
+ background: white;
+ padding: 2rem;
+ border-radius: 12px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ width: 100%;
+ max-width: 500px;
+}
+
+.timer-display {
+ margin-bottom: 2rem;
+}
+
+.progress-container {
+ background: #eee;
+ height: 20px;
+ border-radius: 10px;
+ overflow: hidden;
+ margin-bottom: 1rem;
+}
+
+.progress-bar {
+ height: 100%;
+ background: var(--primary);
+ width: 100%;
+ transition: width 0.1s linear;
+}
+
+.status-text {
+ text-align: center;
+ color: var(--primary-dark);
+ font-weight: bold;
+ min-height: 24px;
+}
+
+.control-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.input-group {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+}
+
+.input-group input {
+ padding: 0.5rem;
+ border: 2px solid var(--primary);
+ border-radius: 4px;
+ width: 120px;
+ /* 修复 Edge 浏览器下的数字输入框样式 */
+ &::-webkit-inner-spin-button,
+ &::-webkit-outer-spin-button {
+ opacity: 1;
+ height: 24px;
+ }
+ &::-ms-clear {
+ display: none;
+ }
+}
+
+.input-group select {
+ padding: 0.5rem;
+ border: 2px solid var(--primary);
+ border-radius: 4px;
+}
+
+.button-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+button {
+ padding: 0.5rem 1rem;
+ border: none;
+ border-radius: 4px;
+ background: var(--primary);
+ color: white;
+ cursor: pointer;
+ flex: 1;
+ min-width: 100px;
+}
+
+button:hover {
+ background: var(--primary-dark);
+}
+
+button:disabled {
+ background: #ccc;
+ cursor: not-allowed;
+}
+
+/* GitHub Corner 动画 */
+.github-corner:hover .octo-arm {
+ animation: octocat-wave 560ms ease-in-out;
+}
+
+@keyframes octocat-wave {
+ 0%, 100% { transform: rotate(0) }
+ 20%, 60% { transform: rotate(-25deg) }
+ 40%, 80% { transform: rotate(10deg) }
+}
+
+@media (max-width: 500px) {
+ .github-corner:hover .octo-arm {
+ animation: none;
+ }
+ .github-corner .octo-arm {
+ animation: octocat-wave 560ms ease-in-out;
+ }
+}
+
+.project-info {
+ text-align: center;
+ margin-bottom: 2rem;
+ color: var(--primary-dark);
+}
+
+.project-info h1 {
+ margin: 0;
+ font-size: 2rem;
+}
+
+.version {
+ font-size: 0.9rem;
+ opacity: 0.8;
+ line-height: 1.4;
+}
+
diff --git a/playground/vite.config.js b/playground/vite.config.js
new file mode 100644
index 0000000..3849afd
--- /dev/null
+++ b/playground/vite.config.js
@@ -0,0 +1,11 @@
+import { defineConfig } from 'vite'
+import pkg from '../package.json'
+
+function getRepoName(url) {
+ const match = url.match(/github\.com\/[\w-]+\/([\w-]+)/)
+ return match ? `/${match[1]}/` : '/'
+}
+
+export default defineConfig({
+ base: getRepoName(pkg.repository.url),
+})