@streamsai/ui 0.2.1 → 0.2.2

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.
@@ -8,10 +8,11 @@ interface RunningIndicatorProps {
8
8
  * changes size, so a row's geometry is stable while it animates. Reduced motion renders the core
9
9
  * static with no halo.
10
10
  *
11
- * The loops start imperatively, not with `animate` keyframes on a `motion` element: inside an
12
- * `AnimatePresence initial={false}` (the leading slot of a NavItem row) motion skips an animation
13
- * that starts on mount, so a declarative loop sat still in rows while the same dot pulsed in the
14
- * filter chips.
11
+ * The loops start imperatively through `startRunningLoop`, not with `animate` keyframes on a
12
+ * `motion` element: inside an `AnimatePresence initial={false}` (the leading slot of a NavItem row)
13
+ * motion skips an animation that starts on mount, so a declarative loop sat still in rows while the
14
+ * same dot pulsed in the filter chips. `startRunningLoop` also puts the loop on the page clock, so
15
+ * every running dot blinks in step, whenever it mounted.
15
16
  */
16
17
  declare function RunningIndicator({ label, className }: RunningIndicatorProps): import("react").JSX.Element;
17
18
  export { RunningIndicator, type RunningIndicatorProps };
@@ -1,7 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef } from 'react';
3
- import { animate, usePrefersReducedMotion } from '../../motion/index.js';
4
- import { runningIndicator } from '../../motion/config.js';
3
+ import { startRunningLoop, usePrefersReducedMotion } from '../../motion/index.js';
5
4
  import { cn } from '../../lib/utils.js';
6
5
  /**
7
6
  * The "an agent is working" cue, the twin of iOS `RunningIndicator`: an 8px green core that
@@ -9,10 +8,11 @@ import { cn } from '../../lib/utils.js';
9
8
  * changes size, so a row's geometry is stable while it animates. Reduced motion renders the core
10
9
  * static with no halo.
11
10
  *
12
- * The loops start imperatively, not with `animate` keyframes on a `motion` element: inside an
13
- * `AnimatePresence initial={false}` (the leading slot of a NavItem row) motion skips an animation
14
- * that starts on mount, so a declarative loop sat still in rows while the same dot pulsed in the
15
- * filter chips.
11
+ * The loops start imperatively through `startRunningLoop`, not with `animate` keyframes on a
12
+ * `motion` element: inside an `AnimatePresence initial={false}` (the leading slot of a NavItem row)
13
+ * motion skips an animation that starts on mount, so a declarative loop sat still in rows while the
14
+ * same dot pulsed in the filter chips. `startRunningLoop` also puts the loop on the page clock, so
15
+ * every running dot blinks in step, whenever it mounted.
16
16
  */
17
17
  function RunningIndicator({ label, className }) {
18
18
  const pulsing = !usePrefersReducedMotion();
@@ -23,11 +23,7 @@ function RunningIndicator({ label, className }) {
23
23
  const halo = haloRef.current;
24
24
  if (!pulsing || !core || !halo)
25
25
  return;
26
- const controls = [
27
- animate(core, runningIndicator.core, runningIndicator.transition),
28
- animate(halo, runningIndicator.halo, runningIndicator.transition),
29
- ];
30
- return () => controls.forEach((control) => control.cancel());
26
+ return startRunningLoop({ core, halo });
31
27
  }, [pulsing]);
32
28
  return (_jsxs("span", { role: "img", "aria-label": label, "data-slot": "running-indicator", className: cn('relative inline-grid size-3 shrink-0 place-items-center', className), children: [pulsing ? (_jsx("span", { ref: haloRef, "aria-hidden": true, "data-slot": "running-indicator-halo", className: "absolute inset-0 rounded-full bg-home-running-halo" })) : null, _jsx("span", { ref: coreRef, "aria-hidden": true, "data-slot": "running-indicator-core", className: "relative size-2 rounded-full bg-home-running" })] }));
33
29
  }
@@ -1,16 +1,18 @@
1
1
  import { type VariantProps } from 'class-variance-authority';
2
2
  declare const statusDotVariants: (props?: ({
3
- status?: "done" | "working" | "stuck" | "idle" | "attention" | "unread" | null | undefined;
3
+ status?: "idle" | "done" | "stuck" | "attention" | "unread" | null | undefined;
4
4
  size?: "sm" | "md" | null | undefined;
5
5
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
6
- interface StatusDotProps extends VariantProps<typeof statusDotVariants> {
6
+ type RestingStatus = NonNullable<VariantProps<typeof statusDotVariants>['status']>;
7
+ interface StatusDotProps {
8
+ /** `working` is always the `RunningIndicator`; the other states are resting dots. */
9
+ status?: RestingStatus | 'working';
10
+ /** Dot diameter of a resting dot. Ignored by `working` and the `home` appearance. */
11
+ size?: VariantProps<typeof statusDotVariants>['size'];
7
12
  label: string;
8
13
  className?: string;
9
- /**
10
- * `home` is the mobile Home dot: an 8px core in a fixed 12px box so every state lines up.
11
- * Working is the `RunningIndicator` (green, pulsing halo) instead of the purple fade.
12
- */
14
+ /** `home` is the mobile Home dot: an 8px core in a fixed 12px box so every state lines up. */
13
15
  appearance?: 'default' | 'home';
14
16
  }
15
- declare function StatusDot({ status, size, label, className, appearance }: StatusDotProps): import("react").JSX.Element;
17
+ declare function StatusDot({ status, size, label, className, appearance, }: StatusDotProps): import("react").JSX.Element;
16
18
  export { StatusDot, statusDotVariants, type StatusDotProps };
@@ -1,14 +1,11 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cva } from 'class-variance-authority';
3
- import { motion } from '../../motion/index.js';
4
- import { loops } from '../../motion/config.js';
5
3
  import { cn } from '../../lib/utils.js';
6
4
  import { RunningIndicator } from './running-indicator.js';
7
5
  const statusDotVariants = cva('inline-block shrink-0 rounded-full', {
8
6
  variants: {
9
7
  status: {
10
8
  idle: 'bg-status-idle',
11
- working: 'bg-status-working',
12
9
  done: 'bg-status-done',
13
10
  stuck: 'bg-status-stuck',
14
11
  attention: 'bg-warning',
@@ -18,12 +15,14 @@ const statusDotVariants = cva('inline-block shrink-0 rounded-full', {
18
15
  },
19
16
  defaultVariants: { status: 'idle', size: 'md' },
20
17
  });
21
- function StatusDot({ status, size, label, className, appearance = 'default' }) {
22
- const isWorking = status === 'working';
18
+ function StatusDot({ status = 'idle', size, label, className, appearance = 'default', }) {
19
+ // One running cue for every surface, so all of them blink in step (`startRunningLoop`).
20
+ if (status === 'working')
21
+ return _jsx(RunningIndicator, { label: label, className: className });
23
22
  if (appearance === 'home') {
24
- return isWorking ? (_jsx(RunningIndicator, { label: label, className: className })) : (_jsx(HomeStatusDot, { status: status, label: label, className: className }));
23
+ return _jsx(HomeStatusDot, { status: status, label: label, className: className });
25
24
  }
26
- return (_jsx(motion.span, { role: "img", "aria-label": label, "data-slot": "status-dot", animate: isWorking ? { opacity: [1, 0.3, 1] } : { opacity: 1 }, transition: isWorking ? loops.pulse : { duration: 0 }, className: cn(statusDotVariants({ status, size }), className) }));
25
+ return (_jsx("span", { role: "img", "aria-label": label, "data-slot": "status-dot", className: cn(statusDotVariants({ status, size }), className) }));
27
26
  }
28
27
  /** A resting Home dot, in the same 12px box as the `RunningIndicator` so rows line up. */
29
28
  function HomeStatusDot({ status, label, className, }) {
package/dist/index.d.ts CHANGED
@@ -21,7 +21,7 @@ export * as Icons from './icons/index.js';
21
21
  export type { Platform, PlatformProviderProps } from './platform/index.js';
22
22
  export { PlatformProvider, usePlatform } from './platform/index.js';
23
23
  export { AnimatePresence, LayoutGroup, animate, motion, useMotionValue, useReducedMotion, useTransform, type HTMLMotionProps, type MotionValue, type Transition, type Variants } from './motion/index.js';
24
- export { duration, ease, spring, distance, scale, stagger, transitions, press, presets, loops, runningIndicator, loopAnimate, loopTransition, popup, sheet, slide, resize, reflow, edgeReveal, stepSwap, staggerDelay } from './motion/index.js';
24
+ export { duration, ease, spring, distance, scale, stagger, transitions, press, presets, loops, runningIndicator, startRunningLoop, loopAnimate, loopTransition, popup, sheet, slide, resize, reflow, edgeReveal, stepSwap, staggerDelay } from './motion/index.js';
25
25
  export { MotionProvider, disableMotion, enableMotion, enablePresenceExits, disablePresenceExits, PageTransition, Collapse, AutoHeight, usePrefersReducedMotion, useAnyModalOpen, Swap, swapVariants, StaggerItem, type PageTransitionProps, type CollapseProps, type AutoHeightProps, type SwapProps, type SwapPresetName, type StaggerItemProps } from './motion/index.js';
26
26
  export { AgentAvatar, type AgentPicture, type AgentPingoPicture, type AgentImagePicture, } from './components/ui/agent-avatar.js';
27
27
  export { AGENT_AVATAR_SIZE, AGENT_AVATAR_RADIUS, AGENT_AVATAR_PX, type AgentAvatarSize, } from './components/ui/avatar-frame.js';
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@ export { PlatformProvider, usePlatform } from './platform/index.js';
22
22
  // prettier-ignore
23
23
  export { AnimatePresence, LayoutGroup, animate, motion, useMotionValue, useReducedMotion, useTransform } from './motion/index.js';
24
24
  // prettier-ignore
25
- export { duration, ease, spring, distance, scale, stagger, transitions, press, presets, loops, runningIndicator, loopAnimate, loopTransition, popup, sheet, slide, resize, reflow, edgeReveal, stepSwap, staggerDelay } from './motion/index.js';
25
+ export { duration, ease, spring, distance, scale, stagger, transitions, press, presets, loops, runningIndicator, startRunningLoop, loopAnimate, loopTransition, popup, sheet, slide, resize, reflow, edgeReveal, stepSwap, staggerDelay } from './motion/index.js';
26
26
  // prettier-ignore
27
27
  export { MotionProvider, disableMotion, enableMotion, enablePresenceExits, disablePresenceExits, PageTransition, Collapse, AutoHeight, usePrefersReducedMotion, useAnyModalOpen, Swap, swapVariants, StaggerItem } from './motion/index.js';
28
28
  // ── Components ───────────────────────────────────────────────────────────────
@@ -758,7 +758,7 @@ export declare const loops: {
758
758
  readonly duration: 1;
759
759
  };
760
760
  /**
761
- * Skeleton and status-dot: smooth opacity float, slow enough to read as "still loading". Give it
761
+ * Skeleton: smooth opacity float, slow enough to read as "still loading". Give it
762
762
  * keyframes that come back to where they start (`[1, 0.4, 1]`): it loops rather than mirrors, so
763
763
  * the browser runs it off the main thread (motion drives a mirrored loop in JS, every frame).
764
764
  */
@@ -802,6 +802,9 @@ export declare const runningIndicator: {
802
802
  readonly halo: {
803
803
  readonly opacity: [1, 0, 1];
804
804
  };
805
+ readonly dot: {
806
+ readonly opacity: [1, 0.25, 1];
807
+ };
805
808
  readonly transition: {
806
809
  readonly repeat: number;
807
810
  readonly repeatType: "loop";
@@ -364,7 +364,7 @@ export const loops = {
364
364
  * the browser runs it (motion drives `rotate` in JS, every frame). */
365
365
  spin: { repeat: Infinity, repeatType: 'loop', ease: 'linear', duration: 1 },
366
366
  /**
367
- * Skeleton and status-dot: smooth opacity float, slow enough to read as "still loading". Give it
367
+ * Skeleton: smooth opacity float, slow enough to read as "still loading". Give it
368
368
  * keyframes that come back to where they start (`[1, 0.4, 1]`): it loops rather than mirrors, so
369
369
  * the browser runs it off the main thread (motion drives a mirrored loop in JS, every frame).
370
370
  */
@@ -387,17 +387,21 @@ export const loops = {
387
387
  },
388
388
  };
389
389
  /**
390
- * `RunningIndicator`: the 8px core scales 0.88 to 1 and the 2px halo fades out and in, on the same
391
- * 2s loop as the iOS dot (`RunningIndicator.swift`), cosine-like so both ends rest. Nothing fades
392
- * the core. The core animates a whole `transform` string, not `scale`, so motion hands the loop to
393
- * the Web Animations API and it keeps running on the compositor while a phone webview scrolls or
394
- * its main thread is busy. Reduced motion stops the whole loop (the dot renders static, no halo),
395
- * unlike `loops.pulse`, which carries on.
390
+ * The one running loop, for every "an agent is working" dot in the product. `RunningIndicator`: the
391
+ * 8px core scales 0.88 to 1 and the 2px halo fades out and in, on the same 2s loop as the iOS dot
392
+ * (`RunningIndicator.swift`), cosine-like so both ends rest. `dot` is the same loop as a fade, for
393
+ * a dot too small to scale (the 6px phase dot of a run row). Start these only through
394
+ * `startRunningLoop` (`running-loop.ts`), which puts every loop on the same clock so dots mounted
395
+ * at different times still blink together. The core animates a whole `transform` string, not
396
+ * `scale`, so motion hands the loop to the Web Animations API and it keeps running on the
397
+ * compositor while a phone webview scrolls or its main thread is busy. Reduced motion stops the
398
+ * whole loop (the dot renders static, no halo), unlike `loops.pulse`, which carries on.
396
399
  */
397
400
  const runningIndicatorEase = [0.37, 0, 0.63, 1];
398
401
  export const runningIndicator = {
399
402
  core: { transform: ['scale(1)', 'scale(0.88)', 'scale(1)'] },
400
403
  halo: { opacity: [1, 0, 1] },
404
+ dot: { opacity: [1, 0.25, 1] },
401
405
  transition: { repeat: Infinity, repeatType: 'loop', ease: runningIndicatorEase, duration: 2 },
402
406
  };
403
407
  /**
@@ -4,5 +4,6 @@
4
4
  */
5
5
  export { LayoutGroup, animate, motion, useMotionValue, useReducedMotion, useTransform, type HTMLMotionProps, type MotionValue, type Transition, type Variants, } from 'motion/react';
6
6
  export { duration, ease, spring, distance, scale, stagger, transitions, press, presets, loops, runningIndicator, loopAnimate, loopTransition, popup, sheet, slide, resize, reflow, glide, edgeReveal, stepSwap, scrollReveal, staggerDelay, } from './config.js';
7
+ export { startRunningLoop } from './running-loop.js';
7
8
  export { MotionProvider, disableMotion, enableMotion, enablePresenceExits, disablePresenceExits, AnimatePresence, PageTransition, Collapse, AutoHeight, usePrefersReducedMotion, Swap, swapVariants, StaggerItem, type PageTransitionProps, type CollapseProps, type AutoHeightProps, type SwapProps, type SwapPresetName, type StaggerItemProps, } from './components.js';
8
9
  export { useAnyModalOpen } from './modal-presence.js';
@@ -4,5 +4,6 @@
4
4
  */
5
5
  export { LayoutGroup, animate, motion, useMotionValue, useReducedMotion, useTransform, } from 'motion/react';
6
6
  export { duration, ease, spring, distance, scale, stagger, transitions, press, presets, loops, runningIndicator, loopAnimate, loopTransition, popup, sheet, slide, resize, reflow, glide, edgeReveal, stepSwap, scrollReveal, staggerDelay, } from './config.js';
7
+ export { startRunningLoop } from './running-loop.js';
7
8
  export { MotionProvider, disableMotion, enableMotion, enablePresenceExits, disablePresenceExits, AnimatePresence, PageTransition, Collapse, AutoHeight, usePrefersReducedMotion, Swap, swapVariants, StaggerItem, } from './components.js';
8
9
  export { useAnyModalOpen } from './modal-presence.js';
@@ -0,0 +1,14 @@
1
+ interface RunningLoopTargets {
2
+ core?: Element | null;
3
+ halo?: Element | null;
4
+ dot?: Element | null;
5
+ }
6
+ /**
7
+ * Starts the running loop on the given elements and returns a function that stops it. Each loop is
8
+ * moved to the position the page clock gives it (time 0 is the start of the page), not the moment
9
+ * it mounted, so every running dot on screen shows the same frame at the same instant, however
10
+ * late or often it mounts. That is what the iOS dot gets from its wall-clock `TimelineView`.
11
+ * `performance.now()` and the document timeline share one origin, and jsdom has no timeline.
12
+ */
13
+ export declare function startRunningLoop(targets: RunningLoopTargets): () => void;
14
+ export {};
@@ -0,0 +1,22 @@
1
+ import { animate } from 'motion/react';
2
+ import { runningIndicator } from './config.js';
3
+ const periodMs = runningIndicator.transition.duration * 1000;
4
+ /**
5
+ * Starts the running loop on the given elements and returns a function that stops it. Each loop is
6
+ * moved to the position the page clock gives it (time 0 is the start of the page), not the moment
7
+ * it mounted, so every running dot on screen shows the same frame at the same instant, however
8
+ * late or often it mounts. That is what the iOS dot gets from its wall-clock `TimelineView`.
9
+ * `performance.now()` and the document timeline share one origin, and jsdom has no timeline.
10
+ */
11
+ export function startRunningLoop(targets) {
12
+ const { core, halo, dot } = targets;
13
+ const controls = [
14
+ core && animate(core, runningIndicator.core, runningIndicator.transition),
15
+ halo && animate(halo, runningIndicator.halo, runningIndicator.transition),
16
+ dot && animate(dot, runningIndicator.dot, runningIndicator.transition),
17
+ ].filter((control) => control != null);
18
+ const seconds = (performance.now() % periodMs) / 1000;
19
+ for (const control of controls)
20
+ control.time = seconds;
21
+ return () => controls.forEach((control) => control.cancel());
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamsai/ui",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Streams design system: shadcn/ui components on Base UI, design tokens (Tailwind v4) and a theme provider for React 19.",
5
5
  "license": "MIT",
6
6
  "repository": {