fireworks-ai 0.2.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.

Potentially problematic release.


This version of fireworks-ai might be problematic. Click here for more details.

Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +320 -0
  3. package/dist/react/AgentProvider.d.ts +15 -0
  4. package/dist/react/AgentProvider.js +30 -0
  5. package/dist/react/ChatInput.d.ts +6 -0
  6. package/dist/react/ChatInput.js +21 -0
  7. package/dist/react/CollapsibleCard.d.ts +9 -0
  8. package/dist/react/CollapsibleCard.js +8 -0
  9. package/dist/react/MessageList.d.ts +3 -0
  10. package/dist/react/MessageList.js +29 -0
  11. package/dist/react/StatusDot.d.ts +5 -0
  12. package/dist/react/StatusDot.js +12 -0
  13. package/dist/react/TextMessage.d.ts +5 -0
  14. package/dist/react/TextMessage.js +13 -0
  15. package/dist/react/ThinkingIndicator.d.ts +3 -0
  16. package/dist/react/ThinkingIndicator.js +5 -0
  17. package/dist/react/ToolCallCard.d.ts +5 -0
  18. package/dist/react/ToolCallCard.js +33 -0
  19. package/dist/react/cn.d.ts +2 -0
  20. package/dist/react/cn.js +5 -0
  21. package/dist/react/index.d.ts +13 -0
  22. package/dist/react/index.js +12 -0
  23. package/dist/react/registry.d.ts +4 -0
  24. package/dist/react/registry.js +10 -0
  25. package/dist/react/registry.test.d.ts +1 -0
  26. package/dist/react/registry.test.js +26 -0
  27. package/dist/react/store.d.ts +28 -0
  28. package/dist/react/store.js +109 -0
  29. package/dist/react/store.test.d.ts +1 -0
  30. package/dist/react/store.test.js +113 -0
  31. package/dist/react/use-agent.d.ts +11 -0
  32. package/dist/react/use-agent.js +96 -0
  33. package/dist/server/index.d.ts +5 -0
  34. package/dist/server/index.js +4 -0
  35. package/dist/server/push-channel.d.ts +8 -0
  36. package/dist/server/push-channel.js +40 -0
  37. package/dist/server/push-channel.test.d.ts +1 -0
  38. package/dist/server/push-channel.test.js +57 -0
  39. package/dist/server/router.d.ts +8 -0
  40. package/dist/server/router.js +52 -0
  41. package/dist/server/session.d.ts +32 -0
  42. package/dist/server/session.js +73 -0
  43. package/dist/server/translator.d.ts +14 -0
  44. package/dist/server/translator.js +151 -0
  45. package/dist/server/translator.test.d.ts +1 -0
  46. package/dist/server/translator.test.js +156 -0
  47. package/dist/types.d.ts +39 -0
  48. package/dist/types.js +1 -0
  49. package/package.json +69 -0
  50. package/src/theme.css +133 -0
@@ -0,0 +1,73 @@
1
+ import { query } from "@anthropic-ai/claude-agent-sdk";
2
+ import { PushChannel } from "./push-channel.js";
3
+ function userMessage(content) {
4
+ return {
5
+ type: "user",
6
+ message: { role: "user", content },
7
+ parent_tool_use_id: null,
8
+ session_id: "",
9
+ };
10
+ }
11
+ const DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
12
+ export class SessionManager {
13
+ sessions = new Map();
14
+ factory;
15
+ idleTimeoutMs;
16
+ constructor(factory, idleTimeoutMs) {
17
+ this.factory = factory;
18
+ this.idleTimeoutMs = idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
19
+ }
20
+ create() {
21
+ const id = crypto.randomUUID();
22
+ const init = this.factory();
23
+ const channel = new PushChannel();
24
+ const abortController = new AbortController();
25
+ const messageIterator = query({
26
+ prompt: channel,
27
+ options: {
28
+ systemPrompt: init.systemPrompt,
29
+ model: init.model,
30
+ tools: init.tools ?? [],
31
+ mcpServers: init.mcpServers,
32
+ allowedTools: init.allowedTools,
33
+ maxTurns: init.maxTurns ?? 200,
34
+ permissionMode: init.permissionMode ?? "bypassPermissions",
35
+ includePartialMessages: true,
36
+ abortController,
37
+ },
38
+ });
39
+ const session = {
40
+ id,
41
+ context: init.context,
42
+ pushMessage: (text) => {
43
+ session.lastActivityAt = Date.now();
44
+ channel.push(userMessage(text));
45
+ },
46
+ messageIterator,
47
+ abort: () => abortController.abort(),
48
+ createdAt: Date.now(),
49
+ lastActivityAt: Date.now(),
50
+ };
51
+ this.sessions.set(id, session);
52
+ return session;
53
+ }
54
+ get(id) {
55
+ return this.sessions.get(id);
56
+ }
57
+ delete(id) {
58
+ const session = this.sessions.get(id);
59
+ if (session) {
60
+ session.abort();
61
+ this.sessions.delete(id);
62
+ }
63
+ }
64
+ cleanup() {
65
+ const now = Date.now();
66
+ for (const [id, session] of this.sessions) {
67
+ if (now - session.lastActivityAt > this.idleTimeoutMs) {
68
+ session.abort();
69
+ this.sessions.delete(id);
70
+ }
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,14 @@
1
+ import type { CustomEvent, SSEEvent } from "../types.js";
2
+ import type { Session } from "./session.js";
3
+ export interface TranslatorConfig<TCtx> {
4
+ onToolResult?: (toolName: string, result: string, session: Session<TCtx>) => CustomEvent[];
5
+ }
6
+ export declare class MessageTranslator<TCtx> {
7
+ private config;
8
+ private toolNames;
9
+ constructor(config?: TranslatorConfig<TCtx>);
10
+ translate(message: Record<string, unknown>, session: Session<TCtx>): SSEEvent[];
11
+ private lastToolId;
12
+ }
13
+ export declare function sseEncode(evt: SSEEvent): string;
14
+ export declare function streamSession<TCtx>(session: Session<TCtx>, translator: MessageTranslator<TCtx>): AsyncGenerator<SSEEvent>;
@@ -0,0 +1,151 @@
1
+ export class MessageTranslator {
2
+ config;
3
+ toolNames = new Map();
4
+ constructor(config) {
5
+ this.config = config ?? {};
6
+ }
7
+ translate(message, session) {
8
+ const events = [];
9
+ const type = message.type;
10
+ switch (type) {
11
+ case "stream_event": {
12
+ if (!("event" in message))
13
+ break;
14
+ const event = message.event;
15
+ switch (event.type) {
16
+ case "message_start": {
17
+ events.push({ event: "message_start", data: "{}" });
18
+ break;
19
+ }
20
+ case "content_block_start": {
21
+ const block = event.content_block;
22
+ if (block?.type === "tool_use" && typeof block.id === "string") {
23
+ const name = block.name;
24
+ this.toolNames.set(block.id, name);
25
+ events.push({
26
+ event: "tool_start",
27
+ data: JSON.stringify({ id: block.id, name }),
28
+ });
29
+ }
30
+ break;
31
+ }
32
+ case "content_block_delta": {
33
+ const delta = event.delta;
34
+ if (delta.type === "text_delta" && typeof delta.text === "string") {
35
+ events.push({
36
+ event: "text_delta",
37
+ data: JSON.stringify({ text: delta.text }),
38
+ });
39
+ }
40
+ else if (delta.type === "input_json_delta" &&
41
+ typeof delta.partial_json === "string") {
42
+ const id = this.lastToolId();
43
+ if (id) {
44
+ events.push({
45
+ event: "tool_input_delta",
46
+ data: JSON.stringify({ id, partialJson: delta.partial_json }),
47
+ });
48
+ }
49
+ }
50
+ break;
51
+ }
52
+ }
53
+ break;
54
+ }
55
+ case "assistant": {
56
+ const msg = message.message;
57
+ if (msg?.content) {
58
+ for (const block of msg.content) {
59
+ if (block.type === "tool_use") {
60
+ const name = block.name;
61
+ const id = block.id;
62
+ this.toolNames.set(id, name);
63
+ events.push({
64
+ event: "tool_call",
65
+ data: JSON.stringify({ id, name, input: block.input }),
66
+ });
67
+ }
68
+ }
69
+ }
70
+ break;
71
+ }
72
+ case "user": {
73
+ const msg = message.message;
74
+ if (Array.isArray(msg?.content)) {
75
+ for (const block of msg.content) {
76
+ if (block.type === "tool_result") {
77
+ const text = extractToolResultText(block);
78
+ const toolName = this.toolNames.get(block.tool_use_id);
79
+ events.push({
80
+ event: "tool_result",
81
+ data: JSON.stringify({ toolUseId: block.tool_use_id, result: text }),
82
+ });
83
+ if (this.config.onToolResult && toolName) {
84
+ for (const c of this.config.onToolResult(toolName, text, session)) {
85
+ events.push({ event: "custom", data: JSON.stringify(c) });
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+ break;
92
+ }
93
+ case "tool_progress": {
94
+ events.push({
95
+ event: "tool_progress",
96
+ data: JSON.stringify({
97
+ toolName: message.tool_name,
98
+ elapsed: message.elapsed_time_seconds,
99
+ }),
100
+ });
101
+ break;
102
+ }
103
+ case "result": {
104
+ if (message.subtype === "success") {
105
+ events.push({
106
+ event: "turn_complete",
107
+ data: JSON.stringify({
108
+ numTurns: message.num_turns ?? 0,
109
+ cost: message.total_cost_usd ?? 0,
110
+ }),
111
+ });
112
+ }
113
+ else {
114
+ events.push({
115
+ event: "session_error",
116
+ data: JSON.stringify({ subtype: message.subtype }),
117
+ });
118
+ }
119
+ break;
120
+ }
121
+ }
122
+ return events;
123
+ }
124
+ lastToolId() {
125
+ const entries = [...this.toolNames.entries()];
126
+ return entries.length > 0 ? entries[entries.length - 1][0] : undefined;
127
+ }
128
+ }
129
+ export function sseEncode(evt) {
130
+ return `event: ${evt.event}\ndata: ${evt.data}\n\n`;
131
+ }
132
+ export async function* streamSession(session, translator) {
133
+ for await (const message of session.messageIterator) {
134
+ const events = translator.translate(message, session);
135
+ for (const evt of events) {
136
+ yield evt;
137
+ }
138
+ }
139
+ }
140
+ function extractToolResultText(block) {
141
+ const content = block.content;
142
+ if (typeof content === "string")
143
+ return content;
144
+ if (Array.isArray(content)) {
145
+ return content
146
+ .filter((p) => p.type === "text" && typeof p.text === "string")
147
+ .map((p) => p.text)
148
+ .join("");
149
+ }
150
+ return "";
151
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,156 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { MessageTranslator, sseEncode } from "./translator.js";
3
+ function stubSession(ctx = {}) {
4
+ return {
5
+ id: "sess-1",
6
+ context: ctx,
7
+ pushMessage: () => { },
8
+ messageIterator: (async function* () { })(),
9
+ abort: () => { },
10
+ createdAt: Date.now(),
11
+ lastActivityAt: Date.now(),
12
+ };
13
+ }
14
+ describe("MessageTranslator", () => {
15
+ const session = stubSession();
16
+ it("emits message_start on stream_event/message_start", () => {
17
+ const t = new MessageTranslator();
18
+ const events = t.translate({ type: "stream_event", event: { type: "message_start" } }, session);
19
+ expect(events).toEqual([{ event: "message_start", data: "{}" }]);
20
+ });
21
+ it("emits tool_start on content_block_start with tool_use", () => {
22
+ const t = new MessageTranslator();
23
+ const events = t.translate({
24
+ type: "stream_event",
25
+ event: {
26
+ type: "content_block_start",
27
+ content_block: { type: "tool_use", id: "tool-1", name: "search" },
28
+ },
29
+ }, session);
30
+ expect(events).toEqual([
31
+ { event: "tool_start", data: JSON.stringify({ id: "tool-1", name: "search" }) },
32
+ ]);
33
+ });
34
+ it("emits text_delta on content_block_delta with text_delta", () => {
35
+ const t = new MessageTranslator();
36
+ const events = t.translate({
37
+ type: "stream_event",
38
+ event: { type: "content_block_delta", delta: { type: "text_delta", text: "hello" } },
39
+ }, session);
40
+ expect(events).toEqual([{ event: "text_delta", data: JSON.stringify({ text: "hello" }) }]);
41
+ });
42
+ it("emits tool_input_delta on content_block_delta with input_json_delta", () => {
43
+ const t = new MessageTranslator();
44
+ // First register a tool so lastToolId works
45
+ t.translate({
46
+ type: "stream_event",
47
+ event: {
48
+ type: "content_block_start",
49
+ content_block: { type: "tool_use", id: "tool-2", name: "read" },
50
+ },
51
+ }, session);
52
+ const events = t.translate({
53
+ type: "stream_event",
54
+ event: {
55
+ type: "content_block_delta",
56
+ delta: { type: "input_json_delta", partial_json: '{"q":' },
57
+ },
58
+ }, session);
59
+ expect(events).toEqual([
60
+ {
61
+ event: "tool_input_delta",
62
+ data: JSON.stringify({ id: "tool-2", partialJson: '{"q":' }),
63
+ },
64
+ ]);
65
+ });
66
+ it("emits tool_call on assistant message with tool_use blocks", () => {
67
+ const t = new MessageTranslator();
68
+ const events = t.translate({
69
+ type: "assistant",
70
+ message: {
71
+ content: [{ type: "tool_use", id: "t-1", name: "fetch", input: { url: "https://x" } }],
72
+ },
73
+ }, session);
74
+ expect(events).toEqual([
75
+ {
76
+ event: "tool_call",
77
+ data: JSON.stringify({ id: "t-1", name: "fetch", input: { url: "https://x" } }),
78
+ },
79
+ ]);
80
+ });
81
+ it("emits tool_result on user message with tool_result blocks", () => {
82
+ const t = new MessageTranslator();
83
+ // Register tool name first
84
+ t.translate({
85
+ type: "assistant",
86
+ message: { content: [{ type: "tool_use", id: "t-2", name: "calc", input: {} }] },
87
+ }, session);
88
+ const events = t.translate({
89
+ type: "user",
90
+ message: {
91
+ role: "user",
92
+ content: [{ type: "tool_result", tool_use_id: "t-2", content: "42" }],
93
+ },
94
+ }, session);
95
+ expect(events).toEqual([
96
+ { event: "tool_result", data: JSON.stringify({ toolUseId: "t-2", result: "42" }) },
97
+ ]);
98
+ });
99
+ it("wraps onToolResult return values as custom SSE events", () => {
100
+ const t = new MessageTranslator({
101
+ onToolResult: (toolName, result) => [{ name: "data_updated", value: { toolName, result } }],
102
+ });
103
+ // Register tool name
104
+ t.translate({
105
+ type: "assistant",
106
+ message: { content: [{ type: "tool_use", id: "t-3", name: "save", input: {} }] },
107
+ }, session);
108
+ const events = t.translate({
109
+ type: "user",
110
+ message: {
111
+ role: "user",
112
+ content: [{ type: "tool_result", tool_use_id: "t-3", content: "ok" }],
113
+ },
114
+ }, session);
115
+ expect(events).toHaveLength(2);
116
+ expect(events[0]).toEqual({
117
+ event: "tool_result",
118
+ data: JSON.stringify({ toolUseId: "t-3", result: "ok" }),
119
+ });
120
+ expect(events[1]).toEqual({
121
+ event: "custom",
122
+ data: JSON.stringify({ name: "data_updated", value: { toolName: "save", result: "ok" } }),
123
+ });
124
+ });
125
+ it("emits tool_progress", () => {
126
+ const t = new MessageTranslator();
127
+ const events = t.translate({ type: "tool_progress", tool_name: "build", elapsed_time_seconds: 5.2 }, session);
128
+ expect(events).toEqual([
129
+ { event: "tool_progress", data: JSON.stringify({ toolName: "build", elapsed: 5.2 }) },
130
+ ]);
131
+ });
132
+ it("emits turn_complete on success result", () => {
133
+ const t = new MessageTranslator();
134
+ const events = t.translate({ type: "result", subtype: "success", num_turns: 3, total_cost_usd: 0.05 }, session);
135
+ expect(events).toEqual([
136
+ { event: "turn_complete", data: JSON.stringify({ numTurns: 3, cost: 0.05 }) },
137
+ ]);
138
+ });
139
+ it("emits session_error on non-success result", () => {
140
+ const t = new MessageTranslator();
141
+ const events = t.translate({ type: "result", subtype: "max_turns" }, session);
142
+ expect(events).toEqual([
143
+ { event: "session_error", data: JSON.stringify({ subtype: "max_turns" }) },
144
+ ]);
145
+ });
146
+ it("returns empty array for unknown message types", () => {
147
+ const t = new MessageTranslator();
148
+ const events = t.translate({ type: "unknown_type" }, session);
149
+ expect(events).toEqual([]);
150
+ });
151
+ });
152
+ describe("sseEncode", () => {
153
+ it("formats as SSE with event and data fields", () => {
154
+ expect(sseEncode({ event: "text_delta", data: '{"text":"hi"}' })).toBe('event: text_delta\ndata: {"text":"hi"}\n\n');
155
+ });
156
+ });
@@ -0,0 +1,39 @@
1
+ import type { ComponentType } from "react";
2
+ export interface SSEEvent {
3
+ event: string;
4
+ data: string;
5
+ }
6
+ export interface CustomEvent<T = unknown> {
7
+ name: string;
8
+ value: T;
9
+ }
10
+ export type ToolCallPhase = "pending" | "streaming_input" | "running" | "complete" | "error";
11
+ export interface ToolCallInfo {
12
+ id: string;
13
+ name: string;
14
+ input: Record<string, unknown>;
15
+ partialInput?: string;
16
+ result?: string;
17
+ error?: string;
18
+ status: ToolCallPhase;
19
+ }
20
+ export interface ChatMessage {
21
+ id: string;
22
+ role: "user" | "assistant" | "system";
23
+ content: string;
24
+ toolCalls?: ToolCallInfo[];
25
+ }
26
+ export interface WidgetProps<TResult = unknown> {
27
+ phase: ToolCallPhase;
28
+ toolUseId: string;
29
+ input: Record<string, unknown>;
30
+ partialInput?: string;
31
+ result?: TResult;
32
+ error?: string;
33
+ }
34
+ export interface WidgetRegistration<TResult = unknown> {
35
+ toolName: string;
36
+ label: string;
37
+ richLabel?: (result: TResult) => string | null;
38
+ component: ComponentType<WidgetProps<TResult>>;
39
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "fireworks-ai",
3
+ "version": "0.2.0",
4
+ "description": "React + Hono toolkit for building chat UIs on top of the Claude Agent SDK",
5
+ "license": "MIT",
6
+ "author": "Dan Leeper",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/quantumleeps/fireworks-ai.git"
10
+ },
11
+ "homepage": "https://github.com/quantumleeps/fireworks-ai#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/quantumleeps/fireworks-ai/issues"
14
+ },
15
+ "keywords": [
16
+ "claude",
17
+ "agent-sdk",
18
+ "react",
19
+ "hono",
20
+ "sse",
21
+ "chat-ui",
22
+ "anthropic"
23
+ ],
24
+ "sideEffects": false,
25
+ "type": "module",
26
+ "exports": {
27
+ "./server": {
28
+ "types": "./dist/server/index.d.ts",
29
+ "import": "./dist/server/index.js"
30
+ },
31
+ "./react": {
32
+ "types": "./dist/react/index.d.ts",
33
+ "import": "./dist/react/index.js"
34
+ },
35
+ "./theme.css": "./src/theme.css"
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src/theme.css"
40
+ ],
41
+ "dependencies": {
42
+ "clsx": "^2.1.1",
43
+ "tailwind-merge": "^3.4.0"
44
+ },
45
+ "peerDependencies": {
46
+ "@anthropic-ai/claude-agent-sdk": ">=0.2.0",
47
+ "hono": ">=4.0.0",
48
+ "react": ">=18.0.0",
49
+ "react-markdown": ">=10.0.0",
50
+ "zustand": ">=5.0.0",
51
+ "immer": ">=10.0.0"
52
+ },
53
+ "devDependencies": {
54
+ "@anthropic-ai/claude-agent-sdk": "latest",
55
+ "@biomejs/biome": "^2.3.14",
56
+ "@types/react": "^19.2.7",
57
+ "hono": "^4.11.9",
58
+ "immer": "^11.1.4",
59
+ "react": "^19.2.0",
60
+ "typescript": "~5.9.3",
61
+ "vitest": "^3.0.0",
62
+ "zustand": "^5.0.11"
63
+ },
64
+ "scripts": {
65
+ "build": "tsc",
66
+ "check": "biome check src/",
67
+ "test": "vitest run"
68
+ }
69
+ }
package/src/theme.css ADDED
@@ -0,0 +1,133 @@
1
+ /*
2
+ * fireworks-ai/theme.css — default theme for fireworks-ai components.
3
+ *
4
+ * Provides the CSS variable bridge (via @theme inline) and a neutral color
5
+ * palette so fireworks-ai components render correctly out of the box.
6
+ *
7
+ * If your app already uses shadcn/ui, you DON'T need this file — your existing
8
+ * shadcn/tailwind.css and color variables are already compatible. Only import
9
+ * this if you're NOT using shadcn.
10
+ *
11
+ * Usage:
12
+ * @import "fireworks-ai/theme.css";
13
+ */
14
+
15
+ /* -- Source scanning ------------------------------------------------------- */
16
+ /* Tells Tailwind to scan fireworks-ai's component source for utility classes. */
17
+
18
+ @source "./react";
19
+
20
+ /* -- Tailwind v4 variable bridge ------------------------------------------ */
21
+ /* Maps CSS custom properties to Tailwind utility classes. */
22
+ /* Same mappings as shadcn/tailwind.css — if you have that, skip this file. */
23
+
24
+ @theme inline {
25
+ --radius-sm: calc(var(--radius) - 4px);
26
+ --radius-md: calc(var(--radius) - 2px);
27
+ --radius-lg: var(--radius);
28
+ --radius-xl: calc(var(--radius) + 4px);
29
+ --color-background: var(--background);
30
+ --color-foreground: var(--foreground);
31
+ --color-card: var(--card);
32
+ --color-card-foreground: var(--card-foreground);
33
+ --color-popover: var(--popover);
34
+ --color-popover-foreground: var(--popover-foreground);
35
+ --color-primary: var(--primary);
36
+ --color-primary-foreground: var(--primary-foreground);
37
+ --color-secondary: var(--secondary);
38
+ --color-secondary-foreground: var(--secondary-foreground);
39
+ --color-muted: var(--muted);
40
+ --color-muted-foreground: var(--muted-foreground);
41
+ --color-accent: var(--accent);
42
+ --color-accent-foreground: var(--accent-foreground);
43
+ --color-destructive: var(--destructive);
44
+ --color-border: var(--border);
45
+ --color-input: var(--input);
46
+ --color-ring: var(--ring);
47
+ }
48
+
49
+ /* -- Light mode (default) ------------------------------------------------- */
50
+
51
+ :root {
52
+ --radius: 0.625rem;
53
+ --background: oklch(1 0 0);
54
+ --foreground: oklch(0.145 0 0);
55
+ --card: oklch(1 0 0);
56
+ --card-foreground: oklch(0.145 0 0);
57
+ --popover: oklch(1 0 0);
58
+ --popover-foreground: oklch(0.145 0 0);
59
+ --primary: oklch(0.205 0 0);
60
+ --primary-foreground: oklch(0.985 0 0);
61
+ --secondary: oklch(0.97 0 0);
62
+ --secondary-foreground: oklch(0.205 0 0);
63
+ --muted: oklch(0.97 0 0);
64
+ --muted-foreground: oklch(0.556 0 0);
65
+ --accent: oklch(0.97 0 0);
66
+ --accent-foreground: oklch(0.205 0 0);
67
+ --destructive: oklch(0.577 0.245 27.325);
68
+ --border: oklch(0.922 0 0);
69
+ --input: oklch(0.922 0 0);
70
+ --ring: oklch(0.708 0 0);
71
+ }
72
+
73
+ /* -- Dark mode (class-based) ---------------------------------------------- */
74
+
75
+ .dark {
76
+ --background: oklch(0.145 0 0);
77
+ --foreground: oklch(0.985 0 0);
78
+ --card: oklch(0.205 0 0);
79
+ --card-foreground: oklch(0.985 0 0);
80
+ --popover: oklch(0.205 0 0);
81
+ --popover-foreground: oklch(0.985 0 0);
82
+ --primary: oklch(0.922 0 0);
83
+ --primary-foreground: oklch(0.205 0 0);
84
+ --secondary: oklch(0.269 0 0);
85
+ --secondary-foreground: oklch(0.985 0 0);
86
+ --muted: oklch(0.269 0 0);
87
+ --muted-foreground: oklch(0.708 0 0);
88
+ --accent: oklch(0.269 0 0);
89
+ --accent-foreground: oklch(0.985 0 0);
90
+ --destructive: oklch(0.704 0.191 22.216);
91
+ --border: oklch(1 0 0 / 10%);
92
+ --input: oklch(1 0 0 / 15%);
93
+ --ring: oklch(0.556 0 0);
94
+ }
95
+
96
+ /* -- Dark mode (system preference) ---------------------------------------- */
97
+ /* Activates automatically unless .light is set on :root. */
98
+
99
+ @media (prefers-color-scheme: dark) {
100
+ :root:not(.light) {
101
+ --background: oklch(0.145 0 0);
102
+ --foreground: oklch(0.985 0 0);
103
+ --card: oklch(0.205 0 0);
104
+ --card-foreground: oklch(0.985 0 0);
105
+ --popover: oklch(0.205 0 0);
106
+ --popover-foreground: oklch(0.985 0 0);
107
+ --primary: oklch(0.922 0 0);
108
+ --primary-foreground: oklch(0.205 0 0);
109
+ --secondary: oklch(0.269 0 0);
110
+ --secondary-foreground: oklch(0.985 0 0);
111
+ --muted: oklch(0.269 0 0);
112
+ --muted-foreground: oklch(0.708 0 0);
113
+ --accent: oklch(0.269 0 0);
114
+ --accent-foreground: oklch(0.985 0 0);
115
+ --destructive: oklch(0.704 0.191 22.216);
116
+ --border: oklch(1 0 0 / 10%);
117
+ --input: oklch(1 0 0 / 15%);
118
+ --ring: oklch(0.556 0 0);
119
+ }
120
+ }
121
+
122
+ /* -- Base resets ---------------------------------------------------------- */
123
+
124
+ *,
125
+ *::before,
126
+ *::after {
127
+ border-color: var(--border);
128
+ }
129
+
130
+ body {
131
+ background-color: var(--background);
132
+ color: var(--foreground);
133
+ }