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,259 @@
|
|
|
1
|
+
import { useLayersStore } from "../../store/layers-store"
|
|
2
|
+
import { Label } from "../ui/label"
|
|
3
|
+
import { Slider } from "../ui/slider"
|
|
4
|
+
import type { LayrProps } from "../preview"
|
|
5
|
+
import { isImageLayer, isTextLayer } from "../../lib/type-checks"
|
|
6
|
+
import { Textarea } from "../ui/textarea"
|
|
7
|
+
import { AlignCenterIcon, AlignJustifyIcon, AlignLeftIcon, AlignRightIcon, ImageIcon, TypeIcon } from "lucide-react"
|
|
8
|
+
import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group"
|
|
9
|
+
import { useState } from "react"
|
|
10
|
+
import { Checkbox } from "../ui/checkbox"
|
|
11
|
+
import { Input } from "../ui/input"
|
|
12
|
+
import { getMaxLineLengthByPt } from "../../lib/config-font"
|
|
13
|
+
|
|
14
|
+
export default function LayersContainer() {
|
|
15
|
+
const { layers, init: setLayers, options } = useLayersStore()
|
|
16
|
+
const [customBraille, setCustomBraille] = useState(false)
|
|
17
|
+
|
|
18
|
+
const updateLayer = (index: number, props: Partial<LayrProps>) => {
|
|
19
|
+
const temp = layers
|
|
20
|
+
temp[index] = {
|
|
21
|
+
...temp[index],
|
|
22
|
+
props: {
|
|
23
|
+
...temp[index].props,
|
|
24
|
+
...props,
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
setLayers(temp)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Change image layer size
|
|
31
|
+
// const handleChangeLayerWidth = (index: number, newWidth: number) => {
|
|
32
|
+
// updateLayer(index, { width: `${newWidth}%` })
|
|
33
|
+
// }
|
|
34
|
+
|
|
35
|
+
const handleChangeTextFontScale = (index: number, scale: number) => {
|
|
36
|
+
updateLayer(index, { scale })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const handleChangeTextAlign = (index: number, align: "center" | "left" | "right" | "justify") => {
|
|
40
|
+
updateLayer(index, { align })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Change layer content
|
|
44
|
+
const handleChangeLayerTextContent = (index: number, newContent: string) => {
|
|
45
|
+
updateLayer(index, { text: newContent })
|
|
46
|
+
if (layers[index].subtype !== "braille" && !customBraille) {
|
|
47
|
+
// Якщо браєль не кастомний, то встановлюється автоматично
|
|
48
|
+
const brailleLayerIndex = layers.findIndex((layer) => layer.subtype === "braille")
|
|
49
|
+
updateLayer(brailleLayerIndex, { text: newContent })
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const getLongestLineLength = (text: string) => {
|
|
54
|
+
return text.split("\n").reduce((maxLength, line) => Math.max(maxLength, line.length), 0)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="bg-gradient-to-br from-slate-50 to-slate-100 rounded-xl border border-slate-200 shadow-lg p-6 space-y-6">
|
|
59
|
+
<div className="flex items-center gap-3 pb-4 border-b border-slate-200">
|
|
60
|
+
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
|
|
61
|
+
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
62
|
+
<path
|
|
63
|
+
strokeLinecap="round"
|
|
64
|
+
strokeLinejoin="round"
|
|
65
|
+
strokeWidth={2}
|
|
66
|
+
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"
|
|
67
|
+
/>
|
|
68
|
+
</svg>
|
|
69
|
+
</div>
|
|
70
|
+
<h2 className="text-xl font-semibold text-slate-800 uppercase">Layers</h2>
|
|
71
|
+
<div className="ml-auto bg-slate-200 text-slate-600 text-xs px-2 py-1 rounded-full">
|
|
72
|
+
{layers.length} {layers.length === 1 ? "layer" : "layers"}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{layers.map((layer, index) => (
|
|
77
|
+
<div
|
|
78
|
+
key={`layer_container_${index}`}
|
|
79
|
+
className="bg-white rounded-lg border border-slate-200 shadow-sm hover:shadow-md transition-shadow duration-200 overflow-hidden"
|
|
80
|
+
>
|
|
81
|
+
<div className="bg-slate-50 px-4 py-3 border-b border-slate-100 flex items-center gap-3">
|
|
82
|
+
{layer.type === "image" ? (
|
|
83
|
+
<ImageIcon className="w-4 h-4 text-primary" />
|
|
84
|
+
) : (
|
|
85
|
+
<TypeIcon className="w-4 h-4 text-primary" />
|
|
86
|
+
)}
|
|
87
|
+
<span className="text-sm font-medium text-slate-700 capitalize">
|
|
88
|
+
{layer.type} Layer {layer.subtype === "braille" ? "(Braille)" : ""}
|
|
89
|
+
</span>
|
|
90
|
+
<div className="ml-auto text-xs text-slate-500">#{index + 1}</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div className="p-4">
|
|
94
|
+
{layer.type === "image" && isImageLayer(layer) && (
|
|
95
|
+
<div className="flex flex-col gap-4">
|
|
96
|
+
<div className="flex items-center justify-center p-4 bg-slate-50 rounded-lg border-2 border-dashed border-slate-200">
|
|
97
|
+
<img
|
|
98
|
+
src={layer.props.imageSrc || "/placeholder.svg"}
|
|
99
|
+
className={`h-16 max-w-full object-contain rounded shadow-sm`}
|
|
100
|
+
alt="Layer preview"
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{layer.type === "text" && layer.subtype !== "braille" && isTextLayer(layer) && (
|
|
107
|
+
<div className="flex flex-col gap-6">
|
|
108
|
+
<div className="space-y-3">
|
|
109
|
+
<Label className="text-sm font-medium text-slate-700">Text Content</Label>
|
|
110
|
+
<Textarea
|
|
111
|
+
placeholder={"Kitchen ↵\nMy"}
|
|
112
|
+
onInput={(e) => handleChangeLayerTextContent(index, e.currentTarget.value)}
|
|
113
|
+
className="min-h-[80px] resize-none border-primary/50 focus:border-primary"
|
|
114
|
+
/>
|
|
115
|
+
<p className={`text-xs ${getLongestLineLength(layer.props.text) > getMaxLineLengthByPt(layer.props.fontSize * (layer.props.scale ?? 1), options.selectedSize.inchs, options.selectedTemplateId) ? "text-red-700" : "text-[#8F8F8F]"}`}>
|
|
116
|
+
Recommended limit: {getMaxLineLengthByPt(layer.props.fontSize * (layer.props.scale ?? 1), options.selectedSize.inchs, options.selectedTemplateId)} chars per line (longest line: {getLongestLineLength(layer.props.text)})
|
|
117
|
+
</p>
|
|
118
|
+
{getLongestLineLength(layer.props.text) > getMaxLineLengthByPt(layer.props.fontSize * (layer.props.scale ?? 1), options.selectedSize.inchs, options.selectedTemplateId) && (
|
|
119
|
+
<div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-lg">
|
|
120
|
+
<svg
|
|
121
|
+
className="w-4 h-4 text-red-500 flex-shrink-0"
|
|
122
|
+
fill="none"
|
|
123
|
+
stroke="currentColor"
|
|
124
|
+
viewBox="0 0 24 24"
|
|
125
|
+
>
|
|
126
|
+
<path
|
|
127
|
+
strokeLinecap="round"
|
|
128
|
+
strokeLinejoin="round"
|
|
129
|
+
strokeWidth={2}
|
|
130
|
+
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"
|
|
131
|
+
/>
|
|
132
|
+
</svg>
|
|
133
|
+
<p className="text-xs text-red-700">
|
|
134
|
+
The maximum recommended text length is {getMaxLineLengthByPt(layer.props.fontSize * (layer.props.scale ?? 1), options.selectedSize.inchs, options.selectedTemplateId)} characters per line.
|
|
135
|
+
</p>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div className="space-y-3">
|
|
141
|
+
<Label className="text-sm font-medium text-slate-700">Text Alignment</Label>
|
|
142
|
+
<ToggleGroup
|
|
143
|
+
type="single"
|
|
144
|
+
onValueChange={(value) =>
|
|
145
|
+
handleChangeTextAlign(index, value as "center" | "left" | "right" | "justify")
|
|
146
|
+
}
|
|
147
|
+
className="justify-start bg-slate-50 p-1 rounded-lg"
|
|
148
|
+
>
|
|
149
|
+
<ToggleGroupItem
|
|
150
|
+
value="left"
|
|
151
|
+
aria-label="Align left"
|
|
152
|
+
className="data-[state=on]:bg-white data-[state=on]:shadow-sm"
|
|
153
|
+
>
|
|
154
|
+
<AlignLeftIcon className="h-4 w-4" />
|
|
155
|
+
</ToggleGroupItem>
|
|
156
|
+
<ToggleGroupItem
|
|
157
|
+
value="center"
|
|
158
|
+
aria-label="Align center"
|
|
159
|
+
className="data-[state=on]:bg-white data-[state=on]:shadow-sm"
|
|
160
|
+
>
|
|
161
|
+
<AlignCenterIcon className="h-4 w-4" />
|
|
162
|
+
</ToggleGroupItem>
|
|
163
|
+
<ToggleGroupItem
|
|
164
|
+
value="justify"
|
|
165
|
+
aria-label="Justify text"
|
|
166
|
+
className="data-[state=on]:bg-white data-[state=on]:shadow-sm"
|
|
167
|
+
>
|
|
168
|
+
<AlignJustifyIcon className="h-4 w-4" />
|
|
169
|
+
</ToggleGroupItem>
|
|
170
|
+
<ToggleGroupItem
|
|
171
|
+
value="right"
|
|
172
|
+
aria-label="Align right"
|
|
173
|
+
className="data-[state=on]:bg-white data-[state=on]:shadow-sm"
|
|
174
|
+
>
|
|
175
|
+
<AlignRightIcon className="h-4 w-4" />
|
|
176
|
+
</ToggleGroupItem>
|
|
177
|
+
</ToggleGroup>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div className="space-y-3">
|
|
181
|
+
<Label className="text-sm font-medium text-slate-700">Font Size</Label>
|
|
182
|
+
<div className="px-3 py-2 bg-slate-50 rounded-lg">
|
|
183
|
+
<Slider
|
|
184
|
+
defaultValue={[1]}
|
|
185
|
+
min={0.5}
|
|
186
|
+
max={1.2}
|
|
187
|
+
step={0.05}
|
|
188
|
+
onValueChange={(value) => handleChangeTextFontScale(index, value[0])}
|
|
189
|
+
className="w-full"
|
|
190
|
+
/>
|
|
191
|
+
<div className="flex justify-between text-xs text-slate-500 mt-1">
|
|
192
|
+
<span>Small</span>
|
|
193
|
+
<span>Large</span>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
|
|
200
|
+
{layer.subtype === "braille" && isTextLayer(layer) && (
|
|
201
|
+
<div className="flex flex-col gap-6">
|
|
202
|
+
<div className="space-y-3">
|
|
203
|
+
<Label className="text-sm font-medium text-slate-700">Braille Preview</Label>
|
|
204
|
+
<div className="p-4 bg-slate-50 rounded-lg border border-slate-200 min-h-[60px] flex items-center">
|
|
205
|
+
<p className="font-braille text-lg text-slate-800">{layer.props.text}</p>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
{customBraille && (
|
|
210
|
+
<div className="space-y-3">
|
|
211
|
+
<Label className="text-sm font-medium text-slate-700">Custom Braille Text</Label>
|
|
212
|
+
<Input
|
|
213
|
+
onInput={(e) => handleChangeLayerTextContent(index, e.currentTarget.value)}
|
|
214
|
+
placeholder="Doctor instead of Dr. Smith"
|
|
215
|
+
/>
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
<div className="flex items-center gap-3 p-3 bg-primary/10 rounded-lg border border-primary/20">
|
|
220
|
+
<Checkbox
|
|
221
|
+
id={`custom-braille-${index}`}
|
|
222
|
+
checked={customBraille}
|
|
223
|
+
onCheckedChange={() => {
|
|
224
|
+
setCustomBraille((prev) => !prev)
|
|
225
|
+
}}
|
|
226
|
+
className="border-primary/20"
|
|
227
|
+
/>
|
|
228
|
+
<Label
|
|
229
|
+
htmlFor={`custom-braille-${index}`}
|
|
230
|
+
className="text-sm font-medium cursor-pointer"
|
|
231
|
+
>
|
|
232
|
+
Enable Custom Braille
|
|
233
|
+
</Label>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
))}
|
|
240
|
+
|
|
241
|
+
{layers.length === 0 && (
|
|
242
|
+
<div className="text-center py-12 text-slate-500">
|
|
243
|
+
<div className="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
244
|
+
<svg className="w-8 h-8 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
245
|
+
<path
|
|
246
|
+
strokeLinecap="round"
|
|
247
|
+
strokeLinejoin="round"
|
|
248
|
+
strokeWidth={2}
|
|
249
|
+
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"
|
|
250
|
+
/>
|
|
251
|
+
</svg>
|
|
252
|
+
</div>
|
|
253
|
+
<p className="text-sm">No layers added yet</p>
|
|
254
|
+
</div>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import type { TCoordinates, TMovableItemProps } from "../movable-item";
|
|
3
|
+
import type { TLayer } from "../preview";
|
|
4
|
+
import { useLayersStore } from "../../store/layers-store";
|
|
5
|
+
// import MovableItem from "../movable-item";
|
|
6
|
+
import { isLayerTextProps } from "../../lib/type-checks";
|
|
7
|
+
import { BASE_FONT_SIZE } from "../../lib/config-font";
|
|
8
|
+
import MovableItem from "../movable-item";
|
|
9
|
+
|
|
10
|
+
export type LayerTextProps = {
|
|
11
|
+
fontSize: number; // px
|
|
12
|
+
text: string;
|
|
13
|
+
scale?: number; // fontSize * scale = text size
|
|
14
|
+
color?: string;
|
|
15
|
+
// defaultCoordinatinates?: {
|
|
16
|
+
// top: '50%' | string;
|
|
17
|
+
// left: '50%' | string;
|
|
18
|
+
// };
|
|
19
|
+
align?: 'center' | 'left' | 'right' | 'justify';
|
|
20
|
+
braille?: boolean;
|
|
21
|
+
} & TMovableItemProps
|
|
22
|
+
|
|
23
|
+
export default function TextLayer({ layerIndex, font = 'font-bebasneue' }: {
|
|
24
|
+
layerIndex: number;
|
|
25
|
+
font?: 'font-bebasneue' | 'font-gothicb' | 'font-braille';
|
|
26
|
+
}) {
|
|
27
|
+
const [layer, setLayer] = useState<TLayer<LayerTextProps>>()
|
|
28
|
+
const { layers, init: updateLayers } = useLayersStore()
|
|
29
|
+
const [fontSize, setFontSize] = useState<number>();
|
|
30
|
+
|
|
31
|
+
const handleLayerUpdate = (coordinates: TCoordinates) => {
|
|
32
|
+
if (layers) {
|
|
33
|
+
const temp = layers;
|
|
34
|
+
temp[layerIndex] = {
|
|
35
|
+
...temp[layerIndex],
|
|
36
|
+
props: {
|
|
37
|
+
...temp[layerIndex].props, coordinates: {
|
|
38
|
+
x: (coordinates.x && coordinates.x > 0) ? coordinates.x : 0,
|
|
39
|
+
y: (coordinates.y && coordinates.y > 0) ? coordinates.y : 0,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
updateLayers(temp)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const layer = layers[layerIndex];
|
|
49
|
+
|
|
50
|
+
if (layers && isLayerTextProps(layer.props)) {
|
|
51
|
+
setLayer({
|
|
52
|
+
...layer,
|
|
53
|
+
props: layer.props
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Вирахування розміру тексту
|
|
57
|
+
if (layer.subtype === 'braille') setFontSize(layer.props.fontSize)
|
|
58
|
+
// const longestLine = layer.props.text.split('\n').sort((a, b) => a.length - b.length);
|
|
59
|
+
calcFontSize() // longestLine[0].length
|
|
60
|
+
|
|
61
|
+
}
|
|
62
|
+
}, [layers[layerIndex]]);
|
|
63
|
+
|
|
64
|
+
// Вираховує розмір тексту
|
|
65
|
+
const calcFontSize = () => {
|
|
66
|
+
const layer = layers[layerIndex];
|
|
67
|
+
if (isLayerTextProps(layer.props) && layer.subtype !== 'braille') {
|
|
68
|
+
setFontSize(BASE_FONT_SIZE * (layer.props.scale ?? 1))
|
|
69
|
+
// let found = false; // Чи був знайдений відповідний розмір для довжини рядку
|
|
70
|
+
// const fontRange = Object.entries(FONT_RANGE);
|
|
71
|
+
// for (const [lengthRange, fontScale] of fontRange) {
|
|
72
|
+
// if (textLength < Number(lengthRange)) {
|
|
73
|
+
// const scale = (layer.props.scale ?? 1) < fontScale ? (layer.props.scale ?? 1) : fontScale;
|
|
74
|
+
// setFontSize(BASE_FONT_SIZE * scale);
|
|
75
|
+
// found = true;
|
|
76
|
+
// break;
|
|
77
|
+
// }
|
|
78
|
+
// }
|
|
79
|
+
// if (!found) {
|
|
80
|
+
// const [_, fontScale] = fontRange[fontRange.length - 1];
|
|
81
|
+
// const scale = (layer.props.scale ?? 1) < fontScale ? (layer.props.scale ?? 1) : fontScale;
|
|
82
|
+
// setFontSize(BASE_FONT_SIZE * scale);
|
|
83
|
+
// }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (layer) {
|
|
88
|
+
const positionTransitionClass = layer.props.movable
|
|
89
|
+
? ""
|
|
90
|
+
: "transition-[top,left] duration-500 ease-in-out";
|
|
91
|
+
|
|
92
|
+
const layerJsx = (
|
|
93
|
+
<div className={`absolute -translate-x-1/2 -translate-y-1/2 ${positionTransitionClass} ${font} select-none`} style={{
|
|
94
|
+
fontSize: `${(fontSize ?? 20) * (layer.subtype === 'braille' ? (layer.props.scale ?? 1) : 1)}pt`,
|
|
95
|
+
top: layer.props.coordinates.y,
|
|
96
|
+
left: layer.props.coordinates.x,
|
|
97
|
+
color: layer.props.color ?? "#111111",
|
|
98
|
+
}}>
|
|
99
|
+
{layer.subtype === 'braille' ? (
|
|
100
|
+
<p
|
|
101
|
+
className='overflow-hidden text-nowrap transition'
|
|
102
|
+
draggable={false}
|
|
103
|
+
style={{
|
|
104
|
+
textAlign: layer.props.align ?? 'center'
|
|
105
|
+
}}
|
|
106
|
+
>{layer.props.text}</p>
|
|
107
|
+
) : layer.props.text.split('\n').map((paragraph, index) => (
|
|
108
|
+
<p
|
|
109
|
+
key={`${paragraph} ${index}`}
|
|
110
|
+
className='overflow-hidden text-nowrap'
|
|
111
|
+
draggable={false}
|
|
112
|
+
style={{
|
|
113
|
+
lineHeight: 1,
|
|
114
|
+
textAlign: layer.props.align ?? 'center'
|
|
115
|
+
}}
|
|
116
|
+
>{paragraph}</p>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return layer.props.movable ? (
|
|
122
|
+
<MovableItem coordinates={layer.props.coordinates} onUpdate={handleLayerUpdate}>
|
|
123
|
+
{layerJsx}
|
|
124
|
+
</MovableItem>
|
|
125
|
+
) : layerJsx
|
|
126
|
+
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
|
2
|
+
import { useLayersStore } from "../store/layers-store";
|
|
3
|
+
|
|
4
|
+
export type TCoordinates = {
|
|
5
|
+
x?: number;
|
|
6
|
+
y?: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type TMovableItemProps = {
|
|
10
|
+
coordinates: TCoordinates;
|
|
11
|
+
movable?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const SNAP_THRESHOLD = 15;
|
|
15
|
+
|
|
16
|
+
const clamp = (value: number, min: number, max: number) => {
|
|
17
|
+
if (Number.isNaN(value)) return min;
|
|
18
|
+
return Math.min(Math.max(value, min), max);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default function MovableItem({ coordinates, onUpdate, children }: {
|
|
22
|
+
coordinates: TCoordinates;
|
|
23
|
+
onUpdate: (coordinates: TCoordinates) => void;
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
}) {
|
|
26
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
27
|
+
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
|
28
|
+
const [initialPosition, setInitialPosition] = useState({ x: 0, y: 0 });
|
|
29
|
+
|
|
30
|
+
const [snapX, setSnapX] = useState(false);
|
|
31
|
+
const [snapY, setSnapY] = useState(false);
|
|
32
|
+
|
|
33
|
+
const activePointerIdRef = useRef<number | null>(null);
|
|
34
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
35
|
+
const bodyStyleBackupRef = useRef<{
|
|
36
|
+
userSelect: string;
|
|
37
|
+
touchAction: string;
|
|
38
|
+
overflow: string;
|
|
39
|
+
} | null>(null);
|
|
40
|
+
|
|
41
|
+
const { resizing } = useLayersStore();
|
|
42
|
+
|
|
43
|
+
const lockPageInteraction = useCallback(() => {
|
|
44
|
+
if (bodyStyleBackupRef.current) return;
|
|
45
|
+
|
|
46
|
+
bodyStyleBackupRef.current = {
|
|
47
|
+
userSelect: document.body.style.userSelect,
|
|
48
|
+
touchAction: document.body.style.touchAction,
|
|
49
|
+
overflow: document.body.style.overflow,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
document.body.style.userSelect = "none";
|
|
53
|
+
document.body.style.touchAction = "none";
|
|
54
|
+
document.body.style.overflow = "hidden";
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const restorePageInteraction = useCallback(() => {
|
|
58
|
+
if (!bodyStyleBackupRef.current) return;
|
|
59
|
+
|
|
60
|
+
document.body.style.userSelect = bodyStyleBackupRef.current.userSelect;
|
|
61
|
+
document.body.style.touchAction = bodyStyleBackupRef.current.touchAction;
|
|
62
|
+
document.body.style.overflow = bodyStyleBackupRef.current.overflow;
|
|
63
|
+
|
|
64
|
+
bodyStyleBackupRef.current = null;
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
const updateByPointerPosition = useCallback(
|
|
68
|
+
(clientX: number, clientY: number) => {
|
|
69
|
+
if (!isDragging || !coordinates || !onUpdate) return;
|
|
70
|
+
|
|
71
|
+
const parent = containerRef.current?.offsetParent as HTMLElement | null;
|
|
72
|
+
const parentRect = parent?.getBoundingClientRect();
|
|
73
|
+
|
|
74
|
+
const scaleX = parent && parent.clientWidth ? (parentRect?.width ? parentRect.width / parent.clientWidth : 1) : 1;
|
|
75
|
+
const scaleY = parent && parent.clientHeight ? (parentRect?.height ? parentRect.height / parent.clientHeight : 1) : 1;
|
|
76
|
+
|
|
77
|
+
const deltaX = (clientX - dragStart.x) / (scaleX || 1);
|
|
78
|
+
const deltaY = (clientY - dragStart.y) / (scaleY || 1);
|
|
79
|
+
|
|
80
|
+
let newX = initialPosition.x + deltaX;
|
|
81
|
+
let newY = initialPosition.y + deltaY;
|
|
82
|
+
let isSnappedX = false;
|
|
83
|
+
let isSnappedY = false;
|
|
84
|
+
|
|
85
|
+
if (parent) {
|
|
86
|
+
const parentWidth = parent.clientWidth;
|
|
87
|
+
const parentHeight = parent.clientHeight;
|
|
88
|
+
const centerX = parentWidth / 2;
|
|
89
|
+
const centerY = parentHeight / 2;
|
|
90
|
+
|
|
91
|
+
if (Math.abs(newX - centerX) < SNAP_THRESHOLD) {
|
|
92
|
+
newX = centerX;
|
|
93
|
+
isSnappedX = true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (Math.abs(newY - centerY) < SNAP_THRESHOLD) {
|
|
97
|
+
newY = centerY;
|
|
98
|
+
isSnappedY = true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
newX = clamp(newX, 0, parentWidth);
|
|
102
|
+
newY = clamp(newY, 0, parentHeight);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setSnapX(isSnappedX);
|
|
106
|
+
setSnapY(isSnappedY);
|
|
107
|
+
|
|
108
|
+
if (!resizing) {
|
|
109
|
+
onUpdate({ x: newX, y: newY });
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
[coordinates, dragStart.x, dragStart.y, initialPosition.x, initialPosition.y, isDragging, onUpdate, resizing],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const finishDragging = useCallback(() => {
|
|
116
|
+
setIsDragging(false);
|
|
117
|
+
setSnapX(false);
|
|
118
|
+
setSnapY(false);
|
|
119
|
+
activePointerIdRef.current = null;
|
|
120
|
+
restorePageInteraction();
|
|
121
|
+
}, [restorePageInteraction]);
|
|
122
|
+
|
|
123
|
+
const handlePointerDown = useCallback(
|
|
124
|
+
(e: React.PointerEvent<HTMLDivElement>) => {
|
|
125
|
+
if (!coordinates || !onUpdate) return;
|
|
126
|
+
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
127
|
+
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
e.stopPropagation();
|
|
130
|
+
|
|
131
|
+
activePointerIdRef.current = e.pointerId;
|
|
132
|
+
|
|
133
|
+
if (e.currentTarget.setPointerCapture) {
|
|
134
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
setIsDragging(true);
|
|
138
|
+
setDragStart({ x: e.clientX, y: e.clientY });
|
|
139
|
+
setInitialPosition({
|
|
140
|
+
x: coordinates?.x || 0,
|
|
141
|
+
y: coordinates?.y || 0,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
lockPageInteraction();
|
|
145
|
+
},
|
|
146
|
+
[coordinates, lockPageInteraction, onUpdate],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const handleGlobalPointerMove = useCallback(
|
|
150
|
+
(e: PointerEvent) => {
|
|
151
|
+
if (!isDragging) return;
|
|
152
|
+
if (activePointerIdRef.current !== null && e.pointerId !== activePointerIdRef.current) return;
|
|
153
|
+
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
updateByPointerPosition(e.clientX, e.clientY);
|
|
156
|
+
},
|
|
157
|
+
[isDragging, updateByPointerPosition],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const handleGlobalPointerUp = useCallback(
|
|
161
|
+
(e: PointerEvent) => {
|
|
162
|
+
if (activePointerIdRef.current !== null && e.pointerId !== activePointerIdRef.current) return;
|
|
163
|
+
finishDragging();
|
|
164
|
+
},
|
|
165
|
+
[finishDragging],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (!isDragging) return;
|
|
170
|
+
|
|
171
|
+
document.addEventListener("pointermove", handleGlobalPointerMove, { passive: false });
|
|
172
|
+
document.addEventListener("pointerup", handleGlobalPointerUp);
|
|
173
|
+
document.addEventListener("pointercancel", handleGlobalPointerUp);
|
|
174
|
+
window.addEventListener("blur", finishDragging);
|
|
175
|
+
|
|
176
|
+
return () => {
|
|
177
|
+
document.removeEventListener("pointermove", handleGlobalPointerMove);
|
|
178
|
+
document.removeEventListener("pointerup", handleGlobalPointerUp);
|
|
179
|
+
document.removeEventListener("pointercancel", handleGlobalPointerUp);
|
|
180
|
+
window.removeEventListener("blur", finishDragging);
|
|
181
|
+
restorePageInteraction();
|
|
182
|
+
};
|
|
183
|
+
}, [finishDragging, handleGlobalPointerMove, handleGlobalPointerUp, isDragging, restorePageInteraction]);
|
|
184
|
+
|
|
185
|
+
const offsetParent = containerRef.current?.offsetParent as HTMLElement | null;
|
|
186
|
+
const offsetParentWidth = offsetParent?.clientWidth ?? 0;
|
|
187
|
+
const offsetParentHeight = offsetParent?.clientHeight ?? 0;
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div
|
|
191
|
+
ref={containerRef}
|
|
192
|
+
className={`${isDragging ? "cursor-grabbing" : "cursor-grab"} select-none`}
|
|
193
|
+
>
|
|
194
|
+
<div
|
|
195
|
+
onPointerDown={handlePointerDown}
|
|
196
|
+
className="touch-none hover:[&>*]:outline-2 hover:[&>*]:outline-blue-600 active:[&>*]:outline-2 active:[&>*]:outline-blue-600"
|
|
197
|
+
>
|
|
198
|
+
{children}
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{isDragging && (
|
|
202
|
+
<div className="absolute top-2 left-2 z-50 rounded bg-blue-500 px-2 py-1 text-xs whitespace-nowrap text-white pointer-events-none">
|
|
203
|
+
X: {coordinates?.x?.toFixed(0)}, Y: {coordinates?.y?.toFixed(0)}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
{snapX && (
|
|
208
|
+
<div
|
|
209
|
+
className="absolute top-0 bottom-0 border-l border-dashed border-red-500 pointer-events-none"
|
|
210
|
+
style={{
|
|
211
|
+
left: offsetParentWidth / 2,
|
|
212
|
+
zIndex: 60,
|
|
213
|
+
}}
|
|
214
|
+
/>
|
|
215
|
+
)}
|
|
216
|
+
|
|
217
|
+
{snapY && (
|
|
218
|
+
<div
|
|
219
|
+
className="absolute left-0 right-0 border-t border-dashed border-red-500 pointer-events-none"
|
|
220
|
+
style={{
|
|
221
|
+
top: offsetParentHeight / 2,
|
|
222
|
+
zIndex: 60,
|
|
223
|
+
}}
|
|
224
|
+
/>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|