diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx
index 0737359a..2848b8e6 100644
--- a/src/components/AppShell.tsx
+++ b/src/components/AppShell.tsx
@@ -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";
@@ -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 so the CSS variables in
// index.css pick it up.
@@ -201,13 +202,19 @@ export function AppShell() {
{t.app.saveDesign}
-
- {t.app.print}
-
+ {/* 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 && (
+
+ {t.app.print}
+
+ )}
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(null);
- const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const urlRef = useRef(null);
const zplRef = useRef(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');
@@ -72,10 +83,30 @@ export function LabelPreviewModal({ onClose }: Props) {
viewport so small previews are still centered. */}