vibrant-tool 0.1.10 → 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.
@@ -9017,6 +9017,248 @@ const useProductStore = create((set) => ({
9017
9017
  }
9018
9018
  }
9019
9019
  }));
9020
+ const FontService = {
9021
+ async getAll(data) {
9022
+ const res = await apiFetch(`/api/fonts?page=${data.page}&limit=${data.limit}&status=true`, {
9023
+ method: "GET"
9024
+ });
9025
+ return res;
9026
+ }
9027
+ };
9028
+ let cachedFontsPromise = null;
9029
+ async function fetchAllFontsCached() {
9030
+ if (!cachedFontsPromise) {
9031
+ cachedFontsPromise = FontService.getAll({ page: 1, limit: 1e3 }).then((res) => res?.data || []).catch((err) => {
9032
+ console.warn("Failed to fetch all fonts:", err);
9033
+ cachedFontsPromise = null;
9034
+ return [];
9035
+ });
9036
+ }
9037
+ return cachedFontsPromise;
9038
+ }
9039
+ async function injectFontFace(fontName, fontUrl) {
9040
+ if (!fontName || !fontUrl || typeof document === "undefined") return;
9041
+ const cleanedName = fontName.trim().replace(/^['"]+|['"]+$/g, "");
9042
+ const id = `font-${cleanedName.replace(/\s+/g, "-").toLowerCase()}`;
9043
+ if (!document.getElementById(id)) {
9044
+ const style = document.createElement("style");
9045
+ style.id = id;
9046
+ style.innerHTML = `
9047
+ @font-face {
9048
+ font-family: '${cleanedName}';
9049
+ src: url('${fontUrl}');
9050
+ font-display: swap;
9051
+ }
9052
+ `;
9053
+ document.head.appendChild(style);
9054
+ }
9055
+ try {
9056
+ await document.fonts.load(`16px "${cleanedName}"`);
9057
+ await document.fonts.ready;
9058
+ } catch {
9059
+ }
9060
+ }
9061
+ async function loadTemplateFontsIntoHead(templateFonts, pagesDesignData) {
9062
+ if (typeof document === "undefined") return;
9063
+ const loadPromises = [];
9064
+ if (Array.isArray(templateFonts) && templateFonts.length > 0) {
9065
+ templateFonts.forEach((f) => {
9066
+ if (f && typeof f === "object") {
9067
+ const fontName = f.fontName || f.fontFamily;
9068
+ const fontUrl = f.fontFile;
9069
+ if (fontName && fontUrl) {
9070
+ loadPromises.push(injectFontFace(fontName, fontUrl));
9071
+ }
9072
+ if (f.fontFamily && f.fontFamily !== fontName && fontUrl) {
9073
+ loadPromises.push(injectFontFace(f.fontFamily, fontUrl));
9074
+ }
9075
+ }
9076
+ });
9077
+ }
9078
+ const designs = Array.isArray(pagesDesignData) ? pagesDesignData : [pagesDesignData];
9079
+ const usedFontNames = /* @__PURE__ */ new Set();
9080
+ designs.forEach((d2) => {
9081
+ if (d2) {
9082
+ const extracted = extractFontFamilies(d2);
9083
+ extracted.forEach((name) => usedFontNames.add(name));
9084
+ }
9085
+ });
9086
+ if (usedFontNames.size > 0) {
9087
+ try {
9088
+ const allFonts = await fetchAllFontsCached();
9089
+ usedFontNames.forEach((name) => {
9090
+ const lower = name.toLowerCase();
9091
+ const matched = allFonts.find((f) => {
9092
+ if (f.fontName && f.fontName.toLowerCase() === lower) return true;
9093
+ if (f.fontFamily && f.fontFamily.toLowerCase() === lower) return true;
9094
+ const children = f.childFonts || f.chieldFonts || [];
9095
+ return children.some(
9096
+ (c2) => c2.fontName && c2.fontName.toLowerCase() === lower || c2.fontFamily && c2.fontFamily.toLowerCase() === lower
9097
+ );
9098
+ });
9099
+ if (matched) {
9100
+ if (matched.fontName && matched.fontFile) {
9101
+ loadPromises.push(injectFontFace(matched.fontName, matched.fontFile));
9102
+ }
9103
+ if (matched.fontFamily && matched.fontFile) {
9104
+ loadPromises.push(injectFontFace(matched.fontFamily, matched.fontFile));
9105
+ }
9106
+ const children = matched.childFonts || matched.chieldFonts || [];
9107
+ children.forEach((c2) => {
9108
+ if (c2.fontName && c2.fontFile) {
9109
+ loadPromises.push(injectFontFace(c2.fontName, c2.fontFile));
9110
+ }
9111
+ });
9112
+ }
9113
+ });
9114
+ } catch (err) {
9115
+ console.warn("Failed to fetch fonts list for font injection:", err);
9116
+ }
9117
+ }
9118
+ await Promise.allSettled(loadPromises);
9119
+ try {
9120
+ await document.fonts.ready;
9121
+ } catch {
9122
+ }
9123
+ }
9124
+ function extractFontFamilies(designJson) {
9125
+ if (!designJson) return [];
9126
+ const fonts = /* @__PURE__ */ new Set();
9127
+ const traverse = (obj) => {
9128
+ if (!obj) return;
9129
+ if (obj.fontFamily && typeof obj.fontFamily === "string") {
9130
+ const cleaned = obj.fontFamily.trim().replace(/^['"]+|['"]+$/g, "");
9131
+ if (cleaned) fonts.add(cleaned);
9132
+ }
9133
+ if (obj.fontId && typeof obj.fontId === "string") {
9134
+ fonts.add(obj.fontId.trim());
9135
+ }
9136
+ if (obj.styles && typeof obj.styles === "object") {
9137
+ Object.values(obj.styles).forEach((line) => {
9138
+ if (line && typeof line === "object") {
9139
+ Object.values(line).forEach((charStyle) => {
9140
+ if (charStyle?.fontFamily && typeof charStyle.fontFamily === "string") {
9141
+ const cleaned = charStyle.fontFamily.trim().replace(/^['"]+|['"]+$/g, "");
9142
+ if (cleaned) fonts.add(cleaned);
9143
+ }
9144
+ });
9145
+ }
9146
+ });
9147
+ }
9148
+ if (Array.isArray(obj.objects)) {
9149
+ obj.objects.forEach(traverse);
9150
+ }
9151
+ };
9152
+ if (Array.isArray(designJson.objects)) {
9153
+ designJson.objects.forEach(traverse);
9154
+ } else if (Array.isArray(designJson)) {
9155
+ designJson.forEach(traverse);
9156
+ } else {
9157
+ traverse(designJson);
9158
+ }
9159
+ return Array.from(fonts);
9160
+ }
9161
+ async function resolveFontIds(fontNames) {
9162
+ if (!fontNames || !fontNames.length) return [];
9163
+ try {
9164
+ const allFonts = await fetchAllFontsCached();
9165
+ const matchedIds = /* @__PURE__ */ new Set();
9166
+ for (const rawName of fontNames) {
9167
+ const name = rawName.trim().replace(/^['"]+|['"]+$/g, "");
9168
+ const lower = name.toLowerCase();
9169
+ if (/^[0-9a-fA-F]{24}$/.test(name)) {
9170
+ matchedIds.add(name);
9171
+ continue;
9172
+ }
9173
+ const matchedFont = allFonts.find((f) => {
9174
+ if (f._id === name) return true;
9175
+ if (f.fontFamily && f.fontFamily.toLowerCase() === lower) return true;
9176
+ if (f.fontName && f.fontName.toLowerCase() === lower) return true;
9177
+ const children = f.childFonts || f.chieldFonts || [];
9178
+ return children.some(
9179
+ (c2) => c2._id === name || c2.fontName && c2.fontName.toLowerCase() === lower || c2.fontFamily && c2.fontFamily.toLowerCase() === lower
9180
+ );
9181
+ });
9182
+ if (matchedFont?._id) {
9183
+ matchedIds.add(matchedFont._id);
9184
+ }
9185
+ }
9186
+ return Array.from(matchedIds);
9187
+ } catch (err) {
9188
+ console.warn("Failed to resolve font IDs:", err);
9189
+ return [];
9190
+ }
9191
+ }
9192
+ async function resolveFontsForExport(designs, templateFonts) {
9193
+ const result = [];
9194
+ const foundNames = /* @__PURE__ */ new Set();
9195
+ if (Array.isArray(templateFonts) && templateFonts.length > 0) {
9196
+ templateFonts.forEach((f) => {
9197
+ if (f && typeof f === "object") {
9198
+ const fontName = f.fontName || f.fontFamily;
9199
+ const fontFile = f.fontFile;
9200
+ if (fontName && fontFile) {
9201
+ result.push({ fontName, fontFamily: f.fontFamily, fontFile });
9202
+ foundNames.add(fontName.toLowerCase());
9203
+ if (f.fontFamily) foundNames.add(f.fontFamily.toLowerCase());
9204
+ }
9205
+ }
9206
+ });
9207
+ }
9208
+ const usedFontNames = /* @__PURE__ */ new Set();
9209
+ const designsArr = Array.isArray(designs) ? designs : [designs];
9210
+ designsArr.forEach((d2) => {
9211
+ if (d2) {
9212
+ const extracted = extractFontFamilies(d2);
9213
+ extracted.forEach((name) => {
9214
+ if (!foundNames.has(name.toLowerCase())) {
9215
+ usedFontNames.add(name);
9216
+ }
9217
+ });
9218
+ }
9219
+ });
9220
+ if (usedFontNames.size > 0) {
9221
+ try {
9222
+ const allFonts = await fetchAllFontsCached();
9223
+ usedFontNames.forEach((name) => {
9224
+ const lower = name.toLowerCase();
9225
+ const matched = allFonts.find((f) => {
9226
+ if (f.fontName && f.fontName.toLowerCase() === lower) return true;
9227
+ if (f.fontFamily && f.fontFamily.toLowerCase() === lower) return true;
9228
+ const children = f.childFonts || f.chieldFonts || [];
9229
+ return children.some(
9230
+ (c2) => c2.fontName && c2.fontName.toLowerCase() === lower || c2.fontFamily && c2.fontFamily.toLowerCase() === lower
9231
+ );
9232
+ });
9233
+ if (matched) {
9234
+ if (matched.fontName && matched.fontFile && !foundNames.has(matched.fontName.toLowerCase())) {
9235
+ result.push({
9236
+ fontName: matched.fontName,
9237
+ fontFamily: matched.fontFamily,
9238
+ fontFile: matched.fontFile
9239
+ });
9240
+ foundNames.add(matched.fontName.toLowerCase());
9241
+ }
9242
+ const children = matched.childFonts || matched.chieldFonts || [];
9243
+ children.forEach((c2) => {
9244
+ const childName = c2.fontName || name;
9245
+ if (c2.fontFile && !foundNames.has(childName.toLowerCase())) {
9246
+ result.push({
9247
+ fontName: childName,
9248
+ fontFamily: c2.fontFamily,
9249
+ fontFile: c2.fontFile
9250
+ });
9251
+ foundNames.add(childName.toLowerCase());
9252
+ }
9253
+ });
9254
+ }
9255
+ });
9256
+ } catch (err) {
9257
+ console.warn("Failed to resolve fonts for export:", err);
9258
+ }
9259
+ }
9260
+ return result;
9261
+ }
9020
9262
  async function loadPageToCanvas(canvas2, json, pageSide) {
9021
9263
  if (!canvas2) return;
9022
9264
  canvas2.discardActiveObject();
@@ -9024,6 +9266,7 @@ async function loadPageToCanvas(canvas2, json, pageSide) {
9024
9266
  if (json) {
9025
9267
  try {
9026
9268
  const parsedJson = typeof json === "string" ? JSON.parse(json) : json;
9269
+ await loadTemplateFontsIntoHead(null, parsedJson);
9027
9270
  const loadPromise = canvas2.loadFromJSON(parsedJson);
9028
9271
  const timeoutPromise = new Promise(
9029
9272
  (_2, reject) => setTimeout(() => reject(new Error("Canvas loadFromJSON timeout")), 4e3)
@@ -9365,7 +9608,23 @@ const HISTORY_CUSTOM_PROPERTIES = [
9365
9608
  "__borderRect",
9366
9609
  "__rotationControlSize",
9367
9610
  "__showDegree",
9368
- "textAlign"
9611
+ "textAlign",
9612
+ "fontFamily",
9613
+ "fontSize",
9614
+ "fontWeight",
9615
+ "fontStyle",
9616
+ "underline",
9617
+ "linethrough",
9618
+ "overline",
9619
+ "fill",
9620
+ "stroke",
9621
+ "strokeWidth",
9622
+ "lineHeight",
9623
+ "charSpacing",
9624
+ "styles",
9625
+ "paintFirst",
9626
+ "strokeUniform",
9627
+ "objectCaching"
9369
9628
  ];
9370
9629
  const useHistoryStore = create((set, get) => ({
9371
9630
  canvas: null,
@@ -9483,248 +9742,6 @@ const useHistoryStore = create((set, get) => ({
9483
9742
  }
9484
9743
  }
9485
9744
  }));
9486
- const FontService = {
9487
- async getAll(data) {
9488
- const res = await apiFetch(`/api/fonts?page=${data.page}&limit=${data.limit}&status=true`, {
9489
- method: "GET"
9490
- });
9491
- return res;
9492
- }
9493
- };
9494
- let cachedFontsPromise = null;
9495
- async function fetchAllFontsCached() {
9496
- if (!cachedFontsPromise) {
9497
- cachedFontsPromise = FontService.getAll({ page: 1, limit: 1e3 }).then((res) => res?.data || []).catch((err) => {
9498
- console.warn("Failed to fetch all fonts:", err);
9499
- cachedFontsPromise = null;
9500
- return [];
9501
- });
9502
- }
9503
- return cachedFontsPromise;
9504
- }
9505
- async function injectFontFace(fontName, fontUrl) {
9506
- if (!fontName || !fontUrl || typeof document === "undefined") return;
9507
- const cleanedName = fontName.trim().replace(/^['"]+|['"]+$/g, "");
9508
- const id = `font-${cleanedName.replace(/\s+/g, "-").toLowerCase()}`;
9509
- if (!document.getElementById(id)) {
9510
- const style = document.createElement("style");
9511
- style.id = id;
9512
- style.innerHTML = `
9513
- @font-face {
9514
- font-family: '${cleanedName}';
9515
- src: url('${fontUrl}');
9516
- font-display: swap;
9517
- }
9518
- `;
9519
- document.head.appendChild(style);
9520
- }
9521
- try {
9522
- await document.fonts.load(`16px "${cleanedName}"`);
9523
- await document.fonts.ready;
9524
- } catch {
9525
- }
9526
- }
9527
- async function loadTemplateFontsIntoHead(templateFonts, pagesDesignData) {
9528
- if (typeof document === "undefined") return;
9529
- const loadPromises = [];
9530
- if (Array.isArray(templateFonts) && templateFonts.length > 0) {
9531
- templateFonts.forEach((f) => {
9532
- if (f && typeof f === "object") {
9533
- const fontName = f.fontName || f.fontFamily;
9534
- const fontUrl = f.fontFile;
9535
- if (fontName && fontUrl) {
9536
- loadPromises.push(injectFontFace(fontName, fontUrl));
9537
- }
9538
- if (f.fontFamily && f.fontFamily !== fontName && fontUrl) {
9539
- loadPromises.push(injectFontFace(f.fontFamily, fontUrl));
9540
- }
9541
- }
9542
- });
9543
- }
9544
- const designs = Array.isArray(pagesDesignData) ? pagesDesignData : [pagesDesignData];
9545
- const usedFontNames = /* @__PURE__ */ new Set();
9546
- designs.forEach((d2) => {
9547
- if (d2) {
9548
- const extracted = extractFontFamilies(d2);
9549
- extracted.forEach((name) => usedFontNames.add(name));
9550
- }
9551
- });
9552
- if (usedFontNames.size > 0) {
9553
- try {
9554
- const allFonts = await fetchAllFontsCached();
9555
- usedFontNames.forEach((name) => {
9556
- const lower = name.toLowerCase();
9557
- const matched = allFonts.find((f) => {
9558
- if (f.fontName && f.fontName.toLowerCase() === lower) return true;
9559
- if (f.fontFamily && f.fontFamily.toLowerCase() === lower) return true;
9560
- const children = f.childFonts || f.chieldFonts || [];
9561
- return children.some(
9562
- (c2) => c2.fontName && c2.fontName.toLowerCase() === lower || c2.fontFamily && c2.fontFamily.toLowerCase() === lower
9563
- );
9564
- });
9565
- if (matched) {
9566
- if (matched.fontName && matched.fontFile) {
9567
- loadPromises.push(injectFontFace(matched.fontName, matched.fontFile));
9568
- }
9569
- if (matched.fontFamily && matched.fontFile) {
9570
- loadPromises.push(injectFontFace(matched.fontFamily, matched.fontFile));
9571
- }
9572
- const children = matched.childFonts || matched.chieldFonts || [];
9573
- children.forEach((c2) => {
9574
- if (c2.fontName && c2.fontFile) {
9575
- loadPromises.push(injectFontFace(c2.fontName, c2.fontFile));
9576
- }
9577
- });
9578
- }
9579
- });
9580
- } catch (err) {
9581
- console.warn("Failed to fetch fonts list for font injection:", err);
9582
- }
9583
- }
9584
- await Promise.allSettled(loadPromises);
9585
- try {
9586
- await document.fonts.ready;
9587
- } catch {
9588
- }
9589
- }
9590
- function extractFontFamilies(designJson) {
9591
- if (!designJson) return [];
9592
- const fonts = /* @__PURE__ */ new Set();
9593
- const traverse = (obj) => {
9594
- if (!obj) return;
9595
- if (obj.fontFamily && typeof obj.fontFamily === "string") {
9596
- const cleaned = obj.fontFamily.trim().replace(/^['"]+|['"]+$/g, "");
9597
- if (cleaned) fonts.add(cleaned);
9598
- }
9599
- if (obj.fontId && typeof obj.fontId === "string") {
9600
- fonts.add(obj.fontId.trim());
9601
- }
9602
- if (obj.styles && typeof obj.styles === "object") {
9603
- Object.values(obj.styles).forEach((line) => {
9604
- if (line && typeof line === "object") {
9605
- Object.values(line).forEach((charStyle) => {
9606
- if (charStyle?.fontFamily && typeof charStyle.fontFamily === "string") {
9607
- const cleaned = charStyle.fontFamily.trim().replace(/^['"]+|['"]+$/g, "");
9608
- if (cleaned) fonts.add(cleaned);
9609
- }
9610
- });
9611
- }
9612
- });
9613
- }
9614
- if (Array.isArray(obj.objects)) {
9615
- obj.objects.forEach(traverse);
9616
- }
9617
- };
9618
- if (Array.isArray(designJson.objects)) {
9619
- designJson.objects.forEach(traverse);
9620
- } else if (Array.isArray(designJson)) {
9621
- designJson.forEach(traverse);
9622
- } else {
9623
- traverse(designJson);
9624
- }
9625
- return Array.from(fonts);
9626
- }
9627
- async function resolveFontIds(fontNames) {
9628
- if (!fontNames || !fontNames.length) return [];
9629
- try {
9630
- const allFonts = await fetchAllFontsCached();
9631
- const matchedIds = /* @__PURE__ */ new Set();
9632
- for (const rawName of fontNames) {
9633
- const name = rawName.trim().replace(/^['"]+|['"]+$/g, "");
9634
- const lower = name.toLowerCase();
9635
- if (/^[0-9a-fA-F]{24}$/.test(name)) {
9636
- matchedIds.add(name);
9637
- continue;
9638
- }
9639
- const matchedFont = allFonts.find((f) => {
9640
- if (f._id === name) return true;
9641
- if (f.fontFamily && f.fontFamily.toLowerCase() === lower) return true;
9642
- if (f.fontName && f.fontName.toLowerCase() === lower) return true;
9643
- const children = f.childFonts || f.chieldFonts || [];
9644
- return children.some(
9645
- (c2) => c2._id === name || c2.fontName && c2.fontName.toLowerCase() === lower || c2.fontFamily && c2.fontFamily.toLowerCase() === lower
9646
- );
9647
- });
9648
- if (matchedFont?._id) {
9649
- matchedIds.add(matchedFont._id);
9650
- }
9651
- }
9652
- return Array.from(matchedIds);
9653
- } catch (err) {
9654
- console.warn("Failed to resolve font IDs:", err);
9655
- return [];
9656
- }
9657
- }
9658
- async function resolveFontsForExport(designs, templateFonts) {
9659
- const result = [];
9660
- const foundNames = /* @__PURE__ */ new Set();
9661
- if (Array.isArray(templateFonts) && templateFonts.length > 0) {
9662
- templateFonts.forEach((f) => {
9663
- if (f && typeof f === "object") {
9664
- const fontName = f.fontName || f.fontFamily;
9665
- const fontFile = f.fontFile;
9666
- if (fontName && fontFile) {
9667
- result.push({ fontName, fontFamily: f.fontFamily, fontFile });
9668
- foundNames.add(fontName.toLowerCase());
9669
- if (f.fontFamily) foundNames.add(f.fontFamily.toLowerCase());
9670
- }
9671
- }
9672
- });
9673
- }
9674
- const usedFontNames = /* @__PURE__ */ new Set();
9675
- const designsArr = Array.isArray(designs) ? designs : [designs];
9676
- designsArr.forEach((d2) => {
9677
- if (d2) {
9678
- const extracted = extractFontFamilies(d2);
9679
- extracted.forEach((name) => {
9680
- if (!foundNames.has(name.toLowerCase())) {
9681
- usedFontNames.add(name);
9682
- }
9683
- });
9684
- }
9685
- });
9686
- if (usedFontNames.size > 0) {
9687
- try {
9688
- const allFonts = await fetchAllFontsCached();
9689
- usedFontNames.forEach((name) => {
9690
- const lower = name.toLowerCase();
9691
- const matched = allFonts.find((f) => {
9692
- if (f.fontName && f.fontName.toLowerCase() === lower) return true;
9693
- if (f.fontFamily && f.fontFamily.toLowerCase() === lower) return true;
9694
- const children = f.childFonts || f.chieldFonts || [];
9695
- return children.some(
9696
- (c2) => c2.fontName && c2.fontName.toLowerCase() === lower || c2.fontFamily && c2.fontFamily.toLowerCase() === lower
9697
- );
9698
- });
9699
- if (matched) {
9700
- if (matched.fontName && matched.fontFile && !foundNames.has(matched.fontName.toLowerCase())) {
9701
- result.push({
9702
- fontName: matched.fontName,
9703
- fontFamily: matched.fontFamily,
9704
- fontFile: matched.fontFile
9705
- });
9706
- foundNames.add(matched.fontName.toLowerCase());
9707
- }
9708
- const children = matched.childFonts || matched.chieldFonts || [];
9709
- children.forEach((c2) => {
9710
- const childName = c2.fontName || name;
9711
- if (c2.fontFile && !foundNames.has(childName.toLowerCase())) {
9712
- result.push({
9713
- fontName: childName,
9714
- fontFamily: c2.fontFamily,
9715
- fontFile: c2.fontFile
9716
- });
9717
- foundNames.add(childName.toLowerCase());
9718
- }
9719
- });
9720
- }
9721
- });
9722
- } catch (err) {
9723
- console.warn("Failed to resolve fonts for export:", err);
9724
- }
9725
- }
9726
- return result;
9727
- }
9728
9745
  const WATERMARK_TEXT = "vibrantprintshop";
9729
9746
  function createWatermark(curWorkarea, pageSide) {
9730
9747
  const curW = curWorkarea.getScaledWidth ? curWorkarea.getScaledWidth() : curWorkarea.width * (curWorkarea.scaleX || 1);
@@ -9817,8 +9834,18 @@ function exportDesignJSON(canvas2, workarea) {
9817
9834
  }
9818
9835
  const ShapeService = {
9819
9836
  async getAll(data) {
9837
+ const params = new URLSearchParams();
9838
+ if (data.page) params.append("page", String(data.page));
9839
+ if (data.limit) params.append("limit", String(data.limit));
9840
+ if (data.category && data.category !== "ALL" && data.category !== "null" && data.category !== "undefined") {
9841
+ params.append("category", data.category);
9842
+ }
9843
+ if (data.search && data.search.trim()) {
9844
+ params.append("search", data.search.trim());
9845
+ }
9846
+ params.append("status", "true");
9820
9847
  const res = await apiFetch(
9821
- `/api/cliparts?page=${data.page}&limit=${data.limit}&category=${data.category}&status=true`,
9848
+ `/api/cliparts?${params.toString()}`,
9822
9849
  {
9823
9850
  method: "GET"
9824
9851
  }
@@ -9828,8 +9855,17 @@ const ShapeService = {
9828
9855
  };
9829
9856
  },
9830
9857
  async getAllExternal(data) {
9858
+ const params = new URLSearchParams();
9859
+ if (data.page) params.append("page", String(data.page));
9860
+ if (data.limit) params.append("limit", String(data.limit));
9861
+ if (data.keyType) params.append("keyType", data.keyType);
9862
+ if (data.isVector) params.append("isVector", "true");
9863
+ if (data.query && data.query.trim()) {
9864
+ params.append("query", data.query.trim());
9865
+ }
9866
+ params.append("status", "true");
9831
9867
  const res = await apiFetch(
9832
- `/api/external/images?page=${data.page}&limit=${data.limit}&keyType=${data.keyType}&status=true${data.query ? `&query=${data.query}` : ""}`,
9868
+ `/api/external/images?${params.toString()}`,
9833
9869
  {
9834
9870
  method: "GET"
9835
9871
  }
@@ -101910,8 +101946,43 @@ function TemplateMockupModal({
101910
101946
  if (!uploadedImageUrl) {
101911
101947
  throw new Error("Server upload returned empty image URL");
101912
101948
  }
101949
+ const canvas22 = useEditorStore.getState().canvas;
101950
+ if (!templateId || !canvas22) return;
101951
+ const { pages: historyPages, currentPage } = useHistoryStore.getState();
101952
+ const editorPages = useEditorStore.getState().pages || {};
101953
+ const currentJson = canvas22.toObject(["name"]);
101954
+ const currentImage = getWorkareaDataUrl(canvas22, 1, currentPage);
101955
+ const pages = await Promise.all(
101956
+ ["front", "back"].map(async (side, i) => {
101957
+ const isCurrent = currentPage === side;
101958
+ const designData = isCurrent ? currentJson : editorPages[side] || historyPages[side]?.history?.at(-1) || null;
101959
+ if (!designData) return null;
101960
+ const sideImage = isCurrent ? currentImage : await renderPageDataUrl(designData, canvas22, 1, side);
101961
+ const uploadedImageUrl2 = await UploadService.uploadDataUrl(
101962
+ sideImage,
101963
+ `template-${templateId}-${side}-${Date.now()}.png`
101964
+ );
101965
+ return {
101966
+ side,
101967
+ designData,
101968
+ image: uploadedImageUrl2,
101969
+ order: i + 1
101970
+ };
101971
+ })
101972
+ );
101973
+ const validPages = pages.filter(Boolean);
101974
+ const usedFontNames = /* @__PURE__ */ new Set();
101975
+ validPages.forEach((p2) => {
101976
+ if (p2?.designData) {
101977
+ const fontsInPage = extractFontFamilies(p2.designData);
101978
+ fontsInPage.forEach((f) => usedFontNames.add(f));
101979
+ }
101980
+ });
101981
+ const resolvedFontIds = await resolveFontIds(Array.from(usedFontNames));
101913
101982
  const res = await TemplateDesignService.updateTemplateDesign(templateId, {
101914
- image: uploadedImageUrl
101983
+ image: uploadedImageUrl,
101984
+ pages: validPages,
101985
+ fonts: resolvedFontIds
101915
101986
  });
101916
101987
  if (res && res.success) {
101917
101988
  setGenerateSuccess(true);
@@ -102116,6 +102187,16 @@ function Topbar() {
102116
102187
  const resetTour = useTourStore((s2) => s2.reset);
102117
102188
  const templateId = getQueryParam("templateId") || getQueryParam("id");
102118
102189
  const designType = (getQueryParam("designType") || getQueryParam("designtype") || getQueryParam("type"))?.toLowerCase();
102190
+ const handleLeavePage = () => {
102191
+ if (designType === "text") {
102192
+ spaNavigate("/design-configuration/text-templates");
102193
+ return;
102194
+ }
102195
+ if (designType === "template") {
102196
+ spaNavigate("/design-configuration/template-designs");
102197
+ return;
102198
+ }
102199
+ };
102119
102200
  const handleSaveTextTemplate = async () => {
102120
102201
  const canvas2 = useEditorStore.getState().canvas;
102121
102202
  if (!templateId || !canvas2) return;
@@ -102210,8 +102291,42 @@ function Topbar() {
102210
102291
  }
102211
102292
  };
102212
102293
  return /* @__PURE__ */ jsxs("div", { className: "w-full h-14 bg-[#111] text-white flex items-center justify-between px-4 border-b border-gray-800 relative z-[9999] whitespace-nowrap", children: [
102213
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 shrink-0", children: [
102214
- /* @__PURE__ */ jsx("h1", { className: "text-lg font-semibold", children: "Vibrant" }),
102294
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 shrink-0", children: [
102295
+ /* @__PURE__ */ jsxs(
102296
+ "button",
102297
+ {
102298
+ id: "topbar-back-button",
102299
+ type: "button",
102300
+ onClick: handleLeavePage,
102301
+ disabled: isSaving,
102302
+ className: `flex items-center gap-1.5 px-2.5 py-1.5 bg-[#222] hover:bg-[#2c2c2c] active:bg-[#1a1a1a] text-gray-300 hover:text-white border border-gray-700/70 hover:border-gray-500 rounded-lg text-xs font-medium transition-all group shadow-sm ${isSaving ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`,
102303
+ title: "Back / Leave page",
102304
+ "aria-label": "Back / Leave page",
102305
+ children: [
102306
+ /* @__PURE__ */ jsx(
102307
+ "svg",
102308
+ {
102309
+ className: "w-4 h-4 transition-transform duration-150 group-hover:-translate-x-0.5 text-gray-300 group-hover:text-white",
102310
+ fill: "none",
102311
+ viewBox: "0 0 24 24",
102312
+ stroke: "currentColor",
102313
+ strokeWidth: 2.2,
102314
+ children: /* @__PURE__ */ jsx(
102315
+ "path",
102316
+ {
102317
+ strokeLinecap: "round",
102318
+ strokeLinejoin: "round",
102319
+ d: "M10 19l-7-7m0 0l7-7m-7 7h18"
102320
+ }
102321
+ )
102322
+ }
102323
+ ),
102324
+ /* @__PURE__ */ jsx("span", { className: "font-semibold tracking-wide", children: "Back" })
102325
+ ]
102326
+ }
102327
+ ),
102328
+ /* @__PURE__ */ jsx("div", { className: "w-px h-5 bg-gray-700/80 hidden sm:block" }),
102329
+ /* @__PURE__ */ jsx("h1", { className: "text-lg font-semibold tracking-tight", children: "Vibrant" }),
102215
102330
  /* @__PURE__ */ jsx("div", { className: "w-px h-6 bg-gray-600 hidden md:block" })
102216
102331
  ] }),
102217
102332
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 shrink-0 ml-4", children: [
@@ -102585,6 +102700,7 @@ function TemplatesPanel() {
102585
102700
  const [page, setPage] = useState(1);
102586
102701
  const scrollRef = useRef(null);
102587
102702
  const [hasMore, setHasMore] = useState(true);
102703
+ const templateId = getQueryParam("templateId");
102588
102704
  useEffect(() => {
102589
102705
  if (!productData?._id) return;
102590
102706
  startLoading();
@@ -102676,7 +102792,7 @@ function TemplatesPanel() {
102676
102792
  )
102677
102793
  ] }),
102678
102794
  /* @__PURE__ */ jsx("div", { ref: scrollRef, className: "h-[calc(100vh-125px)] overflow-auto custom-scrollbar", children: /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3 pb-12 mx-4", children: [
102679
- templates.map((tpl) => /* @__PURE__ */ jsx(
102795
+ templates.filter((t2) => t2.image && t2._id !== templateId).map((tpl) => /* @__PURE__ */ jsx(
102680
102796
  "button",
102681
102797
  {
102682
102798
  className: "rounded overflow-hidden group relative cursor-pointer border border-transparent hover:border-[#02bc71] transition-all",
@@ -103139,26 +103255,63 @@ function AllElements({
103139
103255
  elements: elements2,
103140
103256
  onBack,
103141
103257
  scrollRef,
103142
- onSearch
103258
+ onSearch,
103259
+ initialSearch = "",
103260
+ title,
103261
+ backText
103143
103262
  }) {
103144
- const [search, setSearch] = useState("");
103145
- const { isLoading } = useLoaderStore.getState();
103263
+ const [search, setSearch] = useState(initialSearch);
103264
+ const { isLoading } = useLoaderStore();
103146
103265
  const { addRecentItem } = useRecentElementsStore();
103266
+ useEffect(() => {
103267
+ setSearch(initialSearch);
103268
+ }, [initialSearch]);
103147
103269
  const clearSearch = () => {
103148
103270
  setSearch("");
103149
103271
  onSearch("");
103150
103272
  };
103151
- const handleChange = useCallback(
103152
- (e3) => {
103153
- const value = e3.target.value;
103154
- setSearch(value);
103155
- onSearch(value.trim());
103156
- },
103157
- [onSearch]
103158
- );
103159
- return /* @__PURE__ */ jsxs(Fragment, { children: [
103160
- /* @__PURE__ */ jsx("div", { onClick: onBack, className: "cursor-pointer mx-4 mb-2", children: "← Back" }),
103161
- /* @__PURE__ */ jsxs("div", { className: "bg-neutral-800 px-3 py-2 rounded flex items-center gap-2 mb-4 mx-4 relative", children: [
103273
+ const handleSearchSubmit = () => {
103274
+ onSearch(search.trim());
103275
+ };
103276
+ const handleKeyDown = (e3) => {
103277
+ e3.stopPropagation();
103278
+ if (e3.key === "Enter") {
103279
+ handleSearchSubmit();
103280
+ }
103281
+ };
103282
+ const dynamicBackLabel = backText || (title ? title : "Back");
103283
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
103284
+ /* @__PURE__ */ jsx("div", { className: "mx-4 mb-2.5 flex items-center", children: /* @__PURE__ */ jsxs(
103285
+ "button",
103286
+ {
103287
+ type: "button",
103288
+ onClick: onBack,
103289
+ className: "cursor-pointer flex items-center gap-1.5 text-xs font-semibold text-neutral-300 hover:text-white transition-colors group",
103290
+ title: `Back to ${dynamicBackLabel}`,
103291
+ children: [
103292
+ /* @__PURE__ */ jsxs(
103293
+ "svg",
103294
+ {
103295
+ width: "16",
103296
+ height: "16",
103297
+ viewBox: "0 0 24 24",
103298
+ fill: "none",
103299
+ stroke: "currentColor",
103300
+ strokeWidth: "2.5",
103301
+ strokeLinecap: "round",
103302
+ strokeLinejoin: "round",
103303
+ className: "group-hover:-translate-x-0.5 transition-transform text-neutral-400 group-hover:text-white",
103304
+ children: [
103305
+ /* @__PURE__ */ jsx("line", { x1: "19", y1: "12", x2: "5", y2: "12" }),
103306
+ /* @__PURE__ */ jsx("polyline", { points: "12 19 5 12 12 5" })
103307
+ ]
103308
+ }
103309
+ ),
103310
+ /* @__PURE__ */ jsx("span", { className: "capitalize truncate max-w-[240px]", children: dynamicBackLabel })
103311
+ ]
103312
+ }
103313
+ ) }),
103314
+ /* @__PURE__ */ jsxs("div", { className: "bg-neutral-800 px-3 py-2 rounded flex items-center gap-2 mb-4 mx-4 relative focus-within:ring-1 focus-within:ring-neutral-600", children: [
103162
103315
  /* @__PURE__ */ jsxs(
103163
103316
  "svg",
103164
103317
  {
@@ -103166,7 +103319,8 @@ function AllElements({
103166
103319
  stroke: "currentColor",
103167
103320
  fill: "none",
103168
103321
  viewBox: "0 0 24 24",
103169
- className: "text-gray-300",
103322
+ className: "text-gray-400 shrink-0 cursor-pointer hover:text-white transition-colors",
103323
+ onClick: handleSearchSubmit,
103170
103324
  children: [
103171
103325
  /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "8" }),
103172
103326
  /* @__PURE__ */ jsx("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" })
@@ -103178,9 +103332,10 @@ function AllElements({
103178
103332
  {
103179
103333
  type: "text",
103180
103334
  value: search,
103181
- onChange: handleChange,
103182
- onKeyDown: (e3) => e3.stopPropagation(),
103183
- className: "bg-transparent outline-none text-sm text-gray-300 w-full pr-8"
103335
+ onChange: (e3) => setSearch(e3.target.value),
103336
+ onKeyDown: handleKeyDown,
103337
+ placeholder: title ? `Search in ${title}...` : "Search elements & shapes...",
103338
+ className: "bg-transparent outline-none text-sm text-gray-200 placeholder-gray-500 w-full pr-8"
103184
103339
  }
103185
103340
  ),
103186
103341
  search && /* @__PURE__ */ jsx(
@@ -103188,13 +103343,13 @@ function AllElements({
103188
103343
  {
103189
103344
  type: "button",
103190
103345
  onClick: clearSearch,
103191
- className: "absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-white transition-colors p-1",
103346
+ className: "absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-white transition-colors p-1 cursor-pointer",
103192
103347
  "aria-label": "Clear search",
103193
103348
  children: /* @__PURE__ */ jsxs(
103194
103349
  "svg",
103195
103350
  {
103196
- width: "16",
103197
- height: "16",
103351
+ width: "14",
103352
+ height: "14",
103198
103353
  viewBox: "0 0 24 24",
103199
103354
  fill: "none",
103200
103355
  stroke: "currentColor",
@@ -103208,9 +103363,20 @@ function AllElements({
103208
103363
  }
103209
103364
  )
103210
103365
  ] }),
103211
- /* @__PURE__ */ jsx("hr", { className: "text-neutral-800 h-[0.5px]" }),
103212
- /* @__PURE__ */ jsx("div", { className: "h-[calc(100vh-160px)] overflow-auto", ref: scrollRef, children: /* @__PURE__ */ jsx("div", { className: "mx-4 py-5", children: elements2.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-gray-400 text-sm text-center", children: "No results found" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
103213
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-4 md:grid-cols-3 gap-5", children: elements2.map((shape, i) => /* @__PURE__ */ jsx(
103366
+ /* @__PURE__ */ jsx("hr", { className: "text-neutral-800 h-[0.5px] border-neutral-800" }),
103367
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0 overflow-auto custom-scrollbar", ref: scrollRef, children: /* @__PURE__ */ jsx("div", { className: "mx-4 py-5", children: elements2.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "py-12 text-center", children: [
103368
+ /* @__PURE__ */ jsx("p", { className: "text-gray-400 text-sm", children: search.trim() ? `No results found for "${search}"` : "No elements found" }),
103369
+ search.trim() && /* @__PURE__ */ jsx(
103370
+ "button",
103371
+ {
103372
+ type: "button",
103373
+ onClick: clearSearch,
103374
+ className: "mt-3 text-xs text-emerald-400 hover:underline cursor-pointer",
103375
+ children: "Clear search"
103376
+ }
103377
+ )
103378
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
103379
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-4 md:grid-cols-3 gap-3", children: elements2.map((shape, i) => /* @__PURE__ */ jsx(
103214
103380
  "button",
103215
103381
  {
103216
103382
  type: "button",
@@ -103225,21 +103391,22 @@ function AllElements({
103225
103391
  useSidePanelStore.getState().setActivePanel(null);
103226
103392
  }
103227
103393
  },
103228
- className: "w-full cursor-pointer flex items-center justify-center",
103394
+ className: "w-full cursor-pointer flex items-center justify-center p-2 rounded-lg bg-neutral-900/40 hover:bg-neutral-800 border border-neutral-800/60 hover:border-neutral-700 transition-colors",
103229
103395
  children: shape.image && /* @__PURE__ */ jsx(
103230
103396
  "img",
103231
103397
  {
103232
103398
  src: shape.image,
103233
- alt: shape.category,
103399
+ alt: shape.category || "element",
103234
103400
  width: 150,
103235
103401
  height: 150,
103236
- className: "w-full h-24 object-contain"
103402
+ className: "w-full h-20 object-contain",
103403
+ loading: "lazy"
103237
103404
  }
103238
103405
  )
103239
103406
  },
103240
- i
103407
+ shape._id || shape.image || i
103241
103408
  )) }),
103242
- isLoading && /* @__PURE__ */ jsx("div", { className: "text-center py-4 text-sm text-gray-400", children: "Loading more..." })
103409
+ isLoading && /* @__PURE__ */ jsx("div", { className: "text-center py-4 text-xs text-gray-400 animate-pulse", children: "Loading more elements..." })
103243
103410
  ] }) }) })
103244
103411
  ] });
103245
103412
  }
@@ -103272,72 +103439,181 @@ function ElementsPanel() {
103272
103439
  const [page, setPage] = useState(1);
103273
103440
  const [category, setCategory] = useState(null);
103274
103441
  const [keyType, setKeyType] = useState(null);
103275
- const [error2, setError] = useState(null);
103442
+ const [searchQuery, setSearchQuery] = useState("");
103443
+ const [mainSearch, setMainSearch] = useState("");
103444
+ const [isViewingAll, setIsViewingAll] = useState(false);
103445
+ const [title, setTitle] = useState("");
103276
103446
  const [hasMore, setHasMore] = useState(true);
103277
103447
  const scrollRef = useRef(null);
103278
- const getSVG = async (type = null, categoryValue, pageNo = 1) => {
103448
+ const getSVG = async (type = null, categoryValue = null, searchVal = null, pageNo = 1) => {
103279
103449
  startLoading();
103280
103450
  if (pageNo === 1) {
103281
103451
  setHasMore(true);
103282
103452
  setPage(1);
103283
103453
  setCategory(categoryValue);
103454
+ setKeyType(type);
103455
+ setSearchQuery(searchVal || "");
103456
+ }
103457
+ if (categoryValue === "Recent items") {
103458
+ const q2 = (searchVal || "").toLowerCase().trim();
103459
+ const filtered = q2 ? recentItems.filter(
103460
+ (item) => item.category?.toLowerCase().includes(q2) || item.tags?.some((t2) => t2.toLowerCase().includes(q2))
103461
+ ) : recentItems;
103462
+ setElementsSVG(filtered);
103463
+ setHasMore(false);
103464
+ stopLoading();
103465
+ return;
103284
103466
  }
103285
- const request = type ? ShapeService.getAllExternal({
103286
- isVector: true,
103287
- page: pageNo,
103288
- limit: 30,
103289
- keyType: type,
103290
- query: categoryValue || null
103291
- }) : ShapeService.getAll({
103292
- category: categoryValue,
103293
- page: pageNo,
103294
- limit: 30
103295
- });
103296
- request.then((res) => {
103297
- const items = res.items;
103467
+ try {
103468
+ const request = type ? ShapeService.getAllExternal({
103469
+ isVector: true,
103470
+ page: pageNo,
103471
+ limit: 30,
103472
+ keyType: type,
103473
+ query: searchVal || categoryValue || null
103474
+ }) : ShapeService.getAll({
103475
+ category: categoryValue || null,
103476
+ search: searchVal || null,
103477
+ page: pageNo,
103478
+ limit: 30
103479
+ });
103480
+ const res = await request;
103481
+ const items = res.items || [];
103298
103482
  const newItems = items.filter((x2) => x2.status && x2.image);
103299
103483
  setElementsSVG(
103300
103484
  (prev) => pageNo === 1 ? newItems : [...prev, ...newItems]
103301
103485
  );
103302
103486
  if (newItems.length < 20) setHasMore(false);
103303
- }).catch((err) => setError(err.message)).finally(() => stopLoading());
103487
+ else setHasMore(true);
103488
+ } catch (err) {
103489
+ console.error("Failed to load elements:", err);
103490
+ if (pageNo === 1) setElementsSVG([]);
103491
+ setHasMore(false);
103492
+ } finally {
103493
+ stopLoading();
103494
+ }
103304
103495
  };
103305
103496
  useEffect(() => {
103306
103497
  const el = scrollRef.current;
103307
- if (!el) return;
103498
+ if (!el || !isViewingAll) return;
103308
103499
  const onScroll = () => {
103309
103500
  if (!hasMore || isLoading) return;
103310
103501
  if (el.scrollTop + el.clientHeight >= el.scrollHeight - 200) {
103311
- const next = page + 1;
103312
- setPage(next);
103313
- getSVG(keyType, category, next);
103502
+ setPage((prev) => {
103503
+ const next = prev + 1;
103504
+ getSVG(keyType, category, searchQuery, next);
103505
+ return next;
103506
+ });
103314
103507
  }
103315
103508
  };
103316
103509
  el.addEventListener("scroll", onScroll);
103317
103510
  return () => el.removeEventListener("scroll", onScroll);
103318
- }, [hasMore, isLoading]);
103319
- if (elementsSVG.length > 0) {
103511
+ }, [hasMore, isLoading, isViewingAll, keyType, category, searchQuery]);
103512
+ const handleMainSearch = (val) => {
103513
+ const q2 = val.trim();
103514
+ if (!q2) return;
103515
+ setIsViewingAll(true);
103516
+ setTitle(`Search: "${q2}"`);
103517
+ getSVG(null, null, q2, 1);
103518
+ };
103519
+ const handleBack = () => {
103520
+ if (searchQuery && category && category !== "Recent items") {
103521
+ setSearchQuery("");
103522
+ setPage(1);
103523
+ setTitle(category);
103524
+ getSVG(keyType, category, "", 1);
103525
+ return;
103526
+ }
103527
+ setIsViewingAll(false);
103528
+ setElementsSVG([]);
103529
+ setCategory(null);
103530
+ setKeyType(null);
103531
+ setSearchQuery("");
103532
+ setMainSearch("");
103533
+ setTitle("");
103534
+ setPage(1);
103535
+ setHasMore(true);
103536
+ };
103537
+ if (isViewingAll) {
103320
103538
  return /* @__PURE__ */ jsx(
103321
103539
  AllElements,
103322
103540
  {
103323
103541
  elements: elementsSVG,
103324
103542
  scrollRef,
103543
+ title,
103544
+ backText: title || "Elements",
103545
+ initialSearch: searchQuery,
103325
103546
  onSearch: (value) => {
103326
- setPage(1);
103327
- setHasMore(true);
103328
- getSVG(keyType, value ? value : category, 1);
103547
+ if (value.trim()) {
103548
+ setTitle(category ? `${category} - "${value.trim()}"` : `Search: "${value.trim()}"`);
103549
+ } else {
103550
+ setTitle(
103551
+ category || (keyType === "pixabay" ? "Pixabay Vectors" : keyType === "flat" ? "FlatIcon Icons" : "All Elements")
103552
+ );
103553
+ }
103554
+ getSVG(keyType, category, value, 1);
103329
103555
  },
103330
- onBack: () => {
103331
- setElementsSVG([]);
103332
- setPage(1);
103333
- setCategory(null);
103334
- setKeyType(null);
103335
- setHasMore(true);
103336
- }
103556
+ onBack: handleBack
103337
103557
  }
103338
103558
  );
103339
103559
  }
103340
103560
  return /* @__PURE__ */ jsxs(Fragment, { children: [
103561
+ /* @__PURE__ */ jsxs("div", { className: "bg-neutral-800 px-3 py-2 rounded flex items-center gap-2 mb-4 mx-4 relative focus-within:ring-1 focus-within:ring-neutral-600", children: [
103562
+ /* @__PURE__ */ jsxs(
103563
+ "svg",
103564
+ {
103565
+ width: "18",
103566
+ stroke: "currentColor",
103567
+ fill: "none",
103568
+ viewBox: "0 0 24 24",
103569
+ className: "text-gray-400 shrink-0",
103570
+ children: [
103571
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "8" }),
103572
+ /* @__PURE__ */ jsx("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" })
103573
+ ]
103574
+ }
103575
+ ),
103576
+ /* @__PURE__ */ jsx(
103577
+ "input",
103578
+ {
103579
+ type: "text",
103580
+ placeholder: "Search elements & shapes...",
103581
+ value: mainSearch,
103582
+ onChange: (e3) => setMainSearch(e3.target.value),
103583
+ onKeyDown: (e3) => {
103584
+ e3.stopPropagation();
103585
+ if (e3.key === "Enter" && mainSearch.trim()) {
103586
+ handleMainSearch(mainSearch.trim());
103587
+ }
103588
+ },
103589
+ className: "bg-transparent outline-none text-sm text-gray-200 placeholder-gray-500 w-full pr-7"
103590
+ }
103591
+ ),
103592
+ mainSearch && /* @__PURE__ */ jsx(
103593
+ "button",
103594
+ {
103595
+ type: "button",
103596
+ onClick: () => setMainSearch(""),
103597
+ className: "absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white transition-colors p-1 cursor-pointer",
103598
+ "aria-label": "Clear search",
103599
+ children: /* @__PURE__ */ jsxs(
103600
+ "svg",
103601
+ {
103602
+ width: "14",
103603
+ height: "14",
103604
+ viewBox: "0 0 24 24",
103605
+ fill: "none",
103606
+ stroke: "currentColor",
103607
+ strokeWidth: "2",
103608
+ children: [
103609
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
103610
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
103611
+ ]
103612
+ }
103613
+ )
103614
+ }
103615
+ )
103616
+ ] }),
103341
103617
  /* @__PURE__ */ jsxs("div", { className: "pb-5 mx-4", children: [
103342
103618
  /* @__PURE__ */ jsx("h2", { className: "text-md font-medium mb-3", children: "Upload From Other Media" }),
103343
103619
  /* @__PURE__ */ jsxs("div", { className: "flex gap-4", children: [
@@ -103345,11 +103621,13 @@ function ElementsPanel() {
103345
103621
  "button",
103346
103622
  {
103347
103623
  type: "button",
103348
- className: "cursor-pointer",
103624
+ className: "cursor-pointer hover:opacity-80 transition-opacity",
103349
103625
  onClick: () => {
103350
- setKeyType("pixabay");
103351
- getSVG("pixabay", "", 1);
103626
+ setIsViewingAll(true);
103627
+ setTitle("Pixabay Vectors");
103628
+ getSVG("pixabay", "", "", 1);
103352
103629
  },
103630
+ title: "Search Pixabay Vectors",
103353
103631
  children: /* @__PURE__ */ jsx(PixabayIcon, {})
103354
103632
  }
103355
103633
  ),
@@ -103357,11 +103635,13 @@ function ElementsPanel() {
103357
103635
  "button",
103358
103636
  {
103359
103637
  type: "button",
103360
- className: "cursor-pointer",
103638
+ className: "cursor-pointer hover:opacity-80 transition-opacity",
103361
103639
  onClick: () => {
103362
- setKeyType("flat");
103363
- getSVG("flat", "", 1);
103640
+ setIsViewingAll(true);
103641
+ setTitle("FlatIcon Icons");
103642
+ getSVG("flat", "", "", 1);
103364
103643
  },
103644
+ title: "Search FlatIcon Icons",
103365
103645
  children: /* @__PURE__ */ jsx(
103366
103646
  "img",
103367
103647
  {
@@ -103376,20 +103656,21 @@ function ElementsPanel() {
103376
103656
  )
103377
103657
  ] })
103378
103658
  ] }),
103379
- /* @__PURE__ */ jsx("hr", { className: "text-neutral-800 h-[0.5px]" }),
103380
- /* @__PURE__ */ jsxs("div", { className: "h-[calc(100dvh-180px)] overflow-auto custom-scrollbar", children: [
103659
+ /* @__PURE__ */ jsx("hr", { className: "text-neutral-800 h-[0.5px] border-neutral-800" }),
103660
+ /* @__PURE__ */ jsxs("div", { className: "h-[calc(100dvh-200px)] overflow-auto custom-scrollbar", children: [
103381
103661
  recentItems.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mx-4", children: [
103382
103662
  /* @__PURE__ */ jsxs("div", { className: "py-5", children: [
103383
103663
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
103384
103664
  /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold mb-3", children: "Recent items" }),
103385
103665
  recentItems.length > 3 && /* @__PURE__ */ jsx(
103386
- "div",
103666
+ "button",
103387
103667
  {
103388
- className: "underline cursor-pointer",
103668
+ type: "button",
103669
+ className: "underline text-xs text-neutral-400 hover:text-white cursor-pointer",
103389
103670
  onClick: () => {
103390
- setElementsSVG(recentItems);
103391
- setCategory("Recent items");
103392
- setHasMore(false);
103671
+ setIsViewingAll(true);
103672
+ setTitle("Recent items");
103673
+ getSVG(null, "Recent items", "", 1);
103393
103674
  },
103394
103675
  children: "View all"
103395
103676
  }
@@ -103398,36 +103679,45 @@ function ElementsPanel() {
103398
103679
  /* @__PURE__ */ jsx("div", { className: "grid grid-cols-4 md:grid-cols-3 gap-3", children: recentItems.slice(0, 3).map((shape, i) => /* @__PURE__ */ jsx(
103399
103680
  "button",
103400
103681
  {
103682
+ type: "button",
103401
103683
  onClick: () => {
103402
103684
  addSVGToCanvas("svg", shape.image);
103403
103685
  addRecentItem(shape);
103686
+ if (window.innerWidth < 768) {
103687
+ useSidePanelStore.getState().setActivePanel(null);
103688
+ }
103404
103689
  },
103405
- className: "w-full cursor-pointer flex items-center justify-center",
103690
+ className: "w-full cursor-pointer flex items-center justify-center p-2 rounded-lg bg-neutral-900/40 hover:bg-neutral-800 border border-neutral-800/60 hover:border-neutral-700 transition-colors",
103406
103691
  children: /* @__PURE__ */ jsx(
103407
103692
  "img",
103408
103693
  {
103409
103694
  src: shape.image,
103410
- alt: shape.category,
103695
+ alt: shape.category || "recent element",
103411
103696
  width: 150,
103412
103697
  height: 150,
103413
- className: "w-full h-24 object-contain"
103698
+ className: "w-full h-20 object-contain"
103414
103699
  }
103415
103700
  )
103416
103701
  },
103417
103702
  shape._id || shape.image || i
103418
103703
  )) })
103419
103704
  ] }),
103420
- /* @__PURE__ */ jsx("hr", { className: "text-neutral-800 h-[0.5px]" })
103705
+ /* @__PURE__ */ jsx("hr", { className: "text-neutral-800 h-[0.5px] border-neutral-800" })
103421
103706
  ] }),
103422
103707
  elements.map((item, i) => /* @__PURE__ */ jsxs("div", { className: "mx-4", children: [
103423
103708
  /* @__PURE__ */ jsxs("div", { className: "py-5", children: [
103424
103709
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
103425
103710
  /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold mb-3", children: item.category }),
103426
103711
  /* @__PURE__ */ jsx(
103427
- "div",
103712
+ "button",
103428
103713
  {
103429
- className: "underline cursor-pointer",
103430
- onClick: () => getSVG(null, item?.category, 1),
103714
+ type: "button",
103715
+ className: "underline text-xs text-neutral-400 hover:text-white cursor-pointer",
103716
+ onClick: () => {
103717
+ setIsViewingAll(true);
103718
+ setTitle(item.category);
103719
+ getSVG(null, item.category, "", 1);
103720
+ },
103431
103721
  children: "View all"
103432
103722
  }
103433
103723
  )
@@ -103435,6 +103725,7 @@ function ElementsPanel() {
103435
103725
  /* @__PURE__ */ jsx("div", { className: "grid grid-cols-4 md:grid-cols-3 gap-3", children: item.data.map((shape, j2) => /* @__PURE__ */ jsx(
103436
103726
  "button",
103437
103727
  {
103728
+ type: "button",
103438
103729
  onClick: () => {
103439
103730
  addSVGToCanvas("svg", shape.image);
103440
103731
  addRecentItem({
@@ -103445,8 +103736,11 @@ function ElementsPanel() {
103445
103736
  largeImage: shape.image,
103446
103737
  tags: []
103447
103738
  });
103739
+ if (window.innerWidth < 768) {
103740
+ useSidePanelStore.getState().setActivePanel(null);
103741
+ }
103448
103742
  },
103449
- className: "w-full cursor-pointer flex items-center justify-center",
103743
+ className: "w-full cursor-pointer flex items-center justify-center p-2 rounded-lg bg-neutral-900/40 hover:bg-neutral-800 border border-neutral-800/60 hover:border-neutral-700 transition-colors",
103450
103744
  children: /* @__PURE__ */ jsx(
103451
103745
  "img",
103452
103746
  {
@@ -103454,14 +103748,15 @@ function ElementsPanel() {
103454
103748
  alt: item.category,
103455
103749
  width: 150,
103456
103750
  height: 150,
103457
- className: "w-full h-24 object-contain"
103751
+ className: "w-full h-20 object-contain",
103752
+ loading: "lazy"
103458
103753
  }
103459
103754
  )
103460
103755
  },
103461
103756
  j2
103462
103757
  )) })
103463
103758
  ] }),
103464
- /* @__PURE__ */ jsx("hr", { className: "text-neutral-800 h-[0.5px]" })
103759
+ /* @__PURE__ */ jsx("hr", { className: "text-neutral-800 h-[0.5px] border-neutral-800" })
103465
103760
  ] }, i))
103466
103761
  ] })
103467
103762
  ] });
@@ -158363,6 +158658,20 @@ function DuplicateButton() {
158363
158658
  const duplicate = async () => {
158364
158659
  const active = canvas2.getActiveObject();
158365
158660
  if (!active) return;
158661
+ const currentSide = useHistoryStore.getState().currentPage;
158662
+ const ensureTextLoaded = async (obj) => {
158663
+ if (obj.type === "textbox" || obj.type === "text" || obj.type === "i-text") {
158664
+ if (obj.fontFamily) {
158665
+ try {
158666
+ await document.fonts.load(`16px "${obj.fontFamily}"`);
158667
+ } catch {
158668
+ }
158669
+ }
158670
+ obj.initDimensions?.();
158671
+ obj.set("dirty", true);
158672
+ }
158673
+ obj.__pageSide = currentSide;
158674
+ };
158366
158675
  if (active.type?.toLowerCase() === "activeselection") {
158367
158676
  const selection = active;
158368
158677
  const originals = selection.getObjects().filter((o2) => o2.name !== "workarea");
@@ -158370,13 +158679,14 @@ function DuplicateButton() {
158370
158679
  canvas2.discardActiveObject();
158371
158680
  const clones = await Promise.all(
158372
158681
  originals.map(async (obj) => {
158373
- const cloned2 = await obj.clone();
158682
+ const cloned2 = await obj.clone(HISTORY_CUSTOM_PROPERTIES);
158374
158683
  cloned2.set({
158375
158684
  left: (obj.left ?? 0) + OFFSET$1,
158376
158685
  top: (obj.top ?? 0) + OFFSET$1,
158377
158686
  selectable: true,
158378
158687
  evented: true
158379
158688
  });
158689
+ await ensureTextLoaded(cloned2);
158380
158690
  canvas2.add(cloned2);
158381
158691
  return cloned2;
158382
158692
  })
@@ -158389,18 +158699,21 @@ function DuplicateButton() {
158389
158699
  return;
158390
158700
  }
158391
158701
  if (active.name === "workarea") return;
158392
- const cloned = await active.clone();
158702
+ const cloned = await active.clone(HISTORY_CUSTOM_PROPERTIES);
158393
158703
  cloned.set({
158394
158704
  left: (active.left ?? 0) + OFFSET$1,
158395
158705
  top: (active.top ?? 0) + OFFSET$1,
158396
158706
  selectable: true,
158397
158707
  evented: true
158398
158708
  });
158709
+ await ensureTextLoaded(cloned);
158710
+ cloned.setCoords();
158399
158711
  canvas2.discardActiveObject();
158400
158712
  canvas2.add(cloned);
158401
158713
  canvas2.setActiveObject(cloned);
158402
158714
  bringOverlaysToFront(canvas2);
158403
158715
  canvas2.requestRenderAll();
158716
+ saveState();
158404
158717
  };
158405
158718
  return /* @__PURE__ */ jsx(
158406
158719
  "button",
@@ -160320,6 +160633,175 @@ function createWorkarea(canvas2, container, productData, pageSide) {
160320
160633
  createOverlays(canvas2, workarea, currentSide);
160321
160634
  return workarea;
160322
160635
  }
160636
+ const SNAP_THRESHOLD_PX = 7;
160637
+ const GUIDELINE_COLOR = "#28a128";
160638
+ const GUIDELINE_WIDTH = 1.2;
160639
+ let activeVLine = null;
160640
+ let activeHLine = null;
160641
+ function clearAlignmentGuidelines(canvas2) {
160642
+ if (!canvas2) return;
160643
+ let changed = false;
160644
+ if (activeVLine) {
160645
+ canvas2.remove(activeVLine);
160646
+ activeVLine = null;
160647
+ changed = true;
160648
+ }
160649
+ if (activeHLine) {
160650
+ canvas2.remove(activeHLine);
160651
+ activeHLine = null;
160652
+ changed = true;
160653
+ }
160654
+ canvas2.getObjects().forEach((o2) => {
160655
+ if (o2.name === "guideline-alignment-v" || o2.name === "guideline-alignment-h") {
160656
+ canvas2.remove(o2);
160657
+ changed = true;
160658
+ }
160659
+ });
160660
+ if (changed) {
160661
+ canvas2.requestRenderAll();
160662
+ }
160663
+ }
160664
+ function handleObjectAlignmentSnap(canvas2, target) {
160665
+ if (!canvas2 || !target) return;
160666
+ const targetName = target.name || "";
160667
+ if (targetName === "workarea" || targetName === "bg-image" || targetName === "canvas-grid" || targetName.startsWith("overlay-") || targetName.startsWith("guideline-")) {
160668
+ return;
160669
+ }
160670
+ const workarea = canvas2.getObjects().find((o2) => o2.name === "workarea");
160671
+ if (!workarea) return;
160672
+ const zoom = canvas2.getZoom() || 1;
160673
+ const threshold = SNAP_THRESHOLD_PX / zoom;
160674
+ const waL = workarea.left ?? 0;
160675
+ const waT = workarea.top ?? 0;
160676
+ const waW = workarea.getScaledWidth();
160677
+ const waH = workarea.getScaledHeight();
160678
+ const waCenterX = waL + waW / 2;
160679
+ const waCenterY = waT + waH / 2;
160680
+ const targetCenter = target.getCenterPoint();
160681
+ target.getBoundingRect();
160682
+ const curLeft = target.left ?? 0;
160683
+ target.top ?? 0;
160684
+ let snappedX = false;
160685
+ let snappedY = false;
160686
+ let snapLineX = waCenterX;
160687
+ let snapLineY = waCenterY;
160688
+ const distCenterX = Math.abs(targetCenter.x - waCenterX);
160689
+ if (distCenterX <= threshold) {
160690
+ const diffX = waCenterX - targetCenter.x;
160691
+ target.set("left", curLeft + diffX);
160692
+ target.setCoords();
160693
+ snappedX = true;
160694
+ snapLineX = waCenterX;
160695
+ } else {
160696
+ const otherObjects = canvas2.getObjects().filter(
160697
+ (o2) => o2 !== target && o2.selectable !== false && o2.name !== "workarea" && o2.name !== "bg-image" && !o2.name?.startsWith("overlay-") && !o2.name?.startsWith("guideline-") && o2.name !== "canvas-grid"
160698
+ );
160699
+ for (const other of otherObjects) {
160700
+ const otherCenter = other.getCenterPoint();
160701
+ if (Math.abs(targetCenter.x - otherCenter.x) <= threshold) {
160702
+ const diffX = otherCenter.x - targetCenter.x;
160703
+ target.set("left", curLeft + diffX);
160704
+ target.setCoords();
160705
+ snappedX = true;
160706
+ snapLineX = otherCenter.x;
160707
+ break;
160708
+ }
160709
+ }
160710
+ }
160711
+ const updatedCenter = target.getCenterPoint();
160712
+ const distCenterY = Math.abs(updatedCenter.y - waCenterY);
160713
+ if (distCenterY <= threshold) {
160714
+ const diffY = waCenterY - updatedCenter.y;
160715
+ target.set("top", (target.top ?? 0) + diffY);
160716
+ target.setCoords();
160717
+ snappedY = true;
160718
+ snapLineY = waCenterY;
160719
+ } else {
160720
+ const otherObjects = canvas2.getObjects().filter(
160721
+ (o2) => o2 !== target && o2.selectable !== false && o2.name !== "workarea" && o2.name !== "bg-image" && !o2.name?.startsWith("overlay-") && !o2.name?.startsWith("guideline-") && o2.name !== "canvas-grid"
160722
+ );
160723
+ for (const other of otherObjects) {
160724
+ const otherCenter = other.getCenterPoint();
160725
+ if (Math.abs(updatedCenter.y - otherCenter.y) <= threshold) {
160726
+ const diffY = otherCenter.y - updatedCenter.y;
160727
+ target.set("top", (target.top ?? 0) + diffY);
160728
+ target.setCoords();
160729
+ snappedY = true;
160730
+ snapLineY = otherCenter.y;
160731
+ break;
160732
+ }
160733
+ }
160734
+ }
160735
+ if (snappedX) {
160736
+ const y1 = waT;
160737
+ const y2 = waT + waH;
160738
+ if (!activeVLine) {
160739
+ activeVLine = new vo([snapLineX, y1, snapLineX, y2], {
160740
+ name: "guideline-alignment-v",
160741
+ stroke: GUIDELINE_COLOR,
160742
+ strokeWidth: GUIDELINE_WIDTH / zoom,
160743
+ strokeUniform: true,
160744
+ selectable: false,
160745
+ evented: false,
160746
+ excludeFromExport: true,
160747
+ hasControls: false,
160748
+ hasBorders: false,
160749
+ lockMovementX: true,
160750
+ lockMovementY: true
160751
+ });
160752
+ canvas2.add(activeVLine);
160753
+ } else {
160754
+ activeVLine.set({
160755
+ x1: snapLineX,
160756
+ y1,
160757
+ x2: snapLineX,
160758
+ y2,
160759
+ strokeWidth: GUIDELINE_WIDTH / zoom,
160760
+ visible: true
160761
+ });
160762
+ activeVLine.setCoords();
160763
+ }
160764
+ canvas2.bringObjectToFront(activeVLine);
160765
+ } else if (activeVLine) {
160766
+ canvas2.remove(activeVLine);
160767
+ activeVLine = null;
160768
+ }
160769
+ if (snappedY) {
160770
+ const x1 = waL;
160771
+ const x2 = waL + waW;
160772
+ if (!activeHLine) {
160773
+ activeHLine = new vo([x1, snapLineY, x2, snapLineY], {
160774
+ name: "guideline-alignment-h",
160775
+ stroke: GUIDELINE_COLOR,
160776
+ strokeWidth: GUIDELINE_WIDTH / zoom,
160777
+ strokeUniform: true,
160778
+ selectable: false,
160779
+ evented: false,
160780
+ excludeFromExport: true,
160781
+ hasControls: false,
160782
+ hasBorders: false,
160783
+ lockMovementX: true,
160784
+ lockMovementY: true
160785
+ });
160786
+ canvas2.add(activeHLine);
160787
+ } else {
160788
+ activeHLine.set({
160789
+ x1,
160790
+ y1: snapLineY,
160791
+ x2,
160792
+ y2: snapLineY,
160793
+ strokeWidth: GUIDELINE_WIDTH / zoom,
160794
+ visible: true
160795
+ });
160796
+ activeHLine.setCoords();
160797
+ }
160798
+ canvas2.bringObjectToFront(activeHLine);
160799
+ } else if (activeHLine) {
160800
+ canvas2.remove(activeHLine);
160801
+ activeHLine = null;
160802
+ }
160803
+ canvas2.requestRenderAll();
160804
+ }
160323
160805
  function setupHistoryRecording(canvas2) {
160324
160806
  let modifiedTimer = null;
160325
160807
  const handleObjectAddedOrRemoved = (e3) => {
@@ -160482,13 +160964,21 @@ function setupSelectionHandlers(canvas2, setSelection, clearSelection) {
160482
160964
  configureSelectionControls();
160483
160965
  bringOverlaysToFront(canvas2);
160484
160966
  };
160485
- const handleObjectMoving = () => {
160967
+ const handleObjectMoving = (e3) => {
160968
+ if (e3.target) {
160969
+ handleObjectAlignmentSnap(canvas2, e3.target);
160970
+ }
160486
160971
  bringOverlaysToFront(canvas2);
160487
160972
  };
160973
+ const handleMovementEnd = (e3) => {
160974
+ clearAlignmentGuidelines(canvas2);
160975
+ hideDegree(e3);
160976
+ };
160488
160977
  canvas2.on("object:moving", handleObjectMoving);
160489
- canvas2.on("object:modified", hideDegree);
160490
- canvas2.on("mouse:up", hideDegree);
160978
+ canvas2.on("object:modified", handleMovementEnd);
160979
+ canvas2.on("mouse:up", handleMovementEnd);
160491
160980
  canvas2.on("selection:cleared", () => {
160981
+ clearAlignmentGuidelines(canvas2);
160492
160982
  const bgImg = canvas2.getObjects().find((o2) => o2.name === "bg-image");
160493
160983
  if (bgImg) {
160494
160984
  bgImg.set({
@@ -160506,13 +160996,14 @@ function setupSelectionHandlers(canvas2, setSelection, clearSelection) {
160506
160996
  clearSelection();
160507
160997
  });
160508
160998
  return () => {
160999
+ clearAlignmentGuidelines(canvas2);
160509
161000
  canvas2.off("selection:created");
160510
161001
  canvas2.off("selection:updated");
160511
161002
  canvas2.off("object:scaling", configureSelectionControls);
160512
161003
  canvas2.off("object:rotating");
160513
161004
  canvas2.off("object:moving", handleObjectMoving);
160514
- canvas2.off("object:modified", hideDegree);
160515
- canvas2.off("mouse:up", hideDegree);
161005
+ canvas2.off("object:modified", handleMovementEnd);
161006
+ canvas2.off("mouse:up", handleMovementEnd);
160516
161007
  canvas2.off("selection:cleared");
160517
161008
  };
160518
161009
  }
@@ -160569,6 +161060,9 @@ const CUSTOM_PROPERTIES = [
160569
161060
  "lockMovementY",
160570
161061
  "rx",
160571
161062
  "ry",
161063
+ "src",
161064
+ "crossOrigin",
161065
+ "__maskPath",
160572
161066
  "__radiusOfCanvas",
160573
161067
  "__shapeType",
160574
161068
  "__pageSide",
@@ -160579,10 +161073,28 @@ const CUSTOM_PROPERTIES = [
160579
161073
  "__initialZoom",
160580
161074
  "__borderRect",
160581
161075
  "__rotationControlSize",
160582
- "__showDegree"
161076
+ "__showDegree",
161077
+ "textAlign",
161078
+ "fontFamily",
161079
+ "fontSize",
161080
+ "fontWeight",
161081
+ "fontStyle",
161082
+ "underline",
161083
+ "linethrough",
161084
+ "overline",
161085
+ "fill",
161086
+ "stroke",
161087
+ "strokeWidth",
161088
+ "lineHeight",
161089
+ "charSpacing",
161090
+ "styles",
161091
+ "paintFirst",
161092
+ "strokeUniform",
161093
+ "objectCaching"
160583
161094
  ];
160584
161095
  async function switchCanvasPage(canvas2, currentPage, targetPage, pageJSON, clearSelection, containerRef) {
160585
161096
  if (targetPage === currentPage) return;
161097
+ clearAlignmentGuidelines(canvas2);
160586
161098
  const historyStore = useHistoryStore.getState();
160587
161099
  useLoaderStore.getState().startLoading();
160588
161100
  try {
@@ -161015,8 +161527,9 @@ function ZoomControls({ onReset }) {
161015
161527
  ] }) });
161016
161528
  }
161017
161529
  const OFFSET = 20;
161530
+ let globalClipboard = null;
161531
+ let clipboardSourcePage = null;
161018
161532
  function useKeyboardEvents(canvasParam) {
161019
- const clipboardRef = useRef(null);
161020
161533
  const storeCanvas = useEditorStore((s2) => s2.canvas);
161021
161534
  const historyCanvas = useHistoryStore((s2) => s2.canvas);
161022
161535
  const canvas2 = canvasParam || storeCanvas || historyCanvas;
@@ -161028,30 +161541,56 @@ function useKeyboardEvents(canvasParam) {
161028
161541
  if (!activeCanvas) return;
161029
161542
  const activeObject = activeCanvas.getActiveObject();
161030
161543
  if (!activeObject || activeObject.name === "workarea") return;
161031
- const cloned = await activeObject.clone();
161032
- clipboardRef.current = cloned;
161544
+ const cloned = await activeObject.clone(HISTORY_CUSTOM_PROPERTIES);
161545
+ globalClipboard = cloned;
161546
+ clipboardSourcePage = useHistoryStore.getState().currentPage;
161033
161547
  };
161034
161548
  const handlePaste = async () => {
161035
161549
  const activeCanvas = getActiveCanvas();
161036
- if (!activeCanvas || !clipboardRef.current) return;
161037
- const clonedObj = await clipboardRef.current.clone();
161550
+ if (!activeCanvas || !globalClipboard) return;
161551
+ const currentSide = useHistoryStore.getState().currentPage;
161552
+ const isCrossPagePaste = clipboardSourcePage && clipboardSourcePage !== currentSide;
161553
+ const clonedObj = await globalClipboard.clone(HISTORY_CUSTOM_PROPERTIES);
161038
161554
  activeCanvas.discardActiveObject();
161555
+ const offset = isCrossPagePaste ? 0 : OFFSET;
161039
161556
  clonedObj.set({
161040
- left: (clonedObj.left ?? 0) + OFFSET,
161041
- top: (clonedObj.top ?? 0) + OFFSET,
161042
- evented: true
161557
+ left: (clonedObj.left ?? 0) + offset,
161558
+ top: (clonedObj.top ?? 0) + offset,
161559
+ evented: true,
161560
+ selectable: true
161043
161561
  });
161044
- clipboardRef.current.set({
161045
- left: (clipboardRef.current.left ?? 0) + OFFSET,
161046
- top: (clipboardRef.current.top ?? 0) + OFFSET
161562
+ clonedObj.__pageSide = currentSide;
161563
+ globalClipboard.set({
161564
+ left: (globalClipboard.left ?? 0) + OFFSET,
161565
+ top: (globalClipboard.top ?? 0) + OFFSET
161047
161566
  });
161567
+ clipboardSourcePage = currentSide;
161568
+ const ensureTextLoaded = async (obj) => {
161569
+ if (obj.type === "textbox" || obj.type === "text" || obj.type === "i-text") {
161570
+ if (obj.fontFamily) {
161571
+ try {
161572
+ await document.fonts.load(`16px "${obj.fontFamily}"`);
161573
+ } catch {
161574
+ }
161575
+ }
161576
+ obj.initDimensions?.();
161577
+ obj.set("dirty", true);
161578
+ }
161579
+ };
161048
161580
  if (clonedObj.type?.toLowerCase() === "activeselection") {
161049
- clonedObj.canvas = activeCanvas;
161050
- clonedObj.forEachObject((obj) => {
161051
- activeCanvas.add(obj);
161052
- });
161053
- clonedObj.setCoords();
161581
+ const selection = clonedObj;
161582
+ selection.canvas = activeCanvas;
161583
+ await Promise.all(
161584
+ selection.getObjects().map(async (obj) => {
161585
+ await ensureTextLoaded(obj);
161586
+ obj.__pageSide = currentSide;
161587
+ activeCanvas.add(obj);
161588
+ })
161589
+ );
161590
+ selection.setCoords();
161054
161591
  } else {
161592
+ await ensureTextLoaded(clonedObj);
161593
+ clonedObj.setCoords();
161055
161594
  activeCanvas.add(clonedObj);
161056
161595
  }
161057
161596
  activeCanvas.setActiveObject(clonedObj);
@@ -161069,6 +161608,7 @@ function useKeyboardEvents(canvasParam) {
161069
161608
  const isY = e3.key?.toLowerCase() === "y" || e3.code === "KeyY";
161070
161609
  const isC = e3.key?.toLowerCase() === "c" || e3.code === "KeyC";
161071
161610
  const isV = e3.key?.toLowerCase() === "v" || e3.code === "KeyV";
161611
+ const isA = e3.key?.toLowerCase() === "a" || e3.code === "KeyA";
161072
161612
  if (isZ && !e3.shiftKey) {
161073
161613
  e3.preventDefault();
161074
161614
  useHistoryStore.getState().undo();
@@ -161089,6 +161629,22 @@ function useKeyboardEvents(canvasParam) {
161089
161629
  handlePaste();
161090
161630
  return;
161091
161631
  }
161632
+ if (isA) {
161633
+ const activeCanvas = getActiveCanvas();
161634
+ if (activeCanvas) {
161635
+ e3.preventDefault();
161636
+ const selectableObjects = activeCanvas.getObjects().filter(
161637
+ (obj) => obj.name !== "workarea" && !obj.name?.startsWith("overlay-") && !obj.name?.startsWith("guideline-") && obj.selectable !== false
161638
+ );
161639
+ if (selectableObjects.length > 0) {
161640
+ activeCanvas.discardActiveObject();
161641
+ const sel = new Qo(selectableObjects, { canvas: activeCanvas });
161642
+ activeCanvas.setActiveObject(sel);
161643
+ activeCanvas.requestRenderAll();
161644
+ }
161645
+ }
161646
+ return;
161647
+ }
161092
161648
  const isQuote = e3.key === "'" || e3.code === "Quote";
161093
161649
  if (isQuote) {
161094
161650
  e3.preventDefault();
@@ -161118,13 +161674,14 @@ function Workspace() {
161118
161674
  const drawTool = useDrawStore((s2) => s2.drawTool);
161119
161675
  const setDrawTool = useDrawStore((s2) => s2.setDrawTool);
161120
161676
  const productData = useProductStore((s2) => s2.productData);
161121
- const isTwoSided = productData?.optionsMap?.printing === "4/4";
161122
- const [currentPage, setCurrentPage] = useState("front");
161123
- const [isPanning, setIsPanning] = useState(false);
161677
+ const editorPages = useEditorStore((s2) => s2.pages);
161124
161678
  const pageJSON = useRef({
161125
161679
  front: null,
161126
161680
  back: null
161127
161681
  });
161682
+ const isTwoSided = productData?.optionsMap?.printing === "4/4" || !!pageJSON.current?.back || !!editorPages?.back;
161683
+ const [currentPage, setCurrentPage] = useState("front");
161684
+ const [isPanning, setIsPanning] = useState(false);
161128
161685
  useEffect(() => {
161129
161686
  if (!canvasRef.current || !containerRef.current) return;
161130
161687
  if (fabricRef.current) return;
@@ -161302,9 +161859,48 @@ function Workspace() {
161302
161859
  setCurrentPage(page);
161303
161860
  renderGrid(fabricRef.current);
161304
161861
  };
161862
+ const copyFrontToBack = async () => {
161863
+ if (!fabricRef.current) return;
161864
+ useLoaderStore.getState().startLoading();
161865
+ try {
161866
+ if (currentPage === "front") {
161867
+ pageJSON.current.front = fabricRef.current.toObject(CUSTOM_PROPERTIES);
161868
+ }
161869
+ let frontData = pageJSON.current.front || useEditorStore.getState().pages?.front;
161870
+ if (!frontData) {
161871
+ console.warn("No front design found to copy");
161872
+ return;
161873
+ }
161874
+ const clonedFrontJson = JSON.parse(JSON.stringify(frontData));
161875
+ await loadTemplateFontsIntoHead(null, clonedFrontJson);
161876
+ pageJSON.current.back = clonedFrontJson;
161877
+ useEditorStore.getState().setPages({
161878
+ ...useEditorStore.getState().pages,
161879
+ ...pageJSON.current,
161880
+ back: clonedFrontJson
161881
+ });
161882
+ clearSelection();
161883
+ useHistoryStore.getState().setPage("back");
161884
+ setCurrentPage("back");
161885
+ await loadPageToCanvas(fabricRef.current, clonedFrontJson, "back");
161886
+ const workarea = fabricRef.current.getObjects().find((obj) => obj.name === "workarea");
161887
+ if (workarea) {
161888
+ workarea.__pageSide = "back";
161889
+ createOverlays(fabricRef.current, workarea, "back");
161890
+ bringOverlaysToFront(fabricRef.current);
161891
+ }
161892
+ fabricRef.current.requestRenderAll();
161893
+ renderGrid(fabricRef.current);
161894
+ useHistoryStore.getState().saveState();
161895
+ } catch (err) {
161896
+ console.error("Failed to copy front design to back:", err);
161897
+ } finally {
161898
+ useLoaderStore.getState().stopLoading();
161899
+ }
161900
+ };
161305
161901
  return /* @__PURE__ */ jsxs(Fragment, { children: [
161306
161902
  /* @__PURE__ */ jsxs("div", { className: "absolute bottom-6 z-50 flex items-center bg-white/80 backdrop-blur-xl rounded-2xl shadow-[0_8px_32px_rgba(0,0,0,0.12)] px-1.5 py-1.5 border border-white/40 ring-1 ring-black/5", children: [
161307
- /* @__PURE__ */ jsxs("div", { className: "flex bg-gray-200/50 p-1 rounded-xl", children: [
161903
+ /* @__PURE__ */ jsxs("div", { className: "flex bg-gray-200/50 p-1 rounded-xl items-center gap-1", children: [
161308
161904
  /* @__PURE__ */ jsx(
161309
161905
  "button",
161310
161906
  {
@@ -161320,6 +161916,37 @@ function Workspace() {
161320
161916
  className: `px-4 py-2 rounded-lg text-[11px] font-bold tracking-wider transition-all duration-200 cursor-pointer ${currentPage === "back" ? "bg-white text-[#02bc71] shadow-sm" : "text-gray-500 hover:text-gray-700"}`,
161321
161917
  children: "BACK"
161322
161918
  }
161919
+ ),
161920
+ isTwoSided && /* @__PURE__ */ jsxs(
161921
+ "button",
161922
+ {
161923
+ id: "copy-front-to-back-btn",
161924
+ type: "button",
161925
+ onClick: copyFrontToBack,
161926
+ title: "Copy front design to back design",
161927
+ className: "flex items-center gap-1.5 px-3 py-1.5 ml-1 rounded-lg text-[10px] font-bold tracking-wider bg-white/90 hover:bg-white text-gray-700 hover:text-[#02bc71] border border-gray-300/60 shadow-xs transition-all duration-150 cursor-pointer active:scale-95",
161928
+ children: [
161929
+ /* @__PURE__ */ jsxs(
161930
+ "svg",
161931
+ {
161932
+ width: "13",
161933
+ height: "13",
161934
+ viewBox: "0 0 24 24",
161935
+ fill: "none",
161936
+ stroke: "currentColor",
161937
+ strokeWidth: "2.2",
161938
+ strokeLinecap: "round",
161939
+ strokeLinejoin: "round",
161940
+ className: "text-[#02bc71]",
161941
+ children: [
161942
+ /* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
161943
+ /* @__PURE__ */ jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
161944
+ ]
161945
+ }
161946
+ ),
161947
+ /* @__PURE__ */ jsx("span", { children: "COPY FRONT TO BACK" })
161948
+ ]
161949
+ }
161323
161950
  )
161324
161951
  ] }),
161325
161952
  /* @__PURE__ */ jsx("div", { className: "w-[1px] h-6 bg-gray-300/50 mx-3" }),