Files
OSIT-AE-App-Svelte/documentation/GUIDE__AE_UI_Style_Guidelines.md
Scott Idem 334c3a21bc feat: leads QR scanner — Auto/Multi modes, 4-mode fancy selector, UX polish
Scanner modes (now 4, persisted per exhibit):
- Rapid:   confirm tap → auto-reset (existing, fixed)
- Qualify: confirm tap → navigate to lead detail (existing, fixed)
- Auto:    badge found → auto-add immediately, no confirmation tap needed
- Multi:   BarcodeDetector batch scan → responsive grid of confirm cards

Multi scanner (new ae_comp__lead_qr_scanner_multi.svelte):
- Native BarcodeDetector API (Chrome/Edge/Safari 17+); Firefox fallback message
- 16:9 viewfinder with corner guides + "Align up to 4 badges flat" overlay
- Capture Batch tap → up to 8 QR codes detected in one frame
- Per-card states: loading skeleton, ready (Add/Skip), blocked (opt-out),
  already-captured (View/OK), adding spinner, success (auto-fade), error
- Add All (N) bulk action; cards fade+scale out smoothly on dismiss

Mode selector (ae_tab__add.svelte):
- Replaces Rapid/Qualify toggle with collapsible 4-mode fancy select
- Trigger shows active mode icon (color-coded) + name + description
- 2×2 options grid expands on tap, closes on selection

QR scanner element (element_qr_scanner_v3.svelte):
- object-fit: cover eliminates 4:3 camera letterbox dead zone
- 7-second start timeout with actionable error message
- Starting/error overlays with high-contrast styling
- Try Again button with RefreshCw icon

Style guide updated: icon+text button rule (§8), btn/preset-filled workaround (§12)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 16:30:38 -04:00

13 KiB
Raw Blame History

Aether UI — Design System Style Guidelines

Version: 1.2 (2026-03-20) Author: One Sky IT / Scott Idem Scope: All Aether SvelteKit frontend components Related: AE__UI_Component_Patterns.md, ae-firefly.css, documentation/AE__Components.md


1. Design Philosophy

"Shiny serenity, like a firefly."

The Aether UI is calm, focused, and softly luminous. It must be immediately readable under conference-room lighting, at a glance, by presenters who are nervous and in a hurry. Staff in the Speaker Ready Room need scan-speed identity confirmation. Remote presenters uploading files from home need a clear, unambiguous interface that doesn't waste their time.

Core principles:

  • Identity first. The user's first question is always "Am I in the right place?" Answer it with the hero card — name, time, room — before anything else is shown.
  • Progressive disclosure. Admin fields (codes, IDs, passcodes) are hidden unless edit_mode is active.
  • Theme-aware always. Zero hardcoded colors. Every background, border, and text color must respond to light/dark mode and the active theme via CSS variables.
  • Transitions, not pops. Every interactive state change is smoothed with transition-colors duration-200.
  • Section 508 / WCAG 2.1 AA compliance is non-negotiable. Contrast ratios, focus indicators, ARIA labels, and screen-reader regions are required everywhere.

2. Technical Stack Mandates (2026 Standard)

To maintain codebase health and performance, all new development must adhere to the following stack:

🚀 Svelte 5 Runes

  • Mandatory: Use $state, $derived, and $effect.
  • Snippet pattern: Use {@render snippet()} for reusable UI blocks within components.
  • Avoid: Legacy export let (use $props()), onMount for reactive derived state (use $derived or $effect), and $$slots (use Snippets).

🎨 Tailwind 4 + Skeleton v4

  • Mandatory: Use preset-* classes for interactive elements (e.g., preset-tonal-primary).
  • Forbidden: Legacy Skeleton v3 variant-* classes.
  • Customization: Use Tailwind 4 @theme blocks for project-wide overrides.
  • URLs: Skeleton for Svelte for LLMs docs: https://www.skeleton.dev/llms-svelte.txt

🔣 Lucide Icons

  • Mandatory: Use @lucide/svelte components (e.g., <Calendar size="1em" />).
  • Migration: Replaced all FontAwesome fas fa-* icons in general modules.
  • 🚨 Exception: IDAA Module: The IDAA module must retain FontAwesome and Bootstrap classes. It integrates with Novi CMS which relies on these legacy standards. Do not migrate IDAA icons.

3. The AE_Firefly Theme

App default since 2026-03-06. Set in ae_stores.ts as theme_name = 'AE_Firefly'. File: src/ae-firefly.css | Activated by: data-theme="AE_Firefly"

Role Palette Name Hue Use Case
Primary Luminescent Teal ~184° Primary actions, date/time chips, focus rings, anchor links
Secondary Warm Amber-Gold ~90° Secondary actions, copy/email buttons, soft highlights
Tertiary Night-Sky Indigo ~277° Location/room chips, depth accents
Surface Moonlit Slate ~215233° All backgrounds — off-white (light) → midnight slate (dark)
Warning Amber semantic n/a Disabled/inactive records, "no results" states, code badges
Success Green semantic n/a Active/complete states, file count badges
Error Red semantic n/a Errors, disabled-presenter states

Contrast Guarantees

  • Body text (surface-950 on surface-50): > 15:1 in light mode ✓
  • Primary buttons: ≥ 3:1 for interactive component threshold ✓
  • Designed using OKLCH perceptual lightness — not HSL estimates

4. Color Token Rules

Always Use Theme Tokens

Backgrounds / surfaces:

bg-surface-50-900      ← card faces (light: near-white, dark: deep slate)
bg-surface-100-900     ← inner sections, code blocks, secondary panels
bg-surface-50-950      ← page-level containers
bg-surface-200-800     ← skeleton pulse placeholders, dividers

Borders:

border-surface-200-800  ← standard card/panel borders
border-primary-500      ← focus rings (via focus-visible:ring)
border-warning-500      ← warning/caution interactive zones
border-surface-500/20   ← subtle section dividers

Primary color accents (teal):

bg-primary-500/10       ← tinted chip background (time chips)
text-primary-700 dark:text-primary-300  ← chip text with auto dark mode
hover:text-primary-500  ← link hover color

Tertiary color accents (indigo):

bg-tertiary-500/10      ← tinted chip background (room/location chips)
text-tertiary-700 dark:text-tertiary-300

Skeleton presets:

preset-tonal-warning    ← "no results", disabled rows, code tag badges
preset-tonal-primary    ← primary action buttons
preset-tonal-surface    ← neutral/secondary actions, status badges
preset-tonal-success    ← success states, file count badges
preset-tonal-error      ← error/blocked states
preset-filled-*-500     ← solid-fill buttons (hover state for tonal)

Warning/error semantic backgrounds:

bg-warning-100 border border-warning-300  ← inline warning banners
bg-error-100 border border-error-300      ← inline error banners

Never Use These

Forbidden Reason Replace With
bg-gray-* Fixed color, breaks dark mode bg-surface-* tokens
bg-neutral-* Same — fixed hue bg-surface-* tokens
bg-white Light-mode only bg-surface-50-900
text-gray-* Breaks dark mode opacity-60 on inherited text
text-neutral-* Same opacity-*
border-gray-* Non-theme border border-surface-200-800
bg-yellow-*, text-yellow-* Bypasses warning semantic preset-tonal-warning
bg-red-*, text-red-* Bypasses error semantic bg-error-100 / preset-tonal-error
bg-white dark:bg-gray-800 Manual light/dark pair Let theme tokens handle it — remove entirely
text-gray-600 dark:text-gray-400 Manual light/dark pair opacity-60
rounded-container-token Skeleton v3 class rounded-xl
variant-* (v3) Skeleton v3 class preset-* (v4)
preset-filled-surface-300-700 v3 dual-shade notation bg-surface-200-800 or bg-surface-100-900
preset-filled-surface-400-600 v3 dual-shade notation bg-surface-100-900
overflow-x-scroll Forces scrollbar visible overflow-x-auto

5. Transitions & Animation

All interactive state changes must be smoothed — no hard pops.

Element Classes
Table row transition-colors duration-200 on <tr>
List item card transition-colors duration-200 on <li>
Link hover transition-colors duration-200 on <a>
Info chips transition-colors duration-200 on <span>
QR code toggle (size) transition-all duration-500 on <img>
Collapsible sections Use Skeleton Accordion or CSS transition-all — don't add custom unless needed
Buttons Handled automatically by Skeleton preset classes

6. Loading / Skeleton States

When liveQuery data is still resolving, show pulse placeholders instead of nothing:

<!-- Title placeholder -->
<div class="h-7 w-2/3 bg-surface-200-800 animate-pulse rounded"></div>

<!-- Chip/pill placeholder -->
<div class="h-5 w-1/2 bg-surface-200-800 animate-pulse rounded-full"></div>

<!-- Icon placeholder -->
<div class="h-5 w-5 bg-surface-200-800 animate-pulse rounded-full"></div>

Always wrap in {#if $lq__obj}{...}{:else}...skeleton...{/if}never show real content structure before data exists.


7. Dark Mode Rules

  • Never write dark: overrides for background or text colors. The Firefly theme handles both modes through CSS variables. Writing dark:bg-gray-800 or dark:text-gray-400 bypasses the theme and breaks if the user switches themes.
  • Exception allowed: dark:text-primary-300 and dark:text-tertiary-300 in info chips are intentional — they reference theme variables that gracefully degrade.
  • Exception allowed: dark:border-surface-700 in fine-grained border work when border-surface-200-800 isn't strong enough.

8. Accessibility (Section 508 / WCAG 2.1 AA)

Requirement Implementation
Decorative icons aria-hidden="true" on all icons
Icon-only buttons aria-label="..." or title="..." + visible context
Async content regions role="status" aria-live="polite" on loading/empty sections
Focus indicators focus-visible:ring-2 focus-visible:ring-primary-500 on custom interactive elements
Interactive dialogs aria-haspopup="dialog" on trigger buttons
Form inputs Visible <label> linked via for / id, or explicit aria-label
Color-only information Always pair color coding with icon or text — never color alone
Minimum touch target 44×44px effective hit area for all tap targets
Button label + icon All buttons should include both a Lucide icon and text label. Icon-only is acceptable for space-constrained toolbar/header actions (with title attribute); text-only is acceptable when layout is extremely tight. The icon+text combination aids non-English-native users who may not read the label fluently.

9. Debug Code — Remove Before Committing

These patterns are breakpoint debuggers added during development. Never commit them:

<!-- Breakpoint border debugger — REMOVE before commit -->
sm:border-l-red-400 md:border-l-yellow-400 lg:border-l-gray-100
sm:dark:border-l-red-600 md:dark:border-l-yellow-600 lg:dark:border-l-gray-700

Also flag on code review:

  • console.log(...) that isn't behind a log_lvl guard
  • border-dashed border-y-transparent border-r-transparent left on production components
  • overflow-x-scroll (should be overflow-x-auto)

10. QR Code Pattern — Critical Bug Prevention

The async QR generation code uses a boolean true as a loading placeholder:

$events_sess.pres_mgmt.session_qr_url[$lq__obj.id] = true;   // ← loading
// ... async ...
$events_sess.pres_mgmt.session_qr_url[$lq__obj.id] = result; // ← URL string

Always gate display on typeof ... === 'string', not just truthy:

<!-- ✅ Correct -->
{#if typeof $events_sess.pres_mgmt.session_qr_url?.[$lq__obj.id] === 'string'}

<!-- ❌ Wrong — renders broken <img src="true"> during load -->
{#if $events_sess.pres_mgmt.session_qr_url[$lq__obj.id]}

11. Tailwind Utility Usage Notes

  • opacity-* for muted text: Use opacity-60 (secondary) or opacity-40 (tertiary/hint) instead of text-gray-*. This works in any theme and both modes.
  • / opacity modifier: bg-primary-500/10 is preferred over separate opacity-10 — it targets only the background.
  • text-sm leading-relaxed: Standard for body-level descriptive text in cards.
  • tracking-wide uppercase: Use for section label/eyebrow text with opacity-40.
  • whitespace-pre-wrap: Required for any <pre> or <p> displaying user-entered multi-line text (preserves breaks without horizontal overflow).

12. Known Issues & Workarounds

btn + preset-filled-* resolves to transparent inside card components

Symptom: A button using btn preset-filled-primary (or any preset-filled-*) inside a card div renders with background-color: transparent, making it invisible against the card surface.

Root cause: The Skeleton v4 btn class sets a transparent background via a CSS variable chain. When nested inside a card element, the preset-filled-* class fails to win the specificity battle and the button appears invisible. This affects both light and dark mode.

Workaround: Skip btn and preset-filled-* entirely for buttons inside card elements. Use direct Tailwind token classes instead:

<!-- ✅ Correct — works reliably inside cards -->
<button class="w-full rounded-xl py-5 font-bold flex items-center justify-center gap-2
               bg-primary-500 text-white hover:brightness-110 transition-all cursor-pointer">
    ...
</button>

<!-- Secondary / cancel button inside a card -->
<button class="w-full rounded-lg py-3 text-sm font-medium flex items-center justify-center gap-2
               border border-surface-500/40 hover:bg-surface-200-800 transition-colors cursor-pointer opacity-70">
    ...
</button>

<!-- ❌ Broken inside card — do not use -->
<button class="btn btn-xl preset-filled-primary">...</button>

Scope: btn + preset-* classes work correctly on standalone buttons (e.g. page headers, nav bars). The issue is specific to the card component context. If we migrate away from Skeleton card/btn, this issue goes away.