diff --git a/src/app/globals.css b/src/app/globals.css index ad33682..5c1315a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -351,28 +351,34 @@ code { position: absolute; z-index: 2; inset: 0; + width: 100%; + height: 100%; + overflow: visible; pointer-events: none; } -.duck-hot-smoke > span { - position: absolute; - width: 0.42rem; - height: 0.42rem; - border: 1px solid rgba(242, 236, 221, 0.42); - border-radius: 50%; +.duck-hot-smoke path { + fill: none; + stroke: #f2ecdd; + stroke-width: 4.8; + stroke-linecap: round; + stroke-linejoin: round; opacity: 0; - animation: duck-hot-smoke 1.25s ease-out infinite; + filter: blur(0.35px); + transform-box: fill-box; + transform-origin: center bottom; + animation: duck-hot-wisp 1.25s ease-out infinite; } -.duck-hot-smoke > span:nth-child(1) { left: 33%; bottom: 6%; } -.duck-hot-smoke > span:nth-child(2) { left: 38%; bottom: 7%; animation-delay: 310ms; } -.duck-hot-smoke > span:nth-child(3) { right: 38%; bottom: 7%; animation-delay: 620ms; } -.duck-hot-smoke > span:nth-child(4) { right: 33%; bottom: 6%; animation-delay: 930ms; } +.duck-hot-wisp-left { --smoke-drift: -5px; --smoke-turn: -5deg; animation-delay: -420ms; } +.duck-hot-wisp-center { --smoke-drift: 2px; --smoke-turn: 3deg; animation-delay: -140ms; } +.duck-hot-wisp-right { --smoke-drift: 5px; --smoke-turn: 5deg; animation-delay: -760ms; } -@keyframes duck-hot-smoke { - 0% { transform: translate(0, 0) scale(0.45); opacity: 0; } - 24% { opacity: 0.38; } - 100% { transform: translate(-1px, -12px) scale(1.08); opacity: 0; } +@keyframes duck-hot-wisp { + 0% { transform: translate(0, 8px) scale(0.55); opacity: 0; stroke-width: 4; } + 18% { opacity: 0.56; } + 55% { transform: translate(calc(var(--smoke-drift) * 0.35), -10px) scale(0.82) rotate(var(--smoke-turn)); opacity: 0.42; stroke-width: 5; } + 100% { transform: translate(var(--smoke-drift), -29px) scale(1.08) rotate(var(--smoke-turn)); opacity: 0; stroke-width: 5.6; } } @keyframes duck-stomp { @@ -469,7 +475,7 @@ code { .duck-mark-agitated.duck-mark-settling .duck-stomp-left, .duck-mark-agitated.duck-mark-settling .duck-stomp-right { animation: none; } .duck-mark-overheated .duck-foot-heat { animation: none; } - .duck-hot-smoke > span { animation: none; opacity: 0.35; } + .duck-hot-smoke path { animation: none; opacity: 0.4; } .hero-duck-button:hover .hero-duck, .hero-duck-button:active .hero-duck { transform: none; } html { scroll-behavior: auto; } @@ -1010,6 +1016,8 @@ code { .share-strip-inner { display: flex; align-items: center; justify-content: space-between; gap: 1.5rem; padding-block: clamp(2.5rem, 6vw, 4.2rem); } +.share-strip-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 0.75rem; } + .share-strip .eyebrow { color: var(--orange-deep); } .share-strip .eyebrow::before { border-color: var(--orange-deep); background: radial-gradient(circle, var(--orange) 0 0.1rem, transparent 0.11rem); } @@ -1117,7 +1125,7 @@ code { .behavior-byline { margin-top: 0.45rem; font-size: 0.56rem; } .behavior-footer { min-width: 0; margin-top: -0.4rem; } .share-strip-inner { display: block; } - .share-strip .button-secondary { margin-top: 1.2rem; } + .share-strip-actions { justify-content: flex-start; margin-top: 1.2rem; } .footer-compact { grid-template-columns: 1fr; gap: 0.9rem; padding-block: 2rem 1.1rem; } .footer-compact-links { justify-content: flex-start; } .footer-compact-meta { grid-column: auto; } diff --git a/src/app/page.tsx b/src/app/page.tsx index 334fc6c..8831c09 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,7 @@ import { ArrowRight } from "lucide-react"; import { getAllBehaviors, getRegistryStats } from "@/lib/registry"; import { BehaviorCatalog } from "@/components/BehaviorCatalog"; +import { CopyPromptButton } from "@/components/CopyPromptButton"; import { InteractiveDuck } from "@/components/InteractiveDuck"; import { QuackAnchor } from "@/components/QuackAction"; @@ -69,7 +70,10 @@ export default function HomePage() { Have a new move?Add it to the shelf. - Read the contributor guide + + + Read the contributor guide + > diff --git a/src/components/CopyPromptButton.tsx b/src/components/CopyPromptButton.tsx new file mode 100644 index 0000000..8ed00e7 --- /dev/null +++ b/src/components/CopyPromptButton.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { useState } from "react"; +import { Check, Copy } from "lucide-react"; +import { QuackButton } from "./QuackAction"; + +const REGISTRY_PROMPT = "Please look up https://github.com/ob1-s/uduck-registry and add my Microduck policy to the registry, then open a PR."; + +export function CopyPromptButton() { + const [copied, setCopied] = useState(false); + + function handleCopy() { + void navigator.clipboard.writeText(REGISTRY_PROMPT).then(() => setCopied(true)); + } + + return ( + + {copied ? <> Copied!> : <> 1-click prompt>} + + ); +} diff --git a/src/components/InteractiveDuck.tsx b/src/components/InteractiveDuck.tsx index 814b9d2..1088546 100644 --- a/src/components/InteractiveDuck.tsx +++ b/src/components/InteractiveDuck.tsx @@ -21,13 +21,13 @@ export function InteractiveDuck() { const agitatedTimerRef = useRef(null); const stompTimerRef = useRef(null); const stompFadeTimerRef = useRef(null); - const overheatedTimerRef = useRef(null); const soundCueTimerRef = useRef(null); const passiveOpenTimerRef = useRef(null); const passiveCloseTimerRef = useRef(null); const hoverCueRolledRef = useRef(false); const passiveHoverBoostRef = useRef(false); const passiveHoverBoostUntilRef = useRef(0); + const angryQuackCountRef = useRef(0); const nextAgitatedQuackRef = useRef(0); useEffect(() => { @@ -48,7 +48,6 @@ export function InteractiveDuck() { if (agitatedTimerRef.current !== null) window.clearTimeout(agitatedTimerRef.current); if (stompTimerRef.current !== null) window.clearInterval(stompTimerRef.current); if (stompFadeTimerRef.current !== null) window.clearTimeout(stompFadeTimerRef.current); - if (overheatedTimerRef.current !== null) window.clearTimeout(overheatedTimerRef.current); if (soundCueTimerRef.current !== null) window.clearTimeout(soundCueTimerRef.current); if (passiveOpenTimerRef.current !== null) window.clearTimeout(passiveOpenTimerRef.current); if (passiveCloseTimerRef.current !== null) window.clearTimeout(passiveCloseTimerRef.current); @@ -108,11 +107,12 @@ export function InteractiveDuck() { } function triggerQuack(agitated: boolean, quiet = false, sound: "real" | "action" = "real") { - if (!playQuack(agitated, quiet, sound)) return; + if (!playQuack(agitated, quiet, sound)) return false; setIsQuacking(true); if (mouthTimerRef.current !== null) window.clearTimeout(mouthTimerRef.current); mouthTimerRef.current = window.setTimeout(() => setIsQuacking(false), agitated ? 560 : 420); + return true; } function handleHover() { @@ -179,31 +179,25 @@ export function InteractiveDuck() { recentClicks.push(now); clickTimesRef.current = recentClicks; - const agitated = isAgitated || recentClicks.length >= 5; + const wasAgitated = isAgitated; + const agitated = wasAgitated || recentClicks.length >= 5; + if (agitated && !wasAgitated) angryQuackCountRef.current = 0; setIsAgitated(agitated); - if (agitated && !isOverheated && overheatedTimerRef.current === null) { - overheatedTimerRef.current = window.setTimeout(() => { - setIsOverheated(true); - overheatedTimerRef.current = null; - }, 2600); - } - if (agitated && stompTimerRef.current === null) { playStomp(); stompTimerRef.current = window.setInterval(playStomp, 140); } - triggerQuack(agitated); + const angryQuackPlayed = triggerQuack(agitated); + if (agitated && wasAgitated && angryQuackPlayed) { + angryQuackCountRef.current += 1; + if (angryQuackCountRef.current >= 2) setIsOverheated(true); + } if (agitated) { if (agitatedTimerRef.current !== null) window.clearTimeout(agitatedTimerRef.current); agitatedTimerRef.current = window.setTimeout(() => { - if (overheatedTimerRef.current !== null) { - window.clearTimeout(overheatedTimerRef.current); - overheatedTimerRef.current = null; - } - if (stompTimerRef.current !== null) { window.clearInterval(stompTimerRef.current); stompTimerRef.current = null; @@ -219,12 +213,13 @@ export function InteractiveDuck() { setIsOverheated(false); setIsSettling(false); clickTimesRef.current = []; + angryQuackCountRef.current = 0; nextAgitatedQuackRef.current = 0; stompFadeTimerRef.current = null; }, 140); }, 160); }, 140); - }, 2200); + }, 1800); } } @@ -244,12 +239,11 @@ export function InteractiveDuck() { )} {isOverheated && ( - - - - - - + + + + + )}