@ocai/app-sdk-ui 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ocean AI, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,9 @@
1
+ import { ComponentProps } from 'react';
2
+ import { Button } from '../internal/button';
3
+ export type ActionsProps = ComponentProps<"div">;
4
+ export declare const Actions: ({ className, children, ...props }: ActionsProps) => import("react").JSX.Element;
5
+ export type ActionProps = ComponentProps<typeof Button> & {
6
+ tooltip?: string;
7
+ label?: string;
8
+ };
9
+ export declare const Action: ({ tooltip, children, label, className, variant, size, ...props }: ActionProps) => import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { HTMLAttributes } from 'react';
2
+ export type MessageContentVariant = "user" | "assistant";
3
+ export type MessageContentProps = HTMLAttributes<HTMLDivElement> & {
4
+ variant?: MessageContentVariant;
5
+ };
6
+ export declare const MessageContent: ({ children, className, variant, ...props }: MessageContentProps) => import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { ComponentProps } from 'react';
2
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../internal/collapsible';
3
+ export type ReasoningProps = ComponentProps<typeof Collapsible> & {
4
+ isStreaming?: boolean;
5
+ duration?: number;
6
+ open?: boolean;
7
+ defaultOpen?: boolean;
8
+ onOpenChange?: (open: boolean) => void;
9
+ };
10
+ export declare const Reasoning: import('react').MemoExoticComponent<({ className, isStreaming, duration, open, defaultOpen, onOpenChange, children, ...props }: ReasoningProps) => import("react").JSX.Element>;
11
+ export type ReasoningTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
12
+ variant?: "reasoning" | "tools";
13
+ label?: string;
14
+ };
15
+ export declare const ReasoningTrigger: import('react').MemoExoticComponent<({ className, children, variant, label, ...props }: ReasoningTriggerProps) => import("react").JSX.Element>;
16
+ export declare const reasoningTextClassName = "grid gap-1 text-sm text-muted-foreground/80 **:text-sm [&_code]:bg-transparent [&_code]:p-0 [&_code]:font-semibold [&_code]:text-foreground [&_li]:my-0 [&_ol]:my-1 [&_p]:my-0 [&_ul]:my-1";
17
+ export declare const ReasoningContent: import('react').MemoExoticComponent<({ className, ...props }: ComponentProps<typeof CollapsibleContent>) => import("react").JSX.Element>;
@@ -0,0 +1,5 @@
1
+ import { ComponentProps } from 'react';
2
+ import { Streamdown } from 'streamdown';
3
+ type ResponseProps = ComponentProps<typeof Streamdown>;
4
+ export declare function Response({ className, ...props }: ResponseProps): import("react").JSX.Element;
5
+ export {};
@@ -0,0 +1,23 @@
1
+ import { ToolUIPart } from 'ai';
2
+ import { ComponentProps, ReactNode } from 'react';
3
+ import { Collapsible, CollapsibleContent } from '../internal/collapsible';
4
+ export type ToolProps = ComponentProps<typeof Collapsible>;
5
+ export declare const Tool: ({ className, ...props }: ToolProps) => import("react").JSX.Element;
6
+ export type ToolHeaderProps = {
7
+ type: ToolUIPart["type"];
8
+ state: ToolUIPart["state"];
9
+ title?: string;
10
+ className?: string;
11
+ };
12
+ export declare const ToolHeader: ({ className, type, state, title, ...props }: ToolHeaderProps) => import("react").JSX.Element;
13
+ export type ToolContentProps = ComponentProps<typeof CollapsibleContent>;
14
+ export declare const ToolContent: ({ className, ...props }: ToolContentProps) => import("react").JSX.Element;
15
+ export type ToolInputProps = ComponentProps<"div"> & {
16
+ input: ToolUIPart["input"];
17
+ };
18
+ export declare const ToolInput: ({ className, input, ...props }: ToolInputProps) => import("react").JSX.Element;
19
+ export type ToolOutputProps = ComponentProps<"div"> & {
20
+ output: ReactNode;
21
+ errorText: ToolUIPart["errorText"];
22
+ };
23
+ export declare const ToolOutput: ({ className, output, errorText, ...props }: ToolOutputProps) => import("react").JSX.Element | null;
package/dist/chat.css ADDED
@@ -0,0 +1,75 @@
1
+ /* @ocai/app-sdk-ui/chat.css — AI-chat markdown → Notion/Rune-block look
2
+ (prototype: tables + code shell).
3
+
4
+ Consume by importing INSIDE your Tailwind v4 entry stylesheet, after
5
+ tokens.css, so the @apply rules below are compiled by the app's own
6
+ Tailwind pass against that app's theme tokens:
7
+
8
+ @import "tailwindcss";
9
+ @import "@ocai/app-sdk-ui/tokens.css";
10
+ @import "@ocai/app-sdk-ui/chat.css";
11
+
12
+ A plain <link> or JS-side import will NOT work — Tailwind must see the
13
+ @apply rules to resolve text-sm and friends.
14
+
15
+ Streamdown tags every rendered element with `data-streamdown="<name>"` as
16
+ its styling hook (no shadow DOM, no separate stylesheet), so we restyle
17
+ purely in CSS. Scoped to `.ocui-md`, which this package's Response
18
+ component (chat/response.tsx) sets on the Streamdown root; the compound
19
+ `.ocui-md [data-streamdown=…]` selector (specificity 0,2,0) out-specifies
20
+ Streamdown's own single-class utilities within this same `utilities`
21
+ layer. We touch the table grid and the code-block SHELL (border + header)
22
+ only — the shiki-highlighted code body is left alone so syntax colors
23
+ survive. */
24
+ @layer utilities {
25
+ /* --- Tables: Notion-style bordered grid (header row tinted, 1px cell
26
+ borders, per-column min/max width, horizontal scroll on overflow) --- */
27
+ /* Streamdown nests the <table> in TWO "card" wrappers: the outer
28
+ `table-wrapper` (rounded-lg border bg-sidebar p-2 — hosted the now-removed
29
+ controls) AND an inner MarkdownTable div (rounded-md border bg-background)
30
+ that directly wraps the <table>. The inner one carries NO data-streamdown
31
+ hook, so we target it as "the div directly containing the table" via :has.
32
+ Flatten both cards (controls already off via controls={{ table: false }})
33
+ so only the 1px cell grid shows; the outer keeps the my-4 block spacing. */
34
+ .ocui-md [data-streamdown="table-wrapper"] {
35
+ @apply my-4 w-full gap-0 rounded-none border-0 bg-transparent p-0;
36
+ }
37
+ .ocui-md div:has(> [data-streamdown="table"]) {
38
+ @apply w-full overflow-x-auto rounded-none border-0 bg-transparent;
39
+ }
40
+ .ocui-md [data-streamdown="table"] {
41
+ @apply w-full border-collapse rounded-none text-sm;
42
+ }
43
+ /* Match the rune editor's native table palette exactly. rune-react declares
44
+ --rune-table-header-bg (5% fg) and --rune-table-border (10% fg) only inside
45
+ .rune-editor — out of reach in chat — so we mirror the same recipe off the
46
+ global --foreground / --background seeds, which keeps it identical to the
47
+ editor table in BOTH light and dark (bg-muted drifted to 15% in dark). */
48
+ .ocui-md [data-streamdown="table-header"] {
49
+ @apply bg-transparent;
50
+ }
51
+ .ocui-md [data-streamdown="table-header-cell"],
52
+ .ocui-md [data-streamdown="table-cell"] {
53
+ border: 1px solid color-mix(in oklab, var(--foreground) 10%, var(--background));
54
+ @apply px-3 py-1.5 text-left align-top;
55
+ min-width: 120px;
56
+ max-width: 280px;
57
+ }
58
+ .ocui-md [data-streamdown="table-header-cell"] {
59
+ background: color-mix(in oklab, var(--foreground) 5%, var(--background));
60
+ @apply font-medium text-foreground;
61
+ }
62
+
63
+ /* --- Code block: Rune-style shell only. Container gets the rounded border;
64
+ the header strip (language label + copy/download) gets a tinted bar; the
65
+ `code-block-body` (shiki) keeps its own background + token colors. --- */
66
+ .ocui-md [data-streamdown="code-block"] {
67
+ @apply my-4 overflow-hidden rounded-lg border border-border;
68
+ }
69
+ .ocui-md [data-streamdown="code-block-header"] {
70
+ @apply border-b border-border bg-muted/50 px-3 py-1 text-xs text-muted-foreground;
71
+ }
72
+ .ocui-md [data-streamdown="code-block-body"] {
73
+ @apply text-sm;
74
+ }
75
+ }
@@ -0,0 +1,5 @@
1
+ export { Response } from './chat/response';
2
+ export { MessageContent, type MessageContentProps, type MessageContentVariant, } from './chat/message';
3
+ export { Reasoning, ReasoningContent, ReasoningTrigger, reasoningTextClassName, type ReasoningProps, type ReasoningTriggerProps, } from './chat/reasoning';
4
+ export { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput, type ToolContentProps, type ToolHeaderProps, type ToolInputProps, type ToolOutputProps, type ToolProps, } from './chat/tool';
5
+ export { Action, Actions, type ActionProps, type ActionsProps, } from './chat/actions';
package/dist/index.js ADDED
@@ -0,0 +1,286 @@
1
+ "use client";
2
+ import { createMathPlugin as e } from "@streamdown/math";
3
+ import { Streamdown as t } from "streamdown";
4
+ import { clsx as n } from "clsx";
5
+ import { twMerge as r } from "tailwind-merge";
6
+ import { Fragment as i, jsx as a, jsxs as o } from "react/jsx-runtime";
7
+ import { CheckIcon as s, ChevronDownIcon as c, ClockIcon as l, Loader2Icon as u, WrenchIcon as d, XIcon as f } from "lucide-react";
8
+ import { createContext as p, memo as m, useContext as h, useEffect as g, useState as _ } from "react";
9
+ import { Collapsible as v, Slot as y, Tooltip as b } from "radix-ui";
10
+ import { cva as x } from "class-variance-authority";
11
+ //#region src/lib/cn.ts
12
+ function S(...e) {
13
+ return r(n(e));
14
+ }
15
+ //#endregion
16
+ //#region src/chat/response.tsx
17
+ var C = e({ singleDollarTextMath: !0 });
18
+ function w({ className: e, ...n }) {
19
+ return /* @__PURE__ */ a(t, {
20
+ className: S("ocui-md size-full text-base font-normal leading-6 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_code]:whitespace-pre-wrap [&_code]:wrap-break-word [&_pre]:max-w-full [&_pre]:overflow-x-auto", e),
21
+ plugins: { math: C },
22
+ controls: { table: !1 },
23
+ ...n
24
+ });
25
+ }
26
+ //#endregion
27
+ //#region src/chat/message.tsx
28
+ var T = {
29
+ user: "w-fit wrap-break-word rounded-md bg-card px-3 py-1.5 text-primary",
30
+ assistant: "bg-transparent px-0 py-0 text-left text-foreground"
31
+ }, E = ({ children: e, className: t, variant: n = "user", ...r }) => /* @__PURE__ */ a("div", {
32
+ className: S("flex flex-col gap-2 overflow-hidden text-base font-normal leading-6", T[n], t),
33
+ ...r,
34
+ children: e
35
+ });
36
+ //#endregion
37
+ //#region src/internal/collapsible.tsx
38
+ function D({ ...e }) {
39
+ return /* @__PURE__ */ a(v.Root, {
40
+ "data-slot": "collapsible",
41
+ ...e
42
+ });
43
+ }
44
+ function O({ ...e }) {
45
+ return /* @__PURE__ */ a(v.CollapsibleTrigger, {
46
+ "data-slot": "collapsible-trigger",
47
+ ...e
48
+ });
49
+ }
50
+ function k({ ...e }) {
51
+ return /* @__PURE__ */ a(v.CollapsibleContent, {
52
+ "data-slot": "collapsible-content",
53
+ ...e
54
+ });
55
+ }
56
+ //#endregion
57
+ //#region src/chat/reasoning.tsx
58
+ function A({ prop: e, defaultProp: t, onChange: n }) {
59
+ let [r, i] = _(t), a = e !== void 0;
60
+ return [a ? e : r, (e) => {
61
+ a || i(e), n?.(e);
62
+ }];
63
+ }
64
+ var j = p(null), M = () => {
65
+ let e = h(j);
66
+ if (!e) throw Error("Reasoning components must be used within Reasoning");
67
+ return e;
68
+ }, N = 500, P = m(({ className: e, isStreaming: t = !1, duration: n = 0, open: r, defaultOpen: i = !0, onOpenChange: o, children: s, ...c }) => {
69
+ let [l, u] = A({
70
+ prop: r,
71
+ defaultProp: i,
72
+ onChange: o
73
+ }), [d, f] = _(!1);
74
+ return g(() => {
75
+ if (i && !t && l && !d) {
76
+ let e = setTimeout(() => {
77
+ u(!1), f(!0);
78
+ }, N);
79
+ return () => clearTimeout(e);
80
+ }
81
+ }, [
82
+ t,
83
+ l,
84
+ i,
85
+ u,
86
+ d
87
+ ]), /* @__PURE__ */ a(j.Provider, {
88
+ value: {
89
+ isStreaming: t,
90
+ isOpen: l,
91
+ setIsOpen: u,
92
+ duration: n
93
+ },
94
+ children: /* @__PURE__ */ a(D, {
95
+ className: S("not-prose", e),
96
+ onOpenChange: u,
97
+ open: l,
98
+ ...c,
99
+ children: s
100
+ })
101
+ });
102
+ });
103
+ function F({ variant: e, isStreaming: t, duration: n }) {
104
+ let r = e === "tools";
105
+ return t ? r ? "Working…" : "Thinking…" : n > 0 ? r ? `Worked for ${n}s` : `Thought for ${n}s` : r ? "Worked" : "Thought";
106
+ }
107
+ var I = m(({ className: e, children: t, variant: n = "reasoning", label: r, ...s }) => {
108
+ let { isStreaming: l, isOpen: u, duration: f } = M(), p = r ?? F({
109
+ variant: n,
110
+ isStreaming: l,
111
+ duration: f
112
+ });
113
+ return /* @__PURE__ */ a(O, {
114
+ className: S("flex items-center gap-1 rounded-md px-1.5 py-0.5 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", e),
115
+ ...s,
116
+ children: t ?? /* @__PURE__ */ o(i, { children: [
117
+ n === "tools" && /* @__PURE__ */ a(d, { className: "size-3" }),
118
+ /* @__PURE__ */ a("span", {
119
+ className: S(l && "animate-pulse"),
120
+ children: p
121
+ }),
122
+ /* @__PURE__ */ a(c, { className: S("size-2.5 transition-transform", u ? "rotate-180" : "rotate-0") })
123
+ ] })
124
+ });
125
+ }), L = "grid gap-1 text-sm text-muted-foreground/80 **:text-sm [&_code]:bg-transparent [&_code]:p-0 [&_code]:font-semibold [&_code]:text-foreground [&_li]:my-0 [&_ol]:my-1 [&_p]:my-0 [&_ul]:my-1", R = m(({ className: e, ...t }) => /* @__PURE__ */ a(k, {
126
+ className: S(L, "mt-1.5", e),
127
+ ...t
128
+ }));
129
+ P.displayName = "Reasoning", I.displayName = "ReasoningTrigger", R.displayName = "ReasoningContent";
130
+ //#endregion
131
+ //#region src/chat/tool.tsx
132
+ var z = ({ className: e, ...t }) => /* @__PURE__ */ a(D, {
133
+ className: S("not-prose w-full rounded-md border border-border", e),
134
+ ...t
135
+ }), B = {
136
+ "input-streaming": "Pending",
137
+ "input-available": "Running",
138
+ "approval-requested": "Pending",
139
+ "approval-responded": "Approved",
140
+ "output-available": "Completed",
141
+ "output-error": "Error",
142
+ "output-denied": "Denied"
143
+ }, V = {
144
+ "input-streaming": /* @__PURE__ */ a(u, { className: "size-3.5 animate-spin" }),
145
+ "input-available": /* @__PURE__ */ a(u, { className: "size-3.5 animate-spin" }),
146
+ "approval-requested": /* @__PURE__ */ a(l, { className: "size-3.5" }),
147
+ "approval-responded": /* @__PURE__ */ a(s, { className: "size-3.5" }),
148
+ "output-available": /* @__PURE__ */ a(s, { className: "size-3.5" }),
149
+ "output-error": /* @__PURE__ */ a(f, { className: "size-3.5 text-destructive" }),
150
+ "output-denied": /* @__PURE__ */ a(f, { className: "size-3.5 text-destructive" })
151
+ }, H = ({ state: e }) => /* @__PURE__ */ o("span", {
152
+ className: "flex shrink-0 items-center gap-1 text-xs text-muted-foreground",
153
+ children: [V[e], B[e]]
154
+ }), U = ({ className: e, type: t, state: n, title: r, ...i }) => /* @__PURE__ */ o(O, {
155
+ className: S("group flex w-full min-w-0 items-center justify-between gap-2 rounded-md p-2.5 transition-colors hover:bg-muted/50", e),
156
+ ...i,
157
+ children: [/* @__PURE__ */ o("div", {
158
+ className: "flex min-w-0 flex-1 items-center gap-2",
159
+ children: [/* @__PURE__ */ a(d, { className: "size-3.5 shrink-0 text-muted-foreground" }), /* @__PURE__ */ a("span", {
160
+ className: "truncate text-sm font-medium",
161
+ children: r ?? t
162
+ })]
163
+ }), /* @__PURE__ */ o("div", {
164
+ className: "flex shrink-0 items-center gap-2",
165
+ children: [/* @__PURE__ */ a(H, { state: n }), /* @__PURE__ */ a(c, { className: "size-3.5 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" })]
166
+ })]
167
+ }), W = ({ className: e, ...t }) => /* @__PURE__ */ a(k, {
168
+ className: S("data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in", e),
169
+ ...t
170
+ }), G = ({ className: e, input: t, ...n }) => /* @__PURE__ */ o("div", {
171
+ className: S("space-y-1.5 overflow-hidden px-2.5 pb-2.5", e),
172
+ ...n,
173
+ children: [/* @__PURE__ */ a("h4", {
174
+ className: "text-xs font-medium tracking-wide text-muted-foreground uppercase",
175
+ children: "Parameters"
176
+ }), /* @__PURE__ */ a("pre", {
177
+ className: "overflow-x-auto rounded-md bg-muted/50 p-2.5 font-mono text-xs",
178
+ children: JSON.stringify(t, null, 2)
179
+ })]
180
+ }), K = ({ className: e, output: t, errorText: n, ...r }) => t || n ? /* @__PURE__ */ o("div", {
181
+ className: S("space-y-1.5 px-2.5 pb-2.5", e),
182
+ ...r,
183
+ children: [/* @__PURE__ */ a("h4", {
184
+ className: "text-xs font-medium tracking-wide text-muted-foreground uppercase",
185
+ children: n ? "Error" : "Result"
186
+ }), /* @__PURE__ */ o("div", {
187
+ className: S("overflow-x-auto rounded-md text-xs [&_table]:w-full", n ? "bg-destructive/10 p-2.5 text-destructive" : "bg-muted/50 p-2.5 text-foreground"),
188
+ children: [n && /* @__PURE__ */ a("div", { children: n }), t && /* @__PURE__ */ a("div", { children: t })]
189
+ })]
190
+ }) : null, q = x("group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", {
191
+ variants: {
192
+ variant: {
193
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
194
+ outline: "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
195
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
196
+ ghost: "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
197
+ destructive: "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
198
+ link: "text-primary underline-offset-4 hover:underline"
199
+ },
200
+ size: {
201
+ default: "h-7 gap-1.5 px-2 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
202
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
203
+ sm: "h-6 gap-1 rounded-[min(var(--radius-md),12px)] px-2 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
204
+ lg: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
205
+ icon: "size-7",
206
+ "icon-xs": "size-6 rounded-[min(var(--radius-md),10px)] [&_svg:not([class*='size-'])]:size-3",
207
+ "icon-sm": "size-6 rounded-[min(var(--radius-md),12px)]",
208
+ "icon-lg": "size-8"
209
+ }
210
+ },
211
+ defaultVariants: {
212
+ variant: "default",
213
+ size: "default"
214
+ }
215
+ });
216
+ function J({ className: e, variant: t = "default", size: n = "default", asChild: r = !1, ...i }) {
217
+ return /* @__PURE__ */ a(r ? y.Root : "button", {
218
+ "data-slot": "button",
219
+ "data-variant": t,
220
+ "data-size": n,
221
+ className: S(q({
222
+ variant: t,
223
+ size: n,
224
+ className: e
225
+ })),
226
+ ...i
227
+ });
228
+ }
229
+ //#endregion
230
+ //#region src/internal/tooltip.tsx
231
+ function Y({ delayDuration: e = 0, ...t }) {
232
+ return /* @__PURE__ */ a(b.Provider, {
233
+ "data-slot": "tooltip-provider",
234
+ delayDuration: e,
235
+ ...t
236
+ });
237
+ }
238
+ function X({ ...e }) {
239
+ return /* @__PURE__ */ a(b.Root, {
240
+ "data-slot": "tooltip",
241
+ ...e
242
+ });
243
+ }
244
+ function Z({ ...e }) {
245
+ return /* @__PURE__ */ a(b.Trigger, {
246
+ "data-slot": "tooltip-trigger",
247
+ ...e
248
+ });
249
+ }
250
+ function Q({ className: e, sideOffset: t = 0, children: n, ...r }) {
251
+ return /* @__PURE__ */ a(b.Portal, { children: /* @__PURE__ */ o(b.Content, {
252
+ "data-slot": "tooltip-content",
253
+ sideOffset: t,
254
+ className: S("z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md border border-border bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-[state=instant-open]:animate-in data-[state=instant-open]:fade-in-0 data-[state=instant-open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", e),
255
+ ...r,
256
+ children: [n, /* @__PURE__ */ a(b.Arrow, { className: "z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] border-r border-b border-border bg-popover fill-popover" })]
257
+ }) });
258
+ }
259
+ //#endregion
260
+ //#region src/chat/actions.tsx
261
+ var $ = ({ className: e, children: t, ...n }) => /* @__PURE__ */ a("div", {
262
+ className: S("flex items-center", e),
263
+ ...n,
264
+ children: t
265
+ }), ee = ({ tooltip: e, children: t, label: n, className: r, variant: i = "ghost", size: s = "icon", ...c }) => {
266
+ let l = /* @__PURE__ */ o(J, {
267
+ className: S("relative", r),
268
+ size: s,
269
+ type: "button",
270
+ variant: i,
271
+ ...c,
272
+ children: [t, /* @__PURE__ */ a("span", {
273
+ className: "sr-only",
274
+ children: n || e
275
+ })]
276
+ });
277
+ return e ? /* @__PURE__ */ a(Y, {
278
+ delayDuration: 300,
279
+ children: /* @__PURE__ */ o(X, { children: [/* @__PURE__ */ a(Z, {
280
+ asChild: !0,
281
+ children: l
282
+ }), /* @__PURE__ */ a(Q, { children: /* @__PURE__ */ a("p", { children: e }) })] })
283
+ }) : l;
284
+ };
285
+ //#endregion
286
+ export { ee as Action, $ as Actions, E as MessageContent, P as Reasoning, R as ReasoningContent, I as ReasoningTrigger, w as Response, z as Tool, W as ToolContent, U as ToolHeader, G as ToolInput, K as ToolOutput, L as reasoningTextClassName };
@@ -0,0 +1,10 @@
1
+ import { VariantProps } from 'class-variance-authority';
2
+ import * as React from "react";
3
+ declare const buttonVariants: (props?: ({
4
+ variant?: "link" | "default" | "outline" | "secondary" | "ghost" | "destructive" | null | undefined;
5
+ size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
6
+ } & import('class-variance-authority/types').ClassProp) | undefined) => string;
7
+ declare function Button({ className, variant, size, asChild, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
8
+ asChild?: boolean;
9
+ }): React.JSX.Element;
10
+ export { Button, buttonVariants };
@@ -0,0 +1,6 @@
1
+ import { Collapsible as CollapsiblePrimitive } from 'radix-ui';
2
+ import type * as React from "react";
3
+ declare function Collapsible({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Root>): React.JSX.Element;
4
+ declare function CollapsibleTrigger({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>): React.JSX.Element;
5
+ declare function CollapsibleContent({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>): React.JSX.Element;
6
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent };
@@ -0,0 +1,7 @@
1
+ import { Tooltip as TooltipPrimitive } from 'radix-ui';
2
+ import * as React from "react";
3
+ declare function TooltipProvider({ delayDuration, ...props }: React.ComponentProps<typeof TooltipPrimitive.Provider>): React.JSX.Element;
4
+ declare function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>): React.JSX.Element;
5
+ declare function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>): React.JSX.Element;
6
+ declare function TooltipContent({ className, sideOffset, children, ...props }: React.ComponentProps<typeof TooltipPrimitive.Content>): React.JSX.Element;
7
+ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
@@ -0,0 +1,2 @@
1
+ import { ClassValue } from 'clsx';
2
+ export declare function cn(...inputs: ClassValue[]): string;
@@ -0,0 +1,158 @@
1
+ /*
2
+ * @ocai/app-sdk-ui/tokens.css — OceanAI shared design tokens.
3
+ *
4
+ * Consume by importing INSIDE your Tailwind v4 entry stylesheet, after
5
+ * tailwindcss itself, so the @theme block below is compiled by the app's
6
+ * Tailwind pass:
7
+ *
8
+ * @import "tailwindcss";
9
+ * @import "@ocai/app-sdk-ui/tokens.css";
10
+ *
11
+ * A plain <link> or JS-side import will NOT work — Tailwind must see the
12
+ * @theme block to generate `bg-background` and friends.
13
+ *
14
+ * Dark mode keys off a `.dark` ancestor class (both OceanAI apps toggle a
15
+ * class on <html>).
16
+ *
17
+ * Theming model: two grayscale seeds (--background, --foreground) drive the
18
+ * whole chromeless set via color-mix(in oklab, fg X%, bg). Tokens that carry
19
+ * chroma (--destructive, --chart-*) stay literal. Light is the authoritative
20
+ * set; .dark overrides the seeds and re-tunes percentages where the recipe
21
+ * hits different optima on a dark surface.
22
+ *
23
+ * Derivation for a target lightness L: X = (L_target − L_bg) / (L_fg − L_bg)
24
+ *
25
+ * Apps re-theme by overriding the two seeds after this import; everything
26
+ * else tracks automatically. The chatbot's original hand-authored palette is
27
+ * this exact derivation with seeds fg 0.145 (light) and bg 0.145 / fg 0.985
28
+ * (dark) — override only the seeds and it renders visually identical.
29
+ */
30
+
31
+ @theme inline {
32
+ --color-background: var(--background);
33
+ --color-foreground: var(--foreground);
34
+ --color-card: var(--card);
35
+ --color-card-foreground: var(--card-foreground);
36
+ --color-popover: var(--popover);
37
+ --color-popover-foreground: var(--popover-foreground);
38
+ --color-primary: var(--primary);
39
+ --color-primary-foreground: var(--primary-foreground);
40
+ --color-secondary: var(--secondary);
41
+ --color-secondary-foreground: var(--secondary-foreground);
42
+ --color-muted: var(--muted);
43
+ --color-muted-foreground: var(--muted-foreground);
44
+ --color-accent: var(--accent);
45
+ --color-accent-foreground: var(--accent-foreground);
46
+ --color-destructive: var(--destructive);
47
+ --color-border: var(--border);
48
+ --color-input: var(--input);
49
+ --color-ring: var(--ring);
50
+ --color-chart-1: var(--chart-1);
51
+ --color-chart-2: var(--chart-2);
52
+ --color-chart-3: var(--chart-3);
53
+ --color-chart-4: var(--chart-4);
54
+ --color-chart-5: var(--chart-5);
55
+ --color-sidebar: var(--sidebar);
56
+ --color-sidebar-foreground: var(--sidebar-foreground);
57
+ --color-sidebar-primary: var(--sidebar-primary);
58
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
59
+ --color-sidebar-accent: var(--sidebar-accent);
60
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
61
+ --color-sidebar-border: var(--sidebar-border);
62
+ --color-sidebar-ring: var(--sidebar-ring);
63
+ --radius-sm: calc(var(--radius) - 4px);
64
+ --radius-md: calc(var(--radius) - 2px);
65
+ --radius-lg: var(--radius);
66
+ --radius-xl: calc(var(--radius) + 4px);
67
+ }
68
+
69
+ :root {
70
+ /* The two seeds. Everything chromeless below derives from these. */
71
+ --background: oklch(1 0 0);
72
+ --foreground: oklch(0.293 0 0);
73
+
74
+ --radius: 0.625rem;
75
+
76
+ --card: var(--background);
77
+ --card-foreground: var(--foreground);
78
+ --popover: var(--background);
79
+ --popover-foreground: var(--foreground);
80
+
81
+ --primary: color-mix(in oklab, var(--foreground) 93%, var(--background));
82
+ --primary-foreground: color-mix(in oklab, var(--foreground) 2%, var(--background));
83
+
84
+ --secondary: color-mix(in oklab, var(--foreground) 4%, var(--background));
85
+ --secondary-foreground: color-mix(in oklab, var(--foreground) 93%, var(--background));
86
+
87
+ --muted: color-mix(in oklab, var(--foreground) 4%, var(--background));
88
+ --muted-foreground: color-mix(in oklab, var(--foreground) 52%, var(--background));
89
+
90
+ --accent: color-mix(in oklab, var(--foreground) 4%, var(--background));
91
+ --accent-foreground: color-mix(in oklab, var(--foreground) 93%, var(--background));
92
+
93
+ --border: color-mix(in oklab, var(--foreground) 9%, var(--background));
94
+ --input: color-mix(in oklab, var(--foreground) 9%, var(--background));
95
+
96
+ /* Neutral by default. Apps with a brand accent point this at it
97
+ (zyler: `--ring: var(--editor-accent)`). */
98
+ --ring: color-mix(in oklab, var(--foreground) 42%, var(--background));
99
+
100
+ --destructive: oklch(0.577 0.245 27.325);
101
+
102
+ /* Chart series colors carry chroma — literal, not seed-derived. */
103
+ --chart-1: oklch(0.646 0.222 41.116);
104
+ --chart-2: oklch(0.6 0.118 184.704);
105
+ --chart-3: oklch(0.398 0.07 227.392);
106
+ --chart-4: oklch(0.828 0.189 84.429);
107
+ --chart-5: oklch(0.769 0.188 70.08);
108
+
109
+ --sidebar: color-mix(in oklab, var(--foreground) 2%, var(--background));
110
+ /* Aliases resolve at use time, so the .dark block below doesn't need to
111
+ repeat them — they track their target's dark value automatically. */
112
+ --sidebar-foreground: var(--foreground);
113
+ --sidebar-primary: var(--primary);
114
+ --sidebar-primary-foreground: var(--primary-foreground);
115
+ --sidebar-accent: var(--accent);
116
+ --sidebar-accent-foreground: var(--accent-foreground);
117
+ --sidebar-border: var(--border);
118
+ --sidebar-ring: var(--ring);
119
+ }
120
+
121
+ .dark {
122
+ --background: oklch(0.213 0 0);
123
+ --foreground: oklch(0.952 0 0);
124
+
125
+ --card: color-mix(in oklab, var(--foreground) 7%, var(--background));
126
+ --popover: color-mix(in oklab, var(--foreground) 7%, var(--background));
127
+
128
+ --primary: color-mix(in oklab, var(--foreground) 93%, var(--background));
129
+ --primary-foreground: color-mix(in oklab, var(--foreground) 7%, var(--background));
130
+
131
+ --secondary: color-mix(in oklab, var(--foreground) 15%, var(--background));
132
+ --secondary-foreground: var(--foreground);
133
+
134
+ --muted: color-mix(in oklab, var(--foreground) 15%, var(--background));
135
+ --muted-foreground: color-mix(in oklab, var(--foreground) 67%, var(--background));
136
+
137
+ --accent: color-mix(in oklab, var(--foreground) 15%, var(--background));
138
+ --accent-foreground: var(--foreground);
139
+
140
+ /* Solid (not alpha) so every divider reads the same lightness regardless
141
+ of the surface underneath — 18% against bg lands at oklch L≈0.35.
142
+ --input stays alpha: input chrome reads as a translucent surface, not a
143
+ hairline divider. */
144
+ --border: color-mix(in oklab, var(--foreground) 18%, var(--background));
145
+ --input: color-mix(in oklab, var(--foreground) 15%, transparent);
146
+
147
+ --ring: color-mix(in oklab, var(--foreground) 49%, var(--background));
148
+
149
+ --destructive: oklch(0.704 0.191 22.216);
150
+
151
+ --chart-1: oklch(0.488 0.243 264.376);
152
+ --chart-2: oklch(0.696 0.17 162.48);
153
+ --chart-3: oklch(0.769 0.188 70.08);
154
+ --chart-4: oklch(0.627 0.265 303.9);
155
+ --chart-5: oklch(0.645 0.246 16.439);
156
+
157
+ --sidebar: color-mix(in oklab, var(--foreground) 7%, var(--background));
158
+ }
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@ocai/app-sdk-ui",
3
+ "version": "0.1.0",
4
+ "description": "Shared design tokens and AI-app UI primitives for OceanAI products.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ },
15
+ "./tokens.css": "./dist/tokens.css",
16
+ "./chat.css": "./dist/chat.css",
17
+ "./package.json": "./package.json"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "!dist/**/*.map"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/ocai-labs/app-sdk-ui.git",
29
+ "directory": "packages/ui"
30
+ },
31
+ "sideEffects": [
32
+ "*.css"
33
+ ],
34
+ "peerDependencies": {
35
+ "@streamdown/math": "^1.0.0",
36
+ "ai": "^6.0.0",
37
+ "react": "^19.0.0",
38
+ "react-dom": "^19.0.0",
39
+ "streamdown": "^2.5.0"
40
+ },
41
+ "dependencies": {
42
+ "class-variance-authority": "^0.7.1",
43
+ "clsx": "^2.1.1",
44
+ "lucide-react": "^1.14.0",
45
+ "radix-ui": "^1.4.3",
46
+ "tailwind-merge": "^3.5.0"
47
+ },
48
+ "devDependencies": {
49
+ "@streamdown/math": "^1.0.2",
50
+ "@types/node": "^22.0.0",
51
+ "@types/react": "^19.0.0",
52
+ "@types/react-dom": "^19.0.0",
53
+ "@vitejs/plugin-react": "^6.0.1",
54
+ "ai": "^6.0.0",
55
+ "react": "^19.0.0",
56
+ "react-dom": "^19.0.0",
57
+ "streamdown": "^2.5.0",
58
+ "typescript": "~5.6.0",
59
+ "vite": "^8.0.16",
60
+ "vite-plugin-dts": "^4.5.4"
61
+ },
62
+ "scripts": {
63
+ "build": "vite build && cp src/tokens.css src/chat/chat.css dist/",
64
+ "dev": "vite build --watch",
65
+ "typecheck": "tsc --noEmit"
66
+ }
67
+ }