History

nicer ui

Rhys Sullivan committed 8 months ago

Commit ece99cf1 parent 8f20d5b14 files · +843 −160Browse files

Showing the changes to apps/web/src/components (4 of 14 files). Show all

Unified · Split

4 files
  1. apps/web/src/components/document-panel.tsx +2 −13
  2. apps/web/src/components/icons.tsx +10 −0
  3. apps/web/src/components/markdown.tsx +53 −0
  4. apps/web/src/components/shell.tsx +9 −109
apps/web/src/components/document-panel.tsx+2−13
@@ -1,15 +1,7 @@
1−import { useMemo } from "react";
21 import { cn } from "../lib/utils";
2+import { useFormattedCode } from "../lib/format";
33 import { CodeBlock } from "./code-block";
44
5−function prettyJson(raw: string): string {
6− try {
7− return JSON.stringify(JSON.parse(raw), null, 2);
8− } catch {
9− return raw;
10− }
11−}
12−
135 export function DocumentPanel(props: {
146 title: string;
157 body: string | null;
@@ -17,10 +9,7 @@ export function DocumentPanel(props: { · 1 unmodified line
179 empty: string;
1810 compact?: boolean;
1911 }) {
20− const formatted = useMemo(
21− () => (props.body ? prettyJson(props.body) : null),
22− [props.body],
23− );
12+ const formatted = useFormattedCode(props.body, props.lang);
2413
2514 return (
2615 <section
apps/web/src/components/icons.tsx+10−0
@@ -107,3 +107,13 @@ export function IconSpinner({ className, ...props }: IconProps) { · 106 unmodified lines
107107 </svg>
108108 );
109109 }
110+
111+export function IconInfo({ className, ...props }: IconProps) {
112+ return (
113+ <svg viewBox="0 0 16 16" fill="none" className={cn("size-4", className)} {...props}>
114+ <circle cx="8" cy="8" r="5.75" stroke="currentColor" strokeWidth="1.2" />
115+ <path d="M8 7v3.25" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
116+ <circle cx="8" cy="4.75" r="0.8" fill="currentColor" />
117+ </svg>
118+ );
119+}
apps/web/src/components/markdown.tsx+53−0
@@ -0,0 +1,53 @@
1+import { Streamdown } from "streamdown";
2+import { createCodePlugin } from "@streamdown/code";
3+
4+const codePlugin = createCodePlugin({
5+ themes: ["vitesse-dark", "vitesse-dark"],
6+});
7+
8+const PROSE_CLASSES = [
9+ "text-[13px] leading-relaxed text-muted-foreground",
10+ // paragraphs
11+ "[&_p]:mb-[0.4em] [&_p:last-child]:mb-0",
12+ // bold
13+ "[&_strong]:text-foreground [&_strong]:font-semibold",
14+ "[&_b]:text-foreground [&_b]:font-semibold",
15+ // inline code
16+ "[&_code]:font-mono [&_code]:text-xs [&_code]:bg-muted [&_code]:border [&_code]:border-border",
17+ "[&_code]:rounded-sm [&_code]:px-1.5 [&_code]:py-px [&_code]:text-primary",
18+ // pre blocks
19+ "[&_pre]:bg-muted [&_pre]:border [&_pre]:border-border [&_pre]:rounded-md",
20+ "[&_pre]:px-3 [&_pre]:py-2 [&_pre]:overflow-x-auto [&_pre]:my-2 [&_pre]:text-xs [&_pre]:leading-relaxed",
21+ "[&_pre_code]:bg-transparent [&_pre_code]:border-0 [&_pre_code]:p-0 [&_pre_code]:text-inherit",
22+ // links
23+ "[&_a]:text-primary [&_a]:underline [&_a]:underline-offset-2",
24+ "[&_a]:decoration-primary/30 hover:[&_a]:decoration-primary/80",
25+ // lists
26+ "[&_ul]:pl-5 [&_ul]:my-1.5 [&_ol]:pl-5 [&_ol]:my-1.5",
27+ "[&_li]:mb-0.5 [&_li_::marker]:text-muted-foreground",
28+ // tables
29+ "[&_table]:w-full [&_table]:border-collapse [&_table]:text-xs [&_table]:my-2",
30+ "[&_th]:border [&_th]:border-border [&_th]:px-2 [&_th]:py-1 [&_th]:text-left [&_th]:bg-muted [&_th]:font-semibold [&_th]:text-foreground",
31+ "[&_td]:border [&_td]:border-border [&_td]:px-2 [&_td]:py-1 [&_td]:text-left [&_td]:bg-background",
32+ // headings
33+ "[&_h1]:font-semibold [&_h1]:text-foreground [&_h1]:mt-2 [&_h1]:mb-1 [&_h1]:text-[15px]",
34+ "[&_h2]:font-semibold [&_h2]:text-foreground [&_h2]:mt-2 [&_h2]:mb-1 [&_h2]:text-sm",
35+ "[&_h3]:font-semibold [&_h3]:text-foreground [&_h3]:mt-2 [&_h3]:mb-1 [&_h3]:text-[13px]",
36+ "[&_h4]:font-semibold [&_h4]:text-foreground [&_h4]:mt-2 [&_h4]:mb-1 [&_h4]:text-[13px]",
37+ // blockquote
38+ "[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:my-1.5 [&_blockquote]:text-muted-foreground",
39+ // hr
40+ "[&_hr]:border-0 [&_hr]:border-t [&_hr]:border-border [&_hr]:my-2",
41+ // images
42+ "[&_img]:max-w-full [&_img]:rounded",
43+].join(" ");
44+
45+export function Markdown(props: { children: string; className?: string }) {
46+ return (
47+ <div className={props.className ? `${PROSE_CLASSES} ${props.className}` : PROSE_CLASSES}>
48+ <Streamdown plugins={{ code: codePlugin }} controls={{ code: true }} linkSafety={{ enabled: false }}>
49+ {props.children}
50+ </Streamdown>
51+ </div>
52+ );
53+}
apps/web/src/components/shell.tsx+9−109
@@ -1,31 +1,9 @@
1−import { useState, useMemo } from "react";
21 import { Link, Outlet, useMatchRoute } from "@tanstack/react-router";
32 import { useSources, type Source } from "@executor-v3/react";
43 import { cn } from "../lib/utils";
5−import { IconSources, IconChevron } from "./icons";
4+import { IconSources } from "./icons";
65 import { LoadableBlock } from "./loadable";
76
8−// ── Kind labels + ordering ───────────────────────────────────────────────
9−
10−const kindMeta: Record<string, { label: string; order: number }> = {
11− mcp: { label: "MCP", order: 0 },
12− openapi: { label: "OpenAPI", order: 1 },
13− graphql: { label: "GraphQL", order: 2 },
14− internal: { label: "Internal", order: 3 },
15−};
16−
17−function groupByKind(sources: ReadonlyArray<Source>) {
18− const groups = new Map<string, Source[]>();
19− for (const s of sources) {
20− const list = groups.get(s.kind) ?? [];
21− list.push(s);
22− groups.set(s.kind, list);
23− }
24− return [...groups.entries()].sort(
25− (a, b) => (kindMeta[a[0]]?.order ?? 99) - (kindMeta[b[0]]?.order ?? 99),
26− );
27−}
28−
297 // ── Status dot color ─────────────────────────────────────────────────────
308
319 const statusColor: Record<string, string> = {
@@ -63,7 +41,7 @@ export function AppShell() { · 31 unmodified lines
6341 <nav className="flex flex-1 flex-col p-2 overflow-y-auto">
6442 <NavItem to="/" label="Dashboard" active={!!isHome} />
6543
66− {/* Sources tree */}
44+ {/* Sources */}
6745 <div className="mt-5 mb-1 px-2.5 text-[10px] font-medium uppercase tracking-widest text-muted-foreground/50">
6846 Sources
6947 </div>
@@ -74,7 +52,11 @@ export function AppShell() { · 4 unmodified lines
7452 No sources yet
7553 </div>
7654 ) : (
77− <SourceTree sources={items} matchRoute={matchRoute} />
55+ <div className="flex flex-col gap-px">
56+ {items.map((source) => (
57+ <SourceItem key={source.id} source={source} matchRoute={matchRoute} />
58+ ))}
59+ </div>
7860 )
7961 }
8062 </LoadableBlock>
@@ -96,87 +78,13 @@ export function AppShell() { · 15 unmodified lines
9678 );
9779 }
9880
99−// ── SourceTree ───────────────────────────────────────────────────────────
100−
101−function SourceTree(props: {
102− sources: ReadonlyArray<Source>;
103− matchRoute: ReturnType<typeof useMatchRoute>;
104−}) {
105− const groups = useMemo(() => groupByKind(props.sources), [props.sources]);
106−
107− // If there's only one kind, skip the group headers
108− if (groups.length === 1) {
109− return (
110− <div className="flex flex-col gap-px">
111− {groups[0]![1].map((source) => (
112− <SourceItem key={source.id} source={source} matchRoute={props.matchRoute} depth={0} />
113− ))}
114− </div>
115− );
116− }
117−
118− return (
119− <div className="flex flex-col gap-0.5">
120− {groups.map(([kind, sources]) => (
121− <KindGroup key={kind} kind={kind} sources={sources} matchRoute={props.matchRoute} />
122− ))}
123− </div>
124− );
125−}
126−
127−// ── KindGroup (collapsible) ──────────────────────────────────────────────
128−
129−function KindGroup(props: {
130− kind: string;
131− sources: Source[];
132− matchRoute: ReturnType<typeof useMatchRoute>;
133−}) {
134− // Auto-expand if any source in this group is active
135− const hasActive = props.sources.some((s) =>
136− props.matchRoute({ to: "/sources/$sourceId", params: { sourceId: s.id }, fuzzy: true }),
137− );
138− const [open, setOpen] = useState(true);
139−
140− return (
141− <div>
142− {/* Group header */}
143− <button
144− type="button"
145− onClick={() => setOpen((o) => !o)}
146− className="group flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-[11px] font-medium text-muted-foreground/60 transition-colors hover:text-muted-foreground"
147− >
148− <IconChevron
149− className={cn(
150− "size-2.5 shrink-0 text-muted-foreground/30 transition-transform duration-150",
151− open && "rotate-90",
152− )}
153− />
154− <span className="uppercase tracking-wider">{kindMeta[props.kind]?.label ?? props.kind}</span>
155− <span className="ml-auto tabular-nums text-[10px] text-muted-foreground/30">
156− {props.sources.length}
157− </span>
158− </button>
159−
160− {/* Children */}
161− {open && (
162− <div className="flex flex-col gap-px mt-px">
163− {props.sources.map((source) => (
164− <SourceItem key={source.id} source={source} matchRoute={props.matchRoute} depth={1} />
165− ))}
166− </div>
167− )}
168− </div>
169− );
170−}
171−
17281 // ── SourceItem ───────────────────────────────────────────────────────────
17382
17483 function SourceItem(props: {
17584 source: Source;
17685 matchRoute: ReturnType<typeof useMatchRoute>;
177− depth: number;
17886 }) {
179− const { source, matchRoute, depth } = props;
87+ const { source, matchRoute } = props;
18088 const active = matchRoute({
18189 to: "/sources/$sourceId",
18290 params: { sourceId: source.id },
@@ -189,22 +97,14 @@ function SourceItem(props: { · 6 unmodified lines
18997 params={{ sourceId: source.id }}
19098 search={{ tab: "model" }}
19199 className={cn(
192− "group flex items-center gap-2 rounded-md py-1.5 text-[12px] transition-colors",
193− depth === 0 ? "px-2.5" : "pl-6 pr-2.5",
100+ "group flex items-center gap-2 rounded-md px-2.5 py-1.5 text-[13px] transition-colors",
194101 active
195102 ? "bg-sidebar-active text-foreground font-medium"
196103 : "text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground",
197104 )}
198105 >
199− {/* Tree connector line for nested items */}
200− {depth > 0 && (
201− <span className="absolute left-[18px] h-full w-px bg-sidebar-border" aria-hidden />
202− )}
203−
204106 <IconSources className="size-3 shrink-0 text-muted-foreground/40" />
205107 <span className="flex-1 truncate">{source.name}</span>
206−
207− {/* Status dot + tooltip */}
208108 <span
209109 className={cn("size-1.5 shrink-0 rounded-full", statusColor[source.status] ?? "bg-muted-foreground/30")}
210110 title={source.status}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t