Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions src/components/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
SunIcon,
MoonIcon,
} from "@heroicons/react/16/solid";
import { useLabelStore, useHistory } from "../store/labelStore";
import { useLabelStore, useHistory, canCallLabelary } from "../store/labelStore";
import { localeNames } from "../locales";
import type { LocaleCode } from "../locales";
import { mmToUnit } from "../lib/units";
Expand All @@ -50,6 +50,7 @@ export function AppShell() {
const setLocale = useLabelStore((s) => s.setLocale);
const theme = useLabelStore((s) => s.theme);
const setTheme = useLabelStore((s) => s.setTheme);
const labelaryReady = useLabelStore(canCallLabelary);

// Bridge the theme preference to <html data-theme> so the CSS variables in
// index.css pick it up.
Expand Down Expand Up @@ -201,13 +202,19 @@ export function AppShell() {
{t.app.saveDesign}
</DropdownItem>
<DropdownSeparator />
<DropdownItem
icon={PrinterIcon}
onClick={handlePrint}
disabled={!hasObjects}
>
{t.app.print}
</DropdownItem>
{/* Print also routes through Labelary. Until the user has seen
the privacy notice (shown only via the Preview modal), Print
is hidden so the very first Labelary call cannot bypass the
disclosure. After acknowledgement Print stays available. */}
{labelaryReady && (
<DropdownItem
icon={PrinterIcon}
onClick={handlePrint}
disabled={!hasObjects}
>
{t.app.print}
</DropdownItem>
)}
<DropdownItem
icon={PaperAirplaneIcon}
onClick={openZebraPrint}
Expand Down
58 changes: 50 additions & 8 deletions src/components/Output/LabelPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { XMarkIcon, ArrowDownTrayIcon } from '@heroicons/react/16/solid';
import { useLabelStore, useCurrentObjects } from '../../store/labelStore';
import { useLabelStore, useCurrentObjects, canCallLabelary } from '../../store/labelStore';
import { generateZPL } from '../../lib/zplGenerator';
import { fetchPreview, labelaryErrorMessage } from '../../lib/labelary';
import { triggerDownload } from '../../lib/triggerDownload';
Expand All @@ -15,31 +15,42 @@ export function LabelPreviewModal({ onClose }: Props) {
const t = useT();
const label = useLabelStore((s) => s.label);
const objects = useCurrentObjects();
const noticeAcknowledged = useLabelStore((s) => s.labelaryNoticeAcknowledged);
const acknowledgeLabelaryNotice = useLabelStore((s) => s.acknowledgeLabelaryNotice);
// Same gate as every other Labelary consumer (Print, …) — single source
// of truth in the store. The modal opens only when the gate is on, so in
// practice this resolves to noticeAcknowledged here, but we route through
// the shared selector to stay in lockstep with future call sites.
const canFetch = useLabelStore(canCallLabelary);

const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const urlRef = useRef<string | null>(null);
const zplRef = useRef<string>(generateZPL(label, objects));
// Derived: fetch is in flight while we have neither result nor error yet.
const loading = canFetch && !previewUrl && !error;

useEffect(() => {
if (!canFetch) return;
let cancelled = false;
fetchPreview(zplRef.current, label)
.then((url) => {
if (cancelled) { URL.revokeObjectURL(url); return; }
urlRef.current = url;
setPreviewUrl(url);
setLoading(false);
})
.catch((e: unknown) => {
if (cancelled) return;
setError(labelaryErrorMessage(e));
setLoading(false);
});
return () => {
cancelled = true;
if (urlRef.current) URL.revokeObjectURL(urlRef.current);
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// `label` and the generated ZPL are intentionally captured once at mount
// (via zplRef): the preview should reflect the snapshot the user saw when
// they opened the modal, not refetch when the canvas changes underneath.
}, [canFetch]); // eslint-disable-line react-hooks/exhaustive-deps

const handleDownloadFallback = () => {
triggerDownload(new Blob([zplRef.current], { type: 'text/plain' }), 'label.zpl');
Expand Down Expand Up @@ -72,10 +83,30 @@ export function LabelPreviewModal({ onClose }: Props) {
viewport so small previews are still centered. */}
<div className="flex-1 overflow-auto bg-bg min-h-24 min-w-48">
<div className="min-h-full min-w-full flex items-center justify-center p-4">
{loading && (
{!noticeAcknowledged && (
<div className="flex flex-col gap-3 max-w-80 text-center font-mono text-[10px] text-muted leading-relaxed">
<span className="text-text uppercase tracking-widest">{t.output.previewNoticeTitle}</span>
<span>{t.output.previewNoticeBody}</span>
<a
href="https://labelary.com/privacy.html"
target="_blank"
rel="noreferrer"
className="text-accent hover:underline"
>
{t.output.previewNoticePrivacyLink}
</a>
<button
onClick={acknowledgeLabelaryNotice}
className="self-center mt-1 px-3 py-1.5 rounded text-[10px] font-mono bg-surface-2 border border-border text-text hover:border-accent transition-colors"
>
{t.output.previewNoticeAcknowledge}
</button>
</div>
)}
{canFetch && loading && (
<span className="font-mono text-[10px] text-muted animate-pulse">{t.output.loading}</span>
)}
{!loading && error && (
{canFetch && !loading && error && (
<div className="flex flex-col items-center gap-3 max-w-64 text-center">
<span className="font-mono text-[10px] text-amber-400 leading-relaxed">{error}</span>
<button
Expand All @@ -87,7 +118,7 @@ export function LabelPreviewModal({ onClose }: Props) {
</button>
</div>
)}
{!loading && !error && previewUrl && (
{canFetch && !loading && !error && previewUrl && (
<img
src={previewUrl}
alt="Label preview"
Expand All @@ -96,6 +127,17 @@ export function LabelPreviewModal({ onClose }: Props) {
)}
</div>
</div>

<div className="px-3 py-1 border-t border-border-2 shrink-0 text-center">
<a
href="https://labelary.com/"
target="_blank"
rel="noreferrer"
className="font-mono text-[9px] text-muted hover:text-accent transition-colors"
>
{t.output.previewProvider}
</a>
</div>
</div>
</div>
);
Expand Down
24 changes: 15 additions & 9 deletions src/components/Output/ZPLOutput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export function ZPLOutput({ collapsed, onCollapse, onExpand }: Props) {
const t = useT();
const label = useLabelStore((s) => s.label);
const pages = useLabelStore((s) => s.pages);
// Direct gate check — Preview is the path to the privacy notice, so the
// button must be reachable before acknowledgement. Other Labelary callers
// (AppShell.Print, LabelPreview.fetch) use the stricter canCallLabelary.
const labelaryEnabled = useLabelStore((s) => s.thirdParty.labelary);
const [copied, setCopied] = useState(false);
const [showPreview, setShowPreview] = useState(false);

Expand Down Expand Up @@ -43,15 +47,17 @@ export function ZPLOutput({ collapsed, onCollapse, onExpand }: Props) {
<span className="font-mono text-[10px] text-muted uppercase tracking-widest">{t.output.zplHeading}</span>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => setShowPreview(true)}
disabled={!zpl}
title={t.output.previewHeading}
className="flex items-center gap-1 font-mono text-[10px] text-muted hover:text-accent disabled:opacity-25 disabled:cursor-not-allowed transition-colors"
>
<EyeIcon className="w-4 h-4" />
{t.output.previewHeading}
</button>
{labelaryEnabled && (
<button
onClick={() => setShowPreview(true)}
disabled={!zpl}
title={t.output.previewHeading}
className="flex items-center gap-1 font-mono text-[10px] text-muted hover:text-accent disabled:opacity-25 disabled:cursor-not-allowed transition-colors"
>
<EyeIcon className="w-4 h-4" />
{t.output.previewHeading}
</button>
)}
<button
onClick={handleCopy}
disabled={!zpl}
Expand Down
5 changes: 5 additions & 0 deletions src/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ const ar = {
loading: 'جارٍ التحميل…',
unavailable: 'غير متاح',
previewEmpty: 'تظهر المعاينة\nبعد إجراء تغييرات',
previewProvider: 'معاينة عبر api.labelary.com',
previewNoticeTitle: 'إشعار الخصوصية',
previewNoticeBody: 'يتم إنشاء المعاينة بواسطة الخدمة الخارجية api.labelary.com. يتم إرسال ZPL الكامل للملصق، بما في ذلك أي بيانات حساسة، عبر الشبكة.',
previewNoticePrivacyLink: 'معلومات خصوصية Labelary',
previewNoticeAcknowledge: 'فهمت، تابع',
},

registry: {
Expand Down
5 changes: 5 additions & 0 deletions src/locales/bg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ const bg = {
loading: 'Зарежда…',
unavailable: 'Недостъпно',
previewEmpty: 'Преглед се появява\nслед промени',
previewProvider: 'Преглед чрез api.labelary.com',
previewNoticeTitle: 'Известие за поверителност',
previewNoticeBody: 'Прегледът се изобразява от външната услуга api.labelary.com. Целият ZPL на етикета, включително чувствителните данни, се изпраща през мрежата.',
previewNoticePrivacyLink: 'Информация за поверителност на Labelary',
previewNoticeAcknowledge: 'Разбрах, продължи',
},

registry: {
Expand Down
5 changes: 5 additions & 0 deletions src/locales/cs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ const cs = {
loading: 'Načítání…',
unavailable: 'Nedostupné',
previewEmpty: 'Náhled se zobrazí\npo úpravách',
previewProvider: 'Náhled přes api.labelary.com',
previewNoticeTitle: 'Upozornění na ochranu údajů',
previewNoticeBody: 'Náhled se generuje pomocí externí služby api.labelary.com. Celý ZPL štítku, včetně citlivých údajů, je odesílán přes síť.',
previewNoticePrivacyLink: 'Informace o ochraně údajů Labelary',
previewNoticeAcknowledge: 'Rozumím, pokračovat',
},

registry: {
Expand Down
5 changes: 5 additions & 0 deletions src/locales/da.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ const da = {
loading: 'Indlæser…',
unavailable: 'Ikke tilgængelig',
previewEmpty: 'Forhåndsvisning vises\nefter ændringer',
previewProvider: 'Forhåndsvisning via api.labelary.com',
previewNoticeTitle: 'Privatlivsmeddelelse',
previewNoticeBody: 'Forhåndsvisningen genereres af den eksterne tjeneste api.labelary.com. Hele etikettens ZPL, inklusive følsomme data, sendes over netværket.',
previewNoticePrivacyLink: 'Labelary-privatlivsoplysninger',
previewNoticeAcknowledge: 'Forstået, fortsæt',
},

registry: {
Expand Down
5 changes: 5 additions & 0 deletions src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ const de = {
loading: 'Lädt…',
unavailable: 'Nicht verfügbar',
previewEmpty: 'Vorschau erscheint\nnach Änderungen',
previewProvider: 'Vorschau über api.labelary.com',
previewNoticeTitle: 'Datenschutzhinweis',
previewNoticeBody: 'Die Vorschau wird vom externen Dienst api.labelary.com erstellt. Dabei wird das vollständige Label-ZPL inklusive sensibler Daten übertragen.',
previewNoticePrivacyLink: 'Labelary-Datenschutzhinweise',
previewNoticeAcknowledge: 'Verstanden, fortfahren',
},

registry: {
Expand Down
5 changes: 5 additions & 0 deletions src/locales/el.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ const el = {
loading: 'Φόρτωση…',
unavailable: 'Μη διαθέσιμο',
previewEmpty: 'Η προεπισκόπηση εμφανίζεται\nμετά τις αλλαγές',
previewProvider: 'Προεπισκόπηση μέσω api.labelary.com',
previewNoticeTitle: 'Ειδοποίηση απορρήτου',
previewNoticeBody: 'Η προεπισκόπηση δημιουργείται από την εξωτερική υπηρεσία api.labelary.com. Ολόκληρο το ZPL της ετικέτας, συμπεριλαμβανομένων ευαίσθητων δεδομένων, αποστέλλεται μέσω δικτύου.',
previewNoticePrivacyLink: 'Πληροφορίες απορρήτου Labelary',
previewNoticeAcknowledge: 'Κατάλαβα, συνέχεια',
},

registry: {
Expand Down
5 changes: 5 additions & 0 deletions src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ const en = {
loading: 'Loading…',
unavailable: 'Unavailable',
previewEmpty: 'Preview appears\nafter changes',
previewProvider: 'Preview via api.labelary.com',
previewNoticeTitle: 'Privacy notice',
previewNoticeBody: 'Preview rendering is performed by the external service api.labelary.com. The full label ZPL, including any sensitive data, is sent over the network.',
previewNoticePrivacyLink: 'Labelary privacy information',
previewNoticeAcknowledge: 'Got it, continue',
},

registry: {
Expand Down
5 changes: 5 additions & 0 deletions src/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ const es = {
loading: 'Cargando…',
unavailable: 'No disponible',
previewEmpty: 'La vista previa aparece\ntras los cambios',
previewProvider: 'Vista previa vía api.labelary.com',
previewNoticeTitle: 'Aviso de privacidad',
previewNoticeBody: 'La previsualización se genera mediante el servicio externo api.labelary.com. Se envía el ZPL completo de la etiqueta, incluidos los datos sensibles.',
previewNoticePrivacyLink: 'Información de privacidad de Labelary',
previewNoticeAcknowledge: 'Entendido, continuar',
},

registry: {
Expand Down
5 changes: 5 additions & 0 deletions src/locales/et.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ const et = {
loading: 'Laadimine…',
unavailable: 'Pole saadaval',
previewEmpty: 'Eelvaade kuvatakse\npärast muudatusi',
previewProvider: 'Eelvaade api.labelary.com kaudu',
previewNoticeTitle: 'Privaatsusteatis',
previewNoticeBody: 'Eelvaate genereerib väline teenus api.labelary.com. Sildi terve ZPL, sealhulgas tundlikud andmed, saadetakse võrgu kaudu.',
previewNoticePrivacyLink: 'Labelary privaatsusteave',
previewNoticeAcknowledge: 'Selge, jätka',
},

registry: {
Expand Down
5 changes: 5 additions & 0 deletions src/locales/fa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ const fa = {
loading: 'در حال بارگذاری…',
unavailable: 'در دسترس نیست',
previewEmpty: 'پیش‌نمایش پس از\nتغییرات نمایش داده می‌شود',
previewProvider: 'پیش‌نمایش از طریق api.labelary.com',
previewNoticeTitle: 'اطلاعیه حریم خصوصی',
previewNoticeBody: 'پیش‌نمایش توسط سرویس خارجی api.labelary.com تولید می‌شود. ZPL کامل برچسب، شامل داده‌های حساس، از طریق شبکه ارسال می‌شود.',
previewNoticePrivacyLink: 'اطلاعات حریم خصوصی Labelary',
previewNoticeAcknowledge: 'متوجه شدم، ادامه',
},

registry: {
Expand Down
5 changes: 5 additions & 0 deletions src/locales/fi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ const fi = {
loading: 'Ladataan…',
unavailable: 'Ei saatavilla',
previewEmpty: 'Esikatselu näkyy\nmuutosten jälkeen',
previewProvider: 'Esikatselu palvelusta api.labelary.com',
previewNoticeTitle: 'Tietosuojailmoitus',
previewNoticeBody: 'Esikatselun tuottaa ulkoinen palvelu api.labelary.com. Etiketin koko ZPL, mukaan lukien arkaluonteiset tiedot, lähetetään verkon yli.',
previewNoticePrivacyLink: 'Labelary-tietosuojatiedot',
previewNoticeAcknowledge: 'Selvä, jatka',
},

registry: {
Expand Down
5 changes: 5 additions & 0 deletions src/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ const fr = {
loading: 'Chargement…',
unavailable: 'Indisponible',
previewEmpty: "L'aperçu apparaît\naprès les modifications",
previewProvider: 'Aperçu via api.labelary.com',
previewNoticeTitle: 'Avis de confidentialité',
previewNoticeBody: 'Le rendu de l\'aperçu est effectué par le service externe api.labelary.com. Le ZPL complet de l\'étiquette, y compris toute donnée sensible, est envoyé sur le réseau.',
previewNoticePrivacyLink: 'Informations sur la confidentialité de Labelary',
previewNoticeAcknowledge: 'Compris, continuer',
},

registry: {
Expand Down
5 changes: 5 additions & 0 deletions src/locales/he.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ const he = {
loading: 'טוען…',
unavailable: 'לא זמין',
previewEmpty: 'התצוגה המקדימה מופיעה\nלאחר שינויים',
previewProvider: 'תצוגה מקדימה דרך api.labelary.com',
previewNoticeTitle: 'הודעת פרטיות',
previewNoticeBody: 'התצוגה המקדימה מופקת על ידי השירות החיצוני api.labelary.com. ה-ZPL המלא של התווית, כולל נתונים רגישים, נשלח ברשת.',
previewNoticePrivacyLink: 'מידע פרטיות של Labelary',
previewNoticeAcknowledge: 'הבנתי, המשך',
},

registry: {
Expand Down
5 changes: 5 additions & 0 deletions src/locales/hr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ const hr = {
loading: 'Učitavanje…',
unavailable: 'Nedostupno',
previewEmpty: 'Pregled se prikazuje\nnakon promjena',
previewProvider: 'Pregled putem api.labelary.com',
previewNoticeTitle: 'Obavijest o privatnosti',
previewNoticeBody: 'Pregled generira vanjska usluga api.labelary.com. Cijeli ZPL naljepnice, uključujući osjetljive podatke, šalje se putem mreže.',
previewNoticePrivacyLink: 'Informacije o privatnosti Labelary',
previewNoticeAcknowledge: 'Razumijem, nastavi',
},

registry: {
Expand Down
5 changes: 5 additions & 0 deletions src/locales/hu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ const hu = {
loading: 'Betöltés…',
unavailable: 'Nem elérhető',
previewEmpty: 'Az előnézet megjelenik\nmódosítás után',
previewProvider: 'Előnézet az api.labelary.com útján',
previewNoticeTitle: 'Adatvédelmi tájékoztató',
previewNoticeBody: 'Az előnézetet a külső api.labelary.com szolgáltatás készíti. A címke teljes ZPL-je, beleértve a bizalmas adatokat is, hálózaton keresztül kerül elküldésre.',
previewNoticePrivacyLink: 'Labelary adatvédelmi tájékoztató',
previewNoticeAcknowledge: 'Értem, folytatás',
},

registry: {
Expand Down
5 changes: 5 additions & 0 deletions src/locales/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ const it = {
loading: 'Caricamento…',
unavailable: 'Non disponibile',
previewEmpty: "L'anteprima appare\ndopo le modifiche",
previewProvider: 'Anteprima tramite api.labelary.com',
previewNoticeTitle: 'Informativa sulla privacy',
previewNoticeBody: 'L\'anteprima è generata dal servizio esterno api.labelary.com. L\'intero ZPL dell\'etichetta, inclusi eventuali dati sensibili, viene trasmesso in rete.',
previewNoticePrivacyLink: 'Informativa sulla privacy di Labelary',
previewNoticeAcknowledge: 'Ho capito, continua',
},

registry: {
Expand Down
Loading