From 1e73bbf2c0a69df20d6b1420d693158fa723630c Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 15 May 2026 18:42:56 -0500 Subject: [PATCH] Polish mobile card and signature flow --- index.html | 2 +- public/manifest.webmanifest | 4 ++-- src/App.vue | 47 +++++++++++++++++++++++++++++++++---- src/style.css | 39 +++++++++++++++++++++++------- 4 files changed, 76 insertions(+), 16 deletions(-) diff --git a/index.html b/index.html index 49157a4..4c375f1 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - + diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest index 6a79cfd..1b2e316 100644 --- a/public/manifest.webmanifest +++ b/public/manifest.webmanifest @@ -8,8 +8,8 @@ "display": "standalone", "display_override": ["standalone"], "orientation": "portrait", - "background_color": "#000000", - "theme_color": "#000000", + "background_color": "#0a0a0a", + "theme_color": "#0a0a0a", "icons": [ { "src": "/images/app-icon-192.png", diff --git a/src/App.vue b/src/App.vue index 7fed431..255ea54 100644 --- a/src/App.vue +++ b/src/App.vue @@ -162,6 +162,7 @@ const isCreatingMembership = ref(false) const formError = ref('') const signatureCanvas = ref(null) const signatureHasInk = ref(false) +const signatureStrokes = ref([]) const backupFileInput = ref(null) const backupPassword = ref('') const restorePassword = ref('') @@ -216,6 +217,7 @@ const isAdminLoading = ref(false) const isAdminMenuOpen = ref(false) const mobileMenuOpen = ref(false) let isSigning = false +let activeSignatureStroke = null const form = reactive({ fullName: '', email: '', @@ -753,6 +755,8 @@ const restoreSignupDraft = () => { form.phone = sanitizeText(draft.form?.phone, MAX_PHONE_LENGTH) form.accepted = Boolean(draft.form?.accepted) form.signature = DATA_IMAGE_PATTERN.test(draft.form?.signature || '') ? draft.form.signature : '' + signatureStrokes.value = [] + activeSignatureStroke = null signatureHasInk.value = Boolean(form.signature) const restoredStep = clampSignupStep(draft.step) if (restoredStep === 1 && isPwaStandalone.value) return 2 @@ -913,6 +917,8 @@ const resetForm = () => { form.signature = '' form.accepted = false formError.value = '' + signatureStrokes.value = [] + activeSignatureStroke = null signatureHasInk.value = false } @@ -1122,12 +1128,16 @@ const syncSignatureCanvas = () => { if (!canvas) return const rect = canvas.getBoundingClientRect() + if (rect.width < 1 || rect.height < 1) { + window.requestAnimationFrame(syncSignatureCanvas) + return + } const ratio = Math.max(window.devicePixelRatio || 1, 1) - canvas.width = rect.width * ratio - canvas.height = rect.height * ratio + canvas.width = Math.round(rect.width * ratio) + canvas.height = Math.round(rect.height * ratio) const context = canvas.getContext('2d') - context.scale(ratio, ratio) + context.setTransform(ratio, 0, 0, ratio, 0, 0) context.fillStyle = '#080808' context.fillRect(0, 0, rect.width, rect.height) context.lineCap = 'round' @@ -1135,10 +1145,29 @@ const syncSignatureCanvas = () => { context.lineWidth = 2.4 context.strokeStyle = '#ffffff' + if (signatureStrokes.value.length) { + signatureStrokes.value.forEach((stroke) => { + if (!stroke.length) return + context.beginPath() + context.moveTo(stroke[0].x * rect.width, stroke[0].y * rect.height) + stroke.slice(1).forEach((point) => context.lineTo(point.x * rect.width, point.y * rect.height)) + context.stroke() + }) + form.signature = canvas.toDataURL('image/png') + signatureHasInk.value = true + return + } + if (DATA_IMAGE_PATTERN.test(form.signature)) { + const expectedSignature = form.signature const savedSignature = new Image() savedSignature.onload = () => { + if (form.signature !== expectedSignature || signatureStrokes.value.length) return context.drawImage(savedSignature, 0, 0, rect.width, rect.height) + context.lineCap = 'round' + context.lineJoin = 'round' + context.lineWidth = 2.4 + context.strokeStyle = '#ffffff' } savedSignature.src = form.signature } @@ -1149,6 +1178,8 @@ const getSignaturePoint = (event) => { return { x: event.clientX - rect.left, y: event.clientY - rect.top, + width: rect.width, + height: rect.height, } } @@ -1158,6 +1189,8 @@ const startSignature = (event) => { signatureCanvas.value.setPointerCapture?.(event.pointerId) const context = signatureCanvas.value.getContext('2d') const point = getSignaturePoint(event) + activeSignatureStroke = [{ x: point.x / point.width, y: point.y / point.height }] + signatureStrokes.value.push(activeSignatureStroke) context.beginPath() context.moveTo(point.x, point.y) } @@ -1166,6 +1199,7 @@ const drawSignature = (event) => { if (!isSigning || !signatureCanvas.value) return const context = signatureCanvas.value.getContext('2d') const point = getSignaturePoint(event) + activeSignatureStroke?.push({ x: point.x / point.width, y: point.y / point.height }) context.lineTo(point.x, point.y) context.stroke() signatureHasInk.value = true @@ -1174,10 +1208,13 @@ const drawSignature = (event) => { const endSignature = () => { isSigning = false + activeSignatureStroke = null } const clearSignature = () => { form.signature = '' + signatureStrokes.value = [] + activeSignatureStroke = null signatureHasInk.value = false syncSignatureCanvas() } @@ -2179,7 +2216,9 @@ watch(signupStep, async (step) => { saveSignupDraft() if (step !== 4) return await nextTick() - syncSignatureCanvas() + window.requestAnimationFrame(() => { + window.requestAnimationFrame(syncSignatureCanvas) + }) }) watch(form, () => { diff --git a/src/style.css b/src/style.css index 5f46337..b065011 100644 --- a/src/style.css +++ b/src/style.css @@ -38,7 +38,7 @@ html, body { min-height: 100%; margin: 0; - background: #000; + background: #0a0a0a; } body::before { @@ -2979,41 +2979,62 @@ body.menu-open { } .signup-modal.card-modal { - width: min(100%, 27rem); + width: min(100%, 22rem); } .signup-modal.card-modal > .flex:first-child { - padding: 1.5rem; + padding: 1rem; } .signup-modal.card-modal > .flex:first-child > div { - max-width: 24rem; + max-width: 19rem; } .signup-modal.card-modal .modal-body { - padding: 1.5rem; + max-height: min(30rem, calc(100svh - 12rem)); + padding: 1rem; } .signup-modal.card-modal .card-modal-content, .signup-modal.card-modal .modal-footer-actions { width: 100%; - max-width: 24rem; + max-width: 19rem; margin-inline: auto; } +.signup-modal.card-modal .card-modal-content { + gap: 1rem; +} + .signup-modal.card-modal .modal-footer { - padding: 1.5rem; + padding: 1rem; } .signup-modal.card-modal .l484-card { - width: 100%; + width: 70%; } .signup-modal.card-modal .card-number { - font-size: clamp(1.35rem, 7.4cqw, 1.55rem); + font-size: clamp(0.92rem, 7.4cqw, 1.08rem); letter-spacing: 0.02em; } +.signup-modal.card-modal .card-note { + padding: 0.68rem 0.75rem; + font-size: 0.76rem; + line-height: 1.45; +} + +.signup-modal.card-modal .validation-message { + padding: 0.65rem 0.75rem; + font-size: 0.82rem; +} + +.signup-modal.card-modal .member-keys { + gap: 0.65rem; + padding: 0.8rem; +} + .backup-modal { position: relative; z-index: 1;