feat(desktop,host-service): v2 project colors — icon+color picker and v1 import carry-over

Merged
KI
3mo ago
+1498-77

Links

  • Linear: SUPER-1779 (Laurent Perrier's V2 feedback)

Summary

  • Projects get a host-local accent color (new column + project.setColor), shown as the v1-style tinted border/letter on v2 project thumbnails in settings and the dashboard sidebar.
  • The V2 project settings "Icon" field is now a simple icon+color picker: color swatches plus a 12-glyph grid, alongside the existing upload/reset.
  • V1 → V2 import carries the project's accent color over (both the auto-migrator and the import wizard).

Why / Context

Laurent's V2 feedback: color-coded project icons are how users orient across many projects, and V1 colors were lost on import. His third item — "Remove icon" throwing No procedure found on path v2/project/removeIcon — was already fixed at the source by #5845 (the dead cloud icon machinery was deleted); no call sites remain and reset goes through project.setIcon(null). This PR covers the two remaining items.

How It Works

  • Host-service: projects.color column (drizzle-kit migration 0015, applies on host boot), threaded through ProjectSnapshot, project.list/project.get, and the project:changed event. New project.setColor mutation mirrors setIcon (validates #rrggbb, null clears).
  • Renderer data layer: useHostProjects rows/merge and the dashboard sidebar tree carry color; older hosts and stale IndexedDB snapshots normalize to null.
  • Picker: IconUploadField becomes a popover with ColorSelector swatches (extended with an opt-in Default option) and a lucide glyph grid. Glyphs are baked client-side into the same 128px PNG data-URI the upload path uses, so they flow through resolveProjectIconUrl and appear on every surface with zero resolver changes. Picking a color re-bakes a glyph chosen in the same popover session.
  • Migration: importV1Project best-effort calls setColor after a successful import; the v1 "default" sentinel stays unset, and a failure logs without failing the import.

Manual QA Checklist

Icon + color picker (v2 project settings)

  • Thumbnail opens the popover; color swatch persists and tints the header thumbnail + sidebar rows
  • Glyph pick bakes a tinted tile; picking a color afterwards (same popover session) re-tints the glyph
  • Upload image and Reset icon still work; Default swatch clears the color
  • Offline/remote host shows the offline toast instead of crashing

V1 → V2 import

  • Importing a v1 project with a custom color shows that color on the v2 project
  • v1 projects with the "default" color import with no accent

Migration safety

  • Host DB migration 0015 applies on an existing host.db (additive ALTER TABLE ... ADD color text)

Testing

  • bun run typecheck — pass (all packages)
  • bun run lint — clean
  • apps/desktop bun test — 2541 pass / 0 fail (includes new color carry-over test in runV1Migration.test.ts)
  • packages/host-service bun test — 997 pass / 0 fail
  • Not manually QA'd in a running app — implemented and verified via tests/typecheck only; the QA checklist above is for reviewer validation.

Design Decisions

  • Bake glyphs into PNG data-URIs instead of a new icon scheme: reuses the entire existing icon pipeline (setIcon, resolveProjectIconUrl, every thumbnail surface) with no resolver or protocol changes. Trade-off: a baked glyph can't re-tint retroactively, so only a glyph picked in the same popover session follows a color change.
  • Color as a separate column, not baked into the icon: matches v1 semantics (tinted border works with uploaded images and GitHub avatars too) and is what the migration needs to carry v1 colors that have no glyph.

Known Limitations

  • Picking a color against a host running an older host-service (no setColor) surfaces "No procedure found" as a toast — same version-skew behavior as other new host procedures.
  • V1 project ordering is not carried over: v2 sidebar order lives in a renderer-local collection with its own tabOrder semantics, so it isn't a cheap add. Deferred.

Compatibility Matrix

  • New desktop renderer + old host: color reads normalize to null (no accent); color writes toast a version error.
  • Old desktop renderer + new host: extra color field in list/get/events is ignored.

Summary by cubic

Add host-local project accent colors to v2 and let users remove the project icon. Ships an icon+color picker with tinted thumbnails/sidebar, carries v1 color and hide-image, and fixes picker edge cases (glyphs re-bake only after color saves, swatches disable while saving, strict #rrggbb parsing) (Linear SUPER-1779).

  • New Features

    • host-service: add projects.color and project.setColor; thread through list/get/events. project.setIcon now accepts a "none" sentinel to force the letter tile (no avatar fallback).
    • Project settings: the "Icon" popover has color swatches (Default), a 12-glyph grid baked into PNG data-URIs, Upload, Remove icon, and Reset. Swatches disable during saves; a glyph re-bakes only after the new color persists.
    • Thumbnails and the dashboard sidebar use the accent color (tinted border/letter), including the letter fallback when the icon is removed. Strict #rrggbb parsing avoids malformed tints.
  • Migration

    • DB migration 0016 adds projects.color (additive; applies on host boot).
    • V1 → V2 import copies real hex colors and maps hide_image to the "none" icon; the v1 "default" color sentinel stays unset; failures don’t block import.
    • Mixed versions: color writes and Remove icon against an old host show a version toast; old desktop ignores the extra color field.

Summary by CodeRabbit

  • New Features
    • Added customizable project accent colors across project thumbnails, sidebars, and settings.
    • Added color selection alongside icon uploads, Lucide icons, icon removal, and reset-to-default controls.
    • Projects can now explicitly hide icons instead of displaying a fallback avatar.
    • Existing project colors and icon visibility are preserved during V1 imports.
    • Color selectors and icon controls support disabled states and clearer default styling.