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 @@
1import { useMemo } from "react";
2import { cn } from "../lib/utils";1import { cn } from "../lib/utils";
2import { useFormattedCode } from "../lib/format";
3import { CodeBlock } from "./code-block";3import { CodeBlock } from "./code-block";
4 4
5function prettyJson(raw: string): string {
6 try {
7 return JSON.stringify(JSON.parse(raw), null, 2);
8 } catch {
9 return raw;
10 }
11}
12
13export function DocumentPanel(props: {5export function DocumentPanel(props: {
14 title: string;6 title: string;
15 body: string | null;7 body: string | null;
@@ -17,10 +9,7 @@ export function DocumentPanel(props: { · 1 unmodified line
17 empty: string;9 empty: string;
18 compact?: boolean;10 compact?: boolean;
19}) {11}) {
20 const formatted = useMemo(12 const formatted = useFormattedCode(props.body, props.lang);
21 () => (props.body ? prettyJson(props.body) : null),
22 [props.body],
23 );
24 13
25 return (14 return (
26 <section15 <section
apps/web/src/components/icons.tsx+10−0
@@ -107,3 +107,13 @@ export function IconSpinner({ className, ...props }: IconProps) { · 106 unmodified lines
107 </svg>107 </svg>
108 );108 );
109}109}
110
111export 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 @@
1import { Streamdown } from "streamdown";
2import { createCodePlugin } from "@streamdown/code";
3
4const codePlugin = createCodePlugin({
5 themes: ["vitesse-dark", "vitesse-dark"],
6});
7
8const 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
45export 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 @@
1import { useState, useMemo } from "react";
2import { Link, Outlet, useMatchRoute } from "@tanstack/react-router";1import { Link, Outlet, useMatchRoute } from "@tanstack/react-router";
3import { useSources, type Source } from "@executor-v3/react";2import { useSources, type Source } from "@executor-v3/react";
4import { cn } from "../lib/utils";3import { cn } from "../lib/utils";
5import { IconSources, IconChevron } from "./icons";4import { IconSources } from "./icons";
6import { LoadableBlock } from "./loadable";5import { LoadableBlock } from "./loadable";
7 6
8// ── Kind labels + ordering ───────────────────────────────────────────────
9
10const 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
17function 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
29// ── Status dot color ─────────────────────────────────────────────────────7// ── Status dot color ─────────────────────────────────────────────────────
30 8
31const statusColor: Record<string, string> = {9const statusColor: Record<string, string> = {
@@ -63,7 +41,7 @@ export function AppShell() { · 31 unmodified lines
63 <nav className="flex flex-1 flex-col p-2 overflow-y-auto">41 <nav className="flex flex-1 flex-col p-2 overflow-y-auto">
64 <NavItem to="/" label="Dashboard" active={!!isHome} />42 <NavItem to="/" label="Dashboard" active={!!isHome} />
65 43
66 {/* Sources tree */}44 {/* Sources */}
67 <div className="mt-5 mb-1 px-2.5 text-[10px] font-medium uppercase tracking-widest text-muted-foreground/50">45 <div className="mt-5 mb-1 px-2.5 text-[10px] font-medium uppercase tracking-widest text-muted-foreground/50">
68 Sources46 Sources
69 </div>47 </div>
@@ -74,7 +52,11 @@ export function AppShell() { · 4 unmodified lines
74 No sources yet52 No sources yet
75 </div>53 </div>
76 ) : (54 ) : (
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>
78 )60 )
79 }61 }
80 </LoadableBlock>62 </LoadableBlock>
@@ -96,87 +78,13 @@ export function AppShell() { · 15 unmodified lines
96 );78 );
97}79}
98 80
99// ── SourceTree ───────────────────────────────────────────────────────────
100
101function 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
129function 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
172// ── SourceItem ───────────────────────────────────────────────────────────81// ── SourceItem ───────────────────────────────────────────────────────────
173 82
174function SourceItem(props: {83function SourceItem(props: {
175 source: Source;84 source: Source;
176 matchRoute: ReturnType<typeof useMatchRoute>;85 matchRoute: ReturnType<typeof useMatchRoute>;
177 depth: number;
178}) {86}) {
179 const { source, matchRoute, depth } = props;87 const { source, matchRoute } = props;
180 const active = matchRoute({88 const active = matchRoute({
181 to: "/sources/$sourceId",89 to: "/sources/$sourceId",
182 params: { sourceId: source.id },90 params: { sourceId: source.id },
@@ -189,22 +97,14 @@ function SourceItem(props: { · 6 unmodified lines
189 params={{ sourceId: source.id }}97 params={{ sourceId: source.id }}
190 search={{ tab: "model" }}98 search={{ tab: "model" }}
191 className={cn(99 className={cn(
192 "group flex items-center gap-2 rounded-md py-1.5 text-[12px] transition-colors",100 "group flex items-center gap-2 rounded-md px-2.5 py-1.5 text-[13px] transition-colors",
193 depth === 0 ? "px-2.5" : "pl-6 pr-2.5",
194 active101 active
195 ? "bg-sidebar-active text-foreground font-medium"102 ? "bg-sidebar-active text-foreground font-medium"
196 : "text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground",103 : "text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground",
197 )}104 )}
198 >105 >
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
204 <IconSources className="size-3 shrink-0 text-muted-foreground/40" />106 <IconSources className="size-3 shrink-0 text-muted-foreground/40" />
205 <span className="flex-1 truncate">{source.name}</span>107 <span className="flex-1 truncate">{source.name}</span>
206
207 {/* Status dot + tooltip */}
208 <span108 <span
209 className={cn("size-1.5 shrink-0 rounded-full", statusColor[source.status] ?? "bg-muted-foreground/30")}109 className={cn("size-1.5 shrink-0 rounded-full", statusColor[source.status] ?? "bg-muted-foreground/30")}
210 title={source.status}110 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