Propagate shared copy-button component and clipboard helper.
Deploy Ladill Give / deploy (push) Successful in 44s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-03 23:03:57 +00:00
co-authored by Cursor
parent 8058362bf4
commit e5e380bee9
3 changed files with 88 additions and 0 deletions
+2
View File
@@ -8,6 +8,7 @@ registerLadillSearchShortcut();
import Alpine from 'alpinejs'; import Alpine from 'alpinejs';
import { registerLadillClipboard } from './ladill-clipboard';
import collapse from '@alpinejs/collapse'; import collapse from '@alpinejs/collapse';
import QRCodeStyling from 'qr-code-styling'; import QRCodeStyling from 'qr-code-styling';
window.QRCodeStyling = QRCodeStyling; window.QRCodeStyling = QRCodeStyling;
@@ -15,6 +16,7 @@ import qrcode from 'qrcode-generator';
window.qrcode = qrcode; window.qrcode = qrcode;
Alpine.plugin(collapse); Alpine.plugin(collapse);
registerLadillClipboard(Alpine);
Alpine.data('notificationDropdown', (config = {}) => ({ Alpine.data('notificationDropdown', (config = {}) => ({
open: false, open: false,
+57
View File
@@ -0,0 +1,57 @@
const COPY_FEEDBACK_MS = 2000;
export async function writeClipboardText(text) {
const value = String(text ?? '');
if (!value) {
return false;
}
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(value);
return true;
}
} catch {
// fall through to legacy copy
}
try {
const textarea = document.createElement('textarea');
textarea.value = value;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
const ok = document.execCommand('copy');
document.body.removeChild(textarea);
return ok;
} catch {
return false;
}
}
export function registerLadillClipboard(Alpine) {
Alpine.data('copyButton', (text = '') => ({
copied: false,
text: text ?? '',
resetTimer: null,
async copy() {
const ok = await writeClipboardText(this.text);
if (!ok) {
return;
}
this.copied = true;
if (this.resetTimer) {
clearTimeout(this.resetTimer);
}
this.resetTimer = setTimeout(() => {
this.copied = false;
}, COPY_FEEDBACK_MS);
},
}));
}
@@ -0,0 +1,29 @@
@props([
'text' => '',
'label' => 'Copy',
'copiedLabel' => 'Copied!',
'icon' => false,
'copiedClass' => '',
])
<button
type="button"
x-data="copyButton(@js($text))"
@click="copy()"
:title="copied ? @js($copiedLabel) : @js($icon ? 'Copy' : $label)"
:class="copied ? @js($copiedClass) : ''"
{{ $attributes->class($icon ? 'inline-flex shrink-0 items-center justify-center' : '') }}
>
@if ($icon)
<svg x-show="!copied" class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
<svg x-show="copied" x-cloak class="h-4 w-4 text-emerald-600" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/>
</svg>
@elseif ($slot->isNotEmpty())
{{ $slot }}
@else
<span x-text="copied ? @js($copiedLabel) : @js($label)"></span>
@endif
</button>