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