vibrant-tool 0.1.9 → 0.1.11

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.
@@ -1123,11 +1123,25 @@ const useEditorStore = create((set, get) => ({
1123
1123
  back: null
1124
1124
  },
1125
1125
  templateData: null,
1126
+ // Grid state
1127
+ showGrid: false,
1128
+ setShowGrid: (showGrid) => set({ showGrid }),
1129
+ toggleGrid: () => set((state) => ({ showGrid: !state.showGrid })),
1126
1130
  history: [],
1127
1131
  historyIndex: -1,
1128
1132
  // Crop state
1129
1133
  isCropping: false,
1130
- startCropping: () => set({ isCropping: true }),
1134
+ startCropping: () => {
1135
+ const canvas2 = get().canvas;
1136
+ if (canvas2 && canvas2.isDrawingMode) {
1137
+ canvas2.isDrawingMode = false;
1138
+ canvas2.selection = true;
1139
+ canvas2.defaultCursor = "default";
1140
+ canvas2._isCurrentlyDrawing = false;
1141
+ canvas2.requestRenderAll();
1142
+ }
1143
+ set({ isCropping: true });
1144
+ },
1131
1145
  stopCropping: () => set({ isCropping: false }),
1132
1146
  // Canvas setter (NO history here)
1133
1147
  setCanvas: (canvas2) => {
@@ -8442,6 +8456,7 @@ function createShapeCornerOverlays(shape, bounds, pageSide = "front", overlayCol
8442
8456
  originX: "left",
8443
8457
  originY: "top",
8444
8458
  fill: overlayColor,
8459
+ opacity: 0.6,
8445
8460
  strokeWidth: 0,
8446
8461
  selectable: false,
8447
8462
  evented: false,
@@ -8493,6 +8508,7 @@ function createShapeCornerOverlays(shape, bounds, pageSide = "front", overlayCol
8493
8508
  originX: "left",
8494
8509
  originY: "top",
8495
8510
  fill: overlayColor,
8511
+ opacity: 0.6,
8496
8512
  strokeWidth: 0,
8497
8513
  selectable: false,
8498
8514
  evented: false,
@@ -8789,6 +8805,7 @@ function createOverlays(canvas2, workarea, pageSide) {
8789
8805
  const overlayColor = canvas2.backgroundColor || "#F3F4F6";
8790
8806
  const makeOverlay = (props) => new jr({
8791
8807
  fill: overlayColor,
8808
+ opacity: 0.6,
8792
8809
  selectable: false,
8793
8810
  evented: false,
8794
8811
  excludeFromExport: true,
@@ -8862,10 +8879,14 @@ function createOverlays(canvas2, workarea, pageSide) {
8862
8879
  canvas2.requestRenderAll();
8863
8880
  }
8864
8881
  function bringOverlaysToFront(canvas2) {
8882
+ if (!canvas2) return;
8883
+ canvas2.preserveObjectStacking = true;
8884
+ canvas2.controlsAboveOverlay = true;
8865
8885
  const overlays = canvas2.getObjects().filter((o2) => o2.name?.startsWith("overlay-"));
8866
8886
  const guidelines = canvas2.getObjects().filter((o2) => o2.name?.startsWith("guideline-"));
8867
8887
  overlays.forEach((overlay) => canvas2.bringObjectToFront(overlay));
8868
8888
  guidelines.forEach((g2) => canvas2.bringObjectToFront(g2));
8889
+ canvas2.requestRenderAll();
8869
8890
  }
8870
8891
  const BASE_URL = "https://www.api.vibrantprintshop.com";
8871
8892
  const IMAGE_UPLOAD_URL = "https://upload.vibrantprint.com";
@@ -8996,6 +9017,248 @@ const useProductStore = create((set) => ({
8996
9017
  }
8997
9018
  }
8998
9019
  }));
9020
+ const FontService = {
9021
+ async getAll(data) {
9022
+ const res = await apiFetch(`/api/fonts?page=${data.page}&limit=${data.limit}&status=true`, {
9023
+ method: "GET"
9024
+ });
9025
+ return res;
9026
+ }
9027
+ };
9028
+ let cachedFontsPromise = null;
9029
+ async function fetchAllFontsCached() {
9030
+ if (!cachedFontsPromise) {
9031
+ cachedFontsPromise = FontService.getAll({ page: 1, limit: 1e3 }).then((res) => res?.data || []).catch((err) => {
9032
+ console.warn("Failed to fetch all fonts:", err);
9033
+ cachedFontsPromise = null;
9034
+ return [];
9035
+ });
9036
+ }
9037
+ return cachedFontsPromise;
9038
+ }
9039
+ async function injectFontFace(fontName, fontUrl) {
9040
+ if (!fontName || !fontUrl || typeof document === "undefined") return;
9041
+ const cleanedName = fontName.trim().replace(/^['"]+|['"]+$/g, "");
9042
+ const id = `font-${cleanedName.replace(/\s+/g, "-").toLowerCase()}`;
9043
+ if (!document.getElementById(id)) {
9044
+ const style = document.createElement("style");
9045
+ style.id = id;
9046
+ style.innerHTML = `
9047
+ @font-face {
9048
+ font-family: '${cleanedName}';
9049
+ src: url('${fontUrl}');
9050
+ font-display: swap;
9051
+ }
9052
+ `;
9053
+ document.head.appendChild(style);
9054
+ }
9055
+ try {
9056
+ await document.fonts.load(`16px "${cleanedName}"`);
9057
+ await document.fonts.ready;
9058
+ } catch {
9059
+ }
9060
+ }
9061
+ async function loadTemplateFontsIntoHead(templateFonts, pagesDesignData) {
9062
+ if (typeof document === "undefined") return;
9063
+ const loadPromises = [];
9064
+ if (Array.isArray(templateFonts) && templateFonts.length > 0) {
9065
+ templateFonts.forEach((f) => {
9066
+ if (f && typeof f === "object") {
9067
+ const fontName = f.fontName || f.fontFamily;
9068
+ const fontUrl = f.fontFile;
9069
+ if (fontName && fontUrl) {
9070
+ loadPromises.push(injectFontFace(fontName, fontUrl));
9071
+ }
9072
+ if (f.fontFamily && f.fontFamily !== fontName && fontUrl) {
9073
+ loadPromises.push(injectFontFace(f.fontFamily, fontUrl));
9074
+ }
9075
+ }
9076
+ });
9077
+ }
9078
+ const designs = Array.isArray(pagesDesignData) ? pagesDesignData : [pagesDesignData];
9079
+ const usedFontNames = /* @__PURE__ */ new Set();
9080
+ designs.forEach((d2) => {
9081
+ if (d2) {
9082
+ const extracted = extractFontFamilies(d2);
9083
+ extracted.forEach((name) => usedFontNames.add(name));
9084
+ }
9085
+ });
9086
+ if (usedFontNames.size > 0) {
9087
+ try {
9088
+ const allFonts = await fetchAllFontsCached();
9089
+ usedFontNames.forEach((name) => {
9090
+ const lower = name.toLowerCase();
9091
+ const matched = allFonts.find((f) => {
9092
+ if (f.fontName && f.fontName.toLowerCase() === lower) return true;
9093
+ if (f.fontFamily && f.fontFamily.toLowerCase() === lower) return true;
9094
+ const children = f.childFonts || f.chieldFonts || [];
9095
+ return children.some(
9096
+ (c2) => c2.fontName && c2.fontName.toLowerCase() === lower || c2.fontFamily && c2.fontFamily.toLowerCase() === lower
9097
+ );
9098
+ });
9099
+ if (matched) {
9100
+ if (matched.fontName && matched.fontFile) {
9101
+ loadPromises.push(injectFontFace(matched.fontName, matched.fontFile));
9102
+ }
9103
+ if (matched.fontFamily && matched.fontFile) {
9104
+ loadPromises.push(injectFontFace(matched.fontFamily, matched.fontFile));
9105
+ }
9106
+ const children = matched.childFonts || matched.chieldFonts || [];
9107
+ children.forEach((c2) => {
9108
+ if (c2.fontName && c2.fontFile) {
9109
+ loadPromises.push(injectFontFace(c2.fontName, c2.fontFile));
9110
+ }
9111
+ });
9112
+ }
9113
+ });
9114
+ } catch (err) {
9115
+ console.warn("Failed to fetch fonts list for font injection:", err);
9116
+ }
9117
+ }
9118
+ await Promise.allSettled(loadPromises);
9119
+ try {
9120
+ await document.fonts.ready;
9121
+ } catch {
9122
+ }
9123
+ }
9124
+ function extractFontFamilies(designJson) {
9125
+ if (!designJson) return [];
9126
+ const fonts = /* @__PURE__ */ new Set();
9127
+ const traverse = (obj) => {
9128
+ if (!obj) return;
9129
+ if (obj.fontFamily && typeof obj.fontFamily === "string") {
9130
+ const cleaned = obj.fontFamily.trim().replace(/^['"]+|['"]+$/g, "");
9131
+ if (cleaned) fonts.add(cleaned);
9132
+ }
9133
+ if (obj.fontId && typeof obj.fontId === "string") {
9134
+ fonts.add(obj.fontId.trim());
9135
+ }
9136
+ if (obj.styles && typeof obj.styles === "object") {
9137
+ Object.values(obj.styles).forEach((line) => {
9138
+ if (line && typeof line === "object") {
9139
+ Object.values(line).forEach((charStyle) => {
9140
+ if (charStyle?.fontFamily && typeof charStyle.fontFamily === "string") {
9141
+ const cleaned = charStyle.fontFamily.trim().replace(/^['"]+|['"]+$/g, "");
9142
+ if (cleaned) fonts.add(cleaned);
9143
+ }
9144
+ });
9145
+ }
9146
+ });
9147
+ }
9148
+ if (Array.isArray(obj.objects)) {
9149
+ obj.objects.forEach(traverse);
9150
+ }
9151
+ };
9152
+ if (Array.isArray(designJson.objects)) {
9153
+ designJson.objects.forEach(traverse);
9154
+ } else if (Array.isArray(designJson)) {
9155
+ designJson.forEach(traverse);
9156
+ } else {
9157
+ traverse(designJson);
9158
+ }
9159
+ return Array.from(fonts);
9160
+ }
9161
+ async function resolveFontIds(fontNames) {
9162
+ if (!fontNames || !fontNames.length) return [];
9163
+ try {
9164
+ const allFonts = await fetchAllFontsCached();
9165
+ const matchedIds = /* @__PURE__ */ new Set();
9166
+ for (const rawName of fontNames) {
9167
+ const name = rawName.trim().replace(/^['"]+|['"]+$/g, "");
9168
+ const lower = name.toLowerCase();
9169
+ if (/^[0-9a-fA-F]{24}$/.test(name)) {
9170
+ matchedIds.add(name);
9171
+ continue;
9172
+ }
9173
+ const matchedFont = allFonts.find((f) => {
9174
+ if (f._id === name) return true;
9175
+ if (f.fontFamily && f.fontFamily.toLowerCase() === lower) return true;
9176
+ if (f.fontName && f.fontName.toLowerCase() === lower) return true;
9177
+ const children = f.childFonts || f.chieldFonts || [];
9178
+ return children.some(
9179
+ (c2) => c2._id === name || c2.fontName && c2.fontName.toLowerCase() === lower || c2.fontFamily && c2.fontFamily.toLowerCase() === lower
9180
+ );
9181
+ });
9182
+ if (matchedFont?._id) {
9183
+ matchedIds.add(matchedFont._id);
9184
+ }
9185
+ }
9186
+ return Array.from(matchedIds);
9187
+ } catch (err) {
9188
+ console.warn("Failed to resolve font IDs:", err);
9189
+ return [];
9190
+ }
9191
+ }
9192
+ async function resolveFontsForExport(designs, templateFonts) {
9193
+ const result = [];
9194
+ const foundNames = /* @__PURE__ */ new Set();
9195
+ if (Array.isArray(templateFonts) && templateFonts.length > 0) {
9196
+ templateFonts.forEach((f) => {
9197
+ if (f && typeof f === "object") {
9198
+ const fontName = f.fontName || f.fontFamily;
9199
+ const fontFile = f.fontFile;
9200
+ if (fontName && fontFile) {
9201
+ result.push({ fontName, fontFamily: f.fontFamily, fontFile });
9202
+ foundNames.add(fontName.toLowerCase());
9203
+ if (f.fontFamily) foundNames.add(f.fontFamily.toLowerCase());
9204
+ }
9205
+ }
9206
+ });
9207
+ }
9208
+ const usedFontNames = /* @__PURE__ */ new Set();
9209
+ const designsArr = Array.isArray(designs) ? designs : [designs];
9210
+ designsArr.forEach((d2) => {
9211
+ if (d2) {
9212
+ const extracted = extractFontFamilies(d2);
9213
+ extracted.forEach((name) => {
9214
+ if (!foundNames.has(name.toLowerCase())) {
9215
+ usedFontNames.add(name);
9216
+ }
9217
+ });
9218
+ }
9219
+ });
9220
+ if (usedFontNames.size > 0) {
9221
+ try {
9222
+ const allFonts = await fetchAllFontsCached();
9223
+ usedFontNames.forEach((name) => {
9224
+ const lower = name.toLowerCase();
9225
+ const matched = allFonts.find((f) => {
9226
+ if (f.fontName && f.fontName.toLowerCase() === lower) return true;
9227
+ if (f.fontFamily && f.fontFamily.toLowerCase() === lower) return true;
9228
+ const children = f.childFonts || f.chieldFonts || [];
9229
+ return children.some(
9230
+ (c2) => c2.fontName && c2.fontName.toLowerCase() === lower || c2.fontFamily && c2.fontFamily.toLowerCase() === lower
9231
+ );
9232
+ });
9233
+ if (matched) {
9234
+ if (matched.fontName && matched.fontFile && !foundNames.has(matched.fontName.toLowerCase())) {
9235
+ result.push({
9236
+ fontName: matched.fontName,
9237
+ fontFamily: matched.fontFamily,
9238
+ fontFile: matched.fontFile
9239
+ });
9240
+ foundNames.add(matched.fontName.toLowerCase());
9241
+ }
9242
+ const children = matched.childFonts || matched.chieldFonts || [];
9243
+ children.forEach((c2) => {
9244
+ const childName = c2.fontName || name;
9245
+ if (c2.fontFile && !foundNames.has(childName.toLowerCase())) {
9246
+ result.push({
9247
+ fontName: childName,
9248
+ fontFamily: c2.fontFamily,
9249
+ fontFile: c2.fontFile
9250
+ });
9251
+ foundNames.add(childName.toLowerCase());
9252
+ }
9253
+ });
9254
+ }
9255
+ });
9256
+ } catch (err) {
9257
+ console.warn("Failed to resolve fonts for export:", err);
9258
+ }
9259
+ }
9260
+ return result;
9261
+ }
8999
9262
  async function loadPageToCanvas(canvas2, json, pageSide) {
9000
9263
  if (!canvas2) return;
9001
9264
  canvas2.discardActiveObject();
@@ -9003,6 +9266,7 @@ async function loadPageToCanvas(canvas2, json, pageSide) {
9003
9266
  if (json) {
9004
9267
  try {
9005
9268
  const parsedJson = typeof json === "string" ? JSON.parse(json) : json;
9269
+ await loadTemplateFontsIntoHead(null, parsedJson);
9006
9270
  const loadPromise = canvas2.loadFromJSON(parsedJson);
9007
9271
  const timeoutPromise = new Promise(
9008
9272
  (_2, reject) => setTimeout(() => reject(new Error("Canvas loadFromJSON timeout")), 4e3)
@@ -9102,12 +9366,16 @@ async function loadPageToCanvas(canvas2, json, pageSide) {
9102
9366
  }
9103
9367
  }
9104
9368
  createOverlays(canvas2, workarea, currentSide);
9369
+ if (bgImg) {
9370
+ canvas2.sendObjectToBack(bgImg);
9371
+ }
9105
9372
  canvas2.sendObjectToBack(workarea);
9106
9373
  }
9107
9374
  canvas2.getObjects().forEach((obj) => {
9108
9375
  const isWorkarea = obj.name === "workarea";
9109
9376
  const isOverlayOrGuide = obj.name?.startsWith("overlay-") || obj.name?.startsWith("guideline-");
9110
- if (!isWorkarea && !isOverlayOrGuide) {
9377
+ const isBgImage = obj.name === "bg-image";
9378
+ if (!isWorkarea && !isOverlayOrGuide && !isBgImage) {
9111
9379
  obj.set({
9112
9380
  selectable: true,
9113
9381
  evented: true,
@@ -9245,6 +9513,77 @@ async function renderPageDataUrl(json, baseCanvas, multiplier = 1, targetSide, i
9245
9513
  return "";
9246
9514
  }
9247
9515
  }
9516
+ const useDrawStore = create((set) => ({
9517
+ drawTool: "select",
9518
+ strokeWidth: 5,
9519
+ opacity: 100,
9520
+ color: "#000000",
9521
+ setDrawTool: (tool) => set(() => {
9522
+ if (tool === "highlighter") {
9523
+ return { drawTool: tool, opacity: 50 };
9524
+ }
9525
+ if (tool === "brush") {
9526
+ return { drawTool: tool, opacity: 100 };
9527
+ }
9528
+ return { drawTool: tool };
9529
+ }),
9530
+ setStrokeWidth: (v2) => set({ strokeWidth: v2 }),
9531
+ setOpacity: (v2) => set({ opacity: v2 }),
9532
+ setColor: (v2) => set({ color: v2 })
9533
+ }));
9534
+ function deriveType(objects) {
9535
+ if (objects.length === 0) return "none";
9536
+ if (objects.length > 1) return "multiple";
9537
+ const obj = objects[0];
9538
+ const t2 = obj.type?.toLowerCase();
9539
+ if (t2 === "activeselection") return "multiple";
9540
+ if (t2 === "group") {
9541
+ if (obj.name === "qr") return "qr";
9542
+ if (obj.name === "barcode") return "barcode";
9543
+ return "group";
9544
+ }
9545
+ if (t2 === "textbox" || t2 === "i-text" || t2 === "text") return "textbox";
9546
+ if (t2 === "image") return "image";
9547
+ return t2 || "none";
9548
+ }
9549
+ const useSelectionStore = create((set) => ({
9550
+ type: "none",
9551
+ objects: [],
9552
+ activeColor: null,
9553
+ activeColorIndex: 0,
9554
+ setSelection: (objects) => {
9555
+ if (objects && objects.length > 0) {
9556
+ const drawState = useDrawStore.getState();
9557
+ if (drawState.drawTool !== "select") {
9558
+ drawState.setDrawTool("select");
9559
+ }
9560
+ const canvas2 = useEditorStore.getState().canvas;
9561
+ if (canvas2 && canvas2.isDrawingMode) {
9562
+ canvas2.isDrawingMode = false;
9563
+ canvas2.selection = true;
9564
+ canvas2.defaultCursor = "default";
9565
+ canvas2._isCurrentlyDrawing = false;
9566
+ canvas2.requestRenderAll();
9567
+ }
9568
+ }
9569
+ set({
9570
+ objects,
9571
+ type: deriveType(objects),
9572
+ activeColor: null,
9573
+ activeColorIndex: 0
9574
+ });
9575
+ },
9576
+ setActiveColor: (color, index = 0) => set({
9577
+ activeColor: color,
9578
+ activeColorIndex: index
9579
+ }),
9580
+ clearSelection: () => set({
9581
+ type: "none",
9582
+ objects: [],
9583
+ activeColor: null,
9584
+ activeColorIndex: 0
9585
+ })
9586
+ }));
9248
9587
  const HISTORY_CUSTOM_PROPERTIES = [
9249
9588
  "name",
9250
9589
  "id",
@@ -9255,6 +9594,9 @@ const HISTORY_CUSTOM_PROPERTIES = [
9255
9594
  "lockMovementY",
9256
9595
  "rx",
9257
9596
  "ry",
9597
+ "src",
9598
+ "crossOrigin",
9599
+ "__maskPath",
9258
9600
  "__radiusOfCanvas",
9259
9601
  "__shapeType",
9260
9602
  "__pageSide",
@@ -9265,7 +9607,24 @@ const HISTORY_CUSTOM_PROPERTIES = [
9265
9607
  "__initialZoom",
9266
9608
  "__borderRect",
9267
9609
  "__rotationControlSize",
9268
- "__showDegree"
9610
+ "__showDegree",
9611
+ "textAlign",
9612
+ "fontFamily",
9613
+ "fontSize",
9614
+ "fontWeight",
9615
+ "fontStyle",
9616
+ "underline",
9617
+ "linethrough",
9618
+ "overline",
9619
+ "fill",
9620
+ "stroke",
9621
+ "strokeWidth",
9622
+ "lineHeight",
9623
+ "charSpacing",
9624
+ "styles",
9625
+ "paintFirst",
9626
+ "strokeUniform",
9627
+ "objectCaching"
9269
9628
  ];
9270
9629
  const useHistoryStore = create((set, get) => ({
9271
9630
  canvas: null,
@@ -9288,11 +9647,12 @@ const useHistoryStore = create((set, get) => ({
9288
9647
  setPage: (page) => set({ currentPage: page }),
9289
9648
  saveState: () => {
9290
9649
  const { canvas: canvas2, currentPage, pages, isRestoring } = get();
9291
- if (!canvas2 || isRestoring) return;
9292
- const rawJson = canvas2.toObject(HISTORY_CUSTOM_PROPERTIES);
9650
+ const targetCanvas = canvas2 || useEditorStore.getState().canvas;
9651
+ if (!targetCanvas || isRestoring) return;
9652
+ const rawJson = targetCanvas.toObject(HISTORY_CUSTOM_PROPERTIES);
9293
9653
  const cleanObjects = (rawJson.objects || []).filter((obj) => {
9294
9654
  const name = obj.name || "";
9295
- return name !== "watermark" && !name.startsWith("overlay-") && !name.startsWith("guideline-");
9655
+ return name !== "watermark" && !name.startsWith("overlay-") && !name.startsWith("guideline-") && name !== "canvas-grid";
9296
9656
  });
9297
9657
  const json = { ...rawJson, objects: cleanObjects };
9298
9658
  const history = pages[currentPage]?.history ?? [];
@@ -9312,10 +9672,12 @@ const useHistoryStore = create((set, get) => ({
9312
9672
  },
9313
9673
  undo: async () => {
9314
9674
  const { canvas: canvas2, currentPage, pages, isRestoring } = get();
9315
- if (!canvas2 || isRestoring) return;
9675
+ const targetCanvas = canvas2 || useEditorStore.getState().canvas;
9676
+ if (!targetCanvas || isRestoring) return;
9316
9677
  const page = pages[currentPage];
9317
9678
  if (!page || page.history.length <= 1) return;
9318
- canvas2.discardActiveObject();
9679
+ targetCanvas.discardActiveObject();
9680
+ useSelectionStore.getState().clearSelection();
9319
9681
  const current = page.history.at(-1);
9320
9682
  const previous = page.history.at(-2);
9321
9683
  set({
@@ -9329,8 +9691,8 @@ const useHistoryStore = create((set, get) => ({
9329
9691
  }
9330
9692
  });
9331
9693
  try {
9332
- await loadPageToCanvas(canvas2, previous, currentPage);
9333
- canvas2.requestRenderAll();
9694
+ await loadPageToCanvas(targetCanvas, previous, currentPage);
9695
+ targetCanvas.requestRenderAll();
9334
9696
  const editorStore = useEditorStore.getState();
9335
9697
  if (editorStore.pages) {
9336
9698
  editorStore.setPages({
@@ -9346,10 +9708,12 @@ const useHistoryStore = create((set, get) => ({
9346
9708
  },
9347
9709
  redoAction: async () => {
9348
9710
  const { canvas: canvas2, currentPage, pages, isRestoring } = get();
9349
- if (!canvas2 || isRestoring) return;
9711
+ const targetCanvas = canvas2 || useEditorStore.getState().canvas;
9712
+ if (!targetCanvas || isRestoring) return;
9350
9713
  const page = pages[currentPage];
9351
9714
  if (!page || page.redo.length === 0) return;
9352
- canvas2.discardActiveObject();
9715
+ targetCanvas.discardActiveObject();
9716
+ useSelectionStore.getState().clearSelection();
9353
9717
  const next = page.redo[0];
9354
9718
  set({
9355
9719
  isRestoring: true,
@@ -9362,8 +9726,8 @@ const useHistoryStore = create((set, get) => ({
9362
9726
  }
9363
9727
  });
9364
9728
  try {
9365
- await loadPageToCanvas(canvas2, next, currentPage);
9366
- canvas2.requestRenderAll();
9729
+ await loadPageToCanvas(targetCanvas, next, currentPage);
9730
+ targetCanvas.requestRenderAll();
9367
9731
  const editorStore = useEditorStore.getState();
9368
9732
  if (editorStore.pages) {
9369
9733
  editorStore.setPages({
@@ -9378,248 +9742,6 @@ const useHistoryStore = create((set, get) => ({
9378
9742
  }
9379
9743
  }
9380
9744
  }));
9381
- const FontService = {
9382
- async getAll(data) {
9383
- const res = await apiFetch(`/api/fonts?page=${data.page}&limit=${data.limit}&status=true`, {
9384
- method: "GET"
9385
- });
9386
- return res;
9387
- }
9388
- };
9389
- let cachedFontsPromise = null;
9390
- async function fetchAllFontsCached() {
9391
- if (!cachedFontsPromise) {
9392
- cachedFontsPromise = FontService.getAll({ page: 1, limit: 1e3 }).then((res) => res?.data || []).catch((err) => {
9393
- console.warn("Failed to fetch all fonts:", err);
9394
- cachedFontsPromise = null;
9395
- return [];
9396
- });
9397
- }
9398
- return cachedFontsPromise;
9399
- }
9400
- async function injectFontFace(fontName, fontUrl) {
9401
- if (!fontName || !fontUrl || typeof document === "undefined") return;
9402
- const cleanedName = fontName.trim().replace(/^['"]+|['"]+$/g, "");
9403
- const id = `font-${cleanedName.replace(/\s+/g, "-").toLowerCase()}`;
9404
- if (!document.getElementById(id)) {
9405
- const style = document.createElement("style");
9406
- style.id = id;
9407
- style.innerHTML = `
9408
- @font-face {
9409
- font-family: '${cleanedName}';
9410
- src: url('${fontUrl}');
9411
- font-display: swap;
9412
- }
9413
- `;
9414
- document.head.appendChild(style);
9415
- }
9416
- try {
9417
- await document.fonts.load(`16px "${cleanedName}"`);
9418
- await document.fonts.ready;
9419
- } catch {
9420
- }
9421
- }
9422
- async function loadTemplateFontsIntoHead(templateFonts, pagesDesignData) {
9423
- if (typeof document === "undefined") return;
9424
- const loadPromises = [];
9425
- if (Array.isArray(templateFonts) && templateFonts.length > 0) {
9426
- templateFonts.forEach((f) => {
9427
- if (f && typeof f === "object") {
9428
- const fontName = f.fontName || f.fontFamily;
9429
- const fontUrl = f.fontFile;
9430
- if (fontName && fontUrl) {
9431
- loadPromises.push(injectFontFace(fontName, fontUrl));
9432
- }
9433
- if (f.fontFamily && f.fontFamily !== fontName && fontUrl) {
9434
- loadPromises.push(injectFontFace(f.fontFamily, fontUrl));
9435
- }
9436
- }
9437
- });
9438
- }
9439
- const designs = Array.isArray(pagesDesignData) ? pagesDesignData : [pagesDesignData];
9440
- const usedFontNames = /* @__PURE__ */ new Set();
9441
- designs.forEach((d2) => {
9442
- if (d2) {
9443
- const extracted = extractFontFamilies(d2);
9444
- extracted.forEach((name) => usedFontNames.add(name));
9445
- }
9446
- });
9447
- if (usedFontNames.size > 0) {
9448
- try {
9449
- const allFonts = await fetchAllFontsCached();
9450
- usedFontNames.forEach((name) => {
9451
- const lower = name.toLowerCase();
9452
- const matched = allFonts.find((f) => {
9453
- if (f.fontName && f.fontName.toLowerCase() === lower) return true;
9454
- if (f.fontFamily && f.fontFamily.toLowerCase() === lower) return true;
9455
- const children = f.childFonts || f.chieldFonts || [];
9456
- return children.some(
9457
- (c2) => c2.fontName && c2.fontName.toLowerCase() === lower || c2.fontFamily && c2.fontFamily.toLowerCase() === lower
9458
- );
9459
- });
9460
- if (matched) {
9461
- if (matched.fontName && matched.fontFile) {
9462
- loadPromises.push(injectFontFace(matched.fontName, matched.fontFile));
9463
- }
9464
- if (matched.fontFamily && matched.fontFile) {
9465
- loadPromises.push(injectFontFace(matched.fontFamily, matched.fontFile));
9466
- }
9467
- const children = matched.childFonts || matched.chieldFonts || [];
9468
- children.forEach((c2) => {
9469
- if (c2.fontName && c2.fontFile) {
9470
- loadPromises.push(injectFontFace(c2.fontName, c2.fontFile));
9471
- }
9472
- });
9473
- }
9474
- });
9475
- } catch (err) {
9476
- console.warn("Failed to fetch fonts list for font injection:", err);
9477
- }
9478
- }
9479
- await Promise.allSettled(loadPromises);
9480
- try {
9481
- await document.fonts.ready;
9482
- } catch {
9483
- }
9484
- }
9485
- function extractFontFamilies(designJson) {
9486
- if (!designJson) return [];
9487
- const fonts = /* @__PURE__ */ new Set();
9488
- const traverse = (obj) => {
9489
- if (!obj) return;
9490
- if (obj.fontFamily && typeof obj.fontFamily === "string") {
9491
- const cleaned = obj.fontFamily.trim().replace(/^['"]+|['"]+$/g, "");
9492
- if (cleaned) fonts.add(cleaned);
9493
- }
9494
- if (obj.fontId && typeof obj.fontId === "string") {
9495
- fonts.add(obj.fontId.trim());
9496
- }
9497
- if (obj.styles && typeof obj.styles === "object") {
9498
- Object.values(obj.styles).forEach((line) => {
9499
- if (line && typeof line === "object") {
9500
- Object.values(line).forEach((charStyle) => {
9501
- if (charStyle?.fontFamily && typeof charStyle.fontFamily === "string") {
9502
- const cleaned = charStyle.fontFamily.trim().replace(/^['"]+|['"]+$/g, "");
9503
- if (cleaned) fonts.add(cleaned);
9504
- }
9505
- });
9506
- }
9507
- });
9508
- }
9509
- if (Array.isArray(obj.objects)) {
9510
- obj.objects.forEach(traverse);
9511
- }
9512
- };
9513
- if (Array.isArray(designJson.objects)) {
9514
- designJson.objects.forEach(traverse);
9515
- } else if (Array.isArray(designJson)) {
9516
- designJson.forEach(traverse);
9517
- } else {
9518
- traverse(designJson);
9519
- }
9520
- return Array.from(fonts);
9521
- }
9522
- async function resolveFontIds(fontNames) {
9523
- if (!fontNames || !fontNames.length) return [];
9524
- try {
9525
- const allFonts = await fetchAllFontsCached();
9526
- const matchedIds = /* @__PURE__ */ new Set();
9527
- for (const rawName of fontNames) {
9528
- const name = rawName.trim().replace(/^['"]+|['"]+$/g, "");
9529
- const lower = name.toLowerCase();
9530
- if (/^[0-9a-fA-F]{24}$/.test(name)) {
9531
- matchedIds.add(name);
9532
- continue;
9533
- }
9534
- const matchedFont = allFonts.find((f) => {
9535
- if (f._id === name) return true;
9536
- if (f.fontFamily && f.fontFamily.toLowerCase() === lower) return true;
9537
- if (f.fontName && f.fontName.toLowerCase() === lower) return true;
9538
- const children = f.childFonts || f.chieldFonts || [];
9539
- return children.some(
9540
- (c2) => c2._id === name || c2.fontName && c2.fontName.toLowerCase() === lower || c2.fontFamily && c2.fontFamily.toLowerCase() === lower
9541
- );
9542
- });
9543
- if (matchedFont?._id) {
9544
- matchedIds.add(matchedFont._id);
9545
- }
9546
- }
9547
- return Array.from(matchedIds);
9548
- } catch (err) {
9549
- console.warn("Failed to resolve font IDs:", err);
9550
- return [];
9551
- }
9552
- }
9553
- async function resolveFontsForExport(designs, templateFonts) {
9554
- const result = [];
9555
- const foundNames = /* @__PURE__ */ new Set();
9556
- if (Array.isArray(templateFonts) && templateFonts.length > 0) {
9557
- templateFonts.forEach((f) => {
9558
- if (f && typeof f === "object") {
9559
- const fontName = f.fontName || f.fontFamily;
9560
- const fontFile = f.fontFile;
9561
- if (fontName && fontFile) {
9562
- result.push({ fontName, fontFamily: f.fontFamily, fontFile });
9563
- foundNames.add(fontName.toLowerCase());
9564
- if (f.fontFamily) foundNames.add(f.fontFamily.toLowerCase());
9565
- }
9566
- }
9567
- });
9568
- }
9569
- const usedFontNames = /* @__PURE__ */ new Set();
9570
- const designsArr = Array.isArray(designs) ? designs : [designs];
9571
- designsArr.forEach((d2) => {
9572
- if (d2) {
9573
- const extracted = extractFontFamilies(d2);
9574
- extracted.forEach((name) => {
9575
- if (!foundNames.has(name.toLowerCase())) {
9576
- usedFontNames.add(name);
9577
- }
9578
- });
9579
- }
9580
- });
9581
- if (usedFontNames.size > 0) {
9582
- try {
9583
- const allFonts = await fetchAllFontsCached();
9584
- usedFontNames.forEach((name) => {
9585
- const lower = name.toLowerCase();
9586
- const matched = allFonts.find((f) => {
9587
- if (f.fontName && f.fontName.toLowerCase() === lower) return true;
9588
- if (f.fontFamily && f.fontFamily.toLowerCase() === lower) return true;
9589
- const children = f.childFonts || f.chieldFonts || [];
9590
- return children.some(
9591
- (c2) => c2.fontName && c2.fontName.toLowerCase() === lower || c2.fontFamily && c2.fontFamily.toLowerCase() === lower
9592
- );
9593
- });
9594
- if (matched) {
9595
- if (matched.fontName && matched.fontFile && !foundNames.has(matched.fontName.toLowerCase())) {
9596
- result.push({
9597
- fontName: matched.fontName,
9598
- fontFamily: matched.fontFamily,
9599
- fontFile: matched.fontFile
9600
- });
9601
- foundNames.add(matched.fontName.toLowerCase());
9602
- }
9603
- const children = matched.childFonts || matched.chieldFonts || [];
9604
- children.forEach((c2) => {
9605
- const childName = c2.fontName || name;
9606
- if (c2.fontFile && !foundNames.has(childName.toLowerCase())) {
9607
- result.push({
9608
- fontName: childName,
9609
- fontFamily: c2.fontFamily,
9610
- fontFile: c2.fontFile
9611
- });
9612
- foundNames.add(childName.toLowerCase());
9613
- }
9614
- });
9615
- }
9616
- });
9617
- } catch (err) {
9618
- console.warn("Failed to resolve fonts for export:", err);
9619
- }
9620
- }
9621
- return result;
9622
- }
9623
9745
  const WATERMARK_TEXT = "vibrantprintshop";
9624
9746
  function createWatermark(curWorkarea, pageSide) {
9625
9747
  const curW = curWorkarea.getScaledWidth ? curWorkarea.getScaledWidth() : curWorkarea.width * (curWorkarea.scaleX || 1);
@@ -9663,7 +9785,7 @@ function triggerDownload(url, filename) {
9663
9785
  }
9664
9786
  function exportDesignJSON(canvas2, workarea) {
9665
9787
  const objects = canvas2.getObjects();
9666
- const children = objects.filter((obj) => obj.name !== "workarea" && !obj.name?.startsWith("overlay-")).map((obj) => {
9788
+ const children = objects.filter((obj) => obj.name !== "workarea" && !obj.name?.startsWith("overlay-") && !obj.name?.startsWith("guideline-") && obj.name !== "canvas-grid").map((obj) => {
9667
9789
  const base = obj.toObject();
9668
9790
  return {
9669
9791
  id: obj.id || crypto.randomUUID(),
@@ -9712,8 +9834,18 @@ function exportDesignJSON(canvas2, workarea) {
9712
9834
  }
9713
9835
  const ShapeService = {
9714
9836
  async getAll(data) {
9837
+ const params = new URLSearchParams();
9838
+ if (data.page) params.append("page", String(data.page));
9839
+ if (data.limit) params.append("limit", String(data.limit));
9840
+ if (data.category && data.category !== "ALL" && data.category !== "null" && data.category !== "undefined") {
9841
+ params.append("category", data.category);
9842
+ }
9843
+ if (data.search && data.search.trim()) {
9844
+ params.append("search", data.search.trim());
9845
+ }
9846
+ params.append("status", "true");
9715
9847
  const res = await apiFetch(
9716
- `/api/cliparts?page=${data.page}&limit=${data.limit}&category=${data.category}&status=true`,
9848
+ `/api/cliparts?${params.toString()}`,
9717
9849
  {
9718
9850
  method: "GET"
9719
9851
  }
@@ -9723,8 +9855,17 @@ const ShapeService = {
9723
9855
  };
9724
9856
  },
9725
9857
  async getAllExternal(data) {
9858
+ const params = new URLSearchParams();
9859
+ if (data.page) params.append("page", String(data.page));
9860
+ if (data.limit) params.append("limit", String(data.limit));
9861
+ if (data.keyType) params.append("keyType", data.keyType);
9862
+ if (data.isVector) params.append("isVector", "true");
9863
+ if (data.query && data.query.trim()) {
9864
+ params.append("query", data.query.trim());
9865
+ }
9866
+ params.append("status", "true");
9726
9867
  const res = await apiFetch(
9727
- `/api/external/images?page=${data.page}&limit=${data.limit}&keyType=${data.keyType}&status=true${data.query ? `&query=${data.query}` : ""}`,
9868
+ `/api/external/images?${params.toString()}`,
9728
9869
  {
9729
9870
  method: "GET"
9730
9871
  }
@@ -9888,6 +10029,7 @@ async function addImageToCanvas(url) {
9888
10029
  });
9889
10030
  canvas2.add(img);
9890
10031
  canvas2.setActiveObject(img);
10032
+ bringOverlaysToFront(canvas2);
9891
10033
  canvas2.requestRenderAll();
9892
10034
  }).catch((err) => {
9893
10035
  console.warn("Fabric failed to load image from URL:", url, err);
@@ -9910,7 +10052,7 @@ function captureWorkareaSVG(canvas2, workarea, pageSide) {
9910
10052
  canvas2.renderAll();
9911
10053
  const originalProps = /* @__PURE__ */ new Map();
9912
10054
  objects.forEach((obj) => {
9913
- const isExcluded = obj.excludeFromExport || obj.name?.startsWith("overlay-") || obj.name?.startsWith("guide-") || obj.name?.startsWith("guideline-") || obj.name === "crop-grid";
10055
+ const isExcluded = obj.excludeFromExport || obj.name?.startsWith("overlay-") || obj.name?.startsWith("guide-") || obj.name?.startsWith("guideline-") || obj.name === "canvas-grid" || obj.name === "crop-grid";
9914
10056
  if (isExcluded && obj !== targetWorkarea) {
9915
10057
  originalProps.set(obj, {
9916
10058
  visible: obj.visible,
@@ -101804,8 +101946,43 @@ function TemplateMockupModal({
101804
101946
  if (!uploadedImageUrl) {
101805
101947
  throw new Error("Server upload returned empty image URL");
101806
101948
  }
101949
+ const canvas22 = useEditorStore.getState().canvas;
101950
+ if (!templateId || !canvas22) return;
101951
+ const { pages: historyPages, currentPage } = useHistoryStore.getState();
101952
+ const editorPages = useEditorStore.getState().pages || {};
101953
+ const currentJson = canvas22.toObject(["name"]);
101954
+ const currentImage = getWorkareaDataUrl(canvas22, 1, currentPage);
101955
+ const pages = await Promise.all(
101956
+ ["front", "back"].map(async (side, i) => {
101957
+ const isCurrent = currentPage === side;
101958
+ const designData = isCurrent ? currentJson : editorPages[side] || historyPages[side]?.history?.at(-1) || null;
101959
+ if (!designData) return null;
101960
+ const sideImage = isCurrent ? currentImage : await renderPageDataUrl(designData, canvas22, 1, side);
101961
+ const uploadedImageUrl2 = await UploadService.uploadDataUrl(
101962
+ sideImage,
101963
+ `template-${templateId}-${side}-${Date.now()}.png`
101964
+ );
101965
+ return {
101966
+ side,
101967
+ designData,
101968
+ image: uploadedImageUrl2,
101969
+ order: i + 1
101970
+ };
101971
+ })
101972
+ );
101973
+ const validPages = pages.filter(Boolean);
101974
+ const usedFontNames = /* @__PURE__ */ new Set();
101975
+ validPages.forEach((p2) => {
101976
+ if (p2?.designData) {
101977
+ const fontsInPage = extractFontFamilies(p2.designData);
101978
+ fontsInPage.forEach((f) => usedFontNames.add(f));
101979
+ }
101980
+ });
101981
+ const resolvedFontIds = await resolveFontIds(Array.from(usedFontNames));
101807
101982
  const res = await TemplateDesignService.updateTemplateDesign(templateId, {
101808
- image: uploadedImageUrl
101983
+ image: uploadedImageUrl,
101984
+ pages: validPages,
101985
+ fonts: resolvedFontIds
101809
101986
  });
101810
101987
  if (res && res.success) {
101811
101988
  setGenerateSuccess(true);
@@ -102010,6 +102187,16 @@ function Topbar() {
102010
102187
  const resetTour = useTourStore((s2) => s2.reset);
102011
102188
  const templateId = getQueryParam("templateId") || getQueryParam("id");
102012
102189
  const designType = (getQueryParam("designType") || getQueryParam("designtype") || getQueryParam("type"))?.toLowerCase();
102190
+ const handleLeavePage = () => {
102191
+ if (designType === "text") {
102192
+ spaNavigate("/design-configuration/text-templates");
102193
+ return;
102194
+ }
102195
+ if (designType === "template") {
102196
+ spaNavigate("/design-configuration/template-designs");
102197
+ return;
102198
+ }
102199
+ };
102013
102200
  const handleSaveTextTemplate = async () => {
102014
102201
  const canvas2 = useEditorStore.getState().canvas;
102015
102202
  if (!templateId || !canvas2) return;
@@ -102104,8 +102291,42 @@ function Topbar() {
102104
102291
  }
102105
102292
  };
102106
102293
  return /* @__PURE__ */ jsxs("div", { className: "w-full h-14 bg-[#111] text-white flex items-center justify-between px-4 border-b border-gray-800 relative z-[9999] whitespace-nowrap", children: [
102107
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 shrink-0", children: [
102108
- /* @__PURE__ */ jsx("h1", { className: "text-lg font-semibold", children: "Vibrant" }),
102294
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 shrink-0", children: [
102295
+ /* @__PURE__ */ jsxs(
102296
+ "button",
102297
+ {
102298
+ id: "topbar-back-button",
102299
+ type: "button",
102300
+ onClick: handleLeavePage,
102301
+ disabled: isSaving,
102302
+ className: `flex items-center gap-1.5 px-2.5 py-1.5 bg-[#222] hover:bg-[#2c2c2c] active:bg-[#1a1a1a] text-gray-300 hover:text-white border border-gray-700/70 hover:border-gray-500 rounded-lg text-xs font-medium transition-all group shadow-sm ${isSaving ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`,
102303
+ title: "Back / Leave page",
102304
+ "aria-label": "Back / Leave page",
102305
+ children: [
102306
+ /* @__PURE__ */ jsx(
102307
+ "svg",
102308
+ {
102309
+ className: "w-4 h-4 transition-transform duration-150 group-hover:-translate-x-0.5 text-gray-300 group-hover:text-white",
102310
+ fill: "none",
102311
+ viewBox: "0 0 24 24",
102312
+ stroke: "currentColor",
102313
+ strokeWidth: 2.2,
102314
+ children: /* @__PURE__ */ jsx(
102315
+ "path",
102316
+ {
102317
+ strokeLinecap: "round",
102318
+ strokeLinejoin: "round",
102319
+ d: "M10 19l-7-7m0 0l7-7m-7 7h18"
102320
+ }
102321
+ )
102322
+ }
102323
+ ),
102324
+ /* @__PURE__ */ jsx("span", { className: "font-semibold tracking-wide", children: "Back" })
102325
+ ]
102326
+ }
102327
+ ),
102328
+ /* @__PURE__ */ jsx("div", { className: "w-px h-5 bg-gray-700/80 hidden sm:block" }),
102329
+ /* @__PURE__ */ jsx("h1", { className: "text-lg font-semibold tracking-tight", children: "Vibrant" }),
102109
102330
  /* @__PURE__ */ jsx("div", { className: "w-px h-6 bg-gray-600 hidden md:block" })
102110
102331
  ] }),
102111
102332
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 shrink-0 ml-4", children: [
@@ -102390,7 +102611,23 @@ const UploadIcon = () => /* @__PURE__ */ jsx(
102390
102611
  const useSidePanelStore = create(
102391
102612
  (set) => ({
102392
102613
  activePanel: null,
102393
- setActivePanel: (val) => set({ activePanel: val })
102614
+ setActivePanel: (val) => {
102615
+ if (val !== "draw") {
102616
+ const drawState = useDrawStore.getState();
102617
+ if (drawState.drawTool !== "select") {
102618
+ drawState.setDrawTool("select");
102619
+ }
102620
+ const canvas2 = useEditorStore.getState().canvas;
102621
+ if (canvas2 && canvas2.isDrawingMode) {
102622
+ canvas2.isDrawingMode = false;
102623
+ canvas2.selection = true;
102624
+ canvas2.defaultCursor = "default";
102625
+ canvas2._isCurrentlyDrawing = false;
102626
+ canvas2.requestRenderAll();
102627
+ }
102628
+ }
102629
+ set({ activePanel: val });
102630
+ }
102394
102631
  })
102395
102632
  );
102396
102633
  function Panel({ panelName }) {
@@ -102456,43 +102693,6 @@ function Panel({ panelName }) {
102456
102693
  ] })
102457
102694
  ] });
102458
102695
  }
102459
- function deriveType(objects) {
102460
- if (objects.length === 0) return "none";
102461
- if (objects.length > 1) return "multiple";
102462
- const obj = objects[0];
102463
- const t2 = obj.type?.toLowerCase();
102464
- if (t2 === "activeselection") return "multiple";
102465
- if (t2 === "group") {
102466
- if (obj.name === "qr") return "qr";
102467
- if (obj.name === "barcode") return "barcode";
102468
- return "group";
102469
- }
102470
- if (t2 === "textbox" || t2 === "i-text" || t2 === "text") return "textbox";
102471
- if (t2 === "image") return "image";
102472
- return t2 || "none";
102473
- }
102474
- const useSelectionStore = create((set) => ({
102475
- type: "none",
102476
- objects: [],
102477
- activeColor: null,
102478
- activeColorIndex: 0,
102479
- setSelection: (objects) => set({
102480
- objects,
102481
- type: deriveType(objects),
102482
- activeColor: null,
102483
- activeColorIndex: 0
102484
- }),
102485
- setActiveColor: (color, index = 0) => set({
102486
- activeColor: color,
102487
- activeColorIndex: index
102488
- }),
102489
- clearSelection: () => set({
102490
- type: "none",
102491
- objects: [],
102492
- activeColor: null,
102493
- activeColorIndex: 0
102494
- })
102495
- }));
102496
102696
  function TemplatesPanel() {
102497
102697
  const { productData } = useProductStore();
102498
102698
  const { startLoading, stopLoading, isLoading } = useLoaderStore();
@@ -102500,6 +102700,7 @@ function TemplatesPanel() {
102500
102700
  const [page, setPage] = useState(1);
102501
102701
  const scrollRef = useRef(null);
102502
102702
  const [hasMore, setHasMore] = useState(true);
102703
+ const templateId = getQueryParam("templateId");
102503
102704
  useEffect(() => {
102504
102705
  if (!productData?._id) return;
102505
102706
  startLoading();
@@ -102591,7 +102792,7 @@ function TemplatesPanel() {
102591
102792
  )
102592
102793
  ] }),
102593
102794
  /* @__PURE__ */ jsx("div", { ref: scrollRef, className: "h-[calc(100vh-125px)] overflow-auto custom-scrollbar", children: /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3 pb-12 mx-4", children: [
102594
- templates.map((tpl) => /* @__PURE__ */ jsx(
102795
+ templates.filter((t2) => t2.image && t2._id !== templateId).map((tpl) => /* @__PURE__ */ jsx(
102595
102796
  "button",
102596
102797
  {
102597
102798
  className: "rounded overflow-hidden group relative cursor-pointer border border-transparent hover:border-[#02bc71] transition-all",
@@ -102933,6 +103134,7 @@ async function addSVGToCanvas(type = "svg", url) {
102933
103134
  svg.setCoords();
102934
103135
  canvas2.add(svg);
102935
103136
  canvas2.setActiveObject(svg);
103137
+ bringOverlaysToFront(canvas2);
102936
103138
  useSelectionStore.getState().setSelection([svg]);
102937
103139
  canvas2.requestRenderAll();
102938
103140
  }
@@ -102983,6 +103185,7 @@ async function renderSVGStringToCanvas(type = "svg", svg, options = {}) {
102983
103185
  group.setCoords();
102984
103186
  canvas2.add(group);
102985
103187
  canvas2.setActiveObject(group);
103188
+ bringOverlaysToFront(canvas2);
102986
103189
  useSelectionStore.getState().setSelection([group]);
102987
103190
  canvas2.requestRenderAll();
102988
103191
  return group;
@@ -103052,26 +103255,63 @@ function AllElements({
103052
103255
  elements: elements2,
103053
103256
  onBack,
103054
103257
  scrollRef,
103055
- onSearch
103258
+ onSearch,
103259
+ initialSearch = "",
103260
+ title,
103261
+ backText
103056
103262
  }) {
103057
- const [search, setSearch] = useState("");
103058
- const { isLoading } = useLoaderStore.getState();
103263
+ const [search, setSearch] = useState(initialSearch);
103264
+ const { isLoading } = useLoaderStore();
103059
103265
  const { addRecentItem } = useRecentElementsStore();
103266
+ useEffect(() => {
103267
+ setSearch(initialSearch);
103268
+ }, [initialSearch]);
103060
103269
  const clearSearch = () => {
103061
103270
  setSearch("");
103062
103271
  onSearch("");
103063
103272
  };
103064
- const handleChange = useCallback(
103065
- (e3) => {
103066
- const value = e3.target.value;
103067
- setSearch(value);
103068
- onSearch(value.trim());
103069
- },
103070
- [onSearch]
103071
- );
103072
- return /* @__PURE__ */ jsxs(Fragment, { children: [
103073
- /* @__PURE__ */ jsx("div", { onClick: onBack, className: "cursor-pointer mx-4 mb-2", children: "← Back" }),
103074
- /* @__PURE__ */ jsxs("div", { className: "bg-neutral-800 px-3 py-2 rounded flex items-center gap-2 mb-4 mx-4 relative", children: [
103273
+ const handleSearchSubmit = () => {
103274
+ onSearch(search.trim());
103275
+ };
103276
+ const handleKeyDown = (e3) => {
103277
+ e3.stopPropagation();
103278
+ if (e3.key === "Enter") {
103279
+ handleSearchSubmit();
103280
+ }
103281
+ };
103282
+ const dynamicBackLabel = backText || (title ? title : "Back");
103283
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
103284
+ /* @__PURE__ */ jsx("div", { className: "mx-4 mb-2.5 flex items-center", children: /* @__PURE__ */ jsxs(
103285
+ "button",
103286
+ {
103287
+ type: "button",
103288
+ onClick: onBack,
103289
+ className: "cursor-pointer flex items-center gap-1.5 text-xs font-semibold text-neutral-300 hover:text-white transition-colors group",
103290
+ title: `Back to ${dynamicBackLabel}`,
103291
+ children: [
103292
+ /* @__PURE__ */ jsxs(
103293
+ "svg",
103294
+ {
103295
+ width: "16",
103296
+ height: "16",
103297
+ viewBox: "0 0 24 24",
103298
+ fill: "none",
103299
+ stroke: "currentColor",
103300
+ strokeWidth: "2.5",
103301
+ strokeLinecap: "round",
103302
+ strokeLinejoin: "round",
103303
+ className: "group-hover:-translate-x-0.5 transition-transform text-neutral-400 group-hover:text-white",
103304
+ children: [
103305
+ /* @__PURE__ */ jsx("line", { x1: "19", y1: "12", x2: "5", y2: "12" }),
103306
+ /* @__PURE__ */ jsx("polyline", { points: "12 19 5 12 12 5" })
103307
+ ]
103308
+ }
103309
+ ),
103310
+ /* @__PURE__ */ jsx("span", { className: "capitalize truncate max-w-[240px]", children: dynamicBackLabel })
103311
+ ]
103312
+ }
103313
+ ) }),
103314
+ /* @__PURE__ */ jsxs("div", { className: "bg-neutral-800 px-3 py-2 rounded flex items-center gap-2 mb-4 mx-4 relative focus-within:ring-1 focus-within:ring-neutral-600", children: [
103075
103315
  /* @__PURE__ */ jsxs(
103076
103316
  "svg",
103077
103317
  {
@@ -103079,7 +103319,8 @@ function AllElements({
103079
103319
  stroke: "currentColor",
103080
103320
  fill: "none",
103081
103321
  viewBox: "0 0 24 24",
103082
- className: "text-gray-300",
103322
+ className: "text-gray-400 shrink-0 cursor-pointer hover:text-white transition-colors",
103323
+ onClick: handleSearchSubmit,
103083
103324
  children: [
103084
103325
  /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "8" }),
103085
103326
  /* @__PURE__ */ jsx("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" })
@@ -103091,9 +103332,10 @@ function AllElements({
103091
103332
  {
103092
103333
  type: "text",
103093
103334
  value: search,
103094
- onChange: handleChange,
103095
- onKeyDown: (e3) => e3.stopPropagation(),
103096
- className: "bg-transparent outline-none text-sm text-gray-300 w-full pr-8"
103335
+ onChange: (e3) => setSearch(e3.target.value),
103336
+ onKeyDown: handleKeyDown,
103337
+ placeholder: title ? `Search in ${title}...` : "Search elements & shapes...",
103338
+ className: "bg-transparent outline-none text-sm text-gray-200 placeholder-gray-500 w-full pr-8"
103097
103339
  }
103098
103340
  ),
103099
103341
  search && /* @__PURE__ */ jsx(
@@ -103101,13 +103343,13 @@ function AllElements({
103101
103343
  {
103102
103344
  type: "button",
103103
103345
  onClick: clearSearch,
103104
- className: "absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-white transition-colors p-1",
103346
+ className: "absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-white transition-colors p-1 cursor-pointer",
103105
103347
  "aria-label": "Clear search",
103106
103348
  children: /* @__PURE__ */ jsxs(
103107
103349
  "svg",
103108
103350
  {
103109
- width: "16",
103110
- height: "16",
103351
+ width: "14",
103352
+ height: "14",
103111
103353
  viewBox: "0 0 24 24",
103112
103354
  fill: "none",
103113
103355
  stroke: "currentColor",
@@ -103121,9 +103363,20 @@ function AllElements({
103121
103363
  }
103122
103364
  )
103123
103365
  ] }),
103124
- /* @__PURE__ */ jsx("hr", { className: "text-neutral-800 h-[0.5px]" }),
103125
- /* @__PURE__ */ jsx("div", { className: "h-[calc(100vh-160px)] overflow-auto", ref: scrollRef, children: /* @__PURE__ */ jsx("div", { className: "mx-4 py-5", children: elements2.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-gray-400 text-sm text-center", children: "No results found" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
103126
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-4 md:grid-cols-3 gap-5", children: elements2.map((shape, i) => /* @__PURE__ */ jsx(
103366
+ /* @__PURE__ */ jsx("hr", { className: "text-neutral-800 h-[0.5px] border-neutral-800" }),
103367
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0 overflow-auto custom-scrollbar", ref: scrollRef, children: /* @__PURE__ */ jsx("div", { className: "mx-4 py-5", children: elements2.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "py-12 text-center", children: [
103368
+ /* @__PURE__ */ jsx("p", { className: "text-gray-400 text-sm", children: search.trim() ? `No results found for "${search}"` : "No elements found" }),
103369
+ search.trim() && /* @__PURE__ */ jsx(
103370
+ "button",
103371
+ {
103372
+ type: "button",
103373
+ onClick: clearSearch,
103374
+ className: "mt-3 text-xs text-emerald-400 hover:underline cursor-pointer",
103375
+ children: "Clear search"
103376
+ }
103377
+ )
103378
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
103379
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-4 md:grid-cols-3 gap-3", children: elements2.map((shape, i) => /* @__PURE__ */ jsx(
103127
103380
  "button",
103128
103381
  {
103129
103382
  type: "button",
@@ -103138,21 +103391,22 @@ function AllElements({
103138
103391
  useSidePanelStore.getState().setActivePanel(null);
103139
103392
  }
103140
103393
  },
103141
- className: "w-full cursor-pointer flex items-center justify-center",
103394
+ className: "w-full cursor-pointer flex items-center justify-center p-2 rounded-lg bg-neutral-900/40 hover:bg-neutral-800 border border-neutral-800/60 hover:border-neutral-700 transition-colors",
103142
103395
  children: shape.image && /* @__PURE__ */ jsx(
103143
103396
  "img",
103144
103397
  {
103145
103398
  src: shape.image,
103146
- alt: shape.category,
103399
+ alt: shape.category || "element",
103147
103400
  width: 150,
103148
103401
  height: 150,
103149
- className: "w-full h-24 object-contain"
103402
+ className: "w-full h-20 object-contain",
103403
+ loading: "lazy"
103150
103404
  }
103151
103405
  )
103152
103406
  },
103153
- i
103407
+ shape._id || shape.image || i
103154
103408
  )) }),
103155
- isLoading && /* @__PURE__ */ jsx("div", { className: "text-center py-4 text-sm text-gray-400", children: "Loading more..." })
103409
+ isLoading && /* @__PURE__ */ jsx("div", { className: "text-center py-4 text-xs text-gray-400 animate-pulse", children: "Loading more elements..." })
103156
103410
  ] }) }) })
103157
103411
  ] });
103158
103412
  }
@@ -103185,72 +103439,181 @@ function ElementsPanel() {
103185
103439
  const [page, setPage] = useState(1);
103186
103440
  const [category, setCategory] = useState(null);
103187
103441
  const [keyType, setKeyType] = useState(null);
103188
- const [error2, setError] = useState(null);
103442
+ const [searchQuery, setSearchQuery] = useState("");
103443
+ const [mainSearch, setMainSearch] = useState("");
103444
+ const [isViewingAll, setIsViewingAll] = useState(false);
103445
+ const [title, setTitle] = useState("");
103189
103446
  const [hasMore, setHasMore] = useState(true);
103190
103447
  const scrollRef = useRef(null);
103191
- const getSVG = async (type = null, categoryValue, pageNo = 1) => {
103448
+ const getSVG = async (type = null, categoryValue = null, searchVal = null, pageNo = 1) => {
103192
103449
  startLoading();
103193
103450
  if (pageNo === 1) {
103194
103451
  setHasMore(true);
103195
103452
  setPage(1);
103196
103453
  setCategory(categoryValue);
103454
+ setKeyType(type);
103455
+ setSearchQuery(searchVal || "");
103456
+ }
103457
+ if (categoryValue === "Recent items") {
103458
+ const q2 = (searchVal || "").toLowerCase().trim();
103459
+ const filtered = q2 ? recentItems.filter(
103460
+ (item) => item.category?.toLowerCase().includes(q2) || item.tags?.some((t2) => t2.toLowerCase().includes(q2))
103461
+ ) : recentItems;
103462
+ setElementsSVG(filtered);
103463
+ setHasMore(false);
103464
+ stopLoading();
103465
+ return;
103197
103466
  }
103198
- const request = type ? ShapeService.getAllExternal({
103199
- isVector: true,
103200
- page: pageNo,
103201
- limit: 30,
103202
- keyType: type,
103203
- query: categoryValue || null
103204
- }) : ShapeService.getAll({
103205
- category: categoryValue,
103206
- page: pageNo,
103207
- limit: 30
103208
- });
103209
- request.then((res) => {
103210
- const items = res.items;
103467
+ try {
103468
+ const request = type ? ShapeService.getAllExternal({
103469
+ isVector: true,
103470
+ page: pageNo,
103471
+ limit: 30,
103472
+ keyType: type,
103473
+ query: searchVal || categoryValue || null
103474
+ }) : ShapeService.getAll({
103475
+ category: categoryValue || null,
103476
+ search: searchVal || null,
103477
+ page: pageNo,
103478
+ limit: 30
103479
+ });
103480
+ const res = await request;
103481
+ const items = res.items || [];
103211
103482
  const newItems = items.filter((x2) => x2.status && x2.image);
103212
103483
  setElementsSVG(
103213
103484
  (prev) => pageNo === 1 ? newItems : [...prev, ...newItems]
103214
103485
  );
103215
103486
  if (newItems.length < 20) setHasMore(false);
103216
- }).catch((err) => setError(err.message)).finally(() => stopLoading());
103487
+ else setHasMore(true);
103488
+ } catch (err) {
103489
+ console.error("Failed to load elements:", err);
103490
+ if (pageNo === 1) setElementsSVG([]);
103491
+ setHasMore(false);
103492
+ } finally {
103493
+ stopLoading();
103494
+ }
103217
103495
  };
103218
103496
  useEffect(() => {
103219
103497
  const el = scrollRef.current;
103220
- if (!el) return;
103498
+ if (!el || !isViewingAll) return;
103221
103499
  const onScroll = () => {
103222
103500
  if (!hasMore || isLoading) return;
103223
103501
  if (el.scrollTop + el.clientHeight >= el.scrollHeight - 200) {
103224
- const next = page + 1;
103225
- setPage(next);
103226
- getSVG(keyType, category, next);
103502
+ setPage((prev) => {
103503
+ const next = prev + 1;
103504
+ getSVG(keyType, category, searchQuery, next);
103505
+ return next;
103506
+ });
103227
103507
  }
103228
103508
  };
103229
103509
  el.addEventListener("scroll", onScroll);
103230
103510
  return () => el.removeEventListener("scroll", onScroll);
103231
- }, [hasMore, isLoading]);
103232
- if (elementsSVG.length > 0) {
103511
+ }, [hasMore, isLoading, isViewingAll, keyType, category, searchQuery]);
103512
+ const handleMainSearch = (val) => {
103513
+ const q2 = val.trim();
103514
+ if (!q2) return;
103515
+ setIsViewingAll(true);
103516
+ setTitle(`Search: "${q2}"`);
103517
+ getSVG(null, null, q2, 1);
103518
+ };
103519
+ const handleBack = () => {
103520
+ if (searchQuery && category && category !== "Recent items") {
103521
+ setSearchQuery("");
103522
+ setPage(1);
103523
+ setTitle(category);
103524
+ getSVG(keyType, category, "", 1);
103525
+ return;
103526
+ }
103527
+ setIsViewingAll(false);
103528
+ setElementsSVG([]);
103529
+ setCategory(null);
103530
+ setKeyType(null);
103531
+ setSearchQuery("");
103532
+ setMainSearch("");
103533
+ setTitle("");
103534
+ setPage(1);
103535
+ setHasMore(true);
103536
+ };
103537
+ if (isViewingAll) {
103233
103538
  return /* @__PURE__ */ jsx(
103234
103539
  AllElements,
103235
103540
  {
103236
103541
  elements: elementsSVG,
103237
103542
  scrollRef,
103543
+ title,
103544
+ backText: title || "Elements",
103545
+ initialSearch: searchQuery,
103238
103546
  onSearch: (value) => {
103239
- setPage(1);
103240
- setHasMore(true);
103241
- getSVG(keyType, value ? value : category, 1);
103547
+ if (value.trim()) {
103548
+ setTitle(category ? `${category} - "${value.trim()}"` : `Search: "${value.trim()}"`);
103549
+ } else {
103550
+ setTitle(
103551
+ category || (keyType === "pixabay" ? "Pixabay Vectors" : keyType === "flat" ? "FlatIcon Icons" : "All Elements")
103552
+ );
103553
+ }
103554
+ getSVG(keyType, category, value, 1);
103242
103555
  },
103243
- onBack: () => {
103244
- setElementsSVG([]);
103245
- setPage(1);
103246
- setCategory(null);
103247
- setKeyType(null);
103248
- setHasMore(true);
103249
- }
103556
+ onBack: handleBack
103250
103557
  }
103251
103558
  );
103252
103559
  }
103253
103560
  return /* @__PURE__ */ jsxs(Fragment, { children: [
103561
+ /* @__PURE__ */ jsxs("div", { className: "bg-neutral-800 px-3 py-2 rounded flex items-center gap-2 mb-4 mx-4 relative focus-within:ring-1 focus-within:ring-neutral-600", children: [
103562
+ /* @__PURE__ */ jsxs(
103563
+ "svg",
103564
+ {
103565
+ width: "18",
103566
+ stroke: "currentColor",
103567
+ fill: "none",
103568
+ viewBox: "0 0 24 24",
103569
+ className: "text-gray-400 shrink-0",
103570
+ children: [
103571
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "8" }),
103572
+ /* @__PURE__ */ jsx("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" })
103573
+ ]
103574
+ }
103575
+ ),
103576
+ /* @__PURE__ */ jsx(
103577
+ "input",
103578
+ {
103579
+ type: "text",
103580
+ placeholder: "Search elements & shapes...",
103581
+ value: mainSearch,
103582
+ onChange: (e3) => setMainSearch(e3.target.value),
103583
+ onKeyDown: (e3) => {
103584
+ e3.stopPropagation();
103585
+ if (e3.key === "Enter" && mainSearch.trim()) {
103586
+ handleMainSearch(mainSearch.trim());
103587
+ }
103588
+ },
103589
+ className: "bg-transparent outline-none text-sm text-gray-200 placeholder-gray-500 w-full pr-7"
103590
+ }
103591
+ ),
103592
+ mainSearch && /* @__PURE__ */ jsx(
103593
+ "button",
103594
+ {
103595
+ type: "button",
103596
+ onClick: () => setMainSearch(""),
103597
+ className: "absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white transition-colors p-1 cursor-pointer",
103598
+ "aria-label": "Clear search",
103599
+ children: /* @__PURE__ */ jsxs(
103600
+ "svg",
103601
+ {
103602
+ width: "14",
103603
+ height: "14",
103604
+ viewBox: "0 0 24 24",
103605
+ fill: "none",
103606
+ stroke: "currentColor",
103607
+ strokeWidth: "2",
103608
+ children: [
103609
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
103610
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
103611
+ ]
103612
+ }
103613
+ )
103614
+ }
103615
+ )
103616
+ ] }),
103254
103617
  /* @__PURE__ */ jsxs("div", { className: "pb-5 mx-4", children: [
103255
103618
  /* @__PURE__ */ jsx("h2", { className: "text-md font-medium mb-3", children: "Upload From Other Media" }),
103256
103619
  /* @__PURE__ */ jsxs("div", { className: "flex gap-4", children: [
@@ -103258,11 +103621,13 @@ function ElementsPanel() {
103258
103621
  "button",
103259
103622
  {
103260
103623
  type: "button",
103261
- className: "cursor-pointer",
103624
+ className: "cursor-pointer hover:opacity-80 transition-opacity",
103262
103625
  onClick: () => {
103263
- setKeyType("pixabay");
103264
- getSVG("pixabay", "", 1);
103626
+ setIsViewingAll(true);
103627
+ setTitle("Pixabay Vectors");
103628
+ getSVG("pixabay", "", "", 1);
103265
103629
  },
103630
+ title: "Search Pixabay Vectors",
103266
103631
  children: /* @__PURE__ */ jsx(PixabayIcon, {})
103267
103632
  }
103268
103633
  ),
@@ -103270,11 +103635,13 @@ function ElementsPanel() {
103270
103635
  "button",
103271
103636
  {
103272
103637
  type: "button",
103273
- className: "cursor-pointer",
103638
+ className: "cursor-pointer hover:opacity-80 transition-opacity",
103274
103639
  onClick: () => {
103275
- setKeyType("flat");
103276
- getSVG("flat", "", 1);
103640
+ setIsViewingAll(true);
103641
+ setTitle("FlatIcon Icons");
103642
+ getSVG("flat", "", "", 1);
103277
103643
  },
103644
+ title: "Search FlatIcon Icons",
103278
103645
  children: /* @__PURE__ */ jsx(
103279
103646
  "img",
103280
103647
  {
@@ -103289,20 +103656,21 @@ function ElementsPanel() {
103289
103656
  )
103290
103657
  ] })
103291
103658
  ] }),
103292
- /* @__PURE__ */ jsx("hr", { className: "text-neutral-800 h-[0.5px]" }),
103293
- /* @__PURE__ */ jsxs("div", { className: "h-[calc(100dvh-180px)] overflow-auto custom-scrollbar", children: [
103659
+ /* @__PURE__ */ jsx("hr", { className: "text-neutral-800 h-[0.5px] border-neutral-800" }),
103660
+ /* @__PURE__ */ jsxs("div", { className: "h-[calc(100dvh-200px)] overflow-auto custom-scrollbar", children: [
103294
103661
  recentItems.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mx-4", children: [
103295
103662
  /* @__PURE__ */ jsxs("div", { className: "py-5", children: [
103296
103663
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
103297
103664
  /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold mb-3", children: "Recent items" }),
103298
103665
  recentItems.length > 3 && /* @__PURE__ */ jsx(
103299
- "div",
103666
+ "button",
103300
103667
  {
103301
- className: "underline cursor-pointer",
103668
+ type: "button",
103669
+ className: "underline text-xs text-neutral-400 hover:text-white cursor-pointer",
103302
103670
  onClick: () => {
103303
- setElementsSVG(recentItems);
103304
- setCategory("Recent items");
103305
- setHasMore(false);
103671
+ setIsViewingAll(true);
103672
+ setTitle("Recent items");
103673
+ getSVG(null, "Recent items", "", 1);
103306
103674
  },
103307
103675
  children: "View all"
103308
103676
  }
@@ -103311,36 +103679,45 @@ function ElementsPanel() {
103311
103679
  /* @__PURE__ */ jsx("div", { className: "grid grid-cols-4 md:grid-cols-3 gap-3", children: recentItems.slice(0, 3).map((shape, i) => /* @__PURE__ */ jsx(
103312
103680
  "button",
103313
103681
  {
103682
+ type: "button",
103314
103683
  onClick: () => {
103315
103684
  addSVGToCanvas("svg", shape.image);
103316
103685
  addRecentItem(shape);
103686
+ if (window.innerWidth < 768) {
103687
+ useSidePanelStore.getState().setActivePanel(null);
103688
+ }
103317
103689
  },
103318
- className: "w-full cursor-pointer flex items-center justify-center",
103690
+ className: "w-full cursor-pointer flex items-center justify-center p-2 rounded-lg bg-neutral-900/40 hover:bg-neutral-800 border border-neutral-800/60 hover:border-neutral-700 transition-colors",
103319
103691
  children: /* @__PURE__ */ jsx(
103320
103692
  "img",
103321
103693
  {
103322
103694
  src: shape.image,
103323
- alt: shape.category,
103695
+ alt: shape.category || "recent element",
103324
103696
  width: 150,
103325
103697
  height: 150,
103326
- className: "w-full h-24 object-contain"
103698
+ className: "w-full h-20 object-contain"
103327
103699
  }
103328
103700
  )
103329
103701
  },
103330
103702
  shape._id || shape.image || i
103331
103703
  )) })
103332
103704
  ] }),
103333
- /* @__PURE__ */ jsx("hr", { className: "text-neutral-800 h-[0.5px]" })
103705
+ /* @__PURE__ */ jsx("hr", { className: "text-neutral-800 h-[0.5px] border-neutral-800" })
103334
103706
  ] }),
103335
103707
  elements.map((item, i) => /* @__PURE__ */ jsxs("div", { className: "mx-4", children: [
103336
103708
  /* @__PURE__ */ jsxs("div", { className: "py-5", children: [
103337
103709
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
103338
103710
  /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold mb-3", children: item.category }),
103339
103711
  /* @__PURE__ */ jsx(
103340
- "div",
103712
+ "button",
103341
103713
  {
103342
- className: "underline cursor-pointer",
103343
- onClick: () => getSVG(null, item?.category, 1),
103714
+ type: "button",
103715
+ className: "underline text-xs text-neutral-400 hover:text-white cursor-pointer",
103716
+ onClick: () => {
103717
+ setIsViewingAll(true);
103718
+ setTitle(item.category);
103719
+ getSVG(null, item.category, "", 1);
103720
+ },
103344
103721
  children: "View all"
103345
103722
  }
103346
103723
  )
@@ -103348,6 +103725,7 @@ function ElementsPanel() {
103348
103725
  /* @__PURE__ */ jsx("div", { className: "grid grid-cols-4 md:grid-cols-3 gap-3", children: item.data.map((shape, j2) => /* @__PURE__ */ jsx(
103349
103726
  "button",
103350
103727
  {
103728
+ type: "button",
103351
103729
  onClick: () => {
103352
103730
  addSVGToCanvas("svg", shape.image);
103353
103731
  addRecentItem({
@@ -103358,8 +103736,11 @@ function ElementsPanel() {
103358
103736
  largeImage: shape.image,
103359
103737
  tags: []
103360
103738
  });
103739
+ if (window.innerWidth < 768) {
103740
+ useSidePanelStore.getState().setActivePanel(null);
103741
+ }
103361
103742
  },
103362
- className: "w-full cursor-pointer flex items-center justify-center",
103743
+ className: "w-full cursor-pointer flex items-center justify-center p-2 rounded-lg bg-neutral-900/40 hover:bg-neutral-800 border border-neutral-800/60 hover:border-neutral-700 transition-colors",
103363
103744
  children: /* @__PURE__ */ jsx(
103364
103745
  "img",
103365
103746
  {
@@ -103367,14 +103748,15 @@ function ElementsPanel() {
103367
103748
  alt: item.category,
103368
103749
  width: 150,
103369
103750
  height: 150,
103370
- className: "w-full h-24 object-contain"
103751
+ className: "w-full h-20 object-contain",
103752
+ loading: "lazy"
103371
103753
  }
103372
103754
  )
103373
103755
  },
103374
103756
  j2
103375
103757
  )) })
103376
103758
  ] }),
103377
- /* @__PURE__ */ jsx("hr", { className: "text-neutral-800 h-[0.5px]" })
103759
+ /* @__PURE__ */ jsx("hr", { className: "text-neutral-800 h-[0.5px] border-neutral-800" })
103378
103760
  ] }, i))
103379
103761
  ] })
103380
103762
  ] });
@@ -103387,7 +103769,7 @@ async function addTextToCanvas(fontSize, width, textName) {
103387
103769
  if (!workarea) return;
103388
103770
  const workareaCenter = workarea.getCenterPoint();
103389
103771
  const workareaBounds = workarea.getBoundingRect();
103390
- const maxTextWidth = Math.max(40, workareaBounds.width * 0.8);
103772
+ const maxTextWidth = Math.max(40, workareaBounds.width * 0.85);
103391
103773
  const textWidth = Math.min(width, maxTextWidth);
103392
103774
  const text = new Uo(textName, {
103393
103775
  fontSize,
@@ -103400,19 +103782,26 @@ async function addTextToCanvas(fontSize, width, textName) {
103400
103782
  strokeWidth: 0,
103401
103783
  hasRotatingPoint: true,
103402
103784
  centerTransform: true,
103403
- padding: 5,
103785
+ padding: 0,
103404
103786
  strokeUniform: true,
103405
103787
  paintFirst: "stroke",
103406
103788
  objectCaching: false
103407
103789
  // Ensure maximum sharpness
103408
103790
  });
103791
+ const measuredWidth = Math.ceil(text.calcTextWidth());
103792
+ if (measuredWidth > 0 && !textName.includes("\n")) {
103793
+ text.set({ width: Math.min(Math.max(measuredWidth + 4, 20), maxTextWidth) });
103794
+ text.initDimensions();
103795
+ }
103409
103796
  canvas2.add(text);
103410
103797
  text.set({
103411
103798
  left: workareaCenter.x - text.getScaledWidth() / 2,
103412
103799
  top: workareaCenter.y - text.getScaledHeight() / 2
103413
103800
  });
103414
103801
  canvas2.setActiveObject(text);
103802
+ bringOverlaysToFront(canvas2);
103415
103803
  canvas2.requestRenderAll();
103804
+ useHistoryStore.getState().saveState();
103416
103805
  }
103417
103806
  async function addTextTemplate(id) {
103418
103807
  try {
@@ -103444,6 +103833,7 @@ async function addTextTemplate(id) {
103444
103833
  canvas2.add(...fabricObjects);
103445
103834
  canvas2.renderOnAddRemove = true;
103446
103835
  canvas2.requestRenderAll();
103836
+ useHistoryStore.getState().saveState();
103447
103837
  } catch (err) {
103448
103838
  console.error("Failed to add text template:", err);
103449
103839
  }
@@ -104211,11 +104601,20 @@ function PhotosPanel() {
104211
104601
  }) : !isLoading ? /* @__PURE__ */ jsx("div", { className: "col-span-full py-8 text-center text-xs text-gray-500", children: "No images found" }) : null }) })
104212
104602
  ] });
104213
104603
  }
104604
+ let bgSaveTimer = null;
104605
+ function flushBackgroundSave() {
104606
+ if (bgSaveTimer) {
104607
+ clearTimeout(bgSaveTimer);
104608
+ bgSaveTimer = null;
104609
+ useHistoryStore.getState().saveState();
104610
+ }
104611
+ }
104214
104612
  async function addBackgroundImage(url) {
104215
104613
  const canvas2 = useEditorStore.getState().canvas;
104216
104614
  if (!canvas2) return;
104217
104615
  const workarea = canvas2.getObjects().find((o2) => o2.name === "workarea");
104218
104616
  if (!workarea) return;
104617
+ flushBackgroundSave();
104219
104618
  workarea.set("fill", "#ffffff");
104220
104619
  canvas2.getObjects("image").forEach((o2) => {
104221
104620
  if (o2.name === "bg-image") canvas2.remove(o2);
@@ -104227,7 +104626,16 @@ async function addBackgroundImage(url) {
104227
104626
  const activeSide = workarea.__pageSide || useHistoryStore.getState().currentPage || "front";
104228
104627
  const baseRx = (workarea.rx || workarea.__radiusOfCanvas || 0) * (workarea.scaleX || 1);
104229
104628
  const baseRy = (workarea.ry || workarea.__radiusOfCanvas || 0) * (workarea.scaleY || 1);
104230
- oa.fromURL(url + "?t=vb", { crossOrigin: "anonymous" }).then((img) => {
104629
+ let finalUrl = url;
104630
+ try {
104631
+ const b64 = await getBase64ImageFromURL(url);
104632
+ if (b64 && (b64.startsWith("data:image/png") || b64.startsWith("data:image/jpeg"))) {
104633
+ finalUrl = b64;
104634
+ }
104635
+ } catch {
104636
+ }
104637
+ try {
104638
+ const img = await oa.fromURL(finalUrl, { crossOrigin: "anonymous" });
104231
104639
  const imgW = img.width || 1;
104232
104640
  const imgH = img.height || 1;
104233
104641
  const scale2 = Math.max(waW / imgW, waH / imgH);
@@ -104263,12 +104671,14 @@ async function addBackgroundImage(url) {
104263
104671
  canvas2.add(img);
104264
104672
  canvas2.sendObjectToBack(img);
104265
104673
  canvas2.sendObjectToBack(workarea);
104674
+ bringOverlaysToFront(canvas2);
104266
104675
  canvas2.requestRenderAll();
104267
- }).catch((err) => {
104676
+ useHistoryStore.getState().saveState();
104677
+ } catch (err) {
104268
104678
  console.warn("Fabric failed to load background image:", url, err);
104269
- });
104679
+ }
104270
104680
  }
104271
- async function addBackgroundColor(color) {
104681
+ async function addBackgroundColor(color, immediate = false) {
104272
104682
  const canvas2 = useEditorStore.getState().canvas;
104273
104683
  if (!canvas2) return;
104274
104684
  const workarea = canvas2.getObjects().find((o2) => o2.name === "workarea");
@@ -104277,7 +104687,20 @@ async function addBackgroundColor(color) {
104277
104687
  if (o2.name === "bg-image") canvas2.remove(o2);
104278
104688
  });
104279
104689
  workarea.set("fill", color);
104690
+ bringOverlaysToFront(canvas2);
104280
104691
  canvas2.requestRenderAll();
104692
+ if (bgSaveTimer) {
104693
+ clearTimeout(bgSaveTimer);
104694
+ bgSaveTimer = null;
104695
+ }
104696
+ if (immediate) {
104697
+ useHistoryStore.getState().saveState();
104698
+ } else {
104699
+ bgSaveTimer = setTimeout(() => {
104700
+ useHistoryStore.getState().saveState();
104701
+ bgSaveTimer = null;
104702
+ }, 120);
104703
+ }
104281
104704
  }
104282
104705
  function normalizeHexColor(c2) {
104283
104706
  if (!c2 || typeof c2 !== "string" || c2 === "none" || c2 === "transparent" || c2.trim() === "") return null;
@@ -106037,11 +106460,11 @@ function BackgroundPanel() {
106037
106460
  const [hasMore, setHasMore] = useState(true);
106038
106461
  const [page, setPage] = useState(1);
106039
106462
  const scrollRef = useRef(null);
106040
- const handleApplyBackground = (type, value) => {
106463
+ const handleApplyBackground = async (type, value, immediate = false) => {
106041
106464
  if (type === "color") {
106042
- addBackgroundColor(value);
106465
+ await addBackgroundColor(value, immediate);
106043
106466
  } else {
106044
- addBackgroundImage(value);
106467
+ await addBackgroundImage(value);
106045
106468
  }
106046
106469
  if (window.innerWidth < 768) {
106047
106470
  setActivePanel(null);
@@ -106094,7 +106517,7 @@ function BackgroundPanel() {
106094
106517
  CMYKColorPicker,
106095
106518
  {
106096
106519
  value: "#ffffff",
106097
- onChange: (hex) => handleApplyBackground("color", hex)
106520
+ onChange: (hex) => handleApplyBackground("color", hex, false)
106098
106521
  }
106099
106522
  ),
106100
106523
  CMYK_SAFE_COLORS.map(({ hex, label }) => /* @__PURE__ */ jsx(
@@ -106102,7 +106525,7 @@ function BackgroundPanel() {
106102
106525
  {
106103
106526
  className: "w-8 h-8 rounded border border-neutral-600 cursor-pointer transition-transform hover:scale-110",
106104
106527
  style: { backgroundColor: hex },
106105
- onClick: () => handleApplyBackground("color", hex),
106528
+ onClick: () => handleApplyBackground("color", hex, true),
106106
106529
  title: label
106107
106530
  },
106108
106531
  hex
@@ -106118,7 +106541,7 @@ function BackgroundPanel() {
106118
106541
  const image = await ShapeService.convertImages(item.largeImage);
106119
106542
  const imageUrl = image?.success ? image?.url : null;
106120
106543
  if (!imageUrl) return;
106121
- handleApplyBackground("image", imageUrl);
106544
+ await handleApplyBackground("image", imageUrl);
106122
106545
  if (window.innerWidth < 768) {
106123
106546
  useSidePanelStore.getState().setActivePanel(null);
106124
106547
  }
@@ -106214,24 +106637,6 @@ function UploadPanel() {
106214
106637
  )) })
106215
106638
  ] });
106216
106639
  }
106217
- const useDrawStore = create((set) => ({
106218
- drawTool: "select",
106219
- strokeWidth: 5,
106220
- opacity: 100,
106221
- color: "#000000",
106222
- setDrawTool: (tool) => set(() => {
106223
- if (tool === "highlighter") {
106224
- return { drawTool: tool, opacity: 50 };
106225
- }
106226
- if (tool === "brush") {
106227
- return { drawTool: tool, opacity: 100 };
106228
- }
106229
- return { drawTool: tool };
106230
- }),
106231
- setStrokeWidth: (v2) => set({ strokeWidth: v2 }),
106232
- setOpacity: (v2) => set({ opacity: v2 }),
106233
- setColor: (v2) => set({ color: v2 })
106234
- }));
106235
106640
  function DrawPanel() {
106236
106641
  const {
106237
106642
  drawTool,
@@ -106246,7 +106651,7 @@ function DrawPanel() {
106246
106651
  const isDisabled = drawTool === "select";
106247
106652
  const TOOLS = [
106248
106653
  { key: "select", label: "✦ Selection" },
106249
- { key: "brush", label: "🖊 Brush" },
106654
+ { key: "brush", label: "✏️ Brush" },
106250
106655
  { key: "highlighter", label: "✎ Highlighter" }
106251
106656
  ];
106252
106657
  return /* @__PURE__ */ jsxs("div", { className: "mx-4 select-none", children: [
@@ -111037,8 +111442,8 @@ function MaskPanel() {
111037
111442
  const objWidth = obj.width ?? 1;
111038
111443
  const objHeight = obj.height ?? 1;
111039
111444
  const pathDims = clipPath.getBoundingRect();
111040
- const scaleX = objWidth / pathDims.width;
111041
- const scaleY = objHeight / pathDims.height;
111445
+ const scaleX = objWidth / (clipPath.width || pathDims.width);
111446
+ const scaleY = objHeight / (clipPath.height || pathDims.height);
111042
111447
  clipPath.set({
111043
111448
  scaleX,
111044
111449
  scaleY,
@@ -111047,6 +111452,8 @@ function MaskPanel() {
111047
111452
  angle: 0
111048
111453
  // Should not inherit angle since the parent handles rotation
111049
111454
  });
111455
+ clipPath.__maskPath = svgPath;
111456
+ obj.__maskPath = svgPath;
111050
111457
  obj.set({ clipPath });
111051
111458
  obj.setCoords();
111052
111459
  canvas2.requestRenderAll();
@@ -156997,7 +157404,7 @@ function SidePanel() {
156997
157404
  id: item.id,
156998
157405
  item,
156999
157406
  isActive: activePanel === item.id,
157000
- onClick: () => setActivePanel(item.id)
157407
+ onClick: () => setActivePanel(activePanel === item.id ? null : item.id)
157001
157408
  },
157002
157409
  item.id
157003
157410
  )
@@ -157012,7 +157419,7 @@ function SidePanel() {
157012
157419
  item,
157013
157420
  variant: "mobile",
157014
157421
  isActive: activePanel === item.id,
157015
- onClick: () => setActivePanel(item.id)
157422
+ onClick: () => setActivePanel(activePanel === item.id ? null : item.id)
157016
157423
  },
157017
157424
  item.id
157018
157425
  )
@@ -157138,20 +157545,48 @@ function TextFontSize({
157138
157545
  }
157139
157546
  );
157140
157547
  }
157141
- function TextAlignButton({
157548
+ function TextAlignGroup({
157142
157549
  value,
157143
157550
  onChange
157144
157551
  }) {
157145
- const next = value === "left" ? "center" : value === "center" ? "right" : "left";
157552
+ const current = value === "center" || value === "right" ? value : "left";
157146
157553
  return /* @__PURE__ */ jsxs(
157147
- "button",
157554
+ "div",
157148
157555
  {
157149
- onClick: () => onChange(next),
157150
- className: "p-1 bg-neutral-800 rounded hover:bg-neutral-700",
157556
+ className: "flex items-center bg-neutral-800 rounded p-0.5 gap-0.5 border border-neutral-700",
157557
+ role: "group",
157558
+ "aria-label": "Text alignment",
157151
157559
  children: [
157152
- value === "left" && /* @__PURE__ */ jsx(AlignLeftIcon$1, {}),
157153
- value === "center" && /* @__PURE__ */ jsx(AlignCenterIcon$1, {}),
157154
- value === "right" && /* @__PURE__ */ jsx(AlignRightIcon$1, {})
157560
+ /* @__PURE__ */ jsx(
157561
+ "button",
157562
+ {
157563
+ type: "button",
157564
+ title: "Align Left",
157565
+ onClick: () => onChange("left"),
157566
+ className: `p-1 rounded transition-colors ${current === "left" ? "bg-neutral-600 text-white shadow-sm" : "text-neutral-400 hover:text-white hover:bg-neutral-700"}`,
157567
+ children: /* @__PURE__ */ jsx(AlignLeftIcon$1, {})
157568
+ }
157569
+ ),
157570
+ /* @__PURE__ */ jsx(
157571
+ "button",
157572
+ {
157573
+ type: "button",
157574
+ title: "Align Center",
157575
+ onClick: () => onChange("center"),
157576
+ className: `p-1 rounded transition-colors ${current === "center" ? "bg-neutral-600 text-white shadow-sm" : "text-neutral-400 hover:text-white hover:bg-neutral-700"}`,
157577
+ children: /* @__PURE__ */ jsx(AlignCenterIcon$1, {})
157578
+ }
157579
+ ),
157580
+ /* @__PURE__ */ jsx(
157581
+ "button",
157582
+ {
157583
+ type: "button",
157584
+ title: "Align Right",
157585
+ onClick: () => onChange("right"),
157586
+ className: `p-1 rounded transition-colors ${current === "right" ? "bg-neutral-600 text-white shadow-sm" : "text-neutral-400 hover:text-white hover:bg-neutral-700"}`,
157587
+ children: /* @__PURE__ */ jsx(AlignRightIcon$1, {})
157588
+ }
157589
+ )
157155
157590
  ]
157156
157591
  }
157157
157592
  );
@@ -157204,15 +157639,29 @@ function TextToolbar() {
157204
157639
  }, [type, objects]);
157205
157640
  useEffect(() => {
157206
157641
  if (!text) return;
157207
- setFontSize(text.fontSize ?? 18);
157208
- setLineHeight2((text.lineHeight ?? 1) * 100);
157209
- setCharSpacing(text.charSpacing ?? 0);
157210
- setAlign(text.textAlign ?? "left");
157211
- const dashArray = text.strokeDashArray;
157212
- const currentStrokeStyle = Array.isArray(dashArray) && dashArray.length === 2 && dashArray[0] === 2 && dashArray[1] === 4 ? "Dotted" : Array.isArray(dashArray) && dashArray.length === 2 && dashArray[0] === 10 && dashArray[1] === 5 ? "Dashed" : "Line";
157213
- setStrokeColor(typeof text.stroke === "string" && text.stroke ? text.stroke : "#000000");
157214
- setStrokeSize(Math.max(0, Number(text.strokeWidth ?? 0)));
157215
- setStrokeStyle(currentStrokeStyle);
157642
+ const syncProps = () => {
157643
+ setFontSize(text.fontSize ?? 18);
157644
+ setLineHeight2((text.lineHeight ?? 1) * 100);
157645
+ setCharSpacing(text.charSpacing ?? 0);
157646
+ setAlign(text.textAlign ?? "left");
157647
+ const dashArray = text.strokeDashArray;
157648
+ const currentStrokeStyle = Array.isArray(dashArray) && dashArray.length === 2 && dashArray[0] === 2 && dashArray[1] === 4 ? "Dotted" : Array.isArray(dashArray) && dashArray.length === 2 && dashArray[0] === 10 && dashArray[1] === 5 ? "Dashed" : "Line";
157649
+ setStrokeColor(typeof text.stroke === "string" && text.stroke ? text.stroke : "#000000");
157650
+ setStrokeSize(Math.max(0, Number(text.strokeWidth ?? 0)));
157651
+ setStrokeStyle(currentStrokeStyle);
157652
+ };
157653
+ syncProps();
157654
+ const canvas2 = text.canvas;
157655
+ if (canvas2) {
157656
+ canvas2.on("object:modified", syncProps);
157657
+ canvas2.on("selection:updated", syncProps);
157658
+ }
157659
+ return () => {
157660
+ if (canvas2) {
157661
+ canvas2.off("object:modified", syncProps);
157662
+ canvas2.off("selection:updated", syncProps);
157663
+ }
157664
+ };
157216
157665
  }, [text]);
157217
157666
  const updateText = useCallback(
157218
157667
  (props) => {
@@ -157220,6 +157669,11 @@ function TextToolbar() {
157220
157669
  const canvas2 = text.canvas;
157221
157670
  if (!canvas2) return;
157222
157671
  text.set(props);
157672
+ if (props.textAlign) {
157673
+ text.dirty = true;
157674
+ text.initDimensions?.();
157675
+ text.setCoords();
157676
+ }
157223
157677
  canvas2.requestRenderAll();
157224
157678
  useHistoryStore.getState().saveState();
157225
157679
  },
@@ -157276,7 +157730,7 @@ function TextToolbar() {
157276
157730
  }
157277
157731
  ),
157278
157732
  /* @__PURE__ */ jsx(
157279
- TextAlignButton,
157733
+ TextAlignGroup,
157280
157734
  {
157281
157735
  value: align,
157282
157736
  onChange: (v2) => {
@@ -157532,6 +157986,7 @@ class CropService {
157532
157986
  darkOverlay = null;
157533
157987
  focusedImage = null;
157534
157988
  cropZone = null;
157989
+ maskPath = null;
157535
157990
  onEnd = null;
157536
157991
  init(canvas2) {
157537
157992
  this.canvas = canvas2;
@@ -157543,6 +157998,7 @@ class CropService {
157543
157998
  this.cleanup();
157544
157999
  this.targetImage = image;
157545
158000
  this.onEnd = onEnd || null;
158001
+ this.maskPath = image.__maskPath || image.clipPath?.__maskPath || null;
157546
158002
  const canvas2 = this.canvas;
157547
158003
  canvas2.renderOnAddRemove = false;
157548
158004
  canvas2.discardActiveObject();
@@ -157559,7 +158015,8 @@ class CropService {
157559
158015
  width: naturalWidth,
157560
158016
  height: naturalHeight,
157561
158017
  selectable: false,
157562
- evented: false
158018
+ evented: false,
158019
+ clipPath: void 0
157563
158020
  });
157564
158021
  const cropX = image.cropX || 0;
157565
158022
  const cropY = image.cropY || 0;
@@ -157690,14 +158147,31 @@ class CropService {
157690
158147
  const local = Mn.transformPoint(center, inv);
157691
158148
  const localW = zone.getScaledWidth() / Math.abs(img.scaleX);
157692
158149
  const localH = zone.getScaledHeight() / Math.abs(img.scaleY);
157693
- img.clipPath = new jr({
157694
- left: local.x,
157695
- top: local.y,
157696
- width: localW,
157697
- height: localH,
157698
- originX: "center",
157699
- originY: "center"
157700
- });
158150
+ if (this.maskPath) {
158151
+ const maskClip = new no(this.maskPath, {
158152
+ originX: "center",
158153
+ originY: "center"
158154
+ });
158155
+ const pathW = maskClip.width || 1;
158156
+ const pathH = maskClip.height || 1;
158157
+ maskClip.set({
158158
+ left: local.x,
158159
+ top: local.y,
158160
+ scaleX: localW / pathW,
158161
+ scaleY: localH / pathH,
158162
+ angle: 0
158163
+ });
158164
+ img.clipPath = maskClip;
158165
+ } else {
158166
+ img.clipPath = new jr({
158167
+ left: local.x,
158168
+ top: local.y,
158169
+ width: localW,
158170
+ height: localH,
158171
+ originX: "center",
158172
+ originY: "center"
158173
+ });
158174
+ }
157701
158175
  img.dirty = true;
157702
158176
  }
157703
158177
  /* ======================= APPLY CROP ======================= */
@@ -157722,11 +158196,13 @@ class CropService {
157722
158196
  const minY = Math.min(...ys2);
157723
158197
  const maxX = Math.max(...xs2);
157724
158198
  const maxY = Math.max(...ys2);
158199
+ const newW = Math.min(full.width, maxX - minX);
158200
+ const newH = Math.min(full.height, maxY - minY);
157725
158201
  img.set({
157726
158202
  cropX: Math.max(0, minX),
157727
158203
  cropY: Math.max(0, minY),
157728
- width: Math.min(full.width, maxX - minX),
157729
- height: Math.min(full.height, maxY - minY),
158204
+ width: newW,
158205
+ height: newH,
157730
158206
  scaleX: full.scaleX,
157731
158207
  scaleY: full.scaleY,
157732
158208
  angle: full.angle,
@@ -157738,6 +158214,35 @@ class CropService {
157738
158214
  selectable: true,
157739
158215
  evented: true
157740
158216
  });
158217
+ if (this.maskPath) {
158218
+ const newClip = new no(this.maskPath, {
158219
+ originX: "center",
158220
+ originY: "center"
158221
+ });
158222
+ const pathW = newClip.width || 1;
158223
+ const pathH = newClip.height || 1;
158224
+ newClip.set({
158225
+ scaleX: newW / pathW,
158226
+ scaleY: newH / pathH,
158227
+ left: 0,
158228
+ top: 0,
158229
+ angle: 0
158230
+ });
158231
+ newClip.__maskPath = this.maskPath;
158232
+ img.__maskPath = this.maskPath;
158233
+ img.set({ clipPath: newClip });
158234
+ } else if (img.clipPath && img.clipPath.type === "path") {
158235
+ const pW = img.clipPath.width || 1;
158236
+ const pH = img.clipPath.height || 1;
158237
+ img.clipPath.set({
158238
+ scaleX: newW / pW,
158239
+ scaleY: newH / pH,
158240
+ left: 0,
158241
+ top: 0,
158242
+ originX: "center",
158243
+ originY: "center"
158244
+ });
158245
+ }
157741
158246
  img.setPositionByOrigin(
157742
158247
  new ot(zone.left, zone.top),
157743
158248
  "center",
@@ -157746,7 +158251,9 @@ class CropService {
157746
158251
  img.setCoords();
157747
158252
  this.cleanup();
157748
158253
  this.canvas.setActiveObject(img);
158254
+ bringOverlaysToFront(this.canvas);
157749
158255
  this.canvas.requestRenderAll();
158256
+ useHistoryStore.getState().saveState();
157750
158257
  this.onEnd?.();
157751
158258
  }
157752
158259
  /* ======================== CANCEL ========================== */
@@ -157782,6 +158289,7 @@ class CropService {
157782
158289
  this.fullImage = null;
157783
158290
  this.darkOverlay = null;
157784
158291
  this.targetImage = null;
158292
+ this.maskPath = null;
157785
158293
  }
157786
158294
  }
157787
158295
  const cropService = new CropService();
@@ -157868,7 +158376,7 @@ function PictureToolbar() {
157868
158376
  const [isRemovingBg, setIsRemovingBg] = useState(false);
157869
158377
  const [bgStatus, setBgStatus] = useState(null);
157870
158378
  const activeObj = canvas2?.getActiveObject();
157871
- const hasMask = !!activeObj?.clipPath;
158379
+ const hasMask = !!activeObj?.clipPath || !!activeObj?.__maskPath;
157872
158380
  activeObj && (activeObj.type === "image" || activeObj.isType?.("image"));
157873
158381
  const isBgRemoved = !!activeObj?.__originalSrc || !!activeObj?.__isBgRemoved;
157874
158382
  if (type !== "image" || objects?.[0]?.name === "qr" || objects?.[0]?.name === "barcode")
@@ -157877,8 +158385,9 @@ function PictureToolbar() {
157877
158385
  if (!canvas2) return;
157878
158386
  const obj = canvas2.getActiveObject();
157879
158387
  if (!obj) return;
157880
- if (obj.clipPath) {
158388
+ if (obj.clipPath || obj.__maskPath) {
157881
158389
  obj.set({ clipPath: null });
158390
+ obj.__maskPath = null;
157882
158391
  obj.setCoords();
157883
158392
  canvas2.requestRenderAll();
157884
158393
  saveState();
@@ -158149,6 +158658,20 @@ function DuplicateButton() {
158149
158658
  const duplicate = async () => {
158150
158659
  const active = canvas2.getActiveObject();
158151
158660
  if (!active) return;
158661
+ const currentSide = useHistoryStore.getState().currentPage;
158662
+ const ensureTextLoaded = async (obj) => {
158663
+ if (obj.type === "textbox" || obj.type === "text" || obj.type === "i-text") {
158664
+ if (obj.fontFamily) {
158665
+ try {
158666
+ await document.fonts.load(`16px "${obj.fontFamily}"`);
158667
+ } catch {
158668
+ }
158669
+ }
158670
+ obj.initDimensions?.();
158671
+ obj.set("dirty", true);
158672
+ }
158673
+ obj.__pageSide = currentSide;
158674
+ };
158152
158675
  if (active.type?.toLowerCase() === "activeselection") {
158153
158676
  const selection = active;
158154
158677
  const originals = selection.getObjects().filter((o2) => o2.name !== "workarea");
@@ -158156,35 +158679,41 @@ function DuplicateButton() {
158156
158679
  canvas2.discardActiveObject();
158157
158680
  const clones = await Promise.all(
158158
158681
  originals.map(async (obj) => {
158159
- const cloned2 = await obj.clone();
158682
+ const cloned2 = await obj.clone(HISTORY_CUSTOM_PROPERTIES);
158160
158683
  cloned2.set({
158161
158684
  left: (obj.left ?? 0) + OFFSET$1,
158162
158685
  top: (obj.top ?? 0) + OFFSET$1,
158163
158686
  selectable: true,
158164
158687
  evented: true
158165
158688
  });
158689
+ await ensureTextLoaded(cloned2);
158166
158690
  canvas2.add(cloned2);
158167
158691
  return cloned2;
158168
158692
  })
158169
158693
  );
158170
158694
  const newSelection = clones.length > 1 ? new Qo(clones, { canvas: canvas2 }) : clones[0];
158171
158695
  canvas2.setActiveObject(newSelection);
158696
+ bringOverlaysToFront(canvas2);
158172
158697
  canvas2.requestRenderAll();
158173
158698
  saveState();
158174
158699
  return;
158175
158700
  }
158176
158701
  if (active.name === "workarea") return;
158177
- const cloned = await active.clone();
158702
+ const cloned = await active.clone(HISTORY_CUSTOM_PROPERTIES);
158178
158703
  cloned.set({
158179
158704
  left: (active.left ?? 0) + OFFSET$1,
158180
158705
  top: (active.top ?? 0) + OFFSET$1,
158181
158706
  selectable: true,
158182
158707
  evented: true
158183
158708
  });
158709
+ await ensureTextLoaded(cloned);
158710
+ cloned.setCoords();
158184
158711
  canvas2.discardActiveObject();
158185
158712
  canvas2.add(cloned);
158186
158713
  canvas2.setActiveObject(cloned);
158714
+ bringOverlaysToFront(canvas2);
158187
158715
  canvas2.requestRenderAll();
158716
+ saveState();
158188
158717
  };
158189
158718
  return /* @__PURE__ */ jsx(
158190
158719
  "button",
@@ -158737,44 +159266,50 @@ function PositionPicker() {
158737
159266
  if (!active) return [];
158738
159267
  return active.type?.toLowerCase() === "activeselection" ? active.getObjects() : [active];
158739
159268
  };
158740
- const getWorkarea2 = () => canvas2.getObjects().find((o2) => o2.name === "workarea");
158741
- const normalizeOrigin = (obj) => {
158742
- if (obj.originX === "left" && obj.originY === "top") return;
158743
- const point = obj.getPointByOrigin("left", "top");
158744
- obj.set({ originX: "left", originY: "top" });
158745
- obj.setPositionByOrigin(point, "left", "top");
158746
- };
159269
+ const getAlignmentBoundary = () => canvas2.getObjects().find((o2) => o2.name === "workarea");
158747
159270
  const applyAlign = (align) => {
158748
- const workarea = getWorkarea2();
158749
- if (!workarea) return;
158750
- const left = workarea.left ?? 0;
158751
- const top = workarea.top ?? 0;
158752
- const width = workarea.getScaledWidth();
158753
- const height = workarea.getScaledHeight();
159271
+ const boundary = getAlignmentBoundary();
159272
+ if (!boundary) return;
159273
+ const bBounds = boundary.getBoundingRect();
159274
+ const bLeft = bBounds.left;
159275
+ const bTop = bBounds.top;
159276
+ const bWidth = bBounds.width;
159277
+ const bHeight = bBounds.height;
158754
159278
  getTargets().forEach((obj) => {
158755
- normalizeOrigin(obj);
158756
- const w2 = obj.getScaledWidth();
158757
- const h2 = obj.getScaledHeight();
159279
+ const objBounds = obj.getBoundingRect();
159280
+ const pad = obj.padding || 0;
159281
+ const effLeft = objBounds.left - pad;
159282
+ const effTop = objBounds.top - pad;
159283
+ const effWidth = objBounds.width + pad * 2;
159284
+ const effHeight = objBounds.height + pad * 2;
159285
+ let dx = 0;
159286
+ let dy = 0;
158758
159287
  switch (align) {
158759
159288
  case "left":
158760
- obj.set({ left });
159289
+ dx = bLeft - effLeft;
158761
159290
  break;
158762
159291
  case "center":
158763
- obj.set({ left: left + (width - w2) / 2 });
159292
+ dx = bLeft + (bWidth - effWidth) / 2 - effLeft;
158764
159293
  break;
158765
159294
  case "right":
158766
- obj.set({ left: left + width - w2 });
159295
+ dx = bLeft + bWidth - effWidth - effLeft;
158767
159296
  break;
158768
159297
  case "top":
158769
- obj.set({ top });
159298
+ dy = bTop - effTop;
158770
159299
  break;
158771
159300
  case "middle":
158772
- obj.set({ top: top + (height - h2) / 2 });
159301
+ dy = bTop + (bHeight - effHeight) / 2 - effTop;
158773
159302
  break;
158774
159303
  case "bottom":
158775
- obj.set({ top: top + height - h2 });
159304
+ dy = bTop + bHeight - effHeight - effTop;
158776
159305
  break;
158777
159306
  }
159307
+ if (dx !== 0) {
159308
+ obj.set({ left: (obj.left ?? 0) + dx });
159309
+ }
159310
+ if (dy !== 0) {
159311
+ obj.set({ top: (obj.top ?? 0) + dy });
159312
+ }
158778
159313
  obj.setCoords();
158779
159314
  });
158780
159315
  canvas2.requestRenderAll();
@@ -158797,42 +159332,40 @@ function PositionPicker() {
158797
159332
  const objects = active.getObjects();
158798
159333
  if (objects.length && objects.length > 1) {
158799
159334
  objects.forEach(function(obj) {
158800
- const itemWidth = obj.getBoundingRect().width;
159335
+ const itemWidth = obj.getScaledWidth();
159336
+ const itemHeight = obj.getScaledHeight();
159337
+ obj.set({ originX: "center", originY: "center" });
158801
159338
  switch (align) {
158802
159339
  case "left":
158803
- obj.set({ left: -(groupWidth / 2), originX: "left" });
159340
+ obj.set({ left: -groupWidth / 2 + itemWidth / 2 });
158804
159341
  break;
158805
159342
  case "center":
158806
- obj.set({ left: 0 - itemWidth / 2, originX: "left" });
159343
+ obj.set({ left: 0 });
158807
159344
  break;
158808
159345
  case "right":
158809
- obj.set({
158810
- left: groupWidth / 2 - itemWidth / 2,
158811
- originX: "center"
158812
- });
159346
+ obj.set({ left: groupWidth / 2 - itemWidth / 2 });
158813
159347
  break;
158814
159348
  case "top":
158815
- obj.set({ originY: "top", top: -groupHeight / 2 });
158816
- break;
158817
- case "bottom":
158818
- obj.set({ originY: "bottom", top: groupHeight / 2 });
159349
+ obj.set({ top: -groupHeight / 2 + itemHeight / 2 });
158819
159350
  break;
158820
159351
  case "middle":
158821
- obj.set({
158822
- originY: "center",
158823
- top: -groupHeight / 2 + groupHeight / 2
158824
- });
159352
+ obj.set({ top: 0 });
159353
+ break;
159354
+ case "bottom":
159355
+ obj.set({ top: groupHeight / 2 - itemHeight / 2 });
158825
159356
  break;
158826
159357
  }
158827
159358
  obj.setCoords();
158828
- canvas2.requestRenderAll();
158829
- saveState();
158830
159359
  });
159360
+ active.setCoords();
159361
+ canvas2.requestRenderAll();
159362
+ saveState();
158831
159363
  }
158832
159364
  };
158833
159365
  const bringForward = () => {
158834
159366
  const objects = canvas2.getObjects();
158835
159367
  [...getTargets()].sort((a2, b2) => objects.indexOf(a2) - objects.indexOf(b2)).reverse().forEach((obj) => canvas2.bringObjectForward(obj, true));
159368
+ bringOverlaysToFront(canvas2);
158836
159369
  canvas2.requestRenderAll();
158837
159370
  saveState();
158838
159371
  };
@@ -158861,6 +159394,7 @@ function PositionPicker() {
158861
159394
  const bringToFront = () => {
158862
159395
  const objects = canvas2.getObjects();
158863
159396
  [...getTargets()].sort((a2, b2) => objects.indexOf(a2) - objects.indexOf(b2)).reverse().forEach((obj) => canvas2.bringObjectToFront(obj));
159397
+ bringOverlaysToFront(canvas2);
158864
159398
  canvas2.requestRenderAll();
158865
159399
  saveState();
158866
159400
  };
@@ -159215,6 +159749,7 @@ function GroupButton() {
159215
159749
  canvas2.add(group);
159216
159750
  }
159217
159751
  canvas2.setActiveObject(group);
159752
+ bringOverlaysToFront(canvas2);
159218
159753
  canvas2.requestRenderAll();
159219
159754
  useSelectionStore.getState().setSelection([group]);
159220
159755
  saveState();
@@ -159366,6 +159901,9 @@ function configureObjectControls(obj) {
159366
159901
  obj.__rotationControlSize = rotationSize;
159367
159902
  obj.controls.mtr = createRotateControl(rotationSize, rotationOffset);
159368
159903
  const textObject = isTextObject(obj);
159904
+ if (textObject) {
159905
+ obj.set({ padding: 0 });
159906
+ }
159369
159907
  if (!obj.controls.mb) {
159370
159908
  obj.controls.mb = new ai({
159371
159909
  x: 0,
@@ -159484,6 +160022,7 @@ function UngroupButton() {
159484
160022
  });
159485
160023
  canvas2.discardActiveObject();
159486
160024
  useSelectionStore.getState().clearSelection();
160025
+ bringOverlaysToFront(canvas2);
159487
160026
  canvas2.requestRenderAll();
159488
160027
  saveState();
159489
160028
  };
@@ -159593,12 +160132,31 @@ function Toolbar() {
159593
160132
  ) });
159594
160133
  }
159595
160134
  function applyOpacity(hex, opacity) {
159596
- const alpha = Math.max(0, Math.min(1, opacity / 100));
159597
- const r2 = parseInt(hex.slice(1, 3), 16);
159598
- const g2 = parseInt(hex.slice(3, 5), 16);
159599
- const b2 = parseInt(hex.slice(5, 7), 16);
159600
- return `rgba(${r2}, ${g2}, ${b2}, ${alpha})`;
159601
- }
160135
+ const alpha = Math.max(0, Math.min(1, (opacity ?? 100) / 100));
160136
+ if (!hex) return `rgba(0, 0, 0, ${alpha})`;
160137
+ let clean = hex.trim();
160138
+ if (clean.startsWith("#")) {
160139
+ clean = clean.slice(1);
160140
+ if (clean.length === 3) {
160141
+ clean = clean.split("").map((c2) => c2 + c2).join("");
160142
+ }
160143
+ if (clean.length >= 6) {
160144
+ const r2 = parseInt(clean.slice(0, 2), 16) || 0;
160145
+ const g2 = parseInt(clean.slice(2, 4), 16) || 0;
160146
+ const b2 = parseInt(clean.slice(4, 6), 16) || 0;
160147
+ return `rgba(${r2}, ${g2}, ${b2}, ${alpha})`;
160148
+ }
160149
+ }
160150
+ return clean;
160151
+ }
160152
+ const PENCIL_CURSOR_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
160153
+ <path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" fill="#ffffff" stroke="#ffffff" stroke-width="2.5" stroke-linejoin="round"/>
160154
+ <path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" fill="#1e293b" stroke="#0f172a" stroke-width="1.2" stroke-linejoin="round"/>
160155
+ <polygon points="2,22 5.5,20 4,18.5" fill="#f59e0b"/>
160156
+ <polygon points="2,22 3.2,21.3 2.7,20.8" fill="#000000"/>
160157
+ <path d="m15 5 4 4" stroke="#94a3b8" stroke-width="1.2" stroke-linecap="round"/>
160158
+ </svg>`;
160159
+ const PENCIL_CURSOR = `url("data:image/svg+xml;utf8,${encodeURIComponent(PENCIL_CURSOR_SVG)}") 2 22, crosshair`;
159602
160160
  function useFabricDrawing() {
159603
160161
  const { drawTool, strokeWidth, opacity, color } = useDrawStore();
159604
160162
  useEffect(() => {
@@ -159608,19 +160166,99 @@ function useFabricDrawing() {
159608
160166
  canvas2.isDrawingMode = false;
159609
160167
  canvas2.selection = true;
159610
160168
  canvas2.defaultCursor = "default";
160169
+ canvas2.freeDrawingCursor = "default";
160170
+ canvas2.setCursor("default");
160171
+ if (canvas2.upperCanvasEl) {
160172
+ canvas2.upperCanvasEl.style.cursor = "default";
160173
+ }
160174
+ canvas2._isCurrentlyDrawing = false;
159611
160175
  return;
159612
160176
  }
160177
+ if (canvas2.getActiveObject()) {
160178
+ canvas2.discardActiveObject();
160179
+ useSelectionStore.getState().clearSelection();
160180
+ canvas2.requestRenderAll();
160181
+ }
160182
+ const activeCursor = drawTool === "brush" ? PENCIL_CURSOR : "crosshair";
159613
160183
  canvas2.isDrawingMode = true;
159614
160184
  canvas2.selection = false;
159615
- canvas2.defaultCursor = "crosshair";
160185
+ canvas2.defaultCursor = activeCursor;
160186
+ canvas2.freeDrawingCursor = activeCursor;
160187
+ canvas2.setCursor(activeCursor);
160188
+ if (canvas2.upperCanvasEl) {
160189
+ canvas2.upperCanvasEl.style.cursor = activeCursor;
160190
+ }
159616
160191
  const brush = new oo(canvas2);
159617
- brush.width = strokeWidth;
159618
- brush.color = applyOpacity(color, opacity);
159619
160192
  if (drawTool === "highlighter") {
159620
160193
  brush.width = strokeWidth * 1.5;
159621
- brush.color = applyOpacity(color, 30);
160194
+ brush.color = applyOpacity(color, opacity ?? 50);
160195
+ } else {
160196
+ brush.width = strokeWidth;
160197
+ brush.color = applyOpacity(color, opacity ?? 100);
159622
160198
  }
159623
160199
  canvas2.freeDrawingBrush = brush;
160200
+ const handleMouseDownBefore = (opt) => {
160201
+ if (!canvas2.isDrawingMode) return;
160202
+ const target = opt.target || canvas2.findTarget(opt.e);
160203
+ if (!target) return;
160204
+ const name = target.name || "";
160205
+ if (name === "workarea" || name === "watermark" || name.startsWith("overlay-") || name.startsWith("guideline-")) {
160206
+ return;
160207
+ }
160208
+ if (target.isDrawingPath || name === "drawing-path") {
160209
+ return;
160210
+ }
160211
+ if (target.selectable !== false && target.evented !== false) {
160212
+ canvas2.isDrawingMode = false;
160213
+ canvas2.selection = true;
160214
+ canvas2.defaultCursor = "default";
160215
+ canvas2.freeDrawingCursor = "default";
160216
+ canvas2.setCursor("default");
160217
+ if (canvas2.upperCanvasEl) {
160218
+ canvas2.upperCanvasEl.style.cursor = "default";
160219
+ }
160220
+ canvas2._isCurrentlyDrawing = false;
160221
+ useDrawStore.getState().setDrawTool("select");
160222
+ canvas2.setActiveObject(target);
160223
+ canvas2.requestRenderAll();
160224
+ }
160225
+ };
160226
+ const handlePathCreated = (e3) => {
160227
+ if (e3?.path) {
160228
+ e3.path.name = "drawing-path";
160229
+ e3.path.isDrawingPath = true;
160230
+ }
160231
+ };
160232
+ const handleSelectionChange = () => {
160233
+ if (useDrawStore.getState().drawTool !== "select") {
160234
+ useDrawStore.getState().setDrawTool("select");
160235
+ }
160236
+ if (canvas2.isDrawingMode) {
160237
+ canvas2.isDrawingMode = false;
160238
+ canvas2.selection = true;
160239
+ canvas2.defaultCursor = "default";
160240
+ canvas2.freeDrawingCursor = "default";
160241
+ canvas2.setCursor("default");
160242
+ if (canvas2.upperCanvasEl) {
160243
+ canvas2.upperCanvasEl.style.cursor = "default";
160244
+ }
160245
+ canvas2._isCurrentlyDrawing = false;
160246
+ canvas2.requestRenderAll();
160247
+ }
160248
+ };
160249
+ canvas2.on("mouse:down:before", handleMouseDownBefore);
160250
+ canvas2.on("path:created", handlePathCreated);
160251
+ canvas2.on("selection:created", handleSelectionChange);
160252
+ canvas2.on("selection:updated", handleSelectionChange);
160253
+ return () => {
160254
+ canvas2.off("mouse:down:before", handleMouseDownBefore);
160255
+ canvas2.off("path:created", handlePathCreated);
160256
+ canvas2.off("selection:created", handleSelectionChange);
160257
+ canvas2.off("selection:updated", handleSelectionChange);
160258
+ if (canvas2.upperCanvasEl) {
160259
+ canvas2.upperCanvasEl.style.cursor = "default";
160260
+ }
160261
+ };
159624
160262
  }, [drawTool, strokeWidth, opacity, color]);
159625
160263
  }
159626
160264
  function PointerIcon() {
@@ -159651,6 +160289,89 @@ function HandIcon() {
159651
160289
  /* @__PURE__ */ jsx("path", { d: "M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15" })
159652
160290
  ] });
159653
160291
  }
160292
+ function GridIcon({
160293
+ size = 18,
160294
+ className = ""
160295
+ }) {
160296
+ return /* @__PURE__ */ jsxs(
160297
+ "svg",
160298
+ {
160299
+ width: size,
160300
+ height: size,
160301
+ viewBox: "0 0 24 24",
160302
+ fill: "none",
160303
+ stroke: "currentColor",
160304
+ strokeWidth: "2",
160305
+ strokeLinecap: "round",
160306
+ strokeLinejoin: "round",
160307
+ className,
160308
+ children: [
160309
+ /* @__PURE__ */ jsx("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
160310
+ /* @__PURE__ */ jsx("path", { d: "M3 9h18" }),
160311
+ /* @__PURE__ */ jsx("path", { d: "M3 15h18" }),
160312
+ /* @__PURE__ */ jsx("path", { d: "M9 3v18" }),
160313
+ /* @__PURE__ */ jsx("path", { d: "M15 3v18" })
160314
+ ]
160315
+ }
160316
+ );
160317
+ }
160318
+ function getGridStep(width, height) {
160319
+ const minDim = Math.min(width, height);
160320
+ if (minDim <= 200) return 10;
160321
+ if (minDim <= 400) return 20;
160322
+ if (minDim <= 800) return 25;
160323
+ return 40;
160324
+ }
160325
+ function renderGrid(canvas2) {
160326
+ if (!canvas2) return;
160327
+ canvas2.getObjects().forEach((o2) => {
160328
+ if (o2.name === "canvas-grid") {
160329
+ canvas2.remove(o2);
160330
+ }
160331
+ });
160332
+ const showGrid = useEditorStore.getState().showGrid;
160333
+ if (!showGrid) {
160334
+ canvas2.requestRenderAll();
160335
+ return;
160336
+ }
160337
+ const workarea = canvas2.getObjects().find((o2) => o2.name === "workarea");
160338
+ if (!workarea) {
160339
+ canvas2.requestRenderAll();
160340
+ return;
160341
+ }
160342
+ const waL = workarea.left ?? 0;
160343
+ const waT = workarea.top ?? 0;
160344
+ const waW = workarea.getScaledWidth();
160345
+ const waH = workarea.getScaledHeight();
160346
+ if (waW <= 0 || waH <= 0) return;
160347
+ const step = getGridStep(waW, waH);
160348
+ let pathData = "";
160349
+ for (let x2 = step; x2 < waW; x2 += step) {
160350
+ const roundX = Math.round(x2 * 100) / 100;
160351
+ pathData += `M ${roundX} 0 V ${waH} `;
160352
+ }
160353
+ for (let y2 = step; y2 < waH; y2 += step) {
160354
+ const roundY = Math.round(y2 * 100) / 100;
160355
+ pathData += `M 0 ${roundY} H ${waW} `;
160356
+ }
160357
+ if (!pathData) return;
160358
+ const gridPath = new no(pathData, {
160359
+ name: "canvas-grid",
160360
+ left: waL,
160361
+ top: waT,
160362
+ originX: "left",
160363
+ originY: "top",
160364
+ stroke: "rgba(100, 116, 139, 0.28)",
160365
+ strokeWidth: 1,
160366
+ fill: "",
160367
+ selectable: false,
160368
+ evented: false,
160369
+ excludeFromExport: true
160370
+ });
160371
+ canvas2.add(gridPath);
160372
+ bringOverlaysToFront(canvas2);
160373
+ canvas2.requestRenderAll();
160374
+ }
159654
160375
  function enablePanMode(canvas2) {
159655
160376
  let isDragging = false;
159656
160377
  let lastClientX = 0;
@@ -159912,16 +160633,186 @@ function createWorkarea(canvas2, container, productData, pageSide) {
159912
160633
  createOverlays(canvas2, workarea, currentSide);
159913
160634
  return workarea;
159914
160635
  }
160636
+ const SNAP_THRESHOLD_PX = 7;
160637
+ const GUIDELINE_COLOR = "#28a128";
160638
+ const GUIDELINE_WIDTH = 1.2;
160639
+ let activeVLine = null;
160640
+ let activeHLine = null;
160641
+ function clearAlignmentGuidelines(canvas2) {
160642
+ if (!canvas2) return;
160643
+ let changed = false;
160644
+ if (activeVLine) {
160645
+ canvas2.remove(activeVLine);
160646
+ activeVLine = null;
160647
+ changed = true;
160648
+ }
160649
+ if (activeHLine) {
160650
+ canvas2.remove(activeHLine);
160651
+ activeHLine = null;
160652
+ changed = true;
160653
+ }
160654
+ canvas2.getObjects().forEach((o2) => {
160655
+ if (o2.name === "guideline-alignment-v" || o2.name === "guideline-alignment-h") {
160656
+ canvas2.remove(o2);
160657
+ changed = true;
160658
+ }
160659
+ });
160660
+ if (changed) {
160661
+ canvas2.requestRenderAll();
160662
+ }
160663
+ }
160664
+ function handleObjectAlignmentSnap(canvas2, target) {
160665
+ if (!canvas2 || !target) return;
160666
+ const targetName = target.name || "";
160667
+ if (targetName === "workarea" || targetName === "bg-image" || targetName === "canvas-grid" || targetName.startsWith("overlay-") || targetName.startsWith("guideline-")) {
160668
+ return;
160669
+ }
160670
+ const workarea = canvas2.getObjects().find((o2) => o2.name === "workarea");
160671
+ if (!workarea) return;
160672
+ const zoom = canvas2.getZoom() || 1;
160673
+ const threshold = SNAP_THRESHOLD_PX / zoom;
160674
+ const waL = workarea.left ?? 0;
160675
+ const waT = workarea.top ?? 0;
160676
+ const waW = workarea.getScaledWidth();
160677
+ const waH = workarea.getScaledHeight();
160678
+ const waCenterX = waL + waW / 2;
160679
+ const waCenterY = waT + waH / 2;
160680
+ const targetCenter = target.getCenterPoint();
160681
+ target.getBoundingRect();
160682
+ const curLeft = target.left ?? 0;
160683
+ target.top ?? 0;
160684
+ let snappedX = false;
160685
+ let snappedY = false;
160686
+ let snapLineX = waCenterX;
160687
+ let snapLineY = waCenterY;
160688
+ const distCenterX = Math.abs(targetCenter.x - waCenterX);
160689
+ if (distCenterX <= threshold) {
160690
+ const diffX = waCenterX - targetCenter.x;
160691
+ target.set("left", curLeft + diffX);
160692
+ target.setCoords();
160693
+ snappedX = true;
160694
+ snapLineX = waCenterX;
160695
+ } else {
160696
+ const otherObjects = canvas2.getObjects().filter(
160697
+ (o2) => o2 !== target && o2.selectable !== false && o2.name !== "workarea" && o2.name !== "bg-image" && !o2.name?.startsWith("overlay-") && !o2.name?.startsWith("guideline-") && o2.name !== "canvas-grid"
160698
+ );
160699
+ for (const other of otherObjects) {
160700
+ const otherCenter = other.getCenterPoint();
160701
+ if (Math.abs(targetCenter.x - otherCenter.x) <= threshold) {
160702
+ const diffX = otherCenter.x - targetCenter.x;
160703
+ target.set("left", curLeft + diffX);
160704
+ target.setCoords();
160705
+ snappedX = true;
160706
+ snapLineX = otherCenter.x;
160707
+ break;
160708
+ }
160709
+ }
160710
+ }
160711
+ const updatedCenter = target.getCenterPoint();
160712
+ const distCenterY = Math.abs(updatedCenter.y - waCenterY);
160713
+ if (distCenterY <= threshold) {
160714
+ const diffY = waCenterY - updatedCenter.y;
160715
+ target.set("top", (target.top ?? 0) + diffY);
160716
+ target.setCoords();
160717
+ snappedY = true;
160718
+ snapLineY = waCenterY;
160719
+ } else {
160720
+ const otherObjects = canvas2.getObjects().filter(
160721
+ (o2) => o2 !== target && o2.selectable !== false && o2.name !== "workarea" && o2.name !== "bg-image" && !o2.name?.startsWith("overlay-") && !o2.name?.startsWith("guideline-") && o2.name !== "canvas-grid"
160722
+ );
160723
+ for (const other of otherObjects) {
160724
+ const otherCenter = other.getCenterPoint();
160725
+ if (Math.abs(updatedCenter.y - otherCenter.y) <= threshold) {
160726
+ const diffY = otherCenter.y - updatedCenter.y;
160727
+ target.set("top", (target.top ?? 0) + diffY);
160728
+ target.setCoords();
160729
+ snappedY = true;
160730
+ snapLineY = otherCenter.y;
160731
+ break;
160732
+ }
160733
+ }
160734
+ }
160735
+ if (snappedX) {
160736
+ const y1 = waT;
160737
+ const y2 = waT + waH;
160738
+ if (!activeVLine) {
160739
+ activeVLine = new vo([snapLineX, y1, snapLineX, y2], {
160740
+ name: "guideline-alignment-v",
160741
+ stroke: GUIDELINE_COLOR,
160742
+ strokeWidth: GUIDELINE_WIDTH / zoom,
160743
+ strokeUniform: true,
160744
+ selectable: false,
160745
+ evented: false,
160746
+ excludeFromExport: true,
160747
+ hasControls: false,
160748
+ hasBorders: false,
160749
+ lockMovementX: true,
160750
+ lockMovementY: true
160751
+ });
160752
+ canvas2.add(activeVLine);
160753
+ } else {
160754
+ activeVLine.set({
160755
+ x1: snapLineX,
160756
+ y1,
160757
+ x2: snapLineX,
160758
+ y2,
160759
+ strokeWidth: GUIDELINE_WIDTH / zoom,
160760
+ visible: true
160761
+ });
160762
+ activeVLine.setCoords();
160763
+ }
160764
+ canvas2.bringObjectToFront(activeVLine);
160765
+ } else if (activeVLine) {
160766
+ canvas2.remove(activeVLine);
160767
+ activeVLine = null;
160768
+ }
160769
+ if (snappedY) {
160770
+ const x1 = waL;
160771
+ const x2 = waL + waW;
160772
+ if (!activeHLine) {
160773
+ activeHLine = new vo([x1, snapLineY, x2, snapLineY], {
160774
+ name: "guideline-alignment-h",
160775
+ stroke: GUIDELINE_COLOR,
160776
+ strokeWidth: GUIDELINE_WIDTH / zoom,
160777
+ strokeUniform: true,
160778
+ selectable: false,
160779
+ evented: false,
160780
+ excludeFromExport: true,
160781
+ hasControls: false,
160782
+ hasBorders: false,
160783
+ lockMovementX: true,
160784
+ lockMovementY: true
160785
+ });
160786
+ canvas2.add(activeHLine);
160787
+ } else {
160788
+ activeHLine.set({
160789
+ x1,
160790
+ y1: snapLineY,
160791
+ x2,
160792
+ y2: snapLineY,
160793
+ strokeWidth: GUIDELINE_WIDTH / zoom,
160794
+ visible: true
160795
+ });
160796
+ activeHLine.setCoords();
160797
+ }
160798
+ canvas2.bringObjectToFront(activeHLine);
160799
+ } else if (activeHLine) {
160800
+ canvas2.remove(activeHLine);
160801
+ activeHLine = null;
160802
+ }
160803
+ canvas2.requestRenderAll();
160804
+ }
159915
160805
  function setupHistoryRecording(canvas2) {
159916
160806
  let modifiedTimer = null;
159917
160807
  const handleObjectAddedOrRemoved = (e3) => {
159918
160808
  const target = e3?.target;
159919
160809
  if (!target) return;
159920
160810
  const name = target.name || "";
159921
- if (name === "workarea" || name === "watermark" || name.startsWith("overlay-") || name.startsWith("guideline-")) {
160811
+ if (name === "workarea" || name === "watermark" || name === "bg-image" || name.startsWith("overlay-") || name.startsWith("guideline-") || name === "canvas-grid") {
159922
160812
  return;
159923
160813
  }
159924
160814
  if (useHistoryStore.getState().isRestoring) return;
160815
+ flushBackgroundSave();
159925
160816
  if (modifiedTimer) {
159926
160817
  clearTimeout(modifiedTimer);
159927
160818
  modifiedTimer = null;
@@ -159936,7 +160827,7 @@ function setupHistoryRecording(canvas2) {
159936
160827
  const target = e3?.target;
159937
160828
  if (!target) return;
159938
160829
  const name = target.name || "";
159939
- if (name === "workarea" || name === "watermark" || name.startsWith("overlay-") || name.startsWith("guideline-")) {
160830
+ if (name === "workarea" || name === "watermark" || name.startsWith("overlay-") || name.startsWith("guideline-") || name === "canvas-grid") {
159940
160831
  return;
159941
160832
  }
159942
160833
  if (useHistoryStore.getState().isRestoring) return;
@@ -159959,6 +160850,12 @@ function setupHistoryRecording(canvas2) {
159959
160850
  }
159960
160851
  function setupDoublClickCrop(canvas2) {
159961
160852
  const handleDoubleClick = (e3) => {
160853
+ const isCropping = useEditorStore.getState().isCropping;
160854
+ if (isCropping) {
160855
+ applyCrop(canvas2);
160856
+ useEditorStore.getState().stopCropping();
160857
+ return;
160858
+ }
159962
160859
  const target = e3.target;
159963
160860
  if (target && target.type === "image" && target.name !== "bg-image") {
159964
160861
  useEditorStore.getState().startCropping();
@@ -160004,6 +160901,13 @@ function setupSelectionHandlers(canvas2, setSelection, clearSelection) {
160004
160901
  canvas2.requestRenderAll();
160005
160902
  };
160006
160903
  canvas2.on("selection:created", (e3) => {
160904
+ if (useDrawStore.getState().drawTool !== "select") {
160905
+ useDrawStore.getState().setDrawTool("select");
160906
+ canvas2.isDrawingMode = false;
160907
+ canvas2.selection = true;
160908
+ canvas2.defaultCursor = "default";
160909
+ canvas2._isCurrentlyDrawing = false;
160910
+ }
160007
160911
  if (e3.selected) {
160008
160912
  e3.selected.forEach((o2) => {
160009
160913
  o2.__showDegree = false;
@@ -160011,8 +160915,16 @@ function setupSelectionHandlers(canvas2, setSelection, clearSelection) {
160011
160915
  }
160012
160916
  updateSelection();
160013
160917
  configureSelectionControls();
160918
+ bringOverlaysToFront(canvas2);
160014
160919
  });
160015
160920
  canvas2.on("selection:updated", (e3) => {
160921
+ if (useDrawStore.getState().drawTool !== "select") {
160922
+ useDrawStore.getState().setDrawTool("select");
160923
+ canvas2.isDrawingMode = false;
160924
+ canvas2.selection = true;
160925
+ canvas2.defaultCursor = "default";
160926
+ canvas2._isCurrentlyDrawing = false;
160927
+ }
160016
160928
  const active = canvas2.getActiveObject();
160017
160929
  const bgImg = canvas2.getObjects().find((o2) => o2.name === "bg-image");
160018
160930
  if (bgImg && active !== bgImg) {
@@ -160033,6 +160945,7 @@ function setupSelectionHandlers(canvas2, setSelection, clearSelection) {
160033
160945
  }
160034
160946
  updateSelection();
160035
160947
  configureSelectionControls();
160948
+ bringOverlaysToFront(canvas2);
160036
160949
  });
160037
160950
  canvas2.on("object:scaling", configureSelectionControls);
160038
160951
  canvas2.on("object:rotating", (e3) => {
@@ -160049,10 +160962,23 @@ function setupSelectionHandlers(canvas2, setSelection, clearSelection) {
160049
160962
  o2.__showDegree = false;
160050
160963
  });
160051
160964
  configureSelectionControls();
160965
+ bringOverlaysToFront(canvas2);
160052
160966
  };
160053
- canvas2.on("object:modified", hideDegree);
160054
- canvas2.on("mouse:up", hideDegree);
160967
+ const handleObjectMoving = (e3) => {
160968
+ if (e3.target) {
160969
+ handleObjectAlignmentSnap(canvas2, e3.target);
160970
+ }
160971
+ bringOverlaysToFront(canvas2);
160972
+ };
160973
+ const handleMovementEnd = (e3) => {
160974
+ clearAlignmentGuidelines(canvas2);
160975
+ hideDegree(e3);
160976
+ };
160977
+ canvas2.on("object:moving", handleObjectMoving);
160978
+ canvas2.on("object:modified", handleMovementEnd);
160979
+ canvas2.on("mouse:up", handleMovementEnd);
160055
160980
  canvas2.on("selection:cleared", () => {
160981
+ clearAlignmentGuidelines(canvas2);
160056
160982
  const bgImg = canvas2.getObjects().find((o2) => o2.name === "bg-image");
160057
160983
  if (bgImg) {
160058
160984
  bgImg.set({
@@ -160066,22 +160992,27 @@ function setupSelectionHandlers(canvas2, setSelection, clearSelection) {
160066
160992
  }
160067
160993
  }
160068
160994
  hideDegree();
160995
+ bringOverlaysToFront(canvas2);
160069
160996
  clearSelection();
160070
160997
  });
160071
160998
  return () => {
160999
+ clearAlignmentGuidelines(canvas2);
160072
161000
  canvas2.off("selection:created");
160073
161001
  canvas2.off("selection:updated");
160074
161002
  canvas2.off("object:scaling", configureSelectionControls);
160075
161003
  canvas2.off("object:rotating");
160076
- canvas2.off("object:modified", hideDegree);
160077
- canvas2.off("mouse:up", hideDegree);
161004
+ canvas2.off("object:moving", handleObjectMoving);
161005
+ canvas2.off("object:modified", handleMovementEnd);
161006
+ canvas2.off("mouse:up", handleMovementEnd);
160078
161007
  canvas2.off("selection:cleared");
160079
161008
  };
160080
161009
  }
160081
161010
  function setupObjectAddedHandler(canvas2) {
160082
161011
  const handleObjectAdded = (e3) => {
160083
161012
  const obj = e3.target;
160084
- if (!obj || obj.name === "workarea") return;
161013
+ if (!obj || obj.name === "workarea" || obj.name?.startsWith("overlay-") || obj.name?.startsWith("guideline-")) {
161014
+ return;
161015
+ }
160085
161016
  configureObjectControls(obj);
160086
161017
  bringOverlaysToFront(canvas2);
160087
161018
  };
@@ -160129,6 +161060,9 @@ const CUSTOM_PROPERTIES = [
160129
161060
  "lockMovementY",
160130
161061
  "rx",
160131
161062
  "ry",
161063
+ "src",
161064
+ "crossOrigin",
161065
+ "__maskPath",
160132
161066
  "__radiusOfCanvas",
160133
161067
  "__shapeType",
160134
161068
  "__pageSide",
@@ -160139,10 +161073,28 @@ const CUSTOM_PROPERTIES = [
160139
161073
  "__initialZoom",
160140
161074
  "__borderRect",
160141
161075
  "__rotationControlSize",
160142
- "__showDegree"
161076
+ "__showDegree",
161077
+ "textAlign",
161078
+ "fontFamily",
161079
+ "fontSize",
161080
+ "fontWeight",
161081
+ "fontStyle",
161082
+ "underline",
161083
+ "linethrough",
161084
+ "overline",
161085
+ "fill",
161086
+ "stroke",
161087
+ "strokeWidth",
161088
+ "lineHeight",
161089
+ "charSpacing",
161090
+ "styles",
161091
+ "paintFirst",
161092
+ "strokeUniform",
161093
+ "objectCaching"
160143
161094
  ];
160144
161095
  async function switchCanvasPage(canvas2, currentPage, targetPage, pageJSON, clearSelection, containerRef) {
160145
161096
  if (targetPage === currentPage) return;
161097
+ clearAlignmentGuidelines(canvas2);
160146
161098
  const historyStore = useHistoryStore.getState();
160147
161099
  useLoaderStore.getState().startLoading();
160148
161100
  try {
@@ -160575,41 +161527,75 @@ function ZoomControls({ onReset }) {
160575
161527
  ] }) });
160576
161528
  }
160577
161529
  const OFFSET = 20;
160578
- function useKeyboardEvents(canvas2) {
160579
- const clipboardRef = useRef(null);
161530
+ let globalClipboard = null;
161531
+ let clipboardSourcePage = null;
161532
+ function useKeyboardEvents(canvasParam) {
161533
+ const storeCanvas = useEditorStore((s2) => s2.canvas);
161534
+ const historyCanvas = useHistoryStore((s2) => s2.canvas);
161535
+ const canvas2 = canvasParam || storeCanvas || historyCanvas;
160580
161536
  const saveState = useHistoryStore((s2) => s2.saveState);
160581
161537
  useEffect(() => {
160582
- if (!canvas2) return;
161538
+ const getActiveCanvas = () => canvas2 || useEditorStore.getState().canvas || useHistoryStore.getState().canvas;
160583
161539
  const handleCopy = async () => {
160584
- const activeObject = canvas2.getActiveObject();
161540
+ const activeCanvas = getActiveCanvas();
161541
+ if (!activeCanvas) return;
161542
+ const activeObject = activeCanvas.getActiveObject();
160585
161543
  if (!activeObject || activeObject.name === "workarea") return;
160586
- const cloned = await activeObject.clone();
160587
- clipboardRef.current = cloned;
161544
+ const cloned = await activeObject.clone(HISTORY_CUSTOM_PROPERTIES);
161545
+ globalClipboard = cloned;
161546
+ clipboardSourcePage = useHistoryStore.getState().currentPage;
160588
161547
  };
160589
161548
  const handlePaste = async () => {
160590
- if (!clipboardRef.current) return;
160591
- const clonedObj = await clipboardRef.current.clone();
160592
- canvas2.discardActiveObject();
161549
+ const activeCanvas = getActiveCanvas();
161550
+ if (!activeCanvas || !globalClipboard) return;
161551
+ const currentSide = useHistoryStore.getState().currentPage;
161552
+ const isCrossPagePaste = clipboardSourcePage && clipboardSourcePage !== currentSide;
161553
+ const clonedObj = await globalClipboard.clone(HISTORY_CUSTOM_PROPERTIES);
161554
+ activeCanvas.discardActiveObject();
161555
+ const offset = isCrossPagePaste ? 0 : OFFSET;
160593
161556
  clonedObj.set({
160594
- left: (clonedObj.left ?? 0) + OFFSET,
160595
- top: (clonedObj.top ?? 0) + OFFSET,
160596
- evented: true
161557
+ left: (clonedObj.left ?? 0) + offset,
161558
+ top: (clonedObj.top ?? 0) + offset,
161559
+ evented: true,
161560
+ selectable: true
160597
161561
  });
160598
- clipboardRef.current.set({
160599
- left: (clipboardRef.current.left ?? 0) + OFFSET,
160600
- top: (clipboardRef.current.top ?? 0) + OFFSET
161562
+ clonedObj.__pageSide = currentSide;
161563
+ globalClipboard.set({
161564
+ left: (globalClipboard.left ?? 0) + OFFSET,
161565
+ top: (globalClipboard.top ?? 0) + OFFSET
160601
161566
  });
161567
+ clipboardSourcePage = currentSide;
161568
+ const ensureTextLoaded = async (obj) => {
161569
+ if (obj.type === "textbox" || obj.type === "text" || obj.type === "i-text") {
161570
+ if (obj.fontFamily) {
161571
+ try {
161572
+ await document.fonts.load(`16px "${obj.fontFamily}"`);
161573
+ } catch {
161574
+ }
161575
+ }
161576
+ obj.initDimensions?.();
161577
+ obj.set("dirty", true);
161578
+ }
161579
+ };
160602
161580
  if (clonedObj.type?.toLowerCase() === "activeselection") {
160603
- clonedObj.canvas = canvas2;
160604
- clonedObj.forEachObject((obj) => {
160605
- canvas2.add(obj);
160606
- });
160607
- clonedObj.setCoords();
161581
+ const selection = clonedObj;
161582
+ selection.canvas = activeCanvas;
161583
+ await Promise.all(
161584
+ selection.getObjects().map(async (obj) => {
161585
+ await ensureTextLoaded(obj);
161586
+ obj.__pageSide = currentSide;
161587
+ activeCanvas.add(obj);
161588
+ })
161589
+ );
161590
+ selection.setCoords();
160608
161591
  } else {
160609
- canvas2.add(clonedObj);
161592
+ await ensureTextLoaded(clonedObj);
161593
+ clonedObj.setCoords();
161594
+ activeCanvas.add(clonedObj);
160610
161595
  }
160611
- canvas2.setActiveObject(clonedObj);
160612
- canvas2.requestRenderAll();
161596
+ activeCanvas.setActiveObject(clonedObj);
161597
+ bringOverlaysToFront(activeCanvas);
161598
+ activeCanvas.requestRenderAll();
160613
161599
  saveState();
160614
161600
  };
160615
161601
  const handleKeyDown = (e3) => {
@@ -160617,25 +161603,53 @@ function useKeyboardEvents(canvas2) {
160617
161603
  const isInput = activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement || activeElement?.isContentEditable;
160618
161604
  if (isInput) return;
160619
161605
  const isModifier = e3.ctrlKey || e3.metaKey;
160620
- if (isModifier && e3.key.toLowerCase() === "c") {
161606
+ if (!isModifier) return;
161607
+ const isZ = e3.key?.toLowerCase() === "z" || e3.code === "KeyZ";
161608
+ const isY = e3.key?.toLowerCase() === "y" || e3.code === "KeyY";
161609
+ const isC = e3.key?.toLowerCase() === "c" || e3.code === "KeyC";
161610
+ const isV = e3.key?.toLowerCase() === "v" || e3.code === "KeyV";
161611
+ const isA = e3.key?.toLowerCase() === "a" || e3.code === "KeyA";
161612
+ if (isZ && !e3.shiftKey) {
161613
+ e3.preventDefault();
161614
+ useHistoryStore.getState().undo();
161615
+ return;
161616
+ }
161617
+ if (isZ && e3.shiftKey || isY) {
161618
+ e3.preventDefault();
161619
+ useHistoryStore.getState().redoAction();
161620
+ return;
161621
+ }
161622
+ if (isC) {
160621
161623
  e3.preventDefault();
160622
161624
  handleCopy();
161625
+ return;
160623
161626
  }
160624
- if (isModifier && e3.key.toLowerCase() === "v") {
161627
+ if (isV) {
160625
161628
  e3.preventDefault();
160626
161629
  handlePaste();
161630
+ return;
160627
161631
  }
160628
- if (isModifier && e3.key.toLowerCase() === "z") {
160629
- e3.preventDefault();
160630
- if (e3.shiftKey) {
160631
- useHistoryStore.getState().redoAction();
160632
- } else {
160633
- useHistoryStore.getState().undo();
161632
+ if (isA) {
161633
+ const activeCanvas = getActiveCanvas();
161634
+ if (activeCanvas) {
161635
+ e3.preventDefault();
161636
+ const selectableObjects = activeCanvas.getObjects().filter(
161637
+ (obj) => obj.name !== "workarea" && !obj.name?.startsWith("overlay-") && !obj.name?.startsWith("guideline-") && obj.selectable !== false
161638
+ );
161639
+ if (selectableObjects.length > 0) {
161640
+ activeCanvas.discardActiveObject();
161641
+ const sel = new Qo(selectableObjects, { canvas: activeCanvas });
161642
+ activeCanvas.setActiveObject(sel);
161643
+ activeCanvas.requestRenderAll();
161644
+ }
160634
161645
  }
161646
+ return;
160635
161647
  }
160636
- if (isModifier && e3.key.toLowerCase() === "y") {
161648
+ const isQuote = e3.key === "'" || e3.code === "Quote";
161649
+ if (isQuote) {
160637
161650
  e3.preventDefault();
160638
- useHistoryStore.getState().redoAction();
161651
+ useEditorStore.getState().toggleGrid();
161652
+ return;
160639
161653
  }
160640
161654
  };
160641
161655
  window.addEventListener("keydown", handleKeyDown);
@@ -160651,17 +161665,23 @@ function Workspace() {
160651
161665
  const workareaRef = useRef(null);
160652
161666
  const panHandlersRef = useRef(null);
160653
161667
  const isCropping = useEditorStore((s2) => s2.isCropping);
161668
+ const canvas2 = useEditorStore((s2) => s2.canvas);
160654
161669
  const setCanvas = useEditorStore((s2) => s2.setCanvas);
161670
+ const showGrid = useEditorStore((s2) => s2.showGrid);
161671
+ const toggleGrid = useEditorStore((s2) => s2.toggleGrid);
160655
161672
  const setSelection = useSelectionStore((s2) => s2.setSelection);
160656
161673
  const clearSelection = useSelectionStore((s2) => s2.clearSelection);
161674
+ const drawTool = useDrawStore((s2) => s2.drawTool);
161675
+ const setDrawTool = useDrawStore((s2) => s2.setDrawTool);
160657
161676
  const productData = useProductStore((s2) => s2.productData);
160658
- const isTwoSided = productData?.optionsMap?.printing === "4/4";
160659
- const [currentPage, setCurrentPage] = useState("front");
160660
- const [isPanning, setIsPanning] = useState(false);
161677
+ const editorPages = useEditorStore((s2) => s2.pages);
160661
161678
  const pageJSON = useRef({
160662
161679
  front: null,
160663
161680
  back: null
160664
161681
  });
161682
+ const isTwoSided = productData?.optionsMap?.printing === "4/4" || !!pageJSON.current?.back || !!editorPages?.back;
161683
+ const [currentPage, setCurrentPage] = useState("front");
161684
+ const [isPanning, setIsPanning] = useState(false);
160665
161685
  useEffect(() => {
160666
161686
  if (!canvasRef.current || !containerRef.current) return;
160667
161687
  if (fabricRef.current) return;
@@ -160703,20 +161723,20 @@ function Workspace() {
160703
161723
  }
160704
161724
  if (!isMounted || !canvasRef.current || !containerRef.current || fabricRef.current) return;
160705
161725
  initializeRotateImage();
160706
- const canvas2 = initializeCanvas(canvasRef.current, containerRef.current);
160707
- fabricRef.current = canvas2;
160708
- setCanvas(canvas2);
160709
- useHistoryStore.getState().setCanvas(canvas2);
160710
- const workarea = createWorkarea(canvas2, containerRef.current, product);
161726
+ const canvas22 = initializeCanvas(canvasRef.current, containerRef.current);
161727
+ fabricRef.current = canvas22;
161728
+ setCanvas(canvas22);
161729
+ useHistoryStore.getState().setCanvas(canvas22);
161730
+ const workarea = createWorkarea(canvas22, containerRef.current, product);
160711
161731
  workareaRef.current = workarea;
160712
- const cleanupHistory = setupHistoryRecording(canvas2);
160713
- const cleanupCrop = setupDoublClickCrop(canvas2);
161732
+ const cleanupHistory = setupHistoryRecording(canvas22);
161733
+ const cleanupCrop = setupDoublClickCrop(canvas22);
160714
161734
  const cleanupSelection = setupSelectionHandlers(
160715
- canvas2,
161735
+ canvas22,
160716
161736
  setSelection,
160717
161737
  clearSelection
160718
161738
  );
160719
- const cleanupObjectAdded = setupObjectAddedHandler(canvas2);
161739
+ const cleanupObjectAdded = setupObjectAddedHandler(canvas22);
160720
161740
  if (isTextDesign && templateId) {
160721
161741
  const item = await ShapeService.getTextTemplateById(templateId);
160722
161742
  if (item?.jsonData) {
@@ -160724,7 +161744,7 @@ function Workspace() {
160724
161744
  pageJSON.current.front = parsed;
160725
161745
  await loadTemplateFontsIntoHead(null, parsed);
160726
161746
  useHistoryStore.setState({ isRestoring: true });
160727
- await loadPageToCanvas(canvas2, parsed);
161747
+ await loadPageToCanvas(canvas22, parsed);
160728
161748
  useHistoryStore.setState({ isRestoring: false });
160729
161749
  useHistoryStore.getState().saveState();
160730
161750
  }
@@ -160745,7 +161765,7 @@ function Workspace() {
160745
161765
  if (frontData) {
160746
161766
  pageJSON.current.front = frontData;
160747
161767
  useHistoryStore.setState({ isRestoring: true });
160748
- await loadPageToCanvas(canvas2, frontData);
161768
+ await loadPageToCanvas(canvas22, frontData);
160749
161769
  useHistoryStore.setState({ isRestoring: false });
160750
161770
  useHistoryStore.getState().saveState();
160751
161771
  }
@@ -160762,10 +161782,10 @@ function Workspace() {
160762
161782
  cleanupCrop();
160763
161783
  cleanupSelection();
160764
161784
  cleanupObjectAdded();
160765
- if (canvas2.__cleanupResizeObserver) {
160766
- canvas2.__cleanupResizeObserver();
161785
+ if (canvas22.__cleanupResizeObserver) {
161786
+ canvas22.__cleanupResizeObserver();
160767
161787
  }
160768
- canvas2.dispose();
161788
+ canvas22.dispose();
160769
161789
  fabricRef.current = null;
160770
161790
  setCanvas(null);
160771
161791
  clearSelection();
@@ -160803,23 +161823,30 @@ function Workspace() {
160803
161823
  return setupBorderSync(fabricRef.current);
160804
161824
  }, [fabricRef.current]);
160805
161825
  useEffect(() => {
160806
- const canvas2 = fabricRef.current;
160807
- if (!canvas2) return;
160808
- removePanHandlers(canvas2, panHandlersRef.current);
161826
+ const canvas22 = fabricRef.current;
161827
+ if (!canvas22) return;
161828
+ removePanHandlers(canvas22, panHandlersRef.current);
160809
161829
  panHandlersRef.current = null;
160810
161830
  if (isPanning) {
160811
- panHandlersRef.current = enablePanMode(canvas2);
161831
+ panHandlersRef.current = enablePanMode(canvas22);
160812
161832
  } else {
160813
- disablePanMode(canvas2);
161833
+ disablePanMode(canvas22);
160814
161834
  }
160815
161835
  return () => {
160816
- removePanHandlers(canvas2, panHandlersRef.current);
161836
+ removePanHandlers(canvas22, panHandlersRef.current);
160817
161837
  };
160818
161838
  }, [isPanning]);
160819
161839
  useFabricDrawing();
160820
- useKeyboardEvents(fabricRef.current);
161840
+ useKeyboardEvents(canvas2 || fabricRef.current);
161841
+ useEffect(() => {
161842
+ const activeCanvas = canvas2 || fabricRef.current;
161843
+ if (activeCanvas) {
161844
+ renderGrid(activeCanvas);
161845
+ }
161846
+ }, [showGrid, canvas2, currentPage]);
160821
161847
  const switchPage = async (page) => {
160822
161848
  if (!fabricRef.current) return;
161849
+ setDrawTool("select");
160823
161850
  await switchCanvasPage(
160824
161851
  fabricRef.current,
160825
161852
  currentPage,
@@ -160830,10 +161857,50 @@ function Workspace() {
160830
161857
  );
160831
161858
  useEditorStore.getState().setPages({ ...pageJSON.current });
160832
161859
  setCurrentPage(page);
161860
+ renderGrid(fabricRef.current);
161861
+ };
161862
+ const copyFrontToBack = async () => {
161863
+ if (!fabricRef.current) return;
161864
+ useLoaderStore.getState().startLoading();
161865
+ try {
161866
+ if (currentPage === "front") {
161867
+ pageJSON.current.front = fabricRef.current.toObject(CUSTOM_PROPERTIES);
161868
+ }
161869
+ let frontData = pageJSON.current.front || useEditorStore.getState().pages?.front;
161870
+ if (!frontData) {
161871
+ console.warn("No front design found to copy");
161872
+ return;
161873
+ }
161874
+ const clonedFrontJson = JSON.parse(JSON.stringify(frontData));
161875
+ await loadTemplateFontsIntoHead(null, clonedFrontJson);
161876
+ pageJSON.current.back = clonedFrontJson;
161877
+ useEditorStore.getState().setPages({
161878
+ ...useEditorStore.getState().pages,
161879
+ ...pageJSON.current,
161880
+ back: clonedFrontJson
161881
+ });
161882
+ clearSelection();
161883
+ useHistoryStore.getState().setPage("back");
161884
+ setCurrentPage("back");
161885
+ await loadPageToCanvas(fabricRef.current, clonedFrontJson, "back");
161886
+ const workarea = fabricRef.current.getObjects().find((obj) => obj.name === "workarea");
161887
+ if (workarea) {
161888
+ workarea.__pageSide = "back";
161889
+ createOverlays(fabricRef.current, workarea, "back");
161890
+ bringOverlaysToFront(fabricRef.current);
161891
+ }
161892
+ fabricRef.current.requestRenderAll();
161893
+ renderGrid(fabricRef.current);
161894
+ useHistoryStore.getState().saveState();
161895
+ } catch (err) {
161896
+ console.error("Failed to copy front design to back:", err);
161897
+ } finally {
161898
+ useLoaderStore.getState().stopLoading();
161899
+ }
160833
161900
  };
160834
161901
  return /* @__PURE__ */ jsxs(Fragment, { children: [
160835
161902
  /* @__PURE__ */ jsxs("div", { className: "absolute bottom-6 z-50 flex items-center bg-white/80 backdrop-blur-xl rounded-2xl shadow-[0_8px_32px_rgba(0,0,0,0.12)] px-1.5 py-1.5 border border-white/40 ring-1 ring-black/5", children: [
160836
- /* @__PURE__ */ jsxs("div", { className: "flex bg-gray-200/50 p-1 rounded-xl", children: [
161903
+ /* @__PURE__ */ jsxs("div", { className: "flex bg-gray-200/50 p-1 rounded-xl items-center gap-1", children: [
160837
161904
  /* @__PURE__ */ jsx(
160838
161905
  "button",
160839
161906
  {
@@ -160849,6 +161916,37 @@ function Workspace() {
160849
161916
  className: `px-4 py-2 rounded-lg text-[11px] font-bold tracking-wider transition-all duration-200 cursor-pointer ${currentPage === "back" ? "bg-white text-[#02bc71] shadow-sm" : "text-gray-500 hover:text-gray-700"}`,
160850
161917
  children: "BACK"
160851
161918
  }
161919
+ ),
161920
+ isTwoSided && /* @__PURE__ */ jsxs(
161921
+ "button",
161922
+ {
161923
+ id: "copy-front-to-back-btn",
161924
+ type: "button",
161925
+ onClick: copyFrontToBack,
161926
+ title: "Copy front design to back design",
161927
+ className: "flex items-center gap-1.5 px-3 py-1.5 ml-1 rounded-lg text-[10px] font-bold tracking-wider bg-white/90 hover:bg-white text-gray-700 hover:text-[#02bc71] border border-gray-300/60 shadow-xs transition-all duration-150 cursor-pointer active:scale-95",
161928
+ children: [
161929
+ /* @__PURE__ */ jsxs(
161930
+ "svg",
161931
+ {
161932
+ width: "13",
161933
+ height: "13",
161934
+ viewBox: "0 0 24 24",
161935
+ fill: "none",
161936
+ stroke: "currentColor",
161937
+ strokeWidth: "2.2",
161938
+ strokeLinecap: "round",
161939
+ strokeLinejoin: "round",
161940
+ className: "text-[#02bc71]",
161941
+ children: [
161942
+ /* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
161943
+ /* @__PURE__ */ jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
161944
+ ]
161945
+ }
161946
+ ),
161947
+ /* @__PURE__ */ jsx("span", { children: "COPY FRONT TO BACK" })
161948
+ ]
161949
+ }
160852
161950
  )
160853
161951
  ] }),
160854
161952
  /* @__PURE__ */ jsx("div", { className: "w-[1px] h-6 bg-gray-300/50 mx-3" }),
@@ -160856,20 +161954,35 @@ function Workspace() {
160856
161954
  /* @__PURE__ */ jsx(
160857
161955
  "button",
160858
161956
  {
160859
- onClick: () => setIsPanning(false),
161957
+ onClick: () => {
161958
+ setIsPanning(false);
161959
+ setDrawTool("select");
161960
+ },
160860
161961
  title: "Selection Mode",
160861
- className: `p-2 rounded-xl transition-all duration-200 ${!isPanning ? "bg-[#02bc71] text-white shadow-lg shadow-blue-200" : "text-gray-400 hover:bg-gray-100 hover:text-gray-600"}`,
161962
+ className: `p-2 rounded-xl transition-all duration-200 cursor-pointer ${!isPanning && drawTool === "select" ? "bg-[#02bc71] text-white shadow-lg shadow-green-200" : "text-gray-400 hover:bg-gray-100 hover:text-gray-600"}`,
160862
161963
  children: /* @__PURE__ */ jsx(PointerIcon, {})
160863
161964
  }
160864
161965
  ),
160865
161966
  /* @__PURE__ */ jsx(
160866
161967
  "button",
160867
161968
  {
160868
- onClick: () => setIsPanning(true),
161969
+ onClick: () => {
161970
+ setIsPanning(true);
161971
+ setDrawTool("select");
161972
+ },
160869
161973
  title: "Hand Mode",
160870
- className: `p-2 rounded-xl transition-all duration-200 ${isPanning ? "bg-[#02bc71] text-white shadow-lg shadow-blue-200" : "text-gray-400 hover:bg-gray-100 hover:text-gray-600"}`,
161974
+ className: `p-2 rounded-xl transition-all duration-200 cursor-pointer ${isPanning ? "bg-[#02bc71] text-white shadow-lg shadow-green-200" : "text-gray-400 hover:bg-gray-100 hover:text-gray-600"}`,
160871
161975
  children: /* @__PURE__ */ jsx(HandIcon, {})
160872
161976
  }
161977
+ ),
161978
+ /* @__PURE__ */ jsx(
161979
+ "button",
161980
+ {
161981
+ onClick: () => toggleGrid(),
161982
+ title: showGrid ? "Hide Grid (Ctrl+')" : "Show Grid (Ctrl+')",
161983
+ className: `p-2 rounded-xl transition-all duration-200 cursor-pointer ${showGrid ? "bg-[#02bc71] text-white shadow-lg shadow-green-200" : "text-gray-400 hover:bg-gray-100 hover:text-gray-600"}`,
161984
+ children: /* @__PURE__ */ jsx(GridIcon, {})
161985
+ }
160873
161986
  )
160874
161987
  ] }),
160875
161988
  /* @__PURE__ */ jsx("div", { className: "w-[1px] h-6 bg-gray-300/50 mx-3" }),