Skip to content

[Feature] foundation: PawWork-native UI primitives to stop the CSS override tax #210

@Astro-Han

Description

@Astro-Han

Note: Placeholder / candidate. Not scheduled, not a commitment. Capturing the pattern while it's fresh from #208. This is a developer-facing foundation concern, not a user-facing workflow — the closest template fit is below.

What task are you trying to do?

Ship UI work on the composer, chips, and popovers without paying a CSS-override tax every PR.

PawWork's design language has permanently diverged from opencode's (pill shapes, brand orange #FF5910 / #E56A2E, rounded-full / radius-xl popovers, menu-below composer, custom WorkspaceChip / SendButton), but the underlying primitives still come from @opencode-ai/ui (Kobalte wrapper + popover.css + shared Button variants). Every feature PR that touches chips or popovers pays a tax to override upstream styling, and most of the iteration rounds on #208 were spent fighting that tax rather than moving the product forward.

What do you do today?

Patch case by case. From #208 alone:

  • Three popovers (dialog-select-model, prompt-input variant, workspace-chip) use inline style={{ "border-radius": "16px" }} to beat [data-component="popover-content"] { border-radius: var(--radius-md) }. Tailwind ! modifier and class both lose to source order.
  • SendButton uses custom #E56A2E + inline color: #ffffff on the Icon to beat [data-component="icon"] { color: var(--icon-base) }.
  • as any cast on Kobalte triggerProps to inject data-action attrs (Popover types don't accept data-*).
  • Model chip gets hover via Button component; Variant and Workspace chips rebuild hover manually. One round used invalid hover:bg-background-base-hover token (silently dropped by Tailwind) — only visual review caught it.
  • Starter cards iterated rounded-xlrounded-2xlrounded-full across three rounds because no shared chip primitive pinned the shape.

None of these are bugs. They're all the same shape: brand-layer design colliding with a primitive layer that assumes a different design.

What would a good result look like?

Three focused replacements, each a self-contained sub-PR:

  1. PawWork Popover primitive — thin wrapper over Kobalte.Popover that does NOT pull popover.css. Radius / background / border baked into Tailwind at the primitive layer. Migrate model / variant / workspace popovers off the shared @opencode-ai/ui/popover wrapper.
  2. <Chip> primitive — unified trigger for Model / Variant / Workspace chips. Hover, focus, disabled, truncation in one place. Removes piggyback on Button and removes the "why does this chip not hover like that chip" asymmetry.
  3. Brand-layer design tokens — explicit --pawwork-accent-brand, --pawwork-radius-chip, --pawwork-radius-popover, --pawwork-shadow-chip. Stop co-opting @opencode-ai/ui tokens (--radius-md, --accent-brand) whose semantics live elsewhere.

Acceptance criteria:

  • Zero inline style={{ "border-radius": ... }} in prompt-input.tsx, workspace-chip.tsx, dialog-select-model.tsx
  • Zero as any casts on Popover trigger props in packages/app/src/components
  • A new feature PR adding a chip-shaped trigger requires zero CSS override to match design
  • Brand orange / chip radius / popover radius have a single source of truth in the PawWork layer

Audience

Both — this is developer-facing, but it directly improves the speed and quality of every UI iteration that reaches users. Visible payoff is faster, more consistent visual updates for everyone.

Extra context

Non-goals. Not rewriting sidebar / session list / command palette / tool-execution UI / permission dialogs / chat stream rendering. Not replacing @opencode-ai/ui/button wholesale. Not a frontend rewrite. Not changing the opencode-底座 commitment (session / SDK / providers / tools stay upstream).

When (not now). Proposed after #208 lands and the visual language settles ~2 weeks, and after the dynamic shell tabs series (Task 1-13 Codex handoff) lands. Batch with the next deliberate brand iteration so the swap piggybacks on a visible change instead of landing invisibly. Prevents thrashing the foundation layer while the design it supports is still settling.

Alternatives considered.

  • Keep patching. Sustainable, but the tax compounds with every new chip surface. Current state.
  • Full UI rewrite. 2-3 months, blocked by the opencode-底座 commitment (≤10K users, no 自研底座). Rejected.
  • Wait for opencode upstream to match. Design has permanently diverged. Not coming back.

Open questions.

  • Keep @opencode-ai/ui/popover for non-chip popovers (sidebar context menus etc.) and only introduce a second primitive for chips, or replace the shared wrapper entirely? Probably the former — narrower scope.
  • <Chip> primitive in packages/ui/src/components/chip-pw.tsx (shared) or packages/app/src/components/prompt-input/chip.tsx (app-local)? Probably app-local first, promote if reused.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium priorityappApplication behavior and product flowsenhancementNew feature or requestuiDesign system and user interface

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions