Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/src/features/bots/ChiefBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function ChiefBadge({ crew, compact = false }: { crew: Crew; compact?: bo
return (
<span
title={c.hint(crew.name)}
className="inline-flex shrink-0 items-center gap-1 rounded-full bg-accent/10 px-2 py-0.5 font-medium text-accent text-xs"
className="inline-flex shrink-0 items-center gap-1 rounded-full bg-accent/10 px-2 py-0.5 font-medium text-accent-text text-xs"
>
<Crown aria-hidden size={12} />
{c.badge}
Expand Down
4 changes: 2 additions & 2 deletions app/src/features/chat/EffortPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export function EffortPicker({ bot, onLater }: { bot: Bot; onLater(text: string)
left: `${(at(recommended) / LAST) * 100}%`,
transform: `translateX(${EDGE[at(recommended)] ?? "-50%"})`,
}}
className="absolute top-2.5 whitespace-nowrap text-[0.6875rem] text-accent"
className="absolute top-2.5 whitespace-nowrap text-[0.6875rem] text-accent-text"
>
{m.recommended}
</span>
Expand Down Expand Up @@ -170,7 +170,7 @@ export function EffortPicker({ bot, onLater }: { bot: Bot; onLater(text: string)
pending.current = null;
void set("default", recommended ? m.names[recommended] : m.recommended);
}}
className="shrink-0 rounded-md px-1.5 py-0.5 font-medium text-ink-soft text-xs hover:bg-sunken hover:text-ink"
className="shrink-0 rounded-lg px-1.5 py-0.5 font-medium text-ink-soft text-xs hover:bg-sunken hover:text-ink"
>
{m.useRecommended}
</button>
Expand Down
4 changes: 2 additions & 2 deletions app/src/features/chat/InboundRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function TaskTag({ message }: { message: Message }) {
);
if (message.kind === "routine") {
return (
<span className="inline-flex items-center gap-1 rounded-md border border-accent/40 px-1.5 text-accent text-xs">
<span className="inline-flex items-center gap-1 rounded-lg border border-accent/40 px-1.5 text-accent-text text-xs">
<AlarmClock aria-hidden size={11} />
{t.routines.tag(routine?.name ?? t.routines.tab)}
</span>
Expand All @@ -60,7 +60,7 @@ function TaskTag({ message }: { message: Message }) {
}
const Icon = message.kind === "task" ? ListTodo : Reply;
return (
<span className="inline-flex items-center gap-1 rounded-md border border-line px-1.5 text-muted text-xs">
<span className="inline-flex items-center gap-1 rounded-lg border border-line px-1.5 text-muted text-xs">
<Icon aria-hidden size={11} />
{message.kind === "task" ? t.chat.inbound.task : t.chat.inbound.result}
</span>
Expand Down
2 changes: 1 addition & 1 deletion app/src/features/chat/ModePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export function ModePicker({ bot, onLater }: { bot: Bot; onLater(text: string):
}
}}
trailing={
<span className="shrink-0 rounded-md border border-danger/50 px-2 py-0.5 font-medium text-danger text-xs">
<span className="shrink-0 rounded-lg border border-danger/50 px-2 py-0.5 font-medium text-danger text-xs">
{m.turnOn}
</span>
}
Expand Down
6 changes: 3 additions & 3 deletions app/src/features/chat/ToolLines.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ function ToolLine({ tool, createdAt }: { tool: ToolItem; createdAt: number }) {
title={t.files.showInPanel}
aria-label={`${t.files.showInPanel}: ${tool.summary}`}
onClick={() => showFile(tool.file as string)}
className="grid size-6 shrink-0 place-items-center rounded-md text-muted transition-colors hover:bg-sunken hover:text-ink"
className="grid size-6 shrink-0 place-items-center rounded-lg text-muted transition-colors hover:bg-sunken hover:text-ink"
>
<Files aria-hidden size={14} />
</button>
Expand All @@ -145,7 +145,7 @@ function ToolLine({ tool, createdAt }: { tool: ToolItem; createdAt: number }) {
title={t.screens.showInPanel}
aria-label={`${t.screens.showInPanel}: ${tool.summary}`}
onClick={() => showScreen(tool.file as string)}
className="grid size-6 shrink-0 place-items-center rounded-md text-muted transition-colors hover:bg-sunken hover:text-ink"
className="grid size-6 shrink-0 place-items-center rounded-lg text-muted transition-colors hover:bg-sunken hover:text-ink"
>
<LayoutTemplate aria-hidden size={14} />
</button>
Expand All @@ -156,7 +156,7 @@ function ToolLine({ tool, createdAt }: { tool: ToolItem; createdAt: number }) {
title={t.browser.showInPanel}
aria-label={`${t.browser.showInPanel}: ${title}`}
onClick={() => showBrowser()}
className="grid size-6 shrink-0 place-items-center rounded-md text-muted transition-colors hover:bg-sunken hover:text-ink"
className="grid size-6 shrink-0 place-items-center rounded-lg text-muted transition-colors hover:bg-sunken hover:text-ink"
>
<Globe aria-hidden size={14} />
</button>
Expand Down
2 changes: 1 addition & 1 deletion app/src/features/crews/CrewView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export function CrewView({ crew }: { crew: Crew }) {
type="button"
onClick={openFolder}
title={words.openFolder}
className="flex min-w-0 items-center gap-1 rounded-md px-1 hover:bg-sunken hover:text-ink"
className="flex min-w-0 items-center gap-1 rounded-lg px-1 hover:bg-sunken hover:text-ink"
>
<Folder aria-hidden size={13} className="shrink-0" />
<span className="truncate">{words.folder(crew.workFolder)}</span>
Expand Down
6 changes: 3 additions & 3 deletions app/src/features/crews/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function Sidebar() {
aria-current={overview ? "page" : undefined}
title={words.showAll}
onClick={() => selectCrew(null)}
className={`rounded-md px-2 py-1 font-semibold text-xs uppercase tracking-[0.12em] transition-colors hover:bg-sunken hover:text-ink ${overview ? "bg-sunken text-ink" : "text-muted"}`}
className={`rounded-lg px-2 py-1 font-semibold text-xs uppercase tracking-[0.12em] transition-colors hover:bg-sunken hover:text-ink ${overview ? "bg-sunken text-ink" : "text-muted"}`}
>
{words.label}
</button>
Expand Down Expand Up @@ -98,7 +98,7 @@ function CrewEntry({ crew }: { crew: Crew }) {
aria-label={collapsed ? words.expand(crew.name) : words.collapse(crew.name)}
title={collapsed ? words.expand(crew.name) : words.collapse(crew.name)}
onClick={() => setCollapsed(crew.id, !collapsed)}
className="absolute left-1 z-10 grid size-6 place-items-center rounded-md text-muted hover:bg-line hover:text-ink"
className="absolute left-1 z-10 grid size-6 place-items-center rounded-lg text-muted hover:bg-line hover:text-ink"
>
<ChevronDown
aria-hidden
Expand Down Expand Up @@ -179,7 +179,7 @@ const Conversation = memo(function Conversation({ bot, crew }: { bot: Bot; crew:
</span>
{activity && (
<time
className={`shrink-0 text-xs ${unread ? "font-semibold text-accent" : "text-muted"}`}
className={`shrink-0 text-xs ${unread ? "font-semibold text-accent-text" : "text-muted"}`}
dateTime={new Date(activity.at).toISOString()}
>
{when(activity.at)}
Expand Down
2 changes: 1 addition & 1 deletion app/src/features/messages/MessageRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function TaskTag({ message, task }: { message: Message; task: Task | undefined }
: text.taskStatus[task.status];
}
return (
<span className="inline-flex items-center gap-1 rounded-md border border-line px-1.5 text-muted text-xs">
<span className="inline-flex items-center gap-1 rounded-lg border border-line px-1.5 text-muted text-xs">
<Icon aria-hidden size={11} />
{message.kind === "task" ? text.task : text.result}
{detail && <span>· {detail}</span>}
Expand Down
2 changes: 1 addition & 1 deletion app/src/features/routines/ScheduleFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function ScheduleFields({
onClick={() => toggleDay(day)}
className={`grid h-8 w-10 place-items-center rounded-lg border text-sm transition-colors ${
on
? "border-accent bg-accent/12 font-medium text-accent"
? "border-accent bg-accent/12 font-medium text-accent-text"
: "border-line-strong text-ink-soft hover:bg-sunken"
}`}
>
Expand Down
4 changes: 2 additions & 2 deletions app/src/features/screens/ScreenFocus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function ScreenFocus({
title={t.devices[one]}
aria-label={t.devices[one]}
onClick={() => onDevice(one)}
className={`grid size-7 place-items-center rounded-md transition-colors ${
className={`grid size-7 place-items-center rounded-lg transition-colors ${
one === device ? "bg-panel text-ink shadow-sm" : "text-muted hover:text-ink"
}`}
>
Expand All @@ -85,7 +85,7 @@ export function ScreenFocus({
/>
</div>
<div ref={stage} className="design-board min-h-0 flex-1 overflow-auto p-4">
<div className="mx-auto w-fit overflow-hidden rounded-md shadow-lift ring-1 ring-line">
<div className="mx-auto w-fit overflow-hidden rounded-lg shadow-lift ring-1 ring-line">
<LiveFrame
url={screen.url}
device={device}
Expand Down
4 changes: 2 additions & 2 deletions app/src/features/screens/ScreensPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export function ScreensPanel({
type="button"
onClick={() => setZoom("fit")}
title={t.fit}
className="h-7 min-w-12 rounded-md px-1.5 font-mono text-muted text-xs tabular-nums transition-colors hover:bg-sunken hover:text-ink"
className="h-7 min-w-12 rounded-lg px-1.5 font-mono text-muted text-xs tabular-nums transition-colors hover:bg-sunken hover:text-ink"
>
{Math.round(scale * 100)}%
</button>
Expand Down Expand Up @@ -226,7 +226,7 @@ function Artboard({
)}
</p>
<div
className="relative overflow-hidden rounded-md shadow-lift ring-1 ring-line transition-shadow"
className="relative overflow-hidden rounded-lg shadow-lift ring-1 ring-line transition-shadow"
style={
screen.writing
? { boxShadow: `0 0 0 2px ${bot.color}, 0 0 28px ${bot.color}55` }
Expand Down
22 changes: 17 additions & 5 deletions app/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,27 @@
--line-strong: #b5b5ae;
--ink: #121212;
--ink-soft: #3b3b38;
--muted: #6f6f69;
--muted: #5f5f59;
--accent: #d9481f;
--ok: #1d7f46;
/* The accent as text: the fill above reads under 4.5:1 on the light
backgrounds, so words in it use this darker one (spec 15.3). */
--accent-text: #b83a14;
--ok: #17703d;
--work: #1f5fc4;
--warn: #a35f00;
--danger: #c2372a;
--idle-state: #6f6f69;
--warn: #8a5000;
--danger: #bd3529;
--idle-state: #5f5f59;
/* The check marks of a message the bot has read: a lighter blue than
--work, still 3:1 or more on canvas, panel and sunken in both themes. */
--read: #1787c9;
/* The thin edge around a bot's avatar, so light colors hold their shape. */
--avatar-edge: rgb(0 0 0 / 0.2);
/* Under menus, pickers and dialogs. */
--lift: rgb(0 0 0 / 0.12);
/* Behind a dialog. */
--scrim: rgb(0 0 0 / 0.5);
/* Windows' close button on hover, the same in both themes. */
--close: #c42b1c;
}

[data-theme="dark"] {
Expand All @@ -55,6 +62,7 @@
--ink-soft: #c6c6c0;
--muted: #8c8c86;
--accent: #ff7a59;
--accent-text: #ff7a59;
--ok: #4fc382;
--work: #6ea6ff;
--warn: #f0b24a;
Expand All @@ -63,6 +71,7 @@
--read: #5cc4f2;
--avatar-edge: rgb(255 255 255 / 0.14);
--lift: rgb(0 0 0 / 0.5);
--scrim: rgb(0 0 0 / 0.6);
}

@theme inline {
Expand All @@ -75,6 +84,9 @@
--color-ink-soft: var(--ink-soft);
--color-muted: var(--muted);
--color-accent: var(--accent);
--color-accent-text: var(--accent-text);
--color-scrim: var(--scrim);
--color-close: var(--close);
--color-ok: var(--ok);
--color-work: var(--work);
--color-warn: var(--warn);
Expand Down
2 changes: 1 addition & 1 deletion app/src/setup/Setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function CloseButton({ onClose }: { onClose: () => void }) {
type="button"
aria-label={t.shell.window.close}
onClick={onClose}
className="grid h-9 w-11 place-items-center text-ink-soft hover:bg-[#c42b1c] hover:text-white"
className="grid h-9 w-11 place-items-center text-ink-soft hover:bg-close hover:text-white"
>
<X aria-hidden size={16} strokeWidth={1.5} />
</button>
Expand Down
2 changes: 1 addition & 1 deletion app/src/shell/TitleBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ function WindowControls() {
type="button"
aria-label={t.shell.window.close}
onClick={() => window.close()}
className={`${control} hover:bg-[#c42b1c] hover:text-white`}
className={`${control} hover:bg-close hover:text-white`}
>
<X aria-hidden size={16} strokeWidth={1.5} />
</button>
Expand Down
2 changes: 1 addition & 1 deletion app/src/shell/sidebarToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function SidebarToggle() {
title={`${open ? t.hide : t.show} (Ctrl+B)`}
aria-pressed={!open}
onClick={toggleSidebar}
className="grid h-7 w-7 place-items-center rounded-md text-ink-soft hover:bg-sunken hover:text-ink"
className="grid h-7 w-7 place-items-center rounded-lg text-ink-soft hover:bg-sunken hover:text-ink"
>
<Icon aria-hidden size={15} />
</button>
Expand Down
2 changes: 1 addition & 1 deletion app/src/ui/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function Dialog({ title, onClose, children, footer, width = "md" }: Dialo
}, [onClose]);

return (
<div className="fixed inset-0 z-40 grid animate-fade place-items-center bg-black/50 p-6">
<div className="fixed inset-0 z-40 grid animate-fade place-items-center bg-scrim p-6">
<div
role="dialog"
aria-modal="true"
Expand Down
2 changes: 1 addition & 1 deletion app/src/ui/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function Toaster() {
type="button"
aria-label={t.common.dismiss}
onClick={() => dismiss(toast.id)}
className="grid h-6 w-6 shrink-0 place-items-center rounded-md text-muted hover:bg-sunken hover:text-ink"
className="grid h-6 w-6 shrink-0 place-items-center rounded-lg text-muted hover:bg-sunken hover:text-ink"
>
<X aria-hidden size={14} />
</button>
Expand Down
55 changes: 55 additions & 0 deletions app/src/ui/tokens.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// The color tokens (spec 15.3): every color words are written in reads at
// 4.5:1 or more (WCAG AA) on every background, in both themes.

import { describe, expect, test } from "vitest";
import css from "../index.css?raw";

/** The `--name: #hex` tokens of the first block that opens with `selector {`. */
function tokens(selector: string): Record<string, string> {
const start = css.indexOf(`${selector} {`);
const block = css.slice(start, css.indexOf("}", start));
return Object.fromEntries(
[...block.matchAll(/--([\w-]+):\s*(#[0-9a-f]{6})\b/gi)].map((m) => [m[1], m[2]]),
);
}

function luminance(hex: string): number {
const [r, g, b] = [1, 3, 5].map((at) => {
const c = Number.parseInt(hex.slice(at, at + 2), 16) / 255;
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
}) as [number, number, number];
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

function contrast(a: string, b: string): number {
const [x, y] = [luminance(a), luminance(b)].sort((p, q) => q - p) as [number, number];
return (x + 0.05) / (y + 0.05);
}

const BACKGROUNDS = ["canvas", "panel", "sunken"];
/** Colors text is written in. `accent` is a fill; words use `accent-text`. */
const TEXT = [
"ink",
"ink-soft",
"muted",
"accent-text",
"ok",
"work",
"warn",
"danger",
"idle-state",
];

describe.each([
["light", ":root"],
["dark", '[data-theme="dark"]'],
])("the %s theme", (_name, selector) => {
const theme = tokens(selector);

test.each(TEXT)("%s reads on every background", (text) => {
for (const background of BACKGROUNDS) {
const ratio = contrast(theme[text] as string, theme[background] as string);
expect(ratio, `${text} on ${background}`).toBeGreaterThanOrEqual(4.5);
}
});
});
2 changes: 2 additions & 0 deletions docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,8 @@ Em dev, `pnpm tauri dev` usa o `target\debug\botloftd.exe` como sidecar e instal

Ferramenta de trabalho densa e calma: tipografia forte, grid firme, estados dos bots legíveis de longe (cor + ícone + texto, nunca só cor). Sem gradiente, sem sombra pesada, sem visual de template. Cantos arredondados, como nos apps de chat: botões, campos e linhas de menu com 8 px; cartões, avisos e menus com 12 px; compositor, diálogos e cartões do chat com 16 px. Menus e diálogos flutuam com uma sombra leve (`shadow-lift`, mais forte no tema escuro). Na barra lateral, a conversa aberta é um bloco arredondado recuado das bordas. Tema escuro e claro. Barra de título própria (`decorations: false`) com controles de janela do Windows.

**Cores (tokens).** Toda cor vem de um token em `index.css` (`:root` para o tema claro, `[data-theme="dark"]` para o escuro), exposto ao Tailwind como `--color-*`: fundos `canvas`, `panel` e `sunken`; bordas `line` e `line-strong`; texto `ink`, `ink-soft` e `muted`; estados `ok`, `work`, `warn`, `danger` e `quiet`; `accent`, o laranja do Botloft, como preenchimento (botões, selos, bordas, ícones) e `accent-text` para palavras nessa cor; `read` (os tiques de lida), `scrim` (o fundo atrás de um diálogo) e `close` (o vermelho do botão fechar do Windows). Toda cor de texto passa de 4,5:1 (WCAG AA) sobre os três fundos nos dois temas, e um teste (`ui/tokens.test.ts`) confere isso a cada mudança; no tema claro o laranja de preenchimento não passa como texto, por isso o `accent-text` existe. Ícones podem usar `accent` (pedem 3:1). Ficam fora dos tokens só desenhos: o mascote, o cursor do bot, as cores do seletor de cor e o fundo branco da tela de uma página (22). Os cantos usam só a escala acima (8, 12 e 16 px, `rounded-lg`, `rounded-xl` e `rounded-2xl`) e o círculo (`rounded-full`).

Tamanho: a janela inteira é desenhada numa escala (zoom do webview, `setZoom`), então texto, espaçamento, ícones e avatares crescem juntos. Níveis 100%, 110%, 125% e 150%; o padrão é **125%**, porque o desenho a 100% ficava miúdo num monitor sem ampliação do Windows. Quem já usa a ampliação alta volta para 100% em Configurações (área da conta) ou com Ctrl+-; Ctrl+= aumenta e Ctrl+0 volta ao padrão. A escolha fica no `localStorage` (`botloft.zoom`) e é aplicada antes da primeira pintura, para a janela não piscar pequena. Acima de 150% a janela mínima (900 px) não cabe o layout.

Movimento: o app se mexe para parecer vivo, sem atrapalhar o trabalho. Animações curtas (150 a 400 ms, saída suave) em CSS, sem biblioteca (`motion.css`), e todas param quando o Windows desliga os efeitos de animação (`prefers-reduced-motion`) ou quando o dono liga "Menos animações" nas Configurações (`motion-less.css` repete as mesmas regras para `:root[data-motion="less"]`, e `reducedMotion()` olha os dois).
Expand Down
Loading