bsign-customization-full 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +2 -0
- package/components.json +21 -0
- package/dist/colors/anthracite-gray.webp +0 -0
- package/dist/colors/anthracite-gray_50x50.png +0 -0
- package/dist/colors/dark-wenge.webp +0 -0
- package/dist/colors/dark-wenge_50x50.png +0 -0
- package/dist/colors/indian-rosewood.webp +0 -0
- package/dist/colors/indian-rosewood_50x50.png +0 -0
- package/dist/colors/natural-wood.webp +0 -0
- package/dist/colors/natural-wood_50x50.png +0 -0
- package/dist/colors/redwood.webp +0 -0
- package/dist/colors/redwood_50x50.png +0 -0
- package/dist/colors/walnut.webp +0 -0
- package/dist/colors/walnut_50x50.png +0 -0
- package/dist/html2canvas.esm-BJ_egzt0.js +4802 -0
- package/dist/index-Dw5Zc1iD.js +33162 -0
- package/dist/index.es-Co1KNpGS.js +6681 -0
- package/dist/logo.png +0 -0
- package/dist/purify.es-CKpD2xIC.js +552 -0
- package/dist/sign-constructor.es.js +4 -0
- package/dist/sign-constructor.iife.js +171 -0
- package/dist/size-guide.webp +0 -0
- package/dist/size.webp +0 -0
- package/dist/templates/assets/modern/rectangle-black.webp +0 -0
- package/dist/templates/assets/modern/rectangle-white.webp +0 -0
- package/dist/templates/assets/modern/square-black.webp +0 -0
- package/dist/templates/assets/modern/square-white.webp +0 -0
- package/dist/templates/assets/wave.webp +0 -0
- package/dist/templates/jure.webp +0 -0
- package/dist/templates/modern.webp +0 -0
- package/dist/templates/sherwood.webp +0 -0
- package/dist/templates/wave.webp +0 -0
- package/eslint.config.js +23 -0
- package/index.html +13 -0
- package/modern-debug.svg +39 -0
- package/package.json +62 -0
- package/public/colors/anthracite-gray.webp +0 -0
- package/public/colors/anthracite-gray_50x50.png +0 -0
- package/public/colors/dark-wenge.webp +0 -0
- package/public/colors/dark-wenge_50x50.png +0 -0
- package/public/colors/indian-rosewood.webp +0 -0
- package/public/colors/indian-rosewood_50x50.png +0 -0
- package/public/colors/natural-wood.webp +0 -0
- package/public/colors/natural-wood_50x50.png +0 -0
- package/public/colors/redwood.webp +0 -0
- package/public/colors/redwood_50x50.png +0 -0
- package/public/colors/walnut.webp +0 -0
- package/public/colors/walnut_50x50.png +0 -0
- package/public/logo.png +0 -0
- package/public/size-guide.webp +0 -0
- package/public/size.webp +0 -0
- package/public/templates/assets/modern/rectangle-black.webp +0 -0
- package/public/templates/assets/modern/rectangle-white.webp +0 -0
- package/public/templates/assets/modern/square-black.webp +0 -0
- package/public/templates/assets/modern/square-white.webp +0 -0
- package/public/templates/assets/wave.webp +0 -0
- package/public/templates/jure.webp +0 -0
- package/public/templates/modern.webp +0 -0
- package/public/templates/sherwood.webp +0 -0
- package/public/templates/wave.webp +0 -0
- package/src/App.css +43 -0
- package/src/AppDemo2.tsx +257 -0
- package/src/components/cart-panel.tsx +170 -0
- package/src/components/cart-preview.tsx +356 -0
- package/src/components/cart-view.tsx +113 -0
- package/src/components/constructure-menu.tsx +37 -0
- package/src/components/header.tsx +214 -0
- package/src/components/heading.tsx +28 -0
- package/src/components/icons.tsx +54 -0
- package/src/components/import-file-modal.tsx +252 -0
- package/src/components/layers/grid-view.tsx +29 -0
- package/src/components/layers/image-layer.tsx +128 -0
- package/src/components/layers/layer-forms/material-form.tsx +53 -0
- package/src/components/layers/layer-forms/size-form.tsx +69 -0
- package/src/components/layers/layer-forms/template-form.tsx +39 -0
- package/src/components/layers/layer-forms/text-form.tsx +477 -0
- package/src/components/layers/layers-container.tsx +259 -0
- package/src/components/layers/text-layer.tsx +128 -0
- package/src/components/movable-item.tsx +228 -0
- package/src/components/preview.tsx +258 -0
- package/src/components/resize-button.tsx +83 -0
- package/src/components/size-guide-modal.tsx +47 -0
- package/src/components/size-guide.tsx +98 -0
- package/src/components/ui/button-group.tsx +83 -0
- package/src/components/ui/button.tsx +60 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/dialog.tsx +151 -0
- package/src/components/ui/input-group.tsx +168 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +22 -0
- package/src/components/ui/popover.tsx +54 -0
- package/src/components/ui/progress.tsx +28 -0
- package/src/components/ui/radio-group.tsx +43 -0
- package/src/components/ui/scroll-area.tsx +56 -0
- package/src/components/ui/select.tsx +191 -0
- package/src/components/ui/separator.tsx +25 -0
- package/src/components/ui/sheet.tsx +141 -0
- package/src/components/ui/slider.tsx +61 -0
- package/src/components/ui/spinner.tsx +15 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle-group.tsx +73 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/tooltip.tsx +67 -0
- package/src/fonts/BEBASNEUE-REGULAR.TTF +0 -0
- package/src/fonts/Braille-Regular.ttf +0 -0
- package/src/fonts/GOTHICB.TTF +0 -0
- package/src/hooks/use-mobile.ts +23 -0
- package/src/hooks/use-resize-constraints.ts +62 -0
- package/src/index.css +238 -0
- package/src/index.tsx +141 -0
- package/src/lib/cart-proposal-pdf.ts +350 -0
- package/src/lib/config-font.tsx +109 -0
- package/src/lib/config.ts +730 -0
- package/src/lib/pricing.ts +61 -0
- package/src/lib/type-checks.ts +47 -0
- package/src/lib/utils.ts +146 -0
- package/src/lib/widget-context.tsx +9 -0
- package/src/main.tsx +11 -0
- package/src/store/cart-store.ts +78 -0
- package/src/store/layers-store.ts +337 -0
- package/src/vite-env.d.ts +1 -0
- package/test/preview.html +37 -0
- package/tsconfig.app.json +33 -0
- package/tsconfig.json +13 -0
- package/tsconfig.node.json +25 -0
- package/vite.config.ts +38 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { useLayersStore } from "../../../store/layers-store";
|
|
2
|
+
import { isTextLayer } from "../../../lib/type-checks";
|
|
3
|
+
import { Label } from "../../ui/label";
|
|
4
|
+
import {
|
|
5
|
+
AUTO_ADD_TEXT_LINE_DENSITY_STEP,
|
|
6
|
+
AUTO_ADD_TEXT_MIN_SCALE_RATIO,
|
|
7
|
+
AUTO_ADD_TEXT_SCALE_STEP,
|
|
8
|
+
BASE_FONT_SIZE,
|
|
9
|
+
BASE_TEXT_SCALE_CHANGE_VALUE,
|
|
10
|
+
getMinScaleByTemplate,
|
|
11
|
+
getMaxLineLengthByPt,
|
|
12
|
+
MAX_SCALE,
|
|
13
|
+
} from "../../../lib/config-font";
|
|
14
|
+
import { getTemplateById, getTemplateMaterial } from "../../../lib/config";
|
|
15
|
+
// AlignJustifyIcon, AlignLeftIcon, AlignRightIcon,
|
|
16
|
+
import { AlignCenterIcon, MinusIcon, Plus, PlusIcon, Trash } from "lucide-react";
|
|
17
|
+
// import { Input } from "../../ui/input";
|
|
18
|
+
import { Button } from "../../ui/button";
|
|
19
|
+
// import { ToggleGroup, ToggleGroupItem } from "../../ui/toggle-group";
|
|
20
|
+
import { Checkbox } from "../../ui/checkbox";
|
|
21
|
+
import { useEffect, useState } from "react";
|
|
22
|
+
// import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover";
|
|
23
|
+
// import { icons } from "../../../lib/config";
|
|
24
|
+
import type { TLayer } from "../../preview";
|
|
25
|
+
import type { LayerTextProps } from "../text-layer";
|
|
26
|
+
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "../../ui/input-group";
|
|
27
|
+
import { useIsMobile } from "../../../hooks/use-mobile";
|
|
28
|
+
|
|
29
|
+
type IndexedTextLayer = TLayer<LayerTextProps> & { index: number };
|
|
30
|
+
|
|
31
|
+
export default function TextLayerForm() {
|
|
32
|
+
const { layers: globalLayersState, change, add, remove, editOptions, options, requestTextCentering } = useLayersStore()
|
|
33
|
+
// const [selectedIcon, setSelectedIcon] = useState<string>()
|
|
34
|
+
const [movable, setMovable] = useState(false);
|
|
35
|
+
const isMobile = useIsMobile();
|
|
36
|
+
const selectedTemplate = getTemplateById(options.selectedTemplateId);
|
|
37
|
+
const selectedMaterial = getTemplateMaterial({
|
|
38
|
+
template: selectedTemplate,
|
|
39
|
+
materialId: options.selectedMaterialId,
|
|
40
|
+
});
|
|
41
|
+
const minScale = getMinScaleByTemplate({
|
|
42
|
+
templateId: selectedTemplate.id,
|
|
43
|
+
});
|
|
44
|
+
const clampScale = (value: number) => Math.min(MAX_SCALE, Math.max(minScale, value));
|
|
45
|
+
const normalizeScale = (value: number) => Number(clampScale(value).toFixed(3));
|
|
46
|
+
|
|
47
|
+
const handleChangeTextFontScale = (index: number, scale: number) => {
|
|
48
|
+
change(index, { scale: normalizeScale(scale) })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// const handleChangeTextAlign = (align: "center" | "left" | "right" | "justify") => {
|
|
52
|
+
// const lines = globalLayersState.map((layer, index) => ({ ...layer, index })).filter(layer => layer.type === 'text');
|
|
53
|
+
// lines.forEach(line => {
|
|
54
|
+
// change(line.index, { coordinates: { ...line.props.coordinates, x: 340 }, align });
|
|
55
|
+
// })
|
|
56
|
+
// }
|
|
57
|
+
|
|
58
|
+
const getUpdatedLayersSnapshot = (index: number, props: Partial<LayerTextProps>) =>
|
|
59
|
+
globalLayersState.map((layer, layerIndex) => {
|
|
60
|
+
if (layerIndex !== index || !isTextLayer(layer)) return layer;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
...layer,
|
|
64
|
+
props: {
|
|
65
|
+
...layer.props,
|
|
66
|
+
...props,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const generateBraille = (layersSnapshot = globalLayersState) => {
|
|
72
|
+
const brailleLayerIndex = layersSnapshot.findIndex((layer) => layer.subtype === "braille");
|
|
73
|
+
if (brailleLayerIndex === -1) return;
|
|
74
|
+
|
|
75
|
+
const brailleText = layersSnapshot.reduce((text, layer) => {
|
|
76
|
+
if (layer.type !== "text" || layer.subtype === "braille" || !isTextLayer(layer) || !layer.props.braille) {
|
|
77
|
+
return text;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return `${text}${layer.props.text} `;
|
|
81
|
+
}, "").trim();
|
|
82
|
+
|
|
83
|
+
change(brailleLayerIndex, { text: brailleText });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const handleChangeLayerTextContent = (index: number, newContent: string) => {
|
|
87
|
+
change(index, { text: newContent });
|
|
88
|
+
generateBraille(getUpdatedLayersSnapshot(index, { text: newContent }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const handleSwitchTextBraille = (index: number) => {
|
|
92
|
+
const layer = globalLayersState[index]
|
|
93
|
+
if (isTextLayer(layer)) {
|
|
94
|
+
const nextBrailleState = !layer.props.braille;
|
|
95
|
+
change(index, { braille: nextBrailleState });
|
|
96
|
+
generateBraille(getUpdatedLayersSnapshot(index, { braille: nextBrailleState }));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const ptToPx = (pt: number) => pt * (96 / 72);
|
|
101
|
+
const isIndexedTextLine = (layer: TLayer<unknown> & { index: number }): layer is IndexedTextLayer =>
|
|
102
|
+
layer.type === "text" && layer.subtype !== "braille" && isTextLayer(layer as TLayer<LayerTextProps>);
|
|
103
|
+
|
|
104
|
+
const getIndexedTextLines = () => globalLayersState
|
|
105
|
+
.map((layer, index) => ({ ...layer, index }) as TLayer<unknown> & { index: number })
|
|
106
|
+
.filter(isIndexedTextLine)
|
|
107
|
+
.sort((a, b) => (a.props.coordinates.y ?? 0) - (b.props.coordinates.y ?? 0));
|
|
108
|
+
|
|
109
|
+
const getAutoScaleStepByLinesCount = (linesCount: number, sizeBaseScale: number) => {
|
|
110
|
+
const lineDensityFactor = 1 + (Math.max(0, linesCount - 2) * AUTO_ADD_TEXT_LINE_DENSITY_STEP);
|
|
111
|
+
return AUTO_ADD_TEXT_SCALE_STEP * sizeBaseScale * lineDensityFactor;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const getAutoScaleForLines = (linesCount: number, currentLines: IndexedTextLayer[]) => {
|
|
115
|
+
const sizeBaseScale = clampScale(options.selectedSize.baseTextScale ?? 1);
|
|
116
|
+
|
|
117
|
+
const currentMinScale = currentLines.reduce((minScale, line) => {
|
|
118
|
+
const lineScale = line.props.scale ?? sizeBaseScale;
|
|
119
|
+
return Math.min(minScale, lineScale);
|
|
120
|
+
}, Number.POSITIVE_INFINITY);
|
|
121
|
+
|
|
122
|
+
if (!Number.isFinite(currentMinScale)) return sizeBaseScale;
|
|
123
|
+
|
|
124
|
+
// Every new line reduces scale. The reduction is size-aware and slightly stronger with more lines.
|
|
125
|
+
const sizeAwareStep = getAutoScaleStepByLinesCount(linesCount, sizeBaseScale);
|
|
126
|
+
const oneStepDownScale = clampScale(Number((currentMinScale - sizeAwareStep).toFixed(2)));
|
|
127
|
+
const sizeMinimumScale = clampScale(Number((sizeBaseScale * AUTO_ADD_TEXT_MIN_SCALE_RATIO).toFixed(2)));
|
|
128
|
+
|
|
129
|
+
// Never auto-increase scale if current text is already smaller than our lower bound.
|
|
130
|
+
return clampScale(Math.min(currentMinScale, Math.max(sizeMinimumScale, oneStepDownScale)));
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const getAutoScaleAfterLineRemoval = (currentLinesCount: number, remainingLines: IndexedTextLayer[]) => {
|
|
134
|
+
const sizeBaseScale = clampScale(options.selectedSize.baseTextScale ?? 1);
|
|
135
|
+
|
|
136
|
+
const currentMaxScale = remainingLines.reduce((maxScale, line) => {
|
|
137
|
+
const lineScale = line.props.scale ?? sizeBaseScale;
|
|
138
|
+
return Math.max(maxScale, lineScale);
|
|
139
|
+
}, Number.NEGATIVE_INFINITY);
|
|
140
|
+
|
|
141
|
+
if (!Number.isFinite(currentMaxScale)) return sizeBaseScale;
|
|
142
|
+
|
|
143
|
+
// Mirror auto-add behavior: when a line is removed, increase by one size-aware step.
|
|
144
|
+
const sizeAwareStep = getAutoScaleStepByLinesCount(currentLinesCount, sizeBaseScale);
|
|
145
|
+
const oneStepUpScale = clampScale(Number((currentMaxScale + sizeAwareStep).toFixed(2)));
|
|
146
|
+
const sizeMaximumScale = clampScale(Number(sizeBaseScale.toFixed(2)));
|
|
147
|
+
|
|
148
|
+
return clampScale(Math.max(currentMaxScale, Math.min(sizeMaximumScale, oneStepUpScale)));
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const handleAddNewTextLayer = () => {
|
|
152
|
+
const textLines = getIndexedTextLines();
|
|
153
|
+
|
|
154
|
+
if (!textLines.length) return;
|
|
155
|
+
|
|
156
|
+
const firstLine = textLines[0];
|
|
157
|
+
const baseX = firstLine.props.coordinates.x ?? 340;
|
|
158
|
+
const centerY = textLines.reduce((sum, line) => sum + (line.props.coordinates.y ?? 330), 0) / textLines.length;
|
|
159
|
+
const targetLinesCount = textLines.length + 1;
|
|
160
|
+
const targetScale = getAutoScaleForLines(targetLinesCount, textLines);
|
|
161
|
+
const lineSpacing = Math.max(72, Math.round(ptToPx(BASE_FONT_SIZE * targetScale)));
|
|
162
|
+
|
|
163
|
+
let newLineY = centerY;
|
|
164
|
+
|
|
165
|
+
textLines.forEach((line) => {
|
|
166
|
+
change(line.index, { scale: targetScale });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (!movable) {
|
|
170
|
+
const startY = centerY - ((targetLinesCount - 1) * lineSpacing) / 2;
|
|
171
|
+
|
|
172
|
+
textLines.forEach((line, orderIndex) => {
|
|
173
|
+
change(line.index, {
|
|
174
|
+
coordinates: {
|
|
175
|
+
x: line.props.coordinates.x ?? baseX,
|
|
176
|
+
y: startY + (orderIndex * lineSpacing),
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
newLineY = startY + ((targetLinesCount - 1) * lineSpacing);
|
|
182
|
+
|
|
183
|
+
const brailleLayerIndex = globalLayersState.findIndex((layer) => layer.subtype === "braille");
|
|
184
|
+
if (brailleLayerIndex !== -1) {
|
|
185
|
+
change(brailleLayerIndex, {
|
|
186
|
+
coordinates: {
|
|
187
|
+
x: baseX,
|
|
188
|
+
y: newLineY + Math.round(lineSpacing * 0.75),
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
add({
|
|
195
|
+
type: 'text',
|
|
196
|
+
props: {
|
|
197
|
+
fontSize: BASE_FONT_SIZE,
|
|
198
|
+
text: '',
|
|
199
|
+
coordinates: { x: baseX, y: newLineY },
|
|
200
|
+
movable,
|
|
201
|
+
scale: targetScale,
|
|
202
|
+
braille: true,
|
|
203
|
+
color: selectedMaterial.textColor,
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
generateBraille()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// const handleUploadIcon = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
211
|
+
// const file = e.target.files?.[0];
|
|
212
|
+
// if (!file) return;
|
|
213
|
+
|
|
214
|
+
// const reader = new FileReader();
|
|
215
|
+
// reader.onloadend = () => {
|
|
216
|
+
// setSelectedIcon(reader.result as string);
|
|
217
|
+
// };
|
|
218
|
+
// reader.readAsDataURL(file);
|
|
219
|
+
// }
|
|
220
|
+
|
|
221
|
+
const handleRemoveTextLine = (index: number) => {
|
|
222
|
+
const textLines = getIndexedTextLines();
|
|
223
|
+
const brailleLayerIndex = globalLayersState.findIndex((layer) => layer.subtype === "braille");
|
|
224
|
+
|
|
225
|
+
if (!textLines.length) {
|
|
226
|
+
remove(index);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const remainingLines = textLines.filter((line) => line.index !== index);
|
|
231
|
+
if (!remainingLines.length) {
|
|
232
|
+
if (brailleLayerIndex !== -1) {
|
|
233
|
+
change(brailleLayerIndex, { text: "" });
|
|
234
|
+
}
|
|
235
|
+
remove(index);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const removedLine = textLines.find((line) => line.index === index);
|
|
240
|
+
const baseX = removedLine?.props.coordinates.x ?? remainingLines[0].props.coordinates.x ?? 340;
|
|
241
|
+
const centerY = textLines.reduce((sum, line) => sum + (line.props.coordinates.y ?? 330), 0) / textLines.length;
|
|
242
|
+
const targetScale = getAutoScaleAfterLineRemoval(textLines.length, remainingLines);
|
|
243
|
+
const lineSpacing = Math.max(72, Math.round(ptToPx(BASE_FONT_SIZE * targetScale)));
|
|
244
|
+
|
|
245
|
+
remainingLines.forEach((line) => {
|
|
246
|
+
change(line.index, { scale: targetScale });
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (!movable) {
|
|
250
|
+
const startY = centerY - ((remainingLines.length - 1) * lineSpacing) / 2;
|
|
251
|
+
|
|
252
|
+
remainingLines.forEach((line, orderIndex) => {
|
|
253
|
+
change(line.index, {
|
|
254
|
+
coordinates: {
|
|
255
|
+
x: line.props.coordinates.x ?? baseX,
|
|
256
|
+
y: startY + (orderIndex * lineSpacing),
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
if (brailleLayerIndex !== -1) {
|
|
262
|
+
const lastLineY = startY + ((remainingLines.length - 1) * lineSpacing);
|
|
263
|
+
change(brailleLayerIndex, {
|
|
264
|
+
coordinates: {
|
|
265
|
+
x: baseX,
|
|
266
|
+
y: lastLineY + Math.round(lineSpacing * 0.75),
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (brailleLayerIndex !== -1) {
|
|
273
|
+
const brailleText = remainingLines.reduce((text, line) => line.props.braille ? `${text}${line.props.text} ` : text, "");
|
|
274
|
+
change(brailleLayerIndex, { text: brailleText });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
remove(index);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const handleCenterText = () => {
|
|
281
|
+
requestTextCentering();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
const lines = globalLayersState.map((layer, index) => ({ ...layer, index })).filter(layer => layer.type === 'text' || (layer.type === 'image' && layer.subtype === 'icon'));
|
|
286
|
+
lines.forEach(({ index }) => {
|
|
287
|
+
change(index, { movable })
|
|
288
|
+
});
|
|
289
|
+
}, [movable])
|
|
290
|
+
|
|
291
|
+
// useEffect(() => {
|
|
292
|
+
// const iconIndex = globalLayersState.findIndex(layer => layer.subtype === 'icon');
|
|
293
|
+
// if (selectedIcon) {
|
|
294
|
+
// if (iconIndex !== -1) {
|
|
295
|
+
// change(iconIndex, { imageSrc: selectedIcon, hidden: false });
|
|
296
|
+
// } else {
|
|
297
|
+
// add({
|
|
298
|
+
// type: 'image',
|
|
299
|
+
// subtype: 'icon',
|
|
300
|
+
// props: {
|
|
301
|
+
// imageSrc: selectedIcon,
|
|
302
|
+
// coordinates: { x: -35, y: 94 },
|
|
303
|
+
// width: 150,
|
|
304
|
+
// height: 150,
|
|
305
|
+
// movable,
|
|
306
|
+
// hidden: false
|
|
307
|
+
// }
|
|
308
|
+
// });
|
|
309
|
+
// }
|
|
310
|
+
// } else {
|
|
311
|
+
// if (iconIndex !== -1) {
|
|
312
|
+
// change(iconIndex, { hidden: true });
|
|
313
|
+
// }
|
|
314
|
+
// }
|
|
315
|
+
// }, [selectedIcon])
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<div className="space-y-4 p-1">
|
|
319
|
+
<div className="space-y-1">
|
|
320
|
+
<p className="text-[24px] font-semibold text-[#1C1D1D]">Text</p>
|
|
321
|
+
<p className="text-[12px] text-[#8F8F8F]">Edit lines, style and braille options</p>
|
|
322
|
+
</div>
|
|
323
|
+
{globalLayersState
|
|
324
|
+
.map((layer, index) => ({ ...layer, index }))
|
|
325
|
+
.filter(layer => layer.type === 'text' && layer.subtype !== 'braille')
|
|
326
|
+
.map((layer, lineNumber) => {
|
|
327
|
+
if (!(layer.type === "text" && layer.subtype !== "braille" && isTextLayer(layer))) return null;
|
|
328
|
+
|
|
329
|
+
const currentTextPt = Math.round(layer.props.fontSize * (layer.props.scale ?? 1));
|
|
330
|
+
const maxRecommendedLineLength = getMaxLineLengthByPt(
|
|
331
|
+
currentTextPt,
|
|
332
|
+
options.selectedSize.inchs,
|
|
333
|
+
options.selectedTemplateId
|
|
334
|
+
);
|
|
335
|
+
const isTextOverLimit = layer.props.text.length > maxRecommendedLineLength;
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<div
|
|
339
|
+
key={`text_element_${layer.index}`}
|
|
340
|
+
>
|
|
341
|
+
<div className="flex flex-col gap-6">
|
|
342
|
+
<div className="space-y-[8px]">
|
|
343
|
+
<Label>Line {lineNumber + 1}</Label>
|
|
344
|
+
|
|
345
|
+
<InputGroup>
|
|
346
|
+
<InputGroupInput
|
|
347
|
+
onInput={(e) => handleChangeLayerTextContent(layer.index, e.currentTarget.value)}
|
|
348
|
+
className="focus:outline-none"
|
|
349
|
+
value={layer.props.text}
|
|
350
|
+
/>
|
|
351
|
+
|
|
352
|
+
<InputGroupAddon className="font-medium text-[12px]" align="inline-end">{currentTextPt} pt</InputGroupAddon>
|
|
353
|
+
|
|
354
|
+
<InputGroupButton
|
|
355
|
+
size={"icon-sm"}
|
|
356
|
+
className="rounded-none bg-white border-x border-[#D6D6D6] h-full"
|
|
357
|
+
onClick={() => {
|
|
358
|
+
const currentScale = layer.props.scale ?? 1;
|
|
359
|
+
handleChangeTextFontScale(
|
|
360
|
+
layer.index,
|
|
361
|
+
currentScale + BASE_TEXT_SCALE_CHANGE_VALUE
|
|
362
|
+
);
|
|
363
|
+
}}
|
|
364
|
+
>
|
|
365
|
+
<PlusIcon />
|
|
366
|
+
</InputGroupButton>
|
|
367
|
+
|
|
368
|
+
<InputGroupButton
|
|
369
|
+
size={"icon-sm"}
|
|
370
|
+
className="rounded-none bg-white border-r border-[#D6D6D6] h-full"
|
|
371
|
+
onClick={() => {
|
|
372
|
+
const currentScale = layer.props.scale ?? 1;
|
|
373
|
+
handleChangeTextFontScale(
|
|
374
|
+
layer.index,
|
|
375
|
+
currentScale - BASE_TEXT_SCALE_CHANGE_VALUE
|
|
376
|
+
);
|
|
377
|
+
}}
|
|
378
|
+
>
|
|
379
|
+
<MinusIcon />
|
|
380
|
+
</InputGroupButton>
|
|
381
|
+
|
|
382
|
+
<InputGroupButton
|
|
383
|
+
size={"icon-sm"}
|
|
384
|
+
className="rounded-l-none bg-white h-full"
|
|
385
|
+
onClick={() => handleRemoveTextLine(layer.index)}
|
|
386
|
+
>
|
|
387
|
+
<Trash />
|
|
388
|
+
</InputGroupButton>
|
|
389
|
+
</InputGroup>
|
|
390
|
+
<p className={`text-xs ${isTextOverLimit ? "text-red-700" : "text-[#8F8F8F]"}`}>
|
|
391
|
+
Recommended limit: {maxRecommendedLineLength} chars per line ({layer.props.text.length}/{maxRecommendedLineLength})
|
|
392
|
+
</p>
|
|
393
|
+
|
|
394
|
+
{isTextOverLimit && (
|
|
395
|
+
<div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-lg">
|
|
396
|
+
<svg
|
|
397
|
+
className="w-4 h-4 text-red-500 flex-shrink-0"
|
|
398
|
+
fill="none"
|
|
399
|
+
stroke="currentColor"
|
|
400
|
+
viewBox="0 0 24 24"
|
|
401
|
+
>
|
|
402
|
+
<path
|
|
403
|
+
strokeLinecap="round"
|
|
404
|
+
strokeLinejoin="round"
|
|
405
|
+
strokeWidth={2}
|
|
406
|
+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
|
|
407
|
+
/>
|
|
408
|
+
</svg>
|
|
409
|
+
<p className="text-xs text-red-700">
|
|
410
|
+
The maximum recommended text length is {maxRecommendedLineLength} characters per line.
|
|
411
|
+
</p>
|
|
412
|
+
</div>
|
|
413
|
+
)}
|
|
414
|
+
|
|
415
|
+
<div className="text-sm flex items-center gap-1 cursor-pointer select-none" onClick={() => handleSwitchTextBraille(layer.index)}>
|
|
416
|
+
<Checkbox checked={layer.props.braille} value='add' name={`add-braille-${layer.index}`} />
|
|
417
|
+
<Label className="cursor-pointer text-black" htmlFor={`add-braille-${layer.index}`}>Add braille</Label>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
</div >
|
|
422
|
+
);
|
|
423
|
+
})}
|
|
424
|
+
|
|
425
|
+
<Button className="w-full rounded-[8px] h-[40px]" variant="outline" onClick={() => handleAddNewTextLayer()}><Plus /> Add more text</Button>
|
|
426
|
+
<Button className="w-full rounded-[8px] h-[40px]" variant="outline" onClick={handleCenterText}>
|
|
427
|
+
<AlignCenterIcon />
|
|
428
|
+
Center text
|
|
429
|
+
</Button>
|
|
430
|
+
|
|
431
|
+
<div className="space-y-4">
|
|
432
|
+
<div className="flex text-sm items-center gap-2 cursor-pointer select-none" onClick={() => setMovable(prev => !prev)}>
|
|
433
|
+
<Checkbox checked={movable} value='add' name="movable" />
|
|
434
|
+
<Label className="cursor-pointer text-black" htmlFor='movable'>Edit Position, Style and Free Transform</Label>
|
|
435
|
+
</div>
|
|
436
|
+
{movable && isMobile && (
|
|
437
|
+
<p className="text-xs text-[#6B7280]">
|
|
438
|
+
Tip: drag text with one finger on preview. The page scroll is paused while dragging.
|
|
439
|
+
</p>
|
|
440
|
+
)}
|
|
441
|
+
|
|
442
|
+
<div className="flex text-sm items-center gap-2 cursor-pointer select-none" onClick={() => editOptions({ gridInch: !options.gridInch })}>
|
|
443
|
+
<Checkbox checked={options.gridInch} value='add' name="movable" />
|
|
444
|
+
<Label className="cursor-pointer text-black" htmlFor='movable'>Show inch grid</Label>
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
<div className="flex text-sm items-center gap-2 cursor-pointer select-none" onClick={() => editOptions({ gridCm: !options.gridCm })}>
|
|
448
|
+
<Checkbox checked={options.gridCm} value='add' name="movable" />
|
|
449
|
+
<Label className="cursor-pointer text-black" htmlFor='movable'>Show centimeter grid</Label>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
|
|
453
|
+
{/* {movable && <Popover>
|
|
454
|
+
<PopoverTrigger>
|
|
455
|
+
<Button variant="outline">Add Icon</Button>
|
|
456
|
+
</PopoverTrigger>
|
|
457
|
+
<PopoverContent>
|
|
458
|
+
{!selectedIcon ? <div>
|
|
459
|
+
<Label htmlFor="icon" className="mb-1"><Upload size={16} /> Upload</Label>
|
|
460
|
+
<Input name='icon' id="icon" type="file" className="w-full" accept="image/png, image/svg, image/webp" onInput={handleUploadIcon} />
|
|
461
|
+
<span className="text-xs text-black/50">Image *.png, *.svg, *.webp</span>
|
|
462
|
+
</div> : (
|
|
463
|
+
<Button variant="destructive" size="sm" className="w-full" onClick={() => setSelectedIcon(undefined)}>Remove Icon</Button>
|
|
464
|
+
)}
|
|
465
|
+
<div className="mt-2 flex items-start gap-1 flex-wrap">
|
|
466
|
+
{icons.map(icon => (
|
|
467
|
+
<Button key={icon} size='icon' variant='outline' className={`${icon === selectedIcon ? 'border-2 border-primary' : ''}`} onClick={() => setSelectedIcon(icon)}>
|
|
468
|
+
<img src={icon} className="p-0.5" />
|
|
469
|
+
</Button>
|
|
470
|
+
))}
|
|
471
|
+
</div>
|
|
472
|
+
</PopoverContent>
|
|
473
|
+
</Popover>} */}
|
|
474
|
+
|
|
475
|
+
</div >
|
|
476
|
+
)
|
|
477
|
+
}
|