feat: FASE 3 - Flusso MeasurementTec (selezione ricetta, esecuzione misure, riepilogo)
Implementazione completa del flusso operativo per il ruolo MeasurementTec:
Blueprint measure.py:
- select_recipe: selezione ricetta con ricerca e barcode
- task_list: lista task con conteggi subtask e allegati
- task_execute: esecuzione misure con numpad, calibro USB, feedback real-time
- task_complete: riepilogo con statistiche pass/fail e export CSV
- API AJAX: lookup-barcode, save-traceability, save-measurement
- Autorizzazione role_required("MeasurementTec") su tutte le route
Componenti riutilizzabili:
- numpad.html/js/css: tastierino numerico touch-friendly con keyboard support
- caliper_status.html + caliper.js: integrazione calibro USB via Web Serial API
- barcode_scanner.html + barcode.js: scansione barcode con html5-qrcode
- measurement_feedback.html: feedback visivo pass/warning/fail in tempo reale
- next_measurement.html: indicatore prossima misurazione
- annotation-viewer.js: visualizzatore canvas con marker su disegni tecnici
- csv-export.js: export CSV con locale italiano (delimitatore ;, decimale ,)
Sicurezza:
- Decoratore role_required(*roles) per autorizzazione basata su ruoli
- CSRF token su tutti i POST AJAX
- |tojson per prevenire XSS su annotations_json
- Validazione input lato client e server
i18n: 23+ nuove chiavi tradotte IT/EN per tutti i template FASE 3
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
<!--
|
||||
Barcode Scanner Modal Component
|
||||
Camera-based barcode/QR code scanner
|
||||
Include in pages where barcode scanning is needed
|
||||
-->
|
||||
<div x-data="barcodeScanner()"
|
||||
@barcode-use.window="stopScan(); clear()"
|
||||
x-init="$watch('scanning', val => console.log('Scanning:', val))"
|
||||
x-cloak>
|
||||
|
||||
<!-- Trigger button (styled slot - customize per use case) -->
|
||||
<slot name="trigger">
|
||||
<button @click="startScan()"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 px-4 py-2.5 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-lg text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors duration-200 shadow-sm hover:shadow-md">
|
||||
|
||||
<!-- Barcode icon -->
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z"/>
|
||||
</svg>
|
||||
<span>{{ _('Scansiona Barcode') }}</span>
|
||||
</button>
|
||||
</slot>
|
||||
|
||||
<!-- Modal overlay -->
|
||||
<div x-show="scanning || result"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"
|
||||
style="display: none;">
|
||||
|
||||
<div @click.outside="scanning && !result ? null : (stopScan(), clear())"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl max-w-md w-full overflow-hidden">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center px-6 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
|
||||
{{ _('Scansiona Barcode') }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<button @click="stopScan(); clear()"
|
||||
type="button"
|
||||
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors p-1 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-6">
|
||||
|
||||
<!-- Camera preview container -->
|
||||
<div x-show="scanning && !result"
|
||||
class="relative rounded-xl overflow-hidden bg-slate-900 aspect-square mb-4">
|
||||
|
||||
<!-- Scanner element -->
|
||||
<div id="barcode-reader" class="w-full h-full"></div>
|
||||
|
||||
<!-- Scanning indicator -->
|
||||
<div class="absolute inset-0 pointer-events-none">
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div class="w-64 h-64 border-2 border-white/50 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instruction overlay -->
|
||||
<div class="absolute bottom-0 inset-x-0 bg-gradient-to-t from-black/80 to-transparent p-4">
|
||||
<p class="text-white text-sm text-center font-medium">
|
||||
{{ _('Inquadra il codice a barre nel riquadro') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success result -->
|
||||
<div x-show="result"
|
||||
x-transition
|
||||
class="text-center py-8">
|
||||
|
||||
<!-- Success icon -->
|
||||
<div class="w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400 mb-3">
|
||||
{{ _('Codice rilevato') }}
|
||||
</p>
|
||||
|
||||
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-lg p-4 mb-4">
|
||||
<p class="font-mono text-xl font-bold text-primary break-all" x-text="result"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error message -->
|
||||
<div x-show="error"
|
||||
x-transition
|
||||
class="mb-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<p class="text-sm text-red-800 dark:text-red-200" x-text="error"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex gap-3">
|
||||
<button x-show="result"
|
||||
@click="$dispatch('barcode-use', { code: result }); stopScan(); clear()"
|
||||
type="button"
|
||||
class="flex-1 px-4 py-2.5 bg-primary hover:bg-primary-dark text-white font-medium rounded-lg transition-colors duration-200 shadow-sm hover:shadow-md">
|
||||
{{ _('Usa Codice') }}
|
||||
</button>
|
||||
|
||||
<button x-show="scanning && !result"
|
||||
@click="stopScan()"
|
||||
type="button"
|
||||
class="flex-1 px-4 py-2.5 bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 font-medium rounded-lg transition-colors duration-200">
|
||||
{{ _('Ferma') }}
|
||||
</button>
|
||||
|
||||
<button x-show="!scanning || result"
|
||||
@click="stopScan(); clear()"
|
||||
type="button"
|
||||
class="flex-1 px-4 py-2.5 bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 font-medium rounded-lg transition-colors duration-200">
|
||||
{{ _('Chiudi') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,77 @@
|
||||
<!--
|
||||
Caliper Status Indicator
|
||||
Compact component for navbar or header
|
||||
Shows connection status and last reading
|
||||
-->
|
||||
<div x-data="caliperConnection()"
|
||||
x-init="$watch('connected', value => console.log('Caliper connected:', value))"
|
||||
class="inline-block">
|
||||
|
||||
<!-- Compact indicator button -->
|
||||
<button @click="connected ? disconnect() : connect()"
|
||||
:disabled="!isSupported"
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-lg border transition-all duration-200 hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:class="{
|
||||
'bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700': status === 'disconnected',
|
||||
'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800': status === 'connected',
|
||||
'bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800': status === 'connecting',
|
||||
'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800': status === 'error'
|
||||
}">
|
||||
|
||||
<!-- Status dot with animation -->
|
||||
<span class="relative flex h-2.5 w-2.5">
|
||||
<!-- Ping animation for active states -->
|
||||
<span x-show="status === 'connected' || status === 'connecting'"
|
||||
class="absolute inline-flex h-full w-full rounded-full opacity-75 animate-ping"
|
||||
:class="{
|
||||
'bg-green-400': status === 'connected',
|
||||
'bg-amber-400': status === 'connecting'
|
||||
}"></span>
|
||||
|
||||
<!-- Static dot -->
|
||||
<span class="relative inline-flex rounded-full h-2.5 w-2.5"
|
||||
:class="{
|
||||
'bg-green-500': status === 'connected',
|
||||
'bg-slate-400': status === 'disconnected',
|
||||
'bg-amber-400': status === 'connecting',
|
||||
'bg-red-500': status === 'error'
|
||||
}"></span>
|
||||
</span>
|
||||
|
||||
<!-- Caliper icon -->
|
||||
<svg class="w-4 h-4 text-slate-600 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
|
||||
<!-- Status label -->
|
||||
<span class="text-xs font-medium text-slate-700 dark:text-slate-300"
|
||||
x-text="status === 'connected' ? '{{ _('Calibro') }}' :
|
||||
status === 'connecting' ? '{{ _('Connessione...') }}' :
|
||||
status === 'error' ? '{{ _('Errore calibro') }}' :
|
||||
'{{ _('Calibro') }}'"></span>
|
||||
|
||||
<!-- Last reading (when connected) -->
|
||||
<span x-show="connected && lastReading !== null"
|
||||
x-transition
|
||||
class="font-mono text-xs font-semibold text-primary px-1.5 py-0.5 bg-blue-50 dark:bg-blue-900/30 rounded"
|
||||
x-text="lastReading?.toFixed(3) + ' mm'"></span>
|
||||
|
||||
<!-- Not supported warning icon -->
|
||||
<template x-if="!isSupported">
|
||||
<div class="flex items-center gap-1.5" x-data="{ showTooltip: false }">
|
||||
<svg @mouseenter="showTooltip = true"
|
||||
@mouseleave="showTooltip = false"
|
||||
class="w-4 h-4 text-amber-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
|
||||
<!-- Tooltip -->
|
||||
<div x-show="showTooltip"
|
||||
x-transition
|
||||
class="absolute z-50 px-2 py-1 text-xs text-white bg-slate-900 rounded shadow-lg whitespace-nowrap -translate-y-8">
|
||||
{{ _('Browser non supporta Web Serial API') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,76 @@
|
||||
{# Measurement Feedback Component
|
||||
|
||||
Mostra feedback visivo in tempo reale della misurazione corrente.
|
||||
|
||||
Variabili Alpine.js richieste nel parent component:
|
||||
- currentValue: float | null - valore corrente misurato
|
||||
- nominal: float - valore nominale
|
||||
- utl: float - Upper Tolerance Limit
|
||||
- uwl: float - Upper Warning Limit
|
||||
- lwl: float - Lower Warning Limit
|
||||
- ltl: float - Lower Tolerance Limit
|
||||
- unit: string - unità di misura (es. "mm")
|
||||
- passFailStatus: computed - 'pass' | 'warning' | 'fail'
|
||||
- deviation: computed - scostamento dal nominale
|
||||
- progressWidth: computed - larghezza barra percentuale
|
||||
#}
|
||||
|
||||
<div class="measurement-feedback" x-show="currentValue !== null">
|
||||
<!-- Status bar colorata -->
|
||||
<div class="h-3 rounded-full overflow-hidden bg-slate-200 dark:bg-slate-600">
|
||||
<div class="h-full rounded-full transition-all duration-300"
|
||||
:class="{
|
||||
'bg-measure-pass': passFailStatus === 'pass',
|
||||
'bg-measure-warning': passFailStatus === 'warning',
|
||||
'bg-measure-fail': passFailStatus === 'fail'
|
||||
}"
|
||||
:style="'width: ' + progressWidth + '%'">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status text + deviation -->
|
||||
<div class="flex justify-between items-center mt-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Icona stato - CONFORME -->
|
||||
<template x-if="passFailStatus === 'pass'">
|
||||
<span class="flex items-center gap-1 text-measure-pass font-semibold text-sm">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
{{ _('CONFORME') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- Icona stato - ATTENZIONE -->
|
||||
<template x-if="passFailStatus === 'warning'">
|
||||
<span class="flex items-center gap-1 text-measure-warning font-semibold text-sm">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{{ _('ATTENZIONE') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- Icona stato - NON CONFORME -->
|
||||
<template x-if="passFailStatus === 'fail'">
|
||||
<span class="flex items-center gap-1 text-measure-fail font-semibold text-sm">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
{{ _('NON CONFORME') }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Deviation con segno -->
|
||||
<span class="font-mono text-sm font-medium"
|
||||
:class="{
|
||||
'text-measure-pass': passFailStatus === 'pass',
|
||||
'text-measure-warning': passFailStatus === 'warning',
|
||||
'text-measure-fail': passFailStatus === 'fail'
|
||||
}"
|
||||
x-text="deviation !== null ?
|
||||
(deviation >= 0 ? '+' : '') + deviation.toFixed(3) + ' ' + unit : ''">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
{# Next Measurement Component
|
||||
|
||||
Indicatore della prossima misura da effettuare.
|
||||
|
||||
Variabili Alpine.js richieste nel parent component:
|
||||
- nextSubtask: object | null - prossimo subtask da misurare
|
||||
- marker_number: int
|
||||
- description: string
|
||||
- nominal: float
|
||||
- unit: string
|
||||
#}
|
||||
|
||||
<div x-show="nextSubtask" class="mt-4 p-3 rounded-lg bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700">
|
||||
<div class="flex items-center gap-2 text-sm text-steel dark:text-steel-light">
|
||||
<!-- Arrow right icon -->
|
||||
<svg class="w-4 h-4 text-primary flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/>
|
||||
</svg>
|
||||
<span class="font-medium">{{ _('Prossima misura') }}:</span>
|
||||
<span class="font-semibold text-[var(--text-primary)]">
|
||||
#<span x-text="nextSubtask?.marker_number"></span>
|
||||
<span x-text="nextSubtask?.description" class="ml-1"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Nominal preview -->
|
||||
<div class="mt-1 ml-6 text-xs text-steel dark:text-steel-light">
|
||||
<span>Nom: </span>
|
||||
<span class="font-mono font-medium" x-text="nextSubtask?.nominal?.toFixed(3)"></span>
|
||||
<span class="ml-1" x-text="nextSubtask?.unit || 'mm'"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,121 @@
|
||||
<!-- Numpad Component - Touch-friendly numeric keypad for measurement input -->
|
||||
<div
|
||||
x-data="numpad()"
|
||||
@keydown.window="handleKeydown($event)"
|
||||
class="numpad-container w-full max-w-sm mx-auto"
|
||||
>
|
||||
<!-- Value Display -->
|
||||
<div class="value-display mb-3 p-4 bg-white dark:bg-slate-700 rounded-lg border-2 border-slate-200 dark:border-slate-600 shadow-sm">
|
||||
<div class="flex items-baseline justify-center">
|
||||
<span
|
||||
x-text="displayValue || '0'"
|
||||
class="font-mono text-3xl font-bold text-slate-900 dark:text-white"
|
||||
></span>
|
||||
<span
|
||||
x-text="unit"
|
||||
class="ml-2 text-sm font-medium text-steel dark:text-steel-light"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Keypad Grid 4x4 -->
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<!-- Row 1: 7 8 9 ⌫ -->
|
||||
<button
|
||||
@click="addDigit('7')"
|
||||
type="button"
|
||||
class="min-h-[56px] min-w-[56px] font-mono text-xl font-semibold rounded-xl border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white hover:bg-slate-50 dark:hover:bg-slate-600 active:scale-95 active:bg-slate-100 dark:active:bg-slate-500 transition-all duration-150 select-none cursor-pointer flex items-center justify-center touch-manipulation focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>7</button>
|
||||
|
||||
<button
|
||||
@click="addDigit('8')"
|
||||
type="button"
|
||||
class="min-h-[56px] min-w-[56px] font-mono text-xl font-semibold rounded-xl border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white hover:bg-slate-50 dark:hover:bg-slate-600 active:scale-95 active:bg-slate-100 dark:active:bg-slate-500 transition-all duration-150 select-none cursor-pointer flex items-center justify-center touch-manipulation focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>8</button>
|
||||
|
||||
<button
|
||||
@click="addDigit('9')"
|
||||
type="button"
|
||||
class="min-h-[56px] min-w-[56px] font-mono text-xl font-semibold rounded-xl border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white hover:bg-slate-50 dark:hover:bg-slate-600 active:scale-95 active:bg-slate-100 dark:active:bg-slate-500 transition-all duration-150 select-none cursor-pointer flex items-center justify-center touch-manipulation focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>9</button>
|
||||
|
||||
<button
|
||||
@click="backspace()"
|
||||
type="button"
|
||||
class="min-h-[56px] min-w-[56px] font-mono text-xl font-semibold rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-100 dark:bg-slate-600 text-steel dark:text-steel-light hover:bg-slate-50 dark:hover:bg-slate-600 active:scale-95 active:bg-slate-100 dark:active:bg-slate-500 transition-all duration-150 select-none cursor-pointer flex items-center justify-center touch-manipulation focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>⌫</button>
|
||||
|
||||
<!-- Row 2: 4 5 6 C -->
|
||||
<button
|
||||
@click="addDigit('4')"
|
||||
type="button"
|
||||
class="min-h-[56px] min-w-[56px] font-mono text-xl font-semibold rounded-xl border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white hover:bg-slate-50 dark:hover:bg-slate-600 active:scale-95 active:bg-slate-100 dark:active:bg-slate-500 transition-all duration-150 select-none cursor-pointer flex items-center justify-center touch-manipulation focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>4</button>
|
||||
|
||||
<button
|
||||
@click="addDigit('5')"
|
||||
type="button"
|
||||
class="min-h-[56px] min-w-[56px] font-mono text-xl font-semibold rounded-xl border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white hover:bg-slate-50 dark:hover:bg-slate-600 active:scale-95 active:bg-slate-100 dark:active:bg-slate-500 transition-all duration-150 select-none cursor-pointer flex items-center justify-center touch-manipulation focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>5</button>
|
||||
|
||||
<button
|
||||
@click="addDigit('6')"
|
||||
type="button"
|
||||
class="min-h-[56px] min-w-[56px] font-mono text-xl font-semibold rounded-xl border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white hover:bg-slate-50 dark:hover:bg-slate-600 active:scale-95 active:bg-slate-100 dark:active:bg-slate-500 transition-all duration-150 select-none cursor-pointer flex items-center justify-center touch-manipulation focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>6</button>
|
||||
|
||||
<button
|
||||
@click="clearAll()"
|
||||
type="button"
|
||||
class="min-h-[56px] min-w-[56px] font-mono text-xl font-semibold rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-100 dark:bg-slate-600 text-measure-fail hover:bg-slate-50 dark:hover:bg-slate-600 active:scale-95 active:bg-slate-100 dark:active:bg-slate-500 transition-all duration-150 select-none cursor-pointer flex items-center justify-center touch-manipulation focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>C</button>
|
||||
|
||||
<!-- Row 3: 1 2 3 +/− -->
|
||||
<button
|
||||
@click="addDigit('1')"
|
||||
type="button"
|
||||
class="min-h-[56px] min-w-[56px] font-mono text-xl font-semibold rounded-xl border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white hover:bg-slate-50 dark:hover:bg-slate-600 active:scale-95 active:bg-slate-100 dark:active:bg-slate-500 transition-all duration-150 select-none cursor-pointer flex items-center justify-center touch-manipulation focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>1</button>
|
||||
|
||||
<button
|
||||
@click="addDigit('2')"
|
||||
type="button"
|
||||
class="min-h-[56px] min-w-[56px] font-mono text-xl font-semibold rounded-xl border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white hover:bg-slate-50 dark:hover:bg-slate-600 active:scale-95 active:bg-slate-100 dark:active:bg-slate-500 transition-all duration-150 select-none cursor-pointer flex items-center justify-center touch-manipulation focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>2</button>
|
||||
|
||||
<button
|
||||
@click="addDigit('3')"
|
||||
type="button"
|
||||
class="min-h-[56px] min-w-[56px] font-mono text-xl font-semibold rounded-xl border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white hover:bg-slate-50 dark:hover:bg-slate-600 active:scale-95 active:bg-slate-100 dark:active:bg-slate-500 transition-all duration-150 select-none cursor-pointer flex items-center justify-center touch-manipulation focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>3</button>
|
||||
|
||||
<button
|
||||
@click="toggleSign()"
|
||||
type="button"
|
||||
class="min-h-[56px] min-w-[56px] font-mono text-xl font-semibold rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-100 dark:bg-slate-600 text-steel dark:text-steel-light hover:bg-slate-50 dark:hover:bg-slate-600 active:scale-95 active:bg-slate-100 dark:active:bg-slate-500 transition-all duration-150 select-none cursor-pointer flex items-center justify-center touch-manipulation focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>+/−</button>
|
||||
|
||||
<!-- Row 4: 0 . (spacer) ✓ -->
|
||||
<button
|
||||
@click="addDigit('0')"
|
||||
type="button"
|
||||
class="min-h-[56px] min-w-[56px] font-mono text-xl font-semibold rounded-xl border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white hover:bg-slate-50 dark:hover:bg-slate-600 active:scale-95 active:bg-slate-100 dark:active:bg-slate-500 transition-all duration-150 select-none cursor-pointer flex items-center justify-center touch-manipulation focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>0</button>
|
||||
|
||||
<button
|
||||
@click="addDecimal()"
|
||||
type="button"
|
||||
class="min-h-[56px] min-w-[56px] font-mono text-xl font-semibold rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-100 dark:bg-slate-600 text-steel dark:text-steel-light hover:bg-slate-50 dark:hover:bg-slate-600 active:scale-95 active:bg-slate-100 dark:active:bg-slate-500 transition-all duration-150 select-none cursor-pointer flex items-center justify-center touch-manipulation focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>.</button>
|
||||
|
||||
<!-- Spacer -->
|
||||
<div></div>
|
||||
|
||||
<button
|
||||
@click="confirm()"
|
||||
type="button"
|
||||
:disabled="!hasValue"
|
||||
class="min-h-[56px] min-w-[56px] font-mono text-xl font-semibold rounded-xl border border-primary bg-primary text-white hover:bg-primary-dark active:scale-95 transition-all duration-150 select-none cursor-pointer flex items-center justify-center touch-manipulation focus:outline-none focus:ring-2 focus:ring-primary/50 disabled:opacity-40 disabled:cursor-not-allowed disabled:active:scale-100 disabled:hover:bg-primary"
|
||||
>✓</button>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user