ui

44 modules

  • ui/account_sessions_state.svelte.ts

    Reactive state for managing the authenticated account's auth sessions on a settings page. Reads and mutations flow through a narrow RPC adapter backed by auth/account_actions.ts.

    Holds two AsyncSlots β€” list for the fetch, revoke_all for the bulk revoke β€” plus one KeyedAsyncSlot<string, void> (revoke) keyed by session_id for per-row revoke (independent supersession across concurrent rows; per-row error surfacing). Method names use the submit_* prefix to avoid slot-name collisions.

  • ui/AccountSessions.svelte

    Self-serve session list for the logged-in account. Instantiates an AccountSessionsState against account_sessions_rpc_context and renders a Datatable with per-row revoke and an optional revoke-all. Calling revoke_all clears auth_state.verified so the UI falls back to login.

  • ui/admin_accounts_state.svelte.ts

    Reactive state for admin account management.

    Holds one fetch AsyncSlot (list) plus three KeyedAsyncSlots β€” grant (offer creation, keyed by account_id:role or account_id:role:to_actor_id), revoke (role_grant revoke, keyed by role_grant_id), retract (offer retraction, keyed by offer_id). Per-row supersession is correct (clicking row B no longer aborts row A) and error(key) surfaces failure per-row. Method names use the submit_* prefix to avoid slot-name collisions.

  • ui/admin_invites_state.svelte.ts

    Reactive state for admin invite management.

    Flows every operation through an injected AdminInvitesRpc adapter β€” the class stays decoupled from the concrete RPC client so tests can inject plain-function stubs. Mirrors AdminAccountsRpc / AuditLogRpc.

    Holds two AsyncSlots β€” list (fetch) and create (singular write) β€” plus one KeyedAsyncSlot<Uuid> (remove) for the per-row delete with correct per-row supersession and per-row error surfacing. Method names use the submit_* prefix to avoid slot-name collisions (delete is reserved at top-level positions; renamed for symmetry).

  • ui/admin_rpc_adapters.ts

    Admin RPC adapter helpers for consumer UIs.

    Bridges a typed throwing RPC client to the four narrow admin RPC interfaces the state classes consume β€” AdminAccountsRpc, AdminInvitesRpc, AuditLogRpc, AppSettingsRpc. Two calls at the admin shell layout wire everything.

    Intentionally admin-only despite the backend-side create_standard_rpc_actions rename (admin + role-grant-offer + account). Account-surface methods flow through account_sessions_rpc_context (wired at the self-service layout), and role-grant-offer methods that surface in the admin UI (role_grant_offer_create, role_grant_revoke, role_grant_offer_retract) live inside the AdminAccountsRpc interface β€” they belong to the admin UX, not a separate wire pairing. The UI side and backend factory names diverge by design.

    // `api` is the typed throwing Proxy from `create_frontend_rpc_client`. provide_admin_rpc_contexts(create_admin_rpc_adapters(api));

    The throwing Proxy spreads the JSON-RPC {code, message, data?} onto the thrown Error so form components (e.g. ui/RoleGrantOfferForm.svelte) can match on error.data?.reason via ERROR_ROLE_GRANT_OFFER_* constants β€” optional chaining is required because JSON-RPC data is spec-level optional. Consumers that need a custom unwrap strategy can construct their own object satisfying AdminRpcApi and pass it directly.

    No .svelte.ts suffix β€” this module holds no reactive state, only method-name mappings.

  • ui/admin_sessions_state.svelte.ts

    Reactive state for admin session overview.

    Both the listing and the two revoke-all mutations flow through the shared AdminAccountsRpc adapter (list_sessions, session_revoke_all, token_revoke_all); the listing wraps the admin_session_list RPC method.

    Holds one fetch AsyncSlot (list) plus two KeyedAsyncSlots keyed by account_id β€” revoke_sessions and revoke_tokens. Per-account concurrent revokes are independent (clicking row B does not abort row A) and per-row errors surface via revoke_sessions.error(account_id) / revoke_tokens.error(account_id).

  • ui/AdminAccounts.svelte

    Admin accounts table β€” users with their role_grants and pending offers. Consumes admin_accounts_rpc_context (read via AdminAccountsState) and format_scope_context for label rendering. Per-row actions: grant role (role_grant_offer_create), revoke role_grant (role_grant_revoke, keyed by actor_id), retract pending offer (role_grant_offer_retract).

  • ui/AdminAuditLog.svelte

    Admin audit log viewer. Consumes audit_log_rpc_context; uses audit_log_list RPC for fetches and a separate EventSource against the SSE stream URL for live tailing (toggle via the stream button). Filter by event_type, manual refresh, and live-stream connection status are surfaced in the header.

  • ui/AdminInvites.svelte

    Admin invites manager β€” list, create (invite_create), and delete (invite_delete) RPC actions through admin_invites_rpc_context. Embeds OpenSignupToggle so the same surface controls both the invite-only and open-signup flows.

  • ui/AdminOverview.svelte

    Admin dashboard β€” six summary panels (accounts, sessions, invites, recent activity, security, system) fed by parallel fetch() calls on mount. Consumes all four admin RPC contexts plus auth_state_context; derives role_counts, failed_logins, and role_grant_changes from the audit log slice.

  • ui/AdminRoleGrantHistory.svelte

    Role grant create/revoke history table. Consumes audit_log_rpc_context, calls audit_log.fetch_role_grant_history() once on mount (the audit_log_role_grant_history RPC). Uses format_scope_context to render scope ids as human labels.

  • ui/AdminSessions.svelte

    Cross-account active session list with per-account revoke-all controls for sessions and tokens. Listing (admin_session_list) and both revoke-all mutations (admin_session_revoke_all, admin_token_revoke_all) reuse admin_accounts_rpc_context β€” a single adapter backs both AdminSessionsState and AdminAccountsState.

  • ui/AdminSettings.svelte

    Admin settings shell β€” composes OpenSignupToggle, the logged-in account line, and a confirm-protected logout button. No direct RPC calls; reads auth_state_context and delegates settings to its children.

  • ui/AdminSurface.svelte

    Attack-surface viewer. Fetches GET /api/surface (REST β€” not RPC, since the surface dump exists outside the action surface) and delegates rendering to SurfaceExplorer. Surfaces a retry button on fetch failures.

  • ui/app_settings_state.svelte.ts

    Reactive state for admin app settings management.

    Flows every operation through an injected AppSettingsRpc adapter β€” mirrors AdminInvitesRpc / AuditLogRpc. Tests can inject plain-function stubs and consumers adapt their typed RPC client to the same shape.

    Holds two AsyncSlots β€” list (the initial fetch) and update (the app_settings_update write). Slots track status/error; the canonical settings lives on the class so consumers don't unwrap slot.data.

  • ui/AppShell.svelte

    Sidebar-and-main app shell. Provisions sidebar_state_context (creating a fresh SidebarState if sidebar_state is not supplied) so descendants can read sidebar visibility and toggle it. Optionally binds a global keyboard shortcut and renders a built-in toggle button (or a custom one via the toggle_button snippet).

  • ui/async_slot.svelte.ts

    Composable async-operation slot for Svelte 5 reactive state classes.

    A state class HOLDS one or more AsyncSlots via composition β€” one slot per distinct async operation (e.g. list + create + revoke). Each slot tracks the status, payload, and error of its operation independently, so state classes with multiple write paths don't accumulate ad-hoc creating / updating fields beside a single shared loading / error pair.

    Core surface:

    - Explicit four-value status β€” AsyncStatus from @fuzdev/fuz_util/async.js: `'initial' | 'pending' | 'success' | 'failure'. loading: false, error: null` would be ambiguous between "never tried" and "succeeded once and now resting"; the four-value status removes the need for a per-class submitted / hydrated flag. - Owns data: T | undefined β€” the success payload persists across retries (stale-while-revalidate). The sentinel is undefined (not null) so null stays available as a legitimate success value for nullable Ts. Pass T = void for write-only actions whose response isn't worth keeping. - Supersession via internal AbortController β€” a second run() aborts the first, and superseded results are silently discarded without writing to state. Removes the "in-flight call resolves after the locator advanced" race that locator-style state classes would otherwise need to compensate for. - AbortSignal threaded to the callback β€” RPC clients that accept a signal (or fetch) get cancellation for free; callers can also pass an external signal via {@link RunOptions} to bind the slot's lifetime to a component / page. - preserve_error_on_retry β€” opt-in to keeping the previous error visible while a retry is pending (default clears at the start of each run()). - Per-slot map_error β€” set once in the constructor ({map_error: to_rpc_error_message}); every run() gets the right normalization without re-passing per call. - Public run() β€” slots are composed, not subclassed, so call sites can invoke state.list.run(...) directly.

    @example

    class CellsState { readonly list = new AsyncSlot<{cells: ReadonlyArray<CellJson>}>(); readonly create = new AsyncSlot<{cell: CellJson}>({map_error: to_rpc_error_message}); async fetch() { await this.list.run((signal) => this.#api.cell_list({}, {signal})); } async submit_new(input: CellCreateInput) { const result = await this.create.run(() => this.#api.cell_create(input)); if (result) await this.fetch(); } }
  • ui/audit_log_state.svelte.ts

    Reactive state for the audit log viewer.

    Two fetch primitives (fetch for events, fetch_role_grant_history for the grant/revoke shortcut) flow through an injected RPC adapter; the SSE stream continues to use EventSource directly β€” streams aren't an RPC concern.

    Holds two AsyncSlots β€” list (the main event stream) and role_grant_history (the dedicated role-grant history endpoint). Data lives on the class so SSE pushes and gap-fill calls update it directly.

  • ui/auth_state.svelte.ts

    Reactive state for cookie-based authentication.

    SPA auth pattern: prerendered static HTML served by Hono, no SvelteKit server for SSR sessions. On load, fetches GET /api/account/status which returns the current account (200) or 401 with optional bootstrap_available. Login sends username + password once, then a signed httpOnly cookie handles all subsequent requests.

    @example

    <script lang="ts"> import {AuthState, auth_state_context} from '@fuzdev/fuz_app/ui/auth_state.svelte.js'; const auth = new AuthState(); auth_state_context.set(auth); auth.check_session(); </script> {#if auth.verifying} <p>checking session…</p> {:else if auth.needs_bootstrap} <BootstrapForm /> {:else if !auth.verified} <LoginForm /> {:else} <p>logged in as {auth.account?.username}</p> <button onclick={() => auth.logout()}>logout</button> {/if}
  • ui/BootstrapForm.svelte

    First-keeper bootstrap form β€” used once on a fresh deployment to claim the bootstrap token and create the initial admin account.

    Calls AuthState.bootstrap (POST /api/account/bootstrap) via auth_state_context. Validates token + Username schema + PASSWORD_LENGTH_MIN + password match client-side; submit focuses the first invalid field. Once an account exists, the bootstrap path is disabled server-side and this form should not be mounted.

  • ui/ColumnLayout.svelte

    Two-column layout β€” fixed-width aside on the left, fluid children column on the right. Both columns scroll independently.

  • ui/ConfirmButton.svelte

    Confirmation popover wrapping PopoverButton.

    Clicking the trigger opens a popover with a confirm button. On confirm, calls onconfirm and hides the popover (controlled by hide_on_confirm). Defaults to position="left".

    Trigger content: pass label for a simple string, or a children snippet for custom content (the two are mutually exclusive β€” DEV errors when both are set). pending: boolean overlays a spinner and disables the trigger, mirroring PendingButton semantics so the label stays put while an async operation runs.

    @example

    <ConfirmButton onconfirm={() => delete_item(item.id)} title="delete item" label="delete" pending={state.remove.loading(item.id)} />

    @example

    <!-- custom trigger content via the children snippet --> <ConfirmButton onconfirm={() => grant(item.id, role)} title="offer {role}" pending={state.grant.loading(key)} > {#snippet children(_popover, _confirm)}+ {role}{/snippet} </ConfirmButton>

    @example

    <!-- custom confirm button content --> <ConfirmButton onconfirm={handle_revoke} class="icon_button plain" title="revoke"> revoke {#snippet popover_button_content()}revoke{/snippet} </ConfirmButton>
  • ui/Datatable.svelte

  • ui/datatable.ts

    Types and constants for the Datatable component.

  • ui/form_state.svelte.ts

    Reactive form state for controlling when field errors appear and focusing invalid fields.

    Tracks per-field touched state (set on blur via delegated focusout) and form-level attempted state (set on submit attempt). Errors show after a field is blurred or after a submit attempt, avoiding premature validation while the user is still typing.

    The FormState.form attachment also handles Enter key advancing between focusable elements.

    All trackable inputs must have a name attribute β€” an error is thrown in dev if an input without name loses focus.

    @example

    <script> const form_state = new FormState(); let username = $state.raw(''); const username_valid = $derived(Username.safeParse(username).success); const can_submit = $derived(username.trim() && username_valid); const handle_submit = async () => { form_state.attempt(); if (!can_submit) { if (!username.trim() || !username_valid) form_state.focus('username'); return; } // submit... }; </script> <form {@attach form_state.form()} onsubmit={(e) => { e.preventDefault(); void handle_submit(); }}> <input name="username" bind:value={username} /> {#if form_state.show('username') && username && !username_valid} <p>error message</p> {/if} <PendingButton onclick={handle_submit}>submit</PendingButton> </form>
  • ui/format_scope.ts

    Shared format_scope callback contract for role-grant-display components.

    Role grants and offers carry a scope_id that names a consumer-owned resource (e.g. a classroom uuid). The default render is the raw uuid. Consumers wire a FormatScope via context to render a human label without per-page lookup or forking the components.

  • ui/keyed_async_slot.svelte.ts

    Keyed sibling of AsyncSlot β€” fans the per-instance supersession machinery out across an open set of keys.

    Each key gets its own lazily-created AsyncSlot, so concurrent run(key_a, ...) and run(key_b, ...) calls are independent: a second run() on key_a aborts only key_a's in-flight call, leaving key_b running. The keyed shape replaces the AsyncSlot + SvelteSet<id> pair that state classes previously carried for per-row in-flight tracking, with two genuine wins:

    - Cross-key supersession is correct β€” clicking row B while row A is in flight no longer aborts A; each row has its own AbortController. - Per-key error surfacing β€” error(key) carries the failure for that key only, instead of the last-error-wins shape of a shared slot.

    The backing SvelteMap keeps entries even after a run() resolves β€” components can read error(key) to render an inline per-row failure indicator. Call delete(key) to dismiss an entry, or reset() to clear everything (e.g. on page leave).

    @example

    class AdminInvitesState { readonly remove = new KeyedAsyncSlot<Uuid>(); async submit_delete(id: Uuid): Promise<void> { const ok = await this.remove.run(id, () => this.#rpc().delete({invite_id: id})); if (ok !== undefined) await this.fetch(); } } // In a template: // <button disabled={state.remove.loading(row.id)}> // {state.remove.loading(row.id) ? 'deleting…' : 'delete'} // </button> // {#if state.remove.error(row.id)}<p>{state.remove.error(row.id)}</p>{/if}
  • ui/LoginForm.svelte

    Username + password login form. Calls AuthState.login (which posts to POST /api/account/login) via the auth_state_context. On success navigates to redirect_on_login; surfaces 401 / 429 errors via auth_state.verify_error. Companion to BootstrapForm and SignupForm.

  • ui/LogoutButton.svelte

    Pending-aware log-out button. Wraps PendingButton and calls AuthState.logout (POST /api/account/logout) via auth_state_context. If the caller provides onclick and calls e.preventDefault() from it, the logout is skipped β€” useful for confirm-before-logout flows.

  • ui/MenuLink.svelte

    SvelteKit-aware navigation link. Resolves path via resolve from $app/paths, then derives selected from page.url.pathname β€” fires for both the exact page (/admin/repos) and any descendant page (/admin/repos/foo/log). Exact matches additionally carry aria-current="page" so CSS / assistive tech can distinguish "this exact page" from "current section" when needed.

    The earlier highlighted flag (which fired only on the prefix-only case) was dropped: in every practical use it sat next to .selected with a near-identical visual treatment and just made callers compute two booleans for what reads to users as one "you are here" state. The highlighted *name* is now free for orthogonal emphasis (recent activity, unread badges, search matches) β€” that's how fuz_ui's DocsPageLinks already uses it.

  • ui/OpenSignupToggle.svelte

    Single checkbox bound to AppSettings.open_signup. Consumes app_settings_rpc_context; the toggle calls app_settings_update RPC via AppSettingsState.update_open_signup. Hides gracefully when has_rpc is false (renders an "rpc adapter not wired" notice).

  • ui/popover.svelte.ts

    Popover state class with trigger, content, and container attachments.

    Popover manages visibility, positioning, outside-click dismissal, and ARIA attributes. The trigger, content, and container attachments wire up the DOM via Svelte's {@attach} directive.

    For the common button + popover pattern, see PopoverButton.

    @example

    <script lang="ts"> import {Popover} from '@fuzdev/fuz_app/ui/popover.svelte.js'; const popover = new Popover(); </script> <div class="position:relative" {@attach popover.container}> <button type="button" {@attach popover.trigger({position: 'bottom', align: 'center'})}> toggle </button> {#if popover.visible} <div {@attach popover.content({position: 'bottom', align: 'center'})}> <button type="button" onclick={() => popover.hide()}>close</button> </div> {/if} </div>

    @example

    // programmatic control with callbacks const popover = new Popover({ position: 'right', align: 'start', offset: '8px', disable_outside_click: true, onshow: () => console.log('opened'), onhide: () => console.log('closed'), }); popover.show(); popover.toggle(); popover.hide();
  • ui/PopoverButton.svelte

    Button + popover composition for the common toggle-on-click pattern.

    Wraps a Popover instance with a <button>, a positioned content area, and scale transitions. Spreads extra props onto the button element. Pass popover_content for the popover body, and either children for simple button content or button for a fully custom trigger β€” both receive the Popover instance.

    @example

    <PopoverButton position="bottom" align="center"> {#snippet popover_content(popover)} <div class="box p_md"> <button type="button" onclick={() => popover.hide()}>close</button> </div> {/snippet} open menu </PopoverButton>

    @example

    <!-- custom trigger via the `button` snippet --> <PopoverButton position="right" align="start" disable_outside_click> {#snippet popover_content(popover)} <form onsubmit={() => popover.hide()}> <input name="search" /> <button type="submit">go</button> </form> {/snippet} {#snippet button(popover)} <button type="button" class="icon_button" {@attach popover.trigger()}> search </button> {/snippet} </PopoverButton>

    @see ConfirmButton for a higher-level wrapper with confirmation semantics

  • ui/position_helpers.ts

    CSS position calculation helpers for popovers and floating UI elements.

  • ui/role_grant_offers_state.svelte.ts

    Reactive state for the consentful-role-grants offer flow.

    Maintains one offer cache keyed by id, seeded by the RPC list/history actions and kept live by the six role-grant-offer WebSocket notifications. incoming (recipient-side pending) and outgoing (grantor-side pending) are derived views; history is the full cache ordered newest-first for the grantor/admin history view.

    Wiring is transport-agnostic: the ctor accepts a narrow RPC interface the consumer adapts from their typed client, plus an account_id / actor_id getter pair (typically bound to auth_state). Notification delivery is pull-only via subscribe() β€” the consumer plumbs their FrontendWebsocketClient / ActionPeer receiver to apply_notification.

    Holds six AsyncSlots β€” one per RPC verb. The cache #offers lives on the class (multiple ops write into it via #merge_offers / #remove_offer); the create slot is typed AsyncSlot<RoleGrantOfferJson> so submit_create can return the new offer via the slot's supersession-safe data path, but the other slots' data is unused (no single op owns the cache). Method names use the submit_* prefix to avoid slot-name collisions; the history view stayed natural by naming the fetch slot list_history.

  • ui/RoleGrantOfferForm.svelte

  • ui/RoleGrantOfferHistory.svelte

  • ui/RoleGrantOfferInbox.svelte

  • ui/sidebar_state.svelte.ts

    Reactive sidebar visibility state. Provisioned by AppShell.svelte via sidebar_state_context; consumers read show_sidebar and call toggle_sidebar / activate.

  • ui/SignupForm.svelte

    Account signup form β€” username + password (+ optional email).

    Calls AuthState.signup (POST /api/account/signup) via auth_state_context. Validates the Username schema and PASSWORD_LENGTH_MIN client-side; surfaces 403 (no invite), 409 (duplicate), and 429 (rate limited) inline. Submit focuses the first invalid field. Companion to LoginForm and BootstrapForm.

  • ui/SurfaceExplorer.svelte

    Read-only AppSurface renderer. Tables routes, middleware, environment variables, events, and diagnostics. Routes can be filtered by auth type and expanded to dump params / query / input / output / errors schemas as JSON. Pure presentational β€” AdminSurface handles the fetch.

  • ui/table_state.svelte.ts

    Reactive state for database table pagination and data fetching.

    Holds one AsyncSlot β€” list (the paginated row fetch). Per-row delete uses plain try/catch + scalar deleting / delete_error fields (no slot β€” delete_error must persist past list.run() retries so the failure message stays visible while the user refetches).

    @example

    const table = new TableState(); await table.fetch('accounts', 0, 50); // pagination if (table.has_next) table.go_next(); await table.fetch(table.table_name, table.offset, table.limit); // deletion const deleted = await table.delete_row(table.rows[0]);

    @example

    <script lang="ts"> import {TableState} from '@fuzdev/fuz_app/ui/table_state.svelte.js'; const table = new TableState(); table.fetch('accounts'); </script> {#if table.list.loading} <p>loading…</p> {:else if table.list.error} <p>{table.list.error}</p> {:else} <p>showing {table.showing_start}–{table.showing_end} of {table.total}</p> {/if}
  • ui/ui_fetch.ts

    Authenticated fetch helper for cookie-based session auth.

    Wraps the standard fetch with credentials: 'include' so cookies are sent with every request. Use for all API calls in apps that rely on @fuzdev/fuz_app session middleware.

  • ui/ui_format.ts

    Formatting utilities for UI display.

    Value formatting, relative timestamps, uptime display, absolute timestamp formatting, and audit metadata formatting.