@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 +2 -2
- package/dist/components/button/button.css +1 -1
- package/dist/components/glow/glow.guide.json +3 -2
- package/dist/components/static-noise/static-noise.guide.json +3 -1
- package/dist/components/toggle-group/toggle-group.js +5 -1
- package/dist/components/typewriter/typewriter.css +1 -1
- package/dist/components/typewriter/typewriter.d.ts +3 -1
- package/dist/components/typewriter/typewriter.guide.json +6 -3
- package/dist/components/typewriter/typewriter.js +32 -52
- package/dist/components/typewriter/typewriter.module.js +1 -3
- package/dist/components/typewriter/typewriter.utils.d.ts +5 -3
- package/dist/components/typewriter/typewriter.utils.js +10 -18
- package/package.json +2 -2
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
|
-
"
|
|
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, {
|
|
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-
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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,
|
|
7
|
+
import { useEffect, useState } from "react";
|
|
8
8
|
//#region src/components/typewriter/typewriter.tsx
|
|
9
|
-
var
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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.
|
|
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.
|
|
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"
|