Files
Lumotia/src/lib/ui/LumotiaNotice.svelte
Jake db9c50c548 v0.3 Phase 3: status grammar — LumotiaNotice left-bar refactor
Phase 3 audit found that both LumotiaStatusPill and LumotiaNotice
already shipped in v0.2 Phase 5. Phase 3 reduced to an audit-and-adapt
pass.

LumotiaStatusPill ($lib/components/StatusPill.svelte) is already
correct for quietware. Pattern: neutral pill background + neutral
label text + role colour confined to a 6x6 px dot. Colour is
supplementary, label is always perceivable. Works unchanged across
all three quietware modes. No changes shipped.

LumotiaNotice ($lib/ui/LumotiaNotice.svelte) refactored to the
left-bar accent pattern. Previously the role colour rendered on the
icon outline and on the entire surround border at 30% opacity; in
quietware light mode that left the bright #FFCD00 caution colour
faded against cream paper. New pattern:

  - 4-px solid LEFT border in the tone colour, 100% opacity.
  - Soft tone-tinted background (10% opacity) for ambient cue.
  - Subtle neutral outer border on the other three sides.
  - Icon, title and body text all use --color-text.

Visible signal lives on the large bar (works at any contrast level);
foreground text uses the always-legible neutral. Matches Material
Design "banner with leading accent" and IBM Carbon "inline
notification" guidance. ARIA semantics (role="alert" for danger,
role="status" for the rest) preserved.

Design-system-v2 preview route already imports both primitives. The
updated Notice renders correctly with the existing preview content;
no additional wiring required.

Caution remains a fill-only convention on cream by design. Any
darkened yellow reads as olive ("looks like poop", per Jake), so the
fill-only rule stays in force and this pattern delivers it cleanly.

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

97 lines
3.0 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 now lives on a 4-px solid left border in the tone colour
// (100% opacity, fill-grade chroma); the surrounding container uses
// a soft tone-tinted background and a subtle outer border. Icon,
// title and body text all use --color-text so they remain legible
// across every theme combination, including quietware light mode
// where --color-caution is the bright #FFCD00 fill (would fail as
// text on cream). Same accessibility role semantics retained.
//
// Pattern matches Material Design "banner with leading accent" and
// IBM Carbon "inline notification" guidance: status colour on a
// large fill surface (the bar), neutral foreground text. Works for
// any tone in any theme regardless of WCAG-text contrast on the
// role colour itself.
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;
children?: Snippet;
classes?: string;
}
let {
tone = "info" as Tone,
title,
dismissible = false,
onDismiss,
children,
classes = "",
}: Props = $props();
const palette: Record<Tone, { bar: string; bg: string; icon: typeof Info }> = {
info: {
bar: "border-l-info",
bg: "bg-info/10",
icon: Info,
},
caution: {
bar: "border-l-caution",
bg: "bg-caution/10",
icon: AlertTriangle,
},
danger: {
bar: "border-l-danger",
bg: "bg-danger/10",
icon: AlertOctagon,
},
success: {
bar: "border-l-success",
bg: "bg-success/10",
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="px-4 py-3 rounded-r-lg rounded-l-sm border border-l-4
border-border-subtle text-[13px] flex items-start gap-2.5
{tonePal.bar} {tonePal.bg} {classes}"
>
<ToneIcon size={16} class="mt-0.5 shrink-0 text-text" aria-hidden="true" />
<div class="flex-1 min-w-0 leading-relaxed text-text">
{#if title}
<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>