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

415 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.*
```svelte
<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:**
```svelte
<!-- 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.*
```svelte
<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:**
```svelte
<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.*
```svelte
<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.*
```svelte
<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)
```svelte
<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)
```svelte
<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
```svelte
<span class="text-xs preset-tonal-warning px-2 py-0.5 rounded-md leading-none">
{code}
</span>
```
### Status badge (edit mode only)
```svelte
{#if $ae_loc.edit_mode}
<span class="badge preset-tonal-surface text-xs">code: {obj.code}</span>
{/if}
```
### Success count badge
```svelte
<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".*
```svelte
<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.*
```svelte
<!-- 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:
```svelte
<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.*
```svelte
<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
```
---
## 10. Agreement / Consent Form Layout
*Used for: `ae_comp__event_presenter_form_agree.svelte`, `ae_comp__event_session_poc_form_agree.svelte`.*
```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)
```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:
```svelte
<!-- ✅ 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)
```svelte
<!-- 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
```svelte
<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:**
```svelte
<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`.