|
33 | 33 | ``` |
34 | 34 | --> |
35 | 35 | <script lang="ts"> |
36 | | - import { MotionButton } from '@humanspeak/svelte-motion' |
| 36 | + import { AnimatePresence, MotionButton, MotionSpan } from '@humanspeak/svelte-motion' |
37 | 37 | import CheckIcon from '@lucide/svelte/icons/check' |
38 | 38 | import CopyIcon from '@lucide/svelte/icons/copy' |
| 39 | + import { onDestroy } from 'svelte' |
39 | 40 |
|
40 | 41 | interface CodeSample { |
41 | 42 | /** Stable identifier — surfaces as the small-caps tag on the left of |
|
69 | 70 | columns === 'auto' ? Math.min(samples.length, 3) : Math.max(1, columns) |
70 | 71 | ) |
71 | 72 |
|
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. |
76 | 76 | let copiedId = $state<string | null>(null) |
77 | 77 | let copyResetTimer: ReturnType<typeof setTimeout> | null = null |
78 | 78 |
|
| 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 | +
|
79 | 88 | const copy = async (sample: CodeSample) => { |
| 89 | + showCopyFeedback(sample.id) |
80 | 90 | if (typeof navigator === 'undefined' || !navigator.clipboard) return |
81 | 91 | try { |
82 | 92 | 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) |
89 | 93 | } catch { |
90 | 94 | /* clipboard blocked — fail quiet, the user can select + copy */ |
91 | 95 | } |
92 | 96 | } |
93 | 97 |
|
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 } |
96 | 105 | </script> |
97 | 106 |
|
98 | 107 | <div class="dk-coderef" style="--dk-coderef-cols: {colCount}"> |
|
105 | 114 | </div> |
106 | 115 | <MotionButton |
107 | 116 | type="button" |
108 | | - class="dk-coderef-copy" |
| 117 | + class={copiedId === sample.id ? 'dk-coderef-copy copied' : 'dk-coderef-copy'} |
109 | 118 | aria-label="Copy {sample.label} snippet" |
110 | 119 | onclick={() => copy(sample)} |
111 | | - whileTap={tapScale} |
112 | | - whileHover={hoverScale} |
| 120 | + whileTap={copyPress} |
| 121 | + whileHover={copyHover} |
113 | 122 | > |
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> |
121 | 150 | </MotionButton> |
122 | 151 | </header> |
123 | 152 | <div class="dk-coderef-code"> |
|
189 | 218 | .dk-coderef :global(.dk-coderef-copy) { |
190 | 219 | display: inline-flex; |
191 | 220 | 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; |
193 | 227 | padding: 4px 9px; |
194 | 228 | border: 1px solid var(--brut-rule); |
195 | 229 | background: var(--brut-bg); |
196 | 230 | color: var(--brut-ink-2); |
197 | 231 | font-family: 'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace, monospace; |
198 | 232 | font-size: 10.5px; |
| 233 | + line-height: 1.2; |
199 | 234 | text-transform: lowercase; |
200 | 235 | letter-spacing: 0; |
201 | 236 | cursor: pointer; |
|
209 | 244 | color: var(--brut-accent); |
210 | 245 | border-color: var(--brut-accent); |
211 | 246 | } |
| 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 | + } |
212 | 270 |
|
213 | 271 | /* ── Code area — fills the cell so the scrollbar sits flush ───── */ |
214 | 272 | .dk-coderef-code { |
|
0 commit comments