not-a-spinner 0.1.0 → 0.1.1

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
@@ -301,9 +301,11 @@ function getPhrasesForConfig(messages, locale) {
301
301
  function useTextRotation({
302
302
  phrases: inputPhrases,
303
303
  interval = 3e3,
304
- animationDuration = 400
304
+ animationDuration = 400,
305
+ manual = false
305
306
  }) {
306
307
  const [currentIndex, setCurrentIndex] = (0, import_react.useState)(0);
308
+ const [cycleId, setCycleId] = (0, import_react.useState)(0);
307
309
  const [isExiting, setIsExiting] = (0, import_react.useState)(false);
308
310
  const [isEntering, setIsEntering] = (0, import_react.useState)(false);
309
311
  const poolRef = (0, import_react.useRef)(inputPhrases);
@@ -330,8 +332,20 @@ function useTextRotation({
330
332
  poolRef.current = combined;
331
333
  }
332
334
  }, []);
335
+ const advancePhrase = (0, import_react.useCallback)(() => {
336
+ setCurrentIndex((prev) => {
337
+ const next = prev + 1;
338
+ if (next >= poolRef.current.length) {
339
+ poolRef.current = shufflePhrases(poolRef.current);
340
+ return 0;
341
+ }
342
+ return next;
343
+ });
344
+ setCycleId((c) => c + 1);
345
+ }, []);
333
346
  (0, import_react.useEffect)(() => {
334
347
  mountedRef.current = true;
348
+ if (manual) return;
335
349
  if (poolRef.current.length <= 1) return;
336
350
  timerRef.current = setInterval(() => {
337
351
  if (!mountedRef.current) return;
@@ -361,14 +375,16 @@ function useTextRotation({
361
375
  timerRef.current = null;
362
376
  }
363
377
  };
364
- }, [interval, animationDuration]);
378
+ }, [interval, animationDuration, manual]);
365
379
  const pool = poolRef.current;
366
380
  const phrase = pool.length > 0 ? pool[currentIndex % pool.length] : "";
367
381
  return {
368
382
  currentPhrase: phrase,
369
383
  isExiting,
370
384
  isEntering,
371
- appendPhrases
385
+ appendPhrases,
386
+ advancePhrase,
387
+ cycleId
372
388
  };
373
389
  }
374
390
 
@@ -409,9 +425,10 @@ var NotASpinner = React.forwardRef(
409
425
  () => getPhrasesForConfig(messages, locale),
410
426
  [messages, locale]
411
427
  );
412
- const { currentPhrase, isExiting, isEntering, appendPhrases } = useTextRotation({
428
+ const { currentPhrase, isExiting, isEntering, appendPhrases, advancePhrase, cycleId } = useTextRotation({
413
429
  phrases: resolvedPhrases,
414
- interval
430
+ interval,
431
+ manual: animation === "typewriter"
415
432
  });
416
433
  React.useEffect(() => {
417
434
  if (!fetchPhrase) return;
@@ -445,8 +462,10 @@ var NotASpinner = React.forwardRef(
445
462
  ref,
446
463
  className: cn(notASpinnerVariants({ size, animation }), className),
447
464
  currentPhrase,
465
+ cycleId,
448
466
  interval,
449
467
  dots,
468
+ onEraseComplete: advancePhrase,
450
469
  ...props
451
470
  }
452
471
  );
@@ -466,7 +485,7 @@ var NotASpinner = React.forwardRef(
466
485
  }
467
486
  );
468
487
  NotASpinner.displayName = "NotASpinner";
469
- var TypewriterRenderer = React.forwardRef(({ className, currentPhrase, interval, dots, ...props }, ref) => {
488
+ var TypewriterRenderer = React.forwardRef(({ className, currentPhrase, cycleId, interval, dots, onEraseComplete, ...props }, ref) => {
470
489
  const [displayedChars, setDisplayedChars] = React.useState(0);
471
490
  const [phase, setPhase] = React.useState(
472
491
  "typing"
@@ -476,7 +495,7 @@ var TypewriterRenderer = React.forwardRef(({ className, currentPhrase, interval,
476
495
  phraseRef.current = currentPhrase;
477
496
  setDisplayedChars(0);
478
497
  setPhase("typing");
479
- }, [currentPhrase]);
498
+ }, [cycleId]);
480
499
  React.useEffect(() => {
481
500
  const totalChars = phraseRef.current.length;
482
501
  if (totalChars === 0) return;
@@ -497,10 +516,12 @@ var TypewriterRenderer = React.forwardRef(({ className, currentPhrase, interval,
497
516
  } else if (phase === "erasing") {
498
517
  if (displayedChars > 0) {
499
518
  timer = setTimeout(() => setDisplayedChars((c) => c - 1), erasingSpeed);
519
+ } else {
520
+ onEraseComplete();
500
521
  }
501
522
  }
502
523
  return () => clearTimeout(timer);
503
- }, [phase, displayedChars, interval]);
524
+ }, [phase, displayedChars, interval, onEraseComplete]);
504
525
  const displayedText = phraseRef.current.slice(0, displayedChars);
505
526
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { ref, className, ...props, children: [
506
527
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "nas-text-wrapper", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
package/dist/index.d.cts CHANGED
@@ -24,14 +24,17 @@ interface UseTextRotationOptions {
24
24
  phrases: string[];
25
25
  interval?: number;
26
26
  animationDuration?: number;
27
+ manual?: boolean;
27
28
  }
28
29
  interface UseTextRotationReturn {
29
30
  currentPhrase: string;
30
31
  isExiting: boolean;
31
32
  isEntering: boolean;
32
33
  appendPhrases: (newPhrases: string[]) => void;
34
+ advancePhrase: () => void;
35
+ cycleId: number;
33
36
  }
34
- declare function useTextRotation({ phrases: inputPhrases, interval, animationDuration, }: UseTextRotationOptions): UseTextRotationReturn;
37
+ declare function useTextRotation({ phrases: inputPhrases, interval, animationDuration, manual, }: UseTextRotationOptions): UseTextRotationReturn;
35
38
 
36
39
  declare function createAnthropicFetcher(endpoint: string, locale?: string): () => Promise<string>;
37
40
  declare function createOpenAIFetcher(endpoint: string, locale?: string): () => Promise<string>;
package/dist/index.d.ts CHANGED
@@ -24,14 +24,17 @@ interface UseTextRotationOptions {
24
24
  phrases: string[];
25
25
  interval?: number;
26
26
  animationDuration?: number;
27
+ manual?: boolean;
27
28
  }
28
29
  interface UseTextRotationReturn {
29
30
  currentPhrase: string;
30
31
  isExiting: boolean;
31
32
  isEntering: boolean;
32
33
  appendPhrases: (newPhrases: string[]) => void;
34
+ advancePhrase: () => void;
35
+ cycleId: number;
33
36
  }
34
- declare function useTextRotation({ phrases: inputPhrases, interval, animationDuration, }: UseTextRotationOptions): UseTextRotationReturn;
37
+ declare function useTextRotation({ phrases: inputPhrases, interval, animationDuration, manual, }: UseTextRotationOptions): UseTextRotationReturn;
35
38
 
36
39
  declare function createAnthropicFetcher(endpoint: string, locale?: string): () => Promise<string>;
37
40
  declare function createOpenAIFetcher(endpoint: string, locale?: string): () => Promise<string>;
package/dist/index.mjs CHANGED
@@ -257,9 +257,11 @@ function getPhrasesForConfig(messages, locale) {
257
257
  function useTextRotation({
258
258
  phrases: inputPhrases,
259
259
  interval = 3e3,
260
- animationDuration = 400
260
+ animationDuration = 400,
261
+ manual = false
261
262
  }) {
262
263
  const [currentIndex, setCurrentIndex] = useState(0);
264
+ const [cycleId, setCycleId] = useState(0);
263
265
  const [isExiting, setIsExiting] = useState(false);
264
266
  const [isEntering, setIsEntering] = useState(false);
265
267
  const poolRef = useRef(inputPhrases);
@@ -286,8 +288,20 @@ function useTextRotation({
286
288
  poolRef.current = combined;
287
289
  }
288
290
  }, []);
291
+ const advancePhrase = useCallback(() => {
292
+ setCurrentIndex((prev) => {
293
+ const next = prev + 1;
294
+ if (next >= poolRef.current.length) {
295
+ poolRef.current = shufflePhrases(poolRef.current);
296
+ return 0;
297
+ }
298
+ return next;
299
+ });
300
+ setCycleId((c) => c + 1);
301
+ }, []);
289
302
  useEffect(() => {
290
303
  mountedRef.current = true;
304
+ if (manual) return;
291
305
  if (poolRef.current.length <= 1) return;
292
306
  timerRef.current = setInterval(() => {
293
307
  if (!mountedRef.current) return;
@@ -317,14 +331,16 @@ function useTextRotation({
317
331
  timerRef.current = null;
318
332
  }
319
333
  };
320
- }, [interval, animationDuration]);
334
+ }, [interval, animationDuration, manual]);
321
335
  const pool = poolRef.current;
322
336
  const phrase = pool.length > 0 ? pool[currentIndex % pool.length] : "";
323
337
  return {
324
338
  currentPhrase: phrase,
325
339
  isExiting,
326
340
  isEntering,
327
- appendPhrases
341
+ appendPhrases,
342
+ advancePhrase,
343
+ cycleId
328
344
  };
329
345
  }
330
346
 
@@ -365,9 +381,10 @@ var NotASpinner = React.forwardRef(
365
381
  () => getPhrasesForConfig(messages, locale),
366
382
  [messages, locale]
367
383
  );
368
- const { currentPhrase, isExiting, isEntering, appendPhrases } = useTextRotation({
384
+ const { currentPhrase, isExiting, isEntering, appendPhrases, advancePhrase, cycleId } = useTextRotation({
369
385
  phrases: resolvedPhrases,
370
- interval
386
+ interval,
387
+ manual: animation === "typewriter"
371
388
  });
372
389
  React.useEffect(() => {
373
390
  if (!fetchPhrase) return;
@@ -401,8 +418,10 @@ var NotASpinner = React.forwardRef(
401
418
  ref,
402
419
  className: cn(notASpinnerVariants({ size, animation }), className),
403
420
  currentPhrase,
421
+ cycleId,
404
422
  interval,
405
423
  dots,
424
+ onEraseComplete: advancePhrase,
406
425
  ...props
407
426
  }
408
427
  );
@@ -422,7 +441,7 @@ var NotASpinner = React.forwardRef(
422
441
  }
423
442
  );
424
443
  NotASpinner.displayName = "NotASpinner";
425
- var TypewriterRenderer = React.forwardRef(({ className, currentPhrase, interval, dots, ...props }, ref) => {
444
+ var TypewriterRenderer = React.forwardRef(({ className, currentPhrase, cycleId, interval, dots, onEraseComplete, ...props }, ref) => {
426
445
  const [displayedChars, setDisplayedChars] = React.useState(0);
427
446
  const [phase, setPhase] = React.useState(
428
447
  "typing"
@@ -432,7 +451,7 @@ var TypewriterRenderer = React.forwardRef(({ className, currentPhrase, interval,
432
451
  phraseRef.current = currentPhrase;
433
452
  setDisplayedChars(0);
434
453
  setPhase("typing");
435
- }, [currentPhrase]);
454
+ }, [cycleId]);
436
455
  React.useEffect(() => {
437
456
  const totalChars = phraseRef.current.length;
438
457
  if (totalChars === 0) return;
@@ -453,10 +472,12 @@ var TypewriterRenderer = React.forwardRef(({ className, currentPhrase, interval,
453
472
  } else if (phase === "erasing") {
454
473
  if (displayedChars > 0) {
455
474
  timer = setTimeout(() => setDisplayedChars((c) => c - 1), erasingSpeed);
475
+ } else {
476
+ onEraseComplete();
456
477
  }
457
478
  }
458
479
  return () => clearTimeout(timer);
459
- }, [phase, displayedChars, interval]);
480
+ }, [phase, displayedChars, interval, onEraseComplete]);
460
481
  const displayedText = phraseRef.current.slice(0, displayedChars);
461
482
  return /* @__PURE__ */ jsxs("div", { ref, className, ...props, children: [
462
483
  /* @__PURE__ */ jsx("div", { className: "nas-text-wrapper", children: /* @__PURE__ */ jsxs("span", { children: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "not-a-spinner",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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",