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>
13 KiB
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_modeis 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()),onMountfor reactive derived state (use$derivedor$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
@themeblocks for project-wide overrides. - URLs: Skeleton for Svelte for LLMs docs: https://www.skeleton.dev/llms-svelte.txt
🔣 Lucide Icons
- Mandatory: Use
@lucide/sveltecomponents (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 | ~215–233° | 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-950onsurface-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. Writingdark:bg-gray-800ordark:text-gray-400bypasses the theme and breaks if the user switches themes. - Exception allowed:
dark:text-primary-300anddark:text-tertiary-300in info chips are intentional — they reference theme variables that gracefully degrade. - Exception allowed:
dark:border-surface-700in fine-grained border work whenborder-surface-200-800isn'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 alog_lvlguardborder-dashed border-y-transparent border-r-transparentleft on production componentsoverflow-x-scroll(should beoverflow-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: Useopacity-60(secondary) oropacity-40(tertiary/hint) instead oftext-gray-*. This works in any theme and both modes./opacity modifier:bg-primary-500/10is preferred over separateopacity-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 withopacity-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.