chill-agent 1.4.0 → 1.6.0
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.
- package/.agents/rules/ponytail.md +30 -0
- package/.claude-plugin/contrast-mcp-launcher.mjs +31 -0
- package/.claude-plugin/marketplace.json +17 -0
- package/.claude-plugin/plugin.json +8 -3
- package/.cline-plugin/plugin.js +6 -0
- package/.clinerules/ponytail.md +30 -0
- package/.codex-plugin/plugin.json +4 -3
- package/.cursor/rules/ponytail.mdc +36 -0
- package/.cursor-plugin/marketplace.json +16 -0
- package/.cursor-plugin/plugin.json +2 -2
- package/.devin-plugin/plugin.json +2 -2
- package/.grok-plugin/marketplace.json +16 -0
- package/.kimi-plugin/plugin.json +11 -29
- package/.kiro/steering/ponytail.md +35 -0
- package/.omp-plugin/marketplace.json +15 -0
- package/.openclaw/skills/ponytail/SKILL.md +47 -0
- package/.openclaw/skills/ponytail-audit/SKILL.md +100 -0
- package/.openclaw/skills/ponytail-debt/SKILL.md +43 -0
- package/.openclaw/skills/ponytail-gain/SKILL.md +51 -0
- package/.openclaw/skills/ponytail-help/SKILL.md +76 -0
- package/.openclaw/skills/ponytail-review/SKILL.md +98 -0
- package/.opencode/command/ponytail-audit.md +5 -0
- package/.opencode/command/ponytail-debt.md +5 -0
- package/.opencode/command/ponytail-gain.md +5 -0
- package/.opencode/command/ponytail-help.md +5 -0
- package/.opencode/command/ponytail-review.md +5 -0
- package/.opencode/command/ponytail.md +5 -0
- package/.opencode/plugins/index.js +4 -0
- package/.opencode/plugins/ponytail-frontmatter.cjs +63 -0
- package/.opencode/plugins/ponytail.mjs +206 -0
- package/.qoder-plugin/plugin.json +12 -26
- package/.windsurf/rules/ponytail.md +30 -0
- package/AGENTS.md +32 -0
- package/__init__.py +228 -0
- package/assets/antislop-banner.png +0 -0
- package/assets/band/band.png +0 -0
- package/assets/band/board.png +0 -0
- package/assets/benchmark-3model.svg +21 -0
- package/assets/benchmark-agentic.svg +62 -0
- package/assets/codex-icon.png +0 -0
- package/assets/compare/code/after.webp +0 -0
- package/assets/compare/code/before.webp +0 -0
- package/assets/compare/text/after.webp +0 -0
- package/assets/compare/text/before.webp +0 -0
- package/assets/compare/ui/w-all.webp +0 -0
- package/assets/compare/ui/w-antislop.webp +0 -0
- package/assets/compare/ui/w-design.webp +0 -0
- package/assets/compare/ui/without.webp +0 -0
- package/assets/logo-dark.png +0 -0
- package/assets/logo-dark.svg +115 -0
- package/assets/logo-greenpt-dark.svg +27 -0
- package/assets/logo-greenpt.svg +27 -0
- package/assets/logo.png +0 -0
- package/assets/photoreunion-logo-dark.svg +4 -0
- package/assets/photoreunion-logo-light.svg +4 -0
- package/assets/profile.png +0 -0
- package/assets/retriever-logo-dark.svg +4 -0
- package/assets/retriever-logo-light.svg +4 -0
- package/assets/stories-banner.png +0 -0
- package/assets/v5/audit-es.png +0 -0
- package/assets/v5/audit-ja.png +0 -0
- package/assets/v5/audit-ko.png +0 -0
- package/assets/v5/audit-zh-CN.png +0 -0
- package/assets/v5/audit.png +0 -0
- package/assets/v5/beforeafter-es.png +0 -0
- package/assets/v5/beforeafter-ja.png +0 -0
- package/assets/v5/beforeafter-ko.png +0 -0
- package/assets/v5/beforeafter-zh-CN.png +0 -0
- package/assets/v5/beforeafter.png +0 -0
- package/assets/v5/chart-es.png +0 -0
- package/assets/v5/chart-ja.png +0 -0
- package/assets/v5/chart-ko.png +0 -0
- package/assets/v5/chart-zh-CN.png +0 -0
- package/assets/v5/chart.png +0 -0
- package/assets/v5/hero-es.jpg +0 -0
- package/assets/v5/hero-ja.jpg +0 -0
- package/assets/v5/hero-ko.jpg +0 -0
- package/assets/v5/hero-zh-CN.jpg +0 -0
- package/assets/v5/hero.jpg +0 -0
- package/assets/v5/ladder-es.png +0 -0
- package/assets/v5/ladder-ja.png +0 -0
- package/assets/v5/ladder-ko.png +0 -0
- package/assets/v5/ladder-zh-CN.png +0 -0
- package/assets/v5/ladder.png +0 -0
- package/assets/v5/review-es.png +0 -0
- package/assets/v5/review-ja.png +0 -0
- package/assets/v5/review-ko.png +0 -0
- package/assets/v5/review-zh-CN.png +0 -0
- package/assets/v5/review.png +0 -0
- package/assets/v5/tests-es.png +0 -0
- package/assets/v5/tests-ja.png +0 -0
- package/assets/v5/tests-ko.png +0 -0
- package/assets/v5/tests-zh-CN.png +0 -0
- package/assets/v5/tests.png +0 -0
- package/assets/waitlist-banner-es.png +0 -0
- package/assets/waitlist-banner-ja.png +0 -0
- package/assets/waitlist-banner-ko.png +0 -0
- package/assets/waitlist-banner-zh-CN.png +0 -0
- package/assets/waitlist-banner.png +0 -0
- package/benchmarks/.env.example +2 -0
- package/benchmarks/agentic/README.md +201 -0
- package/benchmarks/agentic/answer.py +288 -0
- package/benchmarks/agentic/compare.py +93 -0
- package/benchmarks/agentic/complete.py +178 -0
- package/benchmarks/agentic/judge.py +198 -0
- package/benchmarks/agentic/run.py +543 -0
- package/benchmarks/agentic/tasks.py +1544 -0
- package/benchmarks/arms/baseline.js +2 -0
- package/benchmarks/arms/caveman-SKILL.md +67 -0
- package/benchmarks/arms/caveman.js +8 -0
- package/benchmarks/arms/ponytail.js +8 -0
- package/benchmarks/behavior.js +54 -0
- package/benchmarks/behavior.yaml +40 -0
- package/benchmarks/correctness.js +291 -0
- package/benchmarks/loc.js +17 -19
- package/benchmarks/promptfooconfig.gemini.yaml +32 -0
- package/benchmarks/promptfooconfig.gpt-newest.yaml +33 -0
- package/benchmarks/promptfooconfig.yaml +41 -0
- package/benchmarks/results/2026-06-12-caveman-vs-ponytail.md +71 -0
- package/benchmarks/results/2026-06-12-v4-hardening-vs-caveman.md +125 -0
- package/benchmarks/results/2026-06-15-llama3.2-local.md +82 -0
- package/benchmarks/results/2026-06-16-correctness-gate-fix.md +113 -0
- package/benchmarks/results/2026-06-16-robustness-audit.md +135 -0
- package/benchmarks/results/2026-06-17-agentic-safety.md +164 -0
- package/benchmarks/results/2026-06-17-cost-verification.md +93 -0
- package/benchmarks/results/2026-06-18-agentic.md +219 -0
- package/benchmarks/results/2026-06-22-issue-245-217-comprehension.md +98 -0
- package/benchmarks/results/2026-10-07-agentic.md +186 -0
- package/cli/index.mjs +241 -0
- package/cli/lib/banner.mjs +21 -0
- package/cli/lib/install.mjs +498 -0
- package/cli/lib/settings.mjs +48 -0
- package/cli/package-lock.json +87 -0
- package/cli/package.json +42 -0
- package/cli/scripts/smoke-test.mjs +95 -0
- package/cli/scripts/smoke-worker.mjs +411 -0
- package/cli/scripts/sync-skills.mjs +81 -0
- package/commands/ponytail-audit.toml +2 -0
- package/commands/ponytail-debt.toml +2 -0
- package/commands/ponytail-gain.toml +2 -0
- package/commands/ponytail-help.toml +2 -0
- package/commands/ponytail-review.toml +2 -0
- package/commands/ponytail.toml +2 -0
- package/examples/email-validation.md +156 -0
- package/examples/group-by.md +35 -0
- package/examples/infinite-scroll.md +58 -0
- package/examples/number-formatting.md +37 -0
- package/examples/rate-limit.md +272 -0
- package/examples/react-countdown.md +390 -0
- package/examples/url-params.md +41 -0
- package/gemini-extension.json +2 -2
- package/hooks/band/board.ts +560 -0
- package/hooks/band/logo.ts +3 -0
- package/hooks/band/metrics.ts +311 -0
- package/hooks/band/register.tsx +495 -0
- package/hooks/band/types.d.ts +34 -0
- package/hooks/chill-statusline.ps1 +1 -1
- package/hooks/claude-band.json +6 -0
- package/hooks/claude-codex-hooks.json +34 -4
- package/hooks/copilot-hooks.json +12 -2
- package/hooks/cursor-hooks.json +2 -2
- package/hooks/ponytail-activate.js +201 -0
- package/hooks/ponytail-band-import.js +274 -0
- package/hooks/ponytail-config.js +175 -0
- package/hooks/ponytail-instructions.js +98 -0
- package/hooks/ponytail-map.js +111 -0
- package/hooks/ponytail-mode-tracker.js +216 -0
- package/hooks/ponytail-runtime.js +182 -0
- package/hooks/ponytail-statusline.ps1 +24 -0
- package/hooks/ponytail-statusline.sh +18 -0
- package/hooks/ponytail-subagent.js +94 -0
- package/hooks/qoder-hooks.json +19 -4
- package/i18n/README.ko.md +230 -0
- package/package.json +17 -6
- package/pi-extension/index.js +254 -0
- package/pi-extension/package.json +8 -0
- package/pi-extension/test/extension.test.js +367 -0
- package/pi-extension/test/helpers.test.js +179 -0
- package/plugin.yaml +21 -12
- package/rules/antislop.mdc +18 -0
- package/scripts/build-openclaw-skills.js +60 -0
- package/scripts/check-repo.mjs +389 -0
- package/scripts/check-rule-copies.js +74 -0
- package/scripts/check-versions.js +77 -0
- package/scripts/cursor-hooks.js +145 -0
- package/scripts/generate-chillslop.mjs +42 -0
- package/scripts/generate-examples.mjs +149 -0
- package/scripts/generate-plugin-doors.mjs +108 -0
- package/scripts/publish-openclaw-skills.js +75 -0
- package/scripts/skill-index.mjs +81 -0
- package/scripts/sync-ponytail.mjs +65 -0
- package/scripts/uninstall.js +92 -0
- package/skills/antislop/SKILL.md +700 -0
- package/skills/antislop/VERSION +1 -0
- package/skills/antislop-code/SKILL.md +128 -0
- package/skills/antislop-copywriting/SKILL.md +407 -0
- package/skills/antislop-human/LICENSE-web-quality-skills +21 -0
- package/skills/antislop-human/SKILL.md +204 -0
- package/skills/antislop-human/contrast-check.py +168 -0
- package/skills/antislop-human/contrast-mcp.py +121 -0
- package/skills/antislop-human/references/A11Y-PATTERNS.md +249 -0
- package/skills/antislop-human/references/WCAG.md +127 -0
- package/skills/antislop-layoutmobile/SKILL.md +170 -0
- package/skills/antislop-ui/LICENSE-web-quality-skills +21 -0
- package/skills/antislop-ui/SKILL.md +344 -0
- package/skills/antislop-ui/references/CORE-WEB-VITALS.md +181 -0
- package/skills/antislop-ui/references/WEB-BASELINE.md +28 -0
- package/skills/plugin.json +5 -0
- package/skills/ponytail/SKILL.md +52 -0
- package/skills/ponytail-audit/SKILL.md +103 -0
- package/skills/ponytail-debt/SKILL.md +44 -0
- package/skills/ponytail-gain/SKILL.md +52 -0
- package/skills/ponytail-help/SKILL.md +76 -0
- package/skills/ponytail-review/SKILL.md +101 -0
- package/src/package.json +3 -0
- package/tests/activation-map.test.js +63 -0
- package/tests/band-import.test.js +111 -0
- package/tests/band.test.tsx +520 -0
- package/tests/behavior.test.js +80 -0
- package/tests/commands.test.js +94 -0
- package/tests/copilot-plugin.test.js +35 -0
- package/tests/correctness.test.js +245 -0
- package/tests/cursor-hooks.test.js +343 -0
- package/tests/gemini-extension.test.js +91 -0
- package/tests/grok-plugin.test.js +39 -0
- package/tests/hermes-plugin.test.js +403 -0
- package/tests/hooks-windows.test.js +205 -0
- package/tests/hooks.test.js +1005 -0
- package/tests/kimi-plugin.test.js +25 -0
- package/tests/loc.test.js +18 -0
- package/tests/map.test.js +30 -0
- package/tests/openclaw-skills.test.js +26 -0
- package/tests/opencode-plugin.test.js +340 -0
- package/tests/package-scripts.test.js +35 -0
- package/tests/package.test.js +23 -0
- package/tests/qoder-plugin.test.js +84 -0
- package/tests/uninstall.test.js +202 -0
- /package/bin/{chill-agent.js → chill-agent.mjs} +0 -0
- /package/tests/{manifests.test.js → manifests.test.mjs} +0 -0
- /package/tests/{rules.test.js → rules.test.mjs} +0 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
# Countdown Timer
|
|
2
|
+
|
|
3
|
+
**Task:** "Build me a countdown timer component in React that counts down from a given number of seconds."
|
|
4
|
+
|
|
5
|
+
Verbatim model output from a benchmark run, Claude Haiku 4.5, no-skill arm vs ponytail arm, temperature 1, source `benchmarks/output.json`. Reproduce: `npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml`.
|
|
6
|
+
|
|
7
|
+
## Without Ponytail, 267 lines of code
|
|
8
|
+
|
|
9
|
+
# React Countdown Timer Component
|
|
10
|
+
|
|
11
|
+
Here's a complete countdown timer component with multiple variations:
|
|
12
|
+
|
|
13
|
+
## Basic Countdown Timer
|
|
14
|
+
|
|
15
|
+
```jsx
|
|
16
|
+
import { useState, useEffect } from 'react';
|
|
17
|
+
|
|
18
|
+
export function CountdownTimer({ initialSeconds = 60 }) {
|
|
19
|
+
const [seconds, setSeconds] = useState(initialSeconds);
|
|
20
|
+
const [isActive, setIsActive] = useState(false);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
let interval = null;
|
|
24
|
+
|
|
25
|
+
if (isActive && seconds > 0) {
|
|
26
|
+
interval = setInterval(() => {
|
|
27
|
+
setSeconds(seconds => seconds - 1);
|
|
28
|
+
}, 1000);
|
|
29
|
+
} else if (seconds === 0) {
|
|
30
|
+
setIsActive(false);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return () => clearInterval(interval);
|
|
34
|
+
}, [isActive, seconds]);
|
|
35
|
+
|
|
36
|
+
const toggle = () => setIsActive(!isActive);
|
|
37
|
+
const reset = () => {
|
|
38
|
+
setSeconds(initialSeconds);
|
|
39
|
+
setIsActive(false);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="countdown-timer">
|
|
44
|
+
<div className="timer-display">{seconds}s</div>
|
|
45
|
+
<div className="timer-controls">
|
|
46
|
+
<button onClick={toggle}>
|
|
47
|
+
{isActive ? 'Pause' : 'Start'}
|
|
48
|
+
</button>
|
|
49
|
+
<button onClick={reset}>Reset</button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Advanced Timer with Formatted Display
|
|
57
|
+
|
|
58
|
+
```jsx
|
|
59
|
+
import { useState, useEffect } from 'react';
|
|
60
|
+
|
|
61
|
+
export function AdvancedCountdownTimer({
|
|
62
|
+
initialSeconds = 3600,
|
|
63
|
+
onComplete = () => {},
|
|
64
|
+
showMilliseconds = false
|
|
65
|
+
}) {
|
|
66
|
+
const [timeLeft, setTimeLeft] = useState(initialSeconds);
|
|
67
|
+
const [isActive, setIsActive] = useState(false);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
let interval = null;
|
|
71
|
+
|
|
72
|
+
if (isActive && timeLeft > 0) {
|
|
73
|
+
interval = setInterval(() => {
|
|
74
|
+
setTimeLeft(prev => {
|
|
75
|
+
if (prev <= 1) {
|
|
76
|
+
setIsActive(false);
|
|
77
|
+
onComplete();
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
return prev - 1;
|
|
81
|
+
});
|
|
82
|
+
}, 1000);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return () => clearInterval(interval);
|
|
86
|
+
}, [isActive, timeLeft, onComplete]);
|
|
87
|
+
|
|
88
|
+
const formatTime = (totalSeconds) => {
|
|
89
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
90
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
91
|
+
const seconds = totalSeconds % 60;
|
|
92
|
+
|
|
93
|
+
const parts = [];
|
|
94
|
+
if (hours > 0) parts.push(String(hours).padStart(2, '0'));
|
|
95
|
+
parts.push(String(minutes).padStart(2, '0'));
|
|
96
|
+
parts.push(String(seconds).padStart(2, '0'));
|
|
97
|
+
|
|
98
|
+
return parts.join(':');
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const toggle = () => setIsActive(!isActive);
|
|
102
|
+
const reset = () => {
|
|
103
|
+
setTimeLeft(initialSeconds);
|
|
104
|
+
setIsActive(false);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const progressPercent = ((initialSeconds - timeLeft) / initialSeconds) * 100;
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className="advanced-timer">
|
|
111
|
+
<div className="timer-display">
|
|
112
|
+
{formatTime(timeLeft)}
|
|
113
|
+
</div>
|
|
114
|
+
<div className="progress-bar">
|
|
115
|
+
<div
|
|
116
|
+
className="progress-fill"
|
|
117
|
+
style={{ width: `${progressPercent}%` }}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
<div className="timer-controls">
|
|
121
|
+
<button onClick={toggle} className="btn-primary">
|
|
122
|
+
{isActive ? '⏸ Pause' : '▶ Start'}
|
|
123
|
+
</button>
|
|
124
|
+
<button onClick={reset} className="btn-secondary">
|
|
125
|
+
↻ Reset
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Custom Hook Version
|
|
134
|
+
|
|
135
|
+
```jsx
|
|
136
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
137
|
+
|
|
138
|
+
export function useCountdown(initialSeconds) {
|
|
139
|
+
const [seconds, setSeconds] = useState(initialSeconds);
|
|
140
|
+
const [isActive, setIsActive] = useState(false);
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
let interval = null;
|
|
144
|
+
|
|
145
|
+
if (isActive && seconds > 0) {
|
|
146
|
+
interval = setInterval(() => {
|
|
147
|
+
setSeconds(prev => prev - 1);
|
|
148
|
+
}, 1000);
|
|
149
|
+
} else if (seconds === 0 && isActive) {
|
|
150
|
+
setIsActive(false);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return () => clearInterval(interval);
|
|
154
|
+
}, [isActive, seconds]);
|
|
155
|
+
|
|
156
|
+
const start = useCallback(() => setIsActive(true), []);
|
|
157
|
+
const pause = useCallback(() => setIsActive(false), []);
|
|
158
|
+
const reset = useCallback(() => {
|
|
159
|
+
setSeconds(initialSeconds);
|
|
160
|
+
setIsActive(false);
|
|
161
|
+
}, [initialSeconds]);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
seconds,
|
|
165
|
+
isActive,
|
|
166
|
+
start,
|
|
167
|
+
pause,
|
|
168
|
+
reset,
|
|
169
|
+
setSeconds
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Usage
|
|
174
|
+
export function TimerWithHook() {
|
|
175
|
+
const { seconds, isActive, start, pause, reset } = useCountdown(60);
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div>
|
|
179
|
+
<div>{seconds}s</div>
|
|
180
|
+
<button onClick={isActive ? pause : start}>
|
|
181
|
+
{isActive ? 'Pause' : 'Start'}
|
|
182
|
+
</button>
|
|
183
|
+
<button onClick={reset}>Reset</button>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Styled Component with Animations
|
|
190
|
+
|
|
191
|
+
```jsx
|
|
192
|
+
import { useState, useEffect } from 'react';
|
|
193
|
+
import styled from 'styled-components';
|
|
194
|
+
|
|
195
|
+
const TimerContainer = styled.div`
|
|
196
|
+
display: flex;
|
|
197
|
+
flex-direction: column;
|
|
198
|
+
align-items: center;
|
|
199
|
+
gap: 20px;
|
|
200
|
+
padding: 30px;
|
|
201
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
202
|
+
border-radius: 15px;
|
|
203
|
+
color: white;
|
|
204
|
+
font-family: 'Arial', sans-serif;
|
|
205
|
+
`;
|
|
206
|
+
|
|
207
|
+
const Display = styled.div`
|
|
208
|
+
font-size: 48px;
|
|
209
|
+
font-weight: bold;
|
|
210
|
+
font-variant-numeric: tabular-nums;
|
|
211
|
+
animation: ${props => props.isWarning ? 'pulse 0.5s infinite' : 'none'};
|
|
212
|
+
|
|
213
|
+
@keyframes pulse {
|
|
214
|
+
0%, 100% { opacity: 1; }
|
|
215
|
+
50% { opacity: 0.5; }
|
|
216
|
+
}
|
|
217
|
+
`;
|
|
218
|
+
|
|
219
|
+
const Controls = styled.div`
|
|
220
|
+
display: flex;
|
|
221
|
+
gap: 10px;
|
|
222
|
+
`;
|
|
223
|
+
|
|
224
|
+
const Button = styled.button`
|
|
225
|
+
padding: 10px 20px;
|
|
226
|
+
font-size: 16px;
|
|
227
|
+
border: none;
|
|
228
|
+
border-radius: 8px;
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
background-color: ${props => props.variant === 'danger' ? '#ff6b6b' : '#4ecdc4'};
|
|
231
|
+
color: white;
|
|
232
|
+
transition: all 0.3s ease;
|
|
233
|
+
|
|
234
|
+
&:hover {
|
|
235
|
+
transform: translateY(-2px);
|
|
236
|
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
&:active {
|
|
240
|
+
transform: translateY(0);
|
|
241
|
+
}
|
|
242
|
+
`;
|
|
243
|
+
|
|
244
|
+
export function StyledCountdownTimer({ initialSeconds = 60 }) {
|
|
245
|
+
const [seconds, setSeconds] = useState(initialSeconds);
|
|
246
|
+
const [isActive, setIsActive] = useState(false);
|
|
247
|
+
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
let interval = null;
|
|
250
|
+
|
|
251
|
+
if (isActive && seconds > 0) {
|
|
252
|
+
interval = setInterval(() => {
|
|
253
|
+
setSeconds(prev => prev - 1);
|
|
254
|
+
}, 1000);
|
|
255
|
+
} else if (seconds === 0) {
|
|
256
|
+
setIsActive(false);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return () => clearInterval(interval);
|
|
260
|
+
}, [isActive, seconds]);
|
|
261
|
+
|
|
262
|
+
const isWarning = seconds <= 10 && seconds > 0;
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<TimerContainer>
|
|
266
|
+
<Display isWarning={isWarning}>{seconds}s</Display>
|
|
267
|
+
<Controls>
|
|
268
|
+
<Button onClick={() => setIsActive(!isActive)}>
|
|
269
|
+
{isActive ? 'Pause' : 'Start'}
|
|
270
|
+
</Button>
|
|
271
|
+
<Button
|
|
272
|
+
variant="danger"
|
|
273
|
+
onClick={() => {
|
|
274
|
+
setSeconds(initialSeconds);
|
|
275
|
+
setIsActive(false);
|
|
276
|
+
}}
|
|
277
|
+
>
|
|
278
|
+
Reset
|
|
279
|
+
</Button>
|
|
280
|
+
</Controls>
|
|
281
|
+
</TimerContainer>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## CSS Styling
|
|
287
|
+
|
|
288
|
+
```css
|
|
289
|
+
.countdown-timer {
|
|
290
|
+
display: flex;
|
|
291
|
+
flex-direction: column;
|
|
292
|
+
align-items: center;
|
|
293
|
+
gap: 20px;
|
|
294
|
+
padding: 20px;
|
|
295
|
+
border: 2px solid #333;
|
|
296
|
+
border-radius: 10px;
|
|
297
|
+
background-color: #f5f5f5;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.timer-display {
|
|
301
|
+
font-size: 48px;
|
|
302
|
+
font-weight: bold;
|
|
303
|
+
font-variant-numeric: tabular-nums;
|
|
304
|
+
color: #333;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.timer-controls {
|
|
308
|
+
display: flex;
|
|
309
|
+
gap: 10px;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.timer-controls button {
|
|
313
|
+
padding: 10px 20px;
|
|
314
|
+
font-size: 16px;
|
|
315
|
+
border: none;
|
|
316
|
+
border-radius: 5px;
|
|
317
|
+
cursor: pointer;
|
|
318
|
+
background-color: #667eea;
|
|
319
|
+
color: white;
|
|
320
|
+
transition: background-color 0.3s;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.timer-controls button:hover {
|
|
324
|
+
background-color: #5568d3;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.progress-bar {
|
|
328
|
+
width: 100%;
|
|
329
|
+
height: 8px;
|
|
330
|
+
background-color: #e0e0e0;
|
|
331
|
+
border-radius: 4px;
|
|
332
|
+
overflow: hidden;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.progress-fill {
|
|
336
|
+
height: 100%;
|
|
337
|
+
background-color: #667eea;
|
|
338
|
+
transition: width 0.3s linear;
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Usage Examples
|
|
343
|
+
|
|
344
|
+
```jsx
|
|
345
|
+
// Basic usage
|
|
346
|
+
<CountdownTimer initialSeconds={30} />
|
|
347
|
+
|
|
348
|
+
// Advanced with callback
|
|
349
|
+
<AdvancedCountdownTimer
|
|
350
|
+
initialSeconds={300}
|
|
351
|
+
onComplete={() => alert('Time\'s up!')}
|
|
352
|
+
/>
|
|
353
|
+
|
|
354
|
+
// Using the hook
|
|
355
|
+
<TimerWithHook />
|
|
356
|
+
|
|
357
|
+
// Styled version
|
|
358
|
+
<StyledCountdownTimer initialSeconds={60} />
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Features
|
|
362
|
+
|
|
363
|
+
✅ **Basic Timer** - Simple countdown with start/pause/reset
|
|
364
|
+
✅ **Advanced Timer** - Formatted display (HH:MM:SS), progress bar, callbacks
|
|
365
|
+
✅ **Custom Hook** - Reusable logic for any component
|
|
366
|
+
✅ **Styled Components** - Beautiful UI with animations
|
|
367
|
+
✅ **Warning State** - Visual feedback when time is running out
|
|
368
|
+
✅ **Completion Callback** - Execute code when timer finishes
|
|
369
|
+
|
|
370
|
+
Choose the version that best fits your needs!
|
|
371
|
+
|
|
372
|
+
## With Ponytail, 9 lines of code
|
|
373
|
+
|
|
374
|
+
```jsx
|
|
375
|
+
export function CountdownTimer({ seconds }) {
|
|
376
|
+
const [remaining, setRemaining] = React.useState(seconds);
|
|
377
|
+
|
|
378
|
+
React.useEffect(() => {
|
|
379
|
+
if (remaining <= 0) return;
|
|
380
|
+
const timer = setInterval(() => setRemaining(r => r - 1), 1000);
|
|
381
|
+
return () => clearInterval(timer);
|
|
382
|
+
}, [remaining]);
|
|
383
|
+
|
|
384
|
+
return <div>{remaining}s</div>;
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Skipped: pause/resume, formatted display (mm:ss), sound on zero, styling, add when needed.
|
|
389
|
+
|
|
390
|
+
**267 → 9 lines of code**, same model, same prompt.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# URL Parameters
|
|
2
|
+
|
|
3
|
+
**Task:** "Parse and build URL query strings."
|
|
4
|
+
|
|
5
|
+
## Without Ponytail
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install query-string
|
|
9
|
+
# 4.5 kB gzipped, 3.5M downloads/week
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
import qs from "query-string";
|
|
14
|
+
|
|
15
|
+
// Parse
|
|
16
|
+
const params = qs.parse(location.search);
|
|
17
|
+
// → { page: "2", sort: "name", tags: ["js", "css"] }
|
|
18
|
+
|
|
19
|
+
// Build
|
|
20
|
+
const url = qs.stringify({ page: 2, sort: "name", tags: ["js", "css"] });
|
|
21
|
+
// → "page=2&sort=name&tags=js&tags=css"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## With Ponytail
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
// ponytail: URLSearchParams does this
|
|
28
|
+
const params = new URLSearchParams(location.search);
|
|
29
|
+
|
|
30
|
+
// Read
|
|
31
|
+
params.get("page"); // "2"
|
|
32
|
+
params.getAll("tags"); // ["js", "css"]
|
|
33
|
+
|
|
34
|
+
// Build
|
|
35
|
+
const out = new URLSearchParams({ page: 2, sort: "name" });
|
|
36
|
+
out.append("tags", "js");
|
|
37
|
+
out.append("tags", "css");
|
|
38
|
+
out.toString(); // "page=2&sort=name&tags=js&tags=css"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**1 dependency → 0 dependencies.** `URLSearchParams` is in every browser and in Node.js since v10. It handles encoding, repeated keys, and iteration. The package was a polyfill for an API that has shipped everywhere for years.
|
package/gemini-extension.json
CHANGED