@abydahana/iconpicker 1.0.0

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 (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +303 -0
  3. package/dist/core/font-loader.d.ts +2 -0
  4. package/dist/core/font-loader.d.ts.map +1 -0
  5. package/dist/core/font-loader.js +35 -0
  6. package/dist/core/font-loader.js.map +1 -0
  7. package/dist/core/grid.d.ts +26 -0
  8. package/dist/core/grid.d.ts.map +1 -0
  9. package/dist/core/grid.js +101 -0
  10. package/dist/core/grid.js.map +1 -0
  11. package/dist/core/icon-picker.d.ts +29 -0
  12. package/dist/core/icon-picker.d.ts.map +1 -0
  13. package/dist/core/icon-picker.js +314 -0
  14. package/dist/core/icon-picker.js.map +1 -0
  15. package/dist/core/popover.d.ts +17 -0
  16. package/dist/core/popover.d.ts.map +1 -0
  17. package/dist/core/popover.js +96 -0
  18. package/dist/core/popover.js.map +1 -0
  19. package/dist/dom/template.d.ts +18 -0
  20. package/dist/dom/template.d.ts.map +1 -0
  21. package/dist/dom/template.js +121 -0
  22. package/dist/dom/template.js.map +1 -0
  23. package/dist/iconpicker.css +547 -0
  24. package/dist/iconsets/bootstrap.d.ts +3 -0
  25. package/dist/iconsets/bootstrap.d.ts.map +1 -0
  26. package/dist/iconsets/bootstrap.js +2059 -0
  27. package/dist/iconsets/bootstrap.js.map +1 -0
  28. package/dist/iconsets/boxicons.d.ts +3 -0
  29. package/dist/iconsets/boxicons.d.ts.map +1 -0
  30. package/dist/iconsets/boxicons.js +823 -0
  31. package/dist/iconsets/boxicons.js.map +1 -0
  32. package/dist/iconsets/fontawesome.d.ts +3 -0
  33. package/dist/iconsets/fontawesome.d.ts.map +1 -0
  34. package/dist/iconsets/fontawesome.js +1904 -0
  35. package/dist/iconsets/fontawesome.js.map +1 -0
  36. package/dist/iconsets/index.d.ts +10 -0
  37. package/dist/iconsets/index.d.ts.map +1 -0
  38. package/dist/iconsets/index.js +29 -0
  39. package/dist/iconsets/index.js.map +1 -0
  40. package/dist/iconsets/lucide.d.ts +3 -0
  41. package/dist/iconsets/lucide.d.ts.map +1 -0
  42. package/dist/iconsets/lucide.js +1816 -0
  43. package/dist/iconsets/lucide.js.map +1 -0
  44. package/dist/iconsets/mdi.d.ts +3 -0
  45. package/dist/iconsets/mdi.d.ts.map +1 -0
  46. package/dist/iconsets/mdi.js +7457 -0
  47. package/dist/iconsets/mdi.js.map +1 -0
  48. package/dist/iconsets/remix.d.ts +3 -0
  49. package/dist/iconsets/remix.d.ts.map +1 -0
  50. package/dist/iconsets/remix.js +2901 -0
  51. package/dist/iconsets/remix.js.map +1 -0
  52. package/dist/iconsets/tabler.d.ts +3 -0
  53. package/dist/iconsets/tabler.d.ts.map +1 -0
  54. package/dist/iconsets/tabler.js +4888 -0
  55. package/dist/iconsets/tabler.js.map +1 -0
  56. package/dist/index.d.ts +14 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +15 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/themes/aksara.css +72 -0
  61. package/dist/themes/base.css +211 -0
  62. package/dist/themes/bootstrap.css +70 -0
  63. package/dist/themes/default.css +72 -0
  64. package/dist/themes/tailwind.css +112 -0
  65. package/dist/types.d.ts +35 -0
  66. package/dist/types.d.ts.map +1 -0
  67. package/dist/types.js +2 -0
  68. package/dist/types.js.map +1 -0
  69. package/package.json +69 -0
  70. package/src/core/font-loader.ts +38 -0
  71. package/src/core/grid.ts +129 -0
  72. package/src/core/icon-picker.ts +353 -0
  73. package/src/core/popover.ts +113 -0
  74. package/src/dom/template.ts +173 -0
  75. package/src/iconsets/bootstrap.ts +2060 -0
  76. package/src/iconsets/boxicons.ts +824 -0
  77. package/src/iconsets/fontawesome.ts +1905 -0
  78. package/src/iconsets/index.ts +31 -0
  79. package/src/iconsets/lucide.ts +1817 -0
  80. package/src/iconsets/mdi.ts +7458 -0
  81. package/src/iconsets/remix.ts +2902 -0
  82. package/src/iconsets/tabler.ts +4889 -0
  83. package/src/index.ts +18 -0
  84. package/src/themes/aksara.css +72 -0
  85. package/src/themes/base.css +211 -0
  86. package/src/themes/bootstrap.css +70 -0
  87. package/src/themes/default.css +72 -0
  88. package/src/themes/index.css +5 -0
  89. package/src/themes/tailwind.css +112 -0
  90. package/src/types.ts +37 -0
@@ -0,0 +1,353 @@
1
+ import type { IconItem, IconPickerOptions, IconSetDefinition } from "../types";
2
+ import { builtInIconSets } from "../iconsets/index";
3
+ import { createPopoverTemplate, renderTabs, type TemplateElements } from "../dom/template";
4
+ import { PopoverPositioner } from "./popover";
5
+ import { IconGrid } from "./grid";
6
+ import { loadIconFont } from "./font-loader";
7
+
8
+ export class IconPicker {
9
+ private target: HTMLElement;
10
+ private inputElement: HTMLInputElement | null = null;
11
+ private options: Required<Omit<IconPickerOptions, "container" | "onSelect" | "onOpen" | "onClose">> & {
12
+ container?: HTMLElement | string;
13
+ onSelect?: (icon: IconItem) => void;
14
+ onOpen?: () => void;
15
+ onClose?: () => void;
16
+ };
17
+
18
+ private sets: IconSetDefinition[] = [];
19
+ private activeSetId = "";
20
+ private currentSearch = "";
21
+ private currentValue = "";
22
+ private isOpen = false;
23
+
24
+ private dom: TemplateElements;
25
+ private positioner: PopoverPositioner;
26
+ private grid: IconGrid;
27
+
28
+ private outsideClickListener: (e: MouseEvent) => void;
29
+ private keydownListener: (e: KeyboardEvent) => void;
30
+ private targetClickListener: (e: MouseEvent) => void;
31
+
32
+ constructor(target: HTMLElement | string, options: IconPickerOptions = {}) {
33
+ const el = typeof target === "string" ? document.querySelector<HTMLElement>(target) : target;
34
+ if (!el) {
35
+ throw new Error(`[IconPicker] Target element "${target}" not found.`);
36
+ }
37
+ this.target = el;
38
+ if (el instanceof HTMLInputElement) {
39
+ this.inputElement = el;
40
+ } else {
41
+ this.inputElement = el.querySelector<HTMLInputElement>("input");
42
+ }
43
+
44
+ // Resolve icon sets
45
+ const rawSets = options.iconSets || ["mdi", "fa", "bi", "tabler", "remix", "boxicons", "lucide"];
46
+ for (const item of rawSets) {
47
+ if (typeof item === "string") {
48
+ const resolved = builtInIconSets[item.toLowerCase()];
49
+ if (resolved && !this.sets.some((s) => s.id === resolved.id)) {
50
+ this.sets.push(resolved);
51
+ }
52
+ } else if (item && typeof item === "object" && item.id) {
53
+ this.sets.push(item);
54
+ }
55
+ }
56
+
57
+ if (this.sets.length === 0) {
58
+ this.sets.push(builtInIconSets.mdi);
59
+ }
60
+
61
+ const defaultSetId =
62
+ options.defaultSet && this.sets.some((s) => s.id === options.defaultSet) ? options.defaultSet : this.sets[0].id;
63
+
64
+ this.options = {
65
+ theme: options.theme || "default",
66
+ placement: options.placement || "bottom-start",
67
+ iconSets: this.sets,
68
+ defaultSet: defaultSetId,
69
+ searchPlaceholder: options.searchPlaceholder || "Search icons...",
70
+ value: options.value || (this.inputElement ? this.inputElement.value : ""),
71
+ container: options.container,
72
+ showTabs: options.showTabs ?? true,
73
+ showFooter: options.showFooter ?? true,
74
+ closeOnSelect: options.closeOnSelect ?? true,
75
+ emptyText: options.emptyText || "No icons found matching your search",
76
+ itemsPerPage: options.itemsPerPage || 120,
77
+ loadFonts: options.loadFonts ?? true,
78
+ fontUrls: options.fontUrls || {},
79
+ onSelect: options.onSelect,
80
+ onOpen: options.onOpen,
81
+ onClose: options.onClose
82
+ };
83
+
84
+ this.activeSetId = this.options.defaultSet;
85
+ this.currentValue = this.options.value;
86
+
87
+ if (this.options.loadFonts) {
88
+ loadIconFont(this.activeSetId, this.options.fontUrls);
89
+ if (this.currentValue) {
90
+ for (const s of this.sets) {
91
+ if (s.prefix && this.currentValue.startsWith(s.prefix)) {
92
+ loadIconFont(s.id, this.options.fontUrls);
93
+ break;
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ // Create DOM structure
100
+ this.dom = createPopoverTemplate(this.options.theme, this.options.searchPlaceholder, this.options.emptyText);
101
+
102
+ if (!this.options.showFooter) {
103
+ this.dom.footer.style.display = "none";
104
+ }
105
+
106
+ // Resolve container
107
+ let containerEl = document.body;
108
+ if (this.options.container) {
109
+ if (typeof this.options.container === "string") {
110
+ const c = document.querySelector<HTMLElement>(this.options.container);
111
+ if (c) containerEl = c;
112
+ } else if (this.options.container instanceof HTMLElement) {
113
+ containerEl = this.options.container;
114
+ }
115
+ }
116
+
117
+ // Initialize Positioner
118
+ this.positioner = new PopoverPositioner(this.target, this.dom.root, {
119
+ placement: this.options.placement,
120
+ container: containerEl
121
+ });
122
+
123
+ // Initialize Grid
124
+ this.grid = new IconGrid({
125
+ container: this.dom.grid,
126
+ bodyElement: this.dom.body,
127
+ emptyElement: this.dom.emptyState,
128
+ chunkSize: this.options.itemsPerPage,
129
+ onSelect: (icon) => this.handleIconSelect(icon)
130
+ });
131
+
132
+ // Event listeners
133
+ const inputGroup = this.target.closest(".input-group");
134
+
135
+ this.outsideClickListener = (e: MouseEvent) => {
136
+ if (!this.isOpen) return;
137
+ const clicked = e.target as Node | null;
138
+ if (!clicked) return;
139
+ if (this.dom.root.contains(clicked)) return;
140
+ if (this.target.contains(clicked)) return;
141
+ if (inputGroup && inputGroup.contains(clicked)) return;
142
+ const card = this.target.closest(".menu-item-card");
143
+ const barIcon = card?.querySelector(".menu-item-icon-preview");
144
+ if (barIcon && barIcon.contains(clicked)) return;
145
+ this.close();
146
+ };
147
+
148
+ this.keydownListener = (e: KeyboardEvent) => {
149
+ if (!this.isOpen) return;
150
+ if (e.key === "Escape") {
151
+ e.preventDefault();
152
+ this.close();
153
+ }
154
+ };
155
+
156
+ this.targetClickListener = (e: MouseEvent) => {
157
+ e.stopPropagation();
158
+ this.toggle();
159
+ };
160
+
161
+ this.bindEvents(inputGroup);
162
+ this.updatePreview(this.currentValue);
163
+ }
164
+
165
+ private bindEvents(inputGroup: Element | null): void {
166
+ this.target.addEventListener("click", this.targetClickListener);
167
+ this.target.addEventListener("focus", () => {
168
+ if (!this.isOpen) this.open();
169
+ });
170
+
171
+ const card = this.target.closest(".menu-item-card");
172
+ if (card) {
173
+ const barIcon = card.querySelector<HTMLElement>(".menu-item-icon-preview");
174
+ if (barIcon) {
175
+ barIcon.style.cursor = "pointer";
176
+ barIcon.setAttribute("role", "button");
177
+ barIcon.title = "Click to change icon";
178
+ barIcon.addEventListener("click", (e) => {
179
+ e.preventDefault();
180
+ e.stopPropagation();
181
+ this.toggle();
182
+ });
183
+ }
184
+ }
185
+
186
+ if (inputGroup) {
187
+ const addons = inputGroup.querySelectorAll<HTMLElement>('.input-group-text, button:not([type="submit"])');
188
+ addons.forEach((addon) => {
189
+ addon.style.cursor = "pointer";
190
+ addon.addEventListener("click", this.targetClickListener);
191
+ });
192
+ }
193
+
194
+ // Search input
195
+ this.dom.searchInput.addEventListener("input", () => {
196
+ const val = this.dom.searchInput.value.trim().toLowerCase();
197
+ this.currentSearch = val;
198
+ this.dom.searchClearBtn.style.display = val ? "flex" : "none";
199
+ this.filterIcons();
200
+ });
201
+
202
+ this.dom.searchClearBtn.addEventListener("click", (e) => {
203
+ e.stopPropagation();
204
+ this.dom.searchInput.value = "";
205
+ this.currentSearch = "";
206
+ this.dom.searchClearBtn.style.display = "none";
207
+ this.filterIcons();
208
+ this.dom.searchInput.focus();
209
+ });
210
+
211
+ // Footer clear button
212
+ this.dom.clearSelectionBtn.addEventListener("click", (e) => {
213
+ e.stopPropagation();
214
+ this.setValue("");
215
+ if (this.options.closeOnSelect) {
216
+ this.close();
217
+ }
218
+ });
219
+
220
+ // Prevent clicks inside popover from propagating to outside listener
221
+ this.dom.root.addEventListener("click", (e) => {
222
+ e.stopPropagation();
223
+ });
224
+ }
225
+
226
+ private filterIcons(): void {
227
+ const activeSet = this.sets.find((s) => s.id === this.activeSetId);
228
+ if (!activeSet) return;
229
+
230
+ let filtered = activeSet.icons;
231
+ if (this.currentSearch) {
232
+ filtered = activeSet.icons.filter((name) => name.toLowerCase().includes(this.currentSearch));
233
+ }
234
+
235
+ this.grid.setIcons(filtered, activeSet, this.currentValue);
236
+ }
237
+
238
+ private handleIconSelect(icon: IconItem): void {
239
+ this.setValue(icon.className);
240
+ if (this.options.onSelect) {
241
+ this.options.onSelect(icon);
242
+ }
243
+ if (this.options.closeOnSelect) {
244
+ this.close();
245
+ }
246
+ }
247
+
248
+ public setValue(val: string): void {
249
+ this.currentValue = val;
250
+ if (this.inputElement) {
251
+ this.inputElement.value = val;
252
+ this.inputElement.dispatchEvent(new Event("input", { bubbles: true }));
253
+ this.inputElement.dispatchEvent(new Event("change", { bubbles: true }));
254
+ }
255
+ this.updatePreview(val);
256
+ this.grid.setSelectedValue(val);
257
+ }
258
+
259
+ public getValue(): string {
260
+ return this.currentValue;
261
+ }
262
+
263
+ private updatePreview(val: string): void {
264
+ if (val) {
265
+ this.dom.previewIcon.className = `iconpicker-preview-icon ${val}`;
266
+ this.dom.previewIcon.style.display = "inline-block";
267
+ this.dom.previewName.textContent = val;
268
+ } else {
269
+ this.dom.previewIcon.className = "iconpicker-preview-icon";
270
+ this.dom.previewIcon.style.display = "none";
271
+ this.dom.previewName.textContent = "None selected";
272
+ }
273
+
274
+ // If target has an icon preview sibling, update it too
275
+ const previewEl = this.target.parentElement?.querySelector<HTMLElement>(
276
+ "[data-icon-preview], .icon-preview, i.input-group-text"
277
+ );
278
+ if (previewEl) {
279
+ const iconTag = previewEl.tagName === "I" ? previewEl : previewEl.querySelector("i");
280
+ if (iconTag) {
281
+ iconTag.className = val || "mdi mdi-emoticon-outline";
282
+ }
283
+ }
284
+ }
285
+
286
+ public open(): void {
287
+ if (this.isOpen) return;
288
+ this.isOpen = true;
289
+
290
+ // Render tabs if enabled
291
+ if (this.options.showTabs) {
292
+ renderTabs(this.dom.tabsContainer, this.sets, this.activeSetId, (setId) => {
293
+ this.activeSetId = setId;
294
+ if (this.options.loadFonts) {
295
+ loadIconFont(setId, this.options.fontUrls);
296
+ }
297
+ renderTabs(this.dom.tabsContainer, this.sets, this.activeSetId, (s) => (this.activeSetId = s));
298
+ this.filterIcons();
299
+ });
300
+ }
301
+
302
+ // Attach to DOM and calculate position
303
+ this.positioner.attach();
304
+ this.filterIcons();
305
+
306
+ requestAnimationFrame(() => {
307
+ this.positioner.updatePosition();
308
+ });
309
+
310
+ setTimeout(() => {
311
+ if (this.isOpen) {
312
+ document.addEventListener("click", this.outsideClickListener);
313
+ }
314
+ }, 20);
315
+ document.addEventListener("keydown", this.keydownListener);
316
+
317
+ setTimeout(() => {
318
+ this.dom.searchInput.focus();
319
+ }, 50);
320
+
321
+ if (this.options.onOpen) {
322
+ this.options.onOpen();
323
+ }
324
+ }
325
+
326
+ public close(): void {
327
+ if (!this.isOpen) return;
328
+ this.isOpen = false;
329
+
330
+ this.positioner.detach();
331
+ document.removeEventListener("click", this.outsideClickListener);
332
+ document.removeEventListener("keydown", this.keydownListener);
333
+
334
+ if (this.options.onClose) {
335
+ this.options.onClose();
336
+ }
337
+ }
338
+
339
+ public toggle(): void {
340
+ if (this.isOpen) {
341
+ this.close();
342
+ } else {
343
+ this.open();
344
+ }
345
+ }
346
+
347
+ public destroy(): void {
348
+ this.close();
349
+ this.target.removeEventListener("click", this.targetClickListener);
350
+ this.grid.destroy();
351
+ this.positioner.detach();
352
+ }
353
+ }
@@ -0,0 +1,113 @@
1
+ import type { IconPickerPlacement } from "../types";
2
+
3
+ export interface PopoverPositionOptions {
4
+ placement?: IconPickerPlacement;
5
+ offset?: number;
6
+ container?: HTMLElement;
7
+ }
8
+
9
+ export class PopoverPositioner {
10
+ private reference: HTMLElement;
11
+ private popover: HTMLElement;
12
+ private options: Required<PopoverPositionOptions>;
13
+ private handleUpdate: () => void;
14
+
15
+ constructor(reference: HTMLElement, popover: HTMLElement, options: PopoverPositionOptions = {}) {
16
+ this.reference = reference;
17
+ this.popover = popover;
18
+ this.options = {
19
+ placement: options.placement || "auto",
20
+ offset: options.offset ?? 6,
21
+ container: options.container || document.body
22
+ };
23
+
24
+ this.handleUpdate = () => {
25
+ if (this.popover.parentElement) {
26
+ this.updatePosition();
27
+ }
28
+ };
29
+ }
30
+
31
+ public attach(): void {
32
+ if (!this.popover.parentElement) {
33
+ this.options.container.appendChild(this.popover);
34
+ }
35
+ this.updatePosition();
36
+ window.addEventListener("resize", this.handleUpdate, { passive: true });
37
+ window.addEventListener("scroll", this.handleUpdate, { passive: true, capture: true });
38
+ }
39
+
40
+ public detach(): void {
41
+ window.removeEventListener("resize", this.handleUpdate);
42
+ window.removeEventListener("scroll", this.handleUpdate, { capture: true });
43
+ if (this.popover.parentElement) {
44
+ this.popover.parentElement.removeChild(this.popover);
45
+ }
46
+ }
47
+
48
+ public updatePosition(): void {
49
+ let refRect = this.reference.getBoundingClientRect();
50
+ if (refRect.width === 0 && refRect.height === 0) {
51
+ const card = this.reference.closest(".menu-item-card");
52
+ if (card) {
53
+ const barIcon = card.querySelector<HTMLElement>(".menu-item-icon-preview");
54
+ if (barIcon && barIcon.getBoundingClientRect().width > 0) {
55
+ refRect = barIcon.getBoundingClientRect();
56
+ }
57
+ }
58
+ }
59
+ const popRect = this.popover.getBoundingClientRect();
60
+ const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
61
+ const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
62
+ const padding = 8;
63
+ const offset = this.options.offset;
64
+
65
+ const placement = this.options.placement;
66
+
67
+ // Determine vertical direction if auto
68
+ const spaceBelow = viewportHeight - refRect.bottom - offset - padding;
69
+ const spaceAbove = refRect.top - offset - padding;
70
+
71
+ let isTop: boolean;
72
+ if (placement.startsWith("top")) {
73
+ // flip to bottom if top has less space
74
+ isTop = !(spaceAbove < popRect.height && spaceBelow > spaceAbove);
75
+ } else if (placement.startsWith("bottom")) {
76
+ // flip to top if bottom has less space
77
+ isTop = spaceBelow < popRect.height && spaceAbove > spaceBelow;
78
+ } else {
79
+ // auto
80
+ isTop = spaceBelow < 280 && spaceAbove > spaceBelow;
81
+ }
82
+
83
+ // Calculate Y
84
+ let top = isTop ? refRect.top - popRect.height - offset : refRect.bottom + offset;
85
+
86
+ // Constrain Y
87
+ if (top < padding) {
88
+ top = padding;
89
+ } else if (top + popRect.height > viewportHeight - padding) {
90
+ top = Math.max(padding, viewportHeight - popRect.height - padding);
91
+ }
92
+
93
+ // Calculate X
94
+ let left = refRect.left;
95
+ if (placement.endsWith("end")) {
96
+ left = refRect.right - popRect.width;
97
+ }
98
+
99
+ // Constrain X within viewport
100
+ if (left + popRect.width > viewportWidth - padding) {
101
+ left = viewportWidth - popRect.width - padding;
102
+ }
103
+ if (left < padding) {
104
+ left = padding;
105
+ }
106
+
107
+ this.popover.style.position = "fixed";
108
+ this.popover.style.zIndex = "99999";
109
+ this.popover.style.top = `${Math.round(top)}px`;
110
+ this.popover.style.left = `${Math.round(left)}px`;
111
+ this.popover.setAttribute("data-placement", isTop ? "top" : "bottom");
112
+ }
113
+ }
@@ -0,0 +1,173 @@
1
+ import type { IconPickerTheme, IconSetDefinition } from "../types";
2
+
3
+ export interface TemplateElements {
4
+ root: HTMLElement;
5
+ header: HTMLElement;
6
+ searchInput: HTMLInputElement;
7
+ searchClearBtn: HTMLButtonElement;
8
+ tabsContainer: HTMLElement;
9
+ body: HTMLElement;
10
+ grid: HTMLElement;
11
+ emptyState: HTMLElement;
12
+ footer: HTMLElement;
13
+ previewIcon: HTMLElement;
14
+ previewName: HTMLElement;
15
+ clearSelectionBtn: HTMLButtonElement;
16
+ }
17
+
18
+ export function createPopoverTemplate(
19
+ theme: IconPickerTheme = "default",
20
+ searchPlaceholder = "Search icons...",
21
+ emptyText = "No icons found matching your search"
22
+ ): TemplateElements {
23
+ const root = document.createElement("div");
24
+ root.className = `iconpicker-popover iconpicker-theme-${theme}`;
25
+ root.setAttribute("role", "dialog");
26
+ root.setAttribute("aria-modal", "false");
27
+
28
+ // Header
29
+ const header = document.createElement("div");
30
+ header.className = "iconpicker-header";
31
+
32
+ // Search input wrapper
33
+ const searchWrapper = document.createElement("div");
34
+ searchWrapper.className = "iconpicker-search-wrapper";
35
+
36
+ const searchIcon = document.createElement("span");
37
+ searchIcon.className = "iconpicker-search-icon";
38
+ searchIcon.innerHTML = `
39
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
40
+ <circle cx="11" cy="11" r="8"></circle>
41
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
42
+ </svg>
43
+ `;
44
+
45
+ const searchInput = document.createElement("input");
46
+ searchInput.type = "text";
47
+ searchInput.className = "iconpicker-search-input";
48
+ searchInput.placeholder = searchPlaceholder;
49
+ searchInput.autocomplete = "off";
50
+ searchInput.spellcheck = false;
51
+
52
+ const searchClearBtn = document.createElement("button");
53
+ searchClearBtn.type = "button";
54
+ searchClearBtn.className = "iconpicker-search-clear";
55
+ searchClearBtn.title = "Clear search";
56
+ searchClearBtn.style.display = "none";
57
+ searchClearBtn.innerHTML = `
58
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
59
+ <line x1="18" y1="6" x2="6" y2="18"></line>
60
+ <line x1="6" y1="6" x2="18" y2="18"></line>
61
+ </svg>
62
+ `;
63
+
64
+ searchWrapper.appendChild(searchIcon);
65
+ searchWrapper.appendChild(searchInput);
66
+ searchWrapper.appendChild(searchClearBtn);
67
+ header.appendChild(searchWrapper);
68
+
69
+ // Tabs container
70
+ const tabsContainer = document.createElement("div");
71
+ tabsContainer.className = "iconpicker-tabs";
72
+ header.appendChild(tabsContainer);
73
+
74
+ // Body
75
+ const body = document.createElement("div");
76
+ body.className = "iconpicker-body";
77
+
78
+ const grid = document.createElement("div");
79
+ grid.className = "iconpicker-grid";
80
+ grid.setAttribute("role", "listbox");
81
+
82
+ const emptyState = document.createElement("div");
83
+ emptyState.className = "iconpicker-empty";
84
+ emptyState.textContent = emptyText;
85
+ emptyState.style.display = "none";
86
+
87
+ body.appendChild(grid);
88
+ body.appendChild(emptyState);
89
+
90
+ // Footer
91
+ const footer = document.createElement("div");
92
+ footer.className = "iconpicker-footer";
93
+
94
+ const previewWrapper = document.createElement("div");
95
+ previewWrapper.className = "iconpicker-selected-preview";
96
+
97
+ const previewIcon = document.createElement("i");
98
+ previewIcon.className = "iconpicker-preview-icon";
99
+
100
+ const previewName = document.createElement("span");
101
+ previewName.className = "iconpicker-preview-name";
102
+ previewName.textContent = "None selected";
103
+
104
+ previewWrapper.appendChild(previewIcon);
105
+ previewWrapper.appendChild(previewName);
106
+
107
+ const clearSelectionBtn = document.createElement("button");
108
+ clearSelectionBtn.type = "button";
109
+ clearSelectionBtn.className = "iconpicker-btn-clear";
110
+ clearSelectionBtn.textContent = "Clear";
111
+
112
+ footer.appendChild(previewWrapper);
113
+ footer.appendChild(clearSelectionBtn);
114
+
115
+ root.appendChild(header);
116
+ root.appendChild(body);
117
+ root.appendChild(footer);
118
+
119
+ return {
120
+ root,
121
+ header,
122
+ searchInput,
123
+ searchClearBtn,
124
+ tabsContainer,
125
+ body,
126
+ grid,
127
+ emptyState,
128
+ footer,
129
+ previewIcon,
130
+ previewName,
131
+ clearSelectionBtn
132
+ };
133
+ }
134
+
135
+ export function renderTabs(
136
+ container: HTMLElement,
137
+ sets: IconSetDefinition[],
138
+ activeSetId: string,
139
+ onSelectSet: (setId: string) => void
140
+ ): void {
141
+ container.innerHTML = "";
142
+ if (sets.length <= 1) {
143
+ container.style.display = "none";
144
+ return;
145
+ }
146
+ container.style.display = "flex";
147
+
148
+ for (const set of sets) {
149
+ const tab = document.createElement("button");
150
+ tab.type = "button";
151
+ tab.className = `iconpicker-tab ${set.id === activeSetId ? "is-active" : ""}`;
152
+ tab.setAttribute("data-set-id", set.id);
153
+ tab.title = set.title;
154
+
155
+ const label = document.createElement("span");
156
+ label.className = "iconpicker-tab-label";
157
+ label.textContent = set.title;
158
+
159
+ const count = document.createElement("span");
160
+ count.className = "iconpicker-tab-count";
161
+ count.textContent = `${set.icons.length}`;
162
+
163
+ tab.appendChild(label);
164
+ tab.appendChild(count);
165
+
166
+ tab.addEventListener("click", (e) => {
167
+ e.stopPropagation();
168
+ onSelectSet(set.id);
169
+ });
170
+
171
+ container.appendChild(tab);
172
+ }
173
+ }