@elabs-ai/components-ai 4.0.0
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/LICENSE +21 -0
- package/README.md +133 -0
- package/dist/_audio-player-media-chrome-KA5DY54G.js +81 -0
- package/dist/_audio-player-media-chrome-KA5DY54G.js.map +1 -0
- package/dist/_flow-boundary-D63PJ65S.js +186 -0
- package/dist/_flow-boundary-D63PJ65S.js.map +1 -0
- package/dist/_interactive-terminal-xterm-ATJ5EW3G.js +11 -0
- package/dist/_interactive-terminal-xterm-ATJ5EW3G.js.map +1 -0
- package/dist/_persona-rive-RFR2EUWP.js +164 -0
- package/dist/_persona-rive-RFR2EUWP.js.map +1 -0
- package/dist/index.d.ts +4079 -0
- package/dist/index.js +11770 -0
- package/dist/index.js.map +1 -0
- package/package.json +87 -0
- package/src/_audio-player-media-chrome.tsx +146 -0
- package/src/_chat-shell-rail.tsx +18 -0
- package/src/_code-block-theme.test.ts +134 -0
- package/src/_code-block-theme.ts +184 -0
- package/src/_flow-boundary.tsx +237 -0
- package/src/_flow-lazy.ts +34 -0
- package/src/_interactive-terminal-xterm.ts +32 -0
- package/src/_lazy-mermaid.test.ts +101 -0
- package/src/_lazy-mermaid.ts +117 -0
- package/src/_persona-rive.tsx +252 -0
- package/src/_streamdown-i18n.ts +119 -0
- package/src/agent-timeline.stories.tsx +66 -0
- package/src/agent-timeline.test.tsx +74 -0
- package/src/agent-timeline.tsx +106 -0
- package/src/agent.tsx +113 -0
- package/src/agentic-workspace.stories.tsx +1099 -0
- package/src/artifact.stories.tsx +62 -0
- package/src/artifact.test.tsx +112 -0
- package/src/artifact.tsx +167 -0
- package/src/asset-preview.stories.tsx +100 -0
- package/src/asset-preview.test.tsx +126 -0
- package/src/asset-preview.tsx +218 -0
- package/src/attachments.tsx +393 -0
- package/src/audio-player.stories.tsx +99 -0
- package/src/audio-player.tsx +170 -0
- package/src/blocks-ai-composer.stories.tsx +83 -0
- package/src/canvas.stories.tsx +142 -0
- package/src/canvas.tsx +29 -0
- package/src/chain-of-thought.tsx +193 -0
- package/src/chat-greeting.stories.tsx +96 -0
- package/src/chat-greeting.test.tsx +91 -0
- package/src/chat-greeting.tsx +91 -0
- package/src/chat-shell.stories.tsx +123 -0
- package/src/chat-shell.test.tsx +113 -0
- package/src/chat-shell.tsx +97 -0
- package/src/chat.stories.tsx +99 -0
- package/src/checkpoint.tsx +54 -0
- package/src/code-block.stories.tsx +81 -0
- package/src/code-block.test.tsx +191 -0
- package/src/code-block.tsx +673 -0
- package/src/commit.tsx +343 -0
- package/src/composer.stories.tsx +188 -0
- package/src/composer.test.tsx +182 -0
- package/src/composer.tsx +202 -0
- package/src/confirmation.stories.tsx +61 -0
- package/src/confirmation.test.tsx +136 -0
- package/src/confirmation.tsx +273 -0
- package/src/connection.tsx +23 -0
- package/src/context-panel.stories.tsx +137 -0
- package/src/context-panel.test.tsx +198 -0
- package/src/context-panel.tsx +609 -0
- package/src/context.stories.tsx +16 -0
- package/src/context.tsx +363 -0
- package/src/controls.tsx +20 -0
- package/src/conversation.stories.tsx +26 -0
- package/src/conversation.test.tsx +123 -0
- package/src/conversation.tsx +143 -0
- package/src/dark-theme-variant.test.ts +47 -0
- package/src/edge.tsx +33 -0
- package/src/environment-variables.tsx +304 -0
- package/src/file-tree.stories.tsx +55 -0
- package/src/file-tree.test.tsx +101 -0
- package/src/file-tree.tsx +390 -0
- package/src/gallery.stories.tsx +182 -0
- package/src/gallery.test.tsx +150 -0
- package/src/gallery.tsx +737 -0
- package/src/grouped-parts.stories.tsx +104 -0
- package/src/grouped-parts.test.tsx +167 -0
- package/src/grouped-parts.tsx +247 -0
- package/src/image.stories.tsx +67 -0
- package/src/image.test.tsx +105 -0
- package/src/image.tsx +115 -0
- package/src/index.ts +93 -0
- package/src/inline-citation.stories.tsx +91 -0
- package/src/inline-citation.test.tsx +96 -0
- package/src/inline-citation.tsx +314 -0
- package/src/interactive-terminal.stories.tsx +165 -0
- package/src/interactive-terminal.test.tsx +448 -0
- package/src/interactive-terminal.tsx +444 -0
- package/src/jsx-preview.stories.tsx +265 -0
- package/src/jsx-preview.test.tsx +75 -0
- package/src/jsx-preview.tsx +409 -0
- package/src/markdown-view.stories.tsx +57 -0
- package/src/markdown-view.test.tsx +36 -0
- package/src/markdown-view.tsx +113 -0
- package/src/message-edit.stories.tsx +150 -0
- package/src/message-edit.test.tsx +115 -0
- package/src/message-edit.tsx +336 -0
- package/src/message-feedback.stories.tsx +53 -0
- package/src/message-feedback.test.tsx +62 -0
- package/src/message-feedback.tsx +117 -0
- package/src/message-form-spec.ts +349 -0
- package/src/message-form.stories.tsx +182 -0
- package/src/message-form.test.tsx +227 -0
- package/src/message-form.tsx +845 -0
- package/src/message-table-spec.ts +281 -0
- package/src/message-table.stories.tsx +176 -0
- package/src/message-table.test.tsx +144 -0
- package/src/message-table.tsx +366 -0
- package/src/message.stories.tsx +262 -0
- package/src/message.test.tsx +508 -0
- package/src/message.tsx +619 -0
- package/src/mic-selector.tsx +339 -0
- package/src/microcopy.test.tsx +123 -0
- package/src/model-selector.stories.tsx +73 -0
- package/src/model-selector.test.tsx +55 -0
- package/src/model-selector.tsx +225 -0
- package/src/motion-config.tsx +49 -0
- package/src/node.tsx +71 -0
- package/src/open-in-chat.tsx +340 -0
- package/src/package-info.tsx +205 -0
- package/src/panel.tsx +20 -0
- package/src/part-groups.ts +258 -0
- package/src/persona-sources.ts +59 -0
- package/src/persona.stories.tsx +111 -0
- package/src/persona.test.tsx +67 -0
- package/src/persona.tsx +148 -0
- package/src/plan.tsx +135 -0
- package/src/prompt-input.stories.tsx +261 -0
- package/src/prompt-input.test.tsx +392 -0
- package/src/prompt-input.tsx +1668 -0
- package/src/queue.tsx +237 -0
- package/src/reasoning.stories.tsx +81 -0
- package/src/reasoning.test.tsx +50 -0
- package/src/reasoning.tsx +229 -0
- package/src/sandbox.stories.tsx +62 -0
- package/src/sandbox.test.tsx +42 -0
- package/src/sandbox.tsx +149 -0
- package/src/schema-display.tsx +412 -0
- package/src/selection-toolbar.stories.tsx +86 -0
- package/src/selection-toolbar.test.tsx +89 -0
- package/src/selection-toolbar.tsx +192 -0
- package/src/shimmer.stories.tsx +10 -0
- package/src/shimmer.tsx +81 -0
- package/src/snippet.stories.tsx +19 -0
- package/src/snippet.test.tsx +116 -0
- package/src/snippet.tsx +138 -0
- package/src/sources.stories.tsx +34 -0
- package/src/sources.test.tsx +129 -0
- package/src/sources.tsx +93 -0
- package/src/speech-input.tsx +312 -0
- package/src/stack-trace.tsx +479 -0
- package/src/streamdown-i18n.test.tsx +163 -0
- package/src/suggestion.stories.tsx +75 -0
- package/src/suggestion.test.tsx +72 -0
- package/src/suggestion.tsx +120 -0
- package/src/task.stories.tsx +22 -0
- package/src/task.test.tsx +36 -0
- package/src/task.tsx +89 -0
- package/src/templates-ai-assistant.stories.tsx +142 -0
- package/src/terminal.tsx +244 -0
- package/src/test-results.stories.tsx +177 -0
- package/src/test-results.test.tsx +241 -0
- package/src/test-results.tsx +408 -0
- package/src/tool-result-card.stories.tsx +143 -0
- package/src/tool-result-card.test.tsx +72 -0
- package/src/tool-result-card.tsx +86 -0
- package/src/tool.stories.tsx +60 -0
- package/src/tool.test.tsx +63 -0
- package/src/tool.tsx +235 -0
- package/src/toolbar.tsx +44 -0
- package/src/transcription.tsx +118 -0
- package/src/voice-selector.tsx +469 -0
- package/src/web-preview.stories.tsx +67 -0
- package/src/web-preview.test.tsx +117 -0
- package/src/web-preview.tsx +283 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { Conversation, ConversationContent, ConversationScrollButton } from "./conversation";
|
|
4
|
+
import { Message, MessageContent, MessageResponse } from "./message";
|
|
5
|
+
import { Composer } from "./composer";
|
|
6
|
+
import { Reasoning, ReasoningContent, ReasoningTrigger } from "./reasoning";
|
|
7
|
+
import { Source, Sources, SourcesContent, SourcesTrigger } from "./sources";
|
|
8
|
+
|
|
9
|
+
interface Msg {
|
|
10
|
+
id: string;
|
|
11
|
+
role: "user" | "assistant";
|
|
12
|
+
text: string;
|
|
13
|
+
reasoning?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const initial: Msg[] = [
|
|
17
|
+
{ id: "1", role: "user", text: "What changed in last week's deploys?" },
|
|
18
|
+
{
|
|
19
|
+
id: "2",
|
|
20
|
+
role: "assistant",
|
|
21
|
+
text: "Three services shipped this week. **Billing** is currently degraded — elevated p95 latency after the last rollout.",
|
|
22
|
+
reasoning:
|
|
23
|
+
"Queried CI for the last 7 days, grouped by service, then cross-referenced the rollback log to flag regressions.",
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function ChatExample() {
|
|
28
|
+
const [messages, setMessages] = useState<Msg[]>(initial);
|
|
29
|
+
const [status, setStatus] = useState<"ready" | "submitted">("ready");
|
|
30
|
+
|
|
31
|
+
const send = (text: string) => {
|
|
32
|
+
setMessages((m) => [...m, { id: String(m.length + 1), role: "user", text }]);
|
|
33
|
+
setStatus("submitted");
|
|
34
|
+
setTimeout(() => {
|
|
35
|
+
setMessages((m) => [
|
|
36
|
+
...m,
|
|
37
|
+
{
|
|
38
|
+
id: String(m.length + 1),
|
|
39
|
+
role: "assistant",
|
|
40
|
+
text: "Three services shipped; billing is degraded.",
|
|
41
|
+
},
|
|
42
|
+
]);
|
|
43
|
+
setStatus("ready");
|
|
44
|
+
}, 600);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="mx-auto flex h-[640px] max-w-2xl flex-col overflow-hidden rounded-xl border">
|
|
49
|
+
<Conversation className="flex-1">
|
|
50
|
+
<ConversationContent>
|
|
51
|
+
{messages.map((m) => (
|
|
52
|
+
<Message from={m.role} key={m.id}>
|
|
53
|
+
<MessageContent>
|
|
54
|
+
{m.reasoning ? (
|
|
55
|
+
<Reasoning>
|
|
56
|
+
<ReasoningTrigger />
|
|
57
|
+
<ReasoningContent>{m.reasoning}</ReasoningContent>
|
|
58
|
+
</Reasoning>
|
|
59
|
+
) : null}
|
|
60
|
+
<MessageResponse>{m.text}</MessageResponse>
|
|
61
|
+
{m.role === "assistant" ? (
|
|
62
|
+
<Sources>
|
|
63
|
+
<SourcesTrigger count={2} />
|
|
64
|
+
<SourcesContent>
|
|
65
|
+
<Source href="https://example.com" title="Deploy log — wk 23" />
|
|
66
|
+
<Source href="https://example.com" title="Billing runbook" />
|
|
67
|
+
</SourcesContent>
|
|
68
|
+
</Sources>
|
|
69
|
+
) : null}
|
|
70
|
+
</MessageContent>
|
|
71
|
+
</Message>
|
|
72
|
+
))}
|
|
73
|
+
</ConversationContent>
|
|
74
|
+
<ConversationScrollButton />
|
|
75
|
+
</Conversation>
|
|
76
|
+
|
|
77
|
+
<Composer
|
|
78
|
+
className="m-3 mt-0"
|
|
79
|
+
placeholder="Ask about deploys…"
|
|
80
|
+
status={status === "submitted" ? "Generating…" : "Awaiting your input"}
|
|
81
|
+
sendStatus={status}
|
|
82
|
+
onSubmit={(message) => {
|
|
83
|
+
const text = message.text?.trim();
|
|
84
|
+
if (text) send(text);
|
|
85
|
+
}}
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const meta = {
|
|
92
|
+
title: "AI/Chat",
|
|
93
|
+
component: ChatExample,
|
|
94
|
+
parameters: { layout: "fullscreen" },
|
|
95
|
+
} satisfies Meta<typeof ChatExample>;
|
|
96
|
+
export default meta;
|
|
97
|
+
type Story = StoryObj<typeof meta>;
|
|
98
|
+
|
|
99
|
+
export const Default: Story = {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@elabs-ai/components-ui";
|
|
4
|
+
import { Separator } from "@elabs-ai/components-ui";
|
|
5
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "@elabs-ai/components-ui";
|
|
6
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
7
|
+
import type { LucideProps } from "lucide-react";
|
|
8
|
+
import { BookmarkIcon } from "lucide-react";
|
|
9
|
+
import type { ComponentProps, HTMLAttributes } from "react";
|
|
10
|
+
|
|
11
|
+
export type CheckpointProps = HTMLAttributes<HTMLDivElement>;
|
|
12
|
+
|
|
13
|
+
export const Checkpoint = ({ className, children, ...props }: CheckpointProps) => (
|
|
14
|
+
<div
|
|
15
|
+
className={cn("flex items-center gap-0.5 overflow-hidden text-muted-foreground", className)}
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
<Separator />
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export type CheckpointIconProps = LucideProps;
|
|
24
|
+
|
|
25
|
+
export const CheckpointIcon = ({ className, children, ...props }: CheckpointIconProps) =>
|
|
26
|
+
children ?? <BookmarkIcon className={cn("size-4 shrink-0", className)} {...props} />;
|
|
27
|
+
|
|
28
|
+
export type CheckpointTriggerProps = ComponentProps<typeof Button> & {
|
|
29
|
+
tooltip?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const CheckpointTrigger = ({
|
|
33
|
+
children,
|
|
34
|
+
variant = "ghost",
|
|
35
|
+
size = "sm",
|
|
36
|
+
tooltip,
|
|
37
|
+
...props
|
|
38
|
+
}: CheckpointTriggerProps) =>
|
|
39
|
+
tooltip ? (
|
|
40
|
+
<Tooltip>
|
|
41
|
+
<TooltipTrigger asChild>
|
|
42
|
+
<Button size={size} type="button" variant={variant} {...props}>
|
|
43
|
+
{children}
|
|
44
|
+
</Button>
|
|
45
|
+
</TooltipTrigger>
|
|
46
|
+
<TooltipContent align="start" side="bottom">
|
|
47
|
+
{tooltip}
|
|
48
|
+
</TooltipContent>
|
|
49
|
+
</Tooltip>
|
|
50
|
+
) : (
|
|
51
|
+
<Button size={size} type="button" variant={variant} {...props}>
|
|
52
|
+
{children}
|
|
53
|
+
</Button>
|
|
54
|
+
);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { CodeBlock } from "./code-block";
|
|
3
|
+
const meta = {
|
|
4
|
+
title: "AI/CodeBlock",
|
|
5
|
+
component: CodeBlock,
|
|
6
|
+
parameters: { layout: "padded" },
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
} satisfies Meta<typeof CodeBlock>;
|
|
9
|
+
export default meta;
|
|
10
|
+
type Story = StoryObj<typeof meta>;
|
|
11
|
+
// A representative mix of comment/keyword/type/string/number/function scopes
|
|
12
|
+
// (#315) — so the derived-from-tokens Shiki theme is visually checkable across
|
|
13
|
+
// both themes (light, dark) via the toolbar's global
|
|
14
|
+
// theme switcher, not just a single flat color.
|
|
15
|
+
const SAMPLE_CODE = [
|
|
16
|
+
"// Adds two numbers together",
|
|
17
|
+
"export function add(a: number, b: number): number {",
|
|
18
|
+
' const label = "sum";',
|
|
19
|
+
" return a + b; // 42",
|
|
20
|
+
"}",
|
|
21
|
+
].join("\n");
|
|
22
|
+
|
|
23
|
+
export const Default: Story = {
|
|
24
|
+
render: () => (
|
|
25
|
+
<div className="max-w-lg">
|
|
26
|
+
<CodeBlock code={SAMPLE_CODE} language="tsx" />
|
|
27
|
+
</div>
|
|
28
|
+
),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Dark theme — verifies the derived-from-tokens Shiki theme in dark
|
|
32
|
+
// (#315). Also proves the region-scoped fix: a `<div data-theme="dark">`
|
|
33
|
+
// decorator (the same pattern as `message-table.stories.tsx`'s `DarkTheme`)
|
|
34
|
+
// resolves ITS OWN theme rather than always the document root's.
|
|
35
|
+
export const DarkTheme: Story = {
|
|
36
|
+
render: () => (
|
|
37
|
+
<div className="max-w-lg">
|
|
38
|
+
<CodeBlock code={SAMPLE_CODE} language="tsx" />
|
|
39
|
+
</div>
|
|
40
|
+
),
|
|
41
|
+
decorators: [
|
|
42
|
+
(Story) => (
|
|
43
|
+
<div data-theme="dark" className="rounded-lg bg-background p-6 text-foreground">
|
|
44
|
+
<Story />
|
|
45
|
+
</div>
|
|
46
|
+
),
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Markup (JSX tags) — exercises the `--code-tag` role (#315 follow-up). A
|
|
51
|
+
// valid <div>/<span> tag must read as its OWN color, never the same as
|
|
52
|
+
// destructive/warning text — the collision a prior revision had (`--code-tag`
|
|
53
|
+
// aliased to `--calc-warning`, so a normal tag looked identical to an error).
|
|
54
|
+
const MARKUP_CODE = [
|
|
55
|
+
"export function Card({ label, count }: CardProps) {",
|
|
56
|
+
" return (",
|
|
57
|
+
' <div className="card" data-count={count}>',
|
|
58
|
+
" <span>{label}</span>",
|
|
59
|
+
" </div>",
|
|
60
|
+
" );",
|
|
61
|
+
"}",
|
|
62
|
+
].join("\n");
|
|
63
|
+
|
|
64
|
+
export const MarkupTags: Story = {
|
|
65
|
+
render: () => (
|
|
66
|
+
<div className="max-w-lg">
|
|
67
|
+
<CodeBlock code={MARKUP_CODE} language="tsx" />
|
|
68
|
+
</div>
|
|
69
|
+
),
|
|
70
|
+
};
|
|
71
|
+
// STREAMING — code is arriving incrementally (loading-states.md `isStreaming`).
|
|
72
|
+
// The partial code keeps rendering (build-up, NOT a skeleton — it isn't
|
|
73
|
+
// fetch-then-show); a `Shimmer` "Generating…" cue is the in-progress affordance.
|
|
74
|
+
export const Streaming: Story = {
|
|
75
|
+
name: "isStreaming",
|
|
76
|
+
render: () => (
|
|
77
|
+
<div className="max-w-lg">
|
|
78
|
+
<CodeBlock code={"export function add(a: number, b"} language="tsx" isStreaming />
|
|
79
|
+
</div>
|
|
80
|
+
),
|
|
81
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type { CSSProperties } from "react";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { render, waitFor } from "@testing-library/react";
|
|
7
|
+
import { CodeBlock } from "./code-block";
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
document.documentElement.removeAttribute("data-theme");
|
|
13
|
+
document.documentElement.style.removeProperty("--code-background");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("CodeBlock", () => {
|
|
17
|
+
// #315 — locks the fix: the highlighter theme must be derived from brand
|
|
18
|
+
// `--code-*` tokens, never Shiki's bundled `github-light`/`github-dark`.
|
|
19
|
+
it("never hardcodes shiki's github-light/github-dark theme (#315)", () => {
|
|
20
|
+
const source = readFileSync(join(__dirname, "code-block.tsx"), "utf8");
|
|
21
|
+
expect(source).not.toMatch(/github-light|github-dark/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("resolves the highlighted <pre> background from the active theme's --code-background token (#315)", async () => {
|
|
25
|
+
document.documentElement.setAttribute("data-theme", "light");
|
|
26
|
+
document.documentElement.style.setProperty("--code-background", "oklch(1 0 0)");
|
|
27
|
+
|
|
28
|
+
const { container } = render(<CodeBlock code="const a = 1;" language="tsx" />);
|
|
29
|
+
|
|
30
|
+
await waitFor(() => {
|
|
31
|
+
const pre = container.querySelector("pre");
|
|
32
|
+
expect(pre?.style.backgroundColor).toBe("rgb(255, 255, 255)");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// #315 follow-up — a CodeBlock nested inside a region-scoped
|
|
37
|
+
// `<div data-theme="…">` (a supported ThemeProvider/decorator pattern, see
|
|
38
|
+
// @.claude/rules/theming.md and 6 other @…-ai story files) must resolve
|
|
39
|
+
// THAT region's `--code-*` tokens, not always the document root's.
|
|
40
|
+
it("resolves --code-* tokens from a region-scoped data-theme ancestor, not the document root's", async () => {
|
|
41
|
+
document.documentElement.setAttribute("data-theme", "light");
|
|
42
|
+
document.documentElement.style.setProperty("--code-background", "oklch(1 0 0)");
|
|
43
|
+
|
|
44
|
+
const { container } = render(
|
|
45
|
+
<div
|
|
46
|
+
data-theme="dark"
|
|
47
|
+
style={{ "--code-background": "oklch(0.25 0.005 75)" } as CSSProperties}
|
|
48
|
+
>
|
|
49
|
+
<CodeBlock code="const a = 1;" language="tsx" />
|
|
50
|
+
</div>,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
await waitFor(() => {
|
|
54
|
+
const pre = container.querySelector("pre");
|
|
55
|
+
// rgb(35, 33, 31) is oklch(0.25 0.005 75) converted to sRGB — the
|
|
56
|
+
// SCOPED dark region's color, never the document root's white.
|
|
57
|
+
expect(pre?.style.backgroundColor).toBe("rgb(35, 33, 31)");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// #315 regression lock — `highlightCode` returns the tokenized result
|
|
62
|
+
// SYNCHRONOUSLY on a cache hit, WITHOUT invoking the subscribed callback.
|
|
63
|
+
// Once the "tsx" highlighter is warm (e.g. after the FIRST theme's
|
|
64
|
+
// highlight), a theme switch re-tokenizes fast enough to resolve as a cache
|
|
65
|
+
// hit before `CodeBlockContent`'s effect re-subscribes — a component that
|
|
66
|
+
// only reacted to the callback would get stuck showing the un-highlighted
|
|
67
|
+
// raw fallback forever after every theme change after the first.
|
|
68
|
+
it("re-highlights with the new theme's colors after data-theme changes at runtime (#315)", async () => {
|
|
69
|
+
document.documentElement.setAttribute("data-theme", "light");
|
|
70
|
+
document.documentElement.style.setProperty("--code-background", "oklch(1 0 0)");
|
|
71
|
+
document.documentElement.style.setProperty("--code-keyword", "oklch(0.38 0.16 264)");
|
|
72
|
+
|
|
73
|
+
const { container } = render(<CodeBlock code="const value = 1;" language="tsx" />);
|
|
74
|
+
|
|
75
|
+
// Initial highlight settles on the light colors.
|
|
76
|
+
await waitFor(() => {
|
|
77
|
+
const pre = container.querySelector("pre");
|
|
78
|
+
expect(pre?.style.backgroundColor).toBe("rgb(255, 255, 255)");
|
|
79
|
+
});
|
|
80
|
+
const spansBefore = container.querySelectorAll("code > span > span");
|
|
81
|
+
expect(spansBefore.length).toBeGreaterThan(1); // real per-token spans, not the 1-span raw fallback
|
|
82
|
+
|
|
83
|
+
// Switch to dark at runtime (ThemeProvider / Storybook's theme
|
|
84
|
+
// decorator both just flip `data-theme`) with a DIFFERENT resolved value.
|
|
85
|
+
document.documentElement.setAttribute("data-theme", "dark");
|
|
86
|
+
document.documentElement.style.setProperty("--code-background", "oklch(0.25 0.005 75)");
|
|
87
|
+
document.documentElement.style.setProperty("--code-keyword", "oklch(0.7 0.16 264)");
|
|
88
|
+
|
|
89
|
+
await waitFor(() => {
|
|
90
|
+
const pre = container.querySelector("pre");
|
|
91
|
+
// rgb(35, 33, 31) is oklch(0.25 0.005 75) converted to sRGB.
|
|
92
|
+
expect(pre?.style.backgroundColor).toBe("rgb(35, 33, 31)");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// The re-highlight actually re-tokenized (still real per-token spans, not
|
|
96
|
+
// stuck on the single-span raw fallback the bug would strand it on).
|
|
97
|
+
const spansAfter = container.querySelectorAll("code > span > span");
|
|
98
|
+
expect(spansAfter.length).toBeGreaterThan(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// #315 regression lock — the blocker: narrowing the attribute to a validated
|
|
102
|
+
// theme name collapses "no `data-theme` attribute yet" (the
|
|
103
|
+
// pre-`ThemeProvider`-mount render) and an EXPLICIT `data-theme="light"` into
|
|
104
|
+
// the SAME value (both fall back to `DEFAULT_THEME`). `:root`'s `--code-*`
|
|
105
|
+
// fallback values are their own
|
|
106
|
+
// distinct placeholder palette, not a copy of `light`'s — so if the
|
|
107
|
+
// highlight cache were keyed on the validated name, a code block that first
|
|
108
|
+
// tokenizes before `data-theme` is set would cache under the SAME key
|
|
109
|
+
// `ThemeProvider` later writes explicitly, and the (correct) light
|
|
110
|
+
// colors would never take effect. This locks the fix: the cache is keyed on
|
|
111
|
+
// the RAW attribute, so the mutation is a genuine re-tokenize.
|
|
112
|
+
it("re-tokenizes when data-theme transitions from unset to an explicit value resolving to the same theme name (#315)", async () => {
|
|
113
|
+
// No data-theme attribute set — the pre-mount state. Simulates :root's
|
|
114
|
+
// fallback --code-background differing from light's own. Uses a code
|
|
115
|
+
// string unique to this test — the highlight cache is module-level and
|
|
116
|
+
// keyed (in part) on the code content, so reusing another test's snippet
|
|
117
|
+
// here would collide with an ALREADY-cached "light" entry from that
|
|
118
|
+
// other test and mask this exact regression.
|
|
119
|
+
document.documentElement.style.setProperty("--code-background", "oklch(0.5 0 0)");
|
|
120
|
+
|
|
121
|
+
const { container } = render(
|
|
122
|
+
<CodeBlock code="const scopeKeyRegressionMarker = 315;" language="tsx" />,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
await waitFor(() => {
|
|
126
|
+
const pre = container.querySelector("pre");
|
|
127
|
+
expect(pre?.style.backgroundColor).toBe("rgb(99, 99, 99)");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ThemeProvider mounts and writes data-theme="light" explicitly, with
|
|
131
|
+
// a DIFFERENT --code-background than the unset-attribute render resolved.
|
|
132
|
+
document.documentElement.setAttribute("data-theme", "light");
|
|
133
|
+
document.documentElement.style.setProperty("--code-background", "oklch(0.1 0 0)");
|
|
134
|
+
|
|
135
|
+
await waitFor(() => {
|
|
136
|
+
const pre = container.querySelector("pre");
|
|
137
|
+
// Must pick up the NEW color — never stuck on the unset-attribute cache entry.
|
|
138
|
+
expect(pre?.style.backgroundColor).toBe("rgb(3, 3, 3)");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("renders the code content", () => {
|
|
143
|
+
const { container } = render(<CodeBlock code="const a = 1;" language="tsx" />);
|
|
144
|
+
expect(container.textContent).toContain("const a = 1;");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("soft-wraps long lines when `wrap` is set (#5)", () => {
|
|
148
|
+
const { container } = render(
|
|
149
|
+
<CodeBlock code="a very long single line of code" language="tsx" wrap />,
|
|
150
|
+
);
|
|
151
|
+
const pre = container.querySelector("pre");
|
|
152
|
+
expect(pre?.className).toContain("whitespace-pre-wrap");
|
|
153
|
+
expect(pre?.className).toContain("break-words");
|
|
154
|
+
// No horizontal-scroll affordance when wrapping (there's nothing to scroll).
|
|
155
|
+
const scroller = container.querySelector(".overflow-auto");
|
|
156
|
+
expect(scroller?.className ?? "").not.toContain("scrollbar-width:thin");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("does not wrap by default, and shows a scroll affordance (#5)", () => {
|
|
160
|
+
const { container } = render(
|
|
161
|
+
<CodeBlock code="a very long single line of code" language="tsx" />,
|
|
162
|
+
);
|
|
163
|
+
const pre = container.querySelector("pre");
|
|
164
|
+
expect(pre?.className).not.toContain("whitespace-pre-wrap");
|
|
165
|
+
// Default mode keeps horizontal scroll with a discoverable thin scrollbar.
|
|
166
|
+
const scroller = container.querySelector(".overflow-auto");
|
|
167
|
+
expect(scroller).not.toBeNull();
|
|
168
|
+
expect(scroller?.className).toContain("scrollbar-width:thin");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("CodeBlock isStreaming (#269, loading-states.md)", () => {
|
|
173
|
+
it("keeps rendering the partial code (build-up), not a skeleton", () => {
|
|
174
|
+
const { container } = render(<CodeBlock code="const partial = " language="tsx" isStreaming />);
|
|
175
|
+
expect(container.textContent).toContain("const partial =");
|
|
176
|
+
expect(container.querySelector(".animate-pulse")).toBeNull();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("shows exactly one in-progress live region while streaming", () => {
|
|
180
|
+
const { container } = render(<CodeBlock code="const a = 1;" language="tsx" isStreaming />);
|
|
181
|
+
const statuses = container.querySelectorAll('[role="status"]');
|
|
182
|
+
expect(statuses).toHaveLength(1);
|
|
183
|
+
expect(statuses[0]).toHaveAttribute("aria-live", "polite");
|
|
184
|
+
expect(statuses[0]).toHaveTextContent("Generating…");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("shows no in-progress cue when not streaming", () => {
|
|
188
|
+
const { container } = render(<CodeBlock code="const a = 1;" language="tsx" />);
|
|
189
|
+
expect(container.querySelector('[role="status"]')).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
});
|