@nice2dev/icons 1.3.7 → 1.3.9

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 (99) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +33 -16
  3. package/dist/cjs/NiceIconPicker.js +289 -92
  4. package/dist/cjs/NiceIconStacker.js +137 -108
  5. package/dist/cjs/client.js +6 -0
  6. package/dist/cjs/createIcon.js +17 -4
  7. package/dist/cjs/iconHtml.js +122 -0
  8. package/dist/cjs/iconParts.js +24 -2
  9. package/dist/cjs/iconStack.js +76 -5
  10. package/dist/cjs/icons/accessibility.js +9 -8
  11. package/dist/cjs/icons/actions.js +38 -35
  12. package/dist/cjs/icons/ai.js +12 -11
  13. package/dist/cjs/icons/brand.js +2 -2
  14. package/dist/cjs/icons/business.js +14 -16
  15. package/dist/cjs/icons/devtech.js +17 -15
  16. package/dist/cjs/icons/ecommerce.js +7 -12
  17. package/dist/cjs/icons/education.js +14 -16
  18. package/dist/cjs/icons/files.js +46 -22
  19. package/dist/cjs/icons/fintech.js +2 -11
  20. package/dist/cjs/icons/food.js +12 -8
  21. package/dist/cjs/icons/fun.js +11 -11
  22. package/dist/cjs/icons/gaming.js +20 -13
  23. package/dist/cjs/icons/health.js +9 -12
  24. package/dist/cjs/icons/index.js +1 -0
  25. package/dist/cjs/icons/input.js +31 -21
  26. package/dist/cjs/icons/legal.js +11 -11
  27. package/dist/cjs/icons/media.js +24 -15
  28. package/dist/cjs/icons/navigation.js +16 -13
  29. package/dist/cjs/icons/omniverk.js +2 -7
  30. package/dist/cjs/icons/realestate.js +14 -14
  31. package/dist/cjs/icons/saas.js +0 -10
  32. package/dist/cjs/icons/science.js +12 -12
  33. package/dist/cjs/icons/shapes.js +20 -17
  34. package/dist/cjs/icons/smarthome.js +0 -11
  35. package/dist/cjs/icons/social.js +7 -6
  36. package/dist/cjs/icons/sports.js +5 -6
  37. package/dist/cjs/icons/sustainability.js +12 -11
  38. package/dist/cjs/icons/travel.js +9 -9
  39. package/dist/cjs/icons/ui.js +55 -31
  40. package/dist/cjs/icons/weather.js +8 -13
  41. package/dist/cjs/index.js +7 -0
  42. package/dist/cjs/nicetodev-icons.css +1 -1
  43. package/dist/cjs/resolver.js +1 -0
  44. package/dist/cjs/stacking.js +38 -11
  45. package/dist/esm/NiceIconPicker.js +292 -95
  46. package/dist/esm/NiceIconStacker.js +141 -112
  47. package/dist/esm/client.js +2 -1
  48. package/dist/esm/createIcon.js +18 -5
  49. package/dist/esm/iconHtml.js +117 -0
  50. package/dist/esm/iconParts.js +24 -3
  51. package/dist/esm/iconStack.js +74 -6
  52. package/dist/esm/icons/accessibility.js +9 -8
  53. package/dist/esm/icons/actions.js +39 -36
  54. package/dist/esm/icons/ai.js +12 -11
  55. package/dist/esm/icons/brand.js +2 -2
  56. package/dist/esm/icons/business.js +15 -17
  57. package/dist/esm/icons/devtech.js +17 -15
  58. package/dist/esm/icons/ecommerce.js +7 -12
  59. package/dist/esm/icons/education.js +14 -16
  60. package/dist/esm/icons/files.js +47 -23
  61. package/dist/esm/icons/fintech.js +2 -11
  62. package/dist/esm/icons/food.js +12 -8
  63. package/dist/esm/icons/fun.js +11 -11
  64. package/dist/esm/icons/gaming.js +21 -14
  65. package/dist/esm/icons/health.js +9 -12
  66. package/dist/esm/icons/index.js +1 -1
  67. package/dist/esm/icons/input.js +31 -21
  68. package/dist/esm/icons/legal.js +12 -12
  69. package/dist/esm/icons/media.js +25 -16
  70. package/dist/esm/icons/navigation.js +16 -13
  71. package/dist/esm/icons/omniverk.js +2 -7
  72. package/dist/esm/icons/realestate.js +15 -15
  73. package/dist/esm/icons/saas.js +0 -10
  74. package/dist/esm/icons/science.js +12 -12
  75. package/dist/esm/icons/shapes.js +20 -17
  76. package/dist/esm/icons/smarthome.js +0 -11
  77. package/dist/esm/icons/social.js +8 -7
  78. package/dist/esm/icons/sports.js +6 -7
  79. package/dist/esm/icons/sustainability.js +12 -11
  80. package/dist/esm/icons/travel.js +10 -10
  81. package/dist/esm/icons/ui.js +56 -33
  82. package/dist/esm/icons/weather.js +9 -14
  83. package/dist/esm/index.js +3 -2
  84. package/dist/esm/nicetodev-icons.css +1 -1
  85. package/dist/esm/resolver.js +1 -0
  86. package/dist/esm/stacking.js +36 -12
  87. package/dist/types/NiceIconPicker.d.ts +66 -20
  88. package/dist/types/NiceIconStacker.d.ts +30 -9
  89. package/dist/types/client.d.ts +4 -2
  90. package/dist/types/createIcon.d.ts +16 -2
  91. package/dist/types/iconHtml.d.ts +44 -0
  92. package/dist/types/iconParts.d.ts +8 -2
  93. package/dist/types/iconStack.d.ts +30 -3
  94. package/dist/types/icons/index.d.ts +1 -1
  95. package/dist/types/icons/ui.d.ts +1 -0
  96. package/dist/types/index.d.ts +4 -2
  97. package/dist/types/stacking.d.ts +25 -5
  98. package/dist/types/types.d.ts +11 -7
  99. package/package.json +2 -2
@@ -1,19 +1,45 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
- import { useRovingFocus, useNiceTranslation, useOverlayFocus } from '@nice2dev/ui-core';
3
- import { forwardRef, useRef, useState, useMemo, useCallback, useImperativeHandle, useEffect } from 'react';
2
+ import { useRovingFocus, useNiceTranslation, useControllableState, useOverlayFocus } from '@nice2dev/ui-core';
3
+ import { forwardRef, useRef, useState, useId, useMemo, useCallback, useImperativeHandle, useEffect, useLayoutEffect } from 'react';
4
+ import { parseIconHtml, scopeIconHtmlIds, iconHtmlAttributes } from './iconHtml.js';
4
5
  import { NtdSearch, NtdCheck } from './icons/actions.js';
5
6
  import { NtdGrid } from './icons/files.js';
6
7
  import { NtdInbox } from './icons/media.js';
7
- import { NtdChevronDown } from './icons/navigation.js';
8
+ import { NtdArrowLeft, NtdChevronDown } from './icons/navigation.js';
8
9
  import { NtdStar, NtdClock } from './icons/ui.js';
10
+ import { iconDisplayName, renderIconStack } from './iconStack.js';
11
+ import { NiceIconStacker } from './NiceIconStacker.js';
9
12
 
10
13
  const MAX_HISTORY_ITEMS = 20;
14
+ /** `useLayoutEffect` in the browser, `useEffect` on the server (no warning there). */
15
+ const useIsoLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
11
16
  /* ─── Small icons: the package's own (until 1.2.0 these were pasted SVGs) ─── */
12
17
  const Chevron = () => jsx(NtdChevronDown, { size: 12, variant: "outline" });
13
18
  const SearchIcon = () => jsx(NtdSearch, { size: 14, variant: "outline" });
14
19
  const StarIcon = ({ filled }) => (jsx(NtdStar, { size: 12, variant: filled ? 'solid' : 'outline' }));
15
20
  const ClockIcon = () => jsx(NtdClock, { size: 12, variant: "outline" });
16
21
  const CheckIcon = () => jsx(NtdCheck, { size: 10, variant: "outline" });
22
+ const NO_NAMES = [];
23
+ const NO_LAYERS = [];
24
+ /** The accent the picker draws with: the page's background, white without a theme. */
25
+ const PAGE_ACCENT = 'var(--nice-bg-primary, white)';
26
+ /** An entry's name as shown: its label, else the export name without `Ntd`. */
27
+ const entryLabel = (entry) => { var _a; return (_a = entry.label) !== null && _a !== void 0 ? _a : iconDisplayName(entry.name); };
28
+ /** The part of the window an element's popup can show in: the window, cut by every ancestor that clips or scrolls. */
29
+ function visibleBounds(el) {
30
+ const bounds = { top: 0, bottom: window.innerHeight, left: 0, right: window.innerWidth };
31
+ for (let p = el.parentElement; p && p !== document.body; p = p.parentElement) {
32
+ const style = window.getComputedStyle(p);
33
+ if (style.overflowX !== 'visible' || style.overflowY !== 'visible') {
34
+ const r = p.getBoundingClientRect();
35
+ bounds.top = Math.max(bounds.top, r.top);
36
+ bounds.bottom = Math.min(bounds.bottom, r.bottom);
37
+ bounds.left = Math.max(bounds.left, r.left);
38
+ bounds.right = Math.min(bounds.right, r.right);
39
+ }
40
+ }
41
+ return bounds;
42
+ }
17
43
  /* ─── localStorage helpers ─── */
18
44
  function loadFromStorage(key, fallback) {
19
45
  if (typeof window === 'undefined') {
@@ -44,40 +70,43 @@ function saveToStorage(key, value) {
44
70
  /**
45
71
  * A fully-featured icon picker dropdown component.
46
72
  *
47
- * Provides a trigger button that opens a searchable, categorized icon grid.
48
- * Supports controlled selection, keyboard navigation (Escape to close),
49
- * click-outside dismissal, and an imperative handle for programmatic control.
73
+ * Provides a trigger button that opens a searchable, categorized icon grid. The value binds both
74
+ * ways — `value` / `defaultValue` + `onChange` (1.3.9) — as the icon's export name or, with
75
+ * `valueFormat="html"`, as the icon drawn in HTML, ready to store. With `stackable` the panel gets
76
+ * an "Overlays" tab where icons are stacked on the chosen one. Names show without `Ntd`; the
77
+ * trigger shows the icon alone or the icon and its name (`displayMode`). Keyboard: arrows in the
78
+ * grid, Escape to close.
50
79
  *
51
80
  * **Styling:** Uses CSS variables for theming (see `NiceIconPicker.css`).
52
81
  * Override `--ntd-picker-bg`, `--ntd-picker-accent`, etc. on a parent element.
53
82
  *
54
83
  * @example
55
84
  * ```tsx
56
- * import { NiceIconPicker } from '@nicetodev/icons';
57
- *
58
- * function IconSelectField({ icons }) {
59
- * const [selected, setSelected] = useState<string | null>(null);
85
+ * import { NiceIconPicker } from '@nice2dev/icons';
60
86
  *
61
- * return (
62
- * <NiceIconPicker
63
- * icons={icons}
64
- * value={selected}
65
- * onChange={(name) => setSelected(name)}
66
- * variant="solid"
67
- * placeholder="Pick icon..."
68
- * />
69
- * );
87
+ * function IconField({ icons, value, onChange }) {
88
+ * // stores `<svg data-ntd-icon="NtdStar" …>…</svg>`, a stack included
89
+ * return <NiceIconPicker icons={icons} value={value} onChange={onChange} stackable />;
70
90
  * }
71
91
  * ```
72
92
  */
73
- const NiceIconPicker = /* @__PURE__ */ forwardRef(function NiceIconPicker({ icons, value = null, values = [], onChange, onChangeMulti, multiSelect = false, maxSelections, placeholder, variant = 'solid', gridIconSize = 'md', triggerIconSize = 'sm', categories: categoriesProp, className, disabled = false, clearable = true, virtualized = false, virtualizedItemCount = 100, showHistory = false, showFavorites = false, storageKeyPrefix = 'ntd-icon-picker', enableDragAndDrop = false, onDragStart, onDragEnd, }, ref) {
93
+ const NiceIconPicker = /* @__PURE__ */ forwardRef(function NiceIconPicker({ icons, value: valueProp, defaultValue, values: valuesProp, defaultValues, onChange, onChangeMulti, valueFormat: valueFormatProp, stackable = false, displayMode = 'icon-name', optionDisplayMode = 'icon', name, 'aria-label': ariaLabel, id, testId, 'data-testid': dataTestId, multiSelect = false, maxSelections, placeholder, variant = 'solid', iconProps, gridIconSize = 'md', triggerIconSize = 'sm', categories: categoriesProp, className, disabled = false, clearable = true, virtualized = false, virtualizedItemCount = 100, showHistory = false, showFavorites = false, storageKeyPrefix = 'ntd-icon-picker', enableDragAndDrop = false, onDragStart, onDragEnd, }, ref) {
74
94
  var _a;
75
95
  const tablistRef = useRef(null);
76
96
  useRovingFocus(tablistRef, { orientation: 'horizontal', wrap: true, activateOnMove: true });
97
+ const viewsRef = useRef(null);
98
+ useRovingFocus(viewsRef, { orientation: 'horizontal', activateOnMove: true });
77
99
  const { t } = useNiceTranslation();
78
100
  const [isOpen, setIsOpen] = useState(false);
79
101
  const [search, setSearch] = useState('');
80
102
  const [activeCategory, setActiveCategory] = useState(null);
103
+ // the panel's view: the icon grid or the stack; `picking` = the grid chooses an icon to stack
104
+ const [view, setView] = useState('icons');
105
+ const [picking, setPicking] = useState(null);
106
+ const panelId = useId();
107
+ const valueFormat = valueFormatProp !== null && valueFormatProp !== void 0 ? valueFormatProp : (stackable ? 'html' : 'name');
108
+ const canStack = stackable && !multiSelect && valueFormat === 'html';
109
+ const drawProps = useMemo(() => ({ accentColor: PAGE_ACCENT, ...iconProps, variant }), [iconProps, variant]);
81
110
  // Virtualization state
82
111
  const [visibleRange, setVisibleRange] = useState({ start: 0, end: virtualizedItemCount });
83
112
  const gridWrapRef = useRef(null);
@@ -86,35 +115,105 @@ const NiceIconPicker = /* @__PURE__ */ forwardRef(function NiceIconPicker({ icon
86
115
  const historyKey = `${storageKeyPrefix}-history`;
87
116
  const [favorites, setFavorites] = useState(() => loadFromStorage(favoritesKey, []));
88
117
  const [history, setHistory] = useState(() => loadFromStorage(historyKey, []));
89
- // Multi-select state (internal when uncontrolled)
90
- const [internalValues, setInternalValues] = useState([]);
91
- const selectedValues = useMemo(() => (multiSelect ? (values.length > 0 ? values : internalValues) : value ? [value] : []), [multiSelect, values, internalValues, value]);
118
+ // The value binds both ways (1.3.9): controlled by `value`, or kept here from `defaultValue`.
119
+ const [value, setValue] = useControllableState({
120
+ value: valueProp,
121
+ defaultValue,
122
+ fallbackValue: null,
123
+ componentName: 'NiceIconPicker',
124
+ });
125
+ const [values, setValues] = useControllableState({
126
+ value: valuesProp,
127
+ defaultValue: defaultValues,
128
+ fallbackValue: NO_NAMES,
129
+ componentName: 'NiceIconPicker',
130
+ });
92
131
  const rootRef = useRef(null);
93
132
  const searchInputRef = useRef(null);
133
+ const stackerRef = useRef(null);
134
+ const serializerRef = useRef(null);
135
+ /* ─── Icons by name: exact, or without the Ntd a stored value may lack ─── */
136
+ const byName = useMemo(() => new Map(icons.map((e) => [e.name, e])), [icons]);
137
+ const entryOf = useCallback((n) => { var _a; return (n ? ((_a = byName.get(n)) !== null && _a !== void 0 ? _a : byName.get(`Ntd${n}`)) : undefined); }, [byName]);
138
+ const labelOf = useCallback((n) => {
139
+ const entry = entryOf(n);
140
+ return entry ? entryLabel(entry) : iconDisplayName(n);
141
+ }, [entryOf]);
142
+ const resolve = useCallback((n) => { var _a; return (_a = entryOf(n)) === null || _a === void 0 ? void 0 : _a.Component; }, [entryOf]);
143
+ /* ─── The single value as a stack — a plain icon is a stack of one ─── */
144
+ const model = useMemo(() => {
145
+ if (!value) {
146
+ return NO_LAYERS;
147
+ }
148
+ const source = parseIconHtml(value);
149
+ if (source) {
150
+ return source.layers;
151
+ }
152
+ const entry = value.includes('<') ? undefined : entryOf(value);
153
+ return entry ? [{ id: '1', icon: entry.name }] : NO_LAYERS;
154
+ }, [value, entryOf]);
155
+ // A stack waiting to be drawn into HTML: shown at once, emitted once drawn (below).
156
+ const [pending, setPending] = useState(null);
157
+ const shown = pending !== null && pending !== void 0 ? pending : model;
158
+ const base = shown[0];
159
+ const onChangeRef = useRef(onChange);
160
+ onChangeRef.current = onChange;
161
+ const emit = useCallback((next, baseName) => {
162
+ var _a, _b;
163
+ setValue(next);
164
+ (_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, next, (_b = entryOf(baseName)) !== null && _b !== void 0 ? _b : null);
165
+ }, [setValue, entryOf]);
166
+ /** Sets the single value to a stack: its base's name, or the stack drawn as HTML. */
167
+ const commit = useCallback((layers) => {
168
+ if (layers.length === 0) {
169
+ setPending(null);
170
+ emit(null);
171
+ }
172
+ else if (valueFormat === 'name') {
173
+ emit(layers[0].icon, layers[0].icon);
174
+ }
175
+ else {
176
+ setPending(layers);
177
+ }
178
+ }, [valueFormat, emit]);
179
+ // The HTML is the icon as drawn: the hidden copy below renders the pending stack, and its
180
+ // markup — ids renamed after the drawing — is the new value, emitted before the paint.
181
+ useIsoLayoutEffect(() => {
182
+ var _a;
183
+ if (!pending) {
184
+ return;
185
+ }
186
+ const svg = (_a = serializerRef.current) === null || _a === void 0 ? void 0 : _a.querySelector('svg');
187
+ setPending(null);
188
+ if (svg) {
189
+ emit(scopeIconHtmlIds(svg.outerHTML), pending[0].icon);
190
+ }
191
+ }, [pending, emit]);
192
+ const selectedValues = useMemo(() => (multiSelect ? values : base ? [base.icon] : NO_NAMES), [multiSelect, values, base]);
94
193
  /* ─── Favorites helpers ─── */
95
- const addFavorite = useCallback((name) => {
194
+ const addFavorite = useCallback((favorite) => {
96
195
  setFavorites((prev) => {
97
- if (prev.includes(name)) {
196
+ if (prev.includes(favorite)) {
98
197
  return prev;
99
198
  }
100
- const next = [...prev, name];
199
+ const next = [...prev, favorite];
101
200
  saveToStorage(favoritesKey, next);
102
201
  return next;
103
202
  });
104
203
  }, [favoritesKey]);
105
- const removeFavorite = useCallback((name) => {
204
+ const removeFavorite = useCallback((favorite) => {
106
205
  setFavorites((prev) => {
107
- const next = prev.filter((n) => n !== name);
206
+ const next = prev.filter((n) => n !== favorite);
108
207
  saveToStorage(favoritesKey, next);
109
208
  return next;
110
209
  });
111
210
  }, [favoritesKey]);
112
- const toggleFavorite = useCallback((name) => {
113
- if (favorites.includes(name)) {
114
- removeFavorite(name);
211
+ const toggleFavorite = useCallback((favorite) => {
212
+ if (favorites.includes(favorite)) {
213
+ removeFavorite(favorite);
115
214
  }
116
215
  else {
117
- addFavorite(name);
216
+ addFavorite(favorite);
118
217
  }
119
218
  }, [favorites, addFavorite, removeFavorite]);
120
219
  const clearFavorites = useCallback(() => {
@@ -122,10 +221,10 @@ const NiceIconPicker = /* @__PURE__ */ forwardRef(function NiceIconPicker({ icon
122
221
  saveToStorage(favoritesKey, []);
123
222
  }, [favoritesKey]);
124
223
  /* ─── History helpers ─── */
125
- const addToHistory = useCallback((name) => {
224
+ const addToHistory = useCallback((used) => {
126
225
  setHistory((prev) => {
127
- const filtered = prev.filter((n) => n !== name);
128
- const next = [name, ...filtered].slice(0, MAX_HISTORY_ITEMS);
226
+ const filtered = prev.filter((n) => n !== used);
227
+ const next = [used, ...filtered].slice(0, MAX_HISTORY_ITEMS);
129
228
  saveToStorage(historyKey, next);
130
229
  return next;
131
230
  });
@@ -162,12 +261,14 @@ const NiceIconPicker = /* @__PURE__ */ forwardRef(function NiceIconPicker({ icon
162
261
  }
163
262
  return result;
164
263
  }, [icons, categoriesProp]);
165
- /* ─── Filter icons ─── */
264
+ /* ─── Filter icons: by the name shown, the export name or the category ─── */
166
265
  const filtered = useMemo(() => {
167
266
  let items = icons;
168
- if (search.trim()) {
169
- const q = search.toLowerCase();
170
- items = items.filter((i) => i.name.toLowerCase().includes(q) || i.category.toLowerCase().includes(q));
267
+ const q = search.trim().toLowerCase();
268
+ if (q) {
269
+ items = items.filter((i) => entryLabel(i).toLowerCase().includes(q) ||
270
+ i.name.toLowerCase().includes(q) ||
271
+ i.category.toLowerCase().includes(q));
171
272
  }
172
273
  if (activeCategory === '__favorites__') {
173
274
  items = items.filter((i) => favorites.includes(i.name));
@@ -205,13 +306,8 @@ const NiceIconPicker = /* @__PURE__ */ forwardRef(function NiceIconPicker({ icon
205
306
  }
206
307
  return map;
207
308
  }, [filtered, virtualized, virtualizedFiltered]);
208
- /* ─── Selected entries ─── */
209
- const selectedEntries = useMemo(() => {
210
- return selectedValues
211
- .map((v) => icons.find((i) => i.name === v))
212
- .filter((e) => e !== undefined);
213
- }, [icons, selectedValues]);
214
- const selectedEntry = (_a = selectedEntries[0]) !== null && _a !== void 0 ? _a : null;
309
+ /* ─── Selected entries (multi-select) ─── */
310
+ const selectedEntries = useMemo(() => values.map((v) => entryOf(v)).filter((e) => e !== undefined), [values, entryOf]);
215
311
  /* ─── Click outside ─── */
216
312
  useEffect(() => {
217
313
  if (!isOpen) {
@@ -230,6 +326,8 @@ const NiceIconPicker = /* @__PURE__ */ forwardRef(function NiceIconPicker({ icon
230
326
  if (!isOpen) {
231
327
  setSearch('');
232
328
  setActiveCategory(null);
329
+ setView('icons');
330
+ setPicking(null);
233
331
  setVisibleRange({ start: 0, end: virtualizedItemCount });
234
332
  }
235
333
  }, [isOpen, virtualizedItemCount]);
@@ -237,14 +335,27 @@ const NiceIconPicker = /* @__PURE__ */ forwardRef(function NiceIconPicker({ icon
237
335
  // The panel is a dialog on the overlay stack: focus on the search field when it opens, Escape
238
336
  // / B close only it (a document listener before) and focus returns to the trigger. The icon
239
337
  // grid is an APG listbox moving in two dimensions — the cells were reachable by Tab only.
338
+ // While the grid picks an icon to stack, Escape goes back to the stack instead (1.3.9).
240
339
  const dropdownRef = useRef(null);
241
340
  useOverlayFocus(dropdownRef, {
242
341
  open: isOpen,
243
342
  modal: false,
244
343
  initialFocus: searchInputRef,
245
- onClose: () => setIsOpen(false),
344
+ onClose: () => (picking ? setPicking(null) : setIsOpen(false)),
246
345
  });
247
346
  useRovingFocus(gridWrapRef, { orientation: 'grid', itemSelector: '.ntd-picker-cell' });
347
+ // into the grid to pick an icon to stack: focus the search; back: the layer it went into
348
+ const wasPicking = useRef(false);
349
+ useEffect(() => {
350
+ var _a, _b, _c;
351
+ if (picking) {
352
+ (_a = searchInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
353
+ }
354
+ else if (wasPicking.current) {
355
+ (_c = (_b = dropdownRef.current) === null || _b === void 0 ? void 0 : _b.querySelector('.nice-icon-stacker__layer--selected .nice-icon-stacker__layer-select')) === null || _c === void 0 ? void 0 : _c.focus();
356
+ }
357
+ wasPicking.current = !!picking;
358
+ }, [picking]);
248
359
  /* ─── Virtualization scroll handler ─── */
249
360
  useEffect(() => {
250
361
  if (!virtualized || !isOpen || !gridWrapRef.current) {
@@ -272,63 +383,83 @@ const NiceIconPicker = /* @__PURE__ */ forwardRef(function NiceIconPicker({ icon
272
383
  }, [virtualized, isOpen, filtered.length]);
273
384
  /* ─── Handlers ─── */
274
385
  const handleSelect = useCallback((entry) => {
386
+ var _a;
387
+ // an icon to stack: into the layer the stacker has selected, then back to the stack
388
+ if (picking) {
389
+ (_a = stackerRef.current) === null || _a === void 0 ? void 0 : _a.add({ icon: entry.name });
390
+ setPicking(null);
391
+ setSearch('');
392
+ return;
393
+ }
275
394
  // Add to history
276
395
  if (showHistory) {
277
396
  addToHistory(entry.name);
278
397
  }
279
398
  if (multiSelect) {
280
- const isSelected = selectedValues.includes(entry.name);
399
+ const isSelected = values.includes(entry.name);
281
400
  let newValues;
282
401
  if (isSelected) {
283
- newValues = selectedValues.filter((v) => v !== entry.name);
402
+ newValues = values.filter((v) => v !== entry.name);
284
403
  }
285
404
  else {
286
- if (maxSelections && selectedValues.length >= maxSelections) {
405
+ if (maxSelections && values.length >= maxSelections) {
287
406
  return; // Max selections reached
288
407
  }
289
- newValues = [...selectedValues, entry.name];
408
+ newValues = [...values, entry.name];
290
409
  }
410
+ setValues(newValues);
291
411
  if (onChangeMulti) {
292
412
  const entries = newValues
293
- .map((v) => icons.find((i) => i.name === v))
413
+ .map((v) => entryOf(v))
294
414
  .filter((e) => e !== undefined);
295
415
  onChangeMulti(newValues, entries);
296
416
  }
297
- setInternalValues(newValues);
298
417
  // Don't close in multi-select mode
299
418
  }
300
419
  else {
301
- onChange === null || onChange === void 0 ? void 0 : onChange(entry.name, entry);
420
+ // the HTML records the variant it was drawn in; swapping the icon keeps its stack
421
+ const own = valueFormat === 'html' ? { variant } : {};
422
+ commit(canStack && base
423
+ ? [{ ...base, icon: entry.name, ...own }, ...shown.slice(1)]
424
+ : [{ id: '1', icon: entry.name, ...own }]);
302
425
  setIsOpen(false);
303
426
  }
304
427
  }, [
428
+ picking,
429
+ variant,
305
430
  multiSelect,
306
- selectedValues,
431
+ values,
432
+ setValues,
307
433
  maxSelections,
308
- icons,
309
- onChange,
434
+ entryOf,
310
435
  onChangeMulti,
311
436
  showHistory,
312
437
  addToHistory,
438
+ valueFormat,
439
+ canStack,
440
+ base,
441
+ shown,
442
+ commit,
313
443
  ]);
314
444
  const handleClear = useCallback((e) => {
315
445
  e.stopPropagation();
316
446
  if (multiSelect) {
447
+ setValues(NO_NAMES);
317
448
  onChangeMulti === null || onChangeMulti === void 0 ? void 0 : onChangeMulti([], []);
318
- setInternalValues([]);
319
449
  }
320
450
  else {
321
- onChange === null || onChange === void 0 ? void 0 : onChange(null, null);
451
+ commit(NO_LAYERS);
452
+ setView('icons');
322
453
  }
323
- }, [multiSelect, onChange, onChangeMulti]);
454
+ }, [multiSelect, setValues, onChangeMulti, commit]);
324
455
  const handleToggle = useCallback(() => {
325
456
  if (!disabled) {
326
457
  setIsOpen((o) => !o);
327
458
  }
328
459
  }, [disabled]);
329
- const handleFavoriteClick = useCallback((e, name) => {
460
+ const handleFavoriteClick = useCallback((e, favorite) => {
330
461
  e.stopPropagation();
331
- toggleFavorite(name);
462
+ toggleFavorite(favorite);
332
463
  }, [toggleFavorite]);
333
464
  /* ─── Drag & Drop handlers ─── */
334
465
  const handleDragStart = useCallback((entry, e) => {
@@ -336,8 +467,7 @@ const NiceIconPicker = /* @__PURE__ */ forwardRef(function NiceIconPicker({ icon
336
467
  return;
337
468
  }
338
469
  // Set various data formats for maximum compatibility
339
- const name = entry.name;
340
- const readName = readableName(name);
470
+ const readName = entryLabel(entry);
341
471
  // Plain text - icon name
342
472
  e.dataTransfer.setData('text/plain', readName);
343
473
  // JSON data with full icon info
@@ -349,14 +479,9 @@ const NiceIconPicker = /* @__PURE__ */ forwardRef(function NiceIconPicker({ icon
349
479
  };
350
480
  e.dataTransfer.setData('application/json', JSON.stringify(iconData));
351
481
  e.dataTransfer.setData('text/x-ntd-icon', JSON.stringify(iconData));
352
- // Try to render SVG to data URL for image dragging
482
+ // The cell's own drawing as an image
353
483
  try {
354
- const tempContainer = document.createElement('div');
355
- tempContainer.style.position = 'absolute';
356
- tempContainer.style.left = '-9999px';
357
- document.body.appendChild(tempContainer);
358
- // Use dynamic import workaround - render to string
359
- const svgElement = document.querySelector(`[data-name="${readName}"] svg`);
484
+ const svgElement = e.currentTarget.querySelector('svg');
360
485
  if (svgElement) {
361
486
  const svgClone = svgElement.cloneNode(true);
362
487
  svgClone.setAttribute('width', '64');
@@ -369,7 +494,6 @@ const NiceIconPicker = /* @__PURE__ */ forwardRef(function NiceIconPicker({ icon
369
494
  img.src = URL.createObjectURL(svgBlob);
370
495
  e.dataTransfer.setDragImage(img, 32, 32);
371
496
  }
372
- document.body.removeChild(tempContainer);
373
497
  }
374
498
  catch (_a) {
375
499
  // Fallback - use default drag image
@@ -380,33 +504,106 @@ const NiceIconPicker = /* @__PURE__ */ forwardRef(function NiceIconPicker({ icon
380
504
  const handleDragEnd = useCallback((entry, e) => {
381
505
  onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd(entry, e);
382
506
  }, [onDragEnd]);
383
- const readableName = (name) => name.replace(/^Ntd/, '');
384
507
  const triggerLabel = useMemo(() => {
385
- if (multiSelect && selectedEntries.length > 0) {
508
+ if (multiSelect) {
386
509
  if (selectedEntries.length === 1) {
387
- return readableName(selectedEntries[0].name);
510
+ return entryLabel(selectedEntries[0]);
388
511
  }
389
- return t('iconPicker.iconsSelected', '{count} icons selected').replace('{count}', String(selectedEntries.length));
390
- }
391
- return selectedEntry
392
- ? readableName(selectedEntry.name)
393
- : (placeholder !== null && placeholder !== void 0 ? placeholder : t('iconPicker.selectIcon', 'Select icon…'));
394
- }, [multiSelect, selectedEntries, selectedEntry, placeholder, t]);
395
- return (jsxs("div", { ref: rootRef, className: `ntd-picker${isOpen ? ' ntd-picker--open' : ''}${disabled ? ' ntd-picker--disabled' : ''}${multiSelect ? ' ntd-picker--multi' : ''}${className ? ` ${className}` : ''}`, children: [jsxs("button", { type: "button", className: "ntd-picker-trigger", onClick: handleToggle, disabled: disabled, "aria-haspopup": "listbox", "aria-expanded": isOpen, children: [jsx("span", { className: "ntd-picker-trigger-icon", children: selectedEntry ? (jsx(selectedEntry.Component, { size: triggerIconSize, variant: variant })) : multiSelect && selectedEntries.length > 1 ? (jsx("span", { className: "ntd-picker-multi-badge", children: selectedEntries.length })) : (jsx(NtdGrid, { size: 16, variant: "outline" })) }), jsx("span", { className: `ntd-picker-trigger-label${!selectedEntry && selectedEntries.length === 0 ? ' ntd-picker-trigger-placeholder' : ''}`, children: triggerLabel }), jsx("span", { className: "ntd-picker-trigger-chevron", children: jsx(Chevron, {}) })] }), isOpen && (jsxs("div", { ref: dropdownRef, className: "ntd-picker-dropdown", role: "dialog", "aria-label": t('iconPicker.selectIcon', 'Select icon…'), children: [jsxs("div", { className: "ntd-picker-search", children: [jsx("span", { className: "ntd-picker-search-icon", children: jsx(SearchIcon, {}) }), jsx("input", { ref: searchInputRef, type: "text", placeholder: t('iconPicker.searchIcons', 'Search icons…'), value: search, onChange: (e) => setSearch(e.target.value), "aria-label": t('iconPicker.searchIcons2', 'Search icons') })] }), (allCategories.length > 1 || showFavorites || showHistory) && (jsxs("div", { className: "ntd-picker-tabs", ref: tablistRef, role: "tablist", children: [jsx("button", { type: "button", className: `ntd-picker-tab${activeCategory === null ? ' ntd-picker-tab--active' : ''}`, onClick: () => setActiveCategory(null), role: "tab", "aria-selected": activeCategory === null, children: t('iconPicker.all', 'All') }), showFavorites && (jsxs("button", { type: "button", className: `ntd-picker-tab ntd-picker-tab-special${activeCategory === '__favorites__' ? ' ntd-picker-tab--active' : ''}`, onClick: () => setActiveCategory('__favorites__'), role: "tab", "aria-selected": activeCategory === '__favorites__', title: t('iconPicker.favorites', 'Favorites'), children: [jsx(StarIcon, { filled: true }), " ", t('iconPicker.favorites', 'Favorites'), favorites.length > 0 && (jsx("span", { className: "ntd-picker-tab-count", children: favorites.length }))] })), showHistory && history.length > 0 && (jsxs("button", { type: "button", className: `ntd-picker-tab ntd-picker-tab-special${activeCategory === '__history__' ? ' ntd-picker-tab--active' : ''}`, onClick: () => setActiveCategory('__history__'), role: "tab", "aria-selected": activeCategory === '__history__', title: t('iconPicker.recentlyUsed', 'Recently used'), children: [jsx(ClockIcon, {}), " ", t('iconPicker.recent', 'Recent')] })), allCategories.map((cat) => (jsx("button", { type: "button", className: `ntd-picker-tab${activeCategory === cat ? ' ntd-picker-tab--active' : ''}`, onClick: () => setActiveCategory(cat), role: "tab", "aria-selected": activeCategory === cat, children: cat }, cat)))] })), jsx("div", { className: "ntd-picker-grid-wrap", ref: gridWrapRef, role: "listbox", "aria-label": t('iconPicker.icons', 'Icons'), "aria-multiselectable": multiSelect || undefined, children: filtered.length === 0 ? (jsxs("div", { className: "ntd-picker-empty", children: [jsx("div", { className: "ntd-picker-empty-icon", children: jsx(NtdInbox, { size: 28, variant: "outline" }) }), activeCategory === '__favorites__'
396
- ? t('iconPicker.noFavoritesYetClickThe', 'No favorites yet. Click the star icon to add.')
397
- : activeCategory === '__history__'
398
- ? t('iconPicker.noRecentIcons', 'No recent icons.')
399
- : t('iconPicker.noIconsFound', 'No icons found')] })) : (jsxs(Fragment, { children: [virtualized && visibleRange.start > 0 && (jsx("div", { style: { height: Math.floor(visibleRange.start / 6) * 48 } })), Array.from(grouped.entries()).map(([category, entries]) => (jsxs("div", { children: [!activeCategory && !(activeCategory === null || activeCategory === void 0 ? void 0 : activeCategory.startsWith('__')) && (jsx("div", { className: "ntd-picker-category-label", children: category })), jsx("div", { className: "ntd-picker-grid", children: entries.map((entry) => {
400
- const isSelected = selectedValues.includes(entry.name);
401
- const isFavorite = favorites.includes(entry.name);
402
- return (jsxs("button", { type: "button", className: `ntd-picker-cell${isSelected ? ' ntd-picker-cell--selected' : ''}${multiSelect ? ' ntd-picker-cell--multi-select' : ''}${enableDragAndDrop ? ' ntd-picker-cell--draggable' : ''}`, onClick: () => handleSelect(entry), "data-name": readableName(entry.name), role: "option", "aria-selected": isSelected, title: readableName(entry.name), draggable: enableDragAndDrop, onDragStart: enableDragAndDrop ? (e) => handleDragStart(entry, e) : undefined, onDragEnd: enableDragAndDrop ? (e) => handleDragEnd(entry, e) : undefined, children: [jsx(entry.Component, { size: gridIconSize, variant: variant }), multiSelect && isSelected && (jsx("span", { className: "ntd-picker-cell-check", children: jsx(CheckIcon, {}) })), showFavorites && (jsx("button", { type: "button", className: `ntd-picker-cell-fav${isFavorite ? ' ntd-picker-cell-fav--active' : ''}`, onClick: (e) => handleFavoriteClick(e, entry.name), title: isFavorite
403
- ? t('iconPicker.removeFromFavorites', 'Remove from favorites')
404
- : t('iconPicker.addToFavorites', 'Add to favorites'), children: jsx(StarIcon, { filled: isFavorite }) }))] }, entry.name));
405
- }) })] }, category))), virtualized && visibleRange.end < filtered.length && (jsx("div", { style: { height: Math.ceil((filtered.length - visibleRange.end) / 6) * 48 } }))] })) }), jsxs("div", { className: "ntd-picker-footer", children: [jsxs("span", { className: "ntd-picker-count", children: [(filtered.length === 1
406
- ? t('iconPicker.iconCountOne', '{count} icon')
407
- : t('iconPicker.iconCountMany', '{count} icons')).replace('{count}', String(filtered.length)), multiSelect && selectedValues.length > 0 && (jsxs(Fragment, { children: [' ', t('iconPicker.selected', '• {selectedValues} selected').replace('{selectedValues}', String(selectedValues.length))] })), maxSelections && multiSelect && (jsxs(Fragment, { children: [' ', t('iconPicker.max', '(max {maxSelections})').replace('{maxSelections}', String(maxSelections))] }))] }), clearable && selectedValues.length > 0 && (jsx("button", { type: "button", className: "ntd-picker-clear", onClick: handleClear, children: multiSelect
512
+ if (selectedEntries.length > 1) {
513
+ return t('iconPicker.iconsSelected', '{count} icons selected').replace('{count}', String(selectedEntries.length));
514
+ }
515
+ }
516
+ else if (base) {
517
+ return labelOf(base.icon);
518
+ }
519
+ return placeholder !== null && placeholder !== void 0 ? placeholder : t('iconPicker.selectIcon', 'Select icon…');
520
+ }, [multiSelect, selectedEntries, base, labelOf, placeholder, t]);
521
+ const hasSelection = multiSelect ? selectedEntries.length > 0 : !!base;
522
+ const iconOnly = displayMode === 'icon';
523
+ // the field's label first, then what is chosen — the icon-only trigger has no text of its own
524
+ const triggerName = ariaLabel
525
+ ? `${ariaLabel}: ${triggerLabel}`
526
+ : iconOnly
527
+ ? triggerLabel
528
+ : undefined;
529
+ const overlays = Math.max(0, shown.length - 1);
530
+ const showGrid = view === 'icons' || !!picking;
531
+ // Where the panel opens (1.3.9): above the trigger when there is more room there, against its
532
+ // right edge near the window's, never taller than the room it has — the stack view is tall.
533
+ // The room is what the window and every clipping or scrolling ancestor leave visible.
534
+ const [placement, setPlacement] = useState({
535
+ up: false,
536
+ end: false,
537
+ });
538
+ useIsoLayoutEffect(() => {
539
+ const root = rootRef.current;
540
+ if (!isOpen || !root || typeof window === 'undefined') {
541
+ return;
542
+ }
543
+ const r = root.getBoundingClientRect();
544
+ const bounds = visibleBounds(root);
545
+ const want = showGrid ? 460 : 620;
546
+ const below = bounds.bottom - r.bottom - 12;
547
+ const above = r.top - bounds.top - 12;
548
+ const up = below < want && above > below;
549
+ const room = up ? above : below;
550
+ setPlacement({
551
+ up,
552
+ end: r.left + 380 > bounds.right - 8 && r.right - bounds.left >= 380,
553
+ maxHeight: room < want ? Math.max(240, Math.floor(room)) : undefined,
554
+ });
555
+ }, [isOpen, showGrid]);
556
+ const pickingInto = picking ? shown.find((l) => l.id === picking.target) : undefined;
557
+ const SingleMulti = (_a = selectedEntries[0]) === null || _a === void 0 ? void 0 : _a.Component;
558
+ return (jsxs("div", { ref: rootRef, id: id, "data-testid": dataTestId !== null && dataTestId !== void 0 ? dataTestId : testId, className: `ntd-picker${isOpen ? ' ntd-picker--open' : ''}${disabled ? ' ntd-picker--disabled' : ''}${multiSelect ? ' ntd-picker--multi' : ''}${iconOnly ? ' ntd-picker--icon-only' : ''}${className ? ` ${className}` : ''}`, children: [jsxs("button", { type: "button", className: "ntd-picker-trigger", onClick: handleToggle, disabled: disabled, "aria-haspopup": "dialog", "aria-expanded": isOpen, "aria-label": triggerName, title: iconOnly ? triggerLabel : undefined, children: [jsx("span", { className: "ntd-picker-trigger-icon", children: !multiSelect && base ? (renderIconStack(shown, resolve, {
559
+ baseProps: { ...drawProps, size: triggerIconSize },
560
+ })) : multiSelect && selectedEntries.length > 1 ? (jsx("span", { className: "ntd-picker-multi-badge", children: selectedEntries.length })) : SingleMulti ? (jsx(SingleMulti, { ...drawProps, size: triggerIconSize })) : (jsx(NtdGrid, { size: 16, variant: "outline" })) }), !iconOnly && (jsxs(Fragment, { children: [jsx("span", { className: `ntd-picker-trigger-label${hasSelection ? '' : ' ntd-picker-trigger-placeholder'}`, children: triggerLabel }), jsx("span", { className: "ntd-picker-trigger-chevron", children: jsx(Chevron, {}) })] }))] }), name &&
561
+ (multiSelect ? (values.map((v) => jsx("input", { type: "hidden", name: name, value: v }, v))) : (jsx("input", { type: "hidden", name: name, value: value !== null && value !== void 0 ? value : '' }))), pending && (jsx("span", { ref: serializerRef, className: "ntd-picker-serializer", hidden: true, children: renderIconStack(pending, resolve, {
562
+ baseProps: {
563
+ ...drawProps,
564
+ width: '1em',
565
+ height: '1em',
566
+ title: labelOf(pending[0].icon),
567
+ ...iconHtmlAttributes(pending),
568
+ },
569
+ }) })), isOpen && (jsxs("div", { ref: dropdownRef, className: `ntd-picker-dropdown${showGrid ? '' : ' ntd-picker-dropdown--stack'}${placement.up ? ' ntd-picker-dropdown--up' : ''}${placement.end ? ' ntd-picker-dropdown--end' : ''}`, style: placement.maxHeight ? { maxHeight: placement.maxHeight } : undefined, role: "dialog", "aria-label": t('iconPicker.selectIcon', 'Select icon…'), children: [canStack &&
570
+ (picking ? (jsxs("div", { className: "ntd-picker-banner", children: [jsx("button", { type: "button", className: "ntd-picker-back", onClick: () => setPicking(null), "aria-label": t('iconPicker.back', 'Back'), title: t('iconPicker.back', 'Back'), children: jsx(NtdArrowLeft, { size: 14, variant: "outline" }) }), jsx("span", { children: t('iconPicker.pickOverlay', 'Pick an icon to put on {icon}').replace('{icon}', pickingInto ? labelOf(pickingInto.icon) : base ? labelOf(base.icon) : '') })] })) : (jsxs("div", { className: "ntd-picker-views", ref: viewsRef, role: "tablist", "aria-label": t('iconPicker.selectIcon', 'Select icon…'), children: [jsx("button", { type: "button", role: "tab", id: `${panelId}-icons`, "aria-selected": view === 'icons', "aria-controls": `${panelId}-panel`, className: `ntd-picker-view${view === 'icons' ? ' ntd-picker-view--active' : ''}`, onClick: () => setView('icons'), children: t('iconPicker.tabIcon', 'Icon') }), jsxs("button", { type: "button", role: "tab", id: `${panelId}-stack`, "aria-selected": view === 'stack', "aria-controls": `${panelId}-panel`, className: `ntd-picker-view${view === 'stack' ? ' ntd-picker-view--active' : ''}`, onClick: () => setView('stack'), disabled: !base, title: base ? undefined : t('iconPicker.stackNeedsIcon', 'Pick an icon first'), children: [t('iconPicker.tabStack', 'Overlays'), overlays > 0 && jsx("span", { className: "ntd-picker-tab-count", children: overlays })] })] }))), jsxs("div", { className: "ntd-picker-panel", id: `${panelId}-panel`, ...(canStack && !picking
571
+ ? {
572
+ role: 'tabpanel',
573
+ 'aria-labelledby': `${panelId}-${view}`,
574
+ }
575
+ : {}), children: [showGrid && (jsxs(Fragment, { children: [jsxs("div", { className: "ntd-picker-search", children: [jsx("span", { className: "ntd-picker-search-icon", children: jsx(SearchIcon, {}) }), jsx("input", { ref: searchInputRef, type: "text", placeholder: t('iconPicker.searchIcons', 'Search icons…'), value: search, onChange: (e) => setSearch(e.target.value), "aria-label": t('iconPicker.searchIcons2', 'Search icons') })] }), (allCategories.length > 1 || showFavorites || showHistory) && (jsxs("div", { className: "ntd-picker-tabs", ref: tablistRef, role: "tablist", children: [jsx("button", { type: "button", className: `ntd-picker-tab${activeCategory === null ? ' ntd-picker-tab--active' : ''}`, onClick: () => setActiveCategory(null), role: "tab", "aria-selected": activeCategory === null, children: t('iconPicker.all', 'All') }), showFavorites && (jsxs("button", { type: "button", className: `ntd-picker-tab ntd-picker-tab-special${activeCategory === '__favorites__' ? ' ntd-picker-tab--active' : ''}`, onClick: () => setActiveCategory('__favorites__'), role: "tab", "aria-selected": activeCategory === '__favorites__', title: t('iconPicker.favorites', 'Favorites'), children: [jsx(StarIcon, { filled: true }), " ", t('iconPicker.favorites', 'Favorites'), favorites.length > 0 && (jsx("span", { className: "ntd-picker-tab-count", children: favorites.length }))] })), showHistory && history.length > 0 && (jsxs("button", { type: "button", className: `ntd-picker-tab ntd-picker-tab-special${activeCategory === '__history__' ? ' ntd-picker-tab--active' : ''}`, onClick: () => setActiveCategory('__history__'), role: "tab", "aria-selected": activeCategory === '__history__', title: t('iconPicker.recentlyUsed', 'Recently used'), children: [jsx(ClockIcon, {}), " ", t('iconPicker.recent', 'Recent')] })), allCategories.map((cat) => (jsx("button", { type: "button", className: `ntd-picker-tab${activeCategory === cat ? ' ntd-picker-tab--active' : ''}`, onClick: () => setActiveCategory(cat), role: "tab", "aria-selected": activeCategory === cat, children: cat }, cat)))] })), jsx("div", { className: "ntd-picker-grid-wrap", ref: gridWrapRef, role: "listbox", "aria-label": t('iconPicker.icons', 'Icons'), "aria-multiselectable": multiSelect || undefined, children: filtered.length === 0 ? (jsxs("div", { className: "ntd-picker-empty", children: [jsx("div", { className: "ntd-picker-empty-icon", children: jsx(NtdInbox, { size: 28, variant: "outline" }) }), activeCategory === '__favorites__'
576
+ ? t('iconPicker.noFavoritesYetClickThe', 'No favorites yet. Click the star icon to add.')
577
+ : activeCategory === '__history__'
578
+ ? t('iconPicker.noRecentIcons', 'No recent icons.')
579
+ : t('iconPicker.noIconsFound', 'No icons found')] })) : (jsxs(Fragment, { children: [virtualized && visibleRange.start > 0 && (jsx("div", { style: { height: Math.floor(visibleRange.start / 6) * 48 } })), Array.from(grouped.entries()).map(([category, entries]) => (jsxs("div", { children: [!activeCategory && (jsx("div", { className: "ntd-picker-category-label", children: category })), jsx("div", { className: `ntd-picker-grid${optionDisplayMode === 'icon-name' ? ' ntd-picker-grid--names' : ''}`, children: entries.map((entry) => {
580
+ const isSelected = !picking && selectedValues.includes(entry.name);
581
+ const isFavorite = favorites.includes(entry.name);
582
+ const label = entryLabel(entry);
583
+ const cell = (jsxs("button", { type: "button", className: `ntd-picker-cell${isSelected ? ' ntd-picker-cell--selected' : ''}${multiSelect ? ' ntd-picker-cell--multi-select' : ''}${enableDragAndDrop ? ' ntd-picker-cell--draggable' : ''}`, onClick: () => handleSelect(entry), "data-name": label, role: "option", "aria-selected": isSelected, "aria-label": label, title: label, draggable: enableDragAndDrop, onDragStart: enableDragAndDrop
584
+ ? (e) => handleDragStart(entry, e)
585
+ : undefined, onDragEnd: enableDragAndDrop ? (e) => handleDragEnd(entry, e) : undefined, children: [jsx(entry.Component, { ...drawProps, size: gridIconSize }), optionDisplayMode === 'icon-name' && (jsx("span", { className: "ntd-picker-cell-name", children: label })), multiSelect && isSelected && (jsx("span", { className: "ntd-picker-cell-check", children: jsx(CheckIcon, {}) }))] }, entry.name));
586
+ if (!showFavorites) {
587
+ return cell;
588
+ }
589
+ // the star beside the cell, not in it — a button in a button is
590
+ // invalid HTML (1.3.9)
591
+ const favLabel = isFavorite
592
+ ? t('iconPicker.removeFromFavorites', 'Remove from favorites')
593
+ : t('iconPicker.addToFavorites', 'Add to favorites');
594
+ return (jsxs("div", { className: "ntd-picker-cell-wrap", children: [cell, jsx("button", { type: "button", tabIndex: -1, className: `ntd-picker-cell-fav${isFavorite ? ' ntd-picker-cell-fav--active' : ''}`, onClick: (e) => handleFavoriteClick(e, entry.name), "aria-label": `${favLabel}: ${label}`, title: favLabel, children: jsx(StarIcon, { filled: isFavorite }) })] }, entry.name));
595
+ }) })] }, category))), virtualized && visibleRange.end < filtered.length && (jsx("div", { style: {
596
+ height: Math.ceil((filtered.length - visibleRange.end) / 6) * 48,
597
+ } }))] })) })] })), canStack && view === 'stack' && base && (jsx("div", { className: "ntd-picker-stack", hidden: !!picking, children: jsx(NiceIconStacker, { ref: stackerRef, icons: resolve, layers: shown, onLayersChange: commit, onAddClick: (target) => {
598
+ var _a;
599
+ setSearch('');
600
+ setActiveCategory(null);
601
+ setPicking({ target: (_a = target === null || target === void 0 ? void 0 : target.id) !== null && _a !== void 0 ? _a : base.id });
602
+ }, previewProps: drawProps, iconLabel: labelOf, showCode: false, keepBase: true }) }))] }), !picking && (jsxs("div", { className: "ntd-picker-footer", children: [jsx("span", { className: "ntd-picker-count", children: showGrid && (jsxs(Fragment, { children: [(filtered.length === 1
603
+ ? t('iconPicker.iconCountOne', '{count} icon')
604
+ : t('iconPicker.iconCountMany', '{count} icons')).replace('{count}', String(filtered.length)), multiSelect && selectedValues.length > 0 && (jsxs(Fragment, { children: [' ', t('iconPicker.selected', '• {selectedValues} selected').replace('{selectedValues}', String(selectedValues.length))] })), maxSelections && multiSelect && (jsxs(Fragment, { children: [' ', t('iconPicker.max', '(max {maxSelections})').replace('{maxSelections}', String(maxSelections))] }))] })) }), clearable && hasSelection && (jsx("button", { type: "button", className: "ntd-picker-clear", onClick: handleClear, children: multiSelect
408
605
  ? t('iconPicker.clearAll', 'Clear all')
409
- : t('iconPicker.clearSelection', 'Clear selection') })), multiSelect && (jsx("button", { type: "button", className: "ntd-picker-done", onClick: () => setIsOpen(false), children: t('iconPicker.done', 'Done') }))] })] }))] }));
606
+ : t('iconPicker.clearSelection', 'Clear selection') })), (multiSelect || view === 'stack') && (jsx("button", { type: "button", className: "ntd-picker-done", onClick: () => setIsOpen(false), children: t('iconPicker.done', 'Done') }))] }))] }))] }));
410
607
  });
411
608
 
412
609
  export { NiceIconPicker };