From c400b738a55952ad4c50b850150800ff60da0171 Mon Sep 17 00:00:00 2001 From: 0xPratik Date: Thu, 17 Sep 2026 22:36:25 +0545 Subject: [PATCH 1/3] Add Silk, Strata, and Echo thinking indicators --- README.md | 16 +++++ package.json | 8 +++ src/index.ts | 2 + src/theme.css | 23 +++++++ src/ui/thinking-indicator.tsx | 18 +++++ src/ui/thinking-mark.tsx | 67 +++++++++++++++++++ .../primitives/thinking-indicator.stories.tsx | 35 ++++++++++ 7 files changed, 169 insertions(+) create mode 100644 src/ui/thinking-indicator.tsx create mode 100644 src/ui/thinking-mark.tsx create mode 100644 stories/primitives/thinking-indicator.stories.tsx diff --git a/README.md b/README.md index 103f62e3..8a075988 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,22 @@ bun run stories # Ladle workbench A new component is not done until it has at least one story per meaningful state, checked in both themes. See [CONTRIBUTING.md](./CONTRIBUTING.md). +## Thinking indicator + +The Corbits logo has three motion variants: `silk` (default), `strata`, and `echo`. + +```tsx +import { ThinkingIndicator, ThinkingMark } from "@corbits/react-ui"; + + + + +``` + +`ThinkingIndicator` supplies an accessible status label; `ThinkingMark` is decorative. +Both accept `variant`, `paused`, `className`, and `style`. Reduced-motion users see a +static highlight instead. + ## License LGPL-2.1-only. See [LICENSE](./LICENSE). diff --git a/package.json b/package.json index 3129f69a..362c44c2 100644 --- a/package.json +++ b/package.json @@ -503,6 +503,14 @@ "types": "./dist/ui/theme-toggle.d.ts", "default": "./dist/ui/theme-toggle.js" }, + "./ui/thinking-indicator": { + "types": "./dist/ui/thinking-indicator.d.ts", + "default": "./dist/ui/thinking-indicator.js" + }, + "./ui/thinking-mark": { + "types": "./dist/ui/thinking-mark.d.ts", + "default": "./dist/ui/thinking-mark.js" + }, "./ui/time-series-chart": { "types": "./dist/ui/time-series-chart.d.ts", "default": "./dist/ui/time-series-chart.js" diff --git a/src/index.ts b/src/index.ts index 2d6a55d8..0801b2fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -111,6 +111,8 @@ export * from "./ui/tabs.js"; export * from "./ui/textarea.js"; export * from "./ui/theme-provider.js"; export * from "./ui/theme-toggle.js"; +export * from "./ui/thinking-indicator.js"; +export * from "./ui/thinking-mark.js"; export * from "./ui/time-series-chart.js"; export * from "./ui/toast.js"; export * from "./ui/token-mosaic.js"; diff --git a/src/theme.css b/src/theme.css index 04dd236f..aa608fd4 100644 --- a/src/theme.css +++ b/src/theme.css @@ -533,3 +533,26 @@ outline-offset: 2px; } } + +/* Ridge-study motion: keep geometry and timing in the original 100 x 70 space. */ +@keyframes corbits-thinking-sweep { + 0% { transform: translateX(8px); opacity: 0; } + 8%, 86% { opacity: 1; } + 100% { transform: translateX(130px); opacity: 0; } +} +@keyframes corbits-thinking-rise { + 0% { transform: translateY(88px); } + 100% { transform: translateY(-30px); } +} +@keyframes corbits-thinking-wave { + 0% { transform: scale(.03); opacity: 0; } + 10% { opacity: 1; } + 75% { opacity: .9; } + 100% { transform: scale(1.35); opacity: 0; } +} +@media (prefers-reduced-motion: reduce) { + .corbits-thinking-silk, .corbits-thinking-strata, .corbits-thinking-echo { animation: none; } + .corbits-thinking-silk { transform: translateX(44px); } + .corbits-thinking-strata { transform: translateY(30px); } + .corbits-thinking-echo { transform: scale(.6); } +} diff --git a/src/ui/thinking-indicator.tsx b/src/ui/thinking-indicator.tsx new file mode 100644 index 00000000..1f22b367 --- /dev/null +++ b/src/ui/thinking-indicator.tsx @@ -0,0 +1,18 @@ +import { cn } from "../lib/utils.js"; +import { ThinkingMark, type ThinkingMarkProps } from "./thinking-mark.js"; + +export interface ThinkingIndicatorProps extends ThinkingMarkProps { + /** Visible status text, also announced by assistive technology. */ + label?: string; +} + +/** A thinking status with Silk, Strata, or Echo motion. Defaults to Silk. */ +export function ThinkingIndicator({ label = "Thinking...", className, style, ...markProps }: ThinkingIndicatorProps) { + return ( + + {/* The mountain carries more visual weight below its SVG center. */} + + {label} + + ); +} diff --git a/src/ui/thinking-mark.tsx b/src/ui/thinking-mark.tsx new file mode 100644 index 00000000..fa34a505 --- /dev/null +++ b/src/ui/thinking-mark.tsx @@ -0,0 +1,67 @@ +import { useId, type CSSProperties } from "react"; +import { cn } from "../lib/utils.js"; +import { CORBITS_MARK_PATH } from "./corbits-mark.js"; + +export type ThinkingVariant = "silk" | "strata" | "echo"; + +export interface ThinkingMarkProps { + variant?: ThinkingVariant; + paused?: boolean; + className?: string; + style?: CSSProperties; +} + +/** Decorative animated Corbits mark. Pair with visible text or ThinkingIndicator. */ +export function ThinkingMark({ variant = "silk", paused = false, className, style }: ThinkingMarkProps) { + const id = useId(); + const gradient = `${id}-gradient`; + const mask = `${id}-mask`; + return ( + + ); +} diff --git a/stories/primitives/thinking-indicator.stories.tsx b/stories/primitives/thinking-indicator.stories.tsx new file mode 100644 index 00000000..e42f40b5 --- /dev/null +++ b/stories/primitives/thinking-indicator.stories.tsx @@ -0,0 +1,35 @@ +import { useState } from "react"; +import { ThinkingIndicator } from "../../src/ui/thinking-indicator.js"; +import { ThinkingMark, type ThinkingVariant } from "../../src/ui/thinking-mark.js"; +import { Button } from "../../src/ui/button.js"; + +export default { title: "Primitives / Thinking indicator" }; + +const variants: ThinkingVariant[] = ["silk", "strata", "echo"]; + +export const AllVariants = () => { + const [paused, setPaused] = useState(false); + return ( +
+ +
+ {variants.map((variant) => ( +
+

{variant}

+
+ +
+ +
+ ))} +
+
+ ); +}; + +export const Silk = () => ; +export const Strata = () => ; +export const Echo = () => ; +export const CustomLabel = () => ; From 5ff5ac899f722af5777a0b6663b38b586014e9a1 Mon Sep 17 00:00:00 2001 From: 0xPratik Date: Mon, 21 Sep 2026 20:31:31 +0545 Subject: [PATCH 2/3] Use theme tokens for thinking indicators --- src/theme.css | 3 +++ src/ui/thinking-indicator.test.tsx | 25 +++++++++++++++++++++++++ src/ui/thinking-mark.tsx | 4 ++-- 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 src/ui/thinking-indicator.test.tsx diff --git a/src/theme.css b/src/theme.css index aa608fd4..87254684 100644 --- a/src/theme.css +++ b/src/theme.css @@ -69,6 +69,7 @@ --secondary-foreground: #2b2627; --muted: #eceff1; --muted-foreground: #5c5555; + --thinking-base: #77716a; --accent: #c5d2de; --accent-foreground: #2b2627; --success: #c1d1be; @@ -138,6 +139,7 @@ --secondary-foreground: #e4d5bc; --muted: #1a1718; --muted-foreground: #a99c86; + --thinking-base: #aaa39a; --accent: #2d455c; --accent-foreground: #e4d5bc; --success: #425a3d; @@ -238,6 +240,7 @@ --color-secondary-foreground: var(--secondary-foreground); --color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground); + --color-thinking-base: var(--thinking-base); --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-success: var(--success); diff --git a/src/ui/thinking-indicator.test.tsx b/src/ui/thinking-indicator.test.tsx new file mode 100644 index 00000000..6702eaf1 --- /dev/null +++ b/src/ui/thinking-indicator.test.tsx @@ -0,0 +1,25 @@ +import { describe, expect, test } from "bun:test"; +import { createElement } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; + +import { ThinkingIndicator } from "./thinking-indicator.js"; +import { ThinkingMark } from "./thinking-mark.js"; + +describe("ThinkingIndicator", () => { + test("renders an accessible status with a decorative Silk mark", () => { + const html = renderToStaticMarkup(createElement(ThinkingIndicator)); + + expect(html).toContain('role="status"'); + expect(html).toContain("Thinking..."); + expect(html).toContain("aria-hidden=\"true\""); + expect(html).toContain("corbits-thinking-silk"); + }); + + test("renders every motion variant", () => { + for (const variant of ["silk", "strata", "echo"] as const) { + const html = renderToStaticMarkup(createElement(ThinkingMark, { variant })); + + expect(html).toContain(`corbits-thinking-${variant}`); + } + }); +}); diff --git a/src/ui/thinking-mark.tsx b/src/ui/thinking-mark.tsx index fa34a505..a861dc0a 100644 --- a/src/ui/thinking-mark.tsx +++ b/src/ui/thinking-mark.tsx @@ -56,10 +56,10 @@ export function ThinkingMark({ variant = "silk", paused = false, className, styl } - + - + From 83c0323a410e7bb765d573df73b68ee51ef727db Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Mon, 21 Sep 2026 11:54:33 -0700 Subject: [PATCH 3/3] fix(thinking): in-shorthand echo delay; paused and reduced-motion tests --- src/theme.css | 3 +++ src/ui/thinking-indicator.test.tsx | 12 ++++++++++++ src/ui/thinking-mark.tsx | 4 +++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/theme.css b/src/theme.css index 87254684..d27c648b 100644 --- a/src/theme.css +++ b/src/theme.css @@ -554,6 +554,9 @@ 100% { transform: scale(1.35); opacity: 0; } } @media (prefers-reduced-motion: reduce) { + /* Static poses land the gradient mid-stroke. These offsets mirror the SVG + geometry in thinking-mark.tsx — change the rect/circle geometry there and + these poses have to be re-picked to match. */ .corbits-thinking-silk, .corbits-thinking-strata, .corbits-thinking-echo { animation: none; } .corbits-thinking-silk { transform: translateX(44px); } .corbits-thinking-strata { transform: translateY(30px); } diff --git a/src/ui/thinking-indicator.test.tsx b/src/ui/thinking-indicator.test.tsx index 6702eaf1..dd4d521b 100644 --- a/src/ui/thinking-indicator.test.tsx +++ b/src/ui/thinking-indicator.test.tsx @@ -22,4 +22,16 @@ describe("ThinkingIndicator", () => { expect(html).toContain(`corbits-thinking-${variant}`); } }); + + test("paused freezes every animated node", () => { + const html = renderToStaticMarkup(createElement(ThinkingMark, { paused: true })); + + expect(html).toContain("animation-play-state:paused"); + }); + + test("echo hides its second wave under reduced motion", () => { + const html = renderToStaticMarkup(createElement(ThinkingMark, { variant: "echo" })); + + expect(html).toContain("motion-reduce:hidden"); + }); }); diff --git a/src/ui/thinking-mark.tsx b/src/ui/thinking-mark.tsx index a861dc0a..f405f49e 100644 --- a/src/ui/thinking-mark.tsx +++ b/src/ui/thinking-mark.tsx @@ -52,7 +52,9 @@ export function ThinkingMark({ variant = "silk", paused = false, className, styl {variant === "strata" && } {variant === "echo" && <> - + {/* The delay lives inside the shorthand — a separate animation-delay + utility can be reset by the shorthand's own emission order. */} + }