diff --git a/.design-sync/NOTES.md b/.design-sync/NOTES.md new file mode 100644 index 00000000..e3a3880b --- /dev/null +++ b/.design-sync/NOTES.md @@ -0,0 +1,58 @@ +# Design sync notes + +Botloft is an app, not a component library, so the design system is a small package built from the +app's own source: `.design-sync/pkg/` (`index.ts` re-exports the components, `styles.css` adds a +layout vocabulary to the app's stylesheet, `build.mjs` builds `dist/` with the app's Vite, +Tailwind and TypeScript). Nothing there reimplements a component. + +## Running it + +- `node .design-sync/pkg/build.mjs` first (cfg.buildCmd), then the converter with + `--node-modules ./app/node_modules --entry ./.design-sync/pkg/dist/index.js`. +- `app/node_modules` must be installed (`pnpm install` in `app/`). +- `.design-sync/node_modules` is a junction to `.ds-sync/node_modules` (gitignored, recreate per + clone). Without it `@types/react` is not found from `.design-sync/pkg` and React props collapse + ([DTS_REACT]). On Windows: `node -e "require('fs').symlinkSync(require('path').resolve('.ds-sync/node_modules'), '.design-sync/node_modules', 'junction')"`. +- Render check and capture use `playwright@1.60.0` in `.ds-sync/` (matches the cached chromium-1223). + +## Fixes made in this repo's setup + +- Vite library mode inlines every asset, so the fonts would land in the CSS as base64 (1.1 MB, every + alphabet). `build.mjs` aliases `@fontsource/*.css` to an empty file and the Latin subsets ship as + files through cfg.extraFonts. +- `icons.gen.ts` is generated from the app's `lucide-react` imports and exported as `icons` + (a namespace, so the 94 icons do not become component cards). `lucide-react` is aliased to + `app/node_modules` because the file sits outside the app. +- `.d.ts` emit: tsc from `app/node_modules/typescript` (TypeScript 7; `bin/tsc` is not in its + exports map, so the path is built from `typescript/package.json`). +- Props that extend HTML attributes (`Button`, `TextField`, `TextArea`, `SelectField`) lose + `onClick`/`value`/`onChange` in extraction, and `BotStateBadge`, `ChiefBadge`, `Menu` reference + app types (`Bot`, `Crew`, `LucideIcon`). All six have hand-written bodies in cfg.dtsPropsFor. + Keep them in step with the app's source. +- Only Tailwind classes compiled into the CSS exist. The app's own set missed common layout + utilities (`max-w-sm`, arbitrary sizes), so `pkg/styles.css` adds a fixed vocabulary with + `@source inline(...)` and scans `.design-sync/previews`. +- Components speak the browser's language. Previews call `setLocaleChoice("en")` (exported from + the package) so cards are English on any machine. +- Fixed overlays (`Dialog`, `Confirm`, `Toaster`) render inside a box with `transform: + translateZ(0)`, which makes it their containing block; without it the per-story capture is + 0 px tall. +- `ListAvatar` is excluded (cfg.componentSrcMap): a wrapper over `BotAvatar` that needs a bot object. + +## Known render warns + +- `[FONT_MISSING] "Cascadia Mono"`: a system fallback after IBM Plex Mono in `--font-mono`; Plex + Mono ships. Accepted. +- `Menu` shows only its closed button: opening takes a click. +- `SelectField` Disabled looks like the enabled one: the app's own control does not style disabled. + +## Re-sync risks + +- `dtsPropsFor` bodies are copies of the app's props; a prop added in the app does not reach the + contract until the body is updated. +- The layout vocabulary in `pkg/styles.css` is a fixed list; the conventions header names its + ranges. Change both together. +- The icon set follows the app's imports: an icon the app stops using disappears from `icons`. +- Bot colors in previews and conventions are sample values, not tokens. +- The scope is the base pieces plus the bot badges (owner's choice on 2026-10-02). Chat rows, + approval cards and the sidebar are not in the package. diff --git a/.design-sync/config.json b/.design-sync/config.json new file mode 100644 index 00000000..009509e0 --- /dev/null +++ b/.design-sync/config.json @@ -0,0 +1,50 @@ +{ + "projectId": "1f0e61c2-d3f4-4621-a902-66ef64e8cc5f", + "shape": "package", + "pkg": "@botloft/ui", + "globalName": "Botloft", + "buildCmd": "node .design-sync/pkg/build.mjs", + "entry": ".design-sync/pkg/dist/index.js", + "srcDir": "../../app/src", + "cssEntry": "dist/styles.css", + "extraFonts": [ + "../../app/node_modules/@fontsource/ibm-plex-sans/latin-400.css", + "../../app/node_modules/@fontsource/ibm-plex-sans/latin-500.css", + "../../app/node_modules/@fontsource/ibm-plex-sans/latin-600.css", + "../../app/node_modules/@fontsource/ibm-plex-mono/latin-400.css", + "../../app/node_modules/@fontsource/ibm-plex-mono/latin-500.css" + ], + "dtsPropsFor": { + "Button": "variant?: \"primary\" | \"secondary\" | \"ghost\" | \"danger\"; size?: \"sm\" | \"md\"; /** An icon from window.Botloft.icons, e.g. icons.Plus. */ icon?: React.ComponentType<{ size?: number; strokeWidth?: number }>; /** Shows only the icon; label becomes its accessible name and tooltip. */ label?: string; onClick?: React.MouseEventHandler; type?: \"button\" | \"submit\"; title?: string; className?: string; id?: string; style?: React.CSSProperties; disabled?: boolean; autoFocus?: boolean; children?: React.ReactNode;", + "TextField": "label: string; hint?: string; /** Shows used / max and warns past it. */ max?: number; value?: string; defaultValue?: string; placeholder?: string; onChange?: React.ChangeEventHandler; type?: string; name?: string; className?: string; id?: string; style?: React.CSSProperties; disabled?: boolean; autoFocus?: boolean; ", + "TextArea": "label: string; hint?: string; /** Shows used / max and warns past it. */ max?: number; value?: string; defaultValue?: string; placeholder?: string; rows?: number; onChange?: React.ChangeEventHandler; name?: string; className?: string; id?: string; style?: React.CSSProperties; disabled?: boolean; autoFocus?: boolean; ", + "SelectField": "label: string; hint?: string; options: { value: string; label: string }[]; value?: string; defaultValue?: string; onChange?: React.ChangeEventHandler; name?: string; className?: string; id?: string; style?: React.CSSProperties; disabled?: boolean; autoFocus?: boolean; ", + "BotStateBadge": "/** The bot: its state and whether the owner paused it. */ bot: { state: \"offline\" | \"launching\" | \"idle\" | \"busy\" | \"needs_approval\" | \"rate_limited\" | \"auth_error\" | \"backoff\" | \"archived\"; paused: boolean }; /** A paused crew shows its stopped bots as paused. */ crewPaused?: boolean; compact?: boolean;", + "ChiefBadge": "/** The crew this bot leads; only its name is read, for the tooltip. */ crew: { name: string }; /** Just the crown, for tight rows. */ compact?: boolean;", + "Menu": "/** Accessible name of the button that opens the menu. */ label: string; /** An icon from window.Botloft.icons, e.g. icons.Ellipsis. */ icon: React.ComponentType<{ size?: number; strokeWidth?: number }>; items: { label: string; icon?: React.ComponentType<{ size?: number; strokeWidth?: number }>; danger?: boolean; disabled?: boolean; /** Makes the item one of a set of choices, marked when chosen. */ checked?: boolean; onSelect: () => void }[];" + }, + "componentSrcMap": { + "ListAvatar": null + }, + "overrides": { + "Dialog": { + "cardMode": "single", + "viewport": "720x520" + }, + "Confirm": { + "cardMode": "single", + "viewport": "720x480" + }, + "BotAvatar": { + "cardMode": "column" + }, + "SidePanel": { + "cardMode": "column" + }, + "Toaster": { + "cardMode": "single", + "primaryStory": "ErrorNotice" + } + }, + "readmeHeader": ".design-sync/conventions.md" +} diff --git a/.design-sync/conventions.md b/.design-sync/conventions.md new file mode 100644 index 00000000..59a9d64b --- /dev/null +++ b/.design-sync/conventions.md @@ -0,0 +1,81 @@ +# Building with Botloft + +Botloft is a desktop app where a crew of Claude Code bots works together. Each bot is a small flame +mascot in its own color. The look is quiet and warm: neutral surfaces, one orange accent, IBM Plex. + +## Setup + +- No provider is needed. Link `styles.css` and load `_ds_bundle.js`; components are on `window.Botloft`. +- **Theme:** light by default. Put `data-theme="dark"` on a wrapper (or ``) and every token + switches. Give the page `bg-canvas text-ink` so the background follows the theme. +- **Language:** the components' own words (state labels, Close, Cancel) follow the browser's + language. Call `Botloft.setLocaleChoice("en")` once before rendering to pin English + (`"pt-BR"` and `"es"` also exist). +- **Icons:** `Botloft.icons.` holds the lucide icons the app uses (`Plus`, `Pause`, `Settings`, + `Ellipsis`, `X`, `Archive`, `Trash2`, `RotateCw`, `FileText`, `Folder`, `Users`, `Crown`...). Pass one + as `icon` to `Button` and `Menu`, or render it: ``. + +## Styling: Tailwind utilities on the design tokens + +Style your own layout with Tailwind classes. **Only classes compiled into `_ds_bundle.css` exist**: +the app's own plus a fixed layout set. Arbitrary values like `h-[360px]` or `w-[37rem]` do NOT exist; +use `style={{ height: 360 }}` for one-off sizes. + +| Family | Classes | +|---|---| +| Surfaces | `bg-canvas` (page), `bg-panel` (cards, dialogs), `bg-sunken` (wells, hover) | +| Text | `text-ink`, `text-ink-soft` (body), `text-muted` (hints), `text-accent` | +| Lines | `border border-line`, `border-line-strong` (controls), `divide-y divide-line` | +| State | `text-ok` ready, `text-work` working, `text-warn` waiting, `text-danger` error, `text-quiet` off; tints like `bg-accent/10`, `bg-danger/10` | +| Type | `text-xs` 12px, `text-sm` 13px, `text-base` 14px (body), `text-lg`...`text-2xl` for titles; `font-medium`, `font-semibold`; `font-mono` for paths and logs | +| Shape | `rounded-lg` (controls), `rounded-xl` (cards, callouts), `rounded-2xl` (dialogs), `rounded-full` (pills); `shadow-lift` for floating surfaces | +| Layout | `flex`, `grid grid-cols-1`...`grid-cols-12`, `gap-*`, `p-*`, `px-*`, `py-*`, `m*-*` on the 0.5-24 scale, `w-*`/`h-*`/`size-*` 0-96, `max-w-xs`...`max-w-7xl` | + +The tokens behind them are CSS variables: `--canvas`, `--panel`, `--sunken`, `--line`, +`--line-strong`, `--ink`, `--ink-soft`, `--muted`, `--accent`, `--ok`, `--work`, `--warn`, +`--danger`. Use `var(--accent)` in inline styles; never hard-code hex for UI colors. +Bot colors are the exception: each bot has its own (`#FF7A59` Botloft orange, `#4FC382`, `#6EA6FF`, +`#F0B24A`, `#C084FC`), passed to `BotAvatar`. + +## Components and their roles + +- `BotAvatar` is the mascot: `color`, `size`, `mood` (idle, working, waiting, tired, sleeping; + it animates), `framed` for Botloft itself on its black square. +- `BotStateBadge` shows a bot's state with icon and word; `ChiefBadge` marks a crew's lead bot. +- `Button` variants: `primary` (one per view), `secondary`, `ghost` (toolbars), `danger`. +- `Dialog`, `Confirm` are modal (fixed, covering the window). `Callout` for notices in a page, + `Toaster` + `notifyError(what, error)` for failures away from a form. +- Fields: `TextField`, `TextArea`, `SelectField` (labeled), `Select` (custom dropdown), + `Choices` (2-6 options side by side), `Switch`, `Tabs`, `Menu`, `Details`, `SidePanel`. + +Read `components///.prompt.md` for each one's props. + +## Voice + +Plain words for people who are not programmers. Name bots by their name ("Scout is waiting for +you"), never "the agent" or "Claude". Never say "daemon"; say "Botloft" or "in the background". + +## Example + +```jsx +const { BotAvatar, BotStateBadge, Button, Callout, icons, setLocaleChoice } = window.Botloft; +setLocaleChoice("en"); + +function CrewCard() { + return ( +
+
+ +
+

Scout

+ +
+
+ + It wants to open arxiv.org for the first time. + +
+ ); +} +``` diff --git a/.design-sync/pkg/build.mjs b/.design-sync/pkg/build.mjs new file mode 100644 index 00000000..fb4c34d0 --- /dev/null +++ b/.design-sync/pkg/build.mjs @@ -0,0 +1,137 @@ +// Builds @botloft/ui into dist/: an ES module with the components (React +// left external), the app's compiled stylesheet with its fonts, and .d.ts +// files for the converter. Uses the app's own Vite, Tailwind and TypeScript +// from app/node_modules, so the output matches what the app ships. +// +// node .design-sync/pkg/build.mjs + +import { execFileSync } from "node:child_process"; +import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const app = resolve(here, "../../app"); +const dist = join(here, "dist"); +const require = createRequire(join(app, "package.json")); +const load = async (name) => import(pathToFileURL(require.resolve(name)).href); + +rmSync(dist, { recursive: true, force: true }); + +// The icons the app uses, as `icons.` (Button takes one as `icon`). +// Read from the app's imports so the set follows the app. +const iconNames = new Set(); +const walk = (dir) => { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const path = join(dir, entry.name); + if (entry.isDirectory()) walk(path); + else if (/\.tsx?$/.test(entry.name) && !/\.test\./.test(entry.name)) { + for (const match of readFileSync(path, "utf8").matchAll(/import \{([^}]*)\} from "lucide-react"/g)) { + for (const name of match[1].split(",")) { + const bare = name.replace(/^\s*type\s+/, "").trim(); + if (/^[A-Z]\w*$/.test(bare) && !/^Lucide/.test(bare) && !/^\s*type\s/.test(name)) { + iconNames.add(bare); + } + } + } + } + } +}; +walk(join(app, "src")); +const iconsModule = `// Generated by build.mjs from the app's lucide-react imports.\nexport { ${[...iconNames].sort().join(", ")} } from "lucide-react";\n`; +writeFileSync(join(here, "icons.gen.ts"), iconsModule); + +const { build } = await load("vite"); +const tailwindcss = (await load("@tailwindcss/vite")).default; +const react = (await load("@vitejs/plugin-react")).default; + +// Library mode inlines every asset, so the fonts would land in the CSS as +// base64, every alphabet included. They are left out here and ship as +// files instead (cfg.extraFonts: the Latin subsets the app needs). +const noFonts = join(here, ".no-fonts.css"); +writeFileSync(noFonts, ""); + +// The app is the root so Tailwind scans app/src and emits every utility +// the app uses: the design agent can then use the same class vocabulary. +try { +await build({ + root: app, + configFile: false, + logLevel: "warn", + plugins: [react(), tailwindcss()], + resolve: { + alias: [ + { find: /^@fontsource\/.*\.css$/, replacement: noFonts }, + // icons.gen.ts sits outside the app, beyond the reach of its node_modules. + { find: /^lucide-react$/, replacement: join(app, "node_modules", "lucide-react") }, + ], + }, + build: { + target: "chrome120", + outDir: dist, + emptyOutDir: true, + minify: false, + lib: { + entry: join(here, "index.ts"), + formats: ["es"], + fileName: () => "index.js", + cssFileName: "styles", + }, + rollupOptions: { + external: (id) => /^react(-dom)?(\/|$)/.test(id), + }, + }, +}); +} finally { + rmSync(noFonts, { force: true }); +} + +// Types: declarations for the app files the entry re-exports, then an +// index.d.ts that is the entry with its paths pointed at them. +const entry = readFileSync(join(here, "index.ts"), "utf8"); +const sources = [...entry.matchAll(/from "\.\.\/\.\.\/app\/src\/([^"]+)"/g)].map( + (match) => + existsSync(join(app, `src/${match[1]}.tsx`)) ? `src/${match[1]}.tsx` : `src/${match[1]}/index.ts`, +); +const tsconfig = join(here, "tsconfig.types.json"); +writeFileSync( + tsconfig, + `${JSON.stringify( + { + extends: join(app, "tsconfig.json").replaceAll("\\", "/"), + compilerOptions: { + noEmit: false, + declaration: true, + emitDeclarationOnly: true, + rootDir: join(app, "src").replaceAll("\\", "/"), + outDir: join(dist, "types", "app").replaceAll("\\", "/"), + types: [], + typeRoots: [join(app, "node_modules", "@types").replaceAll("\\", "/")], + }, + files: sources.map((source) => join(app, source).replaceAll("\\", "/")), + include: [], + }, + null, + 2, + )}\n`, +); +try { + const tsc = join(dirname(require.resolve("typescript/package.json")), "bin", "tsc"); + execFileSync(process.execPath, [tsc, "-p", tsconfig], { + stdio: "inherit", + }); +} finally { + rmSync(tsconfig, { force: true }); +} +writeFileSync( + join(dist, "types", "index.d.ts"), + entry + .split("\n") + .filter((line) => !line.startsWith("import ") && !line.startsWith("//")) + .join("\n") + .replaceAll("../../app/src/", "./app/") + .trimStart(), +); +writeFileSync(join(dist, "types", "icons.gen.d.ts"), iconsModule); +console.log(`built ${dist}`); diff --git a/.design-sync/pkg/index.ts b/.design-sync/pkg/index.ts new file mode 100644 index 00000000..b5b75d2a --- /dev/null +++ b/.design-sync/pkg/index.ts @@ -0,0 +1,25 @@ +// The pieces of Botloft's interface that the design system shares with +// claude.ai/design: the app's own components, re-exported as they are, and +// its stylesheet (tokens, fonts and the Tailwind utilities the app uses). +// Built by build.mjs; nothing here reimplements a component. + +import "./styles.css"; + +export { BOTLOFT_COLOR, BotAvatar, ListAvatar, type Mood } from "../../app/src/features/bots/BotAvatar"; +export { BotStateBadge } from "../../app/src/features/bots/BotStateBadge"; +export { ChiefBadge } from "../../app/src/features/bots/ChiefBadge"; +export { Button, type ButtonVariant } from "../../app/src/ui/Button"; +export { Callout } from "../../app/src/ui/Callout"; +export { Choices } from "../../app/src/ui/Choices"; +export { Confirm } from "../../app/src/ui/Confirm"; +export { Details } from "../../app/src/ui/Details"; +export { Dialog } from "../../app/src/ui/Dialog"; +export { SelectField, TextArea, TextField } from "../../app/src/ui/Field"; +export { Menu, type MenuItem } from "../../app/src/ui/Menu"; +export { Select } from "../../app/src/ui/Select"; +export { SidePanel } from "../../app/src/ui/SidePanel"; +export { Switch } from "../../app/src/ui/Switch"; +export { type Tab, Tabs } from "../../app/src/ui/Tabs"; +export { notifyError, Toaster } from "../../app/src/ui/toast"; +export { setLocaleChoice } from "../../app/src/i18n"; +export * as icons from "./icons.gen"; diff --git a/.design-sync/pkg/package.json b/.design-sync/pkg/package.json new file mode 100644 index 00000000..60755dd0 --- /dev/null +++ b/.design-sync/pkg/package.json @@ -0,0 +1,9 @@ +{ + "name": "@botloft/ui", + "version": "0.8.0", + "private": true, + "description": "Botloft's interface pieces, built from the app's own source for claude.ai/design. Not published.", + "type": "module", + "module": "dist/index.js", + "types": "dist/types/index.d.ts" +} diff --git a/.design-sync/pkg/styles.css b/.design-sync/pkg/styles.css new file mode 100644 index 00000000..c3da96ea --- /dev/null +++ b/.design-sync/pkg/styles.css @@ -0,0 +1,28 @@ +/* The design system's stylesheet: the app's own (tokens, fonts, motion and + every utility the app uses) plus a fixed layout vocabulary, so a design + built with these components can count on common utilities that the app + itself happens not to use. */ +@import "../../app/src/index.css"; + +/* The previews' own layout. */ +@source "../previews"; + +/* Spacing, sizing and layout. */ +@source inline("{p,px,py,pt,pr,pb,pl,m,mx,my,mt,mr,mb,ml,gap,gap-x,gap-y,space-x,space-y}-{0,0.5,1,1.5,2,2.5,3,3.5,4,5,6,7,8,9,10,12,14,16,20,24}"); +@source inline("{w,h,min-w,min-h,size}-{0,1,2,3,4,5,6,7,8,9,10,11,12,14,16,20,24,28,32,36,40,44,48,52,56,60,64,72,80,96,full,screen,auto,fit,min,max}"); +@source inline("{max-w,w}-{3xs,2xs,xs,sm,md,lg,xl,2xl,3xl,4xl,5xl,6xl,7xl,full,none,prose}"); +@source inline("{grid-cols,grid-rows,col-span,row-span}-{1,2,3,4,5,6,7,8,9,10,11,12,full}"); +@source inline("{flex,flex-row,flex-col,flex-wrap,flex-1,flex-none,grow,shrink-0,grid,block,inline-flex,inline-block,hidden,contents}"); +@source inline("{items,self}-{start,center,end,stretch,baseline}"); +@source inline("{justify,content}-{start,center,end,between,around,evenly}"); +@source inline("{relative,absolute,fixed,sticky,inset-0,top-0,right-0,bottom-0,left-0,overflow-hidden,overflow-auto,overflow-y-auto,truncate,whitespace-nowrap,break-words}"); + +/* Color, border and type on the design tokens. */ +@source inline("{bg,text,border,divide,ring,outline,fill,stroke}-{canvas,panel,sunken,line,line-strong,ink,ink-soft,muted,accent,ok,work,warn,danger,quiet,read,transparent,white,black}"); +@source inline("{bg,text,border}-{accent,ok,work,warn,danger}/{5,10,15,20,30,40,50,60}"); +@source inline("{border,border-t,border-r,border-b,border-l,border-0,border-2,divide-x,divide-y}"); +@source inline("rounded{,-none,-sm,-md,-lg,-xl,-2xl,-3xl,-full}"); +@source inline("{shadow-lift,shadow-sm,shadow-none}"); +@source inline("text-{xs,sm,base,lg,xl,2xl,3xl,4xl,5xl,left,center,right}"); +@source inline("{font-sans,font-mono,font-normal,font-medium,font-semibold,font-bold,tracking-tight,leading-tight,leading-snug,leading-relaxed,uppercase,italic,underline}"); +@source inline("{opacity-0,opacity-50,opacity-75,opacity-100,transition,transition-colors,duration-150,duration-200,duration-300}"); diff --git a/.design-sync/previews/BotAvatar.tsx b/.design-sync/previews/BotAvatar.tsx new file mode 100644 index 00000000..64427a2e --- /dev/null +++ b/.design-sync/previews/BotAvatar.tsx @@ -0,0 +1,71 @@ +import { BotAvatar, setLocaleChoice } from "@botloft/ui"; + +// The app follows the browser's language; the cards are in English. +setLocaleChoice("en"); + +const crew = [ + { name: "Chief", color: "#FF7A59" }, + { name: "Scout", color: "#4FC382" }, + { name: "Writer", color: "#6EA6FF" }, + { name: "Editor", color: "#F0B24A" }, + { name: "Ops", color: "#C084FC" }, +]; + +export function Colors() { + return ( +
+ {crew.map((bot) => ( +
+ + {bot.name} +
+ ))} +
+ ); +} + +export function Moods() { + const moods = ["idle", "working", "waiting", "tired", "sleeping"] as const; + return ( +
+ {moods.map((mood) => ( +
+ + {mood} +
+ ))} +
+ ); +} + +export function Sizes() { + return ( +
+ {[20, 28, 40, 64].map((size) => ( + + ))} +
+ ); +} + +export function Framed() { + return ( +
+ +
+

Botloft

+

The app's own mark, on the black square of its icon.

+
+
+ ); +} + +export function DarkTheme() { + return ( +
+ {crew.map((bot) => ( + + ))} +
+ ); +} diff --git a/.design-sync/previews/BotStateBadge.tsx b/.design-sync/previews/BotStateBadge.tsx new file mode 100644 index 00000000..ec8c01a4 --- /dev/null +++ b/.design-sync/previews/BotStateBadge.tsx @@ -0,0 +1,59 @@ +import { BotStateBadge, setLocaleChoice } from "@botloft/ui"; + +// The app follows the browser's language; the cards are in English. +setLocaleChoice("en"); + +type State = + | "offline" + | "launching" + | "idle" + | "busy" + | "needs_approval" + | "rate_limited" + | "auth_error" + | "backoff" + | "archived"; + +const states: State[] = [ + "idle", + "busy", + "needs_approval", + "launching", + "rate_limited", + "backoff", + "auth_error", + "offline", +]; + +export function States() { + return ( +
+ {states.map((state) => ( + + ))} + +
+ ); +} + +export function Compact() { + return ( +
+ + + + +
+ ); +} + +export function DarkTheme() { + return ( +
+ + + + +
+ ); +} diff --git a/.design-sync/previews/Button.tsx b/.design-sync/previews/Button.tsx new file mode 100644 index 00000000..f356fc71 --- /dev/null +++ b/.design-sync/previews/Button.tsx @@ -0,0 +1,66 @@ +import { Button, icons, setLocaleChoice } from "@botloft/ui"; + +// The app follows the browser's language; the cards are in English. +setLocaleChoice("en"); + +export function Variants() { + return ( +
+ + + + +
+ ); +} + +export function WithIcons() { + return ( +
+ + +
+ ); +} + +export function Sizes() { + return ( +
+ + +
+ ); +} + +export function Disabled() { + return ( +
+ + +
+ ); +} + +export function DarkTheme() { + return ( +
+ + + + +
+ ); +} diff --git a/.design-sync/previews/Callout.tsx b/.design-sync/previews/Callout.tsx new file mode 100644 index 00000000..921db5d1 --- /dev/null +++ b/.design-sync/previews/Callout.tsx @@ -0,0 +1,50 @@ +import { Button, Callout, setLocaleChoice } from "@botloft/ui"; + +// The app follows the browser's language; the cards are in English. +setLocaleChoice("en"); + +export function Info() { + return ( +
+ + They pick up where they left off after a restart, too. + +
+ ); +} + +export function Warn() { + return ( +
+ + It continues on its own at 4:10 PM. + +
+ ); +} + +export function DangerWithAction() { + return ( +
+ + Sign in + + } + > + Your bots stopped until you sign in again. + +
+ ); +} + +export function TitleOnly() { + return ( +
+ +
+ ); +} diff --git a/.design-sync/previews/ChiefBadge.tsx b/.design-sync/previews/ChiefBadge.tsx new file mode 100644 index 00000000..64885d1c --- /dev/null +++ b/.design-sync/previews/ChiefBadge.tsx @@ -0,0 +1,39 @@ +import { BotAvatar, BotStateBadge, ChiefBadge, setLocaleChoice } from "@botloft/ui"; + +// The app follows the browser's language; the cards are in English. +setLocaleChoice("en"); + +const crew = { name: "Research" }; + +export function Badge() { + return ( +
+ +
+ ); +} + +export function InBotHeader() { + return ( +
+ +
+
+ Chief + +
+ +
+
+ ); +} + +export function Compact() { + return ( +
+ + Chief + +
+ ); +} diff --git a/.design-sync/previews/Choices.tsx b/.design-sync/previews/Choices.tsx new file mode 100644 index 00000000..c887b7e2 --- /dev/null +++ b/.design-sync/previews/Choices.tsx @@ -0,0 +1,46 @@ +import { Choices, setLocaleChoice } from "@botloft/ui"; +import { useState } from "react"; + +// The app follows the browser's language; the cards are in English. +setLocaleChoice("en"); + +export function Model() { + const [model, setModel] = useState("sonnet"); + return ( +
+ Model + +
+ ); +} + +export function WaitTime() { + const [minutes, setMinutes] = useState(60); + return ( +
+ How long a request waits for you + +
+ ); +} diff --git a/.design-sync/previews/Confirm.tsx b/.design-sync/previews/Confirm.tsx new file mode 100644 index 00000000..d29d1c38 --- /dev/null +++ b/.design-sync/previews/Confirm.tsx @@ -0,0 +1,24 @@ +import { Confirm, setLocaleChoice } from "@botloft/ui"; + +// The app follows the browser's language; the cards are in English. +setLocaleChoice("en"); + +// Confirm opens a Dialog, which covers the window (position: fixed). The +// transform makes this box its containing block. +const stage = { position: "relative", height: 400, transform: "translateZ(0)" } as const; + +export function ArchiveBot() { + return ( +
+ {}} + onClose={() => {}} + > + Scout stops now and leaves the sidebar. Botloft keeps its conversation, and you can delete it + later in Settings > Archived. + +
+ ); +} diff --git a/.design-sync/previews/Details.tsx b/.design-sync/previews/Details.tsx new file mode 100644 index 00000000..cd8dec28 --- /dev/null +++ b/.design-sync/previews/Details.tsx @@ -0,0 +1,35 @@ +import { Callout, Details, setLocaleChoice } from "@botloft/ui"; + +// The app follows the browser's language; the cards are in English. +setLocaleChoice("en"); + +export function Closed() { + return ( +
+

Scout could not start.

+
claude exited with code 1 after 2.3 s
+
+ ); +} + +export function Open() { + return ( +
+

Scout could not start.

+
+ {"claude exited with code 1 after 2.3 s\nNo conversation found with session ID 4f2c…"} +
+
+ ); +} + +export function InCallout() { + return ( +
+ + Botloft keeps working on the current version. +
signature check failed for Botloft_0.8.1_x64-setup.exe
+
+
+ ); +} diff --git a/.design-sync/previews/Dialog.tsx b/.design-sync/previews/Dialog.tsx new file mode 100644 index 00000000..e7950726 --- /dev/null +++ b/.design-sync/previews/Dialog.tsx @@ -0,0 +1,54 @@ +import { Button, Dialog, setLocaleChoice, TextArea, TextField } from "@botloft/ui"; + +// The app follows the browser's language; the cards are in English. +setLocaleChoice("en"); + +// The dialog covers the window (position: fixed). The transform makes this +// box its containing block, so the card shows it at a real size. +const stage = { position: "relative", height: 460, transform: "translateZ(0)" } as const; + +export function EditBot() { + return ( +
+ {}} + footer={ + <> + + + + } + > +
+ +