@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
@@ -8,26 +8,51 @@ var navigation = require('./icons/navigation.js');
8
8
  var iconStack = require('./iconStack.js');
9
9
  var stacking = require('./stacking.js');
10
10
 
11
- const ALIGNS = [
12
- 'center',
13
- 'top',
14
- 'bottom',
15
- 'left',
16
- 'right',
17
- 'top-left',
18
- 'top-right',
19
- 'bottom-left',
20
- 'bottom-right',
21
- 'half-top',
22
- 'half-bottom',
23
- 'half-left',
24
- 'half-right',
25
- 'half-top-left',
26
- 'half-top-right',
27
- 'half-bottom-left',
28
- 'half-bottom-right',
11
+ /** Where a layer can sit, laid out on the 5 × 5 pad as it sits in its parent. */
12
+ const PAD = [
13
+ { align: 'top-left', row: 1, col: 1 },
14
+ { align: 'top', row: 1, col: 3 },
15
+ { align: 'top-right', row: 1, col: 5 },
16
+ { align: 'half-top-left', row: 2, col: 2 },
17
+ { align: 'half-top', row: 2, col: 3 },
18
+ { align: 'half-top-right', row: 2, col: 4 },
19
+ { align: 'left', row: 3, col: 1 },
20
+ { align: 'half-left', row: 3, col: 2 },
21
+ { align: 'center', row: 3, col: 3 },
22
+ { align: 'half-right', row: 3, col: 4 },
23
+ { align: 'right', row: 3, col: 5 },
24
+ { align: 'half-bottom-left', row: 4, col: 2 },
25
+ { align: 'half-bottom', row: 4, col: 3 },
26
+ { align: 'half-bottom-right', row: 4, col: 4 },
27
+ { align: 'bottom-left', row: 5, col: 1 },
28
+ { align: 'bottom', row: 5, col: 3 },
29
+ { align: 'bottom-right', row: 5, col: 5 },
29
30
  ];
30
- const PREVIEW_SIZES = [96, 48, 24];
31
+ /** The positions' names, in words. */
32
+ function alignNames(t) {
33
+ return {
34
+ perfect: t('iconStacker.align.perfect', 'Auto'),
35
+ center: t('iconStacker.align.center', 'Centre'),
36
+ top: t('iconStacker.align.top', 'Top edge'),
37
+ bottom: t('iconStacker.align.bottom', 'Bottom edge'),
38
+ left: t('iconStacker.align.left', 'Left edge'),
39
+ right: t('iconStacker.align.right', 'Right edge'),
40
+ 'top-left': t('iconStacker.align.top-left', 'Top-left corner'),
41
+ 'top-right': t('iconStacker.align.top-right', 'Top-right corner'),
42
+ 'bottom-left': t('iconStacker.align.bottom-left', 'Bottom-left corner'),
43
+ 'bottom-right': t('iconStacker.align.bottom-right', 'Bottom-right corner'),
44
+ 'half-top': t('iconStacker.align.half-top', 'Halfway to the top'),
45
+ 'half-bottom': t('iconStacker.align.half-bottom', 'Halfway to the bottom'),
46
+ 'half-left': t('iconStacker.align.half-left', 'Halfway to the left'),
47
+ 'half-right': t('iconStacker.align.half-right', 'Halfway to the right'),
48
+ 'half-top-left': t('iconStacker.align.half-top-left', 'Halfway to the top-left corner'),
49
+ 'half-top-right': t('iconStacker.align.half-top-right', 'Halfway to the top-right corner'),
50
+ 'half-bottom-left': t('iconStacker.align.half-bottom-left', 'Halfway to the bottom-left corner'),
51
+ 'half-bottom-right': t('iconStacker.align.half-bottom-right', 'Halfway to the bottom-right corner'),
52
+ };
53
+ }
54
+ const STAGE_SIZE = 64;
55
+ const STAGE_SMALL = [32, 24, 16];
31
56
  const NO_LAYERS = [];
32
57
  /** What a colour field shows while the colour is unset (follows the parent). */
33
58
  const AUTO_SWATCH = '#808080';
@@ -45,57 +70,21 @@ function walk(layers) {
45
70
  }
46
71
  return out;
47
72
  }
48
- /** Removes a layer; what sat in it moves up into its parent (a removed base hands over to its first child). */
49
- function removeLayer(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 ? iconStack.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] = iconStack.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
- }
85
73
  /** A colour of the layer's own, or "auto" — following its parent — until one is picked. */
86
- function ColorField({ label, value, onChange, autoLabel, resetLabel }) {
87
- return (jsxRuntime.jsxs("span", { className: "nice-icon-stacker__color", children: [jsxRuntime.jsxs("label", { className: "nice-icon-stacker__field", children: [jsxRuntime.jsx("span", { children: label }), jsxRuntime.jsx("input", { type: "color", value: value !== null && value !== void 0 ? value : AUTO_SWATCH, onChange: (e) => onChange(e.target.value) })] }), value ? (jsxRuntime.jsx("button", { type: "button", className: "nice-icon-stacker__reset", onClick: () => onChange(undefined), "aria-label": resetLabel, title: resetLabel, children: jsxRuntime.jsx(navigation.NtdClose, { size: 12 }) })) : (jsxRuntime.jsx("small", { className: "nice-icon-stacker__auto", children: autoLabel }))] }));
74
+ function Swatch({ label, value, onChange, autoLabel, resetLabel }) {
75
+ return (jsxRuntime.jsxs("span", { className: "nice-icon-stacker__swatch", children: [jsxRuntime.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: [jsxRuntime.jsx("span", { className: "nice-icon-stacker__swatch-dot", style: value ? { background: value } : undefined }), jsxRuntime.jsx("span", { children: label }), jsxRuntime.jsx("input", { type: "color", value: value !== null && value !== void 0 ? value : AUTO_SWATCH, onChange: (e) => onChange(e.target.value) })] }), value && (jsxRuntime.jsx("button", { type: "button", className: "nice-icon-stacker__reset", onClick: () => onChange(undefined), "aria-label": `${resetLabel}: ${label}`, title: resetLabel, children: jsxRuntime.jsx(navigation.NtdClose, { size: 10 }) }))] }));
88
76
  }
89
77
  /**
90
- * NiceIconStacker (1.3.5) — builds an icon stack: the base icon and the icons stacked in it, each
91
- * with its position, size, turn and colours, nested any number of levels deep (1.3.6) with text
92
- * innermost — its size, colour and the value it is bound to — shown live at three sizes and as the
93
- * JSX that draws it. The "add" button puts `current` into the selected layer and selects it, so
94
- * adding again goes one level deeper; select an outer layer to add side by side. Layers leave one
95
- * by one or all at once. `NiceIconStack` draws a stack built here from data.
78
+ * NiceIconStacker (1.3.5) — builds an icon stack: the base icon and the icons stacked in it, nested
79
+ * any number of levels deep (1.3.6) with text innermost. Since 1.3.9 it is laid out to fit a picker's
80
+ * panel: the stack drawn large and small, the layers as a list, and under it the selected layer —
81
+ * where it sits (picked on a pad shaped like its parent), its size and text, with the turn, nudge,
82
+ * colours and binding folded under "More". A new icon goes into the selected layer and is selected,
83
+ * so adding again goes one level deeper; select an outer layer to add side by side. The JSX that
84
+ * draws the stack is folded at the end. `NiceIconStack` draws a stack built here from data.
96
85
  */
97
- const NiceIconStacker = React.forwardRef(function NiceIconStacker({ icons, layers: layersProp, defaultLayers, onLayersChange, current, previewProps, className }, ref) {
98
- var _a, _b, _c, _d;
86
+ const NiceIconStacker = React.forwardRef(function NiceIconStacker({ icons, layers: layersProp, defaultLayers, onLayersChange, current, onAddClick, previewProps, iconLabel = iconStack.iconDisplayName, showCode = true, keepBase = false, className, }, ref) {
87
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
99
88
  const { t } = uiCore.useNiceTranslation();
100
89
  const [layers, setLayers] = uiCore.useControllableState({
101
90
  value: layersProp,
@@ -104,43 +93,47 @@ const NiceIconStacker = React.forwardRef(function NiceIconStacker({ icons, layer
104
93
  onChange: onLayersChange,
105
94
  componentName: 'NiceIconStacker',
106
95
  });
107
- const nextId = React.useRef(0);
108
- const targetGroup = React.useId();
109
96
  const [copied, setCopied] = React.useState(false);
110
- // The layer the next icon goes into — the newest by default, so adding nests deeper.
97
+ // The layer being edited, which the next icon goes into — the newest by default, so adding nests deeper.
111
98
  const [selected, setSelected] = React.useState(undefined);
112
99
  const base = layers[0];
113
100
  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;
101
+ const listRef = React.useRef(null);
102
+ uiCore.useRovingFocus(listRef, {
103
+ orientation: 'vertical',
104
+ itemSelector: '.nice-icon-stacker__layer-select',
105
+ });
106
+ const padRef = React.useRef(null);
107
+ uiCore.useRovingFocus(padRef, {
108
+ orientation: 'grid',
109
+ itemSelector: '[role="radio"]',
110
+ activateOnMove: true,
111
+ });
114
112
  const resolve = React.useCallback((name) => (typeof icons === 'function' ? icons(name) : icons[name]), [icons]);
115
113
  const tree = React.useMemo(() => walk(layers), [layers]);
116
- const code = React.useMemo(() => iconStack.niceIconStackCode(layers), [layers]);
114
+ const code = React.useMemo(() => (showCode ? iconStack.niceIconStackCode(layers) : ''), [layers, showCode]);
117
115
  const update = (id, patch) => setLayers((prev) => prev.map((l) => (l.id === id ? { ...l, ...patch } : l)));
118
- const add = () => {
119
- if (!current) {
116
+ const add = (icon = current) => {
117
+ if (!icon) {
120
118
  return;
121
119
  }
122
- nextId.current += 1;
123
- const id = `${current.icon}-${Date.now().toString(36)}-${nextId.current}`;
120
+ let added = '';
124
121
  setLayers((prev) => {
125
- const into = prev.find((l) => l.id === (target === null || target === void 0 ? void 0 : target.id));
126
- const layer = {
127
- id,
128
- icon: current.icon,
129
- variant: current.variant,
130
- ...(into && into !== prev[0] ? { parent: into.id } : {}),
131
- };
132
- return [...prev, layer];
122
+ var _a;
123
+ const next = iconStack.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);
124
+ added = next.id;
125
+ return next.layers;
133
126
  });
134
- setSelected(id);
127
+ setSelected(added);
135
128
  };
136
129
  const remove = (id) => {
137
- setLayers((prev) => removeLayer(prev, id));
130
+ setLayers((prev) => iconStack.removeStackLayer(prev, id));
138
131
  if (selected === id) {
139
132
  setSelected(undefined);
140
133
  }
141
134
  };
142
135
  const clear = () => {
143
- setLayers(NO_LAYERS);
136
+ setLayers((prev) => (keepBase && prev[0] ? [prev[0]] : NO_LAYERS));
144
137
  setSelected(undefined);
145
138
  };
146
139
  React.useImperativeHandle(ref, () => ({ add, clear }));
@@ -153,30 +146,66 @@ const NiceIconStacker = React.forwardRef(function NiceIconStacker({ icons, layer
153
146
  };
154
147
  const autoLabel = t('iconStacker.auto', 'auto');
155
148
  const resetLabel = t('iconStacker.reset', 'Back to auto');
156
- const addLabel = base && target
157
- ? t('iconStacker.addInto', 'Add {icon} into {target}')
158
- .replace('{icon}', (_c = current === null || current === void 0 ? void 0 : current.icon) !== null && _c !== void 0 ? _c : '')
159
- .replace('{target}', target.icon)
160
- : 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 : '');
161
- return (jsxRuntime.jsxs("div", { className: ['nice-icon-stacker', className].filter(Boolean).join(' '), children: [jsxRuntime.jsxs("div", { className: "nice-icon-stacker__toolbar", children: [jsxRuntime.jsxs("button", { type: "button", onClick: add, disabled: !current, children: [jsxRuntime.jsx(actions.NtdPlus, { size: 14 }), " ", addLabel] }), jsxRuntime.jsxs("button", { type: "button", onClick: clear, disabled: layers.length === 0, children: [jsxRuntime.jsx(actions.NtdTrash, { size: 14 }), " ", t('iconStacker.clear', 'Clear the stack')] })] }), !base ? (jsxRuntime.jsx("p", { className: "nice-icon-stacker__empty", children: t('iconStacker.empty', 'The stack is empty. Add an icon to start one.') })) : (jsxRuntime.jsxs("div", { className: "nice-icon-stacker__body", children: [jsxRuntime.jsx("div", { className: "nice-icon-stacker__preview", "aria-label": t('iconStacker.preview', 'Preview'), role: "group", children: PREVIEW_SIZES.map((s) => (jsxRuntime.jsxs("span", { className: "nice-icon-stacker__preview-cell", children: [iconStack.renderIconStack(layers, icons, {
162
- baseProps: { ...previewProps, size: s, title: base.icon },
163
- }), jsxRuntime.jsxs("small", { children: [s, "px"] })] }, s))) }), jsxRuntime.jsx("ol", { className: "nice-icon-stacker__layers", "aria-label": t('iconStacker.layers', 'Layers'), children: tree.map(({ layer: l, depth }) => {
164
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
165
- const Icon = resolve(l.icon);
166
- const isBase = l === base;
167
- const align = (_a = l.align) !== null && _a !== void 0 ? _a : 'center';
168
- const share = (_b = l.size) !== null && _b !== void 0 ? _b : stacking.defaultStackSize(align);
169
- const innermost = iconStack.stackChildrenOf(layers, l.id).length === 0;
170
- const textSize = (_c = l.textSize) !== null && _c !== void 0 ? _c : stacking.defaultTextSize(((_d = l.text) !== null && _d !== void 0 ? _d : '').length || 1);
171
- const bindingInvalid = !!l.textBinding && !iconStack.isValidTextBinding(l.textBinding);
172
- const depthStyle = {
173
- ['--nice-icon-stacker-depth']: String(depth),
174
- };
175
- const label = (key, fallback) => t(key, fallback).replace('{icon}', l.icon);
176
- return (jsxRuntime.jsxs("li", { className: "nice-icon-stacker__layer", style: depthStyle, children: [jsxRuntime.jsxs("div", { className: "nice-icon-stacker__layer-row", children: [jsxRuntime.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}') }), jsxRuntime.jsx("span", { className: "nice-icon-stacker__layer-icon", children: Icon ? jsxRuntime.jsx(Icon, { size: 24, variant: l.variant }) : null }), jsxRuntime.jsxs("span", { className: "nice-icon-stacker__layer-name", children: [l.icon, isBase && (jsxRuntime.jsx("span", { className: "nice-icon-stacker__badge", children: t('iconStacker.base', 'Base') }))] }), !isBase && (jsxRuntime.jsxs("label", { className: "nice-icon-stacker__field", children: [jsxRuntime.jsx("span", { children: t('iconStacker.align', 'Position') }), jsxRuntime.jsx("select", { value: align, onChange: (e) => update(l.id, { align: e.target.value }), children: ALIGNS.map((a) => (jsxRuntime.jsx("option", { value: a, children: a }, a))) })] })), !isBase && (jsxRuntime.jsxs("label", { className: "nice-icon-stacker__field", children: [jsxRuntime.jsx("span", { children: t('iconStacker.size', 'Size') }), jsxRuntime.jsx("input", { type: "range", min: 0.2, max: 1, step: 0.05, value: share, onChange: (e) => update(l.id, { size: Number(e.target.value) }) }), jsxRuntime.jsxs("output", { children: [Math.round(share * 100), "%"] })] })), jsxRuntime.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: jsxRuntime.jsx(actions.NtdTrash, { size: 14 }) })] }), jsxRuntime.jsxs("details", { className: "nice-icon-stacker__more", children: [jsxRuntime.jsx("summary", { children: t('iconStacker.more', 'More') }), jsxRuntime.jsxs("div", { className: "nice-icon-stacker__more-body", children: [!isBase && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("label", { className: "nice-icon-stacker__field", children: [jsxRuntime.jsx("span", { children: t('iconStacker.rotation', 'Turn') }), jsxRuntime.jsx("input", { type: "range", min: -180, max: 180, step: 15, value: (_e = l.rotation) !== null && _e !== void 0 ? _e : 0, onChange: (e) => update(l.id, { rotation: Number(e.target.value) || undefined }) }), jsxRuntime.jsxs("output", { children: [(_f = l.rotation) !== null && _f !== void 0 ? _f : 0, "\u00B0"] })] }), jsxRuntime.jsxs("label", { className: "nice-icon-stacker__field", children: [jsxRuntime.jsx("span", { children: t('iconStacker.offsetX', 'Shift right') }), jsxRuntime.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 }) }), jsxRuntime.jsxs("output", { children: [(_h = l.offsetX) !== null && _h !== void 0 ? _h : 0, "%"] })] }), jsxRuntime.jsxs("label", { className: "nice-icon-stacker__field", children: [jsxRuntime.jsx("span", { children: t('iconStacker.offsetY', 'Shift down') }), jsxRuntime.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 }) }), jsxRuntime.jsxs("output", { children: [(_k = l.offsetY) !== null && _k !== void 0 ? _k : 0, "%"] })] })] })), jsxRuntime.jsx(ColorField, { label: t('iconStacker.primary', 'Primary'), value: l.primaryColor, onChange: (v) => update(l.id, { primaryColor: v }), autoLabel: autoLabel, resetLabel: resetLabel }), jsxRuntime.jsx(ColorField, { label: t('iconStacker.secondary', 'Secondary'), value: l.secondaryColor, onChange: (v) => update(l.id, { secondaryColor: v }), autoLabel: autoLabel, resetLabel: resetLabel }), jsxRuntime.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) && (jsxRuntime.jsxs("div", { className: "nice-icon-stacker__text", children: [jsxRuntime.jsxs("label", { className: "nice-icon-stacker__field", children: [jsxRuntime.jsx("span", { children: t('iconStacker.text', 'Text') }), jsxRuntime.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 }) })] }), jsxRuntime.jsxs("label", { className: "nice-icon-stacker__field", children: [jsxRuntime.jsx("span", { children: t('iconStacker.textSize', 'Text size') }), jsxRuntime.jsx("input", { type: "range", min: 3, max: 14, step: 0.5, value: textSize, onChange: (e) => update(l.id, { textSize: Number(e.target.value) }) }), jsxRuntime.jsx("output", { children: textSize }), l.textSize !== undefined && (jsxRuntime.jsx("button", { type: "button", className: "nice-icon-stacker__reset", onClick: () => update(l.id, { textSize: undefined }), "aria-label": resetLabel, title: resetLabel, children: jsxRuntime.jsx(navigation.NtdClose, { size: 12 }) }))] }), jsxRuntime.jsx(ColorField, { label: t('iconStacker.textColor', 'Text colour'), value: l.textColor, onChange: (v) => update(l.id, { textColor: v }), autoLabel: autoLabel, resetLabel: resetLabel }), jsxRuntime.jsxs("label", { className: "nice-icon-stacker__field", children: [jsxRuntime.jsx("span", { children: t('iconStacker.bind', 'Bind to') }), jsxRuntime.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));
177
- }) }), jsxRuntime.jsxs("div", { className: "nice-icon-stacker__code", children: [jsxRuntime.jsx("pre", { children: code }), jsxRuntime.jsxs("button", { type: "button", onClick: copy, children: [copied ? jsxRuntime.jsx(actions.NtdCheck, { size: 14 }) : jsxRuntime.jsx(actions.NtdCopy, { size: 14 }), ' ', copied
149
+ const names = alignNames(t);
150
+ const alignName = (a) => names[a];
151
+ const currentName = current ? iconLabel(current.icon) : '';
152
+ const targetName = target ? iconLabel(target.icon) : '';
153
+ const asks = !!onAddClick || !current;
154
+ const addText = asks
155
+ ? t('iconStacker.addIcon', 'Add an icon')
156
+ : t('iconStacker.addShort', 'Add {icon}').replace('{icon}', currentName);
157
+ const addLabel = asks
158
+ ? base && target
159
+ ? t('iconStacker.addIconInto', 'Add an icon into {target}').replace('{target}', targetName)
160
+ : addText
161
+ : base && target
162
+ ? t('iconStacker.addInto', 'Add {icon} into {target}')
163
+ .replace('{icon}', currentName)
164
+ .replace('{target}', targetName)
165
+ : t('iconStacker.add', 'Add {icon} to the stack').replace('{icon}', currentName);
166
+ const clearLabel = keepBase
167
+ ? t('iconStacker.clearOverlays', 'Take off the stacked icons')
168
+ : t('iconStacker.clear', 'Clear the stack');
169
+ // the selected layer, as the inspector edits it
170
+ const edited = target;
171
+ const editedIcon = edited ? resolve(edited.icon) : undefined;
172
+ const editedIsBase = !!edited && edited === base;
173
+ const align = (_c = edited === null || edited === void 0 ? void 0 : edited.align) !== null && _c !== void 0 ? _c : 'perfect';
174
+ // the spot the parent keeps for stacked icons (1.3.8): its size and turn are the layer's
175
+ // defaults while it sits there
176
+ const parentId = edited ? iconStack.stackParentId(layers, edited) : undefined;
177
+ const parentLayer = parentId ? layers.find((p) => p.id === parentId) : undefined;
178
+ const spot = (parentLayer && stacking.perfectStackOf(resolve(parentLayer.icon))) || stacking.BOX_PERFECT_STACK;
179
+ const atSpot = align === 'perfect';
180
+ const share = (_d = edited === null || edited === void 0 ? void 0 : edited.size) !== null && _d !== void 0 ? _d : (atSpot ? spot.size : stacking.defaultStackSize(align));
181
+ const turnDefault = atSpot ? spot.rotation : 0;
182
+ const turn = (_e = edited === null || edited === void 0 ? void 0 : edited.rotation) !== null && _e !== void 0 ? _e : turnDefault;
183
+ const innermost = !!edited && iconStack.stackChildrenOf(layers, edited.id).length === 0;
184
+ const hasText = !!edited && (innermost || !!edited.text || !!edited.textBinding);
185
+ const textSize = (_f = edited === null || edited === void 0 ? void 0 : edited.textSize) !== null && _f !== void 0 ? _f : stacking.defaultTextSize(((_g = edited === null || edited === void 0 ? void 0 : edited.text) !== null && _g !== void 0 ? _g : '').length || 1, (_h = stacking.perfectStackOf(editedIcon)) === null || _h === void 0 ? void 0 : _h.size);
186
+ const bindingInvalid = !!(edited === null || edited === void 0 ? void 0 : edited.textBinding) && !iconStack.isValidTextBinding(edited.textBinding);
187
+ return (jsxRuntime.jsxs("div", { className: ['nice-icon-stacker', className].filter(Boolean).join(' '), children: [jsxRuntime.jsxs("div", { className: "nice-icon-stacker__bar", children: [jsxRuntime.jsx("span", { className: "nice-icon-stacker__title", children: t('iconStacker.layers', 'Layers') }), jsxRuntime.jsxs("button", { type: "button", className: "nice-icon-stacker__add", onClick: () => (onAddClick ? onAddClick(target) : add()), disabled: !onAddClick && !current, "aria-label": addLabel, title: addLabel, children: [jsxRuntime.jsx(actions.NtdPlus, { size: 14 }), " ", addText] }), jsxRuntime.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: jsxRuntime.jsx(actions.NtdTrash, { size: 14 }) })] }), !base || !edited ? (jsxRuntime.jsx("p", { className: "nice-icon-stacker__empty", children: t('iconStacker.empty', 'The stack is empty. Add an icon to start one.') })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "nice-icon-stacker__top", children: [jsxRuntime.jsxs("div", { className: "nice-icon-stacker__preview", "aria-label": t('iconStacker.preview', 'Preview'), role: "group", children: [jsxRuntime.jsx("span", { className: "nice-icon-stacker__preview-main", title: `${STAGE_SIZE}px`, children: iconStack.renderIconStack(layers, icons, {
188
+ baseProps: { ...previewProps, size: STAGE_SIZE, title: iconLabel(base.icon) },
189
+ }) }), jsxRuntime.jsx("span", { className: "nice-icon-stacker__preview-small", children: STAGE_SMALL.map((s) => (jsxRuntime.jsx("span", { title: `${s}px`, children: iconStack.renderIconStack(layers, icons, { baseProps: { ...previewProps, size: s } }) }, s))) })] }), jsxRuntime.jsxs("div", { className: "nice-icon-stacker__layers-wrap", children: [jsxRuntime.jsx("ol", { ref: listRef, className: "nice-icon-stacker__layers", "aria-label": t('iconStacker.layers', 'Layers'), children: tree.map(({ layer: l, depth }) => {
190
+ const Icon = resolve(l.icon);
191
+ const isBase = l === base;
192
+ const isSelected = l === edited;
193
+ const name = iconLabel(l.icon);
194
+ const removable = !(isBase && keepBase);
195
+ const removeLabel = t('iconStacker.remove', 'Remove {icon}').replace('{icon}', name);
196
+ return (jsxRuntime.jsxs("li", { className: `nice-icon-stacker__layer${isSelected ? ' nice-icon-stacker__layer--selected' : ''}`, style: { ['--nice-icon-stacker-depth']: String(depth) }, children: [jsxRuntime.jsxs("button", { type: "button", className: "nice-icon-stacker__layer-select", "aria-pressed": isSelected, onClick: () => setSelected(l.id), onKeyDown: (e) => {
197
+ if (removable && (e.key === 'Delete' || e.key === 'Backspace')) {
198
+ e.preventDefault();
199
+ remove(l.id);
200
+ }
201
+ }, children: [jsxRuntime.jsx("span", { className: "nice-icon-stacker__layer-icon", children: Icon ? jsxRuntime.jsx(Icon, { size: 18, variant: l.variant }) : null }), jsxRuntime.jsx("span", { className: "nice-icon-stacker__layer-name", children: name }), l.text && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [' ', jsxRuntime.jsx("span", { className: "nice-icon-stacker__layer-text", children: l.text })] })), isBase && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [' ', jsxRuntime.jsx("span", { className: "nice-icon-stacker__badge", children: t('iconStacker.base', 'Base') })] }))] }), removable && (jsxRuntime.jsx("button", { type: "button", className: "nice-icon-stacker__remove", onClick: () => remove(l.id), "aria-label": removeLabel, title: removeLabel, children: jsxRuntime.jsx(navigation.NtdClose, { size: 10 }) }))] }, l.id));
202
+ }) }), jsxRuntime.jsx("small", { className: "nice-icon-stacker__hint", children: t('iconStacker.hint', 'A new icon goes into the selected layer.') })] })] }), jsxRuntime.jsxs("div", { className: "nice-icon-stacker__inspector", role: "group", "aria-label": iconLabel(edited.icon), children: [jsxRuntime.jsxs("div", { className: "nice-icon-stacker__inspector-head", children: [editedIcon &&
203
+ React.createElement(editedIcon, { size: 16, variant: edited.variant }), jsxRuntime.jsx("span", { children: iconLabel(edited.icon) }), parentLayer && (jsxRuntime.jsx("small", { children: t('iconStacker.inside', 'in {icon}').replace('{icon}', iconLabel(parentLayer.icon)) }))] }), jsxRuntime.jsxs("div", { className: "nice-icon-stacker__inspector-body", children: [!editedIsBase && (jsxRuntime.jsxs("div", { ref: padRef, className: "nice-icon-stacker__pad", role: "radiogroup", "aria-label": t('iconStacker.align', 'Position'), children: [jsxRuntime.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') }), jsxRuntime.jsx("span", { className: "nice-icon-stacker__pad-box", children: PAD.map(({ align: a, row, col }) => (jsxRuntime.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))) })] })), jsxRuntime.jsxs("div", { className: "nice-icon-stacker__fields", children: [!editedIsBase && (jsxRuntime.jsxs("label", { className: "nice-icon-stacker__field", children: [jsxRuntime.jsx("span", { children: t('iconStacker.size', 'Size') }), jsxRuntime.jsx("input", { type: "range", min: 0.2, max: 1, step: 0.05, value: share, onChange: (e) => update(edited.id, { size: Number(e.target.value) }) }), jsxRuntime.jsxs("output", { children: [Math.round(share * 100), "%"] })] })), hasText && (jsxRuntime.jsxs("label", { className: "nice-icon-stacker__field", children: [jsxRuntime.jsx("span", { children: t('iconStacker.text', 'Text') }), jsxRuntime.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 }) })] }))] })] }), jsxRuntime.jsxs("details", { className: "nice-icon-stacker__more", children: [jsxRuntime.jsx("summary", { children: t('iconStacker.more', 'More') }), jsxRuntime.jsxs("div", { className: "nice-icon-stacker__more-body", children: [!editedIsBase && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("label", { className: "nice-icon-stacker__field", children: [jsxRuntime.jsx("span", { children: t('iconStacker.rotation', 'Turn') }), jsxRuntime.jsx("input", { type: "range", min: -180, max: 180, step: 15, value: turn, onChange: (e) => {
204
+ const v = Number(e.target.value);
205
+ update(edited.id, { rotation: v === turnDefault ? undefined : v });
206
+ } }), jsxRuntime.jsxs("output", { children: [turn, "\u00B0"] })] }), jsxRuntime.jsxs("label", { className: "nice-icon-stacker__field", children: [jsxRuntime.jsx("span", { children: t('iconStacker.offsetX', 'Shift right') }), jsxRuntime.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 }) }), jsxRuntime.jsxs("output", { children: [(_l = edited.offsetX) !== null && _l !== void 0 ? _l : 0, "%"] })] }), jsxRuntime.jsxs("label", { className: "nice-icon-stacker__field", children: [jsxRuntime.jsx("span", { children: t('iconStacker.offsetY', 'Shift down') }), jsxRuntime.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 }) }), jsxRuntime.jsxs("output", { children: [(_o = edited.offsetY) !== null && _o !== void 0 ? _o : 0, "%"] })] })] })), jsxRuntime.jsxs("div", { className: "nice-icon-stacker__swatches", children: [jsxRuntime.jsx(Swatch, { label: t('iconStacker.primary', 'Primary'), value: edited.primaryColor, onChange: (v) => update(edited.id, { primaryColor: v }), autoLabel: autoLabel, resetLabel: resetLabel }), jsxRuntime.jsx(Swatch, { label: t('iconStacker.secondary', 'Secondary'), value: edited.secondaryColor, onChange: (v) => update(edited.id, { secondaryColor: v }), autoLabel: autoLabel, resetLabel: resetLabel }), jsxRuntime.jsx(Swatch, { label: t('iconStacker.accent', 'Accent'), value: edited.accentColor, onChange: (v) => update(edited.id, { accentColor: v }), autoLabel: autoLabel, resetLabel: resetLabel }), hasText && (jsxRuntime.jsx(Swatch, { label: t('iconStacker.textColor', 'Text colour'), value: edited.textColor, onChange: (v) => update(edited.id, { textColor: v }), autoLabel: autoLabel, resetLabel: resetLabel }))] }), hasText && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("label", { className: "nice-icon-stacker__field", children: [jsxRuntime.jsx("span", { children: t('iconStacker.textSize', 'Text size') }), jsxRuntime.jsx("input", { type: "range", min: 3, max: 14, step: 0.5, value: textSize, onChange: (e) => update(edited.id, { textSize: Number(e.target.value) }) }), jsxRuntime.jsx("output", { children: textSize }), edited.textSize !== undefined && (jsxRuntime.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: jsxRuntime.jsx(navigation.NtdClose, { size: 10 }) }))] }), jsxRuntime.jsxs("label", { className: "nice-icon-stacker__field", children: [jsxRuntime.jsx("span", { children: t('iconStacker.bind', 'Bind to') }), jsxRuntime.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 && (jsxRuntime.jsxs("details", { className: "nice-icon-stacker__code", children: [jsxRuntime.jsx("summary", { children: t('iconStacker.code', 'JSX code') }), jsxRuntime.jsx("pre", { children: code }), jsxRuntime.jsxs("button", { type: "button", onClick: copy, children: [copied ? jsxRuntime.jsx(actions.NtdCheck, { size: 14 }) : jsxRuntime.jsx(actions.NtdCopy, { size: 14 }), ' ', copied
178
207
  ? t('iconStacker.copied', 'Copied')
179
- : t('iconStacker.copy', 'Copy the code')] })] })] }))] }));
208
+ : t('iconStacker.copy', 'Copy the code')] })] }))] }))] }));
180
209
  });
181
210
  if (process.env.NODE_ENV !== 'production') {
182
211
  NiceIconStacker.displayName = 'NiceIconStacker';
@@ -2,6 +2,7 @@
2
2
 
3
3
  var animationControls = require('./animationControls.js');
4
4
  var NiceIconPicker = require('./NiceIconPicker.js');
5
+ var iconHtml = require('./iconHtml.js');
5
6
  var NiceIconStacker = require('./NiceIconStacker.js');
6
7
  var iconStack = require('./iconStack.js');
7
8
  var NtdIconMorph = require('./NtdIconMorph.js');
@@ -20,10 +21,15 @@ exports.createHoverAnimation = animationControls.createHoverAnimation;
20
21
  exports.createVisibilityAnimation = animationControls.createVisibilityAnimation;
21
22
  exports.useAnimationSequence = animationControls.useAnimationSequence;
22
23
  exports.NiceIconPicker = NiceIconPicker.NiceIconPicker;
24
+ exports.NiceIconHtml = iconHtml.NiceIconHtml;
25
+ exports.parseIconHtml = iconHtml.parseIconHtml;
23
26
  exports.NiceIconStacker = NiceIconStacker.NiceIconStacker;
24
27
  exports.NiceIconStack = iconStack.NiceIconStack;
28
+ exports.addStackLayer = iconStack.addStackLayer;
29
+ exports.iconDisplayName = iconStack.iconDisplayName;
25
30
  exports.isValidTextBinding = iconStack.isValidTextBinding;
26
31
  exports.niceIconStackCode = iconStack.niceIconStackCode;
32
+ exports.removeStackLayer = iconStack.removeStackLayer;
27
33
  exports.renderIconStack = iconStack.renderIconStack;
28
34
  exports.NtdIconMorph = NtdIconMorph.NtdIconMorph;
29
35
  exports.NtdMorphPair = NtdIconMorph.NtdMorphPair;
@@ -115,7 +115,15 @@ function renderCutoutShape(shape, fill, maskId) {
115
115
  }
116
116
  }
117
117
  function createIcon(options) {
118
+ var _a, _b, _c, _d;
118
119
  const { displayName, viewBox = '0 0 24 24', paths, textCenter = BOX_CENTER } = options;
120
+ const solidDuotone = (props) => paths.solid({ ...props, accentColor: props.secondaryColor, accentOpacity: 1 });
121
+ const perfect = {
122
+ x: (_a = options.perfectStackX) !== null && _a !== void 0 ? _a : textCenter[0],
123
+ y: (_b = options.perfectStackY) !== null && _b !== void 0 ? _b : textCenter[1],
124
+ rotation: (_c = options.perfectStackRotation) !== null && _c !== void 0 ? _c : stacking.BOX_PERFECT_STACK.rotation,
125
+ size: (_d = options.perfectStackSize) !== null && _d !== void 0 ? _d : stacking.BOX_PERFECT_STACK.size,
126
+ };
119
127
  const Icon = React.forwardRef(({ size = 'md', variant = 'solid', cutoutShape = 'rounded-square', color, lightColor, darkColor, primaryColor: propPrimaryColor = 'currentColor', secondaryColor, accentColor, primaryOpacity = 1, secondaryOpacity = 0.4, accentOpacity = 1, strokeWidth = 1, strokeLinecap: propLinecap, strokeLinejoin: propLinejoin, animation, animationDuration, smilAnimation, smilDuration = 1, smilRepeatCount = 'indefinite', smilEasing = 'ease-in-out', smilDelay = 0, gradient, gradientType = 'linear', gradientAngle = 135, gradientAnimation,
120
128
  // Transform props
121
129
  rotate, flipX, flipY, scale, transition, transformOrigin = 'center center',
@@ -158,10 +166,13 @@ function createIcon(options) {
158
166
  };
159
167
  // Resolve renderer with fallback chain. `wireframe` has no authored layer
160
168
  // at all — it is a mechanical re-draw of the full `solid` construction, so
161
- // it never indexes `paths`.
169
+ // it never indexes `paths`. A duotone left out is the solid with its details
170
+ // in secondary (1.3.8): the body keeps primary, the larger area.
162
171
  const renderer = variant === 'wireframe'
163
172
  ? paths.solid
164
- : ((_g = (_f = (_e = (_d = (_c = (_b = paths[variant]) !== null && _b !== void 0 ? _b : (variant === 'sharp' ? paths.solid : undefined)) !== null && _c !== void 0 ? _c : (variant === 'rounded' ? paths.solid : undefined)) !== null && _d !== void 0 ? _d : (variant === 'mini' ? paths.solid : undefined)) !== null && _e !== void 0 ? _e : (variant === '3d' ? paths.solid : undefined)) !== null && _f !== void 0 ? _f : (variant === 'vivid' ? paths.solid : undefined)) !== null && _g !== void 0 ? _g : paths.solid);
173
+ : variant === 'duotone' && !paths.duotone
174
+ ? solidDuotone
175
+ : ((_g = (_f = (_e = (_d = (_c = (_b = paths[variant]) !== null && _b !== void 0 ? _b : (variant === 'sharp' ? paths.solid : undefined)) !== null && _c !== void 0 ? _c : (variant === 'rounded' ? paths.solid : undefined)) !== null && _d !== void 0 ? _d : (variant === 'mini' ? paths.solid : undefined)) !== null && _e !== void 0 ? _e : (variant === '3d' ? paths.solid : undefined)) !== null && _f !== void 0 ? _f : (variant === 'vivid' ? paths.solid : undefined)) !== null && _g !== void 0 ? _g : paths.solid);
165
176
  // 3D variant uses a filter for depth effect
166
177
  const is3d = variant === '3d';
167
178
  const filterId = React.useId();
@@ -236,7 +247,7 @@ function createIcon(options) {
236
247
  accentColor: resolvedAccent,
237
248
  tileColor: gradient ? effectivePrimary : resolvedSecondary,
238
249
  strokeWidth,
239
- textCenter,
250
+ perfect,
240
251
  textColor,
241
252
  textSize,
242
253
  });
@@ -254,8 +265,10 @@ function createIcon(options) {
254
265
  }) })] }) })), smilAnimation ? (needsSmilScaleWrapper ? (jsxRuntime.jsxs("g", { transform: `translate(${center.x}, ${center.y})`, filter: is3d ? `url(#${filterId})` : undefined, children: [jsxRuntime.jsx("g", { transform: `translate(-${center.x}, -${center.y})`, children: iconContent }), smilAnimationElements] })) : (jsxRuntime.jsxs("g", { filter: is3d ? `url(#${filterId})` : undefined, children: [iconContent, smilAnimationElements] }))) : is3d ? (jsxRuntime.jsx("g", { filter: `url(#${filterId})`, children: iconContent })) : (iconContent)] }));
255
266
  });
256
267
  Icon.displayName = displayName;
257
- // lets a parent icon tell this one among its children (stacking, 1.3.5)
268
+ // lets a parent icon tell this one among its children (stacking, 1.3.5), and tells the stacker
269
+ // where a child of this one goes by default (1.3.8)
258
270
  Icon[stacking.NTD_ICON_MARK] = true;
271
+ Icon[stacking.PERFECT_STACK] = perfect;
259
272
  return Icon;
260
273
  }
261
274
 
@@ -0,0 +1,122 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+ var iconStack = require('./iconStack.js');
5
+
6
+ /**
7
+ * An icon — or a stack of icons — kept as HTML (1.3.9): the `<svg>` NiceIconPicker draws, ready to
8
+ * store in a database and to show anywhere, even without React. What it was drawn from rides along
9
+ * in data attributes on the root, so the picker reads its own value back.
10
+ */
11
+ const ATTR_ICON = 'data-ntd-icon';
12
+ const ATTR_VARIANT = 'data-ntd-variant';
13
+ const ATTR_STACK = 'data-ntd-stack';
14
+ const ENTITIES = {
15
+ amp: '&',
16
+ quot: '"',
17
+ apos: "'",
18
+ '#39': "'",
19
+ lt: '<',
20
+ gt: '>',
21
+ };
22
+ /** The value of a data attribute of the root element (the first one in the markup). */
23
+ function readAttr(html, name) {
24
+ const hit = new RegExp(`\\s${name}="([^"]*)"`).exec(html);
25
+ return hit
26
+ ? hit[1].replace(/&(amp|quot|apos|#39|lt|gt);/g, (_, e) => ENTITIES[e])
27
+ : undefined;
28
+ }
29
+ function isLayer(value) {
30
+ const layer = value;
31
+ return (!!layer &&
32
+ typeof layer === 'object' &&
33
+ typeof layer.id === 'string' &&
34
+ typeof layer.icon === 'string');
35
+ }
36
+ /**
37
+ * Reads back what an icon's HTML was drawn from — the base icon and the stack. `null` for anything
38
+ * else: empty, plain text or HTML the picker did not write.
39
+ */
40
+ function parseIconHtml(html) {
41
+ if (!html) {
42
+ return null;
43
+ }
44
+ const icon = readAttr(html, ATTR_ICON);
45
+ if (!icon) {
46
+ return null;
47
+ }
48
+ const stack = readAttr(html, ATTR_STACK);
49
+ if (stack) {
50
+ try {
51
+ const parsed = JSON.parse(stack);
52
+ const layers = Array.isArray(parsed) ? parsed.filter(isLayer) : [];
53
+ if (layers.length > 0) {
54
+ return { icon: layers[0].icon, layers };
55
+ }
56
+ }
57
+ catch (_a) {
58
+ // a damaged stack still shows its base
59
+ }
60
+ }
61
+ const variant = readAttr(html, ATTR_VARIANT);
62
+ return { icon, layers: [{ id: '1', icon, ...(variant ? { variant } : {}) }] };
63
+ }
64
+ /** The data attributes that let the HTML of a stack be read back: the base, and the stack unless it is a plain icon. */
65
+ function iconHtmlAttributes(layers) {
66
+ const base = layers[0];
67
+ if (!base) {
68
+ return {};
69
+ }
70
+ const plain = layers.length === 1 &&
71
+ Object.entries(base).every(([key, value]) => value === undefined || key === 'id' || key === 'icon' || key === 'variant');
72
+ if (plain) {
73
+ return { [ATTR_ICON]: base.icon, ...(base.variant ? { [ATTR_VARIANT]: base.variant } : {}) };
74
+ }
75
+ return { [ATTR_ICON]: base.icon, [ATTR_STACK]: JSON.stringify(layers) };
76
+ }
77
+ /** djb2, base 36 — a short, stable name for a drawing. */
78
+ function hash(text) {
79
+ let h = 5381;
80
+ for (let i = 0; i < text.length; i++) {
81
+ h = ((h << 5) + h + text.charCodeAt(i)) | 0;
82
+ }
83
+ return (h >>> 0).toString(36);
84
+ }
85
+ /**
86
+ * Renames the ids inside an icon's markup (masks, gradients, clip paths) after the drawing itself.
87
+ * React's ids (`_r_3_`) are unique only within the page that drew them; pasted into another page they
88
+ * could hit an element of its own and borrow the wrong mask. Named after the drawing, two copies of
89
+ * one icon share ids that point at identical definitions.
90
+ */
91
+ function scopeIconHtmlIds(markup) {
92
+ const ids = [...new Set([...markup.matchAll(/\sid="([^"]+)"/g)].map((m) => m[1]))];
93
+ if (ids.length === 0) {
94
+ return markup;
95
+ }
96
+ const index = new Map(ids.map((id, i) => [id, i]));
97
+ const rename = (fn) => markup
98
+ .replace(/(\sid=")([^"]+)(")/g, (all, a, id, b) => index.has(id) ? a + fn(id) + b : all)
99
+ .replace(/url\(#([^)]+)\)/g, (all, id) => (index.has(id) ? `url(#${fn(id)})` : all))
100
+ .replace(/(href=")#([^"]+)(")/g, (all, a, id, b) => index.has(id) ? `${a}#${fn(id)}${b}` : all);
101
+ const name = hash(rename((id) => `#${index.get(id)}`));
102
+ return rename((id) => `ntd-${name}-${index.get(id)}`);
103
+ }
104
+ /**
105
+ * NiceIconHtml (1.3.9) — shows an icon kept as HTML in a React page by drawing it again from its
106
+ * data attributes; the markup itself is never injected, so HTML from a database cannot run anything.
107
+ * A plain export name (`'NtdStar'`) works too. The rest of the props go to the base icon.
108
+ */
109
+ function NiceIconHtml({ html, icons, values, fallback = null, ...baseProps }) {
110
+ var _a;
111
+ const source = (_a = parseIconHtml(html)) !== null && _a !== void 0 ? _a : (html && !html.includes('<') ? { layers: [{ id: '1', icon: html }] } : null);
112
+ const drawn = source ? iconStack.renderIconStack(source.layers, icons, { baseProps, values }) : null;
113
+ return jsxRuntime.jsx(jsxRuntime.Fragment, { children: drawn !== null && drawn !== void 0 ? drawn : fallback });
114
+ }
115
+ if (process.env.NODE_ENV !== 'production') {
116
+ NiceIconHtml.displayName = 'NiceIconHtml';
117
+ }
118
+
119
+ exports.NiceIconHtml = NiceIconHtml;
120
+ exports.iconHtmlAttributes = iconHtmlAttributes;
121
+ exports.parseIconHtml = parseIconHtml;
122
+ exports.scopeIconHtmlIds = scopeIconHtmlIds;
@@ -11,6 +11,19 @@ var createIcon = require('./createIcon.js');
11
11
  function detailColor({ variant, secondaryColor, accentColor }) {
12
12
  return variant === 'cutout' ? accentColor : secondaryColor;
13
13
  }
14
+ /**
15
+ * A duotone renderer with its two colours swapped, at full strength (1.3.8) — for a duotone that
16
+ * drew its larger area in secondary (a pale body under a primary outline): the body turns
17
+ * primary, the outline and the details secondary.
18
+ */
19
+ function swapDuotone(render) {
20
+ return (props) => render({
21
+ ...props,
22
+ primaryColor: props.secondaryColor,
23
+ secondaryColor: props.primaryColor,
24
+ secondaryOpacity: 1,
25
+ });
26
+ }
14
27
  /**
15
28
  * `content` drawn only inside `shape` — for accent lines that read as a cut through a shape (a
16
29
  * pencil's band, a calendar's header line, an envelope's flap): clipped, they meet the shape's
@@ -42,6 +55,10 @@ function bellIcon(displayName, ringing) {
42
55
  const parts = ({ primaryColor, strokeWidth }, detail, rings, ringsOpacity = 1) => (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("path", { d: BELL, fill: primaryColor }), jsxRuntime.jsx("path", { d: BELL_CLAPPER, fill: detail }), ringing && (jsxRuntime.jsx("path", { d: BELL_RINGS, stroke: rings, strokeWidth: strokeWidth, opacity: ringsOpacity }))] }));
43
56
  return createIcon.createIcon({
44
57
  displayName,
58
+ // stacked icons and text go into the bell's body, above the clapper (1.3.8)
59
+ perfectStackX: 12,
60
+ perfectStackY: 10.5,
61
+ perfectStackSize: 0.4,
45
62
  paths: {
46
63
  solid: (props) => parts(props, props.primaryColor, props.primaryColor),
47
64
  outline: ({ primaryColor, strokeWidth }) => (jsxRuntime.jsx("path", { d: `${BELL}${BELL_CLAPPER_LINE}${ringing ? BELL_RINGS : ''}`, stroke: primaryColor, strokeWidth: strokeWidth, fill: "none" })),
@@ -60,17 +77,21 @@ const PAPER_FOLD_SHARP = 'M12 2v8h8z';
60
77
  * A paper icon — NtdFile, and every document drawn on the same page (NtdFileText, NtdInvoice,
61
78
  * NtdContract, …), so their pages and folds match. The fold is secondary when the page is filled;
62
79
  * `text` is the document's writing, a path of strokes kept below the fold (y ≥ 11, x 7–17),
63
- * drawn in the accent colour on a filled page and in primary on a stroked one.
80
+ * drawn in the accent colour on a filled page (secondary in duotone) and in primary on a stroked one.
64
81
  */
65
82
  function paperIcon(displayName, text = '') {
66
83
  return createIcon.createIcon({
67
84
  displayName,
85
+ // stacked icons and text go onto the page below the fold (1.3.8), the same on every paper
86
+ perfectStackX: 12,
87
+ perfectStackY: 14,
88
+ perfectStackSize: 0.5,
68
89
  paths: {
69
90
  solid: ({ primaryColor, secondaryColor, accentColor, accentOpacity }) => (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("path", { d: PAPER, fill: primaryColor }), jsxRuntime.jsx("path", { d: PAPER_FOLD, fill: secondaryColor }), text && (jsxRuntime.jsx("path", { d: text, stroke: accentColor, strokeOpacity: accentOpacity, strokeWidth: "1.5", strokeLinecap: "round", fill: "none" }))] })),
70
91
  // square at every corner, the fold included — the derived `sharp` keeps a path's arcs round
71
92
  sharp: ({ primaryColor, secondaryColor, accentColor, accentOpacity }) => (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("path", { d: PAPER_SHARP, fill: primaryColor }), jsxRuntime.jsx("path", { d: PAPER_FOLD_SHARP, fill: secondaryColor }), text && (jsxRuntime.jsx("path", { d: text, stroke: accentColor, strokeOpacity: accentOpacity, strokeWidth: "1.5", fill: "none" }))] })),
72
93
  outline: ({ primaryColor, strokeWidth }) => (jsxRuntime.jsx("path", { d: `${PAPER}${PAPER_FOLD_LINE}${text}`, stroke: primaryColor, strokeWidth: strokeWidth, fill: "none" })),
73
- duotone: ({ primaryColor, secondaryColor, secondaryOpacity, strokeWidth }) => (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("path", { d: PAPER, fill: secondaryColor, opacity: secondaryOpacity }), jsxRuntime.jsx("path", { d: PAPER_FOLD_SHARP, fill: primaryColor, opacity: secondaryOpacity + 0.1 }), jsxRuntime.jsx("path", { d: `${PAPER}${PAPER_FOLD_LINE}${text}`, stroke: primaryColor, strokeWidth: strokeWidth, fill: "none" })] })),
94
+ // duotone (1.3.8): createIcon's default — the solid page, its writing in secondary like the fold
74
95
  vivid: ({ primaryColor, secondaryColor }) => (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("path", { d: PAPER, fill: primaryColor }), jsxRuntime.jsx("path", { d: PAPER, fill: "white", opacity: "0.1" }), jsxRuntime.jsx("path", { d: PAPER_FOLD, fill: secondaryColor }), text && (jsxRuntime.jsx("path", { d: text, stroke: "white", strokeOpacity: "0.9", strokeWidth: "1.5", strokeLinecap: "round", fill: "none" }))] })),
75
96
  },
76
97
  });
@@ -79,4 +100,5 @@ function paperIcon(displayName, text = '') {
79
100
  exports.bellIcon = bellIcon;
80
101
  exports.detailColor = detailColor;
81
102
  exports.paperIcon = paperIcon;
103
+ exports.swapDuotone = swapDuotone;
82
104
  exports.within = within;