Files
OSIT-AE-App-Svelte/documentation/AE__UI_UX_future_ideas.md
Scott Idem bbab9e7c8c feat(idaa): update default recovery meeting limit to 100 and add 75 to stepper
- Update default qry__limit to 100 in idaa_loc
- Add 75 to limit_steps in recovery meetings query component
- Bump AE_IDAA_LOC_VERSION to 2 to apply changes to existing users
- Update IDAA documentation and TODO__Agents.md with SQL optimization task
- Mark implemented UI/UX ideas as done in documentation
2026-05-18 21:25:09 -04:00

27 KiB
Raw Blame History

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/ChicagoCT, America/New_YorkET). 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.