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>
143 lines
5.8 KiB
Svelte
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>
|