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