refactor(web): UI polish for v0.7.0 — neutralised palette, official brand glyphs, lucide (#1522)

* refactor(web): adopt lucide-react for the inline Icon component

The hand-rolled `<Icon>` set drifted in stroke weight and proportion across
its 50+ glyphs as new icons were added. Swap the implementation to dispatch
to `lucide-react` while keeping the same `<Icon name="..." size={X} />` API
so the 246 existing call sites stay untouched.

- Adds `lucide-react` as a dependency (tree-shaken; ~30KB gzipped for the
  ~50 icons we actually import).
- `discord` and `x-brand` keep their bespoke inline SVG paths since lucide
  intentionally does not ship brand artwork.
- `spinner` continues to use the existing `.icon-spin` className for its
  rotation; under the hood it now renders lucide's `Loader2`.
- New `paw` glyph (lucide `PawPrint`) so the Pets nav item stops sharing
  the `sparkles` icon with External MCP.

No behaviour change: the prop surface is identical, fill follows
`currentColor` exactly as before, and aria-hidden / focusable defaults are
preserved. Visual deltas are limited to the strokes themselves (slightly
finer endcaps, more consistent baseline weights) — exactly the
consistency upgrade lucide gives us.

* feat(web): bundle official brand assets for agent icons

`AgentIcon` previously approximated each agent's brand with hand-drawn
SVG (orange Anthropic-ish sparkle, OpenAI-knot ellipses, etc). Replace
those approximations with the real, vendor-published artwork shipped as
static assets under `apps/web/public/agent-icons/`.

- 13 SVG marks sourced from `@lobehub/icons-static-svg` (MIT) — color
  variants where the vendor published one (Claude, Codex, Gemini,
  Copilot, Qwen, Qoder, DeepSeek, Kimi, Mistral/Vibe), monochrome marks
  for the rest (Cursor, OpenCode, Hermes, MiMo, Pi, Kilo).
- 1 PNG mark (Devin) sourced from devin.ai/icon.png, resized to 96×96
  via `sips` since Cognition doesn't publish an SVG.
- Each SVG was cleaned (stripped `<title>` brand text and the library's
  internal `style="flex:none;..."` ; dropped `width/height="1em"` so
  `viewBox` governs sizing) and run through `svgo --multipass`. Total
  bundle footprint: ~36 KB for all 17 files, only loaded on the agent
  cards that render them.
- `AgentIcon` now resolves brands via a small `ICON_EXT` table and
  renders `<img src="/agent-icons/<id>.<ext>">`. Agents without an asset
  (`devin` is the lone outlier removed in this commit because PNG; new
  agents with no shipped artwork at all) fall back to an initial-letter
  pill that reads as "no official mark yet" rather than inventing
  brand artwork.
- Removes the `simple-icons` dependency from a previous iteration since
  `AgentIcon` was its only consumer.

Public-API stable: `<AgentIcon id={a.id} size={X} />` still accepts the
same prop shape; `AvatarMenu`'s small-size usage continues to work.

* refactor(web): polish entry view + Settings dialog UI for v0.7.0

A sweep over the two surfaces that have the most visual surface area in
the app (the entry sidebar / New Project panel on the left, and the
Settings modal). The work converged on a single neutral palette + a
small set of shared dimensional standards documented in CSS, so future
sections that get added slot into the same rhythm.

New Project panel (apps/web/src/components/NewProjectPanel.tsx +
.newproj* rules in index.css)
- Adds a spec comment block at the top of the .newproj rules listing
  the canonical heights (input 30, dropdown 38, compact toggle 36,
  popover item 38) and the neutral colour rules.
- Rebuilds PlatformPicker as a DS-picker-style dropdown trigger +
  popover (the previous 6-card 2×3 grid was ~280px tall; the dropdown
  collapses to a single 38px row with the same multi-select semantics).
- Replaces SurfaceOptions' two heavy `ToggleRow` cards with the new
  compact one-line `CompactToggle`; the descriptive hint moves to a
  native `title` tooltip.
- Compresses the Fidelity card grid (thumb aspect 16/7 → 16/5, tighter
  padding, smaller label).
- Neutralises every selected/active state inside the panel: removes the
  orange accent fills and rings from `.newproj-card.active`,
  `.newproj-title-badge`, `.compact-toggle.on`, `.toggle-row.on`, the
  DS picker popover items + radio/check marks, the trigger open border
  and shadow, and the search-bar background. The Create CTA stays the
  only orange element on the panel.
- Aligns the project-name input focus state across the sidebar:
  border `var(--text)` + 8% black halo (rgba is written out because the
  CSS pipeline collapses `color-mix(... 8%, transparent)` down to a
  solid `var(--text)`, which would render as a 3px solid black band).
- Switches the body card from `flex: 1 1 auto` to `flex: 0 1 auto` so a
  short form variant doesn't leave a white void at the bottom of the
  card, and disables overscroll-bounce on the card so a fast scroll
  doesn't briefly expose the page-level gray under the white surface.
- Pins the privacy footer below the card with a fixed 0 margin-top +
  shorter padding-top so it reads as a label of the card rather than a
  centred dialog footer.

Entry sidebar footer (apps/web/src/components/EntryView.tsx +
.entry-side-foot* rules)
- Replaces the X social pill's `external-link` placeholder glyph with a
  bespoke filled `x-brand` SVG that mirrors the `discord` mark already
  in the icon set.
- Wraps Discord + X in `.entry-side-foot-social` and lets that group
  flex-margin to the right of the row, so the two social pills read as
  a tight pair instead of a fourth pill stuck to the Pet pill.
- Drops the "unadopted" red dot on the Pet pill (it duplicated the call
  to action that the label already carried).
- Shrinks the footer icons to 10px and dims them to 55% / 75% opacity
  on hover so the labels are clearly the focal point — `currentColor`
  on the lucide-rendered SVGs would otherwise make the glyphs full
  black on hover.
- Tightens the env-pill version text cap (180 → 142) so the top row
  ends close to the right edge of the Language + Pet group below it.

Settings dialog (apps/web/src/components/SettingsDialog.tsx +
.modal-settings / .settings-* / .seg-* / .agent-* rules)
- Removes the "SETTINGS" kicker eyebrow above each section title (the
  big-typography title and modal context already make it redundant).
- Switches the sidebar from a card-per-item layout to ChatGPT-style
  single-line pills: hides the `<small>` description, swaps the
  sidebar bg from gray to white, makes the active item a gray pill (no
  border, no shadow) so all items keep a consistent row height
  regardless of state.
- Drops the modal-body's top border (already separated by the
  whitespace between modal-head and the body grid) and pins
  `.modal-settings { height: min(720px, 100vh - 64px) }` so the
  dialog no longer resizes when the user switches between short and
  long sections.
- Compresses the Local CLI / BYOK seg-control from a 2-line ~52px card
  pair to a 1-line ~42px segmented pill that height-matches the active
  sidebar nav-item, and aligns the `.settings-content` padding-top
  with `.settings-sidebar` (22 → 16) so the first content row sits
  level with the first sidebar item.
- Neutralises agent-card selected state, install/docs link colour, and
  protocol-chip active state — same accent-stripping pattern as the
  New Project panel.
- Uniform agent-card height via `min-height: 64px` so installed cards
  (icon + name + version) align with unavailable cards (icon + name +
  not-installed + Install/Docs row).

No prop-API changes, no business-logic edits — this is a pure visual
refactor. Existing tests, providers and daemon contracts are untouched.
This commit is contained in:
Sid 2026-05-13 13:59:19 +08:00 committed by GitHub
parent dc7791ef9d
commit eda182c8a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 755 additions and 1036 deletions

View file

@ -33,6 +33,7 @@
"@open-design/platform": "workspace:*",
"@open-design/sidecar": "workspace:*",
"@open-design/sidecar-proto": "workspace:*",
"lucide-react": "^1.14.0",
"next": "^16.2.5",
"openai": "^6.36.0",
"posthog-js": "^1.205.0",

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#d97757" fill-rule="evenodd" d="M20.998 10.949H24v3.102h-3v3.028h-1.487V20H18v-2.921h-1.487V20H15v-2.921H9V20H7.488v-2.921H6V20H4.487v-2.921H3V14.05H0v-3.1h3V5h17.998zM6 10.949h1.488V8.102H6zm10.51 0H18V8.102h-1.49z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 316 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M19.503 0H4.496A4.496 4.496 0 0 0 0 4.496v15.007A4.496 4.496 0 0 0 4.496 24h15.007A4.496 4.496 0 0 0 24 19.503V4.496A4.496 4.496 0 0 0 19.503 0"/><path fill="url(#a)" d="M9.064 3.344a4.6 4.6 0 0 1 2.285-.312q1.5.173 2.673 1.275.016.015.037.021a.1.1 0 0 0 .043 0 4.55 4.55 0 0 1 3.046.275l.047.022.116.057a4.58 4.58 0 0 1 2.188 2.399q.313.765.315 1.595a4.2 4.2 0 0 1-.134 1.223.12.12 0 0 0 .03.115q.89.91 1.183 2.17.433 2.138-.887 3.854l-.136.166a4.55 4.55 0 0 1-2.201 1.388.12.12 0 0 0-.081.076c-.191.551-.383 1.023-.74 1.494-.9 1.187-2.222 1.846-3.711 1.838q-1.78-.009-3.157-1.302a.11.11 0 0 0-.105-.024c-.388.125-.78.143-1.204.138a4.44 4.44 0 0 1-1.945-.466 4.54 4.54 0 0 1-1.61-1.335c-.152-.202-.303-.392-.414-.617a6 6 0 0 1-.37-.961 4.6 4.6 0 0 1-.014-2.298.1.1 0 0 0 .006-.056.1.1 0 0 0-.027-.048 4.5 4.5 0 0 1-1.034-1.651 3.9 3.9 0 0 1-.251-1.192 5.2 5.2 0 0 1 .141-1.6Q3.659 7.92 5.086 6.97q.318-.212.601-.33a6 6 0 0 1 .646-.227.1.1 0 0 0 .065-.066 4.5 4.5 0 0 1 .829-1.615 4.54 4.54 0 0 1 1.837-1.388m3.482 10.565a.637.637 0 0 0 0 1.272h3.636a.637.637 0 1 0 0-1.272zM8.462 9.23a.637.637 0 0 0-1.106.631l1.272 2.224-1.266 2.136a.636.636 0 1 0 1.095.649l1.454-2.455a.64.64 0 0 0 .005-.64z"/><defs><linearGradient id="a" x1="12" x2="12" y1="3" y2="21" gradientUnits="userSpaceOnUse"><stop stop-color="#b1a7ff"/><stop offset=".5" stop-color="#7a9dff"/><stop offset="1" stop-color="#3941ff"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="url(#a)" d="M17.533 1.829A2.53 2.53 0 0 0 15.11 0h-.737a2.53 2.53 0 0 0-2.484 2.087l-1.263 6.937.314-1.08a2.53 2.53 0 0 1 2.424-1.833h4.284l1.797.706 1.731-.706h-.505a2.53 2.53 0 0 1-2.423-1.829z" transform="translate(0 1)"/><path fill="url(#b)" d="M6.726 20.16A2.53 2.53 0 0 0 9.152 22h1.566c1.37 0 2.49-1.1 2.525-2.48l.17-6.69-.357 1.228a2.53 2.53 0 0 1-2.423 1.83h-4.32l-1.54-.842-1.667.843h.497a2.53 2.53 0 0 1 2.426 1.84z" transform="translate(0 1)"/><path fill="url(#c)" d="M15 0H6.252c-2.5 0-4 3.331-5 6.662-1.184 3.947-2.734 9.225 1.75 9.225H6.78c1.13 0 2.12-.753 2.43-1.847.657-2.317 1.809-6.359 2.713-9.436.46-1.563.842-2.906 1.43-3.742A1.97 1.97 0 0 1 15 0" transform="translate(0 1)"/><path fill="url(#d)" d="M15 0H6.252c-2.5 0-4 3.331-5 6.662-1.184 3.947-2.734 9.225 1.75 9.225H6.78c1.13 0 2.12-.753 2.43-1.847.657-2.317 1.809-6.359 2.713-9.436.46-1.563.842-2.906 1.43-3.742A1.97 1.97 0 0 1 15 0" transform="translate(0 1)"/><path fill="url(#e)" d="M9 22h8.749c2.5 0 4-3.332 5-6.663 1.184-3.948 2.734-9.227-1.75-9.227H17.22a2.53 2.53 0 0 0-2.43 1.848 1149 1149 0 0 1-2.713 9.437c-.46 1.564-.842 2.907-1.43 3.743A1.97 1.97 0 0 1 9 22" transform="translate(0 1)"/><path fill="url(#f)" d="M9 22h8.749c2.5 0 4-3.332 5-6.663 1.184-3.948 2.734-9.227-1.75-9.227H17.22a2.53 2.53 0 0 0-2.43 1.848 1149 1149 0 0 1-2.713 9.437c-.46 1.564-.842 2.907-1.43 3.743A1.97 1.97 0 0 1 9 22" transform="translate(0 1)"/><defs><radialGradient id="a" cx="85.44%" cy="100.653%" r="105.116%" fx="85.44%" fy="100.653%" gradientTransform="matrix(-.5391 -.77634 .664 -.63031 .647 2.304)"><stop offset="9.6%" stop-color="#00aeff"/><stop offset="77.3%" stop-color="#2253ce"/><stop offset="100%" stop-color="#0736c4"/></radialGradient><radialGradient id="b" cx="18.143%" cy="32.928%" r="95.612%" fx="18.143%" fy="32.928%" gradientTransform="matrix(.5469 .78875 -.70175 .61471 .313 -.017)"><stop offset="0%" stop-color="#ffb657"/><stop offset="63.4%" stop-color="#ff5f3d"/><stop offset="92.3%" stop-color="#c02b3c"/></radialGradient><radialGradient id="e" cx="82.987%" cy="-9.792%" r="140.622%" fx="82.987%" fy="-9.792%" gradientTransform="matrix(-.32768 .89198 -.94479 -.30936 1.01 -.87)"><stop offset="6.6%" stop-color="#8c48ff"/><stop offset="50%" stop-color="#f2598a"/><stop offset="89.6%" stop-color="#ffb152"/></radialGradient><linearGradient id="c" x1="39.465%" x2="46.884%" y1="12.117%" y2="103.774%"><stop offset="15.6%" stop-color="#0d91e1"/><stop offset="48.7%" stop-color="#52b471"/><stop offset="65.2%" stop-color="#98bd42"/><stop offset="93.7%" stop-color="#ffc800"/></linearGradient><linearGradient id="d" x1="45.949%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#3dcbff"/><stop offset="24.7%" stop-color="#0588f7" stop-opacity="0"/></linearGradient><linearGradient id="f" x1="83.507%" x2="83.453%" y1="-6.106%" y2="21.131%"><stop offset="5.8%" stop-color="#f8adfa"/><stop offset="70.8%" stop-color="#a86edd" stop-opacity="0"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#1c1b1a" fill-rule="evenodd" viewBox="0 0 24 24"><path d="M22.106 5.68 12.5.135a1 1 0 0 0-.998 0L1.893 5.68a.84.84 0 0 0-.419.726v11.186c0 .3.16.577.42.727l9.607 5.547a1 1 0 0 0 .998 0l9.608-5.547a.84.84 0 0 0 .42-.727V6.407a.84.84 0 0 0-.42-.726zm-.603 1.176L12.228 22.92c-.063.108-.228.064-.228-.061V12.34a.59.59 0 0 0-.295-.51l-9.11-5.26c-.107-.062-.063-.228.062-.228h18.55c.264 0 .428.286.296.514"/></svg>

After

Width:  |  Height:  |  Size: 455 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#4d6bfe" d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.5 5.5 0 0 1-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11 11 0 0 0-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428s-1.67.295-2.687.684a3 3 0 0 1-.465.137 9.6 9.6 0 0 0-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.2 4.2 0 0 0 1.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.7 4.7 0 0 1 1.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614m1-6.44a.306.306 0 0 1 .415-.287.3.3 0 0 1 .2.288.306.306 0 0 1-.31.307.303.303 0 0 1-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.25 1.25 0 0 1-.798-.254c-.274-.23-.47-.358-.552-.758a1.7 1.7 0 0 1 .016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.56.56 0 0 1-.254-.078.253.253 0 0 1-.114-.358c.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452"/></svg>

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="url(#a)" d="M0 4.391A4.39 4.39 0 0 1 4.391 0h15.217A4.39 4.39 0 0 1 24 4.391v15.217A4.39 4.39 0 0 1 19.608 24H4.391A4.39 4.39 0 0 1 0 19.608z"/><path fill="#1e1e2e" fill-rule="evenodd" d="M19.74 1.444a2.816 2.816 0 0 1 2.816 2.816v15.48a2.816 2.816 0 0 1-2.816 2.816H4.26a2.816 2.816 0 0 1-2.816-2.816V4.26A2.816 2.816 0 0 1 4.26 1.444zM7.236 8.564l7.752 3.728-7.752 3.727v2.802l9.557-4.596v-3.866L7.236 5.763z" clip-rule="evenodd"/><defs><linearGradient id="a" x1="24" x2="0" y1="6.587" y2="16.494" gradientUnits="userSpaceOnUse"><stop stop-color="#ee4d5d"/><stop offset=".328" stop-color="#b381dd"/><stop offset=".476" stop-color="#207cfe"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 745 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#1c1b1a" fill-rule="evenodd" viewBox="0 0 24 24"><path d="M0 0v24h24V0zm22.222 22.222H1.778V1.778h20.444zm-7.555-4.964h2.222v1.778h-2.794L12.89 17.83v-2.794h1.778v2.222zm4 0h-1.778v-2.222h-2.222v-1.778h2.793l1.207 1.207zm-7.556-2.591H9.333v-1.778h1.778zm-5.778-1.778h1.778v4h4v1.778H6.54L5.333 17.46v-4.57zm13.334-3.556v1.778h-5.778V9.333h1.987V7.111h-1.987V5.333h2.558l1.206 1.207v2.793zm-11.556-2h2.222l1.778 1.778v2H9.333v-2H7.111v2H5.333V5.333h1.778zm4 0H9.333v-2h1.778z"/></svg>

After

Width:  |  Height:  |  Size: 529 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#1783ff" d="M21.846 0a1.923 1.923 0 1 1 0 3.846H20.15a.226.226 0 0 1-.227-.226V1.923C19.923.861 20.784 0 21.846 0"/><path fill="#fff" d="m11.065 11.199 7.257-7.2c.137-.136.06-.41-.116-.41H14.3a.16.16 0 0 0-.117.051l-7.82 7.756c-.122.12-.302.013-.302-.179V3.82c0-.127-.083-.23-.185-.23h-2.69c-.103 0-.186.103-.186.23v15.95c0 .128.083.23.186.23h2.69c.103 0 .186-.102.186-.23v-3.25a.25.25 0 0 1 .069-.178l2.424-2.406a.16.16 0 0 1 .205-.023l6.484 4.772a7.7 7.7 0 0 0 3.453 1.283c.108.012.2-.095.2-.23v-3.06c0-.117-.07-.212-.164-.227a5 5 0 0 1-2.027-.807l-5.613-4.064c-.117-.078-.132-.279-.028-.381"/></svg>

After

Width:  |  Height:  |  Size: 674 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 1200 1200"><rect fill="#9046ff" rx="260"/><mask id="a" x="272" y="202" maskUnits="userSpaceOnUse" style="mask-type:luminance"><path fill="#fff" d="M926.578 202.793H272.637v795.064h653.941z"/></mask><g mask="url(#a)"><path fill="#fff" d="M398.554 818.914c-82.239 182.116 92.923 227.826 222.118 121.242 38.015 119.504 180.38 30.317 231.562-62.361 112.553-204.228 67.084-412.438 55.406-455.421-80.003-292.931-480.017-293.428-548.84 1.491-16.149 51.679-16.398 110.315-25.342 171.186-4.472 30.809-7.951 50.437-19.628 82.734-6.957 18.639-16.15 35.034-31.057 62.86-22.858 43.236-13.169 126.468 105.097 83.238l11.181-4.969z"/><path fill="#000" d="M636.123 549.353c-32.795 0-37.764-39.256-37.764-62.611 0-21.119 3.727-37.765 10.934-48.449 6.211-9.441 15.404-14.162 26.83-14.162 11.432 0 21.369 4.721 28.324 14.41 7.951 10.933 12.176 27.579 12.176 48.201 0 39.256-15.152 62.611-40.248 62.611zm135.117 0c-32.795 0-37.763-39.256-37.763-62.611 0-21.119 3.726-37.765 10.933-48.449 6.211-9.441 15.404-14.162 26.83-14.162 11.432 0 21.369 4.721 28.324 14.41 7.952 10.933 12.176 27.579 12.176 48.201 0 39.256-15.152 62.611-40.248 62.611z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#1c1b1a" fill-rule="evenodd" viewBox="0 0 24 24"><path d="M.958 15.936a.46.46 0 0 1 .459.44v2.729a.46.46 0 0 1-.918 0v-2.729a.46.46 0 0 1 .459-.44m4.814-2.035a.46.46 0 0 1 .553.45v4.754a.458.458 0 1 1-.918 0V15.48L3.74 17.202a.46.46 0 0 1-.655.016.5.5 0 0 1-.065-.082L.628 14.67a.459.459 0 0 1 .658-.637L3.41 16.22l2.127-2.188a.46.46 0 0 1 .235-.13zm2.068.004a.46.46 0 0 1 .458.445v4.755a.46.46 0 0 1-.458.458.46.46 0 0 1-.458-.458V14.35a.46.46 0 0 1 .458-.445m1.973 2.014a.46.46 0 0 1 .46.457v2.729a.46.46 0 0 1-.784.324.46.46 0 0 1-.134-.324v-2.729a.46.46 0 0 1 .458-.458zm.002-2.045a.46.46 0 0 1 .328.157l2.127 2.19 2.125-2.19a.459.459 0 0 1 .784.318v4.756a.46.46 0 0 1-.455.458.46.46 0 0 1-.458-.458V15.48l-1.667 1.723a.46.46 0 0 1-.65.008l-.005-.005q0-.002-.004-.003l-2.455-2.534a.46.46 0 0 1-.008-.667.46.46 0 0 1 .338-.128m6.797 1.206a.46.46 0 0 1 .53.651A1.966 1.966 0 0 0 19.81 18.4a.46.46 0 0 1 .623.18.46.46 0 0 1-.181.624 2.86 2.86 0 0 1-1.38.353l-.142-.004a2.88 2.88 0 0 1-2.393-4.263.46.46 0 0 1 .274-.21zm.864-.931a2.884 2.884 0 0 1 3.915 3.914.46.46 0 0 1-.402.24l-.057-.004a.5.5 0 0 1-.164-.055.46.46 0 0 1-.182-.622 1.967 1.967 0 0 0-2.669-2.67.459.459 0 1 1-.441-.803M9.59 6.368c1.481 0 1.696 1.202 1.696 1.654v2.648h-.917v-.432c-.26.346-.792.535-1.36.535-.133 0-1.289-.03-1.384-1.136-.082-.932.675-1.61 2.053-1.61h.691c0-.563-.367-.886-.983-.886-.44.013-.864.174-1.2.458l-.36-.664c.484-.379 1.012-.567 1.764-.567m4.427.1c1.263 0 2.082.97 2.083 2.15 0 1.181-.824 2.154-2.083 2.154S11.933 9.8 11.933 8.62s.82-2.153 2.084-2.153zm6.801.015c.68 0 1.202.465 1.197 1.548v2.642H21.1V8.29c0-.312-.002-.98-.63-.98s-.628.667-.628.838v2.524h-.89V8.148c0-.17-.001-.838-.63-.838-.628 0-.628.668-.628.98v2.383h-.917v-4.03h.917V7a1.22 1.22 0 0 1 .947-.516c.398 0 .76.193.982.686a1.32 1.32 0 0 1 1.195-.686zm-18.093.872 1.457-1.772H5.32L3.311 8.07l2.14 2.602H4.24L2.725 8.796 1.21 10.672H0L2.138 8.07.13 5.583h1.138zm4.149 3.317h-.916V6.644h.916zm16.99 0h-.916V6.644h.916zM9.925 8.71c-1.055 0-1.359.412-1.326.742.032.329.324.537.757.537a1.013 1.013 0 0 0 1.014-.968l.002-.31h-.447zm4.093-1.41c-.663 0-1.184.487-1.184 1.32 0 .832.52 1.32 1.184 1.32.662 0 1.182-.49 1.182-1.32 0-.832-.52-1.32-1.182-1.32M6.417 5.001a.57.57 0 0 1 .587.582.588.588 0 0 1-1.175 0A.57.57 0 0 1 6.417 5zm16.991 0a.57.57 0 0 1 .592.582.588.588 0 0 1-1.174 0 .57.57 0 0 1 .357-.542.6.6 0 0 1 .225-.04"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#1c1b1a" fill-rule="evenodd" viewBox="0 0 24 24"><path d="M16 6H8v12h8zm4 16H4V2h16z"/></svg>

After

Width:  |  Height:  |  Size: 139 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#1c1b1a" fill-rule="evenodd" viewBox="0 0 24 24"><path d="M8.341 24c-.53 0-.841-.308-.841-.824v-.271c0-.514.248-.755.708-.926l1.025-.343c.708-.271.954-.583.954-1.303V3.667c0-.72-.246-1.029-.954-1.303L8.2 2.02c-.46-.171-.701-.408-.701-.926v-.27C7.5.309 7.818 0 8.348 0h6.968c.531 0 .85.309.85.824v.271c0 .514-.249.755-.709.926l-1.031.34c-.743.272-.992.583-.992 1.303v16.664c0 .72.249 1.028.992 1.303l1.024.342c.46.172.708.408.708.926v.272c0 .515-.318.824-.85.824L8.342 24z"/></svg>

After

Width:  |  Height:  |  Size: 526 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#111113" viewBox="0 0 24 24"><path fill="#2adb5c" d="M23.376 14.458v-4.056c0-2.304-1.003-4.154-2.748-5.075L11.612.574l-.046.086-.045.086c1.68.886 2.644 2.673 2.644 4.902v4.056a8 8 0 0 1-.014.454l-.005.061q-.007.122-.018.245l-.011.1-.01.076q-.011.102-.025.203l-.018.113-.01.058a10 10 0 0 1-.098.513l-.007.03a7 7 0 0 1-.074.294l-.024.086a8 8 0 0 1-.087.296l-.027.085a10 10 0 0 1-.111.323l-.033.085-.018.046-.098.248q-.029.072-.061.145l-.007.017-.084.187q-.036.083-.077.165l-.089.182a9 9 0 0 1-.176.332q-.046.084-.094.167-.046.08-.095.16a6 6 0 0 1-.201.319c-.034.055-.07.107-.111.169l-.099.144a15 15 0 0 1-.34.457l-.121.151-.107.128-.007.008a7 7 0 0 1-.262.298l-.149.16-.116.12a10 10 0 0 1-.204.198l-.03.03-.072.069a9 9 0 0 1-.263.235l-.025.022-.029.026-.042.035-.22.18-.07.055-.018.013-.194.146q-.043.03-.086.063a8 8 0 0 1-.22.152l-.057.04a9 9 0 0 1-.293.185l-.062.037a10 10 0 0 1-.307.173l-.037.02-.196.103-.108.052-.012.006a6 6 0 0 1-.315.143l-.196.08-.035.014-.086.034-.215.075-.039.014-.064.023a8 8 0 0 1-.323.097l-.63.173a7 7 0 0 1-.33.08l-.07.015a3 3 0 0 1-.157.032l-.065.011-.085.015a2 2 0 0 1-.194.027l-.085.01-.16.018-.034.003a5 5 0 0 1-.246.016h-.033a3 3 0 0 1-.155.005h-.106a3 3 0 0 1-.225-.007H4.86l-.15-.012-.066-.006-.187-.02-.04-.005a5 5 0 0 1-.219-.035l-.054-.01a7 7 0 0 1-.347-.082l-.03-.008-.038-.01a5 5 0 0 1-.269-.086l-.063-.023a4 4 0 0 1-.188-.073l-.071-.031-.016-.007-.16-.074-.026-.013-.014-.007-.671-.351.486.486h.016l8.995 4.742.093.048.03.014.02.01q.083.04.169.076l.016.008.073.032.195.076.022.008.03.012.014.004a5 5 0 0 0 .323.1l.027.007q.11.03.22.055l.018.004.027.006.066.013.088.016q.112.02.226.038l.042.004q.096.013.193.022l.126.012.06.003.05.002.098.005.14.003h.12c.05 0 .1-.002.161-.005h.033l.07-.004.184-.014.033-.003.058-.005.108-.012.081-.01.08-.01.129-.021.082-.014.071-.012q.08-.015.16-.033l.066-.013.059-.013q.144-.033.287-.073l.63-.172a7 7 0 0 0 .397-.122l.04-.015q.112-.038.222-.078.047-.017.093-.037l.03-.012q.102-.04.2-.082l.128-.056.195-.09.021-.01.102-.05q.102-.051.202-.105l.037-.02.073-.038q.12-.067.24-.139l.026-.015a8 8 0 0 0 .322-.2l.065-.045 1.98.902a1.748 1.748 0 0 0 2.472-1.59v-7.33l.004.004z"/><path d="m11.617.576-.093-.047q-.024-.013-.05-.024l-.166-.077-.09-.04a4 4 0 0 0-.194-.074l-.053-.02-.013-.005a5 5 0 0 0-.277-.088l-.07-.019a4 4 0 0 0-.219-.053q-.023-.005-.044-.012L10.253.1l-.057-.011a5 5 0 0 0-.225-.036L9.928.047 9.737.025 9.669.02 9.611.015 9.515.009 9.404.004Q9.329-.001 9.254 0h-.109c-.052 0-.106.004-.16.004L8.884.01 8.7.022l-.083.006h-.008l-.15.019-.12.014-.169.028q-.056.008-.11.018a7 7 0 0 0-.572.133l-.63.172a8 8 0 0 0-.33.1L6.42.549a13 13 0 0 0-.345.126l-.2.083-.128.055a12 12 0 0 0-.318.15 7 7 0 0 0-.311.163q-.122.068-.24.14a9 9 0 0 0-.35.218l-.016.01-.06.04a9 9 0 0 0-.51.37 13 13 0 0 0-.315.254l-.043.034-.01.007-.045.041q-.135.117-.268.24l-.106.101q-.105.1-.207.204l-.056.054-.063.067-.153.164-.12.133-.148.17q-.036.044-.073.086l-.043.053-.123.155-.118.151-.118.16-.075.101-.038.056-.107.155-.11.164a10 10 0 0 0-.168.265q-.017.031-.037.063a12 12 0 0 0-.192.335l-.09.168q-.03.052-.057.105l-.032.065-.092.188-.08.167-.083.192q-.026.057-.05.115l-.021.051q-.052.124-.1.253l-.051.134q-.06.165-.113.33l-.02.057-.002.007-.008.026q-.047.15-.09.301l-.023.089q-.04.15-.075.301l-.007.03a8 8 0 0 0-.057.267l-.008.048q-.021.11-.038.223L.082 8.4q-.016.117-.029.234-.01.077-.017.154a7 7 0 0 0-.02.26l-.009.13Q0 9.37 0 9.564v4.056c0 1.478.42 2.741 1.138 3.692A4.75 4.75 0 0 0 2.73 18.67l9.015 4.753c-1.656-.874-2.728-2.685-2.73-5.051v-4.056q0-.195.01-.39l.01-.128a6 6 0 0 1 .019-.261l.017-.155.029-.234.026-.164.039-.223q.029-.158.064-.313l.008-.031q.035-.15.075-.301l.023-.088q.042-.151.09-.302l.008-.025.021-.063q.053-.165.113-.33l.052-.134q.047-.127.1-.253.034-.084.07-.166a14 14 0 0 1 .164-.358q.044-.095.092-.188l.088-.172a9 9 0 0 1 .187-.338l.096-.164q.051-.085.104-.168l.1-.159a11 11 0 0 1 .567-.786l.123-.155.116-.139.148-.171a14 14 0 0 1 .272-.297 10 10 0 0 1 .432-.425q.132-.124.268-.241l.054-.048a10 10 0 0 1 .553-.435l.092-.068q.112-.08.226-.156.028-.02.06-.04a9 9 0 0 1 .362-.227q.12-.071.241-.139l.11-.058a8 8 0 0 1 .521-.256l.126-.056a8 8 0 0 1 .546-.208l.107-.037q.165-.054.33-.1l.63-.172q.143-.039.287-.072l.097-.02q.094-.022.187-.04l.114-.018q.082-.016.166-.028l.12-.014q.08-.011.157-.019l.083-.006q.092-.008.184-.013.05-.003.101-.004l.16-.006h.11a3 3 0 0 1 .261.008l.153.01.067.007q.096.009.192.022l.043.005a5 5 0 0 1 .281.047l.141.03q.11.025.218.053l.07.019q.141.039.278.087l.067.025a4 4 0 0 1 .449.19l.143.072z"/></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="url(#a)" d="M12.604 1.34q.59 1.035 1.174 2.075a.18.18 0 0 0 .157.091h5.552q.26 0 .446.327l1.454 2.57c.19.337.24.478.024.837q-.39.646-.76 1.3l-.367.658c-.106.196-.223.28-.04.512l2.652 4.637c.172.301.111.494-.043.77q-.656 1.177-1.335 2.34c-.159.272-.352.375-.68.37a43 43 0 0 0-2.327.016.1.1 0 0 0-.081.05 575 575 0 0 1-2.705 4.74c-.169.293-.38.363-.725.364q-1.495.005-3.017.002a.54.54 0 0 1-.465-.271l-1.335-2.323a.09.09 0 0 0-.083-.049H4.982a1.8 1.8 0 0 1-.805-.092l-1.603-2.77a.54.54 0 0 1-.002-.54l1.207-2.12a.2.2 0 0 0 0-.197 551 551 0 0 1-1.875-3.272l-.79-1.395c-.16-.31-.173-.496.095-.965q.697-1.22 1.387-2.436c.132-.234.304-.334.584-.335a338 338 0 0 1 2.589-.001.12.12 0 0 0 .107-.063l2.806-4.895a.49.49 0 0 1 .422-.246c.524-.001 1.053 0 1.583-.006L11.704 1c.341-.003.724.032.9.34m-3.432.403a.06.06 0 0 0-.052.03L6.254 6.788a.16.16 0 0 1-.135.078H3.253q-.084 0-.041.074l5.81 10.156q.037.062-.034.063l-2.795.015a.22.22 0 0 0-.2.116l-1.32 2.31q-.066.117.068.118l5.716.008q.068 0 .104.061l1.403 2.454q.069.122.139 0l5.006-8.76.783-1.382a.055.055 0 0 1 .096 0l1.424 2.53a.12.12 0 0 0 .107.062l2.763-.02a.04.04 0 0 0 .035-.02.04.04 0 0 0 0-.04l-2.9-5.086a.11.11 0 0 1 0-.113l.293-.507 1.12-1.977q.036-.062-.035-.062H9.2q-.088 0-.043-.077l1.434-2.505a.11.11 0 0 0 0-.114L9.225 1.774a.06.06 0 0 0-.053-.031m6.29 8.02q.07 0 .034.06l-.832 1.465-2.613 4.585a.06.06 0 0 1-.05.029.06.06 0 0 1-.05-.029L8.498 9.841q-.03-.051.028-.054l.216-.012 6.722-.012z"/><defs><linearGradient id="a" x1="0%" x2="100%" y1="0%" y2="0%"><stop offset="0%" stop-color="#6336e7" stop-opacity=".84"/><stop offset="100%" stop-color="#6f69f7" stop-opacity=".84"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="gold" d="M3.428 3.4h3.429v3.428H3.428zm13.714 0h3.43v3.428h-3.43z"/><path fill="#ffaf00" d="M3.428 6.828h6.857v3.429H3.429V6.828zm10.286 0h6.857v3.429h-6.857z"/><path fill="#ff8205" d="M3.428 10.258h17.144v3.428H3.428z"/><path fill="#fa500f" d="M3.428 13.686h3.429v3.428H3.428zm6.858 0h3.429v3.428h-3.429zm6.856 0h3.43v3.428h-3.43z"/><path fill="#e10500" d="M0 17.114h10.286v3.429H0zm13.714 0H24v3.429H13.714z"/></svg>

After

Width:  |  Height:  |  Size: 490 B

File diff suppressed because one or more lines are too long

View file

@ -523,7 +523,7 @@ export function EntryView({
: apiProtocolLabel(config.apiProtocol)}
</span>
<span style={{ color: 'var(--text-faint)' }}>·</span>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 180 }}>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 142 }}>
{envMetaLine}
</span>
</button>
@ -552,7 +552,6 @@ export function EntryView({
? t('pet.changePet')
: t('pet.adoptCallout')}
</span>
{!config.pet?.adopted ? <span className="pet-pill-dot" aria-hidden /> : null}
</button>
<span className="pet-pill-divider" aria-hidden />
<button
@ -565,16 +564,18 @@ export function EntryView({
<Icon name={petRailHidden ? 'eye' : 'eye-off'} size={12} />
</button>
</div>
<a
className="foot-pill foot-pill-follow"
href="https://x.com/nexudotio"
target="_blank"
rel="noreferrer noopener"
title="Follow @nexudotio on X for releases and milestones"
aria-label="Follow @nexudotio on X"
>
<Icon name="external-link" size={12} />
</a>
<div className="entry-side-foot-social">
<a
className="foot-pill foot-pill-follow"
href="https://x.com/nexudotio"
target="_blank"
rel="noreferrer noopener"
title="Follow @nexudotio on X for releases and milestones"
aria-label="Follow @nexudotio on X"
>
<Icon name="x-brand" size={12} />
</a>
</div>
</div>
</div>
<button

View file

@ -1,4 +1,61 @@
import type { SVGProps } from 'react';
import {
ArrowLeft,
ArrowUp,
Bell,
Check,
ChevronDown,
ChevronLeft,
ChevronRight,
Copy,
Download,
ExternalLink,
Eye,
EyeOff,
File,
FileCode,
Folder,
History,
Image as ImageIcon,
Import,
KanbanSquare,
Languages,
Link,
Loader2,
type LucideIcon,
MessageSquare,
Mic,
Minus,
MoreHorizontal,
Orbit,
Paperclip,
PawPrint,
PenLine,
Pencil,
Play,
Plus,
Presentation,
RefreshCw,
RotateCw,
Search,
Send,
Settings,
Share,
SlidersHorizontal,
SlidersVertical,
Sparkles,
Square,
SquarePen,
SunMoon,
ThumbsDown,
ThumbsUp,
Trash,
Upload,
X as CloseX,
LayoutGrid,
ZoomIn,
ZoomOut,
} from 'lucide-react';
type IconName =
| 'arrow-left'
@ -12,6 +69,7 @@ type IconName =
| 'close'
| 'copy'
| 'comment'
| 'discord'
| 'download'
| 'draw'
| 'edit'
@ -32,6 +90,7 @@ type IconName =
| 'minus'
| 'more-horizontal'
| 'orbit'
| 'paw'
| 'pencil'
| 'plus'
| 'play'
@ -47,9 +106,12 @@ type IconName =
| 'sparkles'
| 'stop'
| 'sun-moon'
| 'thumbs-down'
| 'thumbs-up'
| 'tweaks'
| 'upload'
| 'trash'
| 'x-brand'
| 'zoom-in'
| 'zoom-out';
@ -58,433 +120,135 @@ interface Props extends Omit<SVGProps<SVGSVGElement>, 'name'> {
size?: number | string;
}
// Dispatch table: each of our internal names → the lucide-react component
// that renders it. `null` means the name is handled specially below
// (brand marks that lucide does not carry, or the spinner that wants its
// own animation className).
const ICON_MAP: Record<IconName, LucideIcon | null> = {
'arrow-left': ArrowLeft,
'arrow-up': ArrowUp,
attach: Paperclip,
bell: Bell,
check: Check,
'chevron-down': ChevronDown,
'chevron-left': ChevronLeft,
'chevron-right': ChevronRight,
close: CloseX,
copy: Copy,
comment: MessageSquare,
discord: null,
download: Download,
draw: PenLine,
edit: SquarePen,
'external-link': ExternalLink,
eye: Eye,
'eye-off': EyeOff,
file: File,
'file-code': FileCode,
folder: Folder,
grid: LayoutGrid,
history: History,
image: ImageIcon,
import: Import,
kanban: KanbanSquare,
languages: Languages,
link: Link,
mic: Mic,
minus: Minus,
'more-horizontal': MoreHorizontal,
orbit: Orbit,
paw: PawPrint,
pencil: Pencil,
plus: Plus,
play: Play,
present: Presentation,
refresh: RefreshCw,
reload: RotateCw,
search: Search,
send: Send,
settings: Settings,
share: Share,
sliders: SlidersVertical,
spinner: null,
sparkles: Sparkles,
stop: Square,
'sun-moon': SunMoon,
'thumbs-down': ThumbsDown,
'thumbs-up': ThumbsUp,
tweaks: SlidersHorizontal,
upload: Upload,
trash: Trash,
'x-brand': null,
'zoom-in': ZoomIn,
'zoom-out': ZoomOut,
};
/**
* Lightweight inline-SVG icon set tuned to the design system. Stroke-based
* (Feather/Lucide style) so they pair cleanly with `currentColor` and adopt
* the local text color. Use sparingly inside buttons that already have
* accessible labels set `aria-hidden` by default.
* Inline icon dispatches to lucide-react under a stable internal name so
* call sites stay decoupled from the icon library. Brand marks (Discord, X)
* are kept as bespoke inline SVG because lucide intentionally does not ship
* brand artwork. The spinner is the only icon that animates; it keeps the
* `icon-spin` className the rest of the app already styles.
*
* Defaults: `size: 14`, `strokeWidth: 1.6` a touch finer than lucide's
* stock 2px stroke, matching the refined typography in this app.
*/
export function Icon({ name, size = 14, strokeWidth = 1.6, ...rest }: Props) {
const common = {
width: size,
height: size,
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
strokeWidth,
strokeLinecap: 'round' as const,
strokeLinejoin: 'round' as const,
'aria-hidden': true,
focusable: 'false' as const,
...rest,
};
switch (name) {
case 'arrow-left':
return (
<svg {...common}>
<path d="M19 12H5" />
<path d="m12 19-7-7 7-7" />
</svg>
);
case 'arrow-up':
return (
<svg {...common}>
<path d="M12 19V5" />
<path d="m5 12 7-7 7 7" />
</svg>
);
case 'attach':
return (
<svg {...common}>
<path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
</svg>
);
case 'bell':
return (
<svg {...common}>
<path d="M6 8a6 6 0 1 1 12 0c0 7 3 9 3 9H3s3-2 3-9" />
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
</svg>
);
case 'check':
return (
<svg {...common}>
<path d="M20 6 9 17l-5-5" />
</svg>
);
case 'chevron-down':
return (
<svg {...common}>
<path d="m6 9 6 6 6-6" />
</svg>
);
case 'chevron-left':
return (
<svg {...common}>
<path d="m15 18-6-6 6-6" />
</svg>
);
case 'chevron-right':
return (
<svg {...common}>
<path d="m9 18 6-6-6-6" />
</svg>
);
case 'close':
return (
<svg {...common}>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
);
case 'copy':
return (
<svg {...common}>
<rect x="9" y="9" width="13" height="13" rx="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
);
case 'comment':
return (
<svg {...common}>
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
);
case 'download':
return (
<svg {...common}>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<path d="m7 10 5 5 5-5" />
<path d="M12 15V3" />
</svg>
);
case 'draw':
return (
<svg {...common}>
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25z" />
<path d="m14.06 6.19 3.75 3.75" />
</svg>
);
case 'edit':
return (
<svg {...common}>
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
);
case 'eye':
return (
<svg {...common}>
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" />
<circle cx="12" cy="12" r="3" />
</svg>
);
case 'eye-off':
return (
<svg {...common}>
<path d="m3 3 18 18" />
<path d="M10.6 10.6a2 2 0 0 0 2.8 2.8" />
<path d="M9.9 4.2A9.9 9.9 0 0 1 12 4c6.5 0 10 8 10 8a17.8 17.8 0 0 1-2.1 3.1" />
<path d="M6.1 6.1C3.5 7.9 2 12 2 12s3.5 8 10 8a9.9 9.9 0 0 0 4.2-.9" />
</svg>
);
case 'external-link':
return (
<svg {...common}>
<path d="M15 3h6v6" />
<path d="M10 14 21 3" />
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
</svg>
);
case 'file':
return (
<svg {...common}>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<path d="M14 2v6h6" />
</svg>
);
case 'file-code':
return (
<svg {...common}>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<path d="M14 2v6h6" />
<path d="m10 13-2 2 2 2" />
<path d="m14 17 2-2-2-2" />
</svg>
);
case 'folder':
return (
<svg {...common}>
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
</svg>
);
case 'grid':
return (
<svg {...common}>
<rect x="3" y="3" width="7" height="7" rx="1" />
<rect x="14" y="3" width="7" height="7" rx="1" />
<rect x="3" y="14" width="7" height="7" rx="1" />
<rect x="14" y="14" width="7" height="7" rx="1" />
</svg>
);
case 'history':
return (
<svg {...common}>
<path d="M3 12a9 9 0 1 0 3-6.7" />
<path d="M3 4v5h5" />
<path d="M12 7v5l3 2" />
</svg>
);
case 'image':
return (
<svg {...common}>
<rect x="3" y="3" width="18" height="18" rx="2" />
<circle cx="9" cy="9" r="2" />
<path d="m21 15-4.5-4.5L7 20" />
</svg>
);
case 'import':
return (
<svg {...common}>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<path d="m17 8-5-5-5 5" />
<path d="M12 3v12" />
</svg>
);
case 'kanban':
return (
<svg {...common}>
<rect x="3" y="4" width="5" height="16" rx="1" />
<rect x="10" y="4" width="5" height="10" rx="1" />
<rect x="17" y="4" width="4" height="13" rx="1" />
</svg>
);
case 'languages':
return (
<svg {...common}>
<path d="m5 8 6 6" />
<path d="m4 14 6-6 2-3" />
<path d="M2 5h12" />
<path d="M7 2h1" />
<path d="m22 22-5-10-5 10" />
<path d="M14 18h6" />
</svg>
);
case 'link':
return (
<svg {...common}>
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 1 0-7.07-7.07L11.75 5.18" />
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 1 0 7.07 7.07l1.71-1.71" />
</svg>
);
case 'mic':
return (
<svg {...common}>
<rect x="9" y="2" width="6" height="11" rx="3" />
<path d="M19 10v1a7 7 0 0 1-14 0v-1" />
<path d="M12 18v3" />
</svg>
);
case 'minus':
return (
<svg {...common}>
<path d="M5 12h14" />
</svg>
);
case 'more-horizontal':
return (
<svg {...common}>
<circle cx="5" cy="12" r="1.4" />
<circle cx="12" cy="12" r="1.4" />
<circle cx="19" cy="12" r="1.4" />
</svg>
);
case 'orbit':
// Tilted elliptical orbit + central body + a small satellite riding the
// path. Reads unmistakably as "orbit/automation" rather than the
// generic refresh loop, and the rotated ellipse keeps the silhouette
// distinct from `refresh` and `reload` at small sizes.
return (
<svg {...common}>
<ellipse
cx="12"
cy="12"
rx="9"
ry="3.5"
transform="rotate(-25 12 12)"
/>
<circle cx="12" cy="12" r="2.25" fill="currentColor" stroke="none" />
<circle cx="16" cy="6.8" r="1.5" fill="currentColor" stroke="none" />
</svg>
);
case 'pencil':
return (
<svg {...common}>
<path d="M12 20h9" />
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4z" />
</svg>
);
case 'plus':
return (
<svg {...common}>
<path d="M12 5v14" />
<path d="M5 12h14" />
</svg>
);
case 'play':
return (
<svg {...common}>
<path d="M6 4v16l14-8z" />
</svg>
);
case 'present':
return (
<svg {...common}>
<rect x="2" y="3" width="20" height="14" rx="2" />
<path d="M8 21h8" />
<path d="M12 17v4" />
</svg>
);
case 'refresh':
return (
<svg {...common}>
<path d="M3 12a9 9 0 0 1 15.9-5.7L21 8" />
<path d="M21 3v5h-5" />
<path d="M21 12a9 9 0 0 1-15.9 5.7L3 16" />
<path d="M3 21v-5h5" />
</svg>
);
case 'reload':
return (
<svg {...common}>
<path d="M21 12a9 9 0 1 1-3-6.7" />
<path d="M21 4v5h-5" />
</svg>
);
case 'search':
return (
<svg {...common}>
<circle cx="11" cy="11" r="7" />
<path d="m21 21-4.3-4.3" />
</svg>
);
case 'send':
return (
<svg {...common}>
<path d="M22 2 11 13" />
<path d="m22 2-7 20-4-9-9-4z" />
</svg>
);
case 'settings':
return (
<svg {...common}>
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 0 1-2.82 2.83l-.06-.07a1.7 1.7 0 0 0-1.88-.33 1.7 1.7 0 0 0-1.04 1.56V21a2 2 0 0 1-4 0v-.1A1.7 1.7 0 0 0 9 19.4a1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.82l.07-.06a1.7 1.7 0 0 0 .33-1.88 1.7 1.7 0 0 0-1.56-1.04H3a2 2 0 0 1 0-4h.1a1.7 1.7 0 0 0 1.56-1.04 1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.07A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1.04-1.56V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1.04 1.56 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.07.06a1.7 1.7 0 0 0-.33 1.87V9a1.7 1.7 0 0 0 1.56 1.04H21a2 2 0 0 1 0 4h-.1a1.7 1.7 0 0 0-1.56 1.04Z" />
</svg>
);
case 'share':
return (
<svg {...common}>
<path d="M4 12v7a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7" />
<path d="m16 6-4-4-4 4" />
<path d="M12 2v13" />
</svg>
);
case 'sliders':
return (
<svg {...common}>
<path d="M4 21v-7" />
<path d="M4 10V3" />
<path d="M12 21v-9" />
<path d="M12 8V3" />
<path d="M20 21v-5" />
<path d="M20 12V3" />
<path d="M1 14h6" />
<path d="M9 8h6" />
<path d="M17 16h6" />
</svg>
);
case 'spinner':
return (
<svg {...common} className={`icon-spin ${rest.className ?? ''}`.trim()}>
<path d="M21 12a9 9 0 1 1-6.22-8.56" />
</svg>
);
case 'sparkles':
return (
<svg {...common}>
<path d="m12 3 1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5z" />
<path d="M19 14v3" />
<path d="M19 21v-1" />
<path d="M22 17h-3" />
<path d="M16 17h-1" />
</svg>
);
case 'stop':
return (
<svg {...common}>
<rect x="6" y="6" width="12" height="12" rx="1.5" />
</svg>
);
case 'sun-moon':
return (
<svg {...common}>
<path d="M12 8a2.83 2.83 0 0 0 4 4 4 4 0 1 1-4-4" />
<path d="M12 2v2" />
<path d="M12 20v2" />
<path d="m4.9 4.9 1.4 1.4" />
<path d="m17.7 17.7 1.4 1.4" />
<path d="M2 12h2" />
<path d="M20 12h2" />
<path d="m6.3 17.7-1.4 1.4" />
<path d="m19.1 4.9-1.4 1.4" />
</svg>
);
case 'tweaks':
return (
<svg {...common}>
<path d="M4 6h13" />
<circle cx="19" cy="6" r="2" />
<path d="M4 18h7" />
<circle cx="13" cy="18" r="2" />
<path d="M17 12H4" />
<circle cx="19" cy="12" r="2" />
</svg>
);
case 'upload':
return (
<svg {...common}>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<path d="m17 8-5-5-5 5" />
<path d="M12 3v12" />
</svg>
);
case 'zoom-in':
return (
<svg {...common}>
<circle cx="11" cy="11" r="7" />
<path d="M11 8v6" />
<path d="M8 11h6" />
<path d="m21 21-4.3-4.3" />
</svg>
);
case 'zoom-out':
return (
<svg {...common}>
<circle cx="11" cy="11" r="7" />
<path d="M8 11h6" />
<path d="m21 21-4.3-4.3" />
</svg>
);
case 'trash':
return (
<svg {...common}>
<path d="M3 6h18" />
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
</svg>
);
default:
return null;
if (name === 'discord') {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="currentColor"
stroke="none"
aria-hidden
focusable="false"
{...rest}
>
<path d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.09.09 0 0 0-.07.03c-.18.33-.39.76-.53 1.09a16.09 16.09 0 0 0-4.8 0c-.14-.34-.35-.76-.54-1.09a.07.07 0 0 0-.07-.03c-1.5.26-2.93.71-4.27 1.33a.06.06 0 0 0-.03.03C2.31 9.39 1.84 13.34 2.07 17.24c0 .03.02.05.04.06a16.18 16.18 0 0 0 4.85 2.43.08.08 0 0 0 .07-.03c.37-.51.7-1.05.99-1.62a.08.08 0 0 0-.04-.11c-.53-.2-1.03-.45-1.51-.73a.08.08 0 0 1-.01-.13c.1-.08.21-.16.3-.24a.08.08 0 0 1 .08-.01c3.21 1.46 6.69 1.46 9.86 0a.08.08 0 0 1 .08.01c.1.08.2.16.3.24a.08.08 0 0 1-.01.13c-.48.28-.98.53-1.51.73a.08.08 0 0 0-.04.11c.3.57.62 1.11 1 1.62a.08.08 0 0 0 .07.03 16.13 16.13 0 0 0 4.86-2.43.07.07 0 0 0 .04-.06c.27-4.5-.45-8.42-2.83-11.88a.06.06 0 0 0-.03-.03zM8.52 14.91c-.95 0-1.74-.87-1.74-1.94s.77-1.94 1.74-1.94c.97 0 1.76.88 1.74 1.94 0 1.07-.78 1.94-1.74 1.94zm6.42 0c-.95 0-1.74-.87-1.74-1.94s.77-1.94 1.74-1.94c.98 0 1.76.88 1.74 1.94 0 1.07-.77 1.94-1.74 1.94z" />
</svg>
);
}
if (name === 'x-brand') {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="currentColor"
stroke="none"
aria-hidden
focusable="false"
{...rest}
>
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
);
}
if (name === 'spinner') {
const className = `icon-spin ${rest.className ?? ''}`.trim();
return (
<Loader2
size={size}
strokeWidth={strokeWidth}
aria-hidden
focusable={false}
{...rest}
className={className}
/>
);
}
const Component = ICON_MAP[name];
if (!Component) return null;
return (
<Component
size={size}
strokeWidth={strokeWidth}
aria-hidden
focusable={false}
{...rest}
/>
);
}

View file

@ -914,31 +914,8 @@ function PlatformPicker({
onChange: (v: NewProjectPlatform[]) => void;
}) {
const [open, setOpen] = useState(false);
const pickerRef = useRef<HTMLDivElement | null>(null);
const wrapRef = useRef<HTMLDivElement | null>(null);
const listboxId = useId();
const selectedOptions = DESIGN_PLATFORMS.filter((option) => value.includes(option.value));
const triggerLabel =
selectedOptions.length === 1
? selectedOptions[0]!.label
: `${selectedOptions.length} platforms`;
const triggerTitle = selectedOptions.map((option) => `${option.label}: ${option.hint}`).join('\n');
useEffect(() => {
if (!open) return;
function onPointer(e: MouseEvent) {
if (pickerRef.current?.contains(e.target as Node)) return;
setOpen(false);
}
function onKey(e: KeyboardEvent) {
if (e.key === 'Escape') setOpen(false);
}
document.addEventListener('mousedown', onPointer);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('mousedown', onPointer);
document.removeEventListener('keydown', onKey);
};
}, [open]);
function togglePlatform(next: NewProjectPlatform) {
const active = value.includes(next);
@ -948,60 +925,97 @@ function PlatformPicker({
onChange(updated.length > 0 ? updated : ['responsive']);
}
useEffect(() => {
if (!open) return;
function onPointer(e: MouseEvent) {
if (wrapRef.current?.contains(e.target as Node)) return;
setOpen(false);
}
function onKey(e: KeyboardEvent) {
if (e.key === 'Escape') setOpen(false);
}
// Defer listener registration by a tick so the very click that opened
// the popover doesn't get re-interpreted as an outside-click on the
// mousedown that follows in the same event cycle.
const tid = window.setTimeout(() => {
document.addEventListener('mousedown', onPointer);
document.addEventListener('keydown', onKey);
}, 0);
return () => {
window.clearTimeout(tid);
document.removeEventListener('mousedown', onPointer);
document.removeEventListener('keydown', onKey);
};
}, [open]);
const primary = DESIGN_PLATFORMS.find((o) => o.value === value[0]) ?? null;
const extraCount = Math.max(0, value.length - 1);
return (
<div className="newproj-section">
<div
className="newproj-section ds-picker platform-picker"
ref={wrapRef}
>
<label className="newproj-label">Target platforms</label>
<p className="platform-picker-hint">
Pick one or more. Responsive web covers browser breakpoints only; add iOS,
Android, tablet app, or desktop app for native cross-platform variants.
</p>
<div className="platform-dropdown" ref={pickerRef}>
<button
type="button"
className="platform-dropdown-trigger"
aria-haspopup="listbox"
aria-expanded={open}
aria-controls={open ? listboxId : undefined}
title={triggerTitle}
onClick={() => setOpen((value) => !value)}
<button
type="button"
className={`ds-picker-trigger${open ? ' open' : ''}${primary ? '' : ' empty'}`}
onClick={() => setOpen((v) => !v)}
aria-haspopup="listbox"
aria-expanded={open}
aria-controls={open ? listboxId : undefined}
>
<span className="ds-picker-meta">
<span className="ds-picker-title">
{primary ? primary.label : 'Pick a platform'}
{extraCount > 0 ? (
<span className="ds-picker-extra-pill">+{extraCount}</span>
) : null}
</span>
</span>
<Icon
name="chevron-down"
size={14}
className="ds-picker-chevron"
style={{ transform: open ? 'rotate(180deg)' : undefined }}
/>
</button>
{open ? (
<div
className="ds-picker-popover"
id={listboxId}
role="listbox"
aria-label="Target platforms"
aria-multiselectable="true"
>
<span className="platform-dropdown-label">{triggerLabel}</span>
<span className="platform-dropdown-count">{selectedOptions.length}</span>
<Icon name="chevron-down" size={12} />
</button>
{open ? (
<div
className="platform-dropdown-menu"
id={listboxId}
role="listbox"
aria-label="Target platforms"
aria-multiselectable="true"
>
<div className="ds-picker-list">
{DESIGN_PLATFORMS.map((option) => {
const active = value.includes(option.value);
return (
<button
key={option.value}
type="button"
className={`platform-dropdown-item${active ? ' active' : ''}`}
role="option"
aria-selected={active}
title={option.hint}
className={`ds-picker-item${active ? ' active' : ''}`}
onClick={() => togglePlatform(option.value)}
>
<span className="platform-dropdown-check" aria-hidden>
{active ? <Icon name="check" size={12} /> : null}
<span className="ds-picker-item-text">
<span className="ds-picker-item-title">{option.label}</span>
<span className="ds-picker-item-sub">{option.hint}</span>
</span>
<span className="platform-dropdown-copy">
<span className="platform-dropdown-title">{option.label}</span>
<span className="platform-dropdown-hint">{option.hint}</span>
<span
className={`ds-picker-mark check${active ? ' active' : ''}`}
aria-hidden
>
{active ? '✓' : ''}
</span>
</button>
);
})}
</div>
) : null}
</div>
</div>
) : null}
</div>
);
}
@ -1021,14 +1035,14 @@ function SurfaceOptions({
return (
<div className="newproj-section surface-options">
<label className="newproj-label">{t('newproj.surfaceOptionsLabel')}</label>
<div className="surface-checkbox-row">
<SurfaceOptionCheckbox
<div className="compact-toggle-list">
<CompactToggle
label={t('newproj.includeLandingPage')}
hint={t('newproj.includeLandingPageHint')}
checked={includeLandingPage}
onChange={onIncludeLandingPage}
/>
<SurfaceOptionCheckbox
<CompactToggle
label={t('newproj.includeOsWidgets')}
hint={t('newproj.includeOsWidgetsHint')}
checked={includeOsWidgets}
@ -1039,33 +1053,33 @@ function SurfaceOptions({
);
}
function SurfaceOptionCheckbox({
// Lightweight inline toggle row. The hint moves to a native tooltip so the
// row stays one line tall — used by SurfaceOptions where the toggles are
// secondary controls and the full card treatment of ToggleRow felt too heavy.
function CompactToggle({
label,
hint,
checked,
onChange,
disabled,
}: {
label: string;
hint: string;
hint?: string;
checked: boolean;
disabled?: boolean;
onChange: (v: boolean) => void;
}) {
return (
<button
type="button"
className={`surface-checkbox${checked ? ' active' : ''}`}
role="checkbox"
aria-checked={checked}
aria-label={`${label}. ${hint}`}
onClick={() => onChange(!checked)}
className={`compact-toggle${checked ? ' on' : ''}${disabled ? ' disabled' : ''}`}
onClick={() => { if (!disabled) onChange(!checked); }}
aria-pressed={checked}
disabled={disabled}
title={hint}
>
<span className="surface-checkbox-box" aria-hidden>
{checked ? <Icon name="check" size={12} /> : null}
</span>
<span className="surface-checkbox-label">{label}</span>
<span className="surface-checkbox-tip" role="tooltip">
{hint}
</span>
<span className="compact-toggle-label">{label}</span>
<span className="compact-toggle-switch" aria-hidden />
</button>
);
}

View file

@ -1542,7 +1542,6 @@ export function SettingsDialog({
</>
) : (
<>
<span className="kicker">{t('settings.kicker')}</span>
<h2>{activeHeader.title}</h2>
<p className="subtitle">{activeHeader.subtitle}</p>
</>
@ -1688,7 +1687,7 @@ export function SettingsDialog({
className={`settings-nav-item${activeSection === 'pet' ? ' active' : ''}`}
onClick={() => setActiveSection('pet')}
>
<Icon name="sparkles" size={18} />
<Icon name="paw" size={18} />
<span>
<strong>{t('pet.navTitle')}</strong>
<small>{t('pet.navHint')}</small>
@ -1957,7 +1956,7 @@ export function SettingsDialog({
}}
aria-pressed={active}
>
<AgentIcon id={a.id} size={40} />
<AgentIcon id={a.id} size={32} />
<div className="agent-card-body">
<div className="agent-card-name">{a.name}</div>
<div className="agent-card-meta">
@ -1990,7 +1989,7 @@ export function SettingsDialog({
role="group"
aria-label={cardLabel}
>
<AgentIcon id={a.id} size={40} />
<AgentIcon id={a.id} size={32} />
<div className="agent-card-body">
<div className="agent-card-name">{a.name}</div>
<div className="agent-card-meta">

File diff suppressed because it is too large Load diff

View file

@ -4,18 +4,42 @@ import { describe, expect, it } from 'vitest';
import { AgentIcon } from '../../src/components/AgentIcon';
describe('AgentIcon', () => {
it('renders Qoder with a dedicated supplied-mark visual', () => {
it('renders a color-baked agent SVG as an <img> pointing at the bundled asset', () => {
// qoder has explicit fill colors (#111113, #2adb5c) so it does NOT
// need theme-aware rendering — `<img>` is fine.
const markup = renderToStaticMarkup(<AgentIcon id="qoder" size={24} />);
expect(markup).toContain('background:#111113');
expect(markup).toContain('fill="#2ADB5C"');
expect(markup).toContain('fill="#FFFFFF"');
expect(markup).toContain('src="/agent-icons/qoder.svg"');
expect(markup).toContain('class="agent-icon"');
expect(markup).toContain('aria-hidden="true"');
});
it('keeps unknown agents on the generic fallback visual', () => {
it('renders Devin as a PNG (Cognition does not publish an SVG mark)', () => {
const markup = renderToStaticMarkup(<AgentIcon id="devin" size={24} />);
expect(markup).toContain('src="/agent-icons/devin.png"');
});
it('renders monochrome SVGs as a CSS-masked <span> so they pick up theme color', () => {
// cursor-agent.svg ships with `fill="currentColor"` and would lose its
// ink under a dark theme if loaded through `<img>` (which would make
// it a separate document that can't inherit `--text`). Rendering it
// as a mask + `background-color: currentColor` lets CSS theme the mark.
const markup = renderToStaticMarkup(<AgentIcon id="cursor-agent" size={24} />);
expect(markup).toContain('class="agent-icon agent-icon-mono"');
expect(markup).toContain('mask-image:url(&quot;/agent-icons/cursor-agent.svg&quot;)');
// Crucially NOT an <img> — that's exactly the regression we're fixing.
expect(markup).not.toContain('<img src="/agent-icons/cursor-agent.svg"');
});
it('falls back to an initial-letter pill for unknown agents', () => {
const markup = renderToStaticMarkup(<AgentIcon id="unknown-agent" size={24} />);
expect(markup).toContain('linear-gradient(135deg, #6b7280 0%, #4b5563 100%)');
expect(markup).not.toContain('fill="#2ADB5C"');
expect(markup).toContain('agent-icon-fallback');
// Initial = first alphabetic char of the id, uppercased.
expect(markup).toContain('>U</span>');
// The fallback uses CSS class styling, not inline gradients.
expect(markup).not.toContain('linear-gradient');
});
});

View file

@ -164,7 +164,9 @@ describe('NewProjectPanel design system defaults', () => {
fireEvent.change(screen.getByTestId('new-project-name'), {
target: { value: 'Responsive web payload' },
});
fireEvent.click(screen.getByRole('checkbox', { name: /OS widgets/i }));
// CompactToggle renders as a `<button aria-pressed>` so screen readers
// announce it as a toggle button; the role is `button`, not `checkbox`.
fireEvent.click(screen.getByRole('button', { name: /OS widgets/i }));
fireEvent.click(screen.getByTestId('create-project'));
const payload = onCreate.mock.calls[0]?.[0];

View file

@ -44,7 +44,7 @@ let
# `nix build .#daemon` will fail with the expected hash printed; copy
# that into `pnpmDepsHash` below. Bump it whenever pnpm-lock.yaml
# changes.
pnpmDepsHash = "sha256-PGFgX4lYyeH2TRAXfUq52A3EOa6bb1gO59hPsXhEk3s=";
pnpmDepsHash = "sha256-/C9tl0CY/vbDr369wVowsrEMxhljX0pnW7kGZbz3Fas=";
# pnpmDepsHash = lib.fakeHash;
in
stdenv.mkDerivation (finalAttrs: {

View file

@ -30,7 +30,7 @@ let
# `nix build .#web` will fail with the expected hash printed; copy
# that into `pnpmDepsHash` below. Bump it whenever pnpm-lock.yaml
# changes.
pnpmDepsHash = "sha256-PGFgX4lYyeH2TRAXfUq52A3EOa6bb1gO59hPsXhEk3s=";
pnpmDepsHash = "sha256-/C9tl0CY/vbDr369wVowsrEMxhljX0pnW7kGZbz3Fas=";
# pnpmDepsHash = lib.fakeHash;
in
stdenv.mkDerivation (finalAttrs: {

View file

@ -207,6 +207,9 @@ importers:
'@open-design/sidecar-proto':
specifier: workspace:*
version: link:../../packages/sidecar-proto
lucide-react:
specifier: ^1.14.0
version: 1.14.0(react@18.3.1)
next:
specifier: ^16.2.5
version: 16.2.5(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -3087,6 +3090,11 @@ packages:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
lucide-react@1.14.0:
resolution: {integrity: sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
lz-string@1.5.0:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true
@ -7587,6 +7595,10 @@ snapshots:
dependencies:
yallist: 4.0.0
lucide-react@1.14.0(react@18.3.1):
dependencies:
react: 18.3.1
lz-string@1.5.0: {}
magic-string@0.30.21: