v0.3 Phase 4f/5a/5b: token architecture + skeleton + browser-preview reclass

Three small phases land together for Jake's round-4 feedback. Each is
focused and reviewable. Larger Dictation/Settings/Tasks/History page
migrations are deferred to focused future sittings.

Phase 4f — semantic source / derived token architecture.

  Added --semantic-{red,blue,green,yellow}-source raw-palette tokens.
  These encode the role's hue identity from Jake's color.adobe.com
  palette (#FF0700 / #FFCD00 / #00FF56 / #000AFF) and stay constant
  across all themes. The existing --color-{danger,caution,success,info}
  tokens are now documented as theme-tuned derivations meeting WCAG
  AA in context. Source tokens make the brand-to-runtime relationship
  traceable in code.

  Added --color-{role}-bg subtle-fill tokens derived via
  color-mix(in oklab, var(--color-{role}) 8%, transparent). Used by
  Notice backgrounds, hover tints and focus halos. HC mode forces
  -bg tokens to transparent so HC notices read as text + icon +
  2-px border + left bar with no body tint.

Phase 5a — page skeleton primitive + slim notice variant.

  NEW src/lib/ui/LumotiaPageSkeleton.svelte. Six-zone layout:
    header (optional)
    notice (optional, slim ambient state)
    primary work surface (required, primaryBleed for full-bleed)
    right rail (optional, lg-screen drawer)
    action bar (optional, bottom)
    metadata (optional, mono micro-footer)
  Every zone except primary is opt-in. Foundation for the
  page-skeleton sweep without forcing any page migration yet.

  LumotiaNotice gained a slim prop. When true: padding collapses
  (px-3 py-2 vs px-4 py-3), icon shrinks (14 vs 16), title block is
  suppressed. For ambient page-level notices like browser-preview.

Phase 5b — browser-preview reclassification.

  src/lib/pages/DictationPage.svelte: the browser-preview state used
  to render as a red error block. That is an environment limitation,
  not a failed action. Reclassified as a slim info notice with
  friendlier copy:

    "You're in a browser preview. Local transcription only works in
     the Lumotia desktop app."

  Other error states (transcription failed, needs review) still
  render through the original danger-tinted block with the
  technical-details affordance preserved. Only the browser-preview
  path branches into LumotiaNotice tone="info" slim.

The larger Dictation page restructure (header recomposition,
Template/Extract Tasks demotion to bottom action bar, empty-state
typography) is reserved for a focused Phase 5c sitting since
DictationPage.svelte is 1 262 LOC of state-heavy code.

Verified.

  - npm run check: 0 errors, 0 warnings across 5707 files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 14:12:33 +01:00
parent dab9c7b5a6
commit 2fcb5aff78
5 changed files with 227 additions and 34 deletions

View File

@@ -285,6 +285,45 @@ LumotiaNotice opacity rebalance:
| Icon | `--color-text` | role colour |
| Title / body | `--color-text` | `--color-text` (unchanged) |
### Phase 4f — Semantic source / derived token architecture. Landed 2026-05-15.
Round-4 feedback formalised the relationship between the brand-source palette and the theme-derived semantic tokens. Source tokens are stable across all themes (they encode the role's hue identity); derived tokens are theme-tuned to meet WCAG AA in context.
Added to `src/design-system/v0.3-quietware-tokens.css`:
```css
:root[data-design="quietware"] {
--semantic-red-source: #FF0700; /* error / failed / destructive / blocked */
--semantic-yellow-source: #FFCD00; /* needs review / caution / pending */
--semantic-green-source: #00FF56; /* ready / saved / complete / safe */
--semantic-blue-source: #000AFF; /* info / processing / system guidance */
}
```
The existing `--color-{danger,caution,success,info}` tokens are documented as derivations of these sources, retuned per theme (muted-Material values in normal modes, clarity-first values in HC). New `--color-{role}-bg` subtle-fill tokens derived via `color-mix(in oklab, var(--color-{role}) 8%, transparent)` — used by Notice backgrounds, hover tints and focus halos. HC mode forces all `-bg` tokens to `transparent` so HC notices render as text + icon + 2-px border + left bar with no body tint.
### Phase 5a — Page skeleton primitive + slim notice variant. Landed 2026-05-15.
Foundation for the page-skeleton sweep without yet migrating any page. Two additions:
- **`LumotiaPageSkeleton.svelte`** ([src/lib/ui/](../../src/lib/ui/LumotiaPageSkeleton.svelte)) — six-zone layout used by every quietware page: header / optional slim notice / primary work surface / optional right rail / optional bottom action bar / optional mono metadata footer. Sidebar lives in AppChrome above the skeleton. Every snippet is optional except `primary`. `primaryBleed` prop drops the surface's inner card chrome for full-bleed transcript canvases.
- **`LumotiaNotice` `slim` prop** — collapses padding, drops the title block, smaller icon. For one-line ambient notices like browser-preview state.
### Phase 5b — Browser-preview reclassification. Landed 2026-05-15.
Targeted scope per Jake's round-4 feedback: the browser-preview state used to render as a red error block in `DictationPage.svelte`. That is an environment limitation, not a failed action. Reclassified as a slim info notice with friendlier copy:
```
Old: bg-danger/10 border-danger/20, red text, technical-details affordance
New: LumotiaNotice tone="info" slim, "You're in a browser preview. Local
transcription only works in the Lumotia desktop app."
```
Other error states (transcription failed, needs review) still render through the original danger-tinted block with the technical-details `<details>` affordance preserved. Only the browser-preview path branches into the info-classed notice.
The larger Dictation page restructure (header recomposition, Template/Extract Tasks demotion to bottom action bar, empty-state typography) is reserved for a focused Phase 5c sitting — `DictationPage.svelte` is 1 262 LOC of state-heavy code and warrants surgical attention rather than a tail-end rush.
### Phase 4e — High-contrast rendering contract. Landed 2026-05-15.
High-contrast is not a colour variant — it's a different rendering contract that the entire UI subscribes to. The tokens block now publishes behavioural variables alongside the palette overrides, so components can opt into the contract by reading them.

View File

@@ -65,6 +65,33 @@
font-display: swap;
}
/* ============================================================
Semantic source palette (Phase 4f, 2026-05-15).
These four hex values are the BRAND-identity source for the
semantic system. They are NEVER used directly as text or fill
in the runtime UI — each --color-{role} is a theme-tuned
derivation that meets WCAG AA in its context. The source tokens
exist so the relationship between brand-source and theme-derived
is traceable in code (and so a future re-tune of one role does
not require chasing every reference).
Red -> error, failed, destructive, blocked
Yellow -> needs review, caution, missing setup, pending
Green -> ready, saved, complete, safe
Blue -> information, processing, neutral system guidance
The brand accent (--color-accent, copper-clay) is intentionally
separate from semantic yellow. Brand is action / Lumotia
identity. Yellow is caution / review.
============================================================ */
:root[data-design="quietware"] {
--semantic-red-source: #FF0700;
--semantic-yellow-source: #FFCD00;
--semantic-green-source: #00FF56;
--semantic-blue-source: #000AFF;
}
/* ============================================================
Quietware — dark mode default.
Warm brown-charcoal atmosphere. Reads as warm when you notice
@@ -89,12 +116,22 @@
--color-accent-subtle: #C9784514;
--color-accent-glow: #C9784525;
/* Semantic palette. Muted-Material hues. */
--color-danger: #FF8A8A;
--color-caution: #F2C94C;
/* Semantic palette. Derived from --semantic-{role}-source by
lowering saturation/chroma and tuning lightness for AA on the
dark surface. Each role keeps the source hue identity. */
--color-danger: #FF8A8A; /* derived from --semantic-red-source #FF0700 */
--color-caution: #F2C94C; /* derived from --semantic-yellow-source #FFCD00 */
--color-warning: var(--color-caution);
--color-success: #79D59B;
--color-info: #8AB4F8;
--color-success: #79D59B; /* derived from --semantic-green-source #00FF56 */
--color-info: #8AB4F8; /* derived from --semantic-blue-source #000AFF */
/* Subtle role fills for Notice backgrounds, hover tints, focus
halos. ~8% role over the surface — passes through atmosphere
without becoming a colour zone. */
--color-danger-bg: color-mix(in oklab, var(--color-danger) 8%, transparent);
--color-caution-bg: color-mix(in oklab, var(--color-caution) 8%, transparent);
--color-success-bg: color-mix(in oklab, var(--color-success) 8%, transparent);
--color-info-bg: color-mix(in oklab, var(--color-info) 8%, transparent);
--color-sidebar: #0F0E0C;
--color-nav-active: #211D18;
@@ -142,11 +179,17 @@
--color-accent-subtle: #9D5F3214;
--color-accent-glow: #9D5F3225;
--color-danger: #B3261E;
--color-caution: #7A5D00;
/* Semantic palette — derivations for light mode, AA on cream. */
--color-danger: #B3261E; /* derived from --semantic-red-source */
--color-caution: #7A5D00; /* derived from --semantic-yellow-source */
--color-warning: var(--color-caution);
--color-success: #1B6B3A;
--color-info: #2457A6;
--color-success: #1B6B3A; /* derived from --semantic-green-source */
--color-info: #2457A6; /* derived from --semantic-blue-source */
--color-danger-bg: color-mix(in oklab, var(--color-danger) 8%, transparent);
--color-caution-bg: color-mix(in oklab, var(--color-caution) 8%, transparent);
--color-success-bg: color-mix(in oklab, var(--color-success) 8%, transparent);
--color-info-bg: color-mix(in oklab, var(--color-info) 8%, transparent);
--color-sidebar: #F3EEE6;
--color-nav-active: #E8E2D6;
@@ -191,6 +234,13 @@
--color-success: #1B6B3A;
--color-info: #005FCC;
/* HC strips fill atmosphere. Notices become text + icon + 2-px
border + left bar with no body tint. Maximum clarity. */
--color-danger-bg: transparent;
--color-caution-bg: transparent;
--color-success-bg: transparent;
--color-info-bg: transparent;
--color-sidebar: #FFFFFF;
--color-nav-active: #000000;
--color-hover: #F0F0F0;

View File

@@ -29,6 +29,10 @@
const prefs = getPreferences();
const tauriRuntimeAvailable = hasTauriRuntime();
const browserPreviewMessage = "You're viewing Lumotia in a normal browser. Local transcription only works in the Tauri desktop app window.";
// v0.3 Phase 5b — shorter calmer copy used for the info-classed
// notice when the same condition surfaces in the UI. Same physical
// limitation, friendlier framing per round-4 feedback.
const browserPreviewSlim = "You're in a browser preview. Local transcription only works in the Lumotia desktop app.";
let transcript = $state("");
let segments = $state([]);
@@ -1127,9 +1131,17 @@
</div>
{/if}
<!-- Error — Sub-task D: structured copy with technical detail behind <details> -->
<!-- Error — Sub-task D: structured copy with technical detail behind <details>.
v0.3 Phase 5b: browser-preview state is reclassified info (not error)
because it is an environment limitation, not a failed action. Other
errors continue to render as danger with technical details affordance. -->
{#if error}
<div class="px-5 pt-2 animate-fade-in flex-shrink-0">
{#if error === browserPreviewMessage}
<LumotiaNotice tone="info" slim dismissible onDismiss={() => { error = ""; errorDetail = ""; }}>
{browserPreviewSlim}
</LumotiaNotice>
{:else}
<div class="px-4 py-3 rounded-lg bg-danger/10 border border-danger/20 text-[12px] text-danger space-y-2">
<div class="flex items-start gap-2">
{#if transcriptionFailed}
@@ -1152,6 +1164,7 @@
>Dismiss</button>
</div>
</div>
{/if}
</div>
{/if}

View File

@@ -32,6 +32,9 @@
title?: string;
dismissible?: boolean;
onDismiss?: () => void;
/** Slim variant collapses to a single line with no title block.
For ambient page-level notices (eg. "You're in browser preview"). */
slim?: boolean;
children?: Snippet;
classes?: string;
}
@@ -41,6 +44,7 @@
title,
dismissible = false,
onDismiss,
slim = false,
children,
classes = "",
}: Props = $props();
@@ -85,13 +89,13 @@
<div
role={ariaRole}
class="px-4 py-3 rounded-r-lg rounded-l-sm border border-l-4
text-[13px] flex items-start gap-2.5
class="rounded-r-lg rounded-l-sm border border-l-4 text-[13px] flex gap-2.5
{slim ? 'px-3 py-2 items-center' : 'px-4 py-3 items-start'}
{tonePal.bar} {tonePal.border} {tonePal.bg} {classes}"
>
<ToneIcon size={16} class="mt-0.5 shrink-0 {tonePal.iconColor}" aria-hidden="true" />
<ToneIcon size={slim ? 14 : 16} class="shrink-0 {slim ? '' : 'mt-0.5'} {tonePal.iconColor}" aria-hidden="true" />
<div class="flex-1 min-w-0 leading-relaxed text-text">
{#if title}
{#if title && !slim}
<p class="font-semibold text-text mb-1">{title}</p>
{/if}
{#if children}{@render children()}{/if}

View File

@@ -0,0 +1,87 @@
<script lang="ts">
// v0.3 Phase 5a — shared page skeleton.
//
// Six-zone layout used by every quietware page so structure stays
// consistent across Dictation / Files / Tasks / History / Settings.
// Sidebar lives in AppChrome above this — the skeleton owns the
// right-of-sidebar column only.
//
// +----------------------------+
// | header | page title + status cluster
// |----------------------------|
// | notice (slim, optional) | one-line ambient state
// |----------------------------|
// | |
// | primary work surface | the page's centre of attention
// | |
// |----------------------------|
// | action bar (optional) | post-capture / per-row actions
// |----------------------------|
// | metadata (mono, tiny) | hotkey hint · backend · model
// +----------------------------+
//
// Every snippet is optional except primary. Pages compose the
// skeleton; the skeleton does not impose copy or controls.
import type { Snippet } from "svelte";
interface Props {
header?: Snippet;
notice?: Snippet;
primary: Snippet;
actionBar?: Snippet;
metadata?: Snippet;
/** Right rail / drawer — opt-in narrow column to the right of the primary surface. */
rightRail?: Snippet;
/** Drop the primary surface's outer padding for full-bleed content (eg. transcript canvas). */
primaryBleed?: boolean;
classes?: string;
}
let {
header,
notice,
primary,
actionBar,
metadata,
rightRail,
primaryBleed = false,
classes = "",
}: Props = $props();
</script>
<div class="flex flex-col h-full min-h-0 bg-bg {classes}">
{#if header}
<div class="shrink-0 px-7 pt-5 pb-3 border-b border-border-subtle">
{@render header()}
</div>
{/if}
{#if notice}
<div class="shrink-0 px-7 pt-3">
{@render notice()}
</div>
{/if}
<div class="flex-1 min-h-0 flex {rightRail ? 'gap-5' : ''} px-7 pt-5 pb-3">
<div class="flex-1 min-w-0 {primaryBleed ? '' : 'bg-bg-card border border-border-subtle rounded-lg overflow-hidden'}">
{@render primary()}
</div>
{#if rightRail}
<aside class="w-72 shrink-0 hidden lg:flex flex-col gap-3">
{@render rightRail()}
</aside>
{/if}
</div>
{#if actionBar}
<div class="shrink-0 px-7 py-3 border-t border-border-subtle bg-bg-elevated/60">
{@render actionBar()}
</div>
{/if}
{#if metadata}
<div class="shrink-0 px-7 py-2 text-[11px] font-mono text-text-tertiary">
{@render metadata()}
</div>
{/if}
</div>