@nice2dev/icons 1.3.15 → 1.3.17

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 (153) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +1 -1
  3. package/dist/cjs/NiceDesktopIconExporter.js +688 -599
  4. package/dist/cjs/NiceFaviconGenerator.js +603 -417
  5. package/dist/cjs/NiceIconPicker.js +788 -508
  6. package/dist/cjs/NiceIconPreview.js +246 -188
  7. package/dist/cjs/NiceIconSetCreator.js +683 -552
  8. package/dist/cjs/NiceIconStacker.js +547 -171
  9. package/dist/cjs/NiceIconValue.js +27 -35
  10. package/dist/cjs/NtdIconBadge.js +384 -360
  11. package/dist/cjs/NtdIconMorph.js +419 -354
  12. package/dist/cjs/NtdStateMorph.js +150 -147
  13. package/dist/cjs/advancedIconSearch.js +373 -510
  14. package/dist/cjs/animationControls.js +518 -642
  15. package/dist/cjs/animationSpeed.js +69 -0
  16. package/dist/cjs/client.js +12 -15
  17. package/dist/cjs/createIcon.js +374 -252
  18. package/dist/cjs/design-tokens.js +324 -390
  19. package/dist/cjs/figmaExport.js +162 -257
  20. package/dist/cjs/figmaSync.js +234 -316
  21. package/dist/cjs/headless-ui.js +240 -368
  22. package/dist/cjs/iconAnalytics.js +337 -393
  23. package/dist/cjs/iconHtml.js +64 -86
  24. package/dist/cjs/iconMetadata.js +392 -472
  25. package/dist/cjs/iconParts.js +120 -90
  26. package/dist/cjs/iconStack.js +136 -155
  27. package/dist/cjs/iconStyles.js +1670 -509
  28. package/dist/cjs/iconValue.js +359 -430
  29. package/dist/cjs/icons/accessibility.js +1119 -134
  30. package/dist/cjs/icons/actions.js +1290 -330
  31. package/dist/cjs/icons/ai.js +1424 -144
  32. package/dist/cjs/icons/brand.js +613 -184
  33. package/dist/cjs/icons/business.js +938 -192
  34. package/dist/cjs/icons/devtech.js +1656 -227
  35. package/dist/cjs/icons/ecommerce.js +802 -111
  36. package/dist/cjs/icons/education.js +1345 -199
  37. package/dist/cjs/icons/files.js +1597 -328
  38. package/dist/cjs/icons/fintech.js +793 -104
  39. package/dist/cjs/icons/food.js +1028 -156
  40. package/dist/cjs/icons/fun.js +804 -109
  41. package/dist/cjs/icons/gaming.js +916 -170
  42. package/dist/cjs/icons/health.js +623 -112
  43. package/dist/cjs/icons/index.js +32 -42
  44. package/dist/cjs/icons/input.js +282 -83
  45. package/dist/cjs/icons/legal.js +1065 -122
  46. package/dist/cjs/icons/media.js +1061 -267
  47. package/dist/cjs/icons/navigation.js +407 -123
  48. package/dist/cjs/icons/omniverk.js +523 -93
  49. package/dist/cjs/icons/realestate.js +1520 -193
  50. package/dist/cjs/icons/saas.js +867 -98
  51. package/dist/cjs/icons/science.js +1344 -202
  52. package/dist/cjs/icons/shapes.js +1316 -308
  53. package/dist/cjs/icons/smarthome.js +786 -90
  54. package/dist/cjs/icons/social.js +591 -59
  55. package/dist/cjs/icons/sports.js +1133 -133
  56. package/dist/cjs/icons/sustainability.js +1211 -139
  57. package/dist/cjs/icons/travel.js +1470 -148
  58. package/dist/cjs/icons/ui.js +1508 -315
  59. package/dist/cjs/icons/weather.js +1160 -173
  60. package/dist/cjs/index.js +559 -568
  61. package/dist/cjs/lottieIntegration.js +261 -266
  62. package/dist/cjs/nicetodev-icons.css +1 -1
  63. package/dist/cjs/particleEffects.js +270 -251
  64. package/dist/cjs/resolver.js +229 -284
  65. package/dist/cjs/rsc.js +175 -193
  66. package/dist/cjs/sharpen.js +73 -130
  67. package/dist/cjs/smilAnimations.js +380 -165
  68. package/dist/cjs/stacking.js +87 -122
  69. package/dist/cjs/tailwind-plugin.js +280 -320
  70. package/dist/cjs/types.js +46 -72
  71. package/dist/cjs/utilities.js +163 -324
  72. package/dist/cjs/variantProps.js +12 -52
  73. package/dist/esm/NiceDesktopIconExporter.js +671 -598
  74. package/dist/esm/NiceFaviconGenerator.js +583 -416
  75. package/dist/esm/NiceIconPicker.js +789 -507
  76. package/dist/esm/NiceIconPreview.js +247 -187
  77. package/dist/esm/NiceIconSetCreator.js +685 -551
  78. package/dist/esm/NiceIconStacker.js +549 -170
  79. package/dist/esm/NiceIconValue.js +29 -34
  80. package/dist/esm/NtdIconBadge.js +388 -359
  81. package/dist/esm/NtdIconMorph.js +423 -353
  82. package/dist/esm/NtdStateMorph.js +152 -146
  83. package/dist/esm/advancedIconSearch.js +391 -509
  84. package/dist/esm/animationControls.js +527 -641
  85. package/dist/esm/animationSpeed.js +69 -0
  86. package/dist/esm/client.js +48 -10
  87. package/dist/esm/createIcon.js +375 -251
  88. package/dist/esm/design-tokens.js +334 -387
  89. package/dist/esm/figmaExport.js +170 -256
  90. package/dist/esm/figmaSync.js +237 -315
  91. package/dist/esm/headless-ui.js +254 -365
  92. package/dist/esm/iconAnalytics.js +342 -392
  93. package/dist/esm/iconHtml.js +67 -85
  94. package/dist/esm/iconMetadata.js +404 -471
  95. package/dist/esm/iconParts.js +125 -89
  96. package/dist/esm/iconStack.js +145 -154
  97. package/dist/esm/iconStyles.js +1681 -508
  98. package/dist/esm/iconValue.js +361 -429
  99. package/dist/esm/icons/accessibility.js +1133 -133
  100. package/dist/esm/icons/actions.js +1320 -329
  101. package/dist/esm/icons/ai.js +1441 -143
  102. package/dist/esm/icons/brand.js +616 -179
  103. package/dist/esm/icons/business.js +957 -191
  104. package/dist/esm/icons/devtech.js +1687 -226
  105. package/dist/esm/icons/ecommerce.js +816 -110
  106. package/dist/esm/icons/education.js +1364 -198
  107. package/dist/esm/icons/files.js +1628 -327
  108. package/dist/esm/icons/fintech.js +807 -103
  109. package/dist/esm/icons/food.js +1043 -153
  110. package/dist/esm/icons/fun.js +819 -108
  111. package/dist/esm/icons/gaming.js +933 -169
  112. package/dist/esm/icons/health.js +636 -111
  113. package/dist/esm/icons/index.js +589 -32
  114. package/dist/esm/icons/input.js +326 -82
  115. package/dist/esm/icons/legal.js +1079 -121
  116. package/dist/esm/icons/media.js +1086 -266
  117. package/dist/esm/icons/navigation.js +421 -122
  118. package/dist/esm/icons/omniverk.js +534 -91
  119. package/dist/esm/icons/realestate.js +1544 -198
  120. package/dist/esm/icons/saas.js +880 -97
  121. package/dist/esm/icons/science.js +1370 -208
  122. package/dist/esm/icons/shapes.js +1343 -307
  123. package/dist/esm/icons/smarthome.js +799 -89
  124. package/dist/esm/icons/social.js +599 -58
  125. package/dist/esm/icons/sports.js +1148 -132
  126. package/dist/esm/icons/sustainability.js +1226 -138
  127. package/dist/esm/icons/travel.js +1486 -147
  128. package/dist/esm/icons/ui.js +1540 -314
  129. package/dist/esm/icons/weather.js +1181 -172
  130. package/dist/esm/index.js +749 -58
  131. package/dist/esm/lottieIntegration.js +264 -265
  132. package/dist/esm/nicetodev-icons.css +1 -1
  133. package/dist/esm/particleEffects.js +271 -250
  134. package/dist/esm/resolver.js +231 -283
  135. package/dist/esm/rsc.js +182 -192
  136. package/dist/esm/sharpen.js +78 -129
  137. package/dist/esm/smilAnimations.js +385 -164
  138. package/dist/esm/stacking.js +99 -121
  139. package/dist/esm/tailwind-plugin.js +282 -319
  140. package/dist/esm/types.js +50 -71
  141. package/dist/esm/utilities.js +177 -321
  142. package/dist/esm/variantProps.js +23 -52
  143. package/dist/types/animationSpeed.d.ts +19 -0
  144. package/dist/types/createIcon.d.ts +6 -0
  145. package/dist/types/iconStyles.d.ts +6 -1
  146. package/dist/types/icons/brand.d.ts +0 -4
  147. package/dist/types/icons/food.d.ts +0 -3
  148. package/dist/types/icons/index.d.ts +3 -3
  149. package/dist/types/icons/omniverk.d.ts +0 -3
  150. package/dist/types/icons/science.d.ts +1 -1
  151. package/dist/types/smilAnimations.d.ts +4 -2
  152. package/dist/types/types.d.ts +7 -0
  153. 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 };