@nice2dev/icons 1.3.5 → 1.3.6

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @nice2dev/icons — Changelog
2
2
 
3
+ ## 1.3.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 4bdd01c: NiceIconStacker builds stacks nested any number of levels deep:
8
+
9
+ - **Nesting:** a layer sits in its `parent` (a new optional field). A layer without one sits in
10
+ the base, as in 1.3.5.
11
+ - **Where icons go:** "Add" puts the current icon into the selected layer and then selects it, so
12
+ adding again nests one level deeper (`<NtdBitcoin><NtdStar><NtdSparkle>1</NtdSparkle></NtdStar></NtdBitcoin>`).
13
+ Select an outer layer to put icons side by side.
14
+ - **Display:**
15
+ - layers are indented by depth;
16
+ - text is the innermost level, offered under the deepest icons;
17
+ - the colours flip at every level.
18
+ - **Removing:** removing a layer moves what sat in it up into its parent.
19
+ - **Handle:** a ref handle (`NiceIconStackerHandle`: `add`, `clear`) lets a parent run "add" from a
20
+ button of its own.
21
+ - **Translations:** new keys `iconStacker.addInto` and `iconStacker.target`.
22
+ - **Smaller text:** the default size of stacked text is three quarters of the 1.3.5 size (9, 7.5,
23
+ 6, 5 by length).
24
+ - **`stackRotation`:** turns a stacked icon about its own centre.
25
+ - **NiceIconStacker:** every layer can turn and take its own primary, secondary and accent colours
26
+ (with "auto" to follow its parent), and its text a size and colour of its own.
27
+ - **Bound text:**
28
+ - in the stacker, text can be bound to a value ("Bind to"): the code writes `{count}`, and the
29
+ text stays a sample;
30
+ - the new `NiceIconStack` draws a stack kept as data with live values (`values={{ count }}`).
31
+ - **`renderIconStack`** draws a stack as one icon tree.
32
+ - **`half-` positions:** `stackAlign` takes every position again with `half-` (`half-top`,
33
+ `half-top-right`, `half-bottom`, …). The icon sits halfway between the centre and that edge or
34
+ corner, inside the parent, so it is cut out like centred content (only full corners are
35
+ badges).
36
+
3
37
  ## 1.3.5
4
38
 
5
39
  ### Patch Changes
package/README.md CHANGED
@@ -220,19 +220,36 @@ centred on the icon:
220
220
  </NtdFolder>
221
221
 
222
222
  <NtdCircle>42</NtdCircle>
223
+
224
+ <NtdBitcoin>
225
+ <NtdStar>
226
+ <NtdSparkle>1</NtdSparkle>
227
+ </NtdStar>
228
+ </NtdBitcoin>
223
229
  ```
224
230
 
231
+ Stacks nest any number of levels deep, with the colours flipping at every level.
232
+
225
233
  - **`stackAlign`:** `center` (default), `top`, `bottom`, `left`, `right`, `top-left`,
226
- `top-right`, `bottom-left`, `bottom-right`.
234
+ `top-right`, `bottom-left`, `bottom-right`, plus each of them with `half-` (`half-top`,
235
+ `half-bottom-right`, …), halfway from the centre to that edge or corner.
227
236
  - **`stackSize`:** the child's share of its parent. Default `0.6` centred, `0.5` elsewhere.
228
237
  - **Colours** follow the parent unless the child sets its own:
229
238
  - In the centre or at an edge, a filled parent cuts the content out. The child takes the
230
239
  parent's accent and its text the parent's primary. On `outline` / `wireframe` everything stays
231
240
  in primary.
232
241
  - A corner badge keeps the parent's colours and sits on an accent-coloured gap.
233
- - **Text:** `textColor` and `textSize` override the text's colour and size.
242
+ - **`stackRotation`:** turns a stacked icon about its own centre, in degrees clockwise. Its
243
+ contents and text turn with it.
244
+ - **Text:** the default size is 9, 7.5, 6 or 5 in the 24-unit box (by length). `textColor` and
245
+ `textSize` override the text's colour and size.
246
+ - **Data:** `NiceIconStack` draws a stack kept as data (NiceIconStacker's layers), its bound text
247
+ filled from `values`. This works with live numbers, for example
248
+ `<NiceIconStack layers={stack} icons={icons} values={{ count }} />`.
234
249
  - **Builder:** `NiceIconStacker` builds a stack interactively and writes the JSX
235
- (`niceIconStackCode`).
250
+ (`niceIconStackCode`). Each icon you add goes into the selected layer, which is the newest one,
251
+ so adding nests deeper. Select an outer layer to put icons side by side. Text is the innermost
252
+ level.
236
253
 
237
254
  ---
238
255
 
@@ -4,6 +4,8 @@ var jsxRuntime = require('react/jsx-runtime');
4
4
  var uiCore = require('@nice2dev/ui-core');
5
5
  var React = require('react');
6
6
  var actions = require('./icons/actions.js');
7
+ var navigation = require('./icons/navigation.js');
8
+ var iconStack = require('./iconStack.js');
7
9
  var stacking = require('./stacking.js');
8
10
 
9
11
  const ALIGNS = [
@@ -16,46 +18,82 @@ const ALIGNS = [
16
18
  'top-right',
17
19
  'bottom-left',
18
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',
19
29
  ];
20
30
  const PREVIEW_SIZES = [96, 48, 24];
21
31
  const NO_LAYERS = [];
22
- /** The stack as JSX, the base wrapping its layers. */
23
- function niceIconStackCode(layers) {
24
- const [base, ...rest] = layers;
32
+ /** What a colour field shows while the colour is unset (follows the parent). */
33
+ const AUTO_SWATCH = '#808080';
34
+ /** The layers in tree order (each followed by what sits in it), with their depth. */
35
+ function walk(layers) {
36
+ const out = [];
37
+ const visit = (layer, depth) => {
38
+ out.push({ layer, depth });
39
+ for (const child of iconStack.stackChildrenOf(layers, layer.id)) {
40
+ visit(child, depth + 1);
41
+ }
42
+ };
43
+ if (layers[0]) {
44
+ visit(layers[0], 0);
45
+ }
46
+ return out;
47
+ }
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];
25
51
  if (!base) {
26
- return '';
52
+ return [];
27
53
  }
28
- const baseAttrs = base.variant ? ` variant="${base.variant}"` : '';
29
- if (rest.length === 0) {
30
- return base.text
31
- ? `<${base.icon}${baseAttrs}>${base.text}</${base.icon}>`
32
- : `<${base.icon}${baseAttrs} />`;
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));
33
60
  }
34
- const lines = rest.map((l) => {
35
- var _a;
36
- const attrs = [
37
- `stackAlign="${(_a = l.align) !== null && _a !== void 0 ? _a : 'center'}"`,
38
- l.size !== undefined ? `stackSize={${l.size}}` : '',
39
- l.variant && l.variant !== base.variant ? `variant="${l.variant}"` : '',
40
- ]
41
- .filter(Boolean)
42
- .join(' ');
43
- return l.text ? ` <${l.icon} ${attrs}>${l.text}</${l.icon}>` : ` <${l.icon} ${attrs} />`;
44
- });
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
+ };
45
72
  return [
46
- `<${base.icon}${baseAttrs}>`,
47
- ...(base.text ? [` ${base.text}`] : []),
48
- ...lines,
49
- `</${base.icon}>`,
50
- ].join('\n');
73
+ newBase,
74
+ ...layers
75
+ .filter((l) => l.id !== base.id && l.id !== heir.id)
76
+ .map((l) => siblings.includes(l)
77
+ ? { ...l, parent: heir.id }
78
+ : l.parent === heir.id
79
+ ? { ...l, parent: undefined }
80
+ : l),
81
+ ];
82
+ }
83
+ /** A colour of the layer's own, or "auto" — following its parent — until one is picked. */
84
+ function ColorField({ label, value, onChange, autoLabel, resetLabel }) {
85
+ 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 }))] }));
51
86
  }
52
87
  /**
53
88
  * NiceIconStacker (1.3.5) — builds an icon stack: the base icon and the icons stacked in it, each
54
- * with its position, size and text, shown live at three sizes and as the JSX that draws it. Icons
55
- * come in through `current` (the "add" button) and leave one by one or all at once.
89
+ * with its position, size, turn and colours, nested any number of levels deep (1.3.6) with text
90
+ * innermost — its size, colour and the value it is bound to — shown live at three sizes and as the
91
+ * JSX that draws it. The "add" button puts `current` into the selected layer and selects it, so
92
+ * adding again goes one level deeper; select an outer layer to add side by side. Layers leave one
93
+ * by one or all at once. `NiceIconStack` draws a stack built here from data.
56
94
  */
57
- function NiceIconStacker({ icons, layers: layersProp, defaultLayers, onLayersChange, current, previewProps, className, }) {
58
- var _a;
95
+ const NiceIconStacker = React.forwardRef(function NiceIconStacker({ icons, layers: layersProp, defaultLayers, onLayersChange, current, previewProps, className }, ref) {
96
+ var _a, _b, _c, _d;
59
97
  const { t } = uiCore.useNiceTranslation();
60
98
  const [layers, setLayers] = uiCore.useControllableState({
61
99
  value: layersProp,
@@ -65,25 +103,45 @@ function NiceIconStacker({ icons, layers: layersProp, defaultLayers, onLayersCha
65
103
  componentName: 'NiceIconStacker',
66
104
  });
67
105
  const nextId = React.useRef(0);
106
+ const targetGroup = React.useId();
68
107
  const [copied, setCopied] = React.useState(false);
108
+ // The layer the next icon goes into — the newest by default, so adding nests deeper.
109
+ const [selected, setSelected] = React.useState(undefined);
110
+ const base = layers[0];
111
+ 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;
69
112
  const resolve = React.useCallback((name) => (typeof icons === 'function' ? icons(name) : icons[name]), [icons]);
70
- const [base, ...rest] = layers;
71
- const code = React.useMemo(() => niceIconStackCode(layers), [layers]);
113
+ const tree = React.useMemo(() => walk(layers), [layers]);
114
+ const code = React.useMemo(() => iconStack.niceIconStackCode(layers), [layers]);
72
115
  const update = (id, patch) => setLayers((prev) => prev.map((l) => (l.id === id ? { ...l, ...patch } : l)));
73
116
  const add = () => {
74
117
  if (!current) {
75
118
  return;
76
119
  }
77
120
  nextId.current += 1;
78
- const layer = {
79
- id: `${current.icon}-${nextId.current}`,
80
- icon: current.icon,
81
- variant: current.variant,
82
- };
83
- setLayers((prev) => [...prev, layer]);
121
+ const id = `${current.icon}-${Date.now().toString(36)}-${nextId.current}`;
122
+ setLayers((prev) => {
123
+ const into = prev.find((l) => l.id === (target === null || target === void 0 ? void 0 : target.id));
124
+ const layer = {
125
+ id,
126
+ icon: current.icon,
127
+ variant: current.variant,
128
+ ...(into && into !== prev[0] ? { parent: into.id } : {}),
129
+ };
130
+ return [...prev, layer];
131
+ });
132
+ setSelected(id);
133
+ };
134
+ const remove = (id) => {
135
+ setLayers((prev) => removeLayer(prev, id));
136
+ if (selected === id) {
137
+ setSelected(undefined);
138
+ }
139
+ };
140
+ const clear = () => {
141
+ setLayers(NO_LAYERS);
142
+ setSelected(undefined);
84
143
  };
85
- const remove = (id) => setLayers((prev) => prev.filter((l) => l.id !== id));
86
- const clear = () => setLayers(NO_LAYERS);
144
+ React.useImperativeHandle(ref, () => ({ add, clear }));
87
145
  const copy = () => {
88
146
  var _a;
89
147
  void ((_a = navigator.clipboard) === null || _a === void 0 ? void 0 : _a.writeText(code).then(() => {
@@ -91,30 +149,36 @@ function NiceIconStacker({ icons, layers: layersProp, defaultLayers, onLayersCha
91
149
  window.setTimeout(() => setCopied(false), 1500);
92
150
  }));
93
151
  };
94
- const renderStack = (size) => {
95
- var _a;
96
- const Base = base ? resolve(base.icon) : undefined;
97
- if (!base || !Base) {
98
- return null;
99
- }
100
- return (jsxRuntime.jsxs(Base, { ...previewProps, variant: (_a = base.variant) !== null && _a !== void 0 ? _a : previewProps === null || previewProps === void 0 ? void 0 : previewProps.variant, size: size, title: base.icon, children: [base.text, rest.map((l) => {
101
- var _a;
102
- const Layer = resolve(l.icon);
103
- return Layer ? (jsxRuntime.jsx(Layer, { stackAlign: (_a = l.align) !== null && _a !== void 0 ? _a : 'center', stackSize: l.size, variant: l.variant !== base.variant ? l.variant : undefined, children: l.text }, l.id)) : null;
104
- })] }));
105
- };
106
- 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 }), ' ', t('iconStacker.add', 'Add {icon} to the stack').replace('{icon}', (_a = current === null || current === void 0 ? void 0 : current.icon) !== null && _a !== void 0 ? _a : '')] }), jsxRuntime.jsxs("button", { type: "button", onClick: clear, disabled: layers.length === 0, children: [jsxRuntime.jsx(actions.NtdTrash, { size: 14 }), " ", t('iconStacker.clear', 'Clear the stack')] })] }), layers.length === 0 ? (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: [renderStack(s), jsxRuntime.jsxs("small", { children: [s, "px"] })] }, s))) }), jsxRuntime.jsx("ol", { className: "nice-icon-stacker__layers", "aria-label": t('iconStacker.layers', 'Layers'), children: layers.map((l, i) => {
107
- var _a, _b, _c;
152
+ const autoLabel = t('iconStacker.auto', 'auto');
153
+ const resetLabel = t('iconStacker.reset', 'Back to auto');
154
+ const addLabel = base && target
155
+ ? t('iconStacker.addInto', 'Add {icon} into {target}')
156
+ .replace('{icon}', (_c = current === null || current === void 0 ? void 0 : current.icon) !== null && _c !== void 0 ? _c : '')
157
+ .replace('{target}', target.icon)
158
+ : 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 : '');
159
+ 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, {
160
+ baseProps: { ...previewProps, size: s, title: base.icon },
161
+ }), 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 }) => {
162
+ var _a, _b, _c, _d, _e, _f, _g, _h;
108
163
  const Icon = resolve(l.icon);
109
- const isBase = i === 0;
164
+ const isBase = l === base;
110
165
  const align = (_a = l.align) !== null && _a !== void 0 ? _a : 'center';
111
166
  const share = (_b = l.size) !== null && _b !== void 0 ? _b : stacking.defaultStackSize(align);
112
- return (jsxRuntime.jsxs("li", { className: "nice-icon-stacker__layer", children: [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.jsxs("label", { className: "nice-icon-stacker__field", children: [jsxRuntime.jsx("span", { children: t('iconStacker.text', 'Text') }), jsxRuntime.jsx("input", { type: "text", value: (_c = l.text) !== null && _c !== void 0 ? _c : '', maxLength: 4, onChange: (e) => update(l.id, { text: e.target.value || undefined }) })] }), jsxRuntime.jsx("button", { type: "button", className: "nice-icon-stacker__remove", onClick: () => remove(l.id), "aria-label": t('iconStacker.remove', 'Remove {icon}').replace('{icon}', l.icon), title: t('iconStacker.remove', 'Remove {icon}').replace('{icon}', l.icon), children: jsxRuntime.jsx(actions.NtdTrash, { size: 14 }) })] }, l.id));
113
- }) }), 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 ? t('iconStacker.copied', 'Copied') : t('iconStacker.copy', 'Copy the code')] })] })] }))] }));
114
- }
167
+ const innermost = iconStack.stackChildrenOf(layers, l.id).length === 0;
168
+ const textSize = (_c = l.textSize) !== null && _c !== void 0 ? _c : stacking.defaultTextSize(((_d = l.text) !== null && _d !== void 0 ? _d : '').length || 1);
169
+ const bindingInvalid = !!l.textBinding && !iconStack.isValidTextBinding(l.textBinding);
170
+ const depthStyle = {
171
+ ['--nice-icon-stacker-depth']: String(depth),
172
+ };
173
+ const label = (key, fallback) => t(key, fallback).replace('{icon}', l.icon);
174
+ 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("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.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: (_g = l.text) !== null && _g !== void 0 ? _g : '', 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: (_h = l.textBinding) !== null && _h !== void 0 ? _h : '', "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));
175
+ }) }), 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
176
+ ? t('iconStacker.copied', 'Copied')
177
+ : t('iconStacker.copy', 'Copy the code')] })] })] }))] }));
178
+ });
115
179
  if (process.env.NODE_ENV !== 'production') {
116
180
  NiceIconStacker.displayName = 'NiceIconStacker';
117
181
  }
118
182
 
183
+ exports.niceIconStackCode = iconStack.niceIconStackCode;
119
184
  exports.NiceIconStacker = NiceIconStacker;
120
- exports.niceIconStackCode = niceIconStackCode;
@@ -3,6 +3,7 @@
3
3
  var animationControls = require('./animationControls.js');
4
4
  var NiceIconPicker = require('./NiceIconPicker.js');
5
5
  var NiceIconStacker = require('./NiceIconStacker.js');
6
+ var iconStack = require('./iconStack.js');
6
7
  var NtdIconMorph = require('./NtdIconMorph.js');
7
8
  var NtdIconBadge = require('./NtdIconBadge.js');
8
9
  var smilAnimations = require('./smilAnimations.js');
@@ -20,7 +21,10 @@ exports.createVisibilityAnimation = animationControls.createVisibilityAnimation;
20
21
  exports.useAnimationSequence = animationControls.useAnimationSequence;
21
22
  exports.NiceIconPicker = NiceIconPicker.NiceIconPicker;
22
23
  exports.NiceIconStacker = NiceIconStacker.NiceIconStacker;
23
- exports.niceIconStackCode = NiceIconStacker.niceIconStackCode;
24
+ exports.NiceIconStack = iconStack.NiceIconStack;
25
+ exports.isValidTextBinding = iconStack.isValidTextBinding;
26
+ exports.niceIconStackCode = iconStack.niceIconStackCode;
27
+ exports.renderIconStack = iconStack.renderIconStack;
24
28
  exports.NtdIconMorph = NtdIconMorph.NtdIconMorph;
25
29
  exports.NtdMorphPair = NtdIconMorph.NtdMorphPair;
26
30
  exports.NtdPathMorph = NtdIconMorph.NtdPathMorph;
@@ -120,7 +120,7 @@ function createIcon(options) {
120
120
  // Transform props
121
121
  rotate, flipX, flipY, scale, transition, transformOrigin = 'center center',
122
122
  // stacking (1.3.5): the parent icon reads a child's own align / size from its props
123
- children, stackAlign: _stackAlign, stackSize: _stackSize, textColor, textSize, title, className, style, ...rest }, ref) => {
123
+ children, stackAlign: _stackAlign, stackSize: _stackSize, stackRotation: _stackRotation, textColor, textSize, title, className, style, ...rest }, ref) => {
124
124
  var _a, _b, _c, _d, _e, _f, _g;
125
125
  const resolvedSize = typeof size === 'number' ? size : ((_a = types.SIZE_MAP[size]) !== null && _a !== void 0 ? _a : 24);
126
126
  const gradId = React.useId();
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+ var React = require('react');
5
+
6
+ /** A name `textBinding` can take: a JavaScript identifier. */
7
+ function isValidTextBinding(name) {
8
+ return !!name && /^[A-Za-z_$][\w$]*$/.test(name);
9
+ }
10
+ /** The id of the layer `layer` sits in, `undefined` for the base. */
11
+ function stackParentId(layers, layer) {
12
+ var _a, _b;
13
+ return layer === layers[0] ? undefined : ((_a = layer.parent) !== null && _a !== void 0 ? _a : (_b = layers[0]) === null || _b === void 0 ? void 0 : _b.id);
14
+ }
15
+ /** The layers that sit directly in layer `id`, in order. */
16
+ function stackChildrenOf(layers, id) {
17
+ return layers.filter((l) => l !== layers[0] && stackParentId(layers, l) === id);
18
+ }
19
+ const COLOR_PROPS = ['primaryColor', 'secondaryColor', 'accentColor', 'textColor'];
20
+ /** The stack as JSX: every layer wrapping the layers that sit in it, text innermost. */
21
+ function niceIconStackCode(layers) {
22
+ const base = layers[0];
23
+ if (!base) {
24
+ return '';
25
+ }
26
+ const bound = layers.filter((l) => isValidTextBinding(l.textBinding));
27
+ const write = (layer, parent, indent) => {
28
+ var _a;
29
+ const attrs = [
30
+ parent ? `stackAlign="${(_a = layer.align) !== null && _a !== void 0 ? _a : 'center'}"` : '',
31
+ parent && layer.size !== undefined ? `stackSize={${layer.size}}` : '',
32
+ parent && layer.rotation ? `stackRotation={${layer.rotation}}` : '',
33
+ layer.variant && layer.variant !== (parent === null || parent === void 0 ? void 0 : parent.variant) ? `variant="${layer.variant}"` : '',
34
+ ...COLOR_PROPS.map((p) => (layer[p] ? `${p}="${layer[p]}"` : '')),
35
+ layer.textSize !== undefined ? `textSize={${layer.textSize}}` : '',
36
+ ]
37
+ .filter(Boolean)
38
+ .join(' ');
39
+ const open = `${layer.icon}${attrs ? ` ${attrs}` : ''}`;
40
+ const text = isValidTextBinding(layer.textBinding) ? `{${layer.textBinding}}` : layer.text;
41
+ const inner = stackChildrenOf(layers, layer.id);
42
+ if (inner.length === 0) {
43
+ return text ? `${indent}<${open}>${text}</${layer.icon}>` : `${indent}<${open} />`;
44
+ }
45
+ return [
46
+ `${indent}<${open}>`,
47
+ ...(text ? [`${indent} ${text}`] : []),
48
+ ...inner.map((child) => write(child, layer, `${indent} `)),
49
+ `${indent}</${layer.icon}>`,
50
+ ].join('\n');
51
+ };
52
+ const notes = bound.map((l) => `// {${l.textBinding}}: bound text${l.text ? `, e.g. "${l.text}"` : ''}`);
53
+ return [...notes, write(base, undefined, '')].join('\n');
54
+ }
55
+ /**
56
+ * The stack drawn as icons — the base wrapping the layers that sit in it, text innermost (1.3.6).
57
+ * One SVG, as nesting the icons by hand would give.
58
+ */
59
+ function renderIconStack(layers, icons, { baseProps, values } = {}) {
60
+ const resolve = (name) => (typeof icons === 'function' ? icons(name) : icons[name]);
61
+ const draw = (layer, parent) => {
62
+ var _a, _b;
63
+ const Icon = resolve(layer.icon);
64
+ if (!Icon) {
65
+ return null;
66
+ }
67
+ const bound = isValidTextBinding(layer.textBinding) ? values === null || values === void 0 ? void 0 : values[layer.textBinding] : undefined;
68
+ const text = bound !== null && bound !== void 0 ? bound : layer.text;
69
+ const own = {
70
+ primaryColor: layer.primaryColor,
71
+ secondaryColor: layer.secondaryColor,
72
+ accentColor: layer.accentColor,
73
+ textColor: layer.textColor,
74
+ textSize: layer.textSize,
75
+ };
76
+ const inner = stackChildrenOf(layers, layer.id).map((child) => draw(child, layer));
77
+ if (!parent) {
78
+ const props = { ...baseProps };
79
+ for (const [k, v] of Object.entries(own)) {
80
+ if (v !== undefined) {
81
+ props[k] = v;
82
+ }
83
+ }
84
+ return (React.createElement(Icon, { ...props, key: layer.id, variant: (_a = layer.variant) !== null && _a !== void 0 ? _a : baseProps === null || baseProps === void 0 ? void 0 : baseProps.variant },
85
+ inner,
86
+ text));
87
+ }
88
+ return (React.createElement(Icon, { ...own, key: layer.id, stackAlign: (_b = layer.align) !== null && _b !== void 0 ? _b : 'center', stackSize: layer.size, stackRotation: layer.rotation, variant: layer.variant !== parent.variant ? layer.variant : undefined },
89
+ inner,
90
+ text));
91
+ };
92
+ return layers[0] ? draw(layers[0], undefined) : null;
93
+ }
94
+ /**
95
+ * NiceIconStack (1.3.6) — draws a stack kept as data (NiceIconStacker's layers) as one icon, its
96
+ * bound text filled from `values`: `<NiceIconStack layers={stack} icons={icons} values={{ count }} />`.
97
+ * The rest of the props go to the base icon.
98
+ */
99
+ function NiceIconStack({ layers, icons, values, ...baseProps }) {
100
+ return jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderIconStack(layers, icons, { baseProps, values }) });
101
+ }
102
+ if (process.env.NODE_ENV !== 'production') {
103
+ NiceIconStack.displayName = 'NiceIconStack';
104
+ }
105
+
106
+ exports.NiceIconStack = NiceIconStack;
107
+ exports.isValidTextBinding = isValidTextBinding;
108
+ exports.niceIconStackCode = niceIconStackCode;
109
+ exports.renderIconStack = renderIconStack;
110
+ exports.stackChildrenOf = stackChildrenOf;
111
+ exports.stackParentId = stackParentId;
package/dist/cjs/index.js CHANGED
@@ -5,6 +5,7 @@ var createIcon = require('./createIcon.js');
5
5
  var smilAnimations = require('./smilAnimations.js');
6
6
  var NiceIconPicker = require('./NiceIconPicker.js');
7
7
  var NiceIconStacker = require('./NiceIconStacker.js');
8
+ var iconStack = require('./iconStack.js');
8
9
  var animationControls = require('./animationControls.js');
9
10
  var figmaExport = require('./figmaExport.js');
10
11
  var navigation = require('./icons/navigation.js');
@@ -70,7 +71,10 @@ exports.generateSmilAnimation = smilAnimations.generateSmilAnimation;
70
71
  exports.isSmilSupported = smilAnimations.isSmilSupported;
71
72
  exports.NiceIconPicker = NiceIconPicker.NiceIconPicker;
72
73
  exports.NiceIconStacker = NiceIconStacker.NiceIconStacker;
73
- exports.niceIconStackCode = NiceIconStacker.niceIconStackCode;
74
+ exports.NiceIconStack = iconStack.NiceIconStack;
75
+ exports.isValidTextBinding = iconStack.isValidTextBinding;
76
+ exports.niceIconStackCode = iconStack.niceIconStackCode;
77
+ exports.renderIconStack = iconStack.renderIconStack;
74
78
  exports.AnimationController = animationControls.AnimationController;
75
79
  exports.InteractiveIcon = animationControls.InteractiveIcon;
76
80
  exports.NiceAnimationController = animationControls.AnimationController;