@viraui/react 2026.0.31 → 2026.0.33

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/catalog.json CHANGED
@@ -95,7 +95,7 @@
95
95
  {
96
96
  "id": "glow",
97
97
  "name": "Glow",
98
- "summary": "Decorative pointer-driven border highlight on a transparent Surface shell (radius only). Put the visual card as children; a shared rAF tracker paints CSS variables without React re-renders.",
98
+ "summary": "Decorative pointer-driven border highlight on a transparent Surface shell (radius only). Put the visual card as children; a shared rAF tracker paints CSS variables without React re-renders. Memory-heavy — use sparingly (rarely).",
99
99
  "guidePath": "./components/glow/glow.guide.json"
100
100
  },
101
101
  {
@@ -197,7 +197,7 @@
197
197
  {
198
198
  "id": "static-noise",
199
199
  "name": "StaticNoise",
200
- "summary": "Decorative SVG turbulence noise overlay for textured surfaces and hero sections.",
200
+ "summary": "Decorative SVG turbulence noise overlay for textured surfaces and hero sections. Memory-heavy — use sparingly (rarely).",
201
201
  "guidePath": "./components/static-noise/static-noise.guide.json"
202
202
  },
203
203
  {
@@ -1 +1 @@
1
- @layer viraui.preflight, viraui.components, viraui.overrides;@layer viraui.components{@property --vui-button-background{syntax:"<color>";inherits:false;initial-value:#0000}@property --vui-button-color{syntax:"<color>";inherits:false;initial-value:#0000}@property --vui-button-height{syntax:"<length>";inherits:false;initial-value:0}@property --vui-button-padding-inline{syntax:"<length>";inherits:false;initial-value:0}@property --vui-button-padding-block{syntax:"<length>";inherits:false;initial-value:0}@property --vui-button-spinner-color{syntax:"<color>";inherits:false;initial-value:#0000}@property --vui-button-icon-size{syntax:"<length>";inherits:true;initial-value:0}@property --vui-button-radius{syntax:"<length-percentage>";inherits:false;initial-value:0}.viraui__button-module__Button_oPhrP{--vui-button-background:var(--global-primary);--vui-button-color:var(--base-1);--vui-button-height:var(--space-2x-large);--vui-button-padding-inline:calc(var(--space-small) + var(--space-2x-small));--vui-button-padding-block:var(--space-small);--vui-button-icon-size:1.125em;--vui-button-spinner-color:currentColor;--vui-button-radius:var(--radius-medium);max-block-size:min-content;-webkit-user-select:none;user-select:none;margin:0;cursor:pointer;font-family:inherit;display:inline-flex;align-items:center;min-inline-size:fit-content;min-block-size:var(--vui-button-height);block-size:var(--vui-button-height);justify-content:center;text-decoration:none;white-space:nowrap;appearance:button;overflow:hidden;position:relative;padding:var(--vui-button-padding-block) var(--vui-button-padding-inline);font-weight:400;font-variation-settings:"wght" 400;gap:var(--space-small);border:1px solid #0000;background:var(--vui-button-background);color:var(--vui-button-color);font-size:var(--font-scale-body-medium);border-radius:var(--vui-button-radius);transition:translate var(--easing-elastic) var(--duration-instant);will-change:translate;&:not(:disabled){background-clip:padding-box}&:not(:disabled,[aria-disabled=true]):active{translate:0 1px}&[data-pill=true]{border-radius:var(--radius-infinite)}&[data-size=big]{--vui-button-height:calc(var(--space-2x-large) + var(--space-x-small));--vui-button-padding-inline:calc(var(--space-medium) + var(--space-2x-small));--vui-button-padding-block:var(--space-small);--vui-button-icon-size:1.125em;--vui-button-radius:var(--radius-medium)}&[data-size=small]{--vui-button-height:var(--space-x-large);--vui-button-padding-inline:var(--space-small);--vui-button-padding-block:var(--space-x-small);--vui-button-icon-size:1em;--vui-button-radius:var(--radius-small);gap:var(--space-x-small);font-size:var(--font-scale-body-small)}&[data-variant=primary]{--vui-button-background:var(--global-primary);--vui-button-color:contrast-color(var(--global-primary));&:hover{--vui-button-background:oklab(from var(--global-primary) calc(l + 0.05) a b)}}&[data-variant=secondary]{--vui-button-background:var(--global-contrast);--vui-button-color:var(--base-6);background-clip:unset;&:hover{--vui-button-background:oklab(from var(--global-contrast) l a b/calc(alpha + 0.03))}@container root style(--is-light-mode: black){--vui-button-background:oklab(from var(--global-contrast) l a b/0.1)}}&[data-variant=flat]{--vui-button-background:#0000;--vui-button-color:var(--base-5);&:hover{--vui-button-background:var(--global-contrast);--vui-button-color:var(--base-7)}}&[data-variant=destructive]{--vui-button-background:oklab(from var(--highlight-red) l a b/12%);--vui-button-color:var(--highlight-red);outline-color:var(--highlight-red);&:hover{--vui-button-background:oklab(from var(--highlight-red) l a b/30%)}}&[data-addon-position=start]{flex-direction:row;padding-inline-end:calc(var(--vui-button-padding-inline) * 1.2)}&[data-addon-position=end]{flex-direction:row-reverse;padding-inline-start:calc(var(--vui-button-padding-inline) * 1.2)}&[data-icon-only=true]{gap:0;padding-block:var(--vui-button-padding-block);padding-inline:var(--vui-button-padding-block);inline-size:var(--vui-button-height);min-inline-size:var(--vui-button-height);max-inline-size:var(--vui-button-height);aspect-ratio:1/1}&:disabled,&:disabled:active,&:disabled:hover,&[aria-disabled=true],&[aria-disabled=true]:active,&[aria-disabled=true]:hover{--vui-button-background:var(--global-disabled-background);--vui-button-color:var(--global-disabled-foreground);cursor:not-allowed;box-shadow:none;translate:0}&[data-variant=flat]:is(:disabled,[aria-disabled=true]),&[data-variant=flat]:is(:disabled,[aria-disabled=true]):active,&[data-variant=flat]:is(:disabled,[aria-disabled=true]):hover{--vui-button-background:#0000}&[data-loading=true][data-disabled]{pointer-events:none;border:0;box-shadow:none;--vui-button-color:var(--base-7)}}.viraui__button-module__Content_w-pX-{display:inline-grid;place-items:center;&[data-loading=true]{opacity:0}&:first-child>svg{block-size:var(--vui-button-icon-size);inline-size:var(--vui-button-icon-size);flex-shrink:0}}.viraui__button-module__SpinnerOverlay_vKqw1{position:absolute;inset:0;display:grid;place-content:center;pointer-events:none;color:var(--vui-button-spinner-color)}}
1
+ @layer viraui.preflight, viraui.components, viraui.overrides;@layer viraui.components{@property --vui-button-background{syntax:"<color>";inherits:false;initial-value:#0000}@property --vui-button-color{syntax:"<color>";inherits:false;initial-value:#0000}@property --vui-button-height{syntax:"<length>";inherits:false;initial-value:0}@property --vui-button-padding-inline{syntax:"<length>";inherits:false;initial-value:0}@property --vui-button-padding-block{syntax:"<length>";inherits:false;initial-value:0}@property --vui-button-spinner-color{syntax:"<color>";inherits:false;initial-value:#0000}@property --vui-button-icon-size{syntax:"<length>";inherits:true;initial-value:0}@property --vui-button-radius{syntax:"<length-percentage>";inherits:false;initial-value:0}.viraui__button-module__Button_oPhrP{--vui-button-background:var(--global-primary);--vui-button-color:var(--base-1);--vui-button-height:var(--space-2x-large);--vui-button-padding-inline:calc(var(--space-small) + var(--space-2x-small));--vui-button-padding-block:var(--space-small);--vui-button-icon-size:1.125em;--vui-button-spinner-color:currentColor;--vui-button-radius:var(--radius-medium);max-block-size:min-content;-webkit-user-select:none;user-select:none;margin:0;cursor:pointer;font-family:inherit;display:inline-flex;align-items:center;min-inline-size:fit-content;min-block-size:var(--vui-button-height);block-size:var(--vui-button-height);justify-content:center;text-decoration:none;white-space:nowrap;appearance:button;overflow:hidden;position:relative;padding:var(--vui-button-padding-block) var(--vui-button-padding-inline);font-weight:400;font-variation-settings:"wght" 400;gap:var(--space-small);border:1px solid #0000;background:var(--vui-button-background);color:var(--vui-button-color);font-size:var(--font-scale-body-medium);border-radius:var(--vui-button-radius);transition:translate var(--easing-elastic) var(--duration-instant);will-change:translate;&:not(:disabled){background-clip:padding-box}&:not(:disabled,[aria-disabled=true]):active{translate:0 1px}&[data-pill=true]{border-radius:var(--radius-infinite)}&[data-size=big]{--vui-button-height:calc(var(--space-2x-large) + var(--space-x-small));--vui-button-padding-inline:calc(var(--space-medium) + var(--space-2x-small));--vui-button-padding-block:var(--space-small);--vui-button-icon-size:1.125em;--vui-button-radius:var(--radius-medium)}&[data-size=small]{--vui-button-height:var(--space-x-large);--vui-button-padding-inline:var(--space-small);--vui-button-padding-block:var(--space-x-small);--vui-button-icon-size:1em;--vui-button-radius:var(--radius-small);gap:var(--space-x-small);font-size:var(--font-scale-body-small)}&[data-variant=primary]{--vui-button-background:var(--global-primary);--vui-button-color:contrast-color(var(--global-primary));&:hover{--vui-button-background:oklab(from var(--global-primary) calc(l + 0.05) a b)}}&[data-variant=secondary]{--vui-button-background:var(--global-contrast);--vui-button-color:var(--base-6);background-clip:unset;&:hover{--vui-button-background:oklab(from var(--global-contrast) l a b/calc(alpha + 0.03))}@container root style(--is-light-mode: black){--vui-button-background:oklab(from var(--global-contrast) l a b/0.1)}}&[data-variant=flat]{--vui-button-background:#0000;--vui-button-color:var(--base-5);background-clip:unset;&:hover{--vui-button-background:var(--global-contrast);--vui-button-color:var(--base-7)}}&[data-variant=destructive]{--vui-button-background:oklab(from var(--highlight-red) l a b/12%);--vui-button-color:var(--highlight-red);outline-color:var(--highlight-red);&:hover{--vui-button-background:oklab(from var(--highlight-red) l a b/30%)}}&[data-addon-position=start]{flex-direction:row;padding-inline-end:calc(var(--vui-button-padding-inline) * 1.2)}&[data-addon-position=end]{flex-direction:row-reverse;padding-inline-start:calc(var(--vui-button-padding-inline) * 1.2)}&[data-icon-only=true]{gap:0;padding-block:var(--vui-button-padding-block);padding-inline:var(--vui-button-padding-block);inline-size:var(--vui-button-height);min-inline-size:var(--vui-button-height);max-inline-size:var(--vui-button-height);aspect-ratio:1/1}&:disabled,&:disabled:active,&:disabled:hover,&[aria-disabled=true],&[aria-disabled=true]:active,&[aria-disabled=true]:hover{--vui-button-background:var(--global-disabled-background);--vui-button-color:var(--global-disabled-foreground);cursor:not-allowed;box-shadow:none;translate:0}&[data-variant=flat]:is(:disabled,[aria-disabled=true]),&[data-variant=flat]:is(:disabled,[aria-disabled=true]):active,&[data-variant=flat]:is(:disabled,[aria-disabled=true]):hover{--vui-button-background:#0000}&[data-loading=true][data-disabled]{pointer-events:none;border:0;box-shadow:none;--vui-button-color:var(--base-7)}}.viraui__button-module__Content_w-pX-{display:inline-grid;place-items:center;&[data-loading=true]{opacity:0}&:first-child>svg{block-size:var(--vui-button-icon-size);inline-size:var(--vui-button-icon-size);flex-shrink:0}}.viraui__button-module__SpinnerOverlay_vKqw1{position:absolute;inset:0;display:grid;place-content:center;pointer-events:none;color:var(--vui-button-spinner-color)}}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "Glow",
3
3
  "structure": "<Glow>{children}</Glow>",
4
- "summary": "Decorative pointer-driven border highlight on a transparent Surface shell (radius only). Put the visual card as children; a shared rAF tracker paints CSS variables without React re-renders.",
4
+ "summary": "Decorative pointer-driven border highlight on a transparent Surface shell (radius only). Put the visual card as children; a shared rAF tracker paints CSS variables without React re-renders. Memory-heavy — use sparingly (rarely).",
5
5
  "whenToUse": [
6
6
  "Wrap a Surface (or other content) that should highlight toward the pointer",
7
7
  "Grid of glow shells that share one pointer highlight path",
@@ -9,6 +9,7 @@
9
9
  "Decorative continuous spin with autoRotate (no pointer)"
10
10
  ],
11
11
  "whenNotToUse": [
12
+ "Default decoration on many cards — memory cost is high; reserve for rare focal moments",
12
13
  "Static borders with no pointer interaction — use Surface border instead",
13
14
  "Accessible status or focus indication — glow is decorative only",
14
15
  "As a replacement for Surface color/padding — compose Surface as children"
@@ -23,7 +24,7 @@
23
24
  "Treating Glow as the visual card (color/padding) instead of composing Surface children",
24
25
  "Setting overflow:hidden on an ancestor Surface when outer glow must remain visible",
25
26
  "Expecting soft bloom Light layer — Glow is border-ring only",
26
- "Use many glows and auto rotate on the same page. It's cpu intensive"
27
+ "Using many Glow instances (especially with autoRotate) on one page memory- and CPU-heavy"
27
28
  ],
28
29
  "related": ["Surface", "Shimmer", "ProgressiveBlur"],
29
30
  "properties": {
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "StaticNoise",
3
3
  "structure": "<StaticNoise />",
4
- "summary": "Decorative SVG turbulence noise overlay for textured surfaces and hero sections.",
4
+ "summary": "Decorative SVG turbulence noise overlay for textured surfaces and hero sections. Memory-heavy — use sparingly (rarely).",
5
5
  "whenToUse": [
6
6
  "Subtle grain or texture overlay on cards, panels, or hero sections",
7
7
  "Glass or vibrant surfaces that need visual depth without extra content",
8
8
  "Decorative atmosphere where the parent owns semantics and layout positioning"
9
9
  ],
10
10
  "whenNotToUse": [
11
+ "Default decoration on many surfaces — memory cost is high; reserve for rare focal moments",
11
12
  "Standalone loading or status feedback without a labeled parent region",
12
13
  "Critical readable content where contrast has not been verified",
13
14
  "Large numbers of simultaneous full-viewport overlays on the same page"
@@ -18,6 +19,7 @@
18
19
  "Consumer owns position, inset, z-index, and sizing for the overlay placement"
19
20
  ],
20
21
  "antiPatterns": [
22
+ "Treating StaticNoise as cheap wallpaper — it is memory-heavy; prefer few instances",
21
23
  "Stacking many full-viewport StaticNoise instances on one screen",
22
24
  "Using StaticNoise as the only loading announcement for a page",
23
25
  "Embedding layout positioning rules inside the component stylesheet"
@@ -5,7 +5,11 @@ import { ToggleGroup } from "@base-ui/react/toggle-group";
5
5
  var ToggleGroup$1 = ({ children, orientation = "horizontal", ...otherProps }) => /* @__PURE__ */ jsx(ToggleGroup, {
6
6
  ...otherProps,
7
7
  orientation,
8
- render: /* @__PURE__ */ jsx(Stack, { direction: orientation === "vertical" ? "column" : "row" }),
8
+ render: /* @__PURE__ */ jsx(Stack, {
9
+ direction: orientation === "vertical" ? "column" : "row",
10
+ columnGap: "2x-small",
11
+ rowGap: "2x-small"
12
+ }),
9
13
  children
10
14
  });
11
15
  //#endregion
@@ -1 +1 @@
1
- @layer viraui.preflight, viraui.components, viraui.overrides;@layer viraui.components{@property --vui-typewriter-caret-color{syntax:"<color>";inherits:true;initial-value:currentcolor}.viraui__typewriter-module__Typewriter_R3sbb{position:relative;isolation:isolate}.viraui__typewriter-module__Display_-IGJh{white-space:pre-wrap;&[data-preserve-space]{display:inline-grid;&>.viraui__typewriter-module__Ghost_R-Yy-,&>.viraui__typewriter-module__Typed_00NZ8{grid-area:1/1}}}.viraui__typewriter-module__Caret_96Yfs{display:inline-block;vertical-align:initial;color:var(--vui-typewriter-caret-color);animation:viraui__typewriter-module__typewriter-caret-blink_yLCpV 1.1s step-start infinite;@media (prefers-reduced-motion:reduce){animation:none}&[data-type=block]{inline-size:.55em;block-size:.95em;margin-inline-start:.04em;background-color:var(--vui-typewriter-caret-color);transform:translateY(.08em)}&[data-type=pipe]:before{content:"|"}&[data-type=underscore]:before{content:"_"}}.viraui__typewriter-module__Ghost_R-Yy-{visibility:hidden;-webkit-user-select:none;user-select:none}.viraui__typewriter-module__Typed_00NZ8{display:inline}.viraui__typewriter-module__A11y_B-rjF{position:absolute;inset-block-start:0;inset-inline-start:0;inline-size:1px;block-size:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0;pointer-events:none}@keyframes viraui__typewriter-module__typewriter-caret-blink_yLCpV{50%{opacity:0}}}
1
+ @layer viraui.preflight, viraui.components, viraui.overrides;@layer viraui.components{@property --vui-typewriter-caret-color{syntax:"<color>";inherits:true;initial-value:currentcolor}.viraui__typewriter-module__Typewriter_R3sbb{position:relative;isolation:isolate}.viraui__typewriter-module__Ghost_R-Yy-{visibility:hidden;white-space:pre-wrap}.viraui__typewriter-module__Typed_00NZ8{white-space:pre-wrap}.viraui__typewriter-module__Typewriter_R3sbb[data-preserve-space]>.viraui__typewriter-module__Typed_00NZ8{position:absolute;inset-inline-start:0;inset-block-start:0}.viraui__typewriter-module__Caret_96Yfs{display:inline-block;vertical-align:initial;color:var(--vui-typewriter-caret-color);animation:viraui__typewriter-module__typewriter-caret-blink_yLCpV 1.1s step-start infinite;@media (prefers-reduced-motion:reduce){animation:none}&[data-type=block]{inline-size:.55em;block-size:.95em;margin-inline-start:.04em;background-color:var(--vui-typewriter-caret-color);transform:translateY(.08em)}&[data-type=pipe]:before{content:"|"}&[data-type=underscore]:before{content:"_"}}.viraui__typewriter-module__A11y_B-rjF{position:absolute;inset-block-start:0;inset-inline-start:0;inline-size:1px;block-size:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0;pointer-events:none}@keyframes viraui__typewriter-module__typewriter-caret-blink_yLCpV{50%{opacity:0}}}
@@ -58,7 +58,9 @@ export type TypewriterProps = React.ComponentPropsWithRef<'span'> & {
58
58
  */
59
59
  loop?: boolean;
60
60
  /**
61
- * Reserves the full phrase width while characters are typed.
61
+ * Reserves the full phrase width and height (including wrap under parent
62
+ * constraints) from first paint through delay, typing, and deletion via an
63
+ * in-flow `visibility: hidden` phrase layer; the typed effect paints absolute on top.
62
64
  *
63
65
  * @default false
64
66
  */
@@ -13,14 +13,17 @@
13
13
  "Cases where reduced-motion users must see every phrase in a multi-text cycle"
14
14
  ],
15
15
  "accessibility": [
16
- "The visual display layer is aria-hidden; completed phrases announce through the polite live region",
16
+ "One phrase layer (Ghost when preserveSpace, clipped A11y otherwise) holds the full current phrase and announces via aria-live",
17
+ "The absolute/in-flow typed layer is aria-hidden and only paints the progressive effect plus caret",
17
18
  "Reduced motion shows the final phrase immediately without cycling delays",
18
19
  "Parent region owns loading or busy semantics when Typewriter is decorative"
19
20
  ],
20
21
  "antiPatterns": [
21
22
  "Using Typewriter as the only source of critical status information",
22
23
  "Embedding layout positioning rules inside the component stylesheet",
23
- "Restarting animation on every parent render by passing unstable inline text arrays without stable content"
24
+ "Restarting animation on every parent render by passing unstable inline text arrays without stable content",
25
+ "Setting parent Text or Title trim={false} solely so preserveSpace can reserve wrapped height",
26
+ "Driving per-character typing through React state (use DOM writes on the typed layer instead)"
24
27
  ],
25
28
  "related": ["Text", "Title", "Shimmer"],
26
29
  "properties": {
@@ -44,7 +47,7 @@
44
47
  "description": "Sets the pause after a phrase completes before deletion starts, in milliseconds."
45
48
  },
46
49
  "preserveSpace": {
47
- "description": "Reserves the full phrase width while characters are typed."
50
+ "description": "Reserves the full phrase width and height (including wrap under parent constraints) from first paint through delay, typing, and deletion."
48
51
  },
49
52
  "speed": {
50
53
  "description": "Sets the delay between typed characters in milliseconds."
@@ -4,45 +4,29 @@ import { clsx } from "../../core/clsx.js";
4
4
  import typewriter_module_default from "./typewriter.module.js";
5
5
  import { resolveTextsKey, runTypewriterAnimation, splitTextsKey } from "./typewriter.utils.js";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
- import { useEffect, useRef } from "react";
7
+ import { useEffect, useState } from "react";
8
8
  //#region src/components/typewriter/typewriter.tsx
9
- var createCaretElement = (caret) => {
10
- if (caret === "none") return null;
11
- const element = document.createElement("span");
12
- element.className = typewriter_module_default.Caret ?? "";
13
- element.dataset.type = caret;
14
- return element;
9
+ var writeTypedText = (typed, value) => {
10
+ const existing = typed.firstChild;
11
+ const textNode = existing instanceof Text ? existing : document.createTextNode("");
12
+ if (textNode !== existing) typed.insertBefore(textNode, existing);
13
+ textNode.data = value;
15
14
  };
16
15
  var Typewriter = ({ caret = "pipe", caretColor, children, className, delay = 0, deleteSpeed = 25, loop = true, pause = 2e3, preserveSpace = false, ref, speed = 40, style, texts, ...otherProps }) => {
17
- const displayRef = useRef(null);
18
- const liveRef = useRef(null);
19
16
  const textsKey = resolveTextsKey(texts, children);
20
17
  const firstText = splitTextsKey(textsKey)[0] ?? "";
18
+ const [phrase, setPhrase] = useState(firstText);
19
+ const [typedHost, setTypedHost] = useState(null);
21
20
  useEffect(() => {
22
- const display = displayRef.current;
23
- const live = liveRef.current;
24
- if (!display || !textsKey) return;
25
- const textNode = document.createTextNode("");
26
- const caretElement = createCaretElement(caret);
27
- let ghostTextNode = null;
28
- const nodes = [];
29
- if (preserveSpace) {
30
- ghostTextNode = document.createTextNode("");
31
- const ghost = document.createElement("span");
32
- ghost.className = typewriter_module_default.Ghost ?? "";
33
- ghost.appendChild(ghostTextNode);
34
- nodes.push(ghost);
35
- const typed = document.createElement("span");
36
- typed.className = typewriter_module_default.Typed ?? "";
37
- typed.appendChild(textNode);
38
- if (caretElement) typed.appendChild(caretElement);
39
- nodes.push(typed);
40
- } else {
41
- nodes.push(textNode);
42
- if (caretElement) nodes.push(caretElement);
43
- }
44
- display.replaceChildren(...nodes);
21
+ setPhrase(firstText);
22
+ }, [firstText, textsKey]);
23
+ useEffect(() => {
24
+ if (!typedHost || !textsKey) return;
45
25
  const controller = new AbortController();
26
+ const writeVisible = (value) => {
27
+ writeTypedText(typedHost, value);
28
+ };
29
+ writeVisible("");
46
30
  runTypewriterAnimation({
47
31
  texts: splitTextsKey(textsKey),
48
32
  loop,
@@ -50,26 +34,23 @@ var Typewriter = ({ caret = "pipe", caretColor, children, className, delay = 0,
50
34
  deleteDelay: deleteSpeed,
51
35
  pause,
52
36
  delay,
53
- textNode,
54
- ghostTextNode,
55
37
  reducedMotion: window.matchMedia("(prefers-reduced-motion: reduce)").matches,
56
- onPhraseComplete: (text) => {
57
- if (live) live.textContent = text;
58
- },
38
+ onPhrase: setPhrase,
39
+ writeVisible,
40
+ onPhraseComplete: setPhrase,
59
41
  signal: controller.signal
60
42
  });
61
43
  return () => {
62
44
  controller.abort();
63
45
  };
64
46
  }, [
65
- caret,
66
47
  delay,
67
48
  deleteSpeed,
68
49
  loop,
69
50
  pause,
70
- preserveSpace,
71
51
  speed,
72
- textsKey
52
+ textsKey,
53
+ typedHost
73
54
  ]);
74
55
  const dynamicStyle = {
75
56
  ...caretColor && { "--vui-typewriter-caret-color": caretColor },
@@ -80,21 +61,20 @@ var Typewriter = ({ caret = "pipe", caretColor, children, className, delay = 0,
80
61
  ref,
81
62
  className: clsx(typewriter_module_default.Typewriter, className),
82
63
  style: dynamicStyle,
64
+ ...preserveSpace && { "data-preserve-space": true },
83
65
  children: [/* @__PURE__ */ jsx("span", {
84
- ref: displayRef,
85
- className: typewriter_module_default.Display,
86
- "aria-hidden": "true",
87
- ...preserveSpace && { "data-preserve-space": true },
88
- children: preserveSpace && firstText && /* @__PURE__ */ jsx("span", {
89
- className: typewriter_module_default.Ghost,
90
- children: firstText
91
- })
92
- }), /* @__PURE__ */ jsx("span", {
93
- ref: liveRef,
94
- className: typewriter_module_default.A11y,
66
+ className: preserveSpace ? typewriter_module_default.Ghost : typewriter_module_default.A11y,
95
67
  "aria-live": "polite",
96
68
  "aria-atomic": "true",
97
- children: firstText
69
+ children: phrase
70
+ }), /* @__PURE__ */ jsx("span", {
71
+ ref: setTypedHost,
72
+ className: typewriter_module_default.Typed,
73
+ "aria-hidden": "true",
74
+ children: caret !== "none" && textsKey && /* @__PURE__ */ jsx("span", {
75
+ className: typewriter_module_default.Caret,
76
+ "data-type": caret
77
+ })
98
78
  })]
99
79
  });
100
80
  };
@@ -1,13 +1,11 @@
1
1
  //#region src/components/typewriter/typewriter.module.css
2
2
  var Typewriter = "viraui__typewriter-module__Typewriter_R3sbb";
3
- var Display = "viraui__typewriter-module__Display_-IGJh";
4
3
  var Ghost = "viraui__typewriter-module__Ghost_R-Yy-";
5
4
  var Typed = "viraui__typewriter-module__Typed_00NZ8";
6
5
  var Caret = "viraui__typewriter-module__Caret_96Yfs";
7
6
  var A11y = "viraui__typewriter-module__A11y_B-rjF";
8
7
  var typewriter_module_default = {
9
8
  Typewriter,
10
- Display,
11
9
  Ghost,
12
10
  Typed,
13
11
  Caret,
@@ -15,4 +13,4 @@ var typewriter_module_default = {
15
13
  A11y
16
14
  };
17
15
  //#endregion
18
- export { A11y, Caret, Display, Ghost, Typed, Typewriter, typewriter_module_default as default };
16
+ export { A11y, Caret, Ghost, Typed, Typewriter, typewriter_module_default as default };
@@ -14,10 +14,12 @@ export type RunTypewriterAnimationOptions = {
14
14
  deleteDelay: number;
15
15
  pause: number;
16
16
  delay: number;
17
- textNode: Text;
18
- ghostTextNode: Text | null;
19
17
  reducedMotion: boolean;
18
+ /** Full current phrase (ghost / reserve). Rare — safe for React state. */
19
+ onPhrase: (phrase: string) => void;
20
+ /** Visible typed substring. Hot path — keep DOM writes, not React setState. */
21
+ writeVisible: (value: string) => void;
20
22
  onPhraseComplete: (text: string) => void;
21
23
  signal: AbortSignal;
22
24
  };
23
- export declare const runTypewriterAnimation: ({ texts, loop, typeDelay, deleteDelay, pause, delay: initialDelay, reducedMotion, onPhraseComplete, signal, ...nodes }: RunTypewriterAnimationOptions) => Promise<void>;
25
+ export declare const runTypewriterAnimation: ({ texts, loop, typeDelay, deleteDelay, pause, delay: initialDelay, reducedMotion, onPhrase, writeVisible, onPhraseComplete, signal, }: RunTypewriterAnimationOptions) => Promise<void>;
@@ -39,29 +39,18 @@ var delay = (milliseconds, signal) => new Promise((resolve, reject) => {
39
39
  }, milliseconds);
40
40
  signal.addEventListener("abort", onAbort, { once: true });
41
41
  });
42
- var runTypewriterAnimation = async ({ texts, loop, typeDelay, deleteDelay, pause, delay: initialDelay, reducedMotion, onPhraseComplete, signal, ...nodes }) => {
42
+ var runTypewriterAnimation = async ({ texts, loop, typeDelay, deleteDelay, pause, delay: initialDelay, reducedMotion, onPhrase, writeVisible, onPhraseComplete, signal }) => {
43
43
  if (texts.length === 0) return;
44
- const display = { node: nodes.textNode };
45
- const ghost = nodes.ghostTextNode ? { node: nodes.ghostTextNode } : null;
46
- const writeDisplayText = (value) => {
47
- display.node.data = value;
48
- };
49
- const writeGhostText = (value) => {
50
- if (ghost) ghost.node.data = value;
51
- };
52
- const setGhostText = (text) => {
53
- writeGhostText(text);
54
- };
55
44
  const typeSuffix = async (graphemes, fromIndex) => {
56
45
  for (let graphemeIndex = fromIndex + 1; graphemeIndex <= graphemes.length; graphemeIndex += 1) {
57
46
  await delay(typeDelay, signal);
58
- writeDisplayText(joinGraphemes(graphemes, graphemeIndex));
47
+ writeVisible(joinGraphemes(graphemes, graphemeIndex));
59
48
  }
60
49
  };
61
50
  const deleteSuffix = async (graphemes, keepCount) => {
62
51
  for (let graphemeIndex = graphemes.length - 1; graphemeIndex >= keepCount; graphemeIndex -= 1) {
63
52
  await delay(deleteDelay, signal);
64
- writeDisplayText(joinGraphemes(graphemes, graphemeIndex));
53
+ writeVisible(joinGraphemes(graphemes, graphemeIndex));
65
54
  }
66
55
  };
67
56
  const getUpcomingGraphemes = (phraseIndex) => {
@@ -70,11 +59,12 @@ var runTypewriterAnimation = async ({ texts, loop, typeDelay, deleteDelay, pause
70
59
  try {
71
60
  if (reducedMotion) {
72
61
  const finalText = texts.at(-1) ?? "";
73
- setGhostText(finalText);
74
- writeDisplayText(finalText);
62
+ onPhrase(finalText);
63
+ writeVisible(finalText);
75
64
  onPhraseComplete(finalText);
76
65
  return;
77
66
  }
67
+ onPhrase(texts[0] ?? "");
78
68
  await delay(initialDelay, signal);
79
69
  let currentGraphemes = [];
80
70
  do
@@ -82,8 +72,10 @@ var runTypewriterAnimation = async ({ texts, loop, typeDelay, deleteDelay, pause
82
72
  const text = texts[phraseIndex] ?? "";
83
73
  const phraseGraphemes = splitGraphemes(text);
84
74
  const isLastPhrase = phraseIndex === texts.length - 1;
85
- setGhostText(text);
86
- await typeSuffix(phraseGraphemes, commonPrefixLength(currentGraphemes, phraseGraphemes) - 1);
75
+ onPhrase(text);
76
+ const sharedPrefixLength = commonPrefixLength(currentGraphemes, phraseGraphemes);
77
+ writeVisible(joinGraphemes(phraseGraphemes, sharedPrefixLength));
78
+ await typeSuffix(phraseGraphemes, sharedPrefixLength - 1);
87
79
  currentGraphemes = phraseGraphemes;
88
80
  onPhraseComplete(text);
89
81
  if (!loop && isLastPhrase) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viraui/react",
3
- "version": "2026.0.31",
3
+ "version": "2026.0.33",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist"
@@ -35,7 +35,7 @@
35
35
  "unplugin-dts": "1.0.3",
36
36
  "vite": "8.1.4",
37
37
  "vite-plugin-static-copy": "4.1.1",
38
- "@viraui/foundation": "2026.0.31",
38
+ "@viraui/foundation": "2026.0.33",
39
39
  "@viraui/icons": "2026.0.1",
40
40
  "@viraui/postcss-config": "2026.0.0",
41
41
  "@viraui/tsconfig": "2026.0.0"