@nice2dev/icons 1.3.17 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const React = require("react");
4
+ const QUERY = "(prefers-reduced-motion: reduce)";
5
+ const hasMatchMedia = () => typeof window !== "undefined" && typeof window.matchMedia === "function";
6
+ function subscribe(callback) {
7
+ if (!hasMatchMedia()) {
8
+ return () => {
9
+ };
10
+ }
11
+ const mq = window.matchMedia(QUERY);
12
+ if (typeof mq.addEventListener === "function") {
13
+ mq.addEventListener("change", callback);
14
+ return () => mq.removeEventListener("change", callback);
15
+ }
16
+ mq.addListener?.(callback);
17
+ return () => mq.removeListener?.(callback);
18
+ }
19
+ function snapshot() {
20
+ return hasMatchMedia() ? window.matchMedia(QUERY).matches === true : false;
21
+ }
22
+ function usePrefersReducedMotion() {
23
+ return React.useSyncExternalStore(subscribe, snapshot, () => false);
24
+ }
25
+ function useStillMotion(mode) {
26
+ const reduce = usePrefersReducedMotion();
27
+ return reduce && mode !== "ignore";
28
+ }
29
+ exports.usePrefersReducedMotion = usePrefersReducedMotion;
30
+ exports.useStillMotion = useStillMotion;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const jsxRuntime = require("react/jsx-runtime");
4
4
  const animationSpeed = require("./animationSpeed.js");
5
+ const reducedMotion = require("./reducedMotion.js");
5
6
  const DEFAULT_CONFIG = {
6
7
  duration: 1,
7
8
  repeatCount: "indefinite",
@@ -322,9 +323,11 @@ function SmilAnimationWrapper({
322
323
  easing = "ease-in-out",
323
324
  delay = 0,
324
325
  center = { x: 12, y: 12 },
326
+ reducedMotion: reducedMotion$1,
325
327
  children
326
328
  }) {
327
- const animationElements = generateSmilAnimation(
329
+ const still = reducedMotion.useStillMotion(reducedMotion$1);
330
+ const generated = generateSmilAnimation(
328
331
  {
329
332
  type: animation,
330
333
  // 1 s before 1.3.17; a given duration keeps its length
@@ -335,6 +338,7 @@ function SmilAnimationWrapper({
335
338
  },
336
339
  center
337
340
  );
341
+ const animationElements = still ? animationSpeed.stillTree(generated) : generated;
338
342
  const needsOriginTransform = [
339
343
  "heartbeat",
340
344
  "scale-pulse",
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
3
  const React = require("react");
4
+ const reducedMotion = require("./reducedMotion.js");
4
5
  /**
5
6
  * @file utilities.ts
6
7
  * @description Utility hooks and functions for @nice2dev/icons
@@ -87,31 +88,8 @@ function useIcon(iconName, options = {}) {
87
88
  }, [iconName, cache, fallback]);
88
89
  return state;
89
90
  }
90
- function subscribeToReducedMotion(callback) {
91
- if (typeof window === "undefined") {
92
- return () => {
93
- };
94
- }
95
- const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
96
- const handler = () => callback();
97
- mediaQuery.addEventListener("change", handler);
98
- return () => mediaQuery.removeEventListener("change", handler);
99
- }
100
- function getReducedMotionSnapshot() {
101
- if (typeof window === "undefined") {
102
- return false;
103
- }
104
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
105
- }
106
- function getReducedMotionServerSnapshot() {
107
- return false;
108
- }
109
91
  function useReducedMotion() {
110
- return React.useSyncExternalStore(
111
- subscribeToReducedMotion,
112
- getReducedMotionSnapshot,
113
- getReducedMotionServerSnapshot
114
- );
92
+ return reducedMotion.usePrefersReducedMotion();
115
93
  }
116
94
  async function preloadIcons(iconNames) {
117
95
  const results = /* @__PURE__ */ new Map();
@@ -145,7 +123,8 @@ function withReducedMotionProps(props, prefersReducedMotion) {
145
123
  ...props,
146
124
  animation: void 0,
147
125
  animationDuration: void 0,
148
- gradientAnimation: void 0
126
+ gradientAnimation: void 0,
127
+ smilAnimation: void 0
149
128
  };
150
129
  }
151
130
  function createAccessibleIconProps(options) {
@@ -186,7 +186,7 @@ const AnimationController = /* @__PURE__ */ forwardRef(function AnimationControl
186
186
  const [singleState, setSingleState] = useState(
187
187
  autoPlay ? "playing" : "idle"
188
188
  );
189
- const singleIterationRef = useRef(0);
189
+ const [singleIteration, setSingleIteration] = useState(0);
190
190
  const sequenceConfig = isSequence ? animation : { steps: [{ animation, duration: 1e3 }] };
191
191
  const sequence = useAnimationSequence(sequenceConfig);
192
192
  const state = isSequence ? sequence.state : singleState;
@@ -245,7 +245,7 @@ const AnimationController = /* @__PURE__ */ forwardRef(function AnimationControl
245
245
  sequence.play();
246
246
  } else {
247
247
  setSingleState("playing");
248
- singleIterationRef.current++;
248
+ setSingleIteration((n) => n + 1);
249
249
  }
250
250
  onStart?.();
251
251
  },
@@ -262,7 +262,7 @@ const AnimationController = /* @__PURE__ */ forwardRef(function AnimationControl
262
262
  sequence.reset();
263
263
  } else {
264
264
  setSingleState("idle");
265
- singleIterationRef.current++;
265
+ setSingleIteration((n) => n + 1);
266
266
  }
267
267
  },
268
268
  goToStep: (index) => {
@@ -308,7 +308,7 @@ const AnimationController = /* @__PURE__ */ forwardRef(function AnimationControl
308
308
  ...children.props.style,
309
309
  ...animationStyles
310
310
  },
311
- key: isSequence ? `seq-${currentStepIndex}-${sequence.loopIteration}` : `single-${singleIterationRef.current}`
311
+ key: isSequence ? `seq-${currentStepIndex}-${sequence.loopIteration}` : `single-${singleIteration}`
312
312
  });
313
313
  return /* @__PURE__ */ jsx(
314
314
  "span",
@@ -333,6 +333,7 @@ const InteractiveIcon = /* @__PURE__ */ forwardRef(
333
333
  const [isActive, setIsActive] = useState(false);
334
334
  const [isLeaving, setIsLeaving] = useState(false);
335
335
  const [hasPlayed, setHasPlayed] = useState(false);
336
+ const [plays, setPlays] = useState(0);
336
337
  const wrapperRef = useRef(null);
337
338
  const timeoutRef = useRef(null);
338
339
  const setRefs = useCallback(
@@ -366,11 +367,13 @@ const InteractiveIcon = /* @__PURE__ */ forwardRef(
366
367
  timeoutRef.current = setTimeout(() => {
367
368
  setIsActive(true);
368
369
  setHasPlayed(true);
370
+ setPlays((n) => n + 1);
369
371
  onInteractionStart?.();
370
372
  }, config.delay);
371
373
  } else {
372
374
  setIsActive(true);
373
375
  setHasPlayed(true);
376
+ setPlays((n) => n + 1);
374
377
  onInteractionStart?.();
375
378
  }
376
379
  }, [disabled, config.once, config.delay, hasPlayed, clearTimer, onInteractionStart]);
@@ -423,7 +426,9 @@ const InteractiveIcon = /* @__PURE__ */ forwardRef(
423
426
  style: {
424
427
  ...children.props.style,
425
428
  ...animationStyles
426
- }
429
+ },
430
+ // a click plays the animation again: a new key restarts it
431
+ ...config.trigger === "click" ? { key: `play-${plays}` } : {}
427
432
  });
428
433
  return /* @__PURE__ */ jsx(
429
434
  "span",
@@ -61,9 +61,42 @@ function retimeTree(node, scale) {
61
61
  };
62
62
  return walk(node);
63
63
  }
64
+ function stillTree(node) {
65
+ const walk = (n) => {
66
+ if (Array.isArray(n)) {
67
+ return n.map(walk);
68
+ }
69
+ if (!React.isValidElement(n)) {
70
+ return n;
71
+ }
72
+ const props = n.props;
73
+ const isHost = typeof n.type === "string";
74
+ if (!isHost && n.type !== React.Fragment) {
75
+ return n;
76
+ }
77
+ const next = {};
78
+ if (isHost && SMIL_ELEMENTS.has(n.type)) {
79
+ next.begin = "0s";
80
+ next.dur = "0.001s";
81
+ next.repeatCount = "1";
82
+ next.repeatDur = void 0;
83
+ next.end = void 0;
84
+ }
85
+ if (props.children !== void 0) {
86
+ next.children = React.Children.map(props.children, walk);
87
+ }
88
+ return Object.keys(next).length > 0 ? React.cloneElement(n, next) : n;
89
+ };
90
+ return walk(node);
91
+ }
92
+ function motionTree(node, scale, still) {
93
+ return still ? stillTree(node) : retimeTree(node, scale);
94
+ }
64
95
  export {
65
96
  ANIMATION_SLOWDOWN,
66
97
  animationScale,
67
98
  durationScale,
68
- retimeTree
99
+ motionTree,
100
+ retimeTree,
101
+ stillTree
69
102
  };
@@ -1,6 +1,7 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
- import { forwardRef, useId, useSyncExternalStore } from "react";
3
- import { durationScale, retimeTree, animationScale } from "./animationSpeed.js";
2
+ import { forwardRef, useId, useState, useSyncExternalStore } from "react";
3
+ import { durationScale, stillTree, motionTree, animationScale } from "./animationSpeed.js";
4
+ import { useStillMotion } from "./reducedMotion.js";
4
5
  import { sharpenTree, roundTree, wireTree, MINI_SCALE } from "./sharpen.js";
5
6
  import { generateSmilAnimation } from "./smilAnimations.js";
6
7
  import { BOX_PERFECT_STACK, renderStackChildren, NTD_ICON_MARK, PERFECT_STACK } from "./stacking.js";
@@ -201,6 +202,10 @@ function createIcon(options) {
201
202
  animation,
202
203
  animationDuration,
203
204
  animationSpeed,
205
+ animationMode = "loop",
206
+ playKey,
207
+ replayOnClick = false,
208
+ reducedMotion = "auto",
204
209
  smilAnimation,
205
210
  smilDuration,
206
211
  smilRepeatCount = "indefinite",
@@ -235,6 +240,8 @@ function createIcon(options) {
235
240
  const gradId = useId();
236
241
  const uid = useId();
237
242
  const center = { x: 12, y: 12 };
243
+ const still = useStillMotion(reducedMotion);
244
+ const [replays, setReplays] = useState(0);
238
245
  const colorScheme = useColorScheme();
239
246
  const adaptiveColor = lightColor && darkColor ? colorScheme === "dark" ? darkColor : lightColor : void 0;
240
247
  const primaryColor = adaptiveColor ?? (color ? `var(${SEMANTIC_COLOR_VARS[color]}, ${SEMANTIC_COLOR_DEFAULTS[color]})` : propPrimaryColor);
@@ -262,7 +269,12 @@ function createIcon(options) {
262
269
  const filterId = useId();
263
270
  const isCutout = variant === "cutout";
264
271
  const maskId = useId();
265
- const classes = ["ntd-icon", animation ? `ntd-anim-${animation}` : "", className ?? ""].filter(Boolean).join(" ");
272
+ const classes = [
273
+ "ntd-icon",
274
+ animation && !still ? `ntd-anim-${animation}` : "",
275
+ reducedMotion === "ignore" ? "ntd-motion-ignore" : "",
276
+ className ?? ""
277
+ ].filter(Boolean).join(" ");
266
278
  const transforms = [];
267
279
  if (rotate) {
268
280
  transforms.push(`rotate(${rotate}deg)`);
@@ -284,12 +296,14 @@ function createIcon(options) {
284
296
  ...animationDuration ? { animationDuration, "--ntd-anim-duration": animationDuration } : {},
285
297
  // animations.css multiplies its times by it (default 2 there, the slowdown)
286
298
  ...animationSpeed !== void 0 ? { "--ntd-anim-scale": String(timeScale) } : {},
299
+ // a looping animation once (1.3.18): animations.css repeats `var(--ntd-anim-iterations, infinite)`
300
+ ...animationMode === "single" ? { "--ntd-anim-iterations": "1" } : {},
287
301
  ...transformString ? { transform: transformString } : {},
288
302
  ...transformString ? { transformOrigin } : {},
289
303
  ...transitionValue ? { transition: transitionValue } : {},
290
304
  ...style
291
305
  };
292
- const smilAnimationElements = smilAnimation ? generateSmilAnimation(
306
+ const smilGenerated = smilAnimation ? generateSmilAnimation(
293
307
  {
294
308
  type: smilAnimation,
295
309
  // 1 s before 1.3.17 by default; a given smilDuration keeps its length
@@ -300,9 +314,10 @@ function createIcon(options) {
300
314
  },
301
315
  center
302
316
  ) : null;
317
+ const smilAnimationElements = still ? stillTree(smilGenerated) : smilGenerated;
303
318
  const needsSmilScaleWrapper = smilAnimation && ["heartbeat", "scale-pulse", "grow", "shrink", "rubber", "pop"].includes(smilAnimation);
304
319
  const rendered = isCutout ? renderCutoutShape(cutoutShape, gradient ? effectivePrimary : resolvedSecondary, maskId) : variant === "sharp" ? sharpenTree(renderer(renderProps)) : variant === "rounded" ? roundTree(renderer(renderProps)) : variant === "wireframe" ? wireTree(renderer(renderProps), effectivePrimary) : renderer(renderProps);
305
- const paced = animated ? retimeTree(rendered, timeScale) : rendered;
320
+ const paced = animated ? motionTree(rendered, timeScale, still) : rendered;
306
321
  const stacked = renderStackChildren(children, {
307
322
  variant,
308
323
  primaryColor: effectivePrimary,
@@ -341,6 +356,10 @@ function createIcon(options) {
341
356
  "aria-hidden": title ? void 0 : true,
342
357
  "aria-label": title,
343
358
  ...rest,
359
+ onClick: replayOnClick ? (event) => {
360
+ setReplays((n) => n + 1);
361
+ rest.onClick?.(event);
362
+ } : rest.onClick,
344
363
  children: [
345
364
  title && /* @__PURE__ */ jsx("title", { children: title }),
346
365
  gradient && /* @__PURE__ */ jsx("defs", { children: renderGradientDef(
@@ -348,7 +367,7 @@ function createIcon(options) {
348
367
  gradient,
349
368
  gradientType,
350
369
  gradientAngle,
351
- gradientAnimation,
370
+ still ? void 0 : gradientAnimation,
352
371
  timeScale
353
372
  ) }),
354
373
  is3d && /* @__PURE__ */ jsx("defs", { children: render3dFilterDef(filterId) }),
@@ -365,7 +384,7 @@ function createIcon(options) {
365
384
  accentOpacity: 1,
366
385
  uid: `${uid}m`
367
386
  });
368
- return animated ? retimeTree(mask, timeScale) : mask;
387
+ return animated ? motionTree(mask, timeScale, still) : mask;
369
388
  })() })
370
389
  ] }) }),
371
390
  smilAnimation ? needsSmilScaleWrapper ? /* @__PURE__ */ jsxs(
@@ -383,7 +402,8 @@ function createIcon(options) {
383
402
  smilAnimationElements
384
403
  ] }) : is3d ? /* @__PURE__ */ jsx("g", { filter: `url(#${filterId})`, children: iconContent }) : iconContent
385
404
  ]
386
- }
405
+ },
406
+ `${playKey ?? ""}:${replays}`
387
407
  );
388
408
  }
389
409
  );