Files
OSIT-AE-App-Svelte/documentation/AE__UI_Component_Patterns.md
2026-03-06 22:00:12 -05:00

14 KiB
Raw Blame History

Aether UI — Component Style Patterns

Version: 1.0 (2026-03-06) Author: One Sky IT / Scott Idem Scope: All Aether SvelteKit frontend components Related: GUIDE__AE_UI_Style_Guidelines.md (color rules, token definitions, a11y)

This document is a recipe book. Copy these patterns directly. Deviate only when a component's specific purpose genuinely requires it — and document why in a comment.


1. Hero Card

Used for: session identity, presenter identity, location identity — the top-of-page "Is this the right one?" card.

<div class="rounded-xl border border-surface-200-800 bg-surface-50-900 shadow-sm overflow-hidden">
    <div class="px-4 pt-4 pb-3 flex flex-col gap-3">
        <!-- primary heading (h1 / h2) -->
        <h1 class="text-2xl font-bold leading-snug">{name}</h1>

        <!-- info chips row -->
        <div class="flex flex-wrap gap-2 items-center">
            <!-- time chip → primary color -->
            <span class="inline-flex items-center gap-1.5 text-sm font-semibold px-3 py-1 rounded-full bg-primary-500/10 text-primary-700 dark:text-primary-300 transition-colors duration-200">
                <span class="fas fa-clock text-xs" aria-hidden="true"></span>
                Mon, Jan 1  2:00 PM
            </span>
            <!-- room/location chip → tertiary color -->
            <span class="inline-flex items-center gap-1.5 text-sm font-semibold px-3 py-1 rounded-full bg-tertiary-500/10 text-tertiary-700 dark:text-tertiary-300 transition-colors duration-200">
                <span class="fas fa-map-marker-alt text-xs" aria-hidden="true"></span>
                Room 201
            </span>
        </div>
    </div>
</div>

Skeleton loading variant:

<!-- While liveQuery resolves -->
<div class="h-7 w-2/3 bg-surface-200-800 animate-pulse rounded"></div>
<div class="h-5 w-1/2 bg-surface-200-800 animate-pulse rounded-full"></div>

2. Standard Content Card

Used for: description text, notes, secondary info panels.

<div class="rounded-lg border border-surface-200-800 bg-surface-50-900 px-4 py-3">
    <!-- optional eyebrow label -->
    <span class="text-xs font-bold uppercase tracking-wide opacity-40 block mb-1">Description</span>
    <p class="whitespace-pre-wrap text-sm leading-relaxed">{description}</p>
</div>

Variant — inner secondary panel:

<div class="bg-surface-100-900 rounded-lg px-3 py-2">
    <!-- inner content -->
</div>

3. Table Row

Used for: session search results tables, any <tbody><tr> list.

<tr
    class="relative transition-colors duration-200"
    class:opacity-50={obj?.hide}
    class:preset-tonal-warning={!obj?.enable}
>
    <td>
        <a
            href="/path/to/{obj.id}"
            class="font-bold text-lg hover:text-primary-500 transition-colors duration-200"
        >
            {obj.name}
        </a>
    </td>
</tr>
  • opacity-50 for hidden/archived records
  • preset-tonal-warning for disabled (not enabled) records — amber background
  • transition-colors duration-200 on both <tr> and <a>

4. List Item Card

Used for: presentation list items, session details lists, any vertical card stack.

<ul class="space-y-4">
    <li class="space-y-3 border border-surface-200-800 bg-surface-50-900 p-4 rounded-xl shadow-sm transition-colors duration-200">

        <!-- Card heading bar -->
        <h4 class="text-lg font-bold rounded-lg px-3 py-2 bg-surface-100-900 flex flex-wrap items-center gap-2">
            {name}
            <!-- code/tag badge -->
            <span class="text-xs preset-tonal-warning px-2 py-0.5 rounded-md leading-none">
                {code}
            </span>
        </h4>

        <!-- Description block -->
        <pre class="whitespace-pre-wrap p-3 bg-surface-100-900 rounded-lg text-sm">{description}</pre>

    </li>
</ul>

Rules:

  • Background goes on <li>, NOT on <ul>
  • <ul> gets only spacing: space-y-4 — never a background color
  • The <ul> container in components should have overflow-x-auto, not overflow-x-scroll

5. Info Chips

Time / Date chip (Primary — Teal)

<span class="inline-flex items-center gap-1.5 text-sm font-semibold px-3 py-1 rounded-full bg-primary-500/10 text-primary-700 dark:text-primary-300 transition-colors duration-200">
    <span class="fas fa-clock text-xs" aria-hidden="true"></span>
    Monday, March 6  2:00 PM
</span>

Location / Room chip (Tertiary — Indigo)

<span class="inline-flex items-center gap-1.5 text-sm font-semibold px-3 py-1 rounded-full bg-tertiary-500/10 text-tertiary-700 dark:text-tertiary-300 transition-colors duration-200">
    <span class="fas fa-map-marker-alt text-xs" aria-hidden="true"></span>
    Main Hall B
</span>

Code / Tag badge

<span class="text-xs preset-tonal-warning px-2 py-0.5 rounded-md leading-none">
    {code}
</span>

Status badge (edit mode only)

{#if $ae_loc.edit_mode}
    <span class="badge preset-tonal-surface text-xs">code: {obj.code}</span>
{/if}

Success count badge

<span class="badge preset-tonal-success" class:hidden={!fileCount}>
    <span class="fas fa-file-alt m-1" aria-hidden="true"></span>
    {fileCount}×
</span>

6. Empty State Panel

Used for: "No results found", "No sessions match your search", "Nothing to show yet".

<section
    class="preset-tonal-warning p-6 rounded-xl shadow-sm lg:max-w-lg mx-auto"
    role="status"
    aria-live="polite"
>
    <div class="flex flex-col items-center gap-2 text-center">
        <span class="fas fa-search text-3xl opacity-50" aria-hidden="true"></span>
        <strong class="text-xl">No sessions found</strong>
        <p class="text-base opacity-80">
            Use the search bar above to find your session.
        </p>
    </div>

    <!-- optional details card -->
    <div class="bg-surface-50-900/60 rounded-lg p-3 mt-4">
        <span class="text-xs font-bold uppercase tracking-wide opacity-50 block mb-2">Search by any of:</span>
        <ul class="space-y-1 text-sm">
            <li class="flex items-center gap-1.5">
                <span class="fas fa-angle-right text-xs opacity-50" aria-hidden="true"></span>
                Session name
            </li>
        </ul>
    </div>
</section>

7. Warning / Error Inline Banners

Used for: disabled records, agreement-required gates.

<!-- Warning (amber — disabled/inactive) -->
<div class="bg-warning-100 p-4 border border-warning-300 rounded-md">
    <h2 class="h3">
        <span class="fas fa-exclamation-triangle text-warning-500 m-1" aria-hidden="true"></span>
        Location Disabled
    </h2>
    <p>This location is currently disabled.</p>
</div>

<!-- Error (red — blocked/failed) -->
<div class="bg-error-100 p-4 border border-error-300 rounded-md">
    <h2 class="h3">
        <span class="fas fa-exclamation-triangle text-error-500 m-1" aria-hidden="true"></span>
        Presenter Disabled
    </h2>
    <p>This presenter is currently disabled.</p>
</div>

8. File Upload Zone (Comp_event_files_upload)

The class_li prop styles the outer upload drop zone container:

<Comp_event_files_upload
    class_li="border border-surface-200-800 rounded-xl p-4 bg-surface-50-900 hover:bg-surface-100-900 transition-colors duration-200"
    link_to_type="event_presenter"
    link_to_id={presenter_id}
>
    {#snippet label()}
        <span>
            <div class="text-lg">
                <span class="fas fa-upload" aria-hidden="true"></span>
                <strong>Upload presenter files</strong>
            </div>
            <div class="text-sm opacity-60 italic">
                Supported: pptx, key, mp4, pdf, docx, xlsx, txt
            </div>
        </span>
    {/snippet}
</Comp_event_files_upload>

Note: The label sub-description text uses opacity-60 italic — never text-gray-600 dark:text-gray-400.


9. Section Component Wrapper

Used for: ae_comp__event_*_obj_li.svelte outer <section> elements.

<section
    class="ae_comp event_X_obj_li px-0.5 py-2 space-y-2 min-w-full w-full container overflow-x-auto {container_class_li}"
>

Rules:

  • overflow-x-auto — never overflow-x-scroll
  • Never include debug breakpoint borders — remove before committing:
    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
    border-dashed border-y-transparent border-r-transparent
    

Used for: ae_comp__event_presenter_form_agree.svelte, ae_comp__event_session_poc_form_agree.svelte.

<!-- Consent text container -->
<div class="bg-surface-100-900 p-4 border border-surface-200-800 rounded-lg space-y-4">
    <Element_data_store ds_code="consent_text" ds_type="html" class_li="p-2" />
</div>

<!-- Presenter name/identity highlight line -->
<p class="text-lg preset-tonal-warning p-2 rounded-t-md">
    <strong>{presenter_name} ({email})</strong>
    agrees to the following terms and conditions:
</p>

11. Modal Usage (Flowbite-Svelte)

<!-- ✅ Correct — no manual color class; theme handles styling -->
<Modal title="Host Profile" bind:open={show_modal}>
    <ProfileComponent />
    {#snippet footer()}
        <button onclick={() => show_modal = false} class="btn preset-tonal-warning">
            Close
        </button>
    {/snippet}
</Modal>

<!-- ❌ Wrong — manual gray overrides bypass the theme -->
<Modal class="bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 ...">

Rule: Never set bg-* or text-* color classes on <Modal>. Let the Flowbite component + active theme handle it. Only structural layout classes (shadow-md, relative, flex, etc.) belong on the class prop if needed.


12. Muted / Secondary Text

Replace all text-gray-* patterns with opacity wrappers on inherited text color:

<!-- ✅ Theme-aware muted text -->
<span class="text-sm opacity-60">Secondary label</span>
<span class="text-sm opacity-40 italic">Hint or placeholder text</span>
<span class="text-xs font-bold uppercase tracking-wide opacity-40">Section eyebrow</span>

<!-- ❌ Fixed-color muted text -->
<span class="text-sm text-gray-500">Secondary label</span>
<span class="text-sm text-gray-600 dark:text-gray-400 italic">Hint text</span>

13. QR Code (Async Toggle)

<!-- Gate on typeof === 'string', not truthy.
     The store holds boolean `true` as a loading placeholder, which would
     render as a broken <img src="true"> if not guarded. -->
{#if $lq__obj && typeof $store.qr_url?.[$lq__obj.id] === 'string'}
    <div class="float-right ml-3 mb-1 flex flex-col items-center gap-1">
        <button
            type="button"
            onclick={() => $store.qr_bigger = !$store.qr_bigger}
            class="rounded focus-visible:ring-2 focus-visible:ring-primary-500"
            title="Toggle QR code size"
            aria-label="Toggle QR code size"
        >
            <img
                src={$store.qr_url[$lq__obj.id]}
                class="transition-all duration-500 rounded border border-surface-200-800"
                class:h-20={!$store.qr_bigger}
                class:w-20={!$store.qr_bigger}
                class:h-40={$store.qr_bigger}
                class:w-40={$store.qr_bigger}
                alt="QR code link to this page"
            />
        </button>
    </div>
{/if}

14. POC / Host Button Pattern

<div class="flex items-center gap-2">
    <span class="text-sm font-semibold opacity-60">Host:</span>
    <button
        type="button"
        class="btn btn-sm preset-tonal-primary transition-colors duration-200"
        onclick={() => show_profile = true}
        aria-haspopup="dialog"
    >
        <span class="fas fa-id-card mr-1" aria-hidden="true"></span>
        {full_name}
    </button>
</div>

15. Icon Usage Rules

Context Pattern
Decorative / visual only <span class="fas fa-clock" aria-hidden="true"></span>
Icon with visible adjacent text aria-hidden="true" on icon, text provides meaning
Icon-only button (no visible text) aria-label="Description" on the <button>
Icon used as bullet point aria-hidden="true" on icon

Never use <i> tags. Always <span class="fas ...">.


16. Native <select> Dark Mode

Browser-native <select> and <option> elements cannot be reliably styled with Tailwind dark: utilities — the browser controls <option> rendering and ignores most CSS overrides. This causes the "light on light hover" bug in dark mode.

Fix — add color-scheme directive to force OS-level dark styling:

<script>
    import { ae_loc } from '$lib/ae_core/ae_stores';
</script>

<!-- Forces browser to render the select widget in dark/light OS mode matching your theme -->
<select
    class="select text-xs p-1"
    style:color-scheme={$ae_loc.dark_mode ? 'dark' : 'light'}
>
    {#each options as opt}
        <option value={opt.value}>{opt.label}</option>
    {/each}
</select>

Why this works: color-scheme: dark instructs the browser to use its native dark-mode widget rendering (dark <select>, dark <option> backgrounds). It's the only cross-browser mechanism that affects <option> hover colors.

Alternative — replace with custom Skeleton/Flowbite component if you need full styling control (e.g., color-coded options, icons). Native <select> is acceptable for simple purpose dropdowns with the color-scheme fix above.

Store reference: $ae_loc.dark_mode — boolean, set by the theme engine in ae_stores.ts.