UI Taxonomy

CareerVector's shared frontend components live in three lib/ packages, split by audience and quality bar. The split keeps product polish out of admin surfaces (where the cost would be unjustified) and keeps admin-grade shortcuts out of product surfaces (where they would be visible to end users).

The three tiers

Tier Dir Package Audience Quality bar Mobile Examples
1 lib/ui-kit/ @cv/ui-kit Everyone (admin + product) 100% mint Yes (mobile + desktop) shells/HeaderBar.svelte
2 lib/ui-product/ @cv/ui-product CV + JobCache product users 100% (desktop only) No shells/SubHeaderBar.svelte
3 lib/admin-ui/ @cv/admin-ui wiki / ops / qa / status surfaces (developer + AI facing) 80% acceptable Yes components/PrivateFooter.svelte, components/PublicFooter.svelte, components/KnowledgeHubControl.svelte, admin-links.ts

Why the bars differ. ui-kit is consumed by literally every surface, so any glitch shows up everywhere — it must be 100%. ui-product is the surface paying customers see — also 100%, but the constraint relaxes on the mobile axis because the product is desktop-first today. admin-ui is internal (operators, QA engineers, AI agents reading the wiki); shipping a "good enough" component there is the right trade because the time we save funds product polish.

Tier 1: ui-kit (general)

  • Audience: everyone — product, admin, status, wiki, ops, qa.
  • Bar: 100% mint quality. No edge-case shortcuts, full accessibility, both viewports.
  • Mobile: yes. Components must work on mobile AND desktop.
  • Package: @cv/ui-kit.
  • Structure: subdivided into shells/ (composed chrome — HeaderBar) and primitives/ (atomic UI — buttons, dropdowns, icons, modals). primitives/ exists as a reserved slot today (no entries yet).

What lives here today

Path Role
lib/ui-kit/src/shells/HeaderBar.svelte Perspective navigation strip with productLabel, perspectiveLabel, appUrl, optional tab strip, optional rightSlot, readOnly badge, mobile tab strip fallback. Used by admin (wiki/ops/qa/status) for the perspective tabs + KnowledgeHubControl right-slot, and by the CV/JobCache product layouts for the top chrome.
lib/ui-kit/src/primitives/ Reserved for atomic UI primitives. Empty today.

Import patterns

Direct deep imports — no barrel re-exports:

import HeaderBar from '@cv/ui-kit/shells/HeaderBar.svelte';

The package exports map enforces this:

"exports": {
    ".": "./src/index.ts",
    "./shells/*": "./src/shells/*",
    "./primitives/*": "./src/primitives/*"
}

./src/index.ts is intentionally export {} to keep tree-shaking friendly and avoid the Svelte compiler's "barrel imports drag in everything" footgun. Use the root import only for shared TypeScript types.

Tier 2: ui-product (CV + JobCache shared)

  • Audience: CareerVector and JobCache product users.
  • Bar: 100% on desktop. Mobile is not a requirement — the products are desktop-first today.
  • Package: @cv/ui-product.
  • Structure: shells/ only (deep imports same as ui-kit).

What lives here today

Path Role
lib/ui-product/src/shells/SubHeaderBar.svelte Workspace-scoped sub-header row that sits directly under HeaderBar. Owns the row layout (flex items-center gap-2 mb-4) and exposes named snippet slots for product-specific content.

The snippet-slot pattern

SubHeaderBar accepts three named Svelte 5 snippet slots, rendered in row order:

<script lang="ts">
    let {
        leadingSlot,
        identitySlot,
        actionsSlot
    }: {
        leadingSlot?: Snippet;
        identitySlot: Snippet;
        actionsSlot?: Snippet;
    } = $props();
</script>

<div class="flex items-center gap-2 mb-4">
    {#if leadingSlot}{@render leadingSlot()}{/if}
    {@render identitySlot()}
    {#if actionsSlot}{@render actionsSlot()}{/if}
</div>
Slot Required Purpose
leadingSlot optional Small left-aligned area (~32-48px) for product-level CTAs — e.g. the Download desktop button.
identitySlot required Workspace identity (name + slug; editor or read-only mirror).
actionsSlot optional Icon button row (copy link, share, duplicate, delete, etc.).

Render order: leading → identity → actions, separated by gap-2, vertically centered, with mb-4 bottom spacing.

Why snippets vs <slot />. Svelte 5's Snippet props are the idiomatic replacement for the legacy slot API. They type-check at the call site, support default values, and let the parent pass props into the rendered content. Named slots via let:-style scoped slots are not the right tool for a fixed, ordered row of named regions — snippet props express the contract directly in TypeScript.

Wrapper invariant: the shell owns the row layout. Consumers never style the wrapping div. Products own their own icon-button visuals so the row can host both CareerVector and JobCache chrome without coupling the shell to either.

Tier 3: admin-ui (wiki, ops, qa)

  • Audience: wiki / ops / qa / status surfaces. Developer- and AI-facing (operators, QA engineers, internal investigation flows, AI agents reading the wiki).
  • Bar: 80% acceptable. Functional, consistent, but does not need product-grade polish.
  • Mobile: yes — operators check ops dashboards from phones.
  • Package: @cv/admin-ui.
  • Structure: components/, plus the admin-links.ts link-set helper and theme.css.

What lives here today

Path Role
lib/admin-ui/src/admin-links.ts AdminLinkSet, CV_ADMIN_LINKS, JC_ADMIN_LINKS, OPS_TABS, QA_TABS. Builds the {Product · QA · Ops · Status · Wiki} cross-perspective footer link set per product.
lib/admin-ui/src/components/PrivateFooter.svelte Admin-perspective footer variant.
lib/admin-ui/src/components/PublicFooter.svelte Public-status footer variant.
lib/admin-ui/src/components/KnowledgeHubControl.svelte KnowledgeHubControl widget — rendered into ui-kit HeaderBar's rightSlot on admin perspectives.
lib/admin-ui/src/components/KnowledgeHubControl.graph.ts Graph-annotation companion for the control.
lib/admin-ui/src/theme.css Admin-surface theme overrides.

How to decide which tier a new component belongs to

  1. Who uses it?

    • Used by every perspective (product + admin) → ui-kit.
    • Used by CV and JobCache product surfaces only → ui-product.
    • Used by wiki, ops, qa, or status only → admin-ui.
  2. What's the cost of dropping a tier?

    • If you'd lose meaningful polish for product users by leaving it in admin-ui, kick it up.
    • If you'd be paying for polish nobody sees by leaving it in ui-kit, demote it.
  3. Litmus test: "If it lands somewhere that's not 100% but it touches a user, kick it up." A component an end user sees cannot live below 100%.

  4. One-tier rule: if a component is only used by one tier today, don't preemptively place it in ui-kit. Promote later when a second tier actually consumes it.

Workspace consumer pattern

ui/src/lib/components/workspace/ composes ui-product's SubHeaderBar into the live workspace surface. The shell is in ui-product; the snippet contents are CV-specific and live in the consuming app.

The compose site is ui/src/routes/[wsId]/+layout.svelte. Stripped down:

<SubHeaderBar>
    {#snippet leadingSlot()}
        <DownloadDesktopButton />
    {/snippet}
    {#snippet identitySlot()}
        <WorkspaceIdentityEditor
            {workspace}
            {displayName}
            bind:nameElement={nameEl}
            onSave={saveWorkspaceField}
        />
    {/snippet}
    {#snippet actionsSlot()}
        <WorkspaceActionButtons
            {workspace}
            onRequestDelete={() => { ... }}
        />
        <ScrapingPill />
    {/snippet}
</SubHeaderBar>

The read-only mirror layout (ui/src/routes/view/[wsId]/+layout.svelte) uses the same SubHeaderBar with WorkspaceIdentityReadOnly in identitySlot and omits actionsSlot entirely.

The workspace snippet components

File Slot Role
ui/src/lib/components/workspace/DownloadDesktopButton.svelte leadingSlot Icon-button dropdown listing desktop downloads (AppImage / .deb / .rpm / .tar.gz / AUR / .msi / .dmg) with platform auto-detection and a "Recommended for your system" badge. Pure UI — renders identically in browser and Tauri.
ui/src/lib/components/workspace/WorkspaceIdentityEditor.svelte identitySlot Editable workspace name (contenteditable h1) + slug (contenteditable span) with per-field debounced save. Calls back to the parent for the actual PATCH so the layout can also broadcast the new name via Yjs awareness.
ui/src/lib/components/workspace/WorkspaceIdentityReadOnly.svelte identitySlot Read-only name + slug for the /view/[wsId] mirror shell. No contenteditable, no debounce.
ui/src/lib/components/workspace/WorkspaceActionButtons.svelte actionsSlot Icon button row: Copy link, Share read-only (live mirror), Share snapshot (frozen mirror), Duplicate (fork), Delete. Owns per-action pending/success state. Calls onRequestDelete to open the layout-level confirmation modal.
ui/src/lib/components/workspace/ScrapingPill.svelte actionsSlot Desktop-only status widget. Renders nothing in a plain browser. Inside Tauri it shows a compact pill — "Native runtime · v" — that expands into a capability report popover.
@cv/public-ui/components/LinkIdentityActions.svelte actionsSlot Shared copy-link and delete controls for CareerVector workspaces and JobCache sessions; products may insert extra actions between them.
ui/src/lib/components/workspace/workspace-actions.ts n/a CareerVector-specific action handlers (createMirror, deleteWorkspaceRequest) plus the WorkspaceLike type shared by the snippet components.

The runtime-capabilities bridge

ui/src/lib/runtime-capabilities.ts is the Tauri IPC bridge. It exposes isTauri(), getAppVersion(), getCapabilityReport(), getWorkspaceBindings(), and observeUrl(). In a plain browser tab every function returns null (or false for isTauri) so callers branch without crashing. ScrapingPill consumes this bridge to gate its entire render — when isTauri() returns false the component emits no DOM at all, so the desktop-only feature is invisible on the web.

What goes where (catalog)

Path Tier Purpose
lib/ui-kit/src/shells/HeaderBar.svelte 1 Perspective tab strip + product/perspective labels + optional rightSlot.
lib/ui-kit/src/primitives/ 1 Reserved for atomic primitives (empty today).
lib/ui-product/src/shells/SubHeaderBar.svelte 2 Workspace sub-header row with leading/identity/actions snippet slots.
lib/admin-ui/src/admin-links.ts 3 Cross-perspective link sets and admin tab definitions for CV and JC.
lib/admin-ui/src/components/PrivateFooter.svelte 3 Admin-perspective footer.
lib/admin-ui/src/components/PublicFooter.svelte 3 Public-status footer.
lib/admin-ui/src/components/KnowledgeHubControl.svelte 3 Admin-perspective KnowledgeHub control widget.
lib/admin-ui/src/components/KnowledgeHubControl.graph.ts 3 Graph annotation for the control.
lib/admin-ui/src/theme.css 3 Admin theme overrides.

Don'ts

  • Don't import ui-product from admin-ui. Cross-tier upward import — admin-ui's 80% bar can't pay for product-grade polish, and the dependency direction would force ui-product changes to ripple into every admin surface.
  • Don't import admin-ui from ui-product. Different audience. Admin-grade visuals would leak into the CV/JobCache surface that end users see.
  • Don't put a component in ui-kit if only one tier uses it. Promote later, when a second tier actually consumes it. Premature promotion taxes everyone with maintenance cost for no consumer benefit.
  • Don't ship product code at admin-ui quality. The 80% bar exists because operators tolerate rough edges that end users do not. Ui-product is 100% — port admin shortcuts up only by reworking them to the product bar.
  • Don't add a barrel export * to lib/ui-kit/src/index.ts or lib/ui-product/src/index.ts. Deep imports (@cv/ui-kit/shells/HeaderBar.svelte) keep tree-shaking healthy. The root index files stay export {} for shared types only.
  • Don't style SubHeaderBar's wrapper from a consumer. The shell owns flex items-center gap-2 mb-4. Products supply only the slot contents.
Source: wiki/content/architecture/UI-TAXONOMY.md