diff --git a/src/components/Canvas/KonvaObject.tsx b/src/components/Canvas/KonvaObject.tsx index f7ff624..994b0e5 100644 --- a/src/components/Canvas/KonvaObject.tsx +++ b/src/components/Canvas/KonvaObject.tsx @@ -1,6 +1,6 @@ import { useMemo } from "react"; import { useFontCacheVersion } from "../../hooks/useFontCacheVersion"; -import { Ellipse, Group, Rect, Text } from "react-konva"; +import { Ellipse, Group, Line, Rect, Text } from "react-konva"; import { lookupBoundVariable, shouldShowFallbackTint } from "../../lib/variableBinding"; import { BarcodeObject } from "./BarcodeObject"; import { LineObject } from "./LineObject"; @@ -348,6 +348,29 @@ function KonvaObjectInner({ stroke={isSelected ? colors.selection : undefined} strokeWidth={isSelected ? 1 : 0} /> + {/* ^FB wrap-guide: a dashed vertical line at blockWidth so + the user sees where the printer will break the text. + Only shown while selected — clutter-free for the common + view, immediate feedback when adjusting blockWidth. + Height = (lines * line-step) where line-step is the font + cap height plus the user-configured blockLineSpacing + (dots), matching how Zebra advances the y-cursor between + wrapped rows. */} + {obj.type === "text" && isSelected && obj.props.blockWidth ? (() => { + const lines = obj.props.blockLines ?? 1; + const spacingPx = dotsToPx(obj.props.blockLineSpacing ?? 0, scale, dpmm); + const lineStep = fontSizePx + spacingPx; + const guideX = dotsToPx(obj.props.blockWidth, scale, dpmm); + return ( + + ); + })() : null} ); } diff --git a/src/components/Properties/BlockTextSettings.tsx b/src/components/Properties/BlockTextSettings.tsx new file mode 100644 index 0000000..5ac4a02 --- /dev/null +++ b/src/components/Properties/BlockTextSettings.tsx @@ -0,0 +1,69 @@ +import { useT } from "../../lib/useT"; +import { labelCls } from "./styles"; +import { NumberInput } from "./NumberInput"; +import { JustifyButtons } from "./JustifyButtons"; +import type { TextProps } from "../../registry/text"; + +interface Props { + props: TextProps; + onChange: (patch: Partial) => void; +} + +/** Block-text (`^FB`) sub-panel: width / max-lines / justify / + * line-spacing plus inline validation hints. Hints are purely + * informational — never auto-correct user-set blockLines, since + * content can legitimately have fewer lines than max (CSV-bound + * rows vary) or more (intentional truncation). The editor surfaces + * the mismatch but leaves the choice to the user. */ +export function BlockTextSettings({ props: p, onChange }: Props) { + const t = useT(); + const contentLines = p.content.split("\n").length; + const maxLines = p.blockLines ?? 1; + const truncates = contentLines > maxLines; + return ( + <> +
+ onChange({ blockWidth })} + /> + onChange({ blockLines })} + /> +
+
+
+ + onChange({ blockJustify })} + /> +
+ onChange({ blockLineSpacing })} + /> +
+ {truncates && ( +

+ {t.registry.text.blockLinesExceededFmt + .replaceAll("{n}", String(contentLines)) + .replaceAll("{max}", String(maxLines))} +

+ )} + {!truncates && contentLines < maxLines && ( +

+ {t.registry.text.blockLinesUsageFmt + .replaceAll("{n}", String(contentLines)) + .replaceAll("{max}", String(maxLines))} +

+ )} + + ); +} diff --git a/src/components/Properties/JustifyButtons.tsx b/src/components/Properties/JustifyButtons.tsx new file mode 100644 index 0000000..15faed6 --- /dev/null +++ b/src/components/Properties/JustifyButtons.tsx @@ -0,0 +1,84 @@ +import type { ReactNode } from "react"; +import { useT } from "../../lib/useT"; +import type { TextProps } from "../../registry/text"; + +type Justify = NonNullable; + +interface Props { + value: Justify; + onChange: (next: Justify) => void; +} + +/** Inline SVG glyphs for the four justify modes. Stroke uses + * `currentColor` so the active/inactive button colour drives the + * icon — single source of truth for theming. Inline SVG (vs. + * Unicode glyphs) avoids font-fallback tofu on systems without the + * niche math/arrow ranges installed. */ +const ICONS: Record = { + L: ( + + + + + + ), + C: ( + + + + + + ), + R: ( + + + + + + ), + J: ( + + + + + + ), +}; + +/** ^FB text-justification toggle: 4 icon buttons (left / centre / + * right / justified) — same MS Word pattern users already know. + * Replaces the legacy ` + onChange( + e.target.checked + ? { + blockWidth: 400, + blockLines: 3, + blockLineSpacing: 0, + blockJustify: "L", + } + : { + blockWidth: undefined, + blockLines: undefined, + blockLineSpacing: undefined, + blockJustify: undefined, + }, + ) + } + /> + {t.registry.text.fieldBlock} + + + {!!p.blockWidth && } +
= { /> {t.registry.text.reverse} - - - - {!!p.blockWidth && ( - <> -
- onChange({ blockWidth })} - /> - onChange({ blockLines })} - /> -
-
-
- - -
- onChange({ blockLineSpacing })} - /> -
- - )}
); },