diff --git a/README.md b/README.md
index d9e8a98e..a6e95562 100644
--- a/README.md
+++ b/README.md
@@ -64,6 +64,10 @@ File menu → **Import ZPL**: paste ZPL code directly, or open a `.zpl` file.
The parser covers the most common ZPL commands. Anything it doesn't recognize is skipped and listed in the import report.
+### Multiple labels (pages)
+
+File menu → **Add page** creates a new page. With multiple pages, the control at the bottom-center of the canvas switches between them and removes them. All pages share the same dimensions; export and import handle each page as a separate label.
+
### Keyboard shortcuts
| Shortcut | Action |
@@ -76,6 +80,7 @@ The parser covers the most common ZPL commands. Anything it doesn't recognize is
| `G` | Toggle grid |
| `S` | Toggle snap |
| `R` | Rotate view (0° → 90° → 180° → 270°) |
+| `Page Up` / `Page Down` | Previous / Next page |
| Middle mouse / Space+drag | Pan canvas |
| Scroll | Pan canvas |
| Ctrl+Scroll | Zoom |
@@ -101,7 +106,7 @@ Use `.json` (File → Save Design) to save your work. It preserves every object
- The canvas is a design preview, not a pixel-perfect simulation: fonts and exact rendering may differ from what the printer produces. Shapes, spacing, and positions should match. For an accurate render, use the **Preview** in the bottom-right panel (powered by Labelary).
- Label preview requires a connection to `api.labelary.com`.
- The Labelary preview doesn't render every ZPL feature. Some less common elements (e.g. Codablock F barcodes) may be missing or wrong in the preview even when the actual print is fine.
-- No support for multi-label documents (multiple labels in one file).
+- The Labelary preview shows only the current page; the printed/exported ZPL still contains every page.
---
diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx
index ffe12aa8..a21e8cfe 100644
--- a/src/components/AppShell.tsx
+++ b/src/components/AppShell.tsx
@@ -20,6 +20,7 @@ import {
ArrowUpTrayIcon,
ArrowDownTrayIcon,
DocumentPlusIcon,
+ DocumentDuplicateIcon,
FolderOpenIcon,
DocumentArrowDownIcon,
PrinterIcon,
@@ -40,8 +41,9 @@ import { useOutputPanel, OUTPUT_DEFAULT_H } from "../hooks/useOutputPanel";
export function AppShell() {
const t = useT();
const label = useLabelStore((s) => s.label);
- const objects = useLabelStore((s) => s.objects);
+ const pages = useLabelStore((s) => s.pages);
const selectObject = useLabelStore((s) => s.selectObject);
+ const addPage = useLabelStore((s) => s.addPage);
const locale = useLabelStore((s) => s.locale);
const setLocale = useLabelStore((s) => s.setLocale);
const { undo, redo, pastStates, futureStates } = useHistory();
@@ -52,7 +54,7 @@ export function AppShell() {
const canUndo = pastStates.length > 0;
const canRedo = futureStates.length > 0;
- const hasObjects = objects.length > 0;
+ const hasObjects = pages.some((p) => p.objects.length > 0);
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
@@ -147,6 +149,9 @@ export function AppShell() {
{t.app.newDesign}
+
+ {t.app.addPage}
+
{t.app.importZpl}
diff --git a/src/components/Canvas/LabelCanvas.tsx b/src/components/Canvas/LabelCanvas.tsx
index 67745d15..ce766ec7 100644
--- a/src/components/Canvas/LabelCanvas.tsx
+++ b/src/components/Canvas/LabelCanvas.tsx
@@ -8,7 +8,7 @@ import { useDroppable, useDndMonitor } from "@dnd-kit/core";
import type { PaletteDragData } from "../../dnd/types";
import { Stage, Layer, Group, Rect, Transformer } from "react-konva";
import type Konva from "konva";
-import { useLabelStore } from "../../store/labelStore";
+import { useLabelStore, useCurrentObjects, currentObjects } from "../../store/labelStore";
import { pxToDots, SCREEN_PX_PER_MM } from "../../lib/coordinates";
import { SNAP_OPTIONS } from "../../lib/units";
import type { Unit } from "../../lib/units";
@@ -24,6 +24,7 @@ import { useColorScheme } from "../../lib/useColorScheme";
import { useCanvasPanZoom } from "./hooks/useCanvasPanZoom";
import { useCanvasLasso } from "./hooks/useCanvasLasso";
import { useKonvaTransformer } from "./hooks/useKonvaTransformer";
+import { PaginationControl } from "./PaginationControl";
import {
axisReversal,
inverseRotateDelta,
@@ -85,7 +86,6 @@ export function LabelCanvas({
const {
label,
- objects,
selectedIds,
addObject,
updateObject,
@@ -94,6 +94,7 @@ export function LabelCanvas({
toggleSelectObject,
selectObjects,
} = useLabelStore();
+ const objects = useCurrentObjects();
useEffect(() => {
const el = containerRef.current;
@@ -131,7 +132,9 @@ export function LabelCanvas({
const tag = (e.target as HTMLElement).tagName;
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
- const { selectedIds: ids, objects: objs } = useLabelStore.getState();
+ const state = useLabelStore.getState();
+ const ids = state.selectedIds;
+ const objs = currentObjects(state);
if (ids.length === 0) return;
e.preventDefault();
@@ -269,7 +272,9 @@ export function LabelCanvas({
// Multi-select: propagate position delta to all other selected objects.
// Read fresh state (getState) to avoid stale closure when multiple DragEnd events
// fire simultaneously during a Transformer group drag.
- const { selectedIds: selIds, objects: currentObjs } = useLabelStore.getState();
+ const state = useLabelStore.getState();
+ const selIds = state.selectedIds;
+ const currentObjs = currentObjects(state);
if (
selIds.length > 1 &&
selIds.includes(id) &&
@@ -303,7 +308,7 @@ export function LabelCanvas({
const objId = node.id();
if (!objId || !stageRef.current) return;
- const { objects: objs } = useLabelStore.getState();
+ const objs = currentObjects(useLabelStore.getState());
const obj = objs.find((o) => o.id === objId);
if (!obj) return;
@@ -437,6 +442,8 @@ export function LabelCanvas({
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
+
+
{/* Bottom-right controls: view options + zoom */}