not-a-spinner 0.1.1 → 0.2.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.
package/dist/index.cjs CHANGED
@@ -316,10 +316,15 @@ function useTextRotation({
316
316
  if (!initializedRef.current) {
317
317
  poolRef.current = shufflePhrases(inputPhrases);
318
318
  initializedRef.current = true;
319
- setCurrentIndex(0);
319
+ setCycleId((c) => c + 1);
320
320
  }
321
321
  }, []);
322
+ const inputChangedRef = (0, import_react.useRef)(false);
322
323
  (0, import_react.useEffect)(() => {
324
+ if (!inputChangedRef.current) {
325
+ inputChangedRef.current = true;
326
+ return;
327
+ }
323
328
  poolRef.current = shufflePhrases(inputPhrases);
324
329
  setCurrentIndex(0);
325
330
  }, [inputPhrases]);
@@ -470,16 +475,16 @@ var NotASpinner = React.forwardRef(
470
475
  }
471
476
  );
472
477
  }
473
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
478
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
474
479
  "div",
475
480
  {
476
481
  ref,
477
482
  className: cn(notASpinnerVariants({ size, animation }), className),
478
483
  ...props,
479
- children: [
480
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "nas-text-wrapper", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: getAnimationClass(), children: currentPhrase }) }),
484
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: getAnimationClass(), children: [
485
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: currentPhrase }),
481
486
  dots && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "nas-dots", children: "..." })
482
- ]
487
+ ] })
483
488
  }
484
489
  );
485
490
  }
@@ -487,49 +492,66 @@ var NotASpinner = React.forwardRef(
487
492
  NotASpinner.displayName = "NotASpinner";
488
493
  var TypewriterRenderer = React.forwardRef(({ className, currentPhrase, cycleId, interval, dots, onEraseComplete, ...props }, ref) => {
489
494
  const [displayedChars, setDisplayedChars] = React.useState(0);
495
+ const [displayedDots, setDisplayedDots] = React.useState(0);
490
496
  const [phase, setPhase] = React.useState(
491
497
  "typing"
492
498
  );
493
499
  const phraseRef = React.useRef(currentPhrase);
500
+ const eraseCalledRef = React.useRef(false);
494
501
  React.useEffect(() => {
495
502
  phraseRef.current = currentPhrase;
496
503
  setDisplayedChars(0);
504
+ setDisplayedDots(0);
497
505
  setPhase("typing");
506
+ eraseCalledRef.current = false;
498
507
  }, [cycleId]);
499
508
  React.useEffect(() => {
500
509
  const totalChars = phraseRef.current.length;
501
510
  if (totalChars === 0) return;
502
511
  const typingSpeed = 50;
503
512
  const erasingSpeed = 30;
513
+ const dotsTypingDuration = 3 * typingSpeed;
514
+ const dotsDismissDuration = 3 * erasingSpeed;
504
515
  const typingDuration = totalChars * typingSpeed;
505
516
  const erasingDuration = totalChars * erasingSpeed;
506
- const holdDuration = Math.max(interval - typingDuration - erasingDuration, 500);
517
+ const holdDuration = Math.max(interval - typingDuration - dotsTypingDuration - dotsDismissDuration - erasingDuration, 500);
507
518
  let timer;
508
519
  if (phase === "typing") {
509
520
  if (displayedChars < totalChars) {
510
521
  timer = setTimeout(() => setDisplayedChars((c) => c + 1), typingSpeed);
522
+ } else {
523
+ timer = setTimeout(() => setPhase("typing-dots"), 0);
524
+ }
525
+ } else if (phase === "typing-dots") {
526
+ if (displayedDots < 3) {
527
+ timer = setTimeout(() => setDisplayedDots((d) => d + 1), typingSpeed);
511
528
  } else {
512
529
  timer = setTimeout(() => setPhase("holding"), 0);
513
530
  }
514
531
  } else if (phase === "holding") {
515
- timer = setTimeout(() => setPhase("erasing"), holdDuration);
532
+ timer = setTimeout(() => setPhase("erasing-dots"), holdDuration);
533
+ } else if (phase === "erasing-dots") {
534
+ if (displayedDots > 0) {
535
+ timer = setTimeout(() => setDisplayedDots((d) => d - 1), erasingSpeed);
536
+ } else {
537
+ timer = setTimeout(() => setPhase("erasing"), 0);
538
+ }
516
539
  } else if (phase === "erasing") {
517
540
  if (displayedChars > 0) {
518
541
  timer = setTimeout(() => setDisplayedChars((c) => c - 1), erasingSpeed);
519
- } else {
542
+ } else if (!eraseCalledRef.current) {
543
+ eraseCalledRef.current = true;
520
544
  onEraseComplete();
521
545
  }
522
546
  }
523
547
  return () => clearTimeout(timer);
524
- }, [phase, displayedChars, interval, onEraseComplete]);
548
+ }, [phase, displayedChars, displayedDots, interval, onEraseComplete]);
525
549
  const displayedText = phraseRef.current.slice(0, displayedChars);
526
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { ref, className, ...props, children: [
527
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "nas-text-wrapper", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
528
- displayedText,
529
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "nas-cursor" })
530
- ] }) }),
531
- dots && displayedChars > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "nas-dots", children: "..." })
532
- ] });
550
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref, className, ...props, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "nas-text-wrapper", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
551
+ displayedText,
552
+ dots && displayedChars > 0 && displayedDots > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "nas-dots", children: "...".slice(0, displayedDots) }),
553
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "nas-cursor" })
554
+ ] }) }) });
533
555
  });
534
556
  TypewriterRenderer.displayName = "TypewriterRenderer";
535
557
 
package/dist/index.mjs CHANGED
@@ -272,10 +272,15 @@ function useTextRotation({
272
272
  if (!initializedRef.current) {
273
273
  poolRef.current = shufflePhrases(inputPhrases);
274
274
  initializedRef.current = true;
275
- setCurrentIndex(0);
275
+ setCycleId((c) => c + 1);
276
276
  }
277
277
  }, []);
278
+ const inputChangedRef = useRef(false);
278
279
  useEffect(() => {
280
+ if (!inputChangedRef.current) {
281
+ inputChangedRef.current = true;
282
+ return;
283
+ }
279
284
  poolRef.current = shufflePhrases(inputPhrases);
280
285
  setCurrentIndex(0);
281
286
  }, [inputPhrases]);
@@ -426,16 +431,16 @@ var NotASpinner = React.forwardRef(
426
431
  }
427
432
  );
428
433
  }
429
- return /* @__PURE__ */ jsxs(
434
+ return /* @__PURE__ */ jsx(
430
435
  "div",
431
436
  {
432
437
  ref,
433
438
  className: cn(notASpinnerVariants({ size, animation }), className),
434
439
  ...props,
435
- children: [
436
- /* @__PURE__ */ jsx("div", { className: "nas-text-wrapper", children: /* @__PURE__ */ jsx("span", { className: getAnimationClass(), children: currentPhrase }) }),
440
+ children: /* @__PURE__ */ jsxs("span", { className: getAnimationClass(), children: [
441
+ /* @__PURE__ */ jsx("span", { children: currentPhrase }),
437
442
  dots && /* @__PURE__ */ jsx("span", { className: "nas-dots", children: "..." })
438
- ]
443
+ ] })
439
444
  }
440
445
  );
441
446
  }
@@ -443,49 +448,66 @@ var NotASpinner = React.forwardRef(
443
448
  NotASpinner.displayName = "NotASpinner";
444
449
  var TypewriterRenderer = React.forwardRef(({ className, currentPhrase, cycleId, interval, dots, onEraseComplete, ...props }, ref) => {
445
450
  const [displayedChars, setDisplayedChars] = React.useState(0);
451
+ const [displayedDots, setDisplayedDots] = React.useState(0);
446
452
  const [phase, setPhase] = React.useState(
447
453
  "typing"
448
454
  );
449
455
  const phraseRef = React.useRef(currentPhrase);
456
+ const eraseCalledRef = React.useRef(false);
450
457
  React.useEffect(() => {
451
458
  phraseRef.current = currentPhrase;
452
459
  setDisplayedChars(0);
460
+ setDisplayedDots(0);
453
461
  setPhase("typing");
462
+ eraseCalledRef.current = false;
454
463
  }, [cycleId]);
455
464
  React.useEffect(() => {
456
465
  const totalChars = phraseRef.current.length;
457
466
  if (totalChars === 0) return;
458
467
  const typingSpeed = 50;
459
468
  const erasingSpeed = 30;
469
+ const dotsTypingDuration = 3 * typingSpeed;
470
+ const dotsDismissDuration = 3 * erasingSpeed;
460
471
  const typingDuration = totalChars * typingSpeed;
461
472
  const erasingDuration = totalChars * erasingSpeed;
462
- const holdDuration = Math.max(interval - typingDuration - erasingDuration, 500);
473
+ const holdDuration = Math.max(interval - typingDuration - dotsTypingDuration - dotsDismissDuration - erasingDuration, 500);
463
474
  let timer;
464
475
  if (phase === "typing") {
465
476
  if (displayedChars < totalChars) {
466
477
  timer = setTimeout(() => setDisplayedChars((c) => c + 1), typingSpeed);
478
+ } else {
479
+ timer = setTimeout(() => setPhase("typing-dots"), 0);
480
+ }
481
+ } else if (phase === "typing-dots") {
482
+ if (displayedDots < 3) {
483
+ timer = setTimeout(() => setDisplayedDots((d) => d + 1), typingSpeed);
467
484
  } else {
468
485
  timer = setTimeout(() => setPhase("holding"), 0);
469
486
  }
470
487
  } else if (phase === "holding") {
471
- timer = setTimeout(() => setPhase("erasing"), holdDuration);
488
+ timer = setTimeout(() => setPhase("erasing-dots"), holdDuration);
489
+ } else if (phase === "erasing-dots") {
490
+ if (displayedDots > 0) {
491
+ timer = setTimeout(() => setDisplayedDots((d) => d - 1), erasingSpeed);
492
+ } else {
493
+ timer = setTimeout(() => setPhase("erasing"), 0);
494
+ }
472
495
  } else if (phase === "erasing") {
473
496
  if (displayedChars > 0) {
474
497
  timer = setTimeout(() => setDisplayedChars((c) => c - 1), erasingSpeed);
475
- } else {
498
+ } else if (!eraseCalledRef.current) {
499
+ eraseCalledRef.current = true;
476
500
  onEraseComplete();
477
501
  }
478
502
  }
479
503
  return () => clearTimeout(timer);
480
- }, [phase, displayedChars, interval, onEraseComplete]);
504
+ }, [phase, displayedChars, displayedDots, interval, onEraseComplete]);
481
505
  const displayedText = phraseRef.current.slice(0, displayedChars);
482
- return /* @__PURE__ */ jsxs("div", { ref, className, ...props, children: [
483
- /* @__PURE__ */ jsx("div", { className: "nas-text-wrapper", children: /* @__PURE__ */ jsxs("span", { children: [
484
- displayedText,
485
- /* @__PURE__ */ jsx("span", { className: "nas-cursor" })
486
- ] }) }),
487
- dots && displayedChars > 0 && /* @__PURE__ */ jsx("span", { className: "nas-dots", children: "..." })
488
- ] });
506
+ return /* @__PURE__ */ jsx("div", { ref, className, ...props, children: /* @__PURE__ */ jsx("div", { className: "nas-text-wrapper", children: /* @__PURE__ */ jsxs("span", { children: [
507
+ displayedText,
508
+ dots && displayedChars > 0 && displayedDots > 0 && /* @__PURE__ */ jsx("span", { className: "nas-dots", children: "...".slice(0, displayedDots) }),
509
+ /* @__PURE__ */ jsx("span", { className: "nas-cursor" })
510
+ ] }) }) });
489
511
  });
490
512
  TypewriterRenderer.displayName = "TypewriterRenderer";
491
513
 
package/dist/styles.css CHANGED
@@ -79,12 +79,6 @@
79
79
  /* Dots */
80
80
  .nas-dots {
81
81
  margin-left: 2px;
82
- animation: nas-pulse 1.5s ease-in-out infinite;
83
- }
84
-
85
- @keyframes nas-pulse {
86
- 0%, 100% { opacity: 0.4; }
87
- 50% { opacity: 1; }
88
82
  }
89
83
 
90
84
  /* Reduced motion */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "not-a-spinner",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Because modern AI doesn't spin, it thinks. Replace loading spinners with AI-style rotating thinking phrases.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",