diff --git a/docs/decisions.md b/docs/decisions.md index 1d43e188..1a01f5ba 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -4865,6 +4865,19 @@ prototypes-lazurio `1acd615` (`app/v1/src/Rail.tsx`, `Switcher.tsx`, `EnvPicker. before). Every interface icon, the shell's and the Launchpad page's, is drawn at the wireframe's stroke of 1.6 (was 1.5). As the glyph takes the picker's ink, a kind's icon is light on a dark host without roles (it stayed near-black, issue #169). + *Addendum 2026-10-11 (Anna):* the Launchpad's column narrows from 264 to 256 px (four + grid modules) with 13.5 px entries and a 16 px title, and the picker's name drops to + 14 px (`--lz-size-meta`), so a name like "MacBook-Air-2" still fits beside the gear. + Text has three levels, after ClickUp (measured there: #202020, #646464 and + #838383-#8d8d8d): ink for what you act on (a tool's name, a button, the active entry); + muted `gray-500` for what explains it (a settings row's description at 14 px, a + group's label such as Oblíbené or a Tools tier, an inactive entry of the column); and + quiet, halfway between `gray-500` and `gray-400` (3.7:1, as ClickUp's timestamps), + only for text that need not be read (a hint, a neutral state, "Podrobnosti"). + Weights between the design system's 400 and 600, since Inter Tight is variable and + read thin beside ChatGPT's 14 px system text: a description at 450, a group's label + and an inactive entry at 500; names, buttons and the active entry stay at 600. + Proposed to the design system as roles for every app. 6. **The keys, with or without the field.** ↓ and ↑ (wrapping), Home, End, Enter and Escape work in the list. Without the search field the focus lands on the current Environment, else the first (on "Všechny Organizace" where the space has none), and diff --git a/docs/launchpad-development.md b/docs/launchpad-development.md index 029e2034..332d18be 100644 --- a/docs/launchpad-development.md +++ b/docs/launchpad-development.md @@ -1597,12 +1597,18 @@ change (without them; the last known sign-ins stay on the cards) and on "Refresh status" (with them again), and shows: - no introduction (root decision 0188, Matěj 2026-10-04: no paragraph that only - restates the page); each tier's note is one short line: "Bez nich tu nic nefunguje.", - "Hodí se skoro každému.", "Zapni jen to, co používáš."; + restates the page), no note under a tier and no line on when the tools were read + (Anna 2026-10-11: each only restated what the page shows); the tier names are quieter + than the tool names they group (muted, 13 px, weight 500; the levels of text are in + decision F36's addendum of 2026-10-11); - on a shared Environment (the Team preset) the warning that signed-in accounts are shared by all operators; it is repeated in the confirmation of an enable; - three groups, **Required**, **Recommended** and **Optional**, in catalog order, each - a settings group of rows. A row carries on the left the tool's real name (gh, + a settings group of rows. A row carries on the left the tool's logo (Anna 2026-10-11: known at first sight; + `src/tools/icons.ts`, bundled simple-icons, CC0, drawn by the same `appMark` as + Integrace; Composio and Codex carry the vendor's own bundled SVG, the OpenAI Blossom + for Codex; executor, without one, shows its first + letter), its real name (gh, composio, bitwarden, wacli, gogcli, neon: what an agent says and what is searched for), one plain sentence on what it is for (root decision 0188; `toolDescription` in `tools-view.ts`, a page-only text: the catalog's `activation.purpose` is rendered into @@ -1993,7 +1999,7 @@ the Launchpad builds no viewer of its own. Integrace and the Marketplace hides it, the rail staying, and shows it again; while the page steps aside for the panel, the toggle stands in the panel's strip. Its right edge is a divider like the panel's, 208–480 px and never leaving the page less than - 400 px, double-click for 264 px. Below 768 px it stays the sheet opened by the button + 400 px, double-click for 256 px. Below 768 px it stays the sheet opened by the button at the top of the page. - **Remembered in this browser**: the panel's width or all of the room, the column's width and whether it is hidden, as one `localStorage` entry, `lazurio.layout`, read diff --git a/src/launchpad/app-mark.ts b/src/launchpad/app-mark.ts new file mode 100644 index 00000000..7ce5ff3d --- /dev/null +++ b/src/launchpad/app-mark.ts @@ -0,0 +1,37 @@ +import type { CatalogIcon } from "../integrations/catalog-schema"; +import type { MarkSvg } from "../tools/icons"; +import { monogram } from "./integrations-view"; + +/** + * The mark of an app or a tool: its bundled logo in a white square, else its + * first letter. One drawing for Integrace and Settings → Tools, so the same + * app looks the same on both pages. A logo is a simple-icons path in its + * colour, or a vendor's own bundled SVG (Composio). + */ +export function appMark( + name: string, + icon: CatalogIcon | MarkSvg | undefined, +): HTMLElement { + const holder = document.createElement("span"); + holder.className = "integration-mark"; + holder.setAttribute("aria-hidden", "true"); + if (icon === undefined) { + holder.classList.add("integration-mark-letter"); + holder.textContent = monogram(name); + return holder; + } + if ("svg" in icon) { + // A bundled constant, parsed as an SVG document, never as HTML. + const parsed = new DOMParser().parseFromString(icon.svg, "image/svg+xml"); + holder.append(document.importNode(parsed.documentElement, true)); + return holder; + } + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("viewBox", "0 0 24 24"); + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path.setAttribute("d", icon.path); + path.setAttribute("fill", `#${icon.hex}`); + svg.append(path); + holder.append(svg); + return holder; +} diff --git a/src/launchpad/index.html b/src/launchpad/index.html index 430ba982..8bd61fe6 100644 --- a/src/launchpad/index.html +++ b/src/launchpad/index.html @@ -57,6 +57,20 @@ --input: var(--lz-line); --muted: var(--lz-gray-25); --muted-fg: var(--lz-ink-muted); + /* Three levels of text, after ClickUp (Anna 2026-10-11; measured there: + * #202020, #646464 and #838383-#8d8d8d): ink for what you act on (a name, + * the active entry); muted for what explains it (a description, a group's + * label, an inactive entry); quiet for the lowest priority (a hint, a + * neutral state, "Podrobnosti"), halfway between gray-500 and gray-400 at + * 3.7:1, as ClickUp's own timestamps, so used only for text that need not + * be read. */ + --quiet-fg: color-mix(in srgb, var(--lz-gray-500), var(--lz-gray-400)); + /* Weights between the design system's 400 and 600 (Inter Tight is + * variable): text people read at 450, so the tight face does not read thin + * beside system fonts; an entry of the column and a group's label at 500, + * as ClickUp's sidebar; names, buttons and the active entry stay at 600. */ + --weight-text: 450; + --weight-label: 500; --hover: var(--lz-gray-100); --link: var(--lz-accent); --primary: var(--lz-accent-button); @@ -76,7 +90,7 @@ --radius-card: var(--lz-radius-md); --font-sans: var(--lz-font-sans); --font-mono: var(--lz-font-mono); - --column-width: 264px; + --column-width: 256px; --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23707070' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); } *, *::before, *::after { box-sizing: border-box; } @@ -129,18 +143,18 @@ .column { position: sticky; top: 0; z-index: 20; display: flex; flex: none; flex-direction: column; gap: 10px; width: var(--column-width); height: 100vh; padding: 16px 10px 10px; background: var(--lz-gray-25); border-right: 1px solid var(--lz-line-faint); } .column-apps, .column-settings { display: flex; flex: 1; min-height: 0; flex-direction: column; gap: 10px; } .column-scroll { display: flex; flex: 1; min-height: 0; flex-direction: column; gap: 14px; overflow-y: auto; } -.column-title { padding: 0 8px; font-size: 18px; font-weight: var(--lz-weight-title); letter-spacing: var(--lz-track-title); } +.column-title { padding: 0 8px; font-size: 16px; font-weight: var(--lz-weight-title); letter-spacing: var(--lz-track-title); } .column-label { padding: 4px 10px 6px; color: var(--muted-fg); font-size: 13px; } .column-favorites { display: grid; gap: 1px; padding-bottom: 8px; border-bottom: 1px solid var(--lz-line-faint); } .column-hint { padding: 2px 10px 4px; color: var(--muted-fg); font-size: 13px; } .column-foot { display: grid; gap: 8px; padding-top: 8px; border-top: 1px solid var(--lz-line-faint); } .column-foot:not(:has(> :not([hidden]))) { display: none; } .menu { display: grid; gap: 1px; margin: 0; padding: 0; list-style: none; } -.menu-item { display: flex; align-items: center; gap: 10px; min-width: 0; height: 34px; padding: 0 10px; border-radius: var(--lz-radius-sm); color: var(--fg); font-size: 14px; text-decoration: none; } +.menu-item { display: flex; align-items: center; gap: 10px; min-width: 0; height: 32px; padding: 0 10px; border-radius: var(--lz-radius-sm); color: var(--fg); font-size: 13.5px; text-decoration: none; } a.menu-item:hover, button.menu-item:hover { background: var(--lz-gray-50); } -button.menu-item { width: 100%; border: 0; background: transparent; font-weight: 400; cursor: pointer; } +button.menu-item { width: 100%; border: 0; background: transparent; font-weight: var(--weight-label); cursor: pointer; } .menu-item[aria-current="page"] { background: var(--hover); font-weight: var(--lz-weight-akce); } -.menu-item .icon { width: 17px; height: 17px; } +.menu-item .icon { width: 16px; height: 16px; } .menu-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .menu-stone { display: block; flex: none; width: 20px; height: 20px; } .menu-mark { display: grid; flex: none; width: 20px; height: 20px; place-items: center; color: var(--muted-fg); } @@ -285,8 +299,8 @@ .row-copy { display: grid; gap: 3px; min-width: 0; } .row-title { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; min-height: 1.25rem; color: var(--fg); font-size: 15px; font-weight: var(--lz-weight-akce); } .row-title code { color: var(--muted-fg); font-weight: 400; } -.row-desc { max-width: 40rem; color: var(--muted-fg); font-size: 13.5px; line-height: 1.45; } -.row-status { color: var(--muted-fg); font-size: 12.5px; } +.row-desc { max-width: 40rem; color: var(--muted-fg); font-size: 14px; font-weight: var(--weight-text); line-height: 1.45; } +.row-status { color: var(--quiet-fg); font-size: 13px; } .row-control { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-start; gap: 8px; min-width: 0; } @container (min-width: 32rem) { .row-control { justify-content: flex-end; } } dl.card { margin: 0; } @@ -298,7 +312,7 @@ .callout[data-kind="warning"], .tools-warning { border: 1px solid var(--warning-border); border-radius: var(--radius-control); background: var(--warning-bg); color: var(--fg); padding: 10px 14px; font-size: 14px; } .callout[data-kind="warning"] strong { color: var(--warning-fg); } .toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; color: var(--muted-fg); font-size: 12px; } -details.technical > summary, details.row-details > summary { display: inline-flex; align-items: center; gap: 0.25rem; cursor: pointer; color: var(--muted-fg); font-size: 13px; list-style: none; border-radius: 4px; } +details.technical > summary, details.row-details > summary { display: inline-flex; align-items: center; gap: 0.25rem; cursor: pointer; color: var(--quiet-fg); font-size: 13px; list-style: none; border-radius: 4px; } details.technical > summary::-webkit-details-marker, details.row-details > summary::-webkit-details-marker { display: none; } details.technical > summary::before, details.row-details > summary::before { content: ""; width: 0.75rem; height: 0.75rem; background: var(--chevron) center / 0.75rem no-repeat; transform: rotate(-90deg); transition: transform 0.15s; } details[open].technical > summary::before, details[open].row-details > summary::before { transform: none; } @@ -524,6 +538,15 @@ .integration-mark { display: grid; flex: none; width: 40px; height: 40px; place-items: center; border: 1px solid var(--lz-line-faint); border-radius: var(--lz-radius-sm); background: var(--lz-white); } .integration-mark svg { width: 24px; height: 24px; } .integration-mark-letter { color: var(--muted-fg); font-weight: var(--lz-weight-akce); font-size: 17px; background: var(--lz-gray-25); } +/* Settings → Tools: the tool's logo beside its name (Anna 2026-10-11), and + * the tier names quieter than the tool names they group. */ +.row-ident { display: flex; align-items: flex-start; gap: 12px; min-width: 0; } +.row-ident > .integration-mark { margin-top: 1px; } +#tools .group-head h2, .column-label { color: var(--muted-fg); font-size: 13px; font-weight: var(--weight-label); } +.column-hint, .menu-soon { color: var(--quiet-fg); } +.menu-item { color: var(--muted-fg); font-weight: var(--weight-label); } +.menu-item[aria-current="page"] { font-weight: var(--lz-weight-akce); } +a.menu-item:hover, button.menu-item:hover, .menu-item[aria-current="page"] { color: var(--fg); } .integration-text { display: grid; gap: 2px; min-width: 0; } .integration-name { font-size: 15px; font-weight: var(--lz-weight-akce); } .integration-line { color: var(--muted-fg); font-size: 13px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } @@ -966,7 +989,6 @@

Product updat
-

diff --git a/src/launchpad/integrations-panel.ts b/src/launchpad/integrations-panel.ts index bfb3a603..dd61fb30 100644 --- a/src/launchpad/integrations-panel.ts +++ b/src/launchpad/integrations-panel.ts @@ -6,6 +6,7 @@ import type { IntegrationApp, IntegrationsOverview, } from "../integrations/model"; +import { appMark } from "./app-mark"; import { addRow, type CustomForm, @@ -37,7 +38,6 @@ import { cardView, disconnectText, integrationsHelp, - monogram, parseIntegrations, serverLine, sourceLines, @@ -199,22 +199,7 @@ export function createIntegrationsPanel( /** The app's mark: its bundled icon, else its first letter. */ function mark(app: IntegrationApp): HTMLElement { - const entry = catalogOf(app.id); - const holder = element("span", "integration-mark"); - holder.setAttribute("aria-hidden", "true"); - if (entry?.icon === undefined) { - holder.classList.add("integration-mark-letter"); - holder.textContent = monogram(app.name); - return holder; - } - const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg.setAttribute("viewBox", "0 0 24 24"); - const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); - path.setAttribute("d", entry.icon.path); - path.setAttribute("fill", `#${entry.icon.hex}`); - svg.append(path); - holder.append(svg); - return holder; + return appMark(app.name, catalogOf(app.id)?.icon); } const active = () => route !== null; diff --git a/src/launchpad/layout-view.ts b/src/launchpad/layout-view.ts index 54ea53c7..d19ddc05 100644 --- a/src/launchpad/layout-view.ts +++ b/src/launchpad/layout-view.ts @@ -21,10 +21,10 @@ export const panelMinWidth = 360; export const pageMinWidth = 400; /** The left column: T3 Code's narrowest sidebar, which the shell's column - * head is drawn for, the width it had before it could be dragged, and the - * widest that still leaves its navigation a column. */ + * head is drawn for, four grid modules by default (Anna 2026-10-11, was + * 264 px), and the widest that still leaves its navigation a column. */ export const columnMinWidth = 208; -export const columnDefaultWidth = 264; +export const columnDefaultWidth = 256; export const columnMaxWidth = 480; /** The right panel's width as the person left it: pixels, or all of the @@ -159,7 +159,7 @@ export function keyPanel(place: PanelPlace, key: DividerKey): PanelWish | null { export type ColumnPlace = Readonly<{ width: number; min: number; max: number }>; /** The column in an app `appWidth` pixels wide (all right of the rail), as - * the person wished, or 264 px: 208–480 px, and never so wide that the page + * the person wished, or 256 px: 208–480 px, and never so wide that the page * beside it has less than its narrowest width. */ export function placeColumn( appWidth: number, diff --git a/src/launchpad/messages.ts b/src/launchpad/messages.ts index dc61a099..f500a700 100644 --- a/src/launchpad/messages.ts +++ b/src/launchpad/messages.ts @@ -100,13 +100,9 @@ const en = { toolsLoading: "Reading tools…", toolsLoadFailed: "The tools could not be read. Try Refresh status; if that does not help, ask an agent for help.", - toolsChecked: "Checked at {time}", toolsTierRequired: "Required", - toolsTierRequiredNote: "Nothing works here without them.", toolsTierRecommended: "Recommended", - toolsTierRecommendedNote: "Useful to almost everyone.", toolsTierOptional: "Optional", - toolsTierOptionalNote: "Turn on only what you use.", toolsInstalled: "Installed, version {version}", toolsInstalledNoVersion: "Installed, version unknown", toolsNotInstalled: "Not installed", @@ -1420,13 +1416,9 @@ const cs: Record = { toolsLoading: "Načítají se nástroje…", toolsLoadFailed: "Nástroje se nepodařilo načíst. Zkuste Obnovit stav; když to nepomůže, požádejte o pomoc agenta.", - toolsChecked: "Zjištěno v {time}", toolsTierRequired: "Povinné", - toolsTierRequiredNote: "Bez nich tu nic nefunguje.", toolsTierRecommended: "Doporučené", - toolsTierRecommendedNote: "Hodí se skoro každému.", toolsTierOptional: "Volitelné", - toolsTierOptionalNote: "Zapni jen to, co používáš.", toolsInstalled: "Nainstalováno, verze {version}", toolsInstalledNoVersion: "Nainstalováno, verze neznámá", toolsNotInstalled: "Není nainstalováno", diff --git a/src/launchpad/tools-panel.ts b/src/launchpad/tools-panel.ts index a9a38027..2e73b7fc 100644 --- a/src/launchpad/tools-panel.ts +++ b/src/launchpad/tools-panel.ts @@ -1,12 +1,13 @@ import { executorToolName, vaultToolName } from "../tools/catalog"; +import { toolIcons } from "../tools/icons"; import type { ToolOverview, ToolsOverview } from "../tools/overview"; import type { ToolSignIn } from "../tools/status"; +import { appMark } from "./app-mark"; import { createExecutorPanel } from "./executor-panel"; import type { MessageKey } from "./messages"; import { answerWithin, autoEnable, - checkedLine, connectionLine, curatedActions, currentNotes, @@ -79,7 +80,6 @@ export function createToolsPanel( }; const groups = find("#tools-groups"); const shared = find("#tools-shared"); - const checked = find("#tools-checked"); const announce = find("#tools-announce"); const refreshButton = find("#tools-refresh"); const mcpButton = find("#tools-mcp"); @@ -976,6 +976,10 @@ export function createToolsPanel( if (tool.command !== tool.name) title.append(element("code", "", tool.command)); text.append(title, element("p", "row-desc", toolDescription(tool, copy))); + // The tool's logo beside its name, so it is known at first sight (Anna + // 2026-10-11); the same mark as on Integrace. + const ident = element("div", "row-ident"); + ident.append(appMark(tool.name, toolIcons[tool.name]), text); const view = toolStatusView( tool, @@ -1181,7 +1185,7 @@ export function createToolsPanel( field.append(label, toggler); controls.append(field); } - main.append(text, controls); + main.append(ident, controls); item.append(main); const details = element("details", "row-details"); @@ -1340,7 +1344,7 @@ export function createToolsPanel( heading.id = `tools-group-${group.tier}`; section.setAttribute("aria-labelledby", heading.id); const head = element("div", "group-head"); - head.append(heading, element("p", "group-note", group.note)); + head.append(heading); const list = element("ul", "card tools-list"); list.setAttribute("aria-labelledby", heading.id); list.replaceChildren(...group.tools.map((tool) => card(tool, copy))); @@ -1414,16 +1418,10 @@ export function createToolsPanel( // another one means the Folder changed since, elsewhere, and its Undo // would be refused. if (notice !== null && !noticeHolds(notice, overview)) notice = null; - checked.textContent = checkedLine( - new Date(), - parsed.locale, - options.copy(), - ); } catch { if (turn !== sequence) return; overview = null; failed = true; - checked.textContent = ""; say(copy.toolsLoadFailed); } finally { if (turn === sequence) { diff --git a/src/launchpad/tools-view.ts b/src/launchpad/tools-view.ts index f6ef8699..3439a542 100644 --- a/src/launchpad/tools-view.ts +++ b/src/launchpad/tools-view.ts @@ -287,7 +287,6 @@ export function organizationDetails( export type ToolGroup = Readonly<{ tier: Tier; title: string; - note: string; tools: readonly ToolOverview[]; }>; @@ -303,16 +302,17 @@ export function toolGroups( tools: readonly ToolOverview[], copy: Copy, ): readonly ToolGroup[] { - const headings: Record = { - required: ["toolsTierRequired", "toolsTierRequiredNote"], - recommended: ["toolsTierRecommended", "toolsTierRecommendedNote"], - optional: ["toolsTierOptional", "toolsTierOptionalNote"], + // Only the tier's name: a sentence under it ("Bez nich tu nic nefunguje") + // repeats what the name says (Anna 2026-10-11). + const headings: Record = { + required: "toolsTierRequired", + recommended: "toolsTierRecommended", + optional: "toolsTierOptional", }; return tiers .map((tier) => ({ tier, - title: copy[headings[tier][0]], - note: copy[headings[tier][1]], + title: copy[headings[tier]], tools: tools.filter( (tool) => tool.tier === tier && @@ -857,15 +857,6 @@ export function noticeHolds( ); } -/** The line above the tools: when they were read. The Folder and its - * revision are nothing a person needs here (Matěj 2026-10-09); Settings → - * General shows the revision. */ -export function checkedLine(at: Date, locale: "cs" | "en", copy: Copy): string { - return fill(copy.toolsChecked, { - time: at.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" }), - }); -} - /** Only an https address of the catalog becomes a link. */ export function sourceLink(source: string): string | null { try { diff --git a/src/shell/styles.ts b/src/shell/styles.ts index 3d4e0319..68f20690 100644 --- a/src/shell/styles.ts +++ b/src/shell/styles.ts @@ -137,6 +137,9 @@ ${baseCss} ${switcherCss} ${identityCss} :host { display: block; } +/* In the column head the name is one step smaller than in a header, so a + name like "MacBook-Air-2" fits beside the gear (Anna 2026-10-11). */ +.pick .identity-name { font-size: var(--lz-size-meta); } .head { display: flex; flex-direction: column; gap: 8px; } /* The picker and the gear of what it names, side by side (lz-picker-row). */ .row { display: flex; align-items: center; gap: var(--lz-space-4); } diff --git a/src/tools/icons.ts b/src/tools/icons.ts new file mode 100644 index 00000000..f68e2235 --- /dev/null +++ b/src/tools/icons.ts @@ -0,0 +1,80 @@ +import type { CatalogIcon } from "../integrations/catalog-schema"; + +// The logo of each curated tool in Settings → Tools, so a person knows the +// tool at first sight (Anna 2026-10-11). Bundled like the Integrace icons: +// simple-icons (CC0-1.0), never fetched from a third party. A tool without +// one here (executor) shows its first letter, as an app without an icon +// does on Integrace. + +const icon = (path: string, hex: string, slug: string): CatalogIcon => + Object.freeze({ path, hex, source: `simple-icons@16.28.0/${slug}` }); + +/** A logo simple-icons does not have: the vendor's own SVG mark, bundled. */ +export type MarkSvg = Readonly<{ svg: string; source: string }>; + +export const toolIcons: Readonly> = + Object.freeze({ + // Codex: the official OpenAI Blossom, as the Lazurio website uses it to + // identify ChatGPT (website-lazurio brand/partners); Anna 2026-10-11 chose + // it over an unofficial Codex icon. Only its fixed width and height dropped. + codex: Object.freeze({ + svg: '', + source: "https://openai.com/brand (OpenAI Blossom, black)", + }), + // Composio's official mark, as Lazurio Docs uses it (guide-assets/composio.svg). + composio: Object.freeze({ + svg: '', + source: + "https://composio.dev/logos/composio-black.svg (retrieved 2026-09-13)", + }), + gh: icon( + "M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12", + "181717", + "github", + ), + claude: icon( + "m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z", + "D97757", + "claude", + ), + git: icon( + "M13.09 23.549a1.54 1.54 0 0 1-2.18 0L.451 13.089a1.54 1.54 0 0 1 0-2.179l7.191-7.19 2.733 2.733a1.85 1.85 0 0 0 .964 2.326v6.66a1.849 1.849 0 1 0 1.54 0V8.957l2.508 2.508a1.85 1.85 0 1 0 1.09-1.09l-2.634-2.634a1.85 1.85 0 0 0-2.378-2.377L8.73 2.63 10.91.451a1.54 1.54 0 0 1 2.179 0l10.459 10.46a1.54 1.54 0 0 1 0 2.179z", + "F03C2E", + "git", + ), + node: icon( + "M11.998,24c-0.321,0-0.641-0.084-0.922-0.247l-2.936-1.737c-0.438-0.245-0.224-0.332-0.08-0.383 c0.585-0.203,0.703-0.25,1.328-0.604c0.065-0.037,0.151-0.023,0.218,0.017l2.256,1.339c0.082,0.045,0.197,0.045,0.272,0l8.795-5.076 c0.082-0.047,0.134-0.141,0.134-0.238V6.921c0-0.099-0.053-0.192-0.137-0.242l-8.791-5.072c-0.081-0.047-0.189-0.047-0.271,0 L3.075,6.68C2.99,6.729,2.936,6.825,2.936,6.921v10.15c0,0.097,0.054,0.189,0.139,0.235l2.409,1.392 c1.307,0.654,2.108-0.116,2.108-0.89V7.787c0-0.142,0.114-0.253,0.256-0.253h1.115c0.139,0,0.255,0.112,0.255,0.253v10.021 c0,1.745-0.95,2.745-2.604,2.745c-0.508,0-0.909,0-2.026-0.551L2.28,18.675c-0.57-0.329-0.922-0.945-0.922-1.604V6.921 c0-0.659,0.353-1.275,0.922-1.603l8.795-5.082c0.557-0.315,1.296-0.315,1.848,0l8.794,5.082c0.57,0.329,0.924,0.944,0.924,1.603 v10.15c0,0.659-0.354,1.273-0.924,1.604l-8.794,5.078C12.643,23.916,12.324,24,11.998,24z M19.099,13.993 c0-1.9-1.284-2.406-3.987-2.763c-2.731-0.361-3.009-0.548-3.009-1.187c0-0.528,0.235-1.233,2.258-1.233 c1.807,0,2.473,0.389,2.747,1.607c0.024,0.115,0.129,0.199,0.247,0.199h1.141c0.071,0,0.138-0.031,0.186-0.081 c0.048-0.054,0.074-0.123,0.067-0.196c-0.177-2.098-1.571-3.076-4.388-3.076c-2.508,0-4.004,1.058-4.004,2.833 c0,1.925,1.488,2.457,3.895,2.695c2.88,0.282,3.103,0.703,3.103,1.269c0,0.983-0.789,1.402-2.642,1.402 c-2.327,0-2.839-0.584-3.011-1.742c-0.02-0.124-0.126-0.215-0.253-0.215h-1.137c-0.141,0-0.254,0.112-0.254,0.253 c0,1.482,0.806,3.248,4.655,3.248C17.501,17.007,19.099,15.91,19.099,13.993z", + "5FA04E", + "nodedotjs", + ), + npm: icon( + "M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z", + "CB3837", + "npm", + ), + bun: icon( + "M12 22.596c6.628 0 12-4.338 12-9.688 0-3.318-2.057-6.248-5.219-7.986-1.286-.715-2.297-1.357-3.139-1.89C14.058 2.025 13.08 1.404 12 1.404c-1.097 0-2.334.785-3.966 1.821a49.92 49.92 0 0 1-2.816 1.697C2.057 6.66 0 9.59 0 12.908c0 5.35 5.372 9.687 12 9.687v.001ZM10.599 4.715c.334-.759.503-1.58.498-2.409 0-.145.202-.187.23-.029.658 2.783-.902 4.162-2.057 4.624-.124.048-.199-.121-.103-.209a5.763 5.763 0 0 0 1.432-1.977Zm2.058-.102a5.82 5.82 0 0 0-.782-2.306v-.016c-.069-.123.086-.263.185-.172 1.962 2.111 1.307 4.067.556 5.051-.082.103-.23-.003-.189-.126a5.85 5.85 0 0 0 .23-2.431Zm1.776-.561a5.727 5.727 0 0 0-1.612-1.806v-.014c-.112-.085-.024-.274.114-.218 2.595 1.087 2.774 3.18 2.459 4.407a.116.116 0 0 1-.049.071.11.11 0 0 1-.153-.026.122.122 0 0 1-.022-.083 5.891 5.891 0 0 0-.737-2.331Zm-5.087.561c-.617.546-1.282.76-2.063 1-.117 0-.195-.078-.156-.181 1.752-.909 2.376-1.649 2.999-2.778 0 0 .155-.118.188.085 0 .304-.349 1.329-.968 1.874Zm4.945 11.237a2.957 2.957 0 0 1-.937 1.553c-.346.346-.8.565-1.286.62a2.178 2.178 0 0 1-1.327-.62 2.955 2.955 0 0 1-.925-1.553.244.244 0 0 1 .064-.198.234.234 0 0 1 .193-.069h3.965a.226.226 0 0 1 .19.07c.05.053.073.125.063.197Zm-5.458-2.176a1.862 1.862 0 0 1-2.384-.245 1.98 1.98 0 0 1-.233-2.447c.207-.319.503-.566.848-.713a1.84 1.84 0 0 1 1.092-.11c.366.075.703.261.967.531a1.98 1.98 0 0 1 .408 2.114 1.931 1.931 0 0 1-.698.869v.001Zm8.495.005a1.86 1.86 0 0 1-2.381-.253 1.964 1.964 0 0 1-.547-1.366c0-.384.11-.76.32-1.079.207-.319.503-.567.849-.713a1.844 1.844 0 0 1 1.093-.108c.367.076.704.262.968.534a1.98 1.98 0 0 1 .4 2.117 1.932 1.932 0 0 1-.702.868Z", + "000000", + "bun", + ), + bitwarden: icon( + "M21.722.296A.964.964 0 0 0 21.018 0H2.982a.959.959 0 0 0-.703.296.96.96 0 0 0-.297.702v12c0 .895.174 1.783.523 2.665.349.88.783 1.66 1.3 2.345.517.68 1.132 1.346 1.848 1.993a21.807 21.807 0 0 0 1.98 1.609c.605.427 1.235.83 1.893 1.212.657.381 1.125.638 1.4.772.276.134.5.241.664.311a.916.916 0 0 0 .814 0c.168-.073.389-.177.667-.311.275-.134.743-.394 1.401-.772a25.305 25.305 0 0 0 1.894-1.212A21.891 21.891 0 0 0 18.348 20c.716-.647 1.33-1.31 1.847-1.993s.949-1.463 1.3-2.345c.35-.879.524-1.767.524-2.665V1.001a.95.95 0 0 0-.297-.705zm-2.325 12.815c0 4.344-7.397 8.087-7.397 8.087V2.57h7.397v10.54z", + "175DDC", + "bitwarden", + ), + wacli: icon( + "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z", + "25D366", + "whatsapp", + ), + gogcli: icon( + "M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z", + "4285F4", + "google", + ), + neon: icon( + "M24 0V24l-9.365-8.045V24H0V0ZM2.942 21.087h8.751V9.563l9.365 8.204V2.919L2.942 2.914Z", + "34D59A", + "neon", + ), + }); diff --git a/tests/layout-view.test.ts b/tests/layout-view.test.ts index 47bf6cea..4a7a1a2e 100644 --- a/tests/layout-view.test.ts +++ b/tests/layout-view.test.ts @@ -120,13 +120,13 @@ describe("the right panel's width", () => { }); describe("the left column's width", () => { - test("264 px by default, the person's width within 208–480 px, and never so wide that the page beside it has less than its narrowest width", () => { + test("256 px by default, the person's width within 208–480 px, and never so wide that the page beside it has less than its narrowest width", () => { expect([columnMinWidth, columnDefaultWidth, columnMaxWidth]).toEqual([ - 208, 264, 480, + 208, 256, 480, ]); // The app right of the rail in a window 1800 px wide. const app = 1800 - 64 - 1; - expect(placeColumn(app, null)).toEqual({ width: 264, min: 208, max: 480 }); + expect(placeColumn(app, null)).toEqual({ width: 256, min: 208, max: 480 }); expect(placeColumn(app, 320)).toMatchObject({ width: 320 }); expect(placeColumn(app, 320.6)).toMatchObject({ width: 321 }); expect(placeColumn(app, 100)).toMatchObject({ width: 208 }); diff --git a/tests/tool-icons.test.ts b/tests/tool-icons.test.ts new file mode 100644 index 00000000..acff43b2 --- /dev/null +++ b/tests/tool-icons.test.ts @@ -0,0 +1,23 @@ +import { expect, test } from "bun:test"; +import { toolCatalog } from "../src/tools/catalog"; +import { toolIcons } from "../src/tools/icons"; + +test("every tool logo belongs to a curated tool and is a bundled, inert drawing", () => { + const names = new Set(toolCatalog.map((tool) => tool.name)); + for (const [name, icon] of Object.entries(toolIcons)) { + expect(names.has(name)).toBe(true); + if ("svg" in icon) { + // A vendor's own mark: a plain SVG, nothing that runs or loads. + expect(icon.svg.startsWith(" group.tools.map((entry) => entry.name)), ).toEqual([["gh"], ["composio"], ["neon", "gogcli", "wacli"]]); - for (const group of groups) expect(group.note.length).toBeGreaterThan(0); } // A tier without a tool has no heading. expect( @@ -514,17 +512,12 @@ test("the main view of Nástroje says nothing technical; what support needs stay copy, ), ]; - const header = checkedLine(new Date(2026, 9, 9, 14, 5), locale, copy); - expect( - header.startsWith(locale === "cs" ? "Zjištěno v" : "Checked at"), - ).toBe(true); // A switched-on tool that is not installed says when agents start // using it, in plain words. expect(connectionLine(tool({ enabled: true }), copy).text).toBe( copy.toolsEnabledNotInstalled.replace("{name}", "composio"), ); for (const said of [ - header, copy.toolsLoadFailed, copy.toolsMcpText, connectionLine(tool({ enabled: true }), copy).text,