Skip to content

Commit 714a84a

Browse files
committed
feat: dogfood motion in docs controls
1 parent 8d0a50f commit 714a84a

6 files changed

Lines changed: 387 additions & 44 deletions

File tree

src/lib/actions/enhanceCodeBlocks.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -164,13 +164,14 @@ const enhance = (container: HTMLElement) => {
164164
copyBtn.addEventListener('click', () => {
165165
const code = getCode(block)
166166
if (!code) return
167-
navigator.clipboard.writeText(code).then(() => {
168-
playCopySuccess(copyBtn, iconCopy, iconCheck)
169-
// Hold the success state ~2s. `scheduleReset` clears any
170-
// prior pending timer so a second click inside the window
171-
// restarts the countdown from now, rather than letting the
172-
// first click's reset snap the icon back early.
173-
motionHandle.scheduleReset(2000)
167+
playCopySuccess(copyBtn, iconCopy, iconCheck)
168+
// Hold the success state ~2s. `scheduleReset` clears any
169+
// prior pending timer so a second click inside the window
170+
// restarts the countdown from now, rather than letting the
171+
// first click's reset snap the icon back early.
172+
motionHandle.scheduleReset(2000)
173+
navigator.clipboard?.writeText(code).catch(() => {
174+
/* clipboard blocked — fail quiet, the user can select + copy */
174175
})
175176
})
176177

src/lib/components/CodeReferenceV2.svelte

Lines changed: 82 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@
3333
```
3434
-->
3535
<script lang="ts">
36-
import { MotionButton } from '@humanspeak/svelte-motion'
36+
import { AnimatePresence, MotionButton, MotionSpan } from '@humanspeak/svelte-motion'
3737
import CheckIcon from '@lucide/svelte/icons/check'
3838
import CopyIcon from '@lucide/svelte/icons/copy'
39+
import { onDestroy } from 'svelte'
3940
4041
interface CodeSample {
4142
/** Stable identifier — surfaces as the small-caps tag on the left of
@@ -69,30 +70,38 @@
6970
columns === 'auto' ? Math.min(samples.length, 3) : Math.max(1, columns)
7071
)
7172
72-
// Track which cell most recently completed a successful copy so we can
73-
// swap the icon to a check and reset after 1.6s. Keyed by sample id so
74-
// multiple cells can show "copied" independently if the user clicks
75-
// through them quickly.
73+
// Track which cell most recently requested a copy so we can swap the icon
74+
// to a check and reset after 1.6s. Keyed by sample id so multiple cells can
75+
// show "copied" independently if the user clicks through them quickly.
7676
let copiedId = $state<string | null>(null)
7777
let copyResetTimer: ReturnType<typeof setTimeout> | null = null
7878
79+
const showCopyFeedback = (sampleId: string) => {
80+
copiedId = sampleId
81+
if (copyResetTimer) clearTimeout(copyResetTimer)
82+
copyResetTimer = setTimeout(() => {
83+
copiedId = null
84+
copyResetTimer = null
85+
}, 1600)
86+
}
87+
7988
const copy = async (sample: CodeSample) => {
89+
showCopyFeedback(sample.id)
8090
if (typeof navigator === 'undefined' || !navigator.clipboard) return
8191
try {
8292
await navigator.clipboard.writeText(sample.code)
83-
copiedId = sample.id
84-
if (copyResetTimer) clearTimeout(copyResetTimer)
85-
copyResetTimer = setTimeout(() => {
86-
copiedId = null
87-
copyResetTimer = null
88-
}, 1600)
8993
} catch {
9094
/* clipboard blocked — fail quiet, the user can select + copy */
9195
}
9296
}
9397
94-
const tapScale = { scale: 0.94 }
95-
const hoverScale = { scale: 1.05 }
98+
onDestroy(() => {
99+
if (copyResetTimer) clearTimeout(copyResetTimer)
100+
})
101+
102+
const copyPress = { scale: 0.96 }
103+
const copyHover = { scale: 1.03 }
104+
const copyStateTransition = { duration: 0.16 }
96105
</script>
97106

98107
<div class="dk-coderef" style="--dk-coderef-cols: {colCount}">
@@ -105,19 +114,39 @@
105114
</div>
106115
<MotionButton
107116
type="button"
108-
class="dk-coderef-copy"
117+
class={copiedId === sample.id ? 'dk-coderef-copy copied' : 'dk-coderef-copy'}
109118
aria-label="Copy {sample.label} snippet"
110119
onclick={() => copy(sample)}
111-
whileTap={tapScale}
112-
whileHover={hoverScale}
120+
whileTap={copyPress}
121+
whileHover={copyHover}
113122
>
114-
{#if copiedId === sample.id}
115-
<CheckIcon size={11} />
116-
<span>copied</span>
117-
{:else}
118-
<CopyIcon size={11} />
119-
<span>copy</span>
120-
{/if}
123+
<AnimatePresence initial={false}>
124+
{#if copiedId === sample.id}
125+
<MotionSpan
126+
key="copied"
127+
class="dk-coderef-copy-state copied-state"
128+
initial={{ opacity: 1, y: 0 }}
129+
animate={{ opacity: 1, y: 0 }}
130+
exit={{ opacity: 0, y: 0 }}
131+
transition={copyStateTransition}
132+
>
133+
<CheckIcon size={11} />
134+
<span>copied</span>
135+
</MotionSpan>
136+
{:else}
137+
<MotionSpan
138+
key="copy"
139+
class="dk-coderef-copy-state copy-state"
140+
initial={{ opacity: 1, y: 0 }}
141+
animate={{ opacity: 1, y: 0 }}
142+
exit={{ opacity: 0, y: 0 }}
143+
transition={copyStateTransition}
144+
>
145+
<CopyIcon size={11} />
146+
<span>copy</span>
147+
</MotionSpan>
148+
{/if}
149+
</AnimatePresence>
121150
</MotionButton>
122151
</header>
123152
<div class="dk-coderef-code">
@@ -189,13 +218,19 @@
189218
.dk-coderef :global(.dk-coderef-copy) {
190219
display: inline-flex;
191220
align-items: center;
192-
gap: 5px;
221+
justify-content: center;
222+
box-sizing: border-box;
223+
position: relative;
224+
overflow: hidden;
225+
width: 74px;
226+
height: 24px;
193227
padding: 4px 9px;
194228
border: 1px solid var(--brut-rule);
195229
background: var(--brut-bg);
196230
color: var(--brut-ink-2);
197231
font-family: 'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace, monospace;
198232
font-size: 10.5px;
233+
line-height: 1.2;
199234
text-transform: lowercase;
200235
letter-spacing: 0;
201236
cursor: pointer;
@@ -209,6 +244,29 @@
209244
color: var(--brut-accent);
210245
border-color: var(--brut-accent);
211246
}
247+
.dk-coderef :global(.dk-coderef-copy.copied) {
248+
border-color: var(--brut-accent);
249+
background: var(
250+
--brut-accent-soft,
251+
color-mix(in srgb, var(--brut-accent) 10%, transparent)
252+
);
253+
}
254+
.dk-coderef :global(.dk-coderef-copy-state) {
255+
display: inline-flex;
256+
align-items: center;
257+
justify-content: center;
258+
gap: 5px;
259+
position: absolute;
260+
inset: 0;
261+
line-height: 1.2;
262+
white-space: nowrap;
263+
}
264+
.dk-coderef :global(.dk-coderef-copy-state.copy-state) {
265+
color: var(--brut-ink-2);
266+
}
267+
.dk-coderef :global(.dk-coderef-copy-state.copied-state) {
268+
color: var(--brut-accent);
269+
}
212270
213271
/* ── Code area — fills the cell so the scrollbar sits flush ───── */
214272
.dk-coderef-code {

src/lib/components/EmbeddedExampleV2.svelte

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
example route.
2929
-->
3030
<script lang="ts">
31-
import { MotionA, MotionButton } from '@humanspeak/svelte-motion'
31+
import { MotionA, MotionButton, MotionSpan } from '@humanspeak/svelte-motion'
3232
import ExternalLink from '@lucide/svelte/icons/external-link'
3333
import RotateCw from '@lucide/svelte/icons/rotate-cw'
3434
import type { Snippet } from 'svelte'
@@ -89,8 +89,10 @@
8989
})
9090
9191
let refreshId = $state(0)
92+
let refreshSpinKey = $state(0)
9293
const refresh = () => {
9394
refreshId++
95+
refreshSpinKey++
9496
}
9597
9698
const tapScale = { scale: 0.96 }
@@ -158,7 +160,16 @@
158160
aria-label="Reset demo"
159161
title="Reset example"
160162
>
161-
<RotateCw size={11} />
163+
{#key refreshSpinKey}
164+
<MotionSpan
165+
class="dk-em-reset-icon"
166+
initial={{ rotate: 0 }}
167+
animate={{ rotate: refreshSpinKey === 0 ? 0 : 360 }}
168+
transition={{ duration: 0.42, ease: [0.16, 1, 0.3, 1] }}
169+
>
170+
<RotateCw size={11} />
171+
</MotionSpan>
172+
{/key}
162173
<span>reset</span>
163174
</MotionButton>
164175
{/if}
@@ -297,6 +308,10 @@
297308
color: var(--brut-accent);
298309
border-color: var(--brut-accent);
299310
}
311+
.dk-em :global(.dk-em-reset-icon) {
312+
display: inline-flex;
313+
transform-origin: center;
314+
}
300315
301316
/* Body — the demo claims the panel. */
302317
.dk-em-body {

src/lib/components/Example.svelte

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
Uses MotionButton/MotionA for hover/tap effects.
55
-->
66
<script lang="ts">
7-
import { MotionA, MotionButton } from '@humanspeak/svelte-motion'
7+
import { MotionA, MotionButton, MotionSpan } from '@humanspeak/svelte-motion'
88
import ExternalLink from '@lucide/svelte/icons/external-link'
99
import LayoutGrid from '@lucide/svelte/icons/layout-grid'
1010
import RotateCw from '@lucide/svelte/icons/rotate-cw'
@@ -29,8 +29,10 @@
2929
})
3030
3131
let refreshId = $state(0)
32+
let refreshSpinKey = $state(0)
3233
const refresh = () => {
3334
refreshId++
35+
refreshSpinKey++
3436
}
3537
3638
const tapScale = { scale: 0.95 }
@@ -70,23 +72,34 @@
7072
</MotionA>
7173
{/if}
7274
{#if sourceUrl}
73-
<MotionButton
74-
onclick={() => window.open(sourceUrl, '_blank')}
75+
<MotionA
76+
href={sourceUrl}
77+
target="_blank"
78+
rel="noopener noreferrer"
7579
whileTap={tapScale}
7680
whileHover={hoverScale}
7781
class="inline-flex items-center justify-center rounded-md border border-border px-2.5 py-1.5 text-sm text-muted-foreground transition-colors hover:border-brand-500/50 hover:text-foreground"
7882
>
7983
{sourceHost}
80-
</MotionButton>
84+
</MotionA>
8185
{/if}
8286
<MotionButton
8387
onclick={refresh}
8488
whileTap={tapScale}
8589
whileHover={hoverScale}
86-
class="inline-flex h-8 w-8 items-center justify-center rounded-full border border-border text-muted-foreground transition-colors hover:border-brand-500/50 hover:text-foreground"
90+
class="legacy-reset inline-flex h-8 w-8 items-center justify-center rounded-full border border-border text-muted-foreground transition-colors hover:border-brand-500/50 hover:text-foreground"
8791
title="Reset example"
8892
>
89-
<RotateCw size={12} />
93+
{#key refreshSpinKey}
94+
<MotionSpan
95+
class="legacy-reset-icon"
96+
initial={{ rotate: 0 }}
97+
animate={{ rotate: refreshSpinKey === 0 ? 0 : 360 }}
98+
transition={{ duration: 0.42, ease: [0.16, 1, 0.3, 1] }}
99+
>
100+
<RotateCw size={12} />
101+
</MotionSpan>
102+
{/key}
90103
</MotionButton>
91104
</div>
92105
</div>
@@ -146,4 +159,8 @@
146159
transparent 1.5px
147160
);
148161
}
162+
:global(.legacy-reset-icon) {
163+
display: inline-flex;
164+
transform-origin: center;
165+
}
149166
</style>

src/lib/components/ExampleV2.svelte

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
-->
3232
<script lang="ts">
3333
import SvelteMarkdown from '@humanspeak/svelte-markdown'
34-
import { MotionA, MotionButton, MotionDiv } from '@humanspeak/svelte-motion'
34+
import { MotionA, MotionButton, MotionDiv, MotionSpan } from '@humanspeak/svelte-motion'
3535
import ExternalLink from '@lucide/svelte/icons/external-link'
3636
import RotateCw from '@lucide/svelte/icons/rotate-cw'
3737
import type { Snippet } from 'svelte'
@@ -194,8 +194,10 @@
194194
)
195195
196196
let refreshId = $state(0)
197+
let refreshSpinKey = $state(0)
197198
const refresh = () => {
198199
refreshId++
200+
refreshSpinKey++
199201
}
200202
201203
const tapScale = { scale: 0.96 }
@@ -207,7 +209,9 @@
207209
the `.lede` blocks on the homepage so the page stitches into
208210
the same sheet vocabulary as the index and /compare. -->
209211
<div class="dk-ex-lede">
210-
<div class="dk-ex-kicker">{figId}{#if tag}<span> / {tag}</span>{/if}</div>
212+
<div class="dk-ex-kicker">
213+
{figId}{#if tag}<span> / {tag}</span>{/if}
214+
</div>
211215
<h2 class="dk-ex-title">
212216
{#if titleShape.prefix}{titleShape.prefix}{/if}<span>{titleShape.accent}</span
213217
>{#if titleShape.end}<span class="end">{titleShape.end}</span>{/if}
@@ -298,7 +302,16 @@
298302
aria-label="Reset demo"
299303
title="Reset example"
300304
>
301-
<RotateCw size={11} />
305+
{#key refreshSpinKey}
306+
<MotionSpan
307+
class="dk-ex-reset-icon"
308+
initial={{ rotate: 0 }}
309+
animate={{ rotate: refreshSpinKey === 0 ? 0 : 360 }}
310+
transition={{ duration: 0.42, ease: [0.16, 1, 0.3, 1] }}
311+
>
312+
<RotateCw size={11} />
313+
</MotionSpan>
314+
{/key}
302315
<span>reset</span>
303316
</MotionButton>
304317
{/if}
@@ -590,7 +603,14 @@
590603
.dk-ex :global(.dk-ex-ctrl.active) {
591604
color: var(--brut-accent);
592605
border-color: var(--brut-accent);
593-
background: var(--brut-accent-soft, color-mix(in srgb, var(--brut-accent) 10%, transparent));
606+
background: var(
607+
--brut-accent-soft,
608+
color-mix(in srgb, var(--brut-accent) 10%, transparent)
609+
);
610+
}
611+
.dk-ex :global(.dk-ex-reset-icon) {
612+
display: inline-flex;
613+
transform-origin: center;
594614
}
595615
.dk-ex :global(.dk-ex-glyph) {
596616
font-family: 'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace, monospace;

0 commit comments

Comments
 (0)