@nice2dev/icons 1.3.8 → 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.
@@ -1,32 +1,56 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
- import { useNiceTranslation, useControllableState } from '@nice2dev/ui-core';
3
- import { forwardRef, useRef, useId, useState, useCallback, useMemo, useImperativeHandle } from 'react';
2
+ import { useNiceTranslation, useControllableState, useRovingFocus } from '@nice2dev/ui-core';
3
+ import React, { forwardRef, useState, useRef, useCallback, useMemo, useImperativeHandle } from 'react';
4
4
  import { NtdPlus, NtdTrash, NtdCheck, NtdCopy } from './icons/actions.js';
5
5
  import { NtdClose } from './icons/navigation.js';
6
- import { niceIconStackCode, renderIconStack, stackParentId, stackChildrenOf, isValidTextBinding } from './iconStack.js';
6
+ import { niceIconStackCode, iconDisplayName, stackParentId, stackChildrenOf, isValidTextBinding, renderIconStack, addStackLayer, removeStackLayer } from './iconStack.js';
7
7
  import { BOX_PERFECT_STACK, defaultStackSize, defaultTextSize, perfectStackOf } from './stacking.js';
8
8
 
9
- const ALIGNS = [
10
- 'perfect',
11
- 'center',
12
- 'top',
13
- 'bottom',
14
- 'left',
15
- 'right',
16
- 'top-left',
17
- 'top-right',
18
- 'bottom-left',
19
- 'bottom-right',
20
- 'half-top',
21
- 'half-bottom',
22
- 'half-left',
23
- 'half-right',
24
- 'half-top-left',
25
- 'half-top-right',
26
- 'half-bottom-left',
27
- 'half-bottom-right',
9
+ /** Where a layer can sit, laid out on the 5 × 5 pad as it sits in its parent. */
10
+ const PAD = [
11
+ { align: 'top-left', row: 1, col: 1 },
12
+ { align: 'top', row: 1, col: 3 },
13
+ { align: 'top-right', row: 1, col: 5 },
14
+ { align: 'half-top-left', row: 2, col: 2 },
15
+ { align: 'half-top', row: 2, col: 3 },
16
+ { align: 'half-top-right', row: 2, col: 4 },
17
+ { align: 'left', row: 3, col: 1 },
18
+ { align: 'half-left', row: 3, col: 2 },
19
+ { align: 'center', row: 3, col: 3 },
20
+ { align: 'half-right', row: 3, col: 4 },
21
+ { align: 'right', row: 3, col: 5 },
22
+ { align: 'half-bottom-left', row: 4, col: 2 },
23
+ { align: 'half-bottom', row: 4, col: 3 },
24
+ { align: 'half-bottom-right', row: 4, col: 4 },
25
+ { align: 'bottom-left', row: 5, col: 1 },
26
+ { align: 'bottom', row: 5, col: 3 },
27
+ { align: 'bottom-right', row: 5, col: 5 },
28
28
  ];
29
- const PREVIEW_SIZES = [96, 48, 24];
29
+ /** The positions' names, in words. */
30
+ function alignNames(t) {
31
+ return {
32
+ perfect: t('iconStacker.align.perfect', 'Auto'),
33
+ center: t('iconStacker.align.center', 'Centre'),
34
+ top: t('iconStacker.align.top', 'Top edge'),
35
+ bottom: t('iconStacker.align.bottom', 'Bottom edge'),
36
+ left: t('iconStacker.align.left', 'Left edge'),
37
+ right: t('iconStacker.align.right', 'Right edge'),
38
+ 'top-left': t('iconStacker.align.top-left', 'Top-left corner'),
39
+ 'top-right': t('iconStacker.align.top-right', 'Top-right corner'),
40
+ 'bottom-left': t('iconStacker.align.bottom-left', 'Bottom-left corner'),
41
+ 'bottom-right': t('iconStacker.align.bottom-right', 'Bottom-right corner'),
42
+ 'half-top': t('iconStacker.align.half-top', 'Halfway to the top'),
43
+ 'half-bottom': t('iconStacker.align.half-bottom', 'Halfway to the bottom'),
44
+ 'half-left': t('iconStacker.align.half-left', 'Halfway to the left'),
45
+ 'half-right': t('iconStacker.align.half-right', 'Halfway to the right'),
46
+ 'half-top-left': t('iconStacker.align.half-top-left', 'Halfway to the top-left corner'),
47
+ 'half-top-right': t('iconStacker.align.half-top-right', 'Halfway to the top-right corner'),
48
+ 'half-bottom-left': t('iconStacker.align.half-bottom-left', 'Halfway to the bottom-left corner'),
49
+ 'half-bottom-right': t('iconStacker.align.half-bottom-right', 'Halfway to the bottom-right corner'),
50
+ };
51
+ }
52
+ const STAGE_SIZE = 64;
53
+ const STAGE_SMALL = [32, 24, 16];
30
54
  const NO_LAYERS = [];
31
55
  /** What a colour field shows while the colour is unset (follows the parent). */
32
56
  const AUTO_SWATCH = '#808080';
@@ -44,57 +68,21 @@ function walk(layers) {
44
68
  }
45
69
  return out;
46
70
  }
47
- /** Removes a layer; what sat in it moves up into its parent (a removed base hands over to its first child). */
48
- function removeLayer(layers, id) {
49
- const base = layers[0];
50
- if (!base) {
51
- return [];
52
- }
53
- if (id !== base.id) {
54
- const gone = layers.find((l) => l.id === id);
55
- const up = gone ? stackParentId(layers, gone) : undefined;
56
- return layers
57
- .filter((l) => l.id !== id)
58
- .map((l) => (l.parent === id ? { ...l, parent: up === base.id ? undefined : up } : l));
59
- }
60
- const [heir, ...siblings] = stackChildrenOf(layers, base.id);
61
- if (!heir) {
62
- return [];
63
- }
64
- const newBase = {
65
- ...heir,
66
- parent: undefined,
67
- align: undefined,
68
- size: undefined,
69
- rotation: undefined,
70
- offsetX: undefined,
71
- offsetY: undefined,
72
- };
73
- return [
74
- newBase,
75
- ...layers
76
- .filter((l) => l.id !== base.id && l.id !== heir.id)
77
- .map((l) => siblings.includes(l)
78
- ? { ...l, parent: heir.id }
79
- : l.parent === heir.id
80
- ? { ...l, parent: undefined }
81
- : l),
82
- ];
83
- }
84
71
  /** A colour of the layer's own, or "auto" — following its parent — until one is picked. */
85
- function ColorField({ label, value, onChange, autoLabel, resetLabel }) {
86
- return (jsxs("span", { className: "nice-icon-stacker__color", children: [jsxs("label", { className: "nice-icon-stacker__field", children: [jsx("span", { children: label }), jsx("input", { type: "color", value: value !== null && value !== void 0 ? value : AUTO_SWATCH, onChange: (e) => onChange(e.target.value) })] }), value ? (jsx("button", { type: "button", className: "nice-icon-stacker__reset", onClick: () => onChange(undefined), "aria-label": resetLabel, title: resetLabel, children: jsx(NtdClose, { size: 12 }) })) : (jsx("small", { className: "nice-icon-stacker__auto", children: autoLabel }))] }));
72
+ function Swatch({ label, value, onChange, autoLabel, resetLabel }) {
73
+ return (jsxs("span", { className: "nice-icon-stacker__swatch", children: [jsxs("label", { className: `nice-icon-stacker__swatch-pick${value ? '' : ' nice-icon-stacker__swatch-pick--auto'}`, title: `${label}: ${value !== null && value !== void 0 ? value : autoLabel}`, children: [jsx("span", { className: "nice-icon-stacker__swatch-dot", style: value ? { background: value } : undefined }), jsx("span", { children: label }), jsx("input", { type: "color", value: value !== null && value !== void 0 ? value : AUTO_SWATCH, onChange: (e) => onChange(e.target.value) })] }), value && (jsx("button", { type: "button", className: "nice-icon-stacker__reset", onClick: () => onChange(undefined), "aria-label": `${resetLabel}: ${label}`, title: resetLabel, children: jsx(NtdClose, { size: 10 }) }))] }));
87
74
  }
88
75
  /**
89
- * NiceIconStacker (1.3.5) — builds an icon stack: the base icon and the icons stacked in it, each
90
- * with its position, size, turn and colours, nested any number of levels deep (1.3.6) with text
91
- * innermost — its size, colour and the value it is bound to — shown live at three sizes and as the
92
- * JSX that draws it. The "add" button puts `current` into the selected layer and selects it, so
93
- * adding again goes one level deeper; select an outer layer to add side by side. Layers leave one
94
- * by one or all at once. `NiceIconStack` draws a stack built here from data.
76
+ * NiceIconStacker (1.3.5) — builds an icon stack: the base icon and the icons stacked in it, nested
77
+ * any number of levels deep (1.3.6) with text innermost. Since 1.3.9 it is laid out to fit a picker's
78
+ * panel: the stack drawn large and small, the layers as a list, and under it the selected layer —
79
+ * where it sits (picked on a pad shaped like its parent), its size and text, with the turn, nudge,
80
+ * colours and binding folded under "More". A new icon goes into the selected layer and is selected,
81
+ * so adding again goes one level deeper; select an outer layer to add side by side. The JSX that
82
+ * draws the stack is folded at the end. `NiceIconStack` draws a stack built here from data.
95
83
  */
96
- const NiceIconStacker = forwardRef(function NiceIconStacker({ icons, layers: layersProp, defaultLayers, onLayersChange, current, previewProps, className }, ref) {
97
- var _a, _b, _c, _d;
84
+ const NiceIconStacker = forwardRef(function NiceIconStacker({ icons, layers: layersProp, defaultLayers, onLayersChange, current, onAddClick, previewProps, iconLabel = iconDisplayName, showCode = true, keepBase = false, className, }, ref) {
85
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
98
86
  const { t } = useNiceTranslation();
99
87
  const [layers, setLayers] = useControllableState({
100
88
  value: layersProp,
@@ -103,43 +91,47 @@ const NiceIconStacker = forwardRef(function NiceIconStacker({ icons, layers: lay
103
91
  onChange: onLayersChange,
104
92
  componentName: 'NiceIconStacker',
105
93
  });
106
- const nextId = useRef(0);
107
- const targetGroup = useId();
108
94
  const [copied, setCopied] = useState(false);
109
- // The layer the next icon goes into — the newest by default, so adding nests deeper.
95
+ // The layer being edited, which the next icon goes into — the newest by default, so adding nests deeper.
110
96
  const [selected, setSelected] = useState(undefined);
111
97
  const base = layers[0];
112
98
  const target = (_b = (_a = layers.find((l) => l.id === selected)) !== null && _a !== void 0 ? _a : layers[layers.length - 1]) !== null && _b !== void 0 ? _b : undefined;
99
+ const listRef = useRef(null);
100
+ useRovingFocus(listRef, {
101
+ orientation: 'vertical',
102
+ itemSelector: '.nice-icon-stacker__layer-select',
103
+ });
104
+ const padRef = useRef(null);
105
+ useRovingFocus(padRef, {
106
+ orientation: 'grid',
107
+ itemSelector: '[role="radio"]',
108
+ activateOnMove: true,
109
+ });
113
110
  const resolve = useCallback((name) => (typeof icons === 'function' ? icons(name) : icons[name]), [icons]);
114
111
  const tree = useMemo(() => walk(layers), [layers]);
115
- const code = useMemo(() => niceIconStackCode(layers), [layers]);
112
+ const code = useMemo(() => (showCode ? niceIconStackCode(layers) : ''), [layers, showCode]);
116
113
  const update = (id, patch) => setLayers((prev) => prev.map((l) => (l.id === id ? { ...l, ...patch } : l)));
117
- const add = () => {
118
- if (!current) {
114
+ const add = (icon = current) => {
115
+ if (!icon) {
119
116
  return;
120
117
  }
121
- nextId.current += 1;
122
- const id = `${current.icon}-${Date.now().toString(36)}-${nextId.current}`;
118
+ let added = '';
123
119
  setLayers((prev) => {
124
- const into = prev.find((l) => l.id === (target === null || target === void 0 ? void 0 : target.id));
125
- const layer = {
126
- id,
127
- icon: current.icon,
128
- variant: current.variant,
129
- ...(into && into !== prev[0] ? { parent: into.id } : {}),
130
- };
131
- return [...prev, layer];
120
+ var _a;
121
+ const next = addStackLayer(prev, icon, (_a = prev.find((l) => l.id === (target === null || target === void 0 ? void 0 : target.id))) === null || _a === void 0 ? void 0 : _a.id);
122
+ added = next.id;
123
+ return next.layers;
132
124
  });
133
- setSelected(id);
125
+ setSelected(added);
134
126
  };
135
127
  const remove = (id) => {
136
- setLayers((prev) => removeLayer(prev, id));
128
+ setLayers((prev) => removeStackLayer(prev, id));
137
129
  if (selected === id) {
138
130
  setSelected(undefined);
139
131
  }
140
132
  };
141
133
  const clear = () => {
142
- setLayers(NO_LAYERS);
134
+ setLayers((prev) => (keepBase && prev[0] ? [prev[0]] : NO_LAYERS));
143
135
  setSelected(undefined);
144
136
  };
145
137
  useImperativeHandle(ref, () => ({ add, clear }));
@@ -152,41 +144,66 @@ const NiceIconStacker = forwardRef(function NiceIconStacker({ icons, layers: lay
152
144
  };
153
145
  const autoLabel = t('iconStacker.auto', 'auto');
154
146
  const resetLabel = t('iconStacker.reset', 'Back to auto');
155
- const addLabel = base && target
156
- ? t('iconStacker.addInto', 'Add {icon} into {target}')
157
- .replace('{icon}', (_c = current === null || current === void 0 ? void 0 : current.icon) !== null && _c !== void 0 ? _c : '')
158
- .replace('{target}', target.icon)
159
- : t('iconStacker.add', 'Add {icon} to the stack').replace('{icon}', (_d = current === null || current === void 0 ? void 0 : current.icon) !== null && _d !== void 0 ? _d : '');
160
- return (jsxs("div", { className: ['nice-icon-stacker', className].filter(Boolean).join(' '), children: [jsxs("div", { className: "nice-icon-stacker__toolbar", children: [jsxs("button", { type: "button", onClick: add, disabled: !current, children: [jsx(NtdPlus, { size: 14 }), " ", addLabel] }), jsxs("button", { type: "button", onClick: clear, disabled: layers.length === 0, children: [jsx(NtdTrash, { size: 14 }), " ", t('iconStacker.clear', 'Clear the stack')] })] }), !base ? (jsx("p", { className: "nice-icon-stacker__empty", children: t('iconStacker.empty', 'The stack is empty. Add an icon to start one.') })) : (jsxs("div", { className: "nice-icon-stacker__body", children: [jsx("div", { className: "nice-icon-stacker__preview", "aria-label": t('iconStacker.preview', 'Preview'), role: "group", children: PREVIEW_SIZES.map((s) => (jsxs("span", { className: "nice-icon-stacker__preview-cell", children: [renderIconStack(layers, icons, {
161
- baseProps: { ...previewProps, size: s, title: base.icon },
162
- }), jsxs("small", { children: [s, "px"] })] }, s))) }), jsx("ol", { className: "nice-icon-stacker__layers", "aria-label": t('iconStacker.layers', 'Layers'), children: tree.map(({ layer: l, depth }) => {
163
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
164
- const Icon = resolve(l.icon);
165
- const isBase = l === base;
166
- const align = (_a = l.align) !== null && _a !== void 0 ? _a : 'perfect';
167
- // the spot the parent keeps for stacked icons (1.3.8): its size and turn are the
168
- // layer's defaults while it sits there
169
- const parentId = stackParentId(layers, l);
170
- const parentLayer = parentId ? layers.find((p) => p.id === parentId) : undefined;
171
- const spot = (parentLayer && perfectStackOf(resolve(parentLayer.icon))) || BOX_PERFECT_STACK;
172
- const atSpot = align === 'perfect';
173
- const share = (_b = l.size) !== null && _b !== void 0 ? _b : (atSpot ? spot.size : defaultStackSize(align));
174
- const turnDefault = atSpot ? spot.rotation : 0;
175
- const turn = (_c = l.rotation) !== null && _c !== void 0 ? _c : turnDefault;
176
- const innermost = stackChildrenOf(layers, l.id).length === 0;
177
- const textSize = (_d = l.textSize) !== null && _d !== void 0 ? _d : defaultTextSize(((_e = l.text) !== null && _e !== void 0 ? _e : '').length || 1, (_f = perfectStackOf(Icon)) === null || _f === void 0 ? void 0 : _f.size);
178
- const bindingInvalid = !!l.textBinding && !isValidTextBinding(l.textBinding);
179
- const depthStyle = {
180
- ['--nice-icon-stacker-depth']: String(depth),
181
- };
182
- const label = (key, fallback) => t(key, fallback).replace('{icon}', l.icon);
183
- return (jsxs("li", { className: "nice-icon-stacker__layer", style: depthStyle, children: [jsxs("div", { className: "nice-icon-stacker__layer-row", children: [jsx("input", { type: "radio", name: targetGroup, checked: (target === null || target === void 0 ? void 0 : target.id) === l.id, onChange: () => setSelected(l.id), "aria-label": label('iconStacker.target', 'Add the next icon into {icon}'), title: label('iconStacker.target', 'Add the next icon into {icon}') }), jsx("span", { className: "nice-icon-stacker__layer-icon", children: Icon ? jsx(Icon, { size: 24, variant: l.variant }) : null }), jsxs("span", { className: "nice-icon-stacker__layer-name", children: [l.icon, isBase && (jsx("span", { className: "nice-icon-stacker__badge", children: t('iconStacker.base', 'Base') }))] }), !isBase && (jsxs("label", { className: "nice-icon-stacker__field", children: [jsx("span", { children: t('iconStacker.align', 'Position') }), jsx("select", { value: align, onChange: (e) => update(l.id, { align: e.target.value }), children: ALIGNS.map((a) => (jsx("option", { value: a, children: a }, a))) })] })), !isBase && (jsxs("label", { className: "nice-icon-stacker__field", children: [jsx("span", { children: t('iconStacker.size', 'Size') }), jsx("input", { type: "range", min: 0.2, max: 1, step: 0.05, value: share, onChange: (e) => update(l.id, { size: Number(e.target.value) }) }), jsxs("output", { children: [Math.round(share * 100), "%"] })] })), jsx("button", { type: "button", className: "nice-icon-stacker__remove", onClick: () => remove(l.id), "aria-label": label('iconStacker.remove', 'Remove {icon}'), title: label('iconStacker.remove', 'Remove {icon}'), children: jsx(NtdTrash, { size: 14 }) })] }), jsxs("details", { className: "nice-icon-stacker__more", children: [jsx("summary", { children: t('iconStacker.more', 'More') }), jsxs("div", { className: "nice-icon-stacker__more-body", children: [!isBase && (jsxs(Fragment, { children: [jsxs("label", { className: "nice-icon-stacker__field", children: [jsx("span", { children: t('iconStacker.rotation', 'Turn') }), jsx("input", { type: "range", min: -180, max: 180, step: 15, value: turn, onChange: (e) => {
184
- const v = Number(e.target.value);
185
- update(l.id, { rotation: v === turnDefault ? undefined : v });
186
- } }), jsxs("output", { children: [turn, "\u00B0"] })] }), jsxs("label", { className: "nice-icon-stacker__field", children: [jsx("span", { children: t('iconStacker.offsetX', 'Shift right') }), jsx("input", { type: "range", min: -50, max: 50, step: 1, value: (_g = l.offsetX) !== null && _g !== void 0 ? _g : 0, onChange: (e) => update(l.id, { offsetX: Number(e.target.value) || undefined }) }), jsxs("output", { children: [(_h = l.offsetX) !== null && _h !== void 0 ? _h : 0, "%"] })] }), jsxs("label", { className: "nice-icon-stacker__field", children: [jsx("span", { children: t('iconStacker.offsetY', 'Shift down') }), jsx("input", { type: "range", min: -50, max: 50, step: 1, value: (_j = l.offsetY) !== null && _j !== void 0 ? _j : 0, onChange: (e) => update(l.id, { offsetY: Number(e.target.value) || undefined }) }), jsxs("output", { children: [(_k = l.offsetY) !== null && _k !== void 0 ? _k : 0, "%"] })] })] })), jsx(ColorField, { label: t('iconStacker.primary', 'Primary'), value: l.primaryColor, onChange: (v) => update(l.id, { primaryColor: v }), autoLabel: autoLabel, resetLabel: resetLabel }), jsx(ColorField, { label: t('iconStacker.secondary', 'Secondary'), value: l.secondaryColor, onChange: (v) => update(l.id, { secondaryColor: v }), autoLabel: autoLabel, resetLabel: resetLabel }), jsx(ColorField, { label: t('iconStacker.accent', 'Accent'), value: l.accentColor, onChange: (v) => update(l.id, { accentColor: v }), autoLabel: autoLabel, resetLabel: resetLabel })] })] }), (innermost || l.text || l.textBinding) && (jsxs("div", { className: "nice-icon-stacker__text", children: [jsxs("label", { className: "nice-icon-stacker__field", children: [jsx("span", { children: t('iconStacker.text', 'Text') }), jsx("input", { type: "text", value: (_l = l.text) !== null && _l !== void 0 ? _l : '', maxLength: 4, onChange: (e) => update(l.id, { text: e.target.value || undefined }) })] }), jsxs("label", { className: "nice-icon-stacker__field", children: [jsx("span", { children: t('iconStacker.textSize', 'Text size') }), jsx("input", { type: "range", min: 3, max: 14, step: 0.5, value: textSize, onChange: (e) => update(l.id, { textSize: Number(e.target.value) }) }), jsx("output", { children: textSize }), l.textSize !== undefined && (jsx("button", { type: "button", className: "nice-icon-stacker__reset", onClick: () => update(l.id, { textSize: undefined }), "aria-label": resetLabel, title: resetLabel, children: jsx(NtdClose, { size: 12 }) }))] }), jsx(ColorField, { label: t('iconStacker.textColor', 'Text colour'), value: l.textColor, onChange: (v) => update(l.id, { textColor: v }), autoLabel: autoLabel, resetLabel: resetLabel }), jsxs("label", { className: "nice-icon-stacker__field", children: [jsx("span", { children: t('iconStacker.bind', 'Bind to') }), jsx("input", { type: "text", value: (_m = l.textBinding) !== null && _m !== void 0 ? _m : '', "aria-invalid": bindingInvalid || undefined, title: t('iconStacker.bindHint', 'The name of the value the text shows, e.g. count — the text above is a sample'), onChange: (e) => update(l.id, { textBinding: e.target.value.trim() || undefined }) })] })] }))] }, l.id));
187
- }) }), jsxs("div", { className: "nice-icon-stacker__code", children: [jsx("pre", { children: code }), jsxs("button", { type: "button", onClick: copy, children: [copied ? jsx(NtdCheck, { size: 14 }) : jsx(NtdCopy, { size: 14 }), ' ', copied
147
+ const names = alignNames(t);
148
+ const alignName = (a) => names[a];
149
+ const currentName = current ? iconLabel(current.icon) : '';
150
+ const targetName = target ? iconLabel(target.icon) : '';
151
+ const asks = !!onAddClick || !current;
152
+ const addText = asks
153
+ ? t('iconStacker.addIcon', 'Add an icon')
154
+ : t('iconStacker.addShort', 'Add {icon}').replace('{icon}', currentName);
155
+ const addLabel = asks
156
+ ? base && target
157
+ ? t('iconStacker.addIconInto', 'Add an icon into {target}').replace('{target}', targetName)
158
+ : addText
159
+ : base && target
160
+ ? t('iconStacker.addInto', 'Add {icon} into {target}')
161
+ .replace('{icon}', currentName)
162
+ .replace('{target}', targetName)
163
+ : t('iconStacker.add', 'Add {icon} to the stack').replace('{icon}', currentName);
164
+ const clearLabel = keepBase
165
+ ? t('iconStacker.clearOverlays', 'Take off the stacked icons')
166
+ : t('iconStacker.clear', 'Clear the stack');
167
+ // the selected layer, as the inspector edits it
168
+ const edited = target;
169
+ const editedIcon = edited ? resolve(edited.icon) : undefined;
170
+ const editedIsBase = !!edited && edited === base;
171
+ const align = (_c = edited === null || edited === void 0 ? void 0 : edited.align) !== null && _c !== void 0 ? _c : 'perfect';
172
+ // the spot the parent keeps for stacked icons (1.3.8): its size and turn are the layer's
173
+ // defaults while it sits there
174
+ const parentId = edited ? stackParentId(layers, edited) : undefined;
175
+ const parentLayer = parentId ? layers.find((p) => p.id === parentId) : undefined;
176
+ const spot = (parentLayer && perfectStackOf(resolve(parentLayer.icon))) || BOX_PERFECT_STACK;
177
+ const atSpot = align === 'perfect';
178
+ const share = (_d = edited === null || edited === void 0 ? void 0 : edited.size) !== null && _d !== void 0 ? _d : (atSpot ? spot.size : defaultStackSize(align));
179
+ const turnDefault = atSpot ? spot.rotation : 0;
180
+ const turn = (_e = edited === null || edited === void 0 ? void 0 : edited.rotation) !== null && _e !== void 0 ? _e : turnDefault;
181
+ const innermost = !!edited && stackChildrenOf(layers, edited.id).length === 0;
182
+ const hasText = !!edited && (innermost || !!edited.text || !!edited.textBinding);
183
+ const textSize = (_f = edited === null || edited === void 0 ? void 0 : edited.textSize) !== null && _f !== void 0 ? _f : defaultTextSize(((_g = edited === null || edited === void 0 ? void 0 : edited.text) !== null && _g !== void 0 ? _g : '').length || 1, (_h = perfectStackOf(editedIcon)) === null || _h === void 0 ? void 0 : _h.size);
184
+ const bindingInvalid = !!(edited === null || edited === void 0 ? void 0 : edited.textBinding) && !isValidTextBinding(edited.textBinding);
185
+ return (jsxs("div", { className: ['nice-icon-stacker', className].filter(Boolean).join(' '), children: [jsxs("div", { className: "nice-icon-stacker__bar", children: [jsx("span", { className: "nice-icon-stacker__title", children: t('iconStacker.layers', 'Layers') }), jsxs("button", { type: "button", className: "nice-icon-stacker__add", onClick: () => (onAddClick ? onAddClick(target) : add()), disabled: !onAddClick && !current, "aria-label": addLabel, title: addLabel, children: [jsx(NtdPlus, { size: 14 }), " ", addText] }), jsx("button", { type: "button", className: "nice-icon-stacker__clear", onClick: clear, disabled: keepBase ? layers.length < 2 : layers.length === 0, "aria-label": clearLabel, title: clearLabel, children: jsx(NtdTrash, { size: 14 }) })] }), !base || !edited ? (jsx("p", { className: "nice-icon-stacker__empty", children: t('iconStacker.empty', 'The stack is empty. Add an icon to start one.') })) : (jsxs(Fragment, { children: [jsxs("div", { className: "nice-icon-stacker__top", children: [jsxs("div", { className: "nice-icon-stacker__preview", "aria-label": t('iconStacker.preview', 'Preview'), role: "group", children: [jsx("span", { className: "nice-icon-stacker__preview-main", title: `${STAGE_SIZE}px`, children: renderIconStack(layers, icons, {
186
+ baseProps: { ...previewProps, size: STAGE_SIZE, title: iconLabel(base.icon) },
187
+ }) }), jsx("span", { className: "nice-icon-stacker__preview-small", children: STAGE_SMALL.map((s) => (jsx("span", { title: `${s}px`, children: renderIconStack(layers, icons, { baseProps: { ...previewProps, size: s } }) }, s))) })] }), jsxs("div", { className: "nice-icon-stacker__layers-wrap", children: [jsx("ol", { ref: listRef, className: "nice-icon-stacker__layers", "aria-label": t('iconStacker.layers', 'Layers'), children: tree.map(({ layer: l, depth }) => {
188
+ const Icon = resolve(l.icon);
189
+ const isBase = l === base;
190
+ const isSelected = l === edited;
191
+ const name = iconLabel(l.icon);
192
+ const removable = !(isBase && keepBase);
193
+ const removeLabel = t('iconStacker.remove', 'Remove {icon}').replace('{icon}', name);
194
+ return (jsxs("li", { className: `nice-icon-stacker__layer${isSelected ? ' nice-icon-stacker__layer--selected' : ''}`, style: { ['--nice-icon-stacker-depth']: String(depth) }, children: [jsxs("button", { type: "button", className: "nice-icon-stacker__layer-select", "aria-pressed": isSelected, onClick: () => setSelected(l.id), onKeyDown: (e) => {
195
+ if (removable && (e.key === 'Delete' || e.key === 'Backspace')) {
196
+ e.preventDefault();
197
+ remove(l.id);
198
+ }
199
+ }, children: [jsx("span", { className: "nice-icon-stacker__layer-icon", children: Icon ? jsx(Icon, { size: 18, variant: l.variant }) : null }), jsx("span", { className: "nice-icon-stacker__layer-name", children: name }), l.text && (jsxs(Fragment, { children: [' ', jsx("span", { className: "nice-icon-stacker__layer-text", children: l.text })] })), isBase && (jsxs(Fragment, { children: [' ', jsx("span", { className: "nice-icon-stacker__badge", children: t('iconStacker.base', 'Base') })] }))] }), removable && (jsx("button", { type: "button", className: "nice-icon-stacker__remove", onClick: () => remove(l.id), "aria-label": removeLabel, title: removeLabel, children: jsx(NtdClose, { size: 10 }) }))] }, l.id));
200
+ }) }), jsx("small", { className: "nice-icon-stacker__hint", children: t('iconStacker.hint', 'A new icon goes into the selected layer.') })] })] }), jsxs("div", { className: "nice-icon-stacker__inspector", role: "group", "aria-label": iconLabel(edited.icon), children: [jsxs("div", { className: "nice-icon-stacker__inspector-head", children: [editedIcon &&
201
+ React.createElement(editedIcon, { size: 16, variant: edited.variant }), jsx("span", { children: iconLabel(edited.icon) }), parentLayer && (jsx("small", { children: t('iconStacker.inside', 'in {icon}').replace('{icon}', iconLabel(parentLayer.icon)) }))] }), jsxs("div", { className: "nice-icon-stacker__inspector-body", children: [!editedIsBase && (jsxs("div", { ref: padRef, className: "nice-icon-stacker__pad", role: "radiogroup", "aria-label": t('iconStacker.align', 'Position'), children: [jsx("button", { type: "button", role: "radio", "aria-checked": align === 'perfect', className: "nice-icon-stacker__pad-auto", title: t('iconStacker.align.perfectHint', 'The spot the icon keeps for what is stacked in it'), onClick: () => update(edited.id, { align: undefined }), children: alignName('perfect') }), jsx("span", { className: "nice-icon-stacker__pad-box", children: PAD.map(({ align: a, row, col }) => (jsx("button", { type: "button", role: "radio", "aria-checked": align === a, "aria-label": alignName(a), title: alignName(a), className: "nice-icon-stacker__pad-dot", style: { gridRow: row, gridColumn: col }, onClick: () => update(edited.id, { align: a }) }, a))) })] })), jsxs("div", { className: "nice-icon-stacker__fields", children: [!editedIsBase && (jsxs("label", { className: "nice-icon-stacker__field", children: [jsx("span", { children: t('iconStacker.size', 'Size') }), jsx("input", { type: "range", min: 0.2, max: 1, step: 0.05, value: share, onChange: (e) => update(edited.id, { size: Number(e.target.value) }) }), jsxs("output", { children: [Math.round(share * 100), "%"] })] })), hasText && (jsxs("label", { className: "nice-icon-stacker__field", children: [jsx("span", { children: t('iconStacker.text', 'Text') }), jsx("input", { type: "text", value: (_j = edited.text) !== null && _j !== void 0 ? _j : '', maxLength: 4, onChange: (e) => update(edited.id, { text: e.target.value || undefined }) })] }))] })] }), jsxs("details", { className: "nice-icon-stacker__more", children: [jsx("summary", { children: t('iconStacker.more', 'More') }), jsxs("div", { className: "nice-icon-stacker__more-body", children: [!editedIsBase && (jsxs(Fragment, { children: [jsxs("label", { className: "nice-icon-stacker__field", children: [jsx("span", { children: t('iconStacker.rotation', 'Turn') }), jsx("input", { type: "range", min: -180, max: 180, step: 15, value: turn, onChange: (e) => {
202
+ const v = Number(e.target.value);
203
+ update(edited.id, { rotation: v === turnDefault ? undefined : v });
204
+ } }), jsxs("output", { children: [turn, "\u00B0"] })] }), jsxs("label", { className: "nice-icon-stacker__field", children: [jsx("span", { children: t('iconStacker.offsetX', 'Shift right') }), jsx("input", { type: "range", min: -50, max: 50, step: 1, value: (_k = edited.offsetX) !== null && _k !== void 0 ? _k : 0, onChange: (e) => update(edited.id, { offsetX: Number(e.target.value) || undefined }) }), jsxs("output", { children: [(_l = edited.offsetX) !== null && _l !== void 0 ? _l : 0, "%"] })] }), jsxs("label", { className: "nice-icon-stacker__field", children: [jsx("span", { children: t('iconStacker.offsetY', 'Shift down') }), jsx("input", { type: "range", min: -50, max: 50, step: 1, value: (_m = edited.offsetY) !== null && _m !== void 0 ? _m : 0, onChange: (e) => update(edited.id, { offsetY: Number(e.target.value) || undefined }) }), jsxs("output", { children: [(_o = edited.offsetY) !== null && _o !== void 0 ? _o : 0, "%"] })] })] })), jsxs("div", { className: "nice-icon-stacker__swatches", children: [jsx(Swatch, { label: t('iconStacker.primary', 'Primary'), value: edited.primaryColor, onChange: (v) => update(edited.id, { primaryColor: v }), autoLabel: autoLabel, resetLabel: resetLabel }), jsx(Swatch, { label: t('iconStacker.secondary', 'Secondary'), value: edited.secondaryColor, onChange: (v) => update(edited.id, { secondaryColor: v }), autoLabel: autoLabel, resetLabel: resetLabel }), jsx(Swatch, { label: t('iconStacker.accent', 'Accent'), value: edited.accentColor, onChange: (v) => update(edited.id, { accentColor: v }), autoLabel: autoLabel, resetLabel: resetLabel }), hasText && (jsx(Swatch, { label: t('iconStacker.textColor', 'Text colour'), value: edited.textColor, onChange: (v) => update(edited.id, { textColor: v }), autoLabel: autoLabel, resetLabel: resetLabel }))] }), hasText && (jsxs(Fragment, { children: [jsxs("label", { className: "nice-icon-stacker__field", children: [jsx("span", { children: t('iconStacker.textSize', 'Text size') }), jsx("input", { type: "range", min: 3, max: 14, step: 0.5, value: textSize, onChange: (e) => update(edited.id, { textSize: Number(e.target.value) }) }), jsx("output", { children: textSize }), edited.textSize !== undefined && (jsx("button", { type: "button", className: "nice-icon-stacker__reset", onClick: () => update(edited.id, { textSize: undefined }), "aria-label": `${resetLabel}: ${t('iconStacker.textSize', 'Text size')}`, title: resetLabel, children: jsx(NtdClose, { size: 10 }) }))] }), jsxs("label", { className: "nice-icon-stacker__field", children: [jsx("span", { children: t('iconStacker.bind', 'Bind to') }), jsx("input", { type: "text", value: (_p = edited.textBinding) !== null && _p !== void 0 ? _p : '', "aria-invalid": bindingInvalid || undefined, title: t('iconStacker.bindHint', 'The name of the value the text shows, e.g. count — the text above is a sample'), onChange: (e) => update(edited.id, { textBinding: e.target.value.trim() || undefined }) })] })] }))] })] })] }), showCode && (jsxs("details", { className: "nice-icon-stacker__code", children: [jsx("summary", { children: t('iconStacker.code', 'JSX code') }), jsx("pre", { children: code }), jsxs("button", { type: "button", onClick: copy, children: [copied ? jsx(NtdCheck, { size: 14 }) : jsx(NtdCopy, { size: 14 }), ' ', copied
188
205
  ? t('iconStacker.copied', 'Copied')
189
- : t('iconStacker.copy', 'Copy the code')] })] })] }))] }));
206
+ : t('iconStacker.copy', 'Copy the code')] })] }))] }))] }));
190
207
  });
191
208
  if (process.env.NODE_ENV !== 'production') {
192
209
  NiceIconStacker.displayName = 'NiceIconStacker';
@@ -1,7 +1,8 @@
1
1
  export { AnimationController, InteractiveIcon, PRESET_INTERACTIONS, PRESET_SEQUENCES, createAnimationSequence, createClickAnimation, createHoverAnimation, createVisibilityAnimation, useAnimationSequence } from './animationControls.js';
2
2
  export { NiceIconPicker } from './NiceIconPicker.js';
3
+ export { NiceIconHtml, parseIconHtml } from './iconHtml.js';
3
4
  export { NiceIconStacker } from './NiceIconStacker.js';
4
- export { NiceIconStack, isValidTextBinding, niceIconStackCode, renderIconStack } from './iconStack.js';
5
+ export { NiceIconStack, addStackLayer, iconDisplayName, isValidTextBinding, niceIconStackCode, removeStackLayer, renderIconStack } from './iconStack.js';
5
6
  export { NtdIconMorph, NtdMorphPair, NtdPathMorph, useMorphState } from './NtdIconMorph.js';
6
7
  export { NtdBadge, NtdIconOverlay, NtdIconStack, NtdStatusDot } from './NtdIconBadge.js';
7
8
  export { SMIL_ANIMATION_TYPES, SmilAnimationWrapper, createSmilAnimationDef, generateSmilAnimation, isSmilSupported } from './smilAnimations.js';
@@ -0,0 +1,117 @@
1
+ import { jsx, Fragment } from 'react/jsx-runtime';
2
+ import { renderIconStack } from './iconStack.js';
3
+
4
+ /**
5
+ * An icon — or a stack of icons — kept as HTML (1.3.9): the `<svg>` NiceIconPicker draws, ready to
6
+ * store in a database and to show anywhere, even without React. What it was drawn from rides along
7
+ * in data attributes on the root, so the picker reads its own value back.
8
+ */
9
+ const ATTR_ICON = 'data-ntd-icon';
10
+ const ATTR_VARIANT = 'data-ntd-variant';
11
+ const ATTR_STACK = 'data-ntd-stack';
12
+ const ENTITIES = {
13
+ amp: '&',
14
+ quot: '"',
15
+ apos: "'",
16
+ '#39': "'",
17
+ lt: '<',
18
+ gt: '>',
19
+ };
20
+ /** The value of a data attribute of the root element (the first one in the markup). */
21
+ function readAttr(html, name) {
22
+ const hit = new RegExp(`\\s${name}="([^"]*)"`).exec(html);
23
+ return hit
24
+ ? hit[1].replace(/&(amp|quot|apos|#39|lt|gt);/g, (_, e) => ENTITIES[e])
25
+ : undefined;
26
+ }
27
+ function isLayer(value) {
28
+ const layer = value;
29
+ return (!!layer &&
30
+ typeof layer === 'object' &&
31
+ typeof layer.id === 'string' &&
32
+ typeof layer.icon === 'string');
33
+ }
34
+ /**
35
+ * Reads back what an icon's HTML was drawn from — the base icon and the stack. `null` for anything
36
+ * else: empty, plain text or HTML the picker did not write.
37
+ */
38
+ function parseIconHtml(html) {
39
+ if (!html) {
40
+ return null;
41
+ }
42
+ const icon = readAttr(html, ATTR_ICON);
43
+ if (!icon) {
44
+ return null;
45
+ }
46
+ const stack = readAttr(html, ATTR_STACK);
47
+ if (stack) {
48
+ try {
49
+ const parsed = JSON.parse(stack);
50
+ const layers = Array.isArray(parsed) ? parsed.filter(isLayer) : [];
51
+ if (layers.length > 0) {
52
+ return { icon: layers[0].icon, layers };
53
+ }
54
+ }
55
+ catch (_a) {
56
+ // a damaged stack still shows its base
57
+ }
58
+ }
59
+ const variant = readAttr(html, ATTR_VARIANT);
60
+ return { icon, layers: [{ id: '1', icon, ...(variant ? { variant } : {}) }] };
61
+ }
62
+ /** The data attributes that let the HTML of a stack be read back: the base, and the stack unless it is a plain icon. */
63
+ function iconHtmlAttributes(layers) {
64
+ const base = layers[0];
65
+ if (!base) {
66
+ return {};
67
+ }
68
+ const plain = layers.length === 1 &&
69
+ Object.entries(base).every(([key, value]) => value === undefined || key === 'id' || key === 'icon' || key === 'variant');
70
+ if (plain) {
71
+ return { [ATTR_ICON]: base.icon, ...(base.variant ? { [ATTR_VARIANT]: base.variant } : {}) };
72
+ }
73
+ return { [ATTR_ICON]: base.icon, [ATTR_STACK]: JSON.stringify(layers) };
74
+ }
75
+ /** djb2, base 36 — a short, stable name for a drawing. */
76
+ function hash(text) {
77
+ let h = 5381;
78
+ for (let i = 0; i < text.length; i++) {
79
+ h = ((h << 5) + h + text.charCodeAt(i)) | 0;
80
+ }
81
+ return (h >>> 0).toString(36);
82
+ }
83
+ /**
84
+ * Renames the ids inside an icon's markup (masks, gradients, clip paths) after the drawing itself.
85
+ * React's ids (`_r_3_`) are unique only within the page that drew them; pasted into another page they
86
+ * could hit an element of its own and borrow the wrong mask. Named after the drawing, two copies of
87
+ * one icon share ids that point at identical definitions.
88
+ */
89
+ function scopeIconHtmlIds(markup) {
90
+ const ids = [...new Set([...markup.matchAll(/\sid="([^"]+)"/g)].map((m) => m[1]))];
91
+ if (ids.length === 0) {
92
+ return markup;
93
+ }
94
+ const index = new Map(ids.map((id, i) => [id, i]));
95
+ const rename = (fn) => markup
96
+ .replace(/(\sid=")([^"]+)(")/g, (all, a, id, b) => index.has(id) ? a + fn(id) + b : all)
97
+ .replace(/url\(#([^)]+)\)/g, (all, id) => (index.has(id) ? `url(#${fn(id)})` : all))
98
+ .replace(/(href=")#([^"]+)(")/g, (all, a, id, b) => index.has(id) ? `${a}#${fn(id)}${b}` : all);
99
+ const name = hash(rename((id) => `#${index.get(id)}`));
100
+ return rename((id) => `ntd-${name}-${index.get(id)}`);
101
+ }
102
+ /**
103
+ * NiceIconHtml (1.3.9) — shows an icon kept as HTML in a React page by drawing it again from its
104
+ * data attributes; the markup itself is never injected, so HTML from a database cannot run anything.
105
+ * A plain export name (`'NtdStar'`) works too. The rest of the props go to the base icon.
106
+ */
107
+ function NiceIconHtml({ html, icons, values, fallback = null, ...baseProps }) {
108
+ var _a;
109
+ const source = (_a = parseIconHtml(html)) !== null && _a !== void 0 ? _a : (html && !html.includes('<') ? { layers: [{ id: '1', icon: html }] } : null);
110
+ const drawn = source ? renderIconStack(source.layers, icons, { baseProps, values }) : null;
111
+ return jsx(Fragment, { children: drawn !== null && drawn !== void 0 ? drawn : fallback });
112
+ }
113
+ if (process.env.NODE_ENV !== 'production') {
114
+ NiceIconHtml.displayName = 'NiceIconHtml';
115
+ }
116
+
117
+ export { NiceIconHtml, iconHtmlAttributes, parseIconHtml, scopeIconHtmlIds };
@@ -14,6 +14,74 @@ function stackParentId(layers, layer) {
14
14
  function stackChildrenOf(layers, id) {
15
15
  return layers.filter((l) => l !== layers[0] && stackParentId(layers, l) === id);
16
16
  }
17
+ /** An icon's name as people read it: the export name without `Ntd` — `NtdAlertBell` → `AlertBell`. */
18
+ function iconDisplayName(name) {
19
+ return name.replace(/^Ntd(?=[A-Z0-9])/, '');
20
+ }
21
+ /** A short id no layer of the stack has yet: `'1'`, `'2'`… (1.3.9 — it was the icon name and a timestamp). */
22
+ function nextStackLayerId(layers) {
23
+ const taken = new Set(layers.map((l) => l.id));
24
+ let n = layers.length + 1;
25
+ while (taken.has(String(n))) {
26
+ n += 1;
27
+ }
28
+ return String(n);
29
+ }
30
+ /**
31
+ * Puts an icon on the stack (1.3.9): into layer `into` (the base when absent or unknown), or as the
32
+ * base of an empty stack. Returns the new layers and the new layer's id.
33
+ */
34
+ function addStackLayer(layers, icon, into) {
35
+ const id = nextStackLayerId(layers);
36
+ const parent = layers.find((l) => l.id === into);
37
+ const layer = {
38
+ id,
39
+ icon: icon.icon,
40
+ ...(icon.variant ? { variant: icon.variant } : {}),
41
+ ...(parent && parent !== layers[0] ? { parent: parent.id } : {}),
42
+ };
43
+ return { layers: [...layers, layer], id };
44
+ }
45
+ /**
46
+ * Takes a layer off the stack (1.3.9; NiceIconStacker's own until then): what sat in it moves up
47
+ * into its parent, and a removed base hands over to its first child.
48
+ */
49
+ function removeStackLayer(layers, id) {
50
+ const base = layers[0];
51
+ if (!base) {
52
+ return [];
53
+ }
54
+ if (id !== base.id) {
55
+ const gone = layers.find((l) => l.id === id);
56
+ const up = gone ? stackParentId(layers, gone) : undefined;
57
+ return layers
58
+ .filter((l) => l.id !== id)
59
+ .map((l) => (l.parent === id ? { ...l, parent: up === base.id ? undefined : up } : l));
60
+ }
61
+ const [heir, ...siblings] = stackChildrenOf(layers, base.id);
62
+ if (!heir) {
63
+ return [];
64
+ }
65
+ const newBase = {
66
+ ...heir,
67
+ parent: undefined,
68
+ align: undefined,
69
+ size: undefined,
70
+ rotation: undefined,
71
+ offsetX: undefined,
72
+ offsetY: undefined,
73
+ };
74
+ return [
75
+ newBase,
76
+ ...layers
77
+ .filter((l) => l.id !== base.id && l.id !== heir.id)
78
+ .map((l) => siblings.includes(l)
79
+ ? { ...l, parent: heir.id }
80
+ : l.parent === heir.id
81
+ ? { ...l, parent: undefined }
82
+ : l),
83
+ ];
84
+ }
17
85
  const COLOR_PROPS = ['primaryColor', 'secondaryColor', 'accentColor', 'textColor'];
18
86
  /** The stack as JSX: every layer wrapping the layers that sit in it, text innermost. */
19
87
  function niceIconStackCode(layers) {
@@ -103,4 +171,4 @@ if (process.env.NODE_ENV !== 'production') {
103
171
  NiceIconStack.displayName = 'NiceIconStack';
104
172
  }
105
173
 
106
- export { NiceIconStack, isValidTextBinding, niceIconStackCode, renderIconStack, stackChildrenOf, stackParentId };
174
+ export { NiceIconStack, addStackLayer, iconDisplayName, isValidTextBinding, niceIconStackCode, removeStackLayer, renderIconStack, stackChildrenOf, stackParentId };
package/dist/esm/index.js CHANGED
@@ -2,8 +2,9 @@ export { GRADIENT_PRESETS, SEMANTIC_COLOR_DEFAULTS, SEMANTIC_COLOR_VARS, SIZE_MA
2
2
  export { createIcon } from './createIcon.js';
3
3
  export { SmilAnimationWrapper as NiceSmilAnimationWrapper, SMIL_ANIMATION_TYPES, SmilAnimationWrapper, createSmilAnimationDef, generateSmilAnimation, isSmilSupported } from './smilAnimations.js';
4
4
  export { NiceIconPicker } from './NiceIconPicker.js';
5
+ export { NiceIconHtml, parseIconHtml } from './iconHtml.js';
5
6
  export { NiceIconStacker } from './NiceIconStacker.js';
6
- export { NiceIconStack, isValidTextBinding, niceIconStackCode, renderIconStack } from './iconStack.js';
7
+ export { NiceIconStack, addStackLayer, iconDisplayName, isValidTextBinding, niceIconStackCode, removeStackLayer, renderIconStack } from './iconStack.js';
7
8
  export { AnimationController, InteractiveIcon, AnimationController as NiceAnimationController, InteractiveIcon as NiceInteractiveIcon, PRESET_INTERACTIONS, PRESET_SEQUENCES, createAnimationSequence, createClickAnimation, createHoverAnimation, createVisibilityAnimation, useAnimationSequence } from './animationControls.js';
8
9
  export { batchExportIcons, copyIconToClipboard, downloadIconAsPng, downloadIconAsSvg, exportIconToDataUrl, exportIconToFigmaNode, exportIconToSvgString, generateFigmaPluginManifest } from './figmaExport.js';
9
10
  export { NtdArrowDown, NtdArrowLeft, NtdArrowRight, NtdArrowUp, NtdChevronDown, NtdChevronLeft, NtdChevronRight, NtdChevronUp, NtdClose, NtdExternalLink, NtdMenu, NtdMoreHorizontal, NtdMoreVertical, NtdRefresh } from './icons/navigation.js';