Files
Lumotia/src/lib/ui/LumotiaNotice.svelte
Jake ce784e58ab v0.3 Phase 5c polish: tighter header, disabled-record clarity, softer notice, attached action bar
Focused polish pass on the Dictation quietware layout per round-10
feedback. No colour exploration; only the eight specific tweaks.

1. Capture header tightened.

  Record button + status block now grouped in their own flex
  container (gap-3) so they read as one capture component. Record
  size dropped from 72px to 64px so the group balances against the
  status text. Timer pushed to far right via flex-1 on the inner
  group. The trio (record / status / timer) feels like one unit.

2. Disabled Record affordance now reads as record.

  Browser-preview state no longer renders as a neutral grey blob.
  Instead:

    bg-transparent
    border-2 border-[color-mix(in_srgb,var(--button-record-bg)_45%,
                                transparent)]
    opacity-80
    cursor-not-allowed
    inner muted-red dot bg-[color-mix(in_srgb,var(--button-record-bg)
                                       _55%,transparent)]

  Result: clearly a record control, clearly disabled, doesn't look
  clickable. aria-label updated to "Record (desktop app only)" in
  that state so screen readers carry the same message.

3. Slim notice border softened.

  LumotiaNotice slim variant now uses border-border-subtle for the
  outer border instead of the role-coloured border. Left bar stays
  full role colour (the visible signal). Icon stays role colour.
  Background stays at the role tinted bg. Net effect: notice is
  ambient, not loud.

4. Action bar attached to transcript surface.

  Dropped the LumotiaPageSkeleton actionBar slot for Dictation;
  moved the buttons into the primary snippet as a bottom row inside
  the same surface. Internal border-t + bg-bg-elevated/40 separator
  keeps it visually nested. Reads as transcript actions, not a
  generic page footer.

  Also dropped primaryBleed so LumotiaPageSkeleton's default card
  chrome wraps the transcript + action dock together — that gives
  the surface boundary Jake asked for in light mode without
  introducing a card-heavy treatment.

5. Compact one-line metadata footer.

  Was: "Smart · {model} · Local only"  +  "{formatMode} · {profile}"
  Now: "{formatMode} · {model} · Local only / Browser preview · {profile}"

  Right-aligned. Single line. No duplicated "Smart". JetBrains Mono
  via the existing --font-family-mono. text-tertiary.

6. Typography corrected.

  Status title moved from Young Serif italic (font-display) to Work
  Sans medium-weight (default body). Young Serif italic is now
  reserved for the empty-state "Talk now, think later." line, which
  remains the only emotional moment in the page.

7. Light mode surface intentional.

  By dropping primaryBleed the skeleton's default surface chrome
  (bg-bg-card + border-border-subtle + rounded-lg) wraps the
  transcript canvas. Light mode now reads as a deliberate paper
  surface, not an open page. Same boundary applies in dark.

8. Colour restraint preserved.

  No token changes. Same red/blue/green/yellow/orange grammar.
  Wireline contract unchanged. Brand orange still reserved for
  logo/identity.

Verified.

  - npm run check: 0 errors, 0 warnings across 5707 files.
  - Headless screenshots confirm both quietware modes deliver the
    polish: tight header, recognisable disabled-record, softer
    notice border, attached action dock, compact metadata,
    Work Sans status title, clear surface boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:14:31 +01:00

143 lines
5.8 KiB
Svelte

<script lang="ts">
// v0.2 Phase 5 primitive — inline notice block. Replaces the ~10
// ad-hoc <div class="bg-{danger,warning,success}/10 …"> patterns
// scattered across pages. Four tones map to the semantic tokens.
//
// v0.3 Phase 3 update — left-bar accent pattern. The visible status
// signal lives on a 4-px solid left border in the tone colour, full
// opacity. The surround uses a soft tone-tinted background + outer
// border in the tone colour. Title and body text use --color-text;
// icon uses the tone colour. Same accessibility role semantics
// retained.
//
// v0.3 Phase 4d opacity rebalance (2026-05-15 round 4 feedback):
// Background role colour at 8% (was 10%, less heavy)
// Outer border role colour at 40% (was neutral subtle)
// Left bar role colour at 100% (unchanged)
// Icon role colour (reverted from --color-text once the
// muted-Material palette landed; previous fill-only-
// on-cream concern dissolved with the new ochre
// #7A5D00 light-mode caution).
// Title + body --color-text (unchanged)
//
// Pattern matches Material Design "banner with leading accent" and
// IBM Carbon "inline notification" guidance.
import { Info, AlertTriangle, AlertOctagon, CheckCircle, X } from "lucide-svelte";
import type { Snippet } from "svelte";
type Tone = "info" | "caution" | "danger" | "success";
interface Props {
tone?: Tone;
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;
/** v0.3 Phase 4i opt-in: render the tactile counterline as an
inset shadow. Off by default — notices already carry enough
semantic language without the extra decoration. Reserve for
cases where the notice deliberately mimics control identity. */
counterline?: boolean;
children?: Snippet;
classes?: string;
}
let {
tone = "info" as Tone,
title,
dismissible = false,
onDismiss,
slim = false,
counterline = false,
children,
classes = "",
}: Props = $props();
// v0.3 Phase 4g — notice usage tokens. Each tone reads four CSS
// variables published by v0.3-quietware-tokens.css:
// --notice-{tone}-bar left bar (signal-tier)
// --notice-{tone}-icon icon colour (signal in dark, ink in light)
// --notice-{tone}-border outer border colour
// --notice-{tone}-bg hand-tuned pale tint surface
//
// v0.3 Phase 4h — counterlines. A subtle 1-px inset shadow in a
// softened complementary hue. Tactile-only detail; not a second
// semantic signal. HC contract zeroes --counterline-width.
//
// --notice-{tone}-counterline softened-complement tint
// --counterline-width 1px in quietware, 0 in HC
//
// v0.2 fallback values are defined at :root in the same tokens file
// via color-mix from the v0.2 --color-{role} tokens, so the Notice
// renders sensibly with or without quietware active.
const palette: Record<Tone, { bar: string; bg: string; border: string; iconColor: string; shadow: string; icon: typeof Info }> = {
info: {
bar: "border-l-[var(--notice-info-bar)]",
bg: "bg-[var(--notice-info-bg)]",
border: "border-[var(--notice-info-border)]",
iconColor: "text-[var(--notice-info-icon)]",
shadow: "shadow-[inset_0_0_0_var(--counterline-width,0)_var(--notice-info-counterline,transparent)]",
icon: Info,
},
caution: {
bar: "border-l-[var(--notice-caution-bar)]",
bg: "bg-[var(--notice-caution-bg)]",
border: "border-[var(--notice-caution-border)]",
iconColor: "text-[var(--notice-caution-icon)]",
shadow: "shadow-[inset_0_0_0_var(--counterline-width,0)_var(--notice-caution-counterline,transparent)]",
icon: AlertTriangle,
},
danger: {
bar: "border-l-[var(--notice-danger-bar)]",
bg: "bg-[var(--notice-danger-bg)]",
border: "border-[var(--notice-danger-border)]",
iconColor: "text-[var(--notice-danger-icon)]",
shadow: "shadow-[inset_0_0_0_var(--counterline-width,0)_var(--notice-danger-counterline,transparent)]",
icon: AlertOctagon,
},
success: {
bar: "border-l-[var(--notice-success-bar)]",
bg: "bg-[var(--notice-success-bg)]",
border: "border-[var(--notice-success-border)]",
iconColor: "text-[var(--notice-success-icon)]",
shadow: "shadow-[inset_0_0_0_var(--counterline-width,0)_var(--notice-success-counterline,transparent)]",
icon: CheckCircle,
},
};
// Danger gets role="alert" so screen readers announce immediately;
// the others use the less-interrupting role="status".
const ariaRole = $derived(tone === "danger" ? "alert" : "status");
const tonePal = $derived(palette[tone]);
const ToneIcon = $derived(tonePal.icon);
</script>
<div
role={ariaRole}
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}
{slim ? 'border-border-subtle' : tonePal.border}
{tonePal.bg} {counterline ? tonePal.shadow : ''} {classes}"
>
<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 && !slim}
<p class="font-semibold text-text mb-1">{title}</p>
{/if}
{#if children}{@render children()}{/if}
</div>
{#if dismissible}
<button
type="button"
class="shrink-0 text-text-tertiary hover:text-text size-5 flex items-center justify-center rounded"
aria-label="Dismiss notice"
onclick={onDismiss}
>
<X size={14} />
</button>
{/if}
</div>