Substratal Apps Substratal Apps Hub specs
GitHub

Docs 00-shared COMPONENTS.md

Components

Semantic components used by SCREEN-SPECS.md (the kind column), each with a behavior contract and the native control on every platform. Build each once per platform as a reusable view; screens compose only these.

1. Behavior contracts (identical everywhere)

Component Contract
StatusBadge Icon + text label (never color only). Label/icon from status-semantics.json. Read by screen readers as "Status: {label}". Non-interactive.
AccessSwitch Reflects the personal entitlement path. ON→OFF opens ACX-03 confirm (title names person + app; optional reason). OFF→ON applies immediately. Optimistic; on error revert + error toast. After OFF success: toast with Undo for 10 s (PATCH back to active). Disabled with caption when the caller lacks rights or the row's only path is org-sourced ("Via {org}" link instead).
PathList Under a resolved status, lists every access path: source, status, org, member_decision, dates, disabled_reason. Collapsed by default when there is one path.
SecretReveal (KEY-03) Modal that cannot be dismissed (Escape, outside click, swipe, back) until "I've copied…" is checked. Monospaced selectable text, Copy button (sensitive clipboard, auto-clear 60 s). Secret never re-rendered after close. Screenshot/recording protection where available.
DestructiveConfirm Platform alert/dialog. Title is a question naming the object. Body states consequence. Destructive button uses destructive style and is not the default button. Optional reason field when the API accepts one.
EntityList / EntityTable Search (debounced 300 ms, server q when > 1 page), status filter chips, cursor pagination, virtualized, pull-to-refresh (touch) / ⌘R·Ctrl+R·F5 (keyboard). Preserves scroll, selection and filters on back-navigation. Empty / no-results / forbidden / error states.
MultiSelect + BulkBar Desktop/web: click, Shift-click (range), ⌘/Ctrl-click (toggle), ⌘/Ctrl+A, marquee on empty space in grids. Touch: "Select" button or long-press enters selection mode; checkboxes appear. A bulk bar shows count + allowed actions; actions run per row with progress and a partial-failure summary.
ContextMenu Right-click / Shift+F10 / Menu key (desktop), long-press (touch), ⋯ button (always visible on hover/focus, and on touch rows). Same items as the row's visible actions.
SchemaForm Renders an Application settings_schema: string+enum → select; string → text; string format: date-time → date-time picker; boolean → switch; integer/number → stepper or number field honoring min/max; object/array → read-only JSON with "Edit on desktop". Each row shows SourceCaption and Reset.
SourceCaption "Changed for this app · Reset" / "Using your global setting" / "App default".
IDChip Monospaced ID + copy button; copying announces "Copied".
Toast Single line + optional action, 5 s (10 s with Undo), pauses on hover/focus, announced politely. Max one visible; queue the rest.
Banner Persistent page-level notice (verify email, restricted plan, offline, maintenance, invitation). Dismissible only when informational.
UsageMeter Label, current / limit, bar; caution at ≥ 80 %, critical at 100 % (semantic roles, not colors).
CommandPalette (GLB-01) Opens with ⌘K / Ctrl+K or search button; fuzzy match across navigation, recent items, actions; server lookups for users/orgs (debounced); ID prefixes resolve directly; Enter runs, ⌘/Ctrl+Enter opens in new window/tab where supported.
OTPField 6 cells or one field; paste distributes; OS one-time-code autofill; auto-submit on completion.
ThemePicker Appearance segmented control (Light · Dark · Match system) plus two radio groups of five theme swatches (light, dark). Each swatch is a miniature of background, sidebar, raised card, accent bar and text lines, with the theme name and a "Default" tag on Moss Paper / Forest Floor. Selecting applies instantly to the whole app with no confirmation; keyboard: arrows move, Space selects; screen readers announce "{name} light theme, 3 of 5, selected". Mobile: 3 columns; desktop: 5. See THEMES.md.
BrandLockup The Substratal logo from brand/ only: horizontal lockup on AUTH-01/02 and About; inline lockup in the web sidebar header; small mark at 24 px and below. Accessible name "Substratal Apps" (decorative when adjacent text already says Substratal). Light/on-dark variant follows the surface. Developer builds: long-press reveals the test-mode toggle.
NotificationRow App icon (Substratal mark for source: platform), title, 2-line body, relative time, unread dot, "Important" marker for priority: high. Opening marks read (optimistic). Swipe/hover actions: Read ⇄ Unread, Archive (Undo). Screen readers read "Unread, Important, {app}: {title}, {time}".
UnreadBadge Count from unread-count only (never computed from loaded pages). Caps display at 99+. Appears on the Inbox nav item, the bell, the app icon/Dock/taskbar/PWA badge. Announced when it changes while focused.
PushPrimer (NTF-04) Explains value before the OS prompt; "Turn on" triggers the OS prompt; "Not now" snoozes 14 days. Never shown before sign-in or more than once per snooze window.
DeviceRow App icon, device name (editable), provider, status (On / Off with reason), last registered; actions: Send test (Hub devices), Rename, Remove (confirm). Never shows a token — only token_hint.
PlanCard Plan name (Free / Standard), what it includes, coverage sources, subscription status badge and period end, primary action (Upgrade / Manage billing / Subscribe the organization). Price from remote config or unit_amount.
CheckoutHandoff Button with inline progress while creating the session → opens Stripe externally → on return shows "Finishing up…" and polls the subscription (2 s, max 30 s). Prevents double submits with the Idempotency-Key.
CredentialFileField Picks/drops/pastes a key file (.p8, FCM JSON), validates shape locally, shows only the filename + "Will be stored encrypted", sends once, discards from memory.
ImagePicker Camera, photo library, file picker, drag-drop, clipboard paste; crop to square; shows upload progress. (Hidden until OPEN-QUESTIONS #1 resolved.)

2. Native mapping

Semantic macOS (SwiftUI) Windows (WinUI 3) iOS (SwiftUI) Android (Compose M3) Linux (GTK4 + libadwaita) Web
StatusBadge Label in capsule Border + FontIcon + TextBlock Label in capsule non-interactive chip / Surface pill GtkBox.status-badge <span role="status">-free pill (icon aria-hidden, text visible)
AccessSwitch Toggle(.switch) ToggleSwitch Toggle Switch (row toggleable) GtkSwitch / AdwSwitchRow <button role="switch" aria-checked>
PathList DisclosureGroup SettingsExpander DisclosureGroup expandable ListItem AdwExpanderRow <details> / disclosure button
EntityList List(selection:) ListView List + .searchable LazyColumn + SearchBar GtkListView / .boxed-list virtualized role="list" / listbox
EntityTable Table ListView + header grid / CommunityToolkit DataTable List (iPad: Table) LazyColumn (expanded: table layout) GtkColumnView .data-table virtualized role="grid" with resizable, sortable columns
Tabs (detail) segmented Picker / TabView SelectorBar segmented Picker PrimaryTabRow / SegmentedButton AdwViewSwitcher / inline switcher role="tablist"
Form (modal) .sheet + Form ContentDialog / page .sheet + Form full-screen Dialog / ModalBottomSheet AdwDialog + AdwPreferencesGroup <dialog> (desktop) / bottom sheet (phone)
DestructiveConfirm .confirmationDialog / .alert ContentDialog .alert / .confirmationDialog AlertDialog AdwAlertDialog <dialog role="alertdialog">
SecretReveal sheet, interactiveDismissDisabled ContentDialog (primary disabled) sheet, interactiveDismissDisabled Dialog (dismissOnBackPress=false) + FLAG_SECURE AdwDialog can-close=false modal <dialog> with cancel event prevented
ContextMenu .contextMenu MenuFlyout / CommandBarFlyout .contextMenu (with preview) DropdownMenu on long-press GtkPopoverMenu custom menu (role="menu") on contextmenu / long-press
Toast bottom HUD overlay (custom) InfoBar / toast region (custom) capsule overlay (custom) Snackbar AdwToast role="status" live region toast
Banner inline GroupBox InfoBar inline section banner card AdwBanner role="region" banner
UsageMeter Gauge(.linearCapacity) ProgressBar Gauge LinearProgressIndicator GtkLevelBar <meter>
SchemaForm rows Form rows SettingsCards Form rows ListItems Adw*Rows form rows (<select>, <input>, switch)
CommandPalette sheet/panel title-bar search + AutoSuggestBox search tab / .searchable SearchBar expanded GtkSearchEntry popover <dialog> combobox (role="combobox" + listbox)
OTPField TextField .oneTimeCode TextBox Number TextField .oneTimeCode OutlinedTextField + ContentType.SmsOtpCode AdwEntryRow digits <input autocomplete="one-time-code" inputmode="numeric">
ImagePicker PhotosPicker + fileImporter + paste FileOpenPicker + camera + paste PhotosPicker + camera Photo Picker + camera intent GtkFileDialog (portal) <input type=file accept=image/* capture> + paste + drop
NotificationRow List row with swipe/contextMenu ListView item + SwipeControl List row + .swipeActions ListItem + SwipeToDismissBox AdwActionRow + popover menu role="listitem" row with menu + swipe
UnreadBadge .badge(n), NSApp.dockTile.badgeLabel InfoBadge, BadgeUpdateManager .badge(n), UNUserNotificationCenter.setBadgeCount BadgedBox, launcher badge via notification count sidebar label count, Unity launcher count badge element + navigator.setAppBadge
PushPrimer sheet → UNUserNotificationCenter.requestAuthorization n/a (local notifications need no prompt) sheet → requestAuthorization dialog → POST_NOTIFICATIONS permission n/a dialog → Notification.requestPermission() + pushManager.subscribe
PlanCard GroupBox Border card Section Card .card <section class=card>
CredentialFileField fileImporter + drop + paste FileOpenPicker + drop fileImporter OpenDocument GtkFileDialog + drop <input type=file> + drop + paste
MultiSelect Table selection ListView SelectionMode=Extended EditButton / selection selection mode GtkMultiSelection aria-multiselectable grid