# Aether UI/UX — Future Ideas

> Collection of concrete UX improvements for the Aether frontend. Each entry includes
> the rationale, current behavior, proposed change, and implementation notes.
> **Date:** 2026-05-17

---

## IDAA Recovery Meetings

### 1. Guided empty state with active filters — ✅ Implemented 2026-05-18

**Current behavior:** When filters return 0 results, the page shows:
"No recovery meetings found matching your criteria."
The member has no indication whether this is a bug, genuinely no data, or just
overly narrow filters.

**Proposed change:** When filters are active AND the result count is 0, show a
helpful prompt instead of the bare message:

```
No meetings found for these filters.
Try broadening your search or [Clear all filters →]
```

**Implementation notes:**
- Add a `has_active_filters` derived in `+page.svelte` that checks whether any of
  `qry__physical`, `qry__virtual`, `qry__type`, or `qry__fulltext_str` is set.
- In the template's `{:else}` block (line ~443), branch on `has_active_filters`:
  - `true`  → show the guided message + "Clear Filters" button
  - `false` → show the existing escape-hatch flow (timed "Refresh Meeting Cache" button
    after 8 seconds, since zero unfiltered results always indicates a problem)
- The "Clear Filters" button resets all four filter fields to `null`/`''` and bumps
  `search_version` to trigger a fresh unfiltered search.
- Distinct from the `error` state — this is a successful search (`qry__status === 'done'`)
  with an empty result set.

**Implemented:** `src/routes/idaa/(idaa)/recovery_meetings/+page.svelte`. `has_active_filters`
derived checks `qry__physical`, `qry__virtual`, `qry__type`, and `qry__fulltext_str`. Empty
state branches on `has_active_filters`: active filters → guided message + "Clear Filters"
button; no active filters → existing escape-hatch flow (timed "Refresh Meeting Cache" after
8 seconds).

---

### 2. Quick-filter chips below the search bar — ✅ Implemented 2026-05-18

**Current behavior:** Members toggle filters via small checkboxes (Virtual, In-person)
and radio buttons (All, IDAA, Caduceus, Family Recovery). These require precise
mouse/tap targeting and scanning several lines of filter UI to discover and use.

**Proposed change:** Add a row of preset chip buttons directly below the search input:

```
[🖥 Virtual] [🏠 In-Person] [🩺 IDAA] [Caduceus] [Family Recovery] [All Types]
```

- Each chip toggles the corresponding filter (`qry__virtual`, `qry__physical`, `qry__type`)
  and triggers an immediate search.
- Selected chips get a filled/pressed style; unselected chips are outlined.
- "All Types" is the default selected state (no type filter). Clicking another type
  chip deselects "All Types" (radio behavior for the type dimension). Virtual and
  In-person are independent toggles (checkbox behavior — can select both).
- The existing checkboxes/radio buttons remain as the underlying state storage
  (`$idaa_loc.recovery_meetings.*`). The chips are a convenience layer — they write
  to the same store fields and call `handle_search_trigger()`.

**Implementation notes:**
- Place in `ae_idaa_comp__event_obj_qry.svelte` between the search input row and the
  current filter rows.
- Optionally hide the existing checkbox/radio filter rows when the chips are present
  (or keep both — the checkboxes serve as accessible form controls; the chips are
  the primary visual interaction).
- On mobile, chips wrap to a second row naturally with `flex-wrap`.

**Implemented:** `ae_idaa_comp__event_obj_qry.svelte`. Chips replaced the old
checkbox/radio/select UI entirely rather than layering on top. Two chip rows:
Row 1 — My Meetings (first), Virtual, In-Person. Row 2 — All / IDAA / Caduceus /
Family Recovery type chips. Cycling sort button replaces separate sort options
(see item below). Max Results uses a +/− stepper. Sort and max are in a third
row below the chips, inside the same `<form>` constraint.

---

### 3. Language: "Searching..." vs "Loading..."

**Current behavior:** The loading state always shows the same message:

```
🔄 Searching...
```

This appears on initial page load (when the user hasn't typed anything) and after
the user clicks Search or toggles a filter. The word "Searching" implies the user
initiated a search, which is misleading on initial page load — it's a cold cache
load, not an active search.

**Proposed change:** Distinguish the two loading contexts:

| Context | Message |
|---------|---------|
| Initial page load (no filters, no search text) | "Loading meetings..." |
| User clicked Search or toggled a filter | "Searching..." (keep current) |

**Implementation notes:**
- In `+page.svelte` template around line 422, check whether `qry__fulltext_str` is
  empty AND no filter checkboxes/radios are active. If so, show "Loading meetings...";
  otherwise show "Searching...".
- This is purely a label change — no logic changes needed. The condition can be the
  same `has_active_filters` derived from item #1.
- Also update the list component's standalone loading state in
  `ae_idaa_comp__event_obj_li.svelte` line 556-558 to use the same distinction.

---

### 4. Filter row collapsing on mobile

**Current behavior:** The query bar has three filter rows (Location checkboxes,
Type radios, Max/Sort selects) plus the search input row and the action button row.
Combined, this takes roughly 200px of vertical space. On mobile — especially inside
the Novi iframe on a phone — meeting cards are pushed below the fold.

**Proposed change:** On viewports below `md` (768px), collapse the Location and Type
filter rows behind a "Filters ▾" toggle. The Max Results and Sort selects stay visible
since they're used frequently. The action buttons (Show Hidden, Create Meeting, Export)
move inside the collapsed panel or stay visible based on available width.

```
[Search input............................] [Search]

[Filters ▾] [Max: 150 ▾] [Sort: Last Updated ▾]
```

Clicking "Filters ▾" expands the panel with Location checkboxes and Type radios.

**Implementation notes:**
- Use a `$state` boolean `show_filters` (session-only, resets on page load).
- Wrap the filter rows in a `{#if show_filters}` block.
- Persist in `$idaa_sess.recovery_meetings.show_filters_expanded` if you want the
  state to survive navigation within the module (same tab session).
- The Tailwind `md:` breakpoint works for the collapse trigger: `class:hidden={!show_filters}`
  combined with `class:md:block` to always show on desktop.
- Test inside the Novi iframe — Bootstrap v3 may add its own `hidden` behavior on
  `md` breakpoints that conflicts with Tailwind's.

---

### 5. Human-readable schedule line on cards

**Current behavior:** The meeting card displays weekdays as a flat, dense span list:

```
Sunday Monday Wednesday Friday
```

The timezone is shown separately as `(America/Chicago)`, and the start time is in
a compact `7:00 PM` format. These three pieces of information are visually separated
and require the member to mentally assemble the schedule.

**Proposed change:** Render a computed one-liner that combines them:

```
🕐 Mondays, Wednesdays, Fridays at 7:00 PM CT
```

- Weekday names are built from the `weekday_*` booleans on the event object.
- "Mondays, Wednesdays" uses the range-joining convention (comma-separated, "and"
  before the last item for two days; "Mondays through Fridays" for consecutive spans
  of 3+ days).
- Timezone abbreviation is extracted from `timezone` (e.g., `America/Chicago` → `CT`,
  `America/New_York` → `ET`). A small lookup table handles the common ones; fall back
  to the raw timezone string for unknown values.
- If `timezone` is null/missing, fall back to the current flat display — don't
  silently drop information.
- Today's meetings could optionally get a subtle "Today" badge or highlight (extra
  polish, not required for the initial version).

**Implementation notes:**
- Add a `$derived` in `ae_idaa_comp__event_obj_li.svelte` that computes the schedule
  string from the event object's `weekday_*` fields, `recurring_start_time`, and
  `timezone`.
- Helper function in `ae_util` for the weekday list → natural language string
  (e.g., `['Monday', 'Wednesday', 'Friday']` → `"Mondays, Wednesdays, and Fridays"`).
- Helper function or small lookup for timezone → abbreviation.
- Fall back to the current flat display when `timezone` is missing to avoid losing
  information.

---

### 6. Show result count during search, not just after

**Current behavior:** The result count badge ("Results: 25") only appears inside the
list wrapper component (`ae_idaa_comp__event_obj_li.svelte` line 98-108) when the
visible result list is non-empty. During loading, the user sees only a spinner with
no indication of how many meetings exist or what the search is operating on.

**Proposed change:** Show a result count line at the page level (in `+page.svelte`)
that is always visible once the first search completes:

```
25 of 140 meetings           ← after search completes, with result count + total
Searching 140 meetings...    ← during initial load (cold cache, no prior result)
0 results for these filters  ← empty but filters are active (ties into item #1)
```

**Implementation notes:**
- Lift the count display from the list component to `+page.svelte`, placed between
  the query bar (`Comp__event_obj_qry`) and the list wrapper.
- The total count is available from the IDB fast path: after the initial unfiltered
  search populates `db_events.event`, the total is `db_events.event.count()` (or
  the count of records matching `account_id`).
- The visible count is `event_id_li.length` after search completes.
- Store the last known total in a `$state` variable so it persists across searches
  (the total changes infrequently). Refresh the total on the first search after
  page load.
- Format: `{visible} of {total} meetings` when filters/search are active;
  `{visible} meetings` when browsing all (no active filters).
- During loading with no prior results: show "Loading meetings..." (from item #3)
  rather than a count.

---

### 7. "Live Now" and "Starting Soon" indicators

**Current behavior:** Meetings are shown in a static list. To find one happening
now, a member must scan the "When" line of multiple cards and compare the time
to their own clock.

**Proposed change:** Add a high-visibility badge or pulse indicator for meetings
that are currently in progress or starting in the next 15 minutes.

- "LIVE NOW" (Green pulse badge) → if `current_time` is within `[start, start + 1 hour]`.
- "STARTING SOON" (Yellow badge) → if `current_time` is within `[start - 15 min, start]`.
- On the card, move the "Join Zoom" or "Join Jitsi" button to the very top or
  make it significantly larger when the meeting is live.

**Implementation notes:**
- Add a `$derived` state `is_live` and `is_starting_soon` to the card component.
- Requires calculating "current time in meeting's timezone" using `Temporal` or
  a date helper.
- Ensure the pulse animation is subtle and respects `prefers-reduced-motion`.

---

### 8. Local Timezone Conversion

**Current behavior:** Meetings show their native timezone (e.g., "7:00 PM America/Chicago").
The "Your TZ" line is currently a placeholder and doesn't perform conversion.

**Proposed change:** Automatically detect the member's browser timezone and
show the converted time if it differs from the meeting's native timezone.

```
🕐 7:00 PM CT (8:00 PM ET your time)
```

**Implementation notes:**
- Use `Intl.DateTimeFormat().resolvedOptions().timeZone` to get the user's TZ.
- If `user_tz !== meeting_tz`, perform the conversion.
- If the conversion results in a different day (e.g., late night ET vs early morning Europe),
  prefix with "Tomorrow at..." or "Yesterday at...".

---

### 9. Favorites / "My Meetings" — ✅ Implemented 2026-05-18

**Current behavior:** Members scan the full list every time they want to find
their regular weekly meeting.

**Proposed change:** Add a "Star" icon to every meeting card.
- Starring a meeting adds it to a `favorites` list stored in `$idaa_loc`.
- Favorited meetings are pinned to the top of the list by default, regardless
  of other sort orders.
- Add a "Favorites" filter toggle in the query bar to show *only* starred meetings.

**Implementation notes:**
- Store as an array of `event_id` strings in `$idaa_loc.recovery_meetings.favorites`.
- Update the `visible_event_obj_li` derived in `+page.svelte` to prioritize
  these IDs in the sort logic.

**Implemented:** Star toggle on the `[event_id]` detail page
(`src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.svelte`).
"My Meetings" filter chip is first in the filter chip row on the list page.
**Implementation differs from proposal:** favorites stored server-side in a
`data_store` record (code: `idaa_meetings_favorites`) as a UUID-keyed JSON map
rather than in `$idaa_loc` — this means favorites persist across browsers and
devices without Novi write capability. Pinning favorites to the top of the list
was not implemented; the filter chip shows only favorites instead.

---

### 10. "Add to Calendar" (iCal / Google)

**Current behavior:** Members must manually create calendar events if they
want reminders for recurring meetings.

**Proposed change:** Add an "Add to Calendar" dropdown button on the meeting
detail page (and optionally the card).
- Generates a `.ics` file or a Google Calendar URL with the recurring rule
  (e.g., "Every Wednesday at 7pm").
- Includes the meeting name, description, and the Zoom/Jitsi link in the location field.

**Implementation notes:**
- Use a helper to generate RFC 5545 `RRULE` strings from the `weekday_*` and
  `recurring_pattern` fields.
- Include the `attend_url` in the calendar event description for one-tap join
  from phone lock screens.

---

### 11. Geographic Search for In-Person Meetings

**Current behavior:** The only location filter is a binary "Physical" checkbox.
Members must use fulltext search (e.g., "Chicago") to find local meetings.

**Proposed change:** Add a "City/State" search input or a map view.
- When `Physical` is checked, show a "Near [City, State]" input.
- Map view (optional): A toggle to switch from "List" to "Map" view, plotting
  meetings on a map using their `location_address_json` coordinates.

**Implementation notes:**
- The `event` table has `location_address_json` which often contains city/state.
- Simple implementation: a city-picker dropdown populated from the distinct
  `location_address_json->>'$.city'` values in the current result set.

---

### 12. Prominent "Join" button for virtual meetings

**Current behavior:** On each meeting card, the Zoom or Jitsi join link is rendered
as a small `btn-sm` inside the content area, visually equivalent to other label/value
rows. The "Meeting Details" button at the top of the card is rendered *larger* than the
join link — meaning the primary action for a member who wants to attend a meeting right
now is visually subordinate to a navigation link.

The copy-to-clipboard button for the join link is gated behind `$ae_loc.manager_access`,
so regular members have no easy way to share the link with a sponsee.

**Proposed change:** For virtual meetings, elevate the join button to a full-width
prominent CTA inside the card header area, directly below the meeting name and badges:

```
┌──────────────────────────────────────────────────┐
│ 📅 Monday Night IDAA Discussion     🖥 Virtual    │
│                                                   │
│  [ 🎥  Join Zoom Meeting          ]  ← full-width │
│  [ 📋  Meeting Details            ]               │
└──────────────────────────────────────────────────┘
```

- The Join button uses `preset-filled` (solid) styling; Meeting Details uses
  `preset-outlined` (hollow). This makes the action hierarchy visually clear.
- On mobile especially, a full-width join button is much easier to tap than a
  small inline link buried inside label rows.
- Replace manager-only clipboard with a Web Share API button for all members on
  virtual meetings: `navigator.share({ title, url })` on mobile triggers the native
  OS share sheet. Fall back to clipboard copy on desktop (where `navigator.share`
  is often unavailable). This lets members easily send a meeting link to a sponsee.
- Passcode, if present, moves to the Meeting Details page — exposing it in the
  list view is unnecessary and clutters the card.

**Implementation notes:**
- In `ae_idaa_comp__event_obj_li.svelte`, move the Zoom/Jitsi attend block from
  the `event__content` section up into the `ae_options` div (line ~200), rendered
  only when `idaa_event_obj?.virtual` is true and an attend URL exists.
- Keep the existing small label/link in `event__content` as a fallback for when
  the prominent button is not shown (non-virtual meetings may still have a URL).
- Web Share: `{#if navigator?.share}` guard; wrap in a try/catch (user cancels
  the share sheet throws `AbortError`).
- The Live Now / Starting Soon badges from item #7, when implemented, should also
  interact with this button — e.g., a pulsing green border when the meeting is live.

---

### 13. "Today's Meetings" section at the top of the list

**Current behavior:** The meeting list shows all results sorted by the selected sort
order. To find a meeting happening today, a member must scan every card's "When" line
and mentally compare it to the current day and time. There is no at-a-glance view
of what's available right now or later today.

This is distinct from the "Live Now" badge in item #7 (which marks individual cards
after they're already displayed in a long list). This is a dedicated section pinned
above the main results.

**Proposed change:** Add a collapsible "Today" section at the very top of the results
list that shows only meetings scheduled on the current day of the week, sorted by
start time:

```
▼ Today — Sunday, May 17                       3 meetings
  ┌─────────────────────────────────────────────────┐
  │ Sunday Serenity Discussion    7:00 AM ET  🔴Live │
  │ IDAA Sunday Big Book          2:00 PM CT         │
  │ Sunday Night IDAA             8:00 PM ET         │
  └─────────────────────────────────────────────────┘

All Meetings (140)
  ...
```

- The section is collapsed by default if it's empty (no meetings today).
- Meetings in the "Today" section also appear in the main list below — this is a
  quick-access shortcut, not a filter.
- Past meetings (start time has already passed today) are dimmed but still shown;
  a meeting may still be in progress.

**Implementation notes:**
- Compute current day-of-week in the browser: `new Date().getDay()` → 0=Sunday, 6=Saturday.
  Map to the `weekday_*` boolean fields on the event object (e.g., day 0 → `weekday_sunday`).
- Filter `visible_event_obj_li` (already computed in the list wrapper) for items where
  the matching `weekday_*` field is truthy. Sort by `recurring_start_time`.
- Store collapse state in `$idaa_sess.recovery_meetings.today_section_expanded` (session
  only; default true so it's visible on first load).
- Renders correctly in the Novi iframe since it's just a filtered sub-list of existing
  data — no additional API calls needed.
- If item #7 (Live Now) is implemented, the "Today" section naturally becomes the host
  for the live/starting-soon badges, since that's where members will look first.

---

### 14. Data freshness indicator *(low priority — deprioritized)*

**Note:** Meeting records change infrequently — once established, a meeting's schedule,
type, and contact info are typically stable for months or years. The occasional update is
usually minor wording. Surfacing a freshness indicator for data this static would add
visual noise with very little member benefit. The existing error state (item #4 in the
bug fix, distinct "Unable to load meetings") and the escape-hatch cache-reset button
already handle the reliability-concern case. This idea is recorded for completeness but
is not recommended for implementation.

---

### 15. "Confirmed meetings only" default filter

**Current behavior:** All meetings are shown by default, including those with
`status === 'unknown'` (not yet confirmed by IDAA Central Office). The "Not Confirmed
by IDAA" warning badge on those cards is alarming-looking but does nothing to prevent
unverified meetings from dominating the list.

There is currently no filter to hide unconfirmed meetings. Members have no choice but
to see them all.

**Business rationale:** IDAA staff want meeting chairs to submit their meeting info for
verification. Defaulting to "confirmed only" creates a natural incentive: unconfirmed
meetings disappear from the default member view, which encourages chairs to contact
IDAA staff and get their meeting verified. It also gives members a cleaner, higher-
confidence list by default — they're not seeing meetings that may be outdated or
inactive.

**Proposed change:** Add a `qry__confirmed` filter field with three states:

| Value | Behavior |
|-------|----------|
| `'confirmed_only'` (default) | Hide meetings where `status === 'unknown'` |
| `'all'` | Show all meetings, confirmed and unconfirmed |
| `'unconfirmed_only'` | Show only unconfirmed (admin/staff use) |

The filter UI shows a simple toggle in the query bar:
```
[✓ Confirmed Only]   ← default, shown as active chip or checkbox
```

When the member switches to "All", the unconfirmed meetings appear with their warning
badge (see item #15 for making that badge useful on mobile).

For trusted/admin users, the default should remain `'all'` so staff can see the full
picture without having to change a setting.

**Implementation notes:**
- Add `qry__confirmed: 'confirmed_only' | 'all' | 'unconfirmed_only'` to
  `$idaa_loc.recovery_meetings` defaults, defaulting to `'confirmed_only'`.
  Trusted users default to `'all'`.
- Apply the filter in both the IDB fast path (`db_events.event.filter()`) and the
  API revalidation secondary filter in `handle_search_refresh`. IDB: check
  `ev.status !== 'unknown'` when `qry__confirmed === 'confirmed_only'`. API: same
  post-fetch client-side filter.
- Pass `qry__confirmed` to `events_func.search__event` if the API supports a
  `status` filter param; otherwise handle it client-side only.
- The `no_results_no_filters` derived (used for the escape-hatch button) should NOT
  treat `qry__confirmed === 'confirmed_only'` as an active filter — it's the default
  state, not a narrowing choice the member made. Only count it as a filter if the
  member explicitly switched it to `'all'` or `'unconfirmed_only'`.
- Add a count badge to the toggle: "Confirmed Only (132 of 140)" so members can see
  how many unconfirmed meetings exist without having to switch the filter.

---

### 16. "Not Confirmed" status — inline explanation on mobile

**Current behavior:** Meetings with `status === 'unknown'` show a warning badge:
`⚠ Not Confirmed by IDAA ⚠`. The badge has a `title` attribute with a full explanation
(~2 sentences). Title tooltips are invisible on mobile — tapping the badge does nothing.
Members on phones see a alarming-looking warning with no explanation of what it means
or what they should do.

**Proposed change:** Make the badge tappable. On tap (or hover on desktop), show an
inline explanation panel directly below the badge:

```
⚠ Not Confirmed by IDAA   [?]

  ↓ (on tap)

  This meeting has not been confirmed by IDAA Central Office.
  Please reach out to the chair for current information.
  If this meeting is active, email info@idaa.org to confirm it.
                                                        [✕ Close]
```

**Implementation notes:**
- Add a `$state show_unconfirmed_info = false` per card (scoped to the `{#each}` block).
- Replace the `title` attribute with an `onclick` toggle that sets `show_unconfirmed_info`.
- The explanation renders in a `{#if show_unconfirmed_info}` block directly below the
  badge row — a simple div with a rounded border and the existing tooltip text.
- The `mailto:info@idaa.org` link in the explanation is already in the tooltip text;
  making it a real clickable link here rather than plain text in a tooltip is a direct
  improvement for mobile members who want to report a confirmed meeting.
- This pattern also applies to other `title`-only tooltips on the page if they appear.
- Note: with item #15 defaulting to "confirmed only", most members will never encounter
  this badge unless they switch to the "All" view. The inline explanation is still worth
  implementing for that audience, but the default filter reduces how often it's seen.

---

---

### 17. Cycling sort button — ✅ Implemented 2026-05-18

**Problem:** Three separate sort chip buttons (Last Updated / Name A→Z / Name Z→A) took
too much horizontal space and caused layout bounce as the selected chip changed width.

**Implemented:** Single cycling button in `ae_idaa_comp__event_obj_qry.svelte`.
Clicking advances through `sort_modes` array (Last Updated → Name A→Z → Name Z→A → repeat)
using `$derived` index + `cycle_sort()` function. Button has `min-w-36` to prevent bounce.
Icon changes per mode (fa-clock / fa-sort-alpha-down / fa-sort-alpha-up-alt). A small
fa-redo icon indicates it's a cycling control.

---

### 18. Collapsible "Meeting Info" data store panel — ✅ Implemented 2026-05-18

**Problem:** The `Element_data_store` panel (code: `recovery_meetings_info`) displays
between the filter bar and the meeting results list. Once a member has read it, it
consumes vertical space on every page load and pushes results below the fold, especially
in the Novi iframe on mobile.

**Implemented:** Toggle button wrapping the `<Element_data_store>` in
`src/routes/idaa/(idaa)/recovery_meetings/+page.svelte`. Button shows
"Meeting Info" with a chevron (up = expanded, down = collapsed). Collapse state
persisted in `$idaa_loc.recovery_meetings.ds_info_collapsed` (localStorage) so the
user's preference survives page reloads. New field added to `idaa_local_data_struct`
in `ae_idaa_stores.ts` — no version bump needed (existing users without the field
get `undefined` which is falsy = expanded, the correct default).

---

## Notes

- The fulltext search (`qry__fulltext_str`) searches against the `default_qry_str`
  field, which is a server-side composite that already includes contact info, day-of-week
  text, meeting type, location, and other metadata. The placeholder text in the search
  input is accurate — it genuinely searches contacts and schedule information despite
  those fields being stored in separate columns.
- All changes must render correctly inside the Novi iframe context (Bootstrap v3.4.1
  CSS conflicts — see `CLIENT__IDAA_and_customized_mods.md` for known issues).
- Mobile testing should cover Android Chrome specifically — the original "no meetings
  found" bug disproportionately affected mobile users with intermittent connections.

