mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: add shape fill/stroke transparency, RGBA color picker, and dash styles
Closes #193. Shapes now support no-fill and no-stroke toggles for drawing outlines or fill-only shapes. Adds RGBA color picker with opacity control, stroke dash styles (solid/dashed/dotted), and i18n for all shape labels.
This commit is contained in:
@@ -0,0 +1,208 @@
|
|||||||
|
// apps/web/src/components/editor/common/shape-color-picker.tsx
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { RgbaColorPicker } from "react-colorful";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const CHECKERBOARD =
|
||||||
|
"repeating-conic-gradient(rgba(128,128,128,0.3) 0% 25%, transparent 0% 50%) 0 0 / 8px 8px";
|
||||||
|
|
||||||
|
function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
||||||
|
const r = Number.parseInt(hex.slice(1, 3), 16);
|
||||||
|
const g = Number.parseInt(hex.slice(3, 5), 16);
|
||||||
|
const b = Number.parseInt(hex.slice(5, 7), 16);
|
||||||
|
return { r, g, b };
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgbToHex(r: number, g: number, b: number): string {
|
||||||
|
const toHex = (n: number) => n.toString(16).padStart(2, "0");
|
||||||
|
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShapeColorPickerProps {
|
||||||
|
label: string;
|
||||||
|
color: string | null;
|
||||||
|
opacity: number;
|
||||||
|
onColorChange: (color: string | null) => void;
|
||||||
|
onOpacityChange: (opacity: number) => void;
|
||||||
|
allowNone?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShapeColorPicker({
|
||||||
|
label,
|
||||||
|
color,
|
||||||
|
opacity,
|
||||||
|
onColorChange,
|
||||||
|
onOpacityChange,
|
||||||
|
allowNone = true,
|
||||||
|
}: ShapeColorPickerProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [hexInput, setHexInput] = useState(color ?? "#000000");
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [popoverPos, setPopoverPos] = useState({ top: 0, left: 0 });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (color) setHexInput(color);
|
||||||
|
}, [color]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
|
||||||
|
if (buttonRef.current) {
|
||||||
|
const rect = buttonRef.current.getBoundingClientRect();
|
||||||
|
setPopoverPos({ top: rect.bottom + 6, left: rect.left });
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
const target = e.target as Node;
|
||||||
|
if (
|
||||||
|
popoverRef.current &&
|
||||||
|
!popoverRef.current.contains(target) &&
|
||||||
|
buttonRef.current &&
|
||||||
|
!buttonRef.current.contains(target)
|
||||||
|
) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
document.addEventListener("keydown", handleEscape);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
document.removeEventListener("keydown", handleEscape);
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const rgbaValue = color ? { ...hexToRgb(color), a: opacity } : { r: 0, g: 0, b: 0, a: 0 };
|
||||||
|
|
||||||
|
const handleRgbaChange = useCallback(
|
||||||
|
(rgba: { r: number; g: number; b: number; a: number }) => {
|
||||||
|
const hex = rgbToHex(rgba.r, rgba.g, rgba.b);
|
||||||
|
onColorChange(hex);
|
||||||
|
onOpacityChange(rgba.a);
|
||||||
|
setHexInput(hex);
|
||||||
|
},
|
||||||
|
[onColorChange, onOpacityChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleHexCommit = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
let hex = value.trim();
|
||||||
|
if (!hex.startsWith("#")) hex = `#${hex}`;
|
||||||
|
if (/^#[0-9a-f]{6}$/i.test(hex)) {
|
||||||
|
onColorChange(hex.toLowerCase());
|
||||||
|
setHexInput(hex.toLowerCase());
|
||||||
|
} else {
|
||||||
|
setHexInput(color ?? "#000000");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[color, onColorChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isNone = color === null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex items-center gap-1.5">
|
||||||
|
<span className="text-xs text-muted-foreground">{label}</span>
|
||||||
|
<button
|
||||||
|
ref={buttonRef}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
className={cn(
|
||||||
|
"relative w-6 h-6 rounded border border-border cursor-pointer",
|
||||||
|
"hover:ring-1 hover:ring-primary/50 transition-shadow shrink-0",
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
background: isNone ? undefined : CHECKERBOARD,
|
||||||
|
}}
|
||||||
|
aria-label={`${label} color picker`}
|
||||||
|
>
|
||||||
|
{isNone ? (
|
||||||
|
<span className="absolute inset-0 rounded bg-white">
|
||||||
|
<span
|
||||||
|
className="absolute inset-0 rounded"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(to top right, transparent calc(50% - 1px), #ef4444 calc(50% - 1px), #ef4444 calc(50% + 1px), transparent calc(50% + 1px))",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className="absolute inset-0 rounded"
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
opacity,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{open &&
|
||||||
|
createPortal(
|
||||||
|
<div
|
||||||
|
ref={popoverRef}
|
||||||
|
className={cn(
|
||||||
|
"fixed z-[9999] p-3 rounded-lg shadow-xl",
|
||||||
|
"bg-card border border-border w-[220px]",
|
||||||
|
)}
|
||||||
|
style={{ top: popoverPos.top, left: popoverPos.left }}
|
||||||
|
>
|
||||||
|
{allowNone && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (isNone) {
|
||||||
|
onColorChange("#000000");
|
||||||
|
setHexInput("#000000");
|
||||||
|
} else {
|
||||||
|
onColorChange(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"w-full mb-2 px-2 py-1 text-xs rounded border transition-colors",
|
||||||
|
isNone
|
||||||
|
? "bg-primary text-primary-foreground border-primary"
|
||||||
|
: "bg-muted text-muted-foreground border-border hover:text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isNone ? `${label}: None` : `No ${label}`}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isNone && (
|
||||||
|
<>
|
||||||
|
<div className="[&_.react-colorful]:!w-full [&_.react-colorful]:!h-[150px] rounded overflow-hidden">
|
||||||
|
<RgbaColorPicker color={rgbaValue} onChange={handleRgbaChange} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 flex items-center gap-1.5">
|
||||||
|
<span className="text-[10px] text-muted-foreground font-medium">#</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={hexInput.replace("#", "")}
|
||||||
|
onChange={(e) => setHexInput(`#${e.target.value}`)}
|
||||||
|
onBlur={(e) => handleHexCommit(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") handleHexCommit(e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
maxLength={6}
|
||||||
|
className="flex-1 h-6 text-xs bg-muted border border-border rounded px-1.5 font-mono"
|
||||||
|
/>
|
||||||
|
<span className="text-[10px] text-muted-foreground font-medium ms-1">
|
||||||
|
{Math.round(opacity * 100)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -408,6 +408,7 @@ function CanvasObjectRenderer({
|
|||||||
tension={a.tension}
|
tension={a.tension}
|
||||||
lineCap={a.lineCap}
|
lineCap={a.lineCap}
|
||||||
lineJoin={a.lineJoin}
|
lineJoin={a.lineJoin}
|
||||||
|
dash={a.dash}
|
||||||
opacity={a.opacity}
|
opacity={a.opacity}
|
||||||
globalCompositeOperation={
|
globalCompositeOperation={
|
||||||
a.globalCompositeOperation as "source-over" | "destination-out" | undefined
|
a.globalCompositeOperation as "source-over" | "destination-out" | undefined
|
||||||
@@ -433,6 +434,7 @@ function CanvasObjectRenderer({
|
|||||||
stroke={a.stroke}
|
stroke={a.stroke}
|
||||||
strokeWidth={a.strokeWidth}
|
strokeWidth={a.strokeWidth}
|
||||||
cornerRadius={a.cornerRadius}
|
cornerRadius={a.cornerRadius}
|
||||||
|
dash={a.dash}
|
||||||
rotation={a.rotation}
|
rotation={a.rotation}
|
||||||
opacity={a.opacity}
|
opacity={a.opacity}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
@@ -457,6 +459,7 @@ function CanvasObjectRenderer({
|
|||||||
fill={a.fill}
|
fill={a.fill}
|
||||||
stroke={a.stroke}
|
stroke={a.stroke}
|
||||||
strokeWidth={a.strokeWidth}
|
strokeWidth={a.strokeWidth}
|
||||||
|
dash={a.dash}
|
||||||
rotation={a.rotation}
|
rotation={a.rotation}
|
||||||
opacity={a.opacity}
|
opacity={a.opacity}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
@@ -510,6 +513,7 @@ function CanvasObjectRenderer({
|
|||||||
strokeWidth={a.strokeWidth}
|
strokeWidth={a.strokeWidth}
|
||||||
pointerLength={a.pointerLength}
|
pointerLength={a.pointerLength}
|
||||||
pointerWidth={a.pointerWidth}
|
pointerWidth={a.pointerWidth}
|
||||||
|
dash={a.dash}
|
||||||
rotation={a.rotation}
|
rotation={a.rotation}
|
||||||
opacity={a.opacity}
|
opacity={a.opacity}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
@@ -534,6 +538,7 @@ function CanvasObjectRenderer({
|
|||||||
fill={a.fill}
|
fill={a.fill}
|
||||||
stroke={a.stroke}
|
stroke={a.stroke}
|
||||||
strokeWidth={a.strokeWidth}
|
strokeWidth={a.strokeWidth}
|
||||||
|
dash={a.dash}
|
||||||
rotation={a.rotation}
|
rotation={a.rotation}
|
||||||
opacity={a.opacity}
|
opacity={a.opacity}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
@@ -559,6 +564,7 @@ function CanvasObjectRenderer({
|
|||||||
fill={a.fill}
|
fill={a.fill}
|
||||||
stroke={a.stroke}
|
stroke={a.stroke}
|
||||||
strokeWidth={a.strokeWidth}
|
strokeWidth={a.strokeWidth}
|
||||||
|
dash={a.dash}
|
||||||
rotation={a.rotation}
|
rotation={a.rotation}
|
||||||
opacity={a.opacity}
|
opacity={a.opacity}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
// apps/web/src/components/editor/options/shape-options.tsx
|
// apps/web/src/components/editor/options/shape-options.tsx
|
||||||
|
|
||||||
|
import { useTranslation } from "@/contexts/i18n-context";
|
||||||
import { useEditorStore } from "@/stores/editor-store";
|
import { useEditorStore } from "@/stores/editor-store";
|
||||||
import type { ToolType } from "@/types/editor";
|
import type { StrokeDashStyle, ToolType } from "@/types/editor";
|
||||||
|
import { ShapeColorPicker } from "../common/shape-color-picker";
|
||||||
|
|
||||||
const SHAPE_TOOLS = new Set<ToolType>([
|
const SHAPE_TOOLS = new Set<ToolType>([
|
||||||
"shape-rect",
|
"shape-rect",
|
||||||
@@ -12,38 +14,49 @@ const SHAPE_TOOLS = new Set<ToolType>([
|
|||||||
"shape-star",
|
"shape-star",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const SHAPE_TYPE_OPTIONS: { value: ToolType; label: string }[] = [
|
|
||||||
{ value: "shape-rect", label: "Rectangle" },
|
|
||||||
{ value: "shape-ellipse", label: "Ellipse" },
|
|
||||||
{ value: "shape-line", label: "Line" },
|
|
||||||
{ value: "shape-arrow", label: "Arrow" },
|
|
||||||
{ value: "shape-polygon", label: "Polygon" },
|
|
||||||
{ value: "shape-star", label: "Star" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function ShapeOptions() {
|
export function ShapeOptions() {
|
||||||
const activeTool = useEditorStore((s) => s.activeTool);
|
const { t } = useTranslation();
|
||||||
const setTool = useEditorStore((s) => s.setTool);
|
const s = t.editor.shapes;
|
||||||
const shapeFill = useEditorStore((s) => s.shapeFill);
|
|
||||||
const shapeStroke = useEditorStore((s) => s.shapeStroke);
|
const activeTool = useEditorStore((st) => st.activeTool);
|
||||||
const shapeStrokeWidth = useEditorStore((s) => s.shapeStrokeWidth);
|
const setTool = useEditorStore((st) => st.setTool);
|
||||||
const shapeCornerRadius = useEditorStore((s) => s.shapeCornerRadius);
|
const shapeFill = useEditorStore((st) => st.shapeFill);
|
||||||
const shapePolygonSides = useEditorStore((s) => s.shapePolygonSides);
|
const shapeFillOpacity = useEditorStore((st) => st.shapeFillOpacity);
|
||||||
const shapeStarPoints = useEditorStore((s) => s.shapeStarPoints);
|
const shapeStroke = useEditorStore((st) => st.shapeStroke);
|
||||||
const setShapeFill = useEditorStore((s) => s.setShapeFill);
|
const shapeStrokeOpacity = useEditorStore((st) => st.shapeStrokeOpacity);
|
||||||
const setShapeStroke = useEditorStore((s) => s.setShapeStroke);
|
const shapeStrokeWidth = useEditorStore((st) => st.shapeStrokeWidth);
|
||||||
const setShapeStrokeWidth = useEditorStore((s) => s.setShapeStrokeWidth);
|
const shapeStrokeDash = useEditorStore((st) => st.shapeStrokeDash);
|
||||||
const setShapeCornerRadius = useEditorStore((s) => s.setShapeCornerRadius);
|
const shapeCornerRadius = useEditorStore((st) => st.shapeCornerRadius);
|
||||||
const setShapePolygonSides = useEditorStore((s) => s.setShapePolygonSides);
|
const shapePolygonSides = useEditorStore((st) => st.shapePolygonSides);
|
||||||
const setShapeStarPoints = useEditorStore((s) => s.setShapeStarPoints);
|
const shapeStarPoints = useEditorStore((st) => st.shapeStarPoints);
|
||||||
|
const setShapeFill = useEditorStore((st) => st.setShapeFill);
|
||||||
|
const setShapeFillOpacity = useEditorStore((st) => st.setShapeFillOpacity);
|
||||||
|
const setShapeStroke = useEditorStore((st) => st.setShapeStroke);
|
||||||
|
const setShapeStrokeOpacity = useEditorStore((st) => st.setShapeStrokeOpacity);
|
||||||
|
const setShapeStrokeWidth = useEditorStore((st) => st.setShapeStrokeWidth);
|
||||||
|
const setShapeStrokeDash = useEditorStore((st) => st.setShapeStrokeDash);
|
||||||
|
const setShapeCornerRadius = useEditorStore((st) => st.setShapeCornerRadius);
|
||||||
|
const setShapePolygonSides = useEditorStore((st) => st.setShapePolygonSides);
|
||||||
|
const setShapeStarPoints = useEditorStore((st) => st.setShapeStarPoints);
|
||||||
|
|
||||||
if (!SHAPE_TOOLS.has(activeTool)) return null;
|
if (!SHAPE_TOOLS.has(activeTool)) return null;
|
||||||
|
|
||||||
|
const SHAPE_TYPE_OPTIONS: { value: ToolType; label: string }[] = [
|
||||||
|
{ value: "shape-rect", label: s.rectangle },
|
||||||
|
{ value: "shape-ellipse", label: s.ellipse },
|
||||||
|
{ value: "shape-line", label: s.line },
|
||||||
|
{ value: "shape-arrow", label: s.arrow },
|
||||||
|
{ value: "shape-polygon", label: s.polygon },
|
||||||
|
{ value: "shape-star", label: s.star },
|
||||||
|
];
|
||||||
|
|
||||||
|
const showFill = activeTool !== "shape-line";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* Shape type selector */}
|
{/* Shape type selector */}
|
||||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
Shape
|
{s.shape}
|
||||||
<select
|
<select
|
||||||
value={activeTool}
|
value={activeTool}
|
||||||
onChange={(e) => setTool(e.target.value as ToolType)}
|
onChange={(e) => setTool(e.target.value as ToolType)}
|
||||||
@@ -58,30 +71,28 @@ export function ShapeOptions() {
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
{/* Fill color */}
|
{/* Fill color */}
|
||||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
{showFill && (
|
||||||
Fill
|
<ShapeColorPicker
|
||||||
<input
|
label={s.fill}
|
||||||
type="color"
|
color={shapeFill}
|
||||||
value={shapeFill}
|
opacity={shapeFillOpacity}
|
||||||
onChange={(e) => setShapeFill(e.target.value)}
|
onColorChange={setShapeFill}
|
||||||
className="w-6 h-6 border border-border rounded cursor-pointer"
|
onOpacityChange={setShapeFillOpacity}
|
||||||
/>
|
/>
|
||||||
</label>
|
)}
|
||||||
|
|
||||||
{/* Stroke color */}
|
{/* Stroke color */}
|
||||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
<ShapeColorPicker
|
||||||
Stroke
|
label={s.stroke}
|
||||||
<input
|
color={shapeStroke}
|
||||||
type="color"
|
opacity={shapeStrokeOpacity}
|
||||||
value={shapeStroke}
|
onColorChange={setShapeStroke}
|
||||||
onChange={(e) => setShapeStroke(e.target.value)}
|
onOpacityChange={setShapeStrokeOpacity}
|
||||||
className="w-6 h-6 border border-border rounded cursor-pointer"
|
/>
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{/* Stroke width */}
|
{/* Stroke width */}
|
||||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
Width
|
{s.strokeWidth}
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min={0}
|
min={0}
|
||||||
@@ -100,10 +111,24 @@ export function ShapeOptions() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
{/* Stroke dash style */}
|
||||||
|
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
|
{s.dashStyle}
|
||||||
|
<select
|
||||||
|
value={shapeStrokeDash}
|
||||||
|
onChange={(e) => setShapeStrokeDash(e.target.value as StrokeDashStyle)}
|
||||||
|
className="h-6 text-xs bg-muted border border-border rounded px-1"
|
||||||
|
>
|
||||||
|
<option value="solid">{s.solid}</option>
|
||||||
|
<option value="dashed">{s.dashed}</option>
|
||||||
|
<option value="dotted">{s.dotted}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
{/* Corner radius (only for rect) */}
|
{/* Corner radius (only for rect) */}
|
||||||
{activeTool === "shape-rect" && (
|
{activeTool === "shape-rect" && (
|
||||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
Radius
|
{s.cornerRadius}
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min={0}
|
min={0}
|
||||||
@@ -126,7 +151,7 @@ export function ShapeOptions() {
|
|||||||
{/* Polygon sides */}
|
{/* Polygon sides */}
|
||||||
{activeTool === "shape-polygon" && (
|
{activeTool === "shape-polygon" && (
|
||||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
Sides
|
{s.sides}
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={3}
|
min={3}
|
||||||
@@ -141,7 +166,7 @@ export function ShapeOptions() {
|
|||||||
{/* Star points */}
|
{/* Star points */}
|
||||||
{activeTool === "shape-star" && (
|
{activeTool === "shape-star" && (
|
||||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
Points
|
{s.points}
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={3}
|
min={3}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import type Konva from "konva";
|
import type Konva from "konva";
|
||||||
import { useCallback, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { generateId } from "@/lib/utils";
|
import { generateId } from "@/lib/utils";
|
||||||
import { useEditorStore } from "@/stores/editor-store";
|
import { dashStyleToArray, hexToRgba, useEditorStore } from "@/stores/editor-store";
|
||||||
import type { CanvasObject, ToolType } from "@/types/editor";
|
import type { CanvasObject, ToolType } from "@/types/editor";
|
||||||
|
|
||||||
interface PendingShape {
|
interface PendingShape {
|
||||||
@@ -46,6 +46,22 @@ function constrainToDimension(
|
|||||||
return { w, h };
|
return { w, h };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function computeShapeColors(state: {
|
||||||
|
shapeFill: string | null;
|
||||||
|
shapeFillOpacity: number;
|
||||||
|
shapeStroke: string | null;
|
||||||
|
shapeStrokeOpacity: number;
|
||||||
|
shapeStrokeDash: "solid" | "dashed" | "dotted";
|
||||||
|
shapeStrokeWidth: number;
|
||||||
|
}) {
|
||||||
|
const fill = state.shapeFill ? hexToRgba(state.shapeFill, state.shapeFillOpacity) : undefined;
|
||||||
|
const stroke = state.shapeStroke
|
||||||
|
? hexToRgba(state.shapeStroke, state.shapeStrokeOpacity)
|
||||||
|
: undefined;
|
||||||
|
const dash = dashStyleToArray(state.shapeStrokeDash, state.shapeStrokeWidth);
|
||||||
|
return { fill, stroke, dash };
|
||||||
|
}
|
||||||
|
|
||||||
export function useShapeTool() {
|
export function useShapeTool() {
|
||||||
const dragRef = useRef<DragState | null>(null);
|
const dragRef = useRef<DragState | null>(null);
|
||||||
const pendingRef = useRef<PendingShape | null>(null);
|
const pendingRef = useRef<PendingShape | null>(null);
|
||||||
@@ -59,8 +75,11 @@ export function useShapeTool() {
|
|||||||
const {
|
const {
|
||||||
activeTool,
|
activeTool,
|
||||||
shapeFill,
|
shapeFill,
|
||||||
|
shapeFillOpacity,
|
||||||
shapeStroke,
|
shapeStroke,
|
||||||
|
shapeStrokeOpacity,
|
||||||
shapeStrokeWidth,
|
shapeStrokeWidth,
|
||||||
|
shapeStrokeDash,
|
||||||
shapeCornerRadius,
|
shapeCornerRadius,
|
||||||
shapePolygonSides,
|
shapePolygonSides,
|
||||||
shapeStarPoints,
|
shapeStarPoints,
|
||||||
@@ -78,6 +97,14 @@ export function useShapeTool() {
|
|||||||
const y = (pointer.y - panOffset.y) / zoom;
|
const y = (pointer.y - panOffset.y) / zoom;
|
||||||
|
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
|
const { fill, stroke, dash } = computeShapeColors({
|
||||||
|
shapeFill,
|
||||||
|
shapeFillOpacity,
|
||||||
|
shapeStroke,
|
||||||
|
shapeStrokeOpacity,
|
||||||
|
shapeStrokeDash,
|
||||||
|
shapeStrokeWidth,
|
||||||
|
});
|
||||||
let obj: CanvasObject;
|
let obj: CanvasObject;
|
||||||
|
|
||||||
switch (activeTool) {
|
switch (activeTool) {
|
||||||
@@ -91,10 +118,11 @@ export function useShapeTool() {
|
|||||||
y,
|
y,
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
fill: shapeFill,
|
fill,
|
||||||
stroke: shapeStroke,
|
stroke,
|
||||||
strokeWidth: shapeStrokeWidth,
|
strokeWidth: shapeStrokeWidth,
|
||||||
cornerRadius: shapeCornerRadius,
|
cornerRadius: shapeCornerRadius,
|
||||||
|
dash,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
@@ -110,9 +138,10 @@ export function useShapeTool() {
|
|||||||
y,
|
y,
|
||||||
radiusX: 0,
|
radiusX: 0,
|
||||||
radiusY: 0,
|
radiusY: 0,
|
||||||
fill: shapeFill,
|
fill,
|
||||||
stroke: shapeStroke,
|
stroke,
|
||||||
strokeWidth: shapeStrokeWidth,
|
strokeWidth: shapeStrokeWidth,
|
||||||
|
dash,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
@@ -125,11 +154,12 @@ export function useShapeTool() {
|
|||||||
layerId: activeLayerId,
|
layerId: activeLayerId,
|
||||||
attrs: {
|
attrs: {
|
||||||
points: [x, y, x, y],
|
points: [x, y, x, y],
|
||||||
stroke: shapeStroke,
|
stroke,
|
||||||
strokeWidth: shapeStrokeWidth,
|
strokeWidth: shapeStrokeWidth,
|
||||||
tension: 0,
|
tension: 0,
|
||||||
lineCap: "round",
|
lineCap: "round",
|
||||||
lineJoin: "round",
|
lineJoin: "round",
|
||||||
|
dash,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
globalCompositeOperation: "source-over",
|
globalCompositeOperation: "source-over",
|
||||||
},
|
},
|
||||||
@@ -142,11 +172,12 @@ export function useShapeTool() {
|
|||||||
layerId: activeLayerId,
|
layerId: activeLayerId,
|
||||||
attrs: {
|
attrs: {
|
||||||
points: [x, y, x, y],
|
points: [x, y, x, y],
|
||||||
fill: shapeFill,
|
fill,
|
||||||
stroke: shapeStroke,
|
stroke,
|
||||||
strokeWidth: shapeStrokeWidth,
|
strokeWidth: shapeStrokeWidth,
|
||||||
pointerLength: 15,
|
pointerLength: 15,
|
||||||
pointerWidth: 12,
|
pointerWidth: 12,
|
||||||
|
dash,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
@@ -162,9 +193,10 @@ export function useShapeTool() {
|
|||||||
y,
|
y,
|
||||||
sides: shapePolygonSides,
|
sides: shapePolygonSides,
|
||||||
radius: 0,
|
radius: 0,
|
||||||
fill: shapeFill,
|
fill,
|
||||||
stroke: shapeStroke,
|
stroke,
|
||||||
strokeWidth: shapeStrokeWidth,
|
strokeWidth: shapeStrokeWidth,
|
||||||
|
dash,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
@@ -181,9 +213,10 @@ export function useShapeTool() {
|
|||||||
numPoints: shapeStarPoints,
|
numPoints: shapeStarPoints,
|
||||||
innerRadius: 0,
|
innerRadius: 0,
|
||||||
outerRadius: 0,
|
outerRadius: 0,
|
||||||
fill: shapeFill,
|
fill,
|
||||||
stroke: shapeStroke,
|
stroke,
|
||||||
strokeWidth: shapeStrokeWidth,
|
strokeWidth: shapeStrokeWidth,
|
||||||
|
dash,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
@@ -193,12 +226,10 @@ export function useShapeTool() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't add the object yet -- wait until the user drags past the threshold
|
|
||||||
pendingRef.current = { startX: x, startY: y, toolType: activeTool, obj };
|
pendingRef.current = { startX: x, startY: y, toolType: activeTool, obj };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleMouseMove = useCallback((e: Konva.KonvaEventObject<MouseEvent>) => {
|
const handleMouseMove = useCallback((e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||||
// If we have a pending shape but haven't committed it yet, check threshold
|
|
||||||
if (pendingRef.current && !dragRef.current) {
|
if (pendingRef.current && !dragRef.current) {
|
||||||
const stage = e.target.getStage();
|
const stage = e.target.getStage();
|
||||||
if (!stage) return;
|
if (!stage) return;
|
||||||
@@ -214,7 +245,6 @@ export function useShapeTool() {
|
|||||||
|
|
||||||
if (Math.abs(dx) < MIN_DRAG_THRESHOLD && Math.abs(dy) < MIN_DRAG_THRESHOLD) return;
|
if (Math.abs(dx) < MIN_DRAG_THRESHOLD && Math.abs(dy) < MIN_DRAG_THRESHOLD) return;
|
||||||
|
|
||||||
// Threshold exceeded -- add the object to the store and promote to active drag
|
|
||||||
const { obj, startX, startY, toolType } = pendingRef.current;
|
const { obj, startX, startY, toolType } = pendingRef.current;
|
||||||
useEditorStore.getState().addObject(obj);
|
useEditorStore.getState().addObject(obj);
|
||||||
dragRef.current = { startX, startY, objectId: obj.id, toolType };
|
dragRef.current = { startX, startY, objectId: obj.id, toolType };
|
||||||
@@ -313,7 +343,6 @@ export function useShapeTool() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleMouseUp = useCallback(() => {
|
const handleMouseUp = useCallback(() => {
|
||||||
// Click without drag -- pending shape was never added, just discard it
|
|
||||||
if (pendingRef.current) {
|
if (pendingRef.current) {
|
||||||
pendingRef.current = null;
|
pendingRef.current = null;
|
||||||
}
|
}
|
||||||
@@ -325,7 +354,6 @@ export function useShapeTool() {
|
|||||||
const obj = objects.find((o) => o.id === objectId);
|
const obj = objects.find((o) => o.id === objectId);
|
||||||
|
|
||||||
if (obj) {
|
if (obj) {
|
||||||
// Remove degenerate shapes that are still too small
|
|
||||||
const attrs = obj.attrs;
|
const attrs = obj.attrs;
|
||||||
let isDegenerate = false;
|
let isDegenerate = false;
|
||||||
if ("width" in attrs && "height" in attrs) {
|
if ("width" in attrs && "height" in attrs) {
|
||||||
|
|||||||
@@ -10,9 +10,31 @@ import type {
|
|||||||
EditorState,
|
EditorState,
|
||||||
FilterConfig,
|
FilterConfig,
|
||||||
SelectionMode,
|
SelectionMode,
|
||||||
|
StrokeDashStyle,
|
||||||
ToolType,
|
ToolType,
|
||||||
} from "@/types/editor";
|
} from "@/types/editor";
|
||||||
|
|
||||||
|
export function hexToRgba(hex: string, opacity: number): string {
|
||||||
|
const r = Number.parseInt(hex.slice(1, 3), 16);
|
||||||
|
const g = Number.parseInt(hex.slice(3, 5), 16);
|
||||||
|
const b = Number.parseInt(hex.slice(5, 7), 16);
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dashStyleToArray(
|
||||||
|
style: StrokeDashStyle,
|
||||||
|
strokeWidth: number,
|
||||||
|
): number[] | undefined {
|
||||||
|
switch (style) {
|
||||||
|
case "dashed":
|
||||||
|
return [strokeWidth * 4, strokeWidth * 2];
|
||||||
|
case "dotted":
|
||||||
|
return [strokeWidth, strokeWidth * 2];
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helpers for objects that use points arrays (line, arrow) vs positioned objects
|
// Helpers for objects that use points arrays (line, arrow) vs positioned objects
|
||||||
function hasPointsArray(obj: CanvasObject): obj is CanvasObject & { attrs: { points: number[] } } {
|
function hasPointsArray(obj: CanvasObject): obj is CanvasObject & { attrs: { points: number[] } } {
|
||||||
return "points" in obj.attrs && Array.isArray((obj.attrs as { points: number[] }).points);
|
return "points" in obj.attrs && Array.isArray((obj.attrs as { points: number[] }).points);
|
||||||
@@ -165,9 +187,12 @@ export const useEditorStore = create<EditorState & EditorStateExtensions>()(
|
|||||||
editingTextId: null,
|
editingTextId: null,
|
||||||
|
|
||||||
// --- Shape settings ---
|
// --- Shape settings ---
|
||||||
shapeFill: "#3b82f6",
|
shapeFill: "#3b82f6" as string | null,
|
||||||
shapeStroke: "#000000",
|
shapeFillOpacity: 1,
|
||||||
|
shapeStroke: "#000000" as string | null,
|
||||||
|
shapeStrokeOpacity: 1,
|
||||||
shapeStrokeWidth: 2,
|
shapeStrokeWidth: 2,
|
||||||
|
shapeStrokeDash: "solid" as const,
|
||||||
shapeCornerRadius: 0,
|
shapeCornerRadius: 0,
|
||||||
shapePolygonSides: 6,
|
shapePolygonSides: 6,
|
||||||
shapeStarPoints: 5,
|
shapeStarPoints: 5,
|
||||||
@@ -1148,8 +1173,13 @@ export const useEditorStore = create<EditorState & EditorStateExtensions>()(
|
|||||||
|
|
||||||
// Shape settings
|
// Shape settings
|
||||||
setShapeFill: (fill) => set({ shapeFill: fill }),
|
setShapeFill: (fill) => set({ shapeFill: fill }),
|
||||||
|
setShapeFillOpacity: (opacity) =>
|
||||||
|
set({ shapeFillOpacity: Math.max(0, Math.min(1, opacity)) }),
|
||||||
setShapeStroke: (stroke) => set({ shapeStroke: stroke }),
|
setShapeStroke: (stroke) => set({ shapeStroke: stroke }),
|
||||||
|
setShapeStrokeOpacity: (opacity) =>
|
||||||
|
set({ shapeStrokeOpacity: Math.max(0, Math.min(1, opacity)) }),
|
||||||
setShapeStrokeWidth: (width) => set({ shapeStrokeWidth: width }),
|
setShapeStrokeWidth: (width) => set({ shapeStrokeWidth: width }),
|
||||||
|
setShapeStrokeDash: (dash) => set({ shapeStrokeDash: dash }),
|
||||||
setShapeCornerRadius: (radius) => set({ shapeCornerRadius: radius }),
|
setShapeCornerRadius: (radius) => set({ shapeCornerRadius: radius }),
|
||||||
setShapePolygonSides: (sides) => set({ shapePolygonSides: sides }),
|
setShapePolygonSides: (sides) => set({ shapePolygonSides: sides }),
|
||||||
setShapeStarPoints: (points) => set({ shapeStarPoints: points }),
|
setShapeStarPoints: (points) => set({ shapeStarPoints: points }),
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
export type SelectionMode = "new" | "add" | "subtract";
|
export type SelectionMode = "new" | "add" | "subtract";
|
||||||
|
|
||||||
|
export type StrokeDashStyle = "solid" | "dashed" | "dotted";
|
||||||
|
|
||||||
export type ToolType =
|
export type ToolType =
|
||||||
| "move"
|
| "move"
|
||||||
| "marquee-rect"
|
| "marquee-rect"
|
||||||
@@ -36,11 +38,12 @@ export type ToolType =
|
|||||||
|
|
||||||
export interface LineAttrs {
|
export interface LineAttrs {
|
||||||
points: number[];
|
points: number[];
|
||||||
stroke: string;
|
stroke?: string;
|
||||||
strokeWidth: number;
|
strokeWidth: number;
|
||||||
tension: number;
|
tension: number;
|
||||||
lineCap: "butt" | "round" | "square";
|
lineCap: "butt" | "round" | "square";
|
||||||
lineJoin: "bevel" | "round" | "miter";
|
lineJoin: "bevel" | "round" | "miter";
|
||||||
|
dash?: number[];
|
||||||
opacity: number;
|
opacity: number;
|
||||||
globalCompositeOperation: string;
|
globalCompositeOperation: string;
|
||||||
shadowBlur?: number;
|
shadowBlur?: number;
|
||||||
@@ -54,10 +57,11 @@ export interface RectAttrs {
|
|||||||
y: number;
|
y: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
fill: string;
|
fill?: string;
|
||||||
stroke: string;
|
stroke?: string;
|
||||||
strokeWidth: number;
|
strokeWidth: number;
|
||||||
cornerRadius: number;
|
cornerRadius: number;
|
||||||
|
dash?: number[];
|
||||||
rotation: number;
|
rotation: number;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
}
|
}
|
||||||
@@ -67,9 +71,10 @@ export interface EllipseAttrs {
|
|||||||
y: number;
|
y: number;
|
||||||
radiusX: number;
|
radiusX: number;
|
||||||
radiusY: number;
|
radiusY: number;
|
||||||
fill: string;
|
fill?: string;
|
||||||
stroke: string;
|
stroke?: string;
|
||||||
strokeWidth: number;
|
strokeWidth: number;
|
||||||
|
dash?: number[];
|
||||||
rotation: number;
|
rotation: number;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
}
|
}
|
||||||
@@ -106,11 +111,12 @@ export interface ImageAttrs {
|
|||||||
|
|
||||||
export interface ArrowAttrs {
|
export interface ArrowAttrs {
|
||||||
points: number[];
|
points: number[];
|
||||||
fill: string;
|
fill?: string;
|
||||||
stroke: string;
|
stroke?: string;
|
||||||
strokeWidth: number;
|
strokeWidth: number;
|
||||||
pointerLength: number;
|
pointerLength: number;
|
||||||
pointerWidth: number;
|
pointerWidth: number;
|
||||||
|
dash?: number[];
|
||||||
rotation: number;
|
rotation: number;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
}
|
}
|
||||||
@@ -120,9 +126,10 @@ export interface PolygonAttrs {
|
|||||||
y: number;
|
y: number;
|
||||||
sides: number;
|
sides: number;
|
||||||
radius: number;
|
radius: number;
|
||||||
fill: string;
|
fill?: string;
|
||||||
stroke: string;
|
stroke?: string;
|
||||||
strokeWidth: number;
|
strokeWidth: number;
|
||||||
|
dash?: number[];
|
||||||
rotation: number;
|
rotation: number;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
}
|
}
|
||||||
@@ -133,9 +140,10 @@ export interface StarAttrs {
|
|||||||
numPoints: number;
|
numPoints: number;
|
||||||
innerRadius: number;
|
innerRadius: number;
|
||||||
outerRadius: number;
|
outerRadius: number;
|
||||||
fill: string;
|
fill?: string;
|
||||||
stroke: string;
|
stroke?: string;
|
||||||
strokeWidth: number;
|
strokeWidth: number;
|
||||||
|
dash?: number[];
|
||||||
rotation: number;
|
rotation: number;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
}
|
}
|
||||||
@@ -305,9 +313,12 @@ export interface EditorState {
|
|||||||
editingTextId: string | null;
|
editingTextId: string | null;
|
||||||
|
|
||||||
// Shape settings
|
// Shape settings
|
||||||
shapeFill: string;
|
shapeFill: string | null;
|
||||||
shapeStroke: string;
|
shapeFillOpacity: number;
|
||||||
|
shapeStroke: string | null;
|
||||||
|
shapeStrokeOpacity: number;
|
||||||
shapeStrokeWidth: number;
|
shapeStrokeWidth: number;
|
||||||
|
shapeStrokeDash: StrokeDashStyle;
|
||||||
shapeCornerRadius: number;
|
shapeCornerRadius: number;
|
||||||
shapePolygonSides: number;
|
shapePolygonSides: number;
|
||||||
shapeStarPoints: number;
|
shapeStarPoints: number;
|
||||||
@@ -455,9 +466,12 @@ export interface EditorState {
|
|||||||
setSpongeFlow: (flow: number) => void;
|
setSpongeFlow: (flow: number) => void;
|
||||||
|
|
||||||
// Shape settings
|
// Shape settings
|
||||||
setShapeFill: (fill: string) => void;
|
setShapeFill: (fill: string | null) => void;
|
||||||
setShapeStroke: (stroke: string) => void;
|
setShapeFillOpacity: (opacity: number) => void;
|
||||||
|
setShapeStroke: (stroke: string | null) => void;
|
||||||
|
setShapeStrokeOpacity: (opacity: number) => void;
|
||||||
setShapeStrokeWidth: (width: number) => void;
|
setShapeStrokeWidth: (width: number) => void;
|
||||||
|
setShapeStrokeDash: (dash: StrokeDashStyle) => void;
|
||||||
setShapeCornerRadius: (radius: number) => void;
|
setShapeCornerRadius: (radius: number) => void;
|
||||||
setShapePolygonSides: (sides: number) => void;
|
setShapePolygonSides: (sides: number) => void;
|
||||||
setShapeStarPoints: (points: number) => void;
|
setShapeStarPoints: (points: number) => void;
|
||||||
|
|||||||
@@ -1365,6 +1365,27 @@ export const ar: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "فشل فك الترميز من الخادم",
|
serverDecodeFailed: "فشل فك الترميز من الخادم",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "الإعدادات",
|
heading: "الإعدادات",
|
||||||
|
|||||||
@@ -1379,6 +1379,27 @@ export const de: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "Server-Dekodierung fehlgeschlagen",
|
serverDecodeFailed: "Server-Dekodierung fehlgeschlagen",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "Einstellungen",
|
heading: "Einstellungen",
|
||||||
|
|||||||
@@ -1324,6 +1324,27 @@ export const en = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "Server decode failed",
|
serverDecodeFailed: "Server decode failed",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "Settings",
|
heading: "Settings",
|
||||||
|
|||||||
@@ -1361,6 +1361,27 @@ export const es: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "Error de decodificacion del servidor",
|
serverDecodeFailed: "Error de decodificacion del servidor",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "Configuracion",
|
heading: "Configuracion",
|
||||||
|
|||||||
@@ -1380,6 +1380,27 @@ export const fr: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "Echec du decodage serveur",
|
serverDecodeFailed: "Echec du decodage serveur",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "Parametres",
|
heading: "Parametres",
|
||||||
|
|||||||
@@ -1362,6 +1362,27 @@ export const hi: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "सर्वर डीकोड विफल",
|
serverDecodeFailed: "सर्वर डीकोड विफल",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "सेटिंग्स",
|
heading: "सेटिंग्स",
|
||||||
|
|||||||
@@ -1374,6 +1374,27 @@ export const id: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "Dekoding server gagal",
|
serverDecodeFailed: "Dekoding server gagal",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "Pengaturan",
|
heading: "Pengaturan",
|
||||||
|
|||||||
@@ -1373,6 +1373,27 @@ export const it: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "Decodifica del server non riuscita",
|
serverDecodeFailed: "Decodifica del server non riuscita",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "Impostazioni",
|
heading: "Impostazioni",
|
||||||
|
|||||||
@@ -1332,6 +1332,27 @@ export const ja: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "サーバーでのデコードに失敗しました",
|
serverDecodeFailed: "サーバーでのデコードに失敗しました",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "設定",
|
heading: "設定",
|
||||||
|
|||||||
@@ -1317,6 +1317,27 @@ export const ko: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "서버 디코딩 실패",
|
serverDecodeFailed: "서버 디코딩 실패",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "설정",
|
heading: "설정",
|
||||||
|
|||||||
@@ -1376,6 +1376,27 @@ export const nl: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "Server-decodering mislukt",
|
serverDecodeFailed: "Server-decodering mislukt",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "Instellingen",
|
heading: "Instellingen",
|
||||||
|
|||||||
@@ -1377,6 +1377,27 @@ export const pl: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "Błąd dekodowania na serwerze",
|
serverDecodeFailed: "Błąd dekodowania na serwerze",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "Ustawienia",
|
heading: "Ustawienia",
|
||||||
|
|||||||
@@ -1373,6 +1373,27 @@ export const ptBR: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "Falha na decodificacao do servidor",
|
serverDecodeFailed: "Falha na decodificacao do servidor",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "Configuracoes",
|
heading: "Configuracoes",
|
||||||
|
|||||||
@@ -1375,6 +1375,27 @@ export const ru: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "Ошибка декодирования на сервере",
|
serverDecodeFailed: "Ошибка декодирования на сервере",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "Настройки",
|
heading: "Настройки",
|
||||||
|
|||||||
@@ -1372,6 +1372,27 @@ export const sv: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "Serveravkodning misslyckades",
|
serverDecodeFailed: "Serveravkodning misslyckades",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "Installningar",
|
heading: "Installningar",
|
||||||
|
|||||||
@@ -1354,6 +1354,27 @@ export const th: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "การถอดรหัสจากเซิร์ฟเวอร์ล้มเหลว",
|
serverDecodeFailed: "การถอดรหัสจากเซิร์ฟเวอร์ล้มเหลว",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "ตั้งค่า",
|
heading: "ตั้งค่า",
|
||||||
|
|||||||
@@ -1377,6 +1377,27 @@ export const tr: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "Sunucu çözümleme hatası",
|
serverDecodeFailed: "Sunucu çözümleme hatası",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "Ayarlar",
|
heading: "Ayarlar",
|
||||||
|
|||||||
@@ -1375,6 +1375,27 @@ export const uk: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "Помилка декодування на сервері",
|
serverDecodeFailed: "Помилка декодування на сервері",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "Налаштування",
|
heading: "Налаштування",
|
||||||
|
|||||||
@@ -1374,6 +1374,27 @@ export const vi: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "Máy chủ giải mã thất bại",
|
serverDecodeFailed: "Máy chủ giải mã thất bại",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "Cài đặt",
|
heading: "Cài đặt",
|
||||||
|
|||||||
@@ -1306,6 +1306,27 @@ export const zhCN: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "服务器解码失败",
|
serverDecodeFailed: "服务器解码失败",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "设置",
|
heading: "设置",
|
||||||
|
|||||||
@@ -1304,6 +1304,27 @@ export const zhTW: TranslationKeys = {
|
|||||||
errors: {
|
errors: {
|
||||||
serverDecodeFailed: "伺服器解碼失敗",
|
serverDecodeFailed: "伺服器解碼失敗",
|
||||||
},
|
},
|
||||||
|
shapes: {
|
||||||
|
shape: "Shape",
|
||||||
|
fill: "Fill",
|
||||||
|
stroke: "Stroke",
|
||||||
|
strokeWidth: "Width",
|
||||||
|
cornerRadius: "Radius",
|
||||||
|
sides: "Sides",
|
||||||
|
points: "Points",
|
||||||
|
dashStyle: "Dash",
|
||||||
|
solid: "Solid",
|
||||||
|
dashed: "Dashed",
|
||||||
|
dotted: "Dotted",
|
||||||
|
none: "None",
|
||||||
|
opacity: "Opacity",
|
||||||
|
rectangle: "Rectangle",
|
||||||
|
ellipse: "Ellipse",
|
||||||
|
line: "Line",
|
||||||
|
arrow: "Arrow",
|
||||||
|
polygon: "Polygon",
|
||||||
|
star: "Star",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
heading: "設定",
|
heading: "設定",
|
||||||
|
|||||||
Reference in New Issue
Block a user