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.
|
|
@ -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",
|
||||
|
|
|
|||
1
apps/web/public/agent-icons/claude.svg
Normal 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 |
1
apps/web/public/agent-icons/codex.svg
Normal 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 |
1
apps/web/public/agent-icons/copilot.svg
Normal 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 |
1
apps/web/public/agent-icons/cursor-agent.svg
Normal 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 |
1
apps/web/public/agent-icons/deepseek.svg
Normal 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 |
BIN
apps/web/public/agent-icons/devin.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
1
apps/web/public/agent-icons/gemini.svg
Normal 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 |
1
apps/web/public/agent-icons/hermes.svg
Normal file
|
After Width: | Height: | Size: 18 KiB |
1
apps/web/public/agent-icons/kilo.svg
Normal 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 |
1
apps/web/public/agent-icons/kimi.svg
Normal 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 |
1
apps/web/public/agent-icons/kiro.svg
Normal 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 |
1
apps/web/public/agent-icons/mimo.svg
Normal 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 |
1
apps/web/public/agent-icons/opencode.svg
Normal 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 |
1
apps/web/public/agent-icons/pi.svg
Normal 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 |
1
apps/web/public/agent-icons/qoder.svg
Normal 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 |
1
apps/web/public/agent-icons/qwen.svg
Normal 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 |
1
apps/web/public/agent-icons/vibe.svg
Normal 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 |
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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("/agent-icons/cursor-agent.svg")');
|
||||
// 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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||