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) andprimitives/(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 theadmin-links.tslink-set helper andtheme.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
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.
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.
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%.
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 |
@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 *tolib/ui-kit/src/index.tsorlib/ui-product/src/index.ts. Deep imports (@cv/ui-kit/shells/HeaderBar.svelte) keep tree-shaking healthy. The root index files stayexport {}for shared types only. - Don't style
SubHeaderBar's wrapper from a consumer. The shell ownsflex items-center gap-2 mb-4. Products supply only the slot contents.