@nice2dev/icons 1.3.15 → 1.3.16

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.
Files changed (140) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/cjs/NiceDesktopIconExporter.js +688 -599
  3. package/dist/cjs/NiceFaviconGenerator.js +603 -417
  4. package/dist/cjs/NiceIconPicker.js +788 -508
  5. package/dist/cjs/NiceIconPreview.js +246 -188
  6. package/dist/cjs/NiceIconSetCreator.js +683 -552
  7. package/dist/cjs/NiceIconStacker.js +547 -171
  8. package/dist/cjs/NiceIconValue.js +27 -35
  9. package/dist/cjs/NtdIconBadge.js +384 -360
  10. package/dist/cjs/NtdIconMorph.js +419 -354
  11. package/dist/cjs/NtdStateMorph.js +150 -147
  12. package/dist/cjs/advancedIconSearch.js +373 -510
  13. package/dist/cjs/animationControls.js +518 -642
  14. package/dist/cjs/client.js +12 -15
  15. package/dist/cjs/createIcon.js +349 -251
  16. package/dist/cjs/design-tokens.js +324 -390
  17. package/dist/cjs/figmaExport.js +162 -257
  18. package/dist/cjs/figmaSync.js +234 -316
  19. package/dist/cjs/headless-ui.js +240 -368
  20. package/dist/cjs/iconAnalytics.js +337 -393
  21. package/dist/cjs/iconHtml.js +64 -86
  22. package/dist/cjs/iconMetadata.js +392 -472
  23. package/dist/cjs/iconParts.js +120 -90
  24. package/dist/cjs/iconStack.js +136 -155
  25. package/dist/cjs/iconStyles.js +1609 -504
  26. package/dist/cjs/iconValue.js +359 -430
  27. package/dist/cjs/icons/accessibility.js +1119 -134
  28. package/dist/cjs/icons/actions.js +1290 -330
  29. package/dist/cjs/icons/ai.js +1424 -144
  30. package/dist/cjs/icons/brand.js +1344 -172
  31. package/dist/cjs/icons/business.js +903 -193
  32. package/dist/cjs/icons/devtech.js +1656 -227
  33. package/dist/cjs/icons/ecommerce.js +802 -111
  34. package/dist/cjs/icons/education.js +1345 -199
  35. package/dist/cjs/icons/files.js +1597 -328
  36. package/dist/cjs/icons/fintech.js +793 -104
  37. package/dist/cjs/icons/food.js +1507 -153
  38. package/dist/cjs/icons/fun.js +786 -109
  39. package/dist/cjs/icons/gaming.js +916 -170
  40. package/dist/cjs/icons/health.js +623 -112
  41. package/dist/cjs/icons/index.js +32 -35
  42. package/dist/cjs/icons/input.js +282 -83
  43. package/dist/cjs/icons/legal.js +1065 -122
  44. package/dist/cjs/icons/media.js +1061 -267
  45. package/dist/cjs/icons/navigation.js +387 -122
  46. package/dist/cjs/icons/omniverk.js +747 -90
  47. package/dist/cjs/icons/realestate.js +1520 -193
  48. package/dist/cjs/icons/saas.js +867 -98
  49. package/dist/cjs/icons/science.js +1451 -202
  50. package/dist/cjs/icons/shapes.js +1316 -308
  51. package/dist/cjs/icons/smarthome.js +786 -90
  52. package/dist/cjs/icons/social.js +591 -59
  53. package/dist/cjs/icons/sports.js +1225 -133
  54. package/dist/cjs/icons/sustainability.js +1211 -139
  55. package/dist/cjs/icons/travel.js +1470 -148
  56. package/dist/cjs/icons/ui.js +1508 -315
  57. package/dist/cjs/icons/weather.js +1160 -173
  58. package/dist/cjs/index.js +566 -568
  59. package/dist/cjs/lottieIntegration.js +261 -266
  60. package/dist/cjs/nicetodev-icons.css +1 -1
  61. package/dist/cjs/particleEffects.js +270 -251
  62. package/dist/cjs/resolver.js +229 -284
  63. package/dist/cjs/rsc.js +175 -193
  64. package/dist/cjs/sharpen.js +73 -130
  65. package/dist/cjs/smilAnimations.js +371 -165
  66. package/dist/cjs/stacking.js +87 -122
  67. package/dist/cjs/tailwind-plugin.js +280 -320
  68. package/dist/cjs/types.js +46 -72
  69. package/dist/cjs/utilities.js +163 -324
  70. package/dist/cjs/variantProps.js +12 -52
  71. package/dist/esm/NiceDesktopIconExporter.js +671 -598
  72. package/dist/esm/NiceFaviconGenerator.js +583 -416
  73. package/dist/esm/NiceIconPicker.js +789 -507
  74. package/dist/esm/NiceIconPreview.js +247 -187
  75. package/dist/esm/NiceIconSetCreator.js +685 -551
  76. package/dist/esm/NiceIconStacker.js +549 -170
  77. package/dist/esm/NiceIconValue.js +29 -34
  78. package/dist/esm/NtdIconBadge.js +388 -359
  79. package/dist/esm/NtdIconMorph.js +423 -353
  80. package/dist/esm/NtdStateMorph.js +152 -146
  81. package/dist/esm/advancedIconSearch.js +391 -509
  82. package/dist/esm/animationControls.js +527 -641
  83. package/dist/esm/client.js +48 -10
  84. package/dist/esm/createIcon.js +350 -250
  85. package/dist/esm/design-tokens.js +334 -387
  86. package/dist/esm/figmaExport.js +170 -256
  87. package/dist/esm/figmaSync.js +237 -315
  88. package/dist/esm/headless-ui.js +254 -365
  89. package/dist/esm/iconAnalytics.js +342 -392
  90. package/dist/esm/iconHtml.js +67 -85
  91. package/dist/esm/iconMetadata.js +404 -471
  92. package/dist/esm/iconParts.js +125 -89
  93. package/dist/esm/iconStack.js +145 -154
  94. package/dist/esm/iconStyles.js +1620 -503
  95. package/dist/esm/iconValue.js +361 -429
  96. package/dist/esm/icons/accessibility.js +1133 -133
  97. package/dist/esm/icons/actions.js +1320 -329
  98. package/dist/esm/icons/ai.js +1441 -143
  99. package/dist/esm/icons/brand.js +1351 -171
  100. package/dist/esm/icons/business.js +931 -201
  101. package/dist/esm/icons/devtech.js +1687 -226
  102. package/dist/esm/icons/ecommerce.js +816 -110
  103. package/dist/esm/icons/education.js +1364 -198
  104. package/dist/esm/icons/files.js +1628 -327
  105. package/dist/esm/icons/fintech.js +807 -103
  106. package/dist/esm/icons/food.js +1524 -152
  107. package/dist/esm/icons/fun.js +801 -108
  108. package/dist/esm/icons/gaming.js +933 -169
  109. package/dist/esm/icons/health.js +636 -111
  110. package/dist/esm/icons/index.js +596 -32
  111. package/dist/esm/icons/input.js +326 -82
  112. package/dist/esm/icons/legal.js +1079 -121
  113. package/dist/esm/icons/media.js +1086 -266
  114. package/dist/esm/icons/navigation.js +401 -121
  115. package/dist/esm/icons/omniverk.js +759 -89
  116. package/dist/esm/icons/realestate.js +1544 -198
  117. package/dist/esm/icons/saas.js +880 -97
  118. package/dist/esm/icons/science.js +1477 -208
  119. package/dist/esm/icons/shapes.js +1343 -307
  120. package/dist/esm/icons/smarthome.js +799 -89
  121. package/dist/esm/icons/social.js +599 -58
  122. package/dist/esm/icons/sports.js +1240 -132
  123. package/dist/esm/icons/sustainability.js +1226 -138
  124. package/dist/esm/icons/travel.js +1486 -147
  125. package/dist/esm/icons/ui.js +1540 -314
  126. package/dist/esm/icons/weather.js +1181 -172
  127. package/dist/esm/index.js +756 -58
  128. package/dist/esm/lottieIntegration.js +264 -265
  129. package/dist/esm/nicetodev-icons.css +1 -1
  130. package/dist/esm/particleEffects.js +271 -250
  131. package/dist/esm/resolver.js +231 -283
  132. package/dist/esm/rsc.js +182 -192
  133. package/dist/esm/sharpen.js +78 -129
  134. package/dist/esm/smilAnimations.js +376 -164
  135. package/dist/esm/stacking.js +99 -121
  136. package/dist/esm/tailwind-plugin.js +282 -319
  137. package/dist/esm/types.js +50 -71
  138. package/dist/esm/utilities.js +177 -321
  139. package/dist/esm/variantProps.js +23 -52
  140. package/package.json +15 -22
@@ -1,651 +1,724 @@
1
- import { jsxs, jsx } from 'react/jsx-runtime';
2
- import { useNiceTranslation } from '@nice2dev/ui-core';
3
- import { useState, useMemo, useCallback } from 'react';
4
-
5
- /* ══════════════════════════════════════════════════════════════════════════════
6
- Constants
7
- ══════════════════════════════════════════════════════════════════════════════ */
8
- /** Standard desktop icon sizes used across platforms */
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { useNiceTranslation } from "@nice2dev/ui-core";
3
+ import { useState, useMemo, useCallback } from "react";
4
+ /* empty css */
9
5
  const DESKTOP_ICON_SIZES = [
10
- { size: 16, label: '16×16', platforms: ['windows', 'linux'] },
11
- { size: 24, label: '24×24', platforms: ['linux'] },
12
- { size: 32, label: '32×32', platforms: ['windows', 'linux'] },
13
- { size: 48, label: '48×48', platforms: ['windows', 'linux'] },
14
- { size: 64, label: '64×64', platforms: ['windows', 'linux'] },
15
- { size: 128, label: '128×128', platforms: ['windows', 'macos', 'linux'] },
16
- { size: 256, label: '256×256', platforms: ['windows', 'macos', 'linux'] },
17
- { size: 512, label: '512×512', platforms: ['macos', 'linux'] },
18
- { size: 1024, label: '1024×1024', platforms: ['macos'] },
6
+ { size: 16, label: "16×16", platforms: ["windows", "linux"] },
7
+ { size: 24, label: "24×24", platforms: ["linux"] },
8
+ { size: 32, label: "32×32", platforms: ["windows", "linux"] },
9
+ { size: 48, label: "48×48", platforms: ["windows", "linux"] },
10
+ { size: 64, label: "64×64", platforms: ["windows", "linux"] },
11
+ { size: 128, label: "128×128", platforms: ["windows", "macos", "linux"] },
12
+ { size: 256, label: "256×256", platforms: ["windows", "macos", "linux"] },
13
+ { size: 512, label: "512×512", platforms: ["macos", "linux"] },
14
+ { size: 1024, label: "1024×1024", platforms: ["macos"] }
19
15
  ];
20
- /** Retina/HiDPI scale factors */
21
16
  const RETINA_VARIANTS = [
22
- { scale: 1, suffix: '' },
23
- { scale: 2, suffix: '@2x' },
24
- { scale: 3, suffix: '@3x' },
17
+ { scale: 1, suffix: "" },
18
+ { scale: 2, suffix: "@2x" },
19
+ { scale: 3, suffix: "@3x" }
25
20
  ];
26
- /** Default export configuration */
27
21
  const DEFAULT_CONFIG = {
28
- name: 'icon',
29
- includeWindows: true,
30
- includeMacOS: true,
31
- includeLinux: true,
32
- includeRetina: true,
33
- backgroundColor: undefined, // transparent
34
- padding: 0.1,
35
- fillColor: '#1e1e1e',
36
- sizes: [16, 32, 48, 64, 128, 256, 512],
22
+ name: "icon",
23
+ includeWindows: true,
24
+ includeMacOS: true,
25
+ includeLinux: true,
26
+ includeRetina: true,
27
+ backgroundColor: void 0,
28
+ // transparent
29
+ padding: 0.1,
30
+ fillColor: "#1e1e1e",
31
+ sizes: [16, 32, 48, 64, 128, 256, 512]
37
32
  };
38
- /** Linux hicolor theme icon directories */
39
33
  const LINUX_HICOLOR_DIRS = [
40
- { size: 16, dir: 'hicolor/16x16/apps' },
41
- { size: 24, dir: 'hicolor/24x24/apps' },
42
- { size: 32, dir: 'hicolor/32x32/apps' },
43
- { size: 48, dir: 'hicolor/48x48/apps' },
44
- { size: 64, dir: 'hicolor/64x64/apps' },
45
- { size: 128, dir: 'hicolor/128x128/apps' },
46
- { size: 256, dir: 'hicolor/256x256/apps' },
47
- { size: 512, dir: 'hicolor/512x512/apps' },
34
+ { size: 16, dir: "hicolor/16x16/apps" },
35
+ { size: 24, dir: "hicolor/24x24/apps" },
36
+ { size: 32, dir: "hicolor/32x32/apps" },
37
+ { size: 48, dir: "hicolor/48x48/apps" },
38
+ { size: 64, dir: "hicolor/64x64/apps" },
39
+ { size: 128, dir: "hicolor/128x128/apps" },
40
+ { size: 256, dir: "hicolor/256x256/apps" },
41
+ { size: 512, dir: "hicolor/512x512/apps" }
48
42
  ];
49
- /* ══════════════════════════════════════════════════════════════════════════════
50
- Utility Functions
51
- ══════════════════════════════════════════════════════════════════════════════ */
52
- /**
53
- * Render an icon component to a canvas at the specified size
54
- */
55
43
  async function renderIconToCanvas(IconComponent, size, fillColor, backgroundColor, padding = 0.1) {
56
- const canvas = document.createElement('canvas');
57
- canvas.width = size;
58
- canvas.height = size;
59
- const ctx = canvas.getContext('2d');
60
- // Fill background (or make transparent)
61
- if (backgroundColor) {
62
- ctx.fillStyle = backgroundColor;
63
- ctx.fillRect(0, 0, size, size);
64
- }
65
- else {
66
- ctx.clearRect(0, 0, size, size);
67
- }
68
- // Render icon to temporary container
69
- const container = document.createElement('div');
70
- const iconSize = Math.round(size * (1 - padding * 2));
71
- const offset = Math.round(size * padding);
72
- const { createRoot } = await import('react-dom/client');
73
- const root = createRoot(container);
74
- return new Promise((resolve) => {
75
- root.render(jsx(IconComponent, { size: iconSize, fill: fillColor }));
76
- // Wait for render
77
- setTimeout(() => {
78
- const svg = container.querySelector('svg');
79
- if (svg) {
80
- const svgData = new XMLSerializer().serializeToString(svg);
81
- const img = new Image();
82
- img.onload = () => {
83
- ctx.drawImage(img, offset, offset, iconSize, iconSize);
84
- root.unmount();
85
- resolve(canvas);
86
- };
87
- img.onerror = () => {
88
- root.unmount();
89
- resolve(canvas);
90
- };
91
- img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData)));
92
- }
93
- else {
94
- root.unmount();
95
- resolve(canvas);
96
- }
97
- }, 50);
98
- });
44
+ const canvas = document.createElement("canvas");
45
+ canvas.width = size;
46
+ canvas.height = size;
47
+ const ctx = canvas.getContext("2d");
48
+ if (backgroundColor) {
49
+ ctx.fillStyle = backgroundColor;
50
+ ctx.fillRect(0, 0, size, size);
51
+ } else {
52
+ ctx.clearRect(0, 0, size, size);
53
+ }
54
+ const container = document.createElement("div");
55
+ const iconSize = Math.round(size * (1 - padding * 2));
56
+ const offset = Math.round(size * padding);
57
+ const { createRoot } = await import("react-dom/client");
58
+ const root = createRoot(container);
59
+ return new Promise((resolve) => {
60
+ root.render(/* @__PURE__ */ jsx(IconComponent, { size: iconSize, fill: fillColor }));
61
+ setTimeout(() => {
62
+ const svg = container.querySelector("svg");
63
+ if (svg) {
64
+ const svgData = new XMLSerializer().serializeToString(svg);
65
+ const img = new Image();
66
+ img.onload = () => {
67
+ ctx.drawImage(img, offset, offset, iconSize, iconSize);
68
+ root.unmount();
69
+ resolve(canvas);
70
+ };
71
+ img.onerror = () => {
72
+ root.unmount();
73
+ resolve(canvas);
74
+ };
75
+ img.src = "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svgData)));
76
+ } else {
77
+ root.unmount();
78
+ resolve(canvas);
79
+ }
80
+ }, 50);
81
+ });
99
82
  }
100
- /**
101
- * Convert canvas to PNG blob
102
- */
103
83
  function canvasToBlob(canvas) {
104
- return new Promise((resolve, reject) => {
105
- canvas.toBlob((blob) => {
106
- if (blob) {
107
- resolve(blob);
108
- }
109
- else {
110
- reject(new Error('Failed to convert canvas to blob'));
111
- }
112
- }, 'image/png');
113
- });
84
+ return new Promise((resolve, reject) => {
85
+ canvas.toBlob((blob) => {
86
+ if (blob) {
87
+ resolve(blob);
88
+ } else {
89
+ reject(new Error("Failed to convert canvas to blob"));
90
+ }
91
+ }, "image/png");
92
+ });
114
93
  }
115
- /**
116
- * Create a Windows ICO file from multiple PNG images.
117
- * ICO format: https://en.wikipedia.org/wiki/ICO_(file_format)
118
- */
119
94
  async function createIcoFile(pngBuffers) {
120
- // Sort by size descending
121
- const sorted = [...pngBuffers].sort((a, b) => b.size - a.size);
122
- // ICO header: 6 bytes
123
- // Image entry: 16 bytes each
124
- const headerSize = 6;
125
- const entrySize = 16;
126
- const directorySize = headerSize + entrySize * sorted.length;
127
- // Calculate total size and offsets
128
- let currentOffset = directorySize;
129
- const entries = [];
130
- for (const png of sorted) {
131
- entries.push({ size: png.size, offset: currentOffset, data: png.data });
132
- currentOffset += png.data.byteLength;
133
- }
134
- // Create the ICO buffer
135
- const totalSize = currentOffset;
136
- const buffer = new ArrayBuffer(totalSize);
137
- const view = new DataView(buffer);
138
- const uint8 = new Uint8Array(buffer);
139
- // Write ICO header
140
- view.setUint16(0, 0, true); // Reserved (must be 0)
141
- view.setUint16(2, 1, true); // Type (1 = ICO)
142
- view.setUint16(4, sorted.length, true); // Number of images
143
- // Write image directory entries
144
- let entryOffset = headerSize;
145
- for (const entry of entries) {
146
- const size = entry.size > 255 ? 0 : entry.size; // 0 means 256
147
- view.setUint8(entryOffset + 0, size); // Width
148
- view.setUint8(entryOffset + 1, size); // Height
149
- view.setUint8(entryOffset + 2, 0); // Color palette (0 = no palette)
150
- view.setUint8(entryOffset + 3, 0); // Reserved
151
- view.setUint16(entryOffset + 4, 1, true); // Color planes
152
- view.setUint16(entryOffset + 6, 32, true); // Bits per pixel
153
- view.setUint32(entryOffset + 8, entry.data.byteLength, true); // Image data size
154
- view.setUint32(entryOffset + 12, entry.offset, true); // Image data offset
155
- entryOffset += entrySize;
156
- }
157
- // Write image data
158
- for (const entry of entries) {
159
- uint8.set(new Uint8Array(entry.data), entry.offset);
160
- }
161
- return new Blob([buffer], { type: 'image/x-icon' });
95
+ const sorted = [...pngBuffers].sort((a, b) => b.size - a.size);
96
+ const headerSize = 6;
97
+ const entrySize = 16;
98
+ const directorySize = headerSize + entrySize * sorted.length;
99
+ let currentOffset = directorySize;
100
+ const entries = [];
101
+ for (const png of sorted) {
102
+ entries.push({ size: png.size, offset: currentOffset, data: png.data });
103
+ currentOffset += png.data.byteLength;
104
+ }
105
+ const totalSize = currentOffset;
106
+ const buffer = new ArrayBuffer(totalSize);
107
+ const view = new DataView(buffer);
108
+ const uint8 = new Uint8Array(buffer);
109
+ view.setUint16(0, 0, true);
110
+ view.setUint16(2, 1, true);
111
+ view.setUint16(4, sorted.length, true);
112
+ let entryOffset = headerSize;
113
+ for (const entry of entries) {
114
+ const size = entry.size > 255 ? 0 : entry.size;
115
+ view.setUint8(entryOffset + 0, size);
116
+ view.setUint8(entryOffset + 1, size);
117
+ view.setUint8(entryOffset + 2, 0);
118
+ view.setUint8(entryOffset + 3, 0);
119
+ view.setUint16(entryOffset + 4, 1, true);
120
+ view.setUint16(entryOffset + 6, 32, true);
121
+ view.setUint32(entryOffset + 8, entry.data.byteLength, true);
122
+ view.setUint32(entryOffset + 12, entry.offset, true);
123
+ entryOffset += entrySize;
124
+ }
125
+ for (const entry of entries) {
126
+ uint8.set(new Uint8Array(entry.data), entry.offset);
127
+ }
128
+ return new Blob([buffer], { type: "image/x-icon" });
162
129
  }
163
- /**
164
- * Create a ZIP file containing multiple files
165
- */
166
130
  async function createZipFile(files) {
167
- // Simple ZIP implementation without external libraries
168
- // Uses store (no compression) method for simplicity
169
- const encoder = new TextEncoder();
170
- const fileEntries = [];
171
- let offset = 0;
172
- // Prepare file entries
173
- for (const file of files) {
174
- const nameBytes = encoder.encode(file.name);
175
- let dataBytes;
176
- if (typeof file.data === 'string') {
177
- dataBytes = encoder.encode(file.data);
178
- }
179
- else if (file.data instanceof Blob) {
180
- dataBytes = new Uint8Array(await file.data.arrayBuffer());
181
- }
182
- else {
183
- dataBytes = new Uint8Array(file.data);
184
- }
185
- const crc32 = calculateCRC32(dataBytes);
186
- fileEntries.push({ name: nameBytes, data: dataBytes, crc32, offset });
187
- // Local file header size: 30 + name length + data length
188
- offset += 30 + nameBytes.length + dataBytes.length;
189
- }
190
- const centralDirectoryOffset = offset;
191
- // Calculate central directory size
192
- let centralDirSize = 0;
193
- for (const entry of fileEntries) {
194
- centralDirSize += 46 + entry.name.length;
131
+ const encoder = new TextEncoder();
132
+ const fileEntries = [];
133
+ let offset = 0;
134
+ for (const file of files) {
135
+ const nameBytes = encoder.encode(file.name);
136
+ let dataBytes;
137
+ if (typeof file.data === "string") {
138
+ dataBytes = encoder.encode(file.data);
139
+ } else if (file.data instanceof Blob) {
140
+ dataBytes = new Uint8Array(await file.data.arrayBuffer());
141
+ } else {
142
+ dataBytes = new Uint8Array(file.data);
195
143
  }
196
- // Total size: local headers + data + central directory + end of central directory
197
- const totalSize = offset + centralDirSize + 22;
198
- const buffer = new ArrayBuffer(totalSize);
199
- const view = new DataView(buffer);
200
- const bytes = new Uint8Array(buffer);
201
- let pos = 0;
202
- // Write local file headers and data
203
- for (const entry of fileEntries) {
204
- // Local file header signature
205
- view.setUint32(pos, 0x04034b50, true);
206
- pos += 4;
207
- // Version needed
208
- view.setUint16(pos, 20, true);
209
- pos += 2;
210
- // General purpose bit flag
211
- view.setUint16(pos, 0, true);
212
- pos += 2;
213
- // Compression method (0 = store)
214
- view.setUint16(pos, 0, true);
215
- pos += 2;
216
- // File time & date (use current)
217
- const now = new Date();
218
- const dosTime = ((now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1)) & 0xffff;
219
- const dosDate = (((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate()) & 0xffff;
220
- view.setUint16(pos, dosTime, true);
221
- pos += 2;
222
- view.setUint16(pos, dosDate, true);
223
- pos += 2;
224
- // CRC-32
225
- view.setUint32(pos, entry.crc32, true);
226
- pos += 4;
227
- // Compressed size
228
- view.setUint32(pos, entry.data.length, true);
229
- pos += 4;
230
- // Uncompressed size
231
- view.setUint32(pos, entry.data.length, true);
232
- pos += 4;
233
- // File name length
234
- view.setUint16(pos, entry.name.length, true);
235
- pos += 2;
236
- // Extra field length
237
- view.setUint16(pos, 0, true);
238
- pos += 2;
239
- // File name
240
- bytes.set(entry.name, pos);
241
- pos += entry.name.length;
242
- // File data
243
- bytes.set(entry.data, pos);
244
- pos += entry.data.length;
245
- }
246
- // Write central directory
247
- for (const entry of fileEntries) {
248
- // Central directory file header signature
249
- view.setUint32(pos, 0x02014b50, true);
250
- pos += 4;
251
- // Version made by
252
- view.setUint16(pos, 20, true);
253
- pos += 2;
254
- // Version needed
255
- view.setUint16(pos, 20, true);
256
- pos += 2;
257
- // General purpose bit flag
258
- view.setUint16(pos, 0, true);
259
- pos += 2;
260
- // Compression method
261
- view.setUint16(pos, 0, true);
262
- pos += 2;
263
- // File time & date
264
- const now = new Date();
265
- const dosTime = ((now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1)) & 0xffff;
266
- const dosDate = (((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate()) & 0xffff;
267
- view.setUint16(pos, dosTime, true);
268
- pos += 2;
269
- view.setUint16(pos, dosDate, true);
270
- pos += 2;
271
- // CRC-32
272
- view.setUint32(pos, entry.crc32, true);
273
- pos += 4;
274
- // Compressed size
275
- view.setUint32(pos, entry.data.length, true);
276
- pos += 4;
277
- // Uncompressed size
278
- view.setUint32(pos, entry.data.length, true);
279
- pos += 4;
280
- // File name length
281
- view.setUint16(pos, entry.name.length, true);
282
- pos += 2;
283
- // Extra field length
284
- view.setUint16(pos, 0, true);
285
- pos += 2;
286
- // File comment length
287
- view.setUint16(pos, 0, true);
288
- pos += 2;
289
- // Disk number start
290
- view.setUint16(pos, 0, true);
291
- pos += 2;
292
- // Internal file attributes
293
- view.setUint16(pos, 0, true);
294
- pos += 2;
295
- // External file attributes
296
- view.setUint32(pos, 0, true);
297
- pos += 4;
298
- // Relative offset of local header
299
- view.setUint32(pos, entry.offset, true);
300
- pos += 4;
301
- // File name
302
- bytes.set(entry.name, pos);
303
- pos += entry.name.length;
304
- }
305
- // End of central directory record
306
- view.setUint32(pos, 0x06054b50, true);
144
+ const crc32 = calculateCRC32(dataBytes);
145
+ fileEntries.push({ name: nameBytes, data: dataBytes, crc32, offset });
146
+ offset += 30 + nameBytes.length + dataBytes.length;
147
+ }
148
+ const centralDirectoryOffset = offset;
149
+ let centralDirSize = 0;
150
+ for (const entry of fileEntries) {
151
+ centralDirSize += 46 + entry.name.length;
152
+ }
153
+ const totalSize = offset + centralDirSize + 22;
154
+ const buffer = new ArrayBuffer(totalSize);
155
+ const view = new DataView(buffer);
156
+ const bytes = new Uint8Array(buffer);
157
+ let pos = 0;
158
+ for (const entry of fileEntries) {
159
+ view.setUint32(pos, 67324752, true);
160
+ pos += 4;
161
+ view.setUint16(pos, 20, true);
162
+ pos += 2;
163
+ view.setUint16(pos, 0, true);
164
+ pos += 2;
165
+ view.setUint16(pos, 0, true);
166
+ pos += 2;
167
+ const now = /* @__PURE__ */ new Date();
168
+ const dosTime = (now.getHours() << 11 | now.getMinutes() << 5 | now.getSeconds() >> 1) & 65535;
169
+ const dosDate = (now.getFullYear() - 1980 << 9 | now.getMonth() + 1 << 5 | now.getDate()) & 65535;
170
+ view.setUint16(pos, dosTime, true);
171
+ pos += 2;
172
+ view.setUint16(pos, dosDate, true);
173
+ pos += 2;
174
+ view.setUint32(pos, entry.crc32, true);
175
+ pos += 4;
176
+ view.setUint32(pos, entry.data.length, true);
307
177
  pos += 4;
308
- // Disk number
178
+ view.setUint32(pos, entry.data.length, true);
179
+ pos += 4;
180
+ view.setUint16(pos, entry.name.length, true);
181
+ pos += 2;
309
182
  view.setUint16(pos, 0, true);
310
183
  pos += 2;
311
- // Disk number with CD
184
+ bytes.set(entry.name, pos);
185
+ pos += entry.name.length;
186
+ bytes.set(entry.data, pos);
187
+ pos += entry.data.length;
188
+ }
189
+ for (const entry of fileEntries) {
190
+ view.setUint32(pos, 33639248, true);
191
+ pos += 4;
192
+ view.setUint16(pos, 20, true);
193
+ pos += 2;
194
+ view.setUint16(pos, 20, true);
195
+ pos += 2;
312
196
  view.setUint16(pos, 0, true);
313
197
  pos += 2;
314
- // Number of entries on this disk
315
- view.setUint16(pos, fileEntries.length, true);
198
+ view.setUint16(pos, 0, true);
199
+ pos += 2;
200
+ const now = /* @__PURE__ */ new Date();
201
+ const dosTime = (now.getHours() << 11 | now.getMinutes() << 5 | now.getSeconds() >> 1) & 65535;
202
+ const dosDate = (now.getFullYear() - 1980 << 9 | now.getMonth() + 1 << 5 | now.getDate()) & 65535;
203
+ view.setUint16(pos, dosTime, true);
316
204
  pos += 2;
317
- // Total number of entries
318
- view.setUint16(pos, fileEntries.length, true);
205
+ view.setUint16(pos, dosDate, true);
319
206
  pos += 2;
320
- // Size of central directory
321
- view.setUint32(pos, centralDirSize, true);
207
+ view.setUint32(pos, entry.crc32, true);
208
+ pos += 4;
209
+ view.setUint32(pos, entry.data.length, true);
322
210
  pos += 4;
323
- // Offset of central directory
324
- view.setUint32(pos, centralDirectoryOffset, true);
211
+ view.setUint32(pos, entry.data.length, true);
325
212
  pos += 4;
326
- // Comment length
213
+ view.setUint16(pos, entry.name.length, true);
214
+ pos += 2;
215
+ view.setUint16(pos, 0, true);
216
+ pos += 2;
217
+ view.setUint16(pos, 0, true);
218
+ pos += 2;
219
+ view.setUint16(pos, 0, true);
220
+ pos += 2;
327
221
  view.setUint16(pos, 0, true);
328
- return new Blob([buffer], { type: 'application/zip' });
222
+ pos += 2;
223
+ view.setUint32(pos, 0, true);
224
+ pos += 4;
225
+ view.setUint32(pos, entry.offset, true);
226
+ pos += 4;
227
+ bytes.set(entry.name, pos);
228
+ pos += entry.name.length;
229
+ }
230
+ view.setUint32(pos, 101010256, true);
231
+ pos += 4;
232
+ view.setUint16(pos, 0, true);
233
+ pos += 2;
234
+ view.setUint16(pos, 0, true);
235
+ pos += 2;
236
+ view.setUint16(pos, fileEntries.length, true);
237
+ pos += 2;
238
+ view.setUint16(pos, fileEntries.length, true);
239
+ pos += 2;
240
+ view.setUint32(pos, centralDirSize, true);
241
+ pos += 4;
242
+ view.setUint32(pos, centralDirectoryOffset, true);
243
+ pos += 4;
244
+ view.setUint16(pos, 0, true);
245
+ return new Blob([buffer], { type: "application/zip" });
329
246
  }
330
- /**
331
- * Calculate CRC-32 for ZIP files
332
- */
333
247
  function calculateCRC32(data) {
334
- let crc = 0xffffffff;
335
- const table = getCRC32Table();
336
- for (let i = 0; i < data.length; i++) {
337
- crc = (crc >>> 8) ^ table[(crc ^ data[i]) & 0xff];
338
- }
339
- return (crc ^ 0xffffffff) >>> 0;
248
+ let crc = 4294967295;
249
+ const table = getCRC32Table();
250
+ for (let i = 0; i < data.length; i++) {
251
+ crc = crc >>> 8 ^ table[(crc ^ data[i]) & 255];
252
+ }
253
+ return (crc ^ 4294967295) >>> 0;
340
254
  }
341
255
  let crc32Table = null;
342
256
  function getCRC32Table() {
343
- if (crc32Table) {
344
- return crc32Table;
345
- }
346
- crc32Table = new Uint32Array(256);
347
- for (let i = 0; i < 256; i++) {
348
- let c = i;
349
- for (let j = 0; j < 8; j++) {
350
- c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
351
- }
352
- crc32Table[i] = c;
353
- }
257
+ if (crc32Table) {
354
258
  return crc32Table;
259
+ }
260
+ crc32Table = new Uint32Array(256);
261
+ for (let i = 0; i < 256; i++) {
262
+ let c = i;
263
+ for (let j = 0; j < 8; j++) {
264
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
265
+ }
266
+ crc32Table[i] = c;
267
+ }
268
+ return crc32Table;
355
269
  }
356
- /* ══════════════════════════════════════════════════════════════════════════════
357
- Hook
358
- ══════════════════════════════════════════════════════════════════════════════ */
359
- /**
360
- * Hook for exporting desktop icons in multiple formats and sizes.
361
- *
362
- * @example
363
- * ```tsx
364
- * const { exportAll, downloadAll, icons, isExporting } = useDesktopIconExporter(MyIcon);
365
- *
366
- * // Export all icons
367
- * await exportAll();
368
- *
369
- * // Download as ZIP
370
- * await downloadAll();
371
- * ```
372
- */
373
270
  function useDesktopIconExporter(icon, initialConfig) {
374
- const [config, setConfig] = useState({
375
- ...DEFAULT_CONFIG,
376
- ...initialConfig,
377
- });
378
- const [icons, setIcons] = useState([]);
379
- const [isExporting, setIsExporting] = useState(false);
380
- const [progress, setProgress] = useState(0);
381
- // Calculate total number of icons to export
382
- const totalIcons = useMemo(() => {
383
- let count = 0;
384
- const scales = config.includeRetina ? RETINA_VARIANTS.length : 1;
385
- for (const size of config.sizes) {
386
- if (config.includeWindows || config.includeLinux) {
387
- count += scales;
388
- }
389
- if (config.includeMacOS && size >= 128) {
390
- count += scales;
391
- }
392
- }
393
- return count;
394
- }, [config]);
395
- // Export all icons as PNGs
396
- const exportAll = useCallback(async () => {
397
- setIsExporting(true);
398
- setProgress(0);
399
- const exported = [];
400
- let completed = 0;
401
- try {
402
- const scales = config.includeRetina ? RETINA_VARIANTS : [RETINA_VARIANTS[0]];
403
- for (const size of config.sizes) {
404
- for (const variant of scales) {
405
- const actualSize = size * variant.scale;
406
- const canvas = await renderIconToCanvas(icon, actualSize, config.fillColor, config.backgroundColor, config.padding);
407
- const dataUrl = canvas.toDataURL('image/png');
408
- const blob = await canvasToBlob(canvas);
409
- // Determine platforms
410
- const platforms = [];
411
- if (config.includeWindows && size <= 256) {
412
- platforms.push('windows');
413
- }
414
- if (config.includeMacOS && size >= 128) {
415
- platforms.push('macos');
416
- }
417
- if (config.includeLinux) {
418
- platforms.push('linux');
419
- }
420
- exported.push({
421
- filename: `${config.name}-${size}x${size}${variant.suffix}.png`,
422
- size,
423
- scale: variant.scale,
424
- platform: platforms.join(','),
425
- dataUrl,
426
- blob,
427
- });
428
- completed++;
429
- setProgress((completed / totalIcons) * 100);
430
- }
431
- }
432
- setIcons(exported);
433
- }
434
- catch (error) {
435
- console.error('Failed to export icons:', error);
271
+ const [config, setConfig] = useState({
272
+ ...DEFAULT_CONFIG,
273
+ ...initialConfig
274
+ });
275
+ const [icons, setIcons] = useState([]);
276
+ const [isExporting, setIsExporting] = useState(false);
277
+ const [progress, setProgress] = useState(0);
278
+ const totalIcons = useMemo(() => {
279
+ let count = 0;
280
+ const scales = config.includeRetina ? RETINA_VARIANTS.length : 1;
281
+ for (const size of config.sizes) {
282
+ if (config.includeWindows || config.includeLinux) {
283
+ count += scales;
284
+ }
285
+ if (config.includeMacOS && size >= 128) {
286
+ count += scales;
287
+ }
288
+ }
289
+ return count;
290
+ }, [config]);
291
+ const exportAll = useCallback(async () => {
292
+ setIsExporting(true);
293
+ setProgress(0);
294
+ const exported = [];
295
+ let completed = 0;
296
+ try {
297
+ const scales = config.includeRetina ? RETINA_VARIANTS : [RETINA_VARIANTS[0]];
298
+ for (const size of config.sizes) {
299
+ for (const variant of scales) {
300
+ const actualSize = size * variant.scale;
301
+ const canvas = await renderIconToCanvas(
302
+ icon,
303
+ actualSize,
304
+ config.fillColor,
305
+ config.backgroundColor,
306
+ config.padding
307
+ );
308
+ const dataUrl = canvas.toDataURL("image/png");
309
+ const blob = await canvasToBlob(canvas);
310
+ const platforms = [];
311
+ if (config.includeWindows && size <= 256) {
312
+ platforms.push("windows");
313
+ }
314
+ if (config.includeMacOS && size >= 128) {
315
+ platforms.push("macos");
316
+ }
317
+ if (config.includeLinux) {
318
+ platforms.push("linux");
319
+ }
320
+ exported.push({
321
+ filename: `${config.name}-${size}x${size}${variant.suffix}.png`,
322
+ size,
323
+ scale: variant.scale,
324
+ platform: platforms.join(","),
325
+ dataUrl,
326
+ blob
327
+ });
328
+ completed++;
329
+ setProgress(completed / totalIcons * 100);
436
330
  }
437
- finally {
438
- setIsExporting(false);
331
+ }
332
+ setIcons(exported);
333
+ } catch (error) {
334
+ console.error("Failed to export icons:", error);
335
+ } finally {
336
+ setIsExporting(false);
337
+ }
338
+ }, [icon, config, totalIcons]);
339
+ const exportWindows = useCallback(async () => {
340
+ const windowsSizes = config.sizes.filter((s) => s <= 256);
341
+ const pngBuffers = [];
342
+ for (const size of windowsSizes) {
343
+ const canvas = await renderIconToCanvas(
344
+ icon,
345
+ size,
346
+ config.fillColor,
347
+ config.backgroundColor,
348
+ config.padding
349
+ );
350
+ const blob = await canvasToBlob(canvas);
351
+ const buffer = await blob.arrayBuffer();
352
+ pngBuffers.push({ size, data: buffer });
353
+ }
354
+ const icoBlob = await createIcoFile(pngBuffers);
355
+ return URL.createObjectURL(icoBlob);
356
+ }, [icon, config]);
357
+ const exportMacOS = useCallback(async () => {
358
+ const macSizes = config.sizes.filter((s) => s >= 128);
359
+ const files = [];
360
+ for (const size of macSizes) {
361
+ const scales = config.includeRetina ? RETINA_VARIANTS : [RETINA_VARIANTS[0]];
362
+ for (const variant of scales) {
363
+ const actualSize = size * variant.scale;
364
+ const canvas = await renderIconToCanvas(
365
+ icon,
366
+ actualSize,
367
+ config.fillColor,
368
+ config.backgroundColor,
369
+ config.padding
370
+ );
371
+ const blob = await canvasToBlob(canvas);
372
+ files.push({
373
+ name: `${config.name}.iconset/icon_${size}x${size}${variant.suffix}.png`,
374
+ data: blob
375
+ });
376
+ }
377
+ }
378
+ files.push({
379
+ name: "README.txt",
380
+ data: new Blob(
381
+ [
382
+ `macOS Icon Set
383
+ =============
384
+
385
+ To convert to .icns format, use the following command in Terminal:
386
+
387
+ iconutil -c icns ${config.name}.iconset
388
+
389
+ This will create ${config.name}.icns
390
+
391
+ Note: iconutil is only available on macOS.
392
+ `
393
+ ],
394
+ { type: "text/plain" }
395
+ )
396
+ });
397
+ const zipBlob = await createZipFile(files);
398
+ return URL.createObjectURL(zipBlob);
399
+ }, [icon, config]);
400
+ const exportLinux = useCallback(async () => {
401
+ const linuxIcons = [];
402
+ for (const { size, dir } of LINUX_HICOLOR_DIRS) {
403
+ if (!config.sizes.includes(size)) {
404
+ continue;
405
+ }
406
+ const canvas = await renderIconToCanvas(
407
+ icon,
408
+ size,
409
+ config.fillColor,
410
+ config.backgroundColor,
411
+ config.padding
412
+ );
413
+ const dataUrl = canvas.toDataURL("image/png");
414
+ const blob = await canvasToBlob(canvas);
415
+ linuxIcons.push({
416
+ filename: `${dir}/${config.name}.png`,
417
+ size,
418
+ scale: 1,
419
+ platform: "linux",
420
+ dataUrl,
421
+ blob
422
+ });
423
+ }
424
+ return linuxIcons;
425
+ }, [icon, config]);
426
+ const downloadSingle = useCallback((exportedIcon) => {
427
+ const link = document.createElement("a");
428
+ link.href = exportedIcon.dataUrl;
429
+ link.download = exportedIcon.filename;
430
+ link.click();
431
+ }, []);
432
+ const downloadAll = useCallback(async () => {
433
+ setIsExporting(true);
434
+ setProgress(0);
435
+ try {
436
+ const files = [];
437
+ await exportAll();
438
+ for (const exportedIcon of icons) {
439
+ if (exportedIcon.blob) {
440
+ files.push({ name: `png/${exportedIcon.filename}`, data: exportedIcon.blob });
439
441
  }
440
- }, [icon, config, totalIcons]);
441
- // Export Windows ICO file
442
- const exportWindows = useCallback(async () => {
442
+ }
443
+ if (config.includeWindows) {
443
444
  const windowsSizes = config.sizes.filter((s) => s <= 256);
444
445
  const pngBuffers = [];
445
446
  for (const size of windowsSizes) {
446
- const canvas = await renderIconToCanvas(icon, size, config.fillColor, config.backgroundColor, config.padding);
447
- const blob = await canvasToBlob(canvas);
448
- const buffer = await blob.arrayBuffer();
449
- pngBuffers.push({ size, data: buffer });
447
+ const canvas = await renderIconToCanvas(
448
+ icon,
449
+ size,
450
+ config.fillColor,
451
+ config.backgroundColor,
452
+ config.padding
453
+ );
454
+ const blob = await canvasToBlob(canvas);
455
+ pngBuffers.push({ size, data: await blob.arrayBuffer() });
450
456
  }
451
457
  const icoBlob = await createIcoFile(pngBuffers);
452
- return URL.createObjectURL(icoBlob);
453
- }, [icon, config]);
454
- // Export macOS icons (as ZIP with PNG set - ICNS requires native tools)
455
- const exportMacOS = useCallback(async () => {
458
+ files.push({ name: `windows/${config.name}.ico`, data: icoBlob });
459
+ }
460
+ if (config.includeMacOS) {
456
461
  const macSizes = config.sizes.filter((s) => s >= 128);
457
- const files = [];
462
+ const scales = config.includeRetina ? RETINA_VARIANTS : [RETINA_VARIANTS[0]];
458
463
  for (const size of macSizes) {
459
- const scales = config.includeRetina ? RETINA_VARIANTS : [RETINA_VARIANTS[0]];
460
- for (const variant of scales) {
461
- const actualSize = size * variant.scale;
462
- const canvas = await renderIconToCanvas(icon, actualSize, config.fillColor, config.backgroundColor, config.padding);
463
- const blob = await canvasToBlob(canvas);
464
- // macOS naming convention: icon_128x128@2x.png
465
- files.push({
466
- name: `${config.name}.iconset/icon_${size}x${size}${variant.suffix}.png`,
467
- data: blob,
468
- });
469
- }
464
+ for (const variant of scales) {
465
+ const actualSize = size * variant.scale;
466
+ const canvas = await renderIconToCanvas(
467
+ icon,
468
+ actualSize,
469
+ config.fillColor,
470
+ config.backgroundColor,
471
+ config.padding
472
+ );
473
+ const blob = await canvasToBlob(canvas);
474
+ files.push({
475
+ name: `macos/${config.name}.iconset/icon_${size}x${size}${variant.suffix}.png`,
476
+ data: blob
477
+ });
478
+ }
470
479
  }
471
- // Add README with iconutil instructions
472
480
  files.push({
473
- name: 'README.txt',
474
- data: new Blob([
475
- `macOS Icon Set
476
- =============
477
-
478
- To convert to .icns format, use the following command in Terminal:
479
-
480
- iconutil -c icns ${config.name}.iconset
481
-
482
- This will create ${config.name}.icns
483
-
484
- Note: iconutil is only available on macOS.
485
- `,
486
- ], { type: 'text/plain' }),
481
+ name: "macos/README.txt",
482
+ data: `To convert to .icns: iconutil -c icns ${config.name}.iconset`
487
483
  });
488
- const zipBlob = await createZipFile(files);
489
- return URL.createObjectURL(zipBlob);
490
- }, [icon, config]);
491
- // Export Linux PNG set
492
- const exportLinux = useCallback(async () => {
493
- const linuxIcons = [];
484
+ }
485
+ if (config.includeLinux) {
494
486
  for (const { size, dir } of LINUX_HICOLOR_DIRS) {
495
- if (!config.sizes.includes(size)) {
496
- continue;
497
- }
498
- const canvas = await renderIconToCanvas(icon, size, config.fillColor, config.backgroundColor, config.padding);
499
- const dataUrl = canvas.toDataURL('image/png');
500
- const blob = await canvasToBlob(canvas);
501
- linuxIcons.push({
502
- filename: `${dir}/${config.name}.png`,
503
- size,
504
- scale: 1,
505
- platform: 'linux',
506
- dataUrl,
507
- blob,
508
- });
487
+ if (!config.sizes.includes(size)) {
488
+ continue;
489
+ }
490
+ const canvas = await renderIconToCanvas(
491
+ icon,
492
+ size,
493
+ config.fillColor,
494
+ config.backgroundColor,
495
+ config.padding
496
+ );
497
+ const blob = await canvasToBlob(canvas);
498
+ files.push({ name: `linux/${dir}/${config.name}.png`, data: blob });
509
499
  }
510
- return linuxIcons;
511
- }, [icon, config]);
512
- // Download single icon
513
- const downloadSingle = useCallback((exportedIcon) => {
514
- const link = document.createElement('a');
515
- link.href = exportedIcon.dataUrl;
516
- link.download = exportedIcon.filename;
517
- link.click();
518
- }, []);
519
- // Download all as ZIP
520
- const downloadAll = useCallback(async () => {
521
- setIsExporting(true);
522
- setProgress(0);
523
- try {
524
- const files = [];
525
- // Export all PNG icons first
526
- await exportAll();
527
- // Add PNG icons
528
- for (const exportedIcon of icons) {
529
- if (exportedIcon.blob) {
530
- files.push({ name: `png/${exportedIcon.filename}`, data: exportedIcon.blob });
531
- }
500
+ }
501
+ const zipBlob = await createZipFile(files);
502
+ const url = URL.createObjectURL(zipBlob);
503
+ const link = document.createElement("a");
504
+ link.href = url;
505
+ link.download = `${config.name}-icons.zip`;
506
+ link.click();
507
+ URL.revokeObjectURL(url);
508
+ } catch (error) {
509
+ console.error("Failed to download icons:", error);
510
+ } finally {
511
+ setIsExporting(false);
512
+ }
513
+ }, [icon, icons, config, exportAll]);
514
+ return {
515
+ config,
516
+ setConfig,
517
+ icons,
518
+ isExporting,
519
+ progress,
520
+ exportAll,
521
+ exportWindows,
522
+ exportMacOS,
523
+ exportLinux,
524
+ downloadAll,
525
+ downloadSingle
526
+ };
527
+ }
528
+ const NiceDesktopIconExporter = ({
529
+ icon,
530
+ initialConfig,
531
+ onExport,
532
+ className = ""
533
+ }) => {
534
+ const { t } = useNiceTranslation();
535
+ const {
536
+ config,
537
+ setConfig,
538
+ icons,
539
+ isExporting,
540
+ progress,
541
+ exportAll,
542
+ downloadAll,
543
+ downloadSingle
544
+ } = useDesktopIconExporter(icon, initialConfig);
545
+ const handleExport = async () => {
546
+ await exportAll();
547
+ onExport?.(icons);
548
+ };
549
+ const IconComponent = icon;
550
+ return /* @__PURE__ */ jsxs("div", { className: `nice-desktop-icon-exporter ${className}`, children: [
551
+ /* @__PURE__ */ jsxs("div", { className: "nice-desktop-icon-exporter__preview", children: [
552
+ /* @__PURE__ */ jsx("h3", { children: t("desktopIconExporter.preview", "Preview") }),
553
+ /* @__PURE__ */ jsx("div", { className: "nice-desktop-icon-exporter__preview-grid", children: [16, 32, 48, 64, 128, 256].map((size) => /* @__PURE__ */ jsxs("div", { className: "nice-desktop-icon-exporter__preview-item", children: [
554
+ /* @__PURE__ */ jsx(
555
+ "div",
556
+ {
557
+ className: "nice-desktop-icon-exporter__preview-box",
558
+ style: {
559
+ width: Math.min(size, 128),
560
+ height: Math.min(size, 128),
561
+ backgroundColor: config.backgroundColor || "#f0f0f0"
562
+ },
563
+ children: /* @__PURE__ */ jsx(
564
+ IconComponent,
565
+ {
566
+ size: Math.min(size, 128) * (1 - config.padding * 2),
567
+ fill: config.fillColor
568
+ }
569
+ )
570
+ }
571
+ ),
572
+ /* @__PURE__ */ jsxs("span", { children: [
573
+ size,
574
+ "×",
575
+ size
576
+ ] })
577
+ ] }, size)) })
578
+ ] }),
579
+ /* @__PURE__ */ jsxs("div", { className: "nice-desktop-icon-exporter__config", children: [
580
+ /* @__PURE__ */ jsx("h3", { children: t("desktopIconExporter.configuration", "Configuration") }),
581
+ /* @__PURE__ */ jsxs("label", { children: [
582
+ t("desktopIconExporter.iconName", "Icon Name:"),
583
+ /* @__PURE__ */ jsx(
584
+ "input",
585
+ {
586
+ type: "text",
587
+ value: config.name,
588
+ onChange: (e) => setConfig((c) => ({ ...c, name: e.target.value }))
589
+ }
590
+ )
591
+ ] }),
592
+ /* @__PURE__ */ jsxs("label", { children: [
593
+ t("desktopIconExporter.fillColor", "Fill Color:"),
594
+ /* @__PURE__ */ jsx(
595
+ "input",
596
+ {
597
+ type: "color",
598
+ value: config.fillColor,
599
+ onChange: (e) => setConfig((c) => ({ ...c, fillColor: e.target.value }))
600
+ }
601
+ )
602
+ ] }),
603
+ /* @__PURE__ */ jsxs("label", { children: [
604
+ t("desktopIconExporter.background", "Background:"),
605
+ /* @__PURE__ */ jsx(
606
+ "input",
607
+ {
608
+ type: "color",
609
+ value: config.backgroundColor || "#ffffff",
610
+ onChange: (e) => setConfig((c) => ({ ...c, backgroundColor: e.target.value }))
611
+ }
612
+ ),
613
+ /* @__PURE__ */ jsx("button", { onClick: () => setConfig((c) => ({ ...c, backgroundColor: void 0 })), children: t("desktopIconExporter.transparent", "Transparent") })
614
+ ] }),
615
+ /* @__PURE__ */ jsxs("label", { children: [
616
+ t("desktopIconExporter.padding", "Padding: {padding}%").replace(
617
+ "{padding}",
618
+ String(Math.round(config.padding * 100))
619
+ ),
620
+ /* @__PURE__ */ jsx(
621
+ "input",
622
+ {
623
+ type: "range",
624
+ min: "0",
625
+ max: "0.3",
626
+ step: "0.01",
627
+ value: config.padding,
628
+ onChange: (e) => setConfig((c) => ({ ...c, padding: parseFloat(e.target.value) }))
629
+ }
630
+ )
631
+ ] }),
632
+ /* @__PURE__ */ jsxs("div", { className: "nice-desktop-icon-exporter__platforms", children: [
633
+ /* @__PURE__ */ jsxs("label", { children: [
634
+ /* @__PURE__ */ jsx(
635
+ "input",
636
+ {
637
+ type: "checkbox",
638
+ checked: config.includeWindows,
639
+ onChange: (e) => setConfig((c) => ({ ...c, includeWindows: e.target.checked }))
532
640
  }
533
- // Add Windows ICO
534
- if (config.includeWindows) {
535
- const windowsSizes = config.sizes.filter((s) => s <= 256);
536
- const pngBuffers = [];
537
- for (const size of windowsSizes) {
538
- const canvas = await renderIconToCanvas(icon, size, config.fillColor, config.backgroundColor, config.padding);
539
- const blob = await canvasToBlob(canvas);
540
- pngBuffers.push({ size, data: await blob.arrayBuffer() });
541
- }
542
- const icoBlob = await createIcoFile(pngBuffers);
543
- files.push({ name: `windows/${config.name}.ico`, data: icoBlob });
641
+ ),
642
+ t("desktopIconExporter.windowsIco", "Windows (.ico)")
643
+ ] }),
644
+ /* @__PURE__ */ jsxs("label", { children: [
645
+ /* @__PURE__ */ jsx(
646
+ "input",
647
+ {
648
+ type: "checkbox",
649
+ checked: config.includeMacOS,
650
+ onChange: (e) => setConfig((c) => ({ ...c, includeMacOS: e.target.checked }))
544
651
  }
545
- // Add macOS iconset
546
- if (config.includeMacOS) {
547
- const macSizes = config.sizes.filter((s) => s >= 128);
548
- const scales = config.includeRetina ? RETINA_VARIANTS : [RETINA_VARIANTS[0]];
549
- for (const size of macSizes) {
550
- for (const variant of scales) {
551
- const actualSize = size * variant.scale;
552
- const canvas = await renderIconToCanvas(icon, actualSize, config.fillColor, config.backgroundColor, config.padding);
553
- const blob = await canvasToBlob(canvas);
554
- files.push({
555
- name: `macos/${config.name}.iconset/icon_${size}x${size}${variant.suffix}.png`,
556
- data: blob,
557
- });
558
- }
559
- }
560
- files.push({
561
- name: 'macos/README.txt',
562
- data: `To convert to .icns: iconutil -c icns ${config.name}.iconset`,
563
- });
652
+ ),
653
+ t("desktopIconExporter.macosIconset", "macOS (.iconset)")
654
+ ] }),
655
+ /* @__PURE__ */ jsxs("label", { children: [
656
+ /* @__PURE__ */ jsx(
657
+ "input",
658
+ {
659
+ type: "checkbox",
660
+ checked: config.includeLinux,
661
+ onChange: (e) => setConfig((c) => ({ ...c, includeLinux: e.target.checked }))
564
662
  }
565
- // Add Linux icons
566
- if (config.includeLinux) {
567
- for (const { size, dir } of LINUX_HICOLOR_DIRS) {
568
- if (!config.sizes.includes(size)) {
569
- continue;
570
- }
571
- const canvas = await renderIconToCanvas(icon, size, config.fillColor, config.backgroundColor, config.padding);
572
- const blob = await canvasToBlob(canvas);
573
- files.push({ name: `linux/${dir}/${config.name}.png`, data: blob });
574
- }
663
+ ),
664
+ t("desktopIconExporter.linuxHicolor", "Linux (hicolor)")
665
+ ] }),
666
+ /* @__PURE__ */ jsxs("label", { children: [
667
+ /* @__PURE__ */ jsx(
668
+ "input",
669
+ {
670
+ type: "checkbox",
671
+ checked: config.includeRetina,
672
+ onChange: (e) => setConfig((c) => ({ ...c, includeRetina: e.target.checked }))
575
673
  }
576
- // Create and download ZIP
577
- const zipBlob = await createZipFile(files);
578
- const url = URL.createObjectURL(zipBlob);
579
- const link = document.createElement('a');
580
- link.href = url;
581
- link.download = `${config.name}-icons.zip`;
582
- link.click();
583
- URL.revokeObjectURL(url);
584
- }
585
- catch (error) {
586
- console.error('Failed to download icons:', error);
587
- }
588
- finally {
589
- setIsExporting(false);
590
- }
591
- }, [icon, icons, config, exportAll]);
592
- return {
593
- config,
594
- setConfig,
595
- icons,
596
- isExporting,
597
- progress,
598
- exportAll,
599
- exportWindows,
600
- exportMacOS,
601
- exportLinux,
602
- downloadAll,
603
- downloadSingle,
604
- };
605
- }
606
- /* ══════════════════════════════════════════════════════════════════════════════
607
- Component
608
- ══════════════════════════════════════════════════════════════════════════════ */
609
- /**
610
- * Desktop icon exporter component with UI for configuring and downloading icons.
611
- *
612
- * @example
613
- * ```tsx
614
- * import { NiceDesktopIconExporter } from '@nice2dev/icons';
615
- * import { NtdRocket } from '@nice2dev/icons/paths';
616
- *
617
- * function App() {
618
- * return (
619
- * <NiceDesktopIconExporter
620
- * icon={NtdRocket}
621
- * initialConfig={{ name: 'my-app', fillColor: '#ff6b35' }}
622
- * />
623
- * );
624
- * }
625
- * ```
626
- */
627
- const NiceDesktopIconExporter = ({ icon, initialConfig, onExport, className = '', }) => {
628
- const { t } = useNiceTranslation();
629
- const { config, setConfig, icons, isExporting, progress, exportAll, downloadAll, downloadSingle, } = useDesktopIconExporter(icon, initialConfig);
630
- const handleExport = async () => {
631
- await exportAll();
632
- onExport === null || onExport === void 0 ? void 0 : onExport(icons);
633
- };
634
- const IconComponent = icon;
635
- return (jsxs("div", { className: `nice-desktop-icon-exporter ${className}`, children: [jsxs("div", { className: "nice-desktop-icon-exporter__preview", children: [jsx("h3", { children: t('desktopIconExporter.preview', 'Preview') }), jsx("div", { className: "nice-desktop-icon-exporter__preview-grid", children: [16, 32, 48, 64, 128, 256].map((size) => (jsxs("div", { className: "nice-desktop-icon-exporter__preview-item", children: [jsx("div", { className: "nice-desktop-icon-exporter__preview-box", style: {
636
- width: Math.min(size, 128),
637
- height: Math.min(size, 128),
638
- backgroundColor: config.backgroundColor || '#f0f0f0',
639
- }, children: jsx(IconComponent, { size: Math.min(size, 128) * (1 - config.padding * 2), fill: config.fillColor }) }), jsxs("span", { children: [size, "\u00D7", size] })] }, size))) })] }), jsxs("div", { className: "nice-desktop-icon-exporter__config", children: [jsx("h3", { children: t('desktopIconExporter.configuration', 'Configuration') }), jsxs("label", { children: [t('desktopIconExporter.iconName', 'Icon Name:'), jsx("input", { type: "text", value: config.name, onChange: (e) => setConfig((c) => ({ ...c, name: e.target.value })) })] }), jsxs("label", { children: [t('desktopIconExporter.fillColor', 'Fill Color:'), jsx("input", { type: "color", value: config.fillColor, onChange: (e) => setConfig((c) => ({ ...c, fillColor: e.target.value })) })] }), jsxs("label", { children: [t('desktopIconExporter.background', 'Background:'), jsx("input", { type: "color", value: config.backgroundColor || '#ffffff', onChange: (e) => setConfig((c) => ({ ...c, backgroundColor: e.target.value })) }), jsx("button", { onClick: () => setConfig((c) => ({ ...c, backgroundColor: undefined })), children: t('desktopIconExporter.transparent', 'Transparent') })] }), jsxs("label", { children: [t('desktopIconExporter.padding', 'Padding: {padding}%').replace('{padding}', String(Math.round(config.padding * 100))), jsx("input", { type: "range", min: "0", max: "0.3", step: "0.01", value: config.padding, onChange: (e) => setConfig((c) => ({ ...c, padding: parseFloat(e.target.value) })) })] }), jsxs("div", { className: "nice-desktop-icon-exporter__platforms", children: [jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.includeWindows, onChange: (e) => setConfig((c) => ({ ...c, includeWindows: e.target.checked })) }), t('desktopIconExporter.windowsIco', 'Windows (.ico)')] }), jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.includeMacOS, onChange: (e) => setConfig((c) => ({ ...c, includeMacOS: e.target.checked })) }), t('desktopIconExporter.macosIconset', 'macOS (.iconset)')] }), jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.includeLinux, onChange: (e) => setConfig((c) => ({ ...c, includeLinux: e.target.checked })) }), t('desktopIconExporter.linuxHicolor', 'Linux (hicolor)')] }), jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.includeRetina, onChange: (e) => setConfig((c) => ({ ...c, includeRetina: e.target.checked })) }), t('desktopIconExporter.hidpiRetina2x3x', 'HiDPI/Retina (@2x, @3x)')] })] }), jsxs("div", { className: "nice-desktop-icon-exporter__sizes", children: [jsx("h4", { children: t('desktopIconExporter.sizes', 'Sizes:') }), DESKTOP_ICON_SIZES.map(({ size, label }) => (jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.sizes.includes(size), onChange: (e) => {
640
- setConfig((c) => ({
641
- ...c,
642
- sizes: e.target.checked
643
- ? [...c.sizes, size].sort((a, b) => a - b)
644
- : c.sizes.filter((s) => s !== size),
645
- }));
646
- } }), label] }, size)))] })] }), jsxs("div", { className: "nice-desktop-icon-exporter__actions", children: [jsx("button", { onClick: handleExport, disabled: isExporting, children: isExporting
647
- ? t('desktopIconExporter.exporting', 'Exporting... {progress}%').replace('{progress}', String(Math.round(progress)))
648
- : t('desktopIconExporter.generateIcons', 'Generate Icons') }), jsx("button", { onClick: downloadAll, disabled: isExporting || icons.length === 0, children: t('desktopIconExporter.downloadAllZip', 'Download All (ZIP)') })] }), icons.length > 0 && (jsxs("div", { className: "nice-desktop-icon-exporter__results", children: [jsx("h3", { children: t('desktopIconExporter.generatedIcons', 'Generated Icons ({icons})').replace('{icons}', String(icons.length)) }), jsx("div", { className: "nice-desktop-icon-exporter__results-grid", children: icons.map((exportedIcon) => (jsxs("div", { className: "nice-desktop-icon-exporter__result-item", children: [jsx("img", { src: exportedIcon.dataUrl, alt: exportedIcon.filename }), jsx("span", { children: exportedIcon.filename }), jsx("button", { onClick: () => downloadSingle(exportedIcon), children: t('desktopIconExporter.download', 'Download') })] }, exportedIcon.filename))) })] }))] }));
674
+ ),
675
+ t("desktopIconExporter.hidpiRetina2x3x", "HiDPI/Retina (@2x, @3x)")
676
+ ] })
677
+ ] }),
678
+ /* @__PURE__ */ jsxs("div", { className: "nice-desktop-icon-exporter__sizes", children: [
679
+ /* @__PURE__ */ jsx("h4", { children: t("desktopIconExporter.sizes", "Sizes:") }),
680
+ DESKTOP_ICON_SIZES.map(({ size, label }) => /* @__PURE__ */ jsxs("label", { children: [
681
+ /* @__PURE__ */ jsx(
682
+ "input",
683
+ {
684
+ type: "checkbox",
685
+ checked: config.sizes.includes(size),
686
+ onChange: (e) => {
687
+ setConfig((c) => ({
688
+ ...c,
689
+ sizes: e.target.checked ? [...c.sizes, size].sort((a, b) => a - b) : c.sizes.filter((s) => s !== size)
690
+ }));
691
+ }
692
+ }
693
+ ),
694
+ label
695
+ ] }, size))
696
+ ] })
697
+ ] }),
698
+ /* @__PURE__ */ jsxs("div", { className: "nice-desktop-icon-exporter__actions", children: [
699
+ /* @__PURE__ */ jsx("button", { onClick: handleExport, disabled: isExporting, children: isExporting ? t("desktopIconExporter.exporting", "Exporting... {progress}%").replace(
700
+ "{progress}",
701
+ String(Math.round(progress))
702
+ ) : t("desktopIconExporter.generateIcons", "Generate Icons") }),
703
+ /* @__PURE__ */ jsx("button", { onClick: downloadAll, disabled: isExporting || icons.length === 0, children: t("desktopIconExporter.downloadAllZip", "Download All (ZIP)") })
704
+ ] }),
705
+ icons.length > 0 && /* @__PURE__ */ jsxs("div", { className: "nice-desktop-icon-exporter__results", children: [
706
+ /* @__PURE__ */ jsx("h3", { children: t("desktopIconExporter.generatedIcons", "Generated Icons ({icons})").replace(
707
+ "{icons}",
708
+ String(icons.length)
709
+ ) }),
710
+ /* @__PURE__ */ jsx("div", { className: "nice-desktop-icon-exporter__results-grid", children: icons.map((exportedIcon) => /* @__PURE__ */ jsxs("div", { className: "nice-desktop-icon-exporter__result-item", children: [
711
+ /* @__PURE__ */ jsx("img", { src: exportedIcon.dataUrl, alt: exportedIcon.filename }),
712
+ /* @__PURE__ */ jsx("span", { children: exportedIcon.filename }),
713
+ /* @__PURE__ */ jsx("button", { onClick: () => downloadSingle(exportedIcon), children: t("desktopIconExporter.download", "Download") })
714
+ ] }, exportedIcon.filename)) })
715
+ ] })
716
+ ] });
717
+ };
718
+ export {
719
+ DESKTOP_ICON_SIZES,
720
+ LINUX_HICOLOR_DIRS,
721
+ NiceDesktopIconExporter,
722
+ RETINA_VARIANTS,
723
+ useDesktopIconExporter
649
724
  };
650
-
651
- export { DESKTOP_ICON_SIZES, LINUX_HICOLOR_DIRS, NiceDesktopIconExporter, RETINA_VARIANTS, useDesktopIconExporter };