Buttons & Actions
This category covers the reusable button and action-bar controls used across the Praxrr UI. All components live under $ui/ (source: packages/praxrr-app/src/lib/client/ui/) and use Svelte 4 export let prop declarations with forwarded on:click handlers. They compose into connected control strips for list pages, toolbars, and dropdown triggers.
Button
Section titled “Button”Multi-variant button (primary/secondary/danger/ghost) with optional icon, responsive sizing, tooltip, and anchor mode. Renders a <button> normally, or an <a> when href is set. The element is wrapped in a Tooltip.
import Button from '$ui/button/Button.svelte';| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
text |
string |
'' |
No | Button label text; when empty and icon is set, the button renders icon-only (square padding). |
variant |
'primary' | 'secondary' | 'danger' | 'ghost' |
'secondary' |
No | Visual style. primary=accent fill, secondary=bordered neutral, danger=red fill, ghost=transparent. |
size |
'xs' | 'sm' | 'md' |
'sm' |
No | Size preset controlling padding, rounding, text size, and icon size (xs=12, sm=14, md=16). |
disabled |
boolean |
false |
No | Disables the button (ignored in anchor mode); applies not-allowed cursor and 50% opacity. |
icon |
ComponentType | null |
null |
No | Lucide/Svelte icon component to render alongside (or instead of) the text. |
iconColor |
string |
'' |
No | Tailwind class(es) applied to the icon; falls back to a neutral color for the ghost variant. |
textColor |
string |
'' |
No | Tailwind class(es) applied to the text span; falls back to a neutral color for the ghost variant. |
iconPosition |
'left' | 'right' |
'left' |
No | Whether the icon renders before or after the text/slot. |
type |
'button' | 'submit' |
'button' |
No | Native button type (button mode only). |
responsive |
boolean |
false |
No | When true, auto-switches to the xs size on screens below 768px via a matchMedia listener. |
hideTextOnMobile |
boolean |
false |
No | Visually hides the text on mobile (sr-only md:not-sr-only md:inline); it stays available to screen readers. |
fullWidth |
boolean |
false |
No | Applies w-full to stretch the button to its container width. |
href |
string | undefined |
undefined |
No | When provided, renders an <a> anchor instead of a <button>. |
target |
string | undefined |
undefined |
No | Anchor target attribute (anchor mode only). |
rel |
string | undefined |
undefined |
No | Anchor rel attribute (anchor mode only). |
justify |
'center' | 'between' |
'center' |
No | Content alignment; 'between' is used for dropdown-style triggers (justify-between). |
title |
string |
'' |
No | Native title attribute, applied only when no tooltip is set. |
ariaLabel |
string |
'' |
No | aria-label; falls back to tooltip, then to title for icon-only buttons. |
tooltip |
string |
'' |
No | Tooltip text rendered via the wrapping Tooltip component. |
tooltipPosition |
'top' | 'bottom' |
'bottom' |
No | Position passed to the Tooltip. |
<script> import Button from '$ui/button/Button.svelte'; import { Plus, Filter } from 'lucide-svelte';
function handleAdd() { // ... }</script>
<Button text="Add" variant="primary" icon={Plus} on:click={handleAdd} /><Button text="Next" variant="secondary" icon={Filter} iconPosition="right" /><Button text="Docs" variant="ghost" href="/docs" target="_blank" rel="noopener" />The interactive island below reproduces the variant and size API against standard Tailwind colors.
Events & Slots
Section titled “Events & Slots”on:click— forwarded from the underlying<button>or<a>.on:keydown— forwarded from the underlying<button>or<a>; used by dropdown triggers such asDropdownSelectto close on Escape.- Default slot — rendered between the text and the right-position icon, for custom content such as dropdown chevrons.
Variants & Notes
Section titled “Variants & Notes”- Variants:
primary,secondary,danger,ghost. Sizes:xs,sm,md. - Icon-only mode auto-triggers when
iconis set andtextis empty (uses squareiconOnlySizeClasses). - Svelte 4-style
export letprops with a forwardedon:click; wrapped in$ui/tooltip/Tooltip.svelte. - Responsive mode registers a
(max-width: 767px)matchMedialistener on mount and cleans it up on destroy. iconSizeis derived from the effective size.
ActionButton
Section titled “ActionButton”Bordered icon button sized for action bars, with an optional dropdown region. Fixed 40px height; square by default. Hovering previews the dropdown; a click, Enter, or Space pins it open.
import ActionButton from '$ui/actions/ActionButton.svelte';| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
icon |
ComponentType | undefined |
undefined |
No | Icon component rendered at size 20 inside the button. |
iconClass |
string |
'' |
No | Extra Tailwind class(es) appended to the icon. |
square |
boolean |
true |
No | Fixed square size (h-10 w-10); when false, uses h-10 with horizontal padding (px-4). |
hasDropdown |
boolean |
false |
No | Enables the dropdown slot region and the trigger’s popup ARIA attributes. |
dropdownPosition |
'left' | 'right' | 'middle' |
'left' |
No | Passed through to the dropdown slot as a slot prop for positioning. |
disabled |
boolean |
false |
No | Disables the button; applies not-allowed cursor and 50% opacity, removes hover styles. |
label |
string |
— | Yes | Accessible name. Applied as both aria-label and the native title tooltip on the button. |
type |
'button' | 'submit' |
'button' |
No | Native button type. |
variant |
'neutral' | 'danger' |
'neutral' |
No | Hover style; danger adds a red icon tint on group hover. |
popupRole |
'menu' | 'listbox' | 'dialog' |
'menu' |
No | Value of the trigger’s aria-haspopup. 'dialog' focuses the first tabbable control on keyboard open and disables auto-close on item choice. |
<script> import ActionButton from '$ui/actions/ActionButton.svelte'; import Dropdown from '$ui/dropdown/Dropdown.svelte'; import DropdownItem from '$ui/dropdown/DropdownItem.svelte'; import { Plus, Filter, FileText } from 'lucide-svelte';
function add() { // ... }</script>
<ActionButton icon={Plus} label="Add" on:click={add} />
<ActionButton icon={Filter} label="Filter" hasDropdown={true} dropdownPosition="right"> <svelte:fragment slot="dropdown" let:dropdownPosition let:close> <Dropdown position={dropdownPosition} role="menu" label="Filter" onClose={close}> <DropdownItem icon={FileText} label="Option A" /> </Dropdown> </svelte:fragment></ActionButton>Events & Slots
Section titled “Events & Slots”on:click— forwarded from the inner<button>.- Default slot — rendered inside the button, after the icon.
dropdownslot — exposes slot propsdropdownPosition,open(boolean), andclose(function that closes the dropdown and clears the pinned and hover state); rendered only while open andhasDropdownis true, inside a wrapperdivwhose id matches the trigger’saria-controls.
Keyboard & ARIA
Section titled “Keyboard & ARIA”| Input | Behavior |
|---|---|
| Hover | Opens a preview; closes 100ms after the pointer leaves the wrapper. |
| Click / Enter / Space | Pins the dropdown open; a second activation while pinned closes it. Keyboard activation (event.detail === 0) also moves focus into the dropdown. |
| ArrowDown on the trigger | Pins the dropdown open and moves focus into it. |
| Escape inside the wrapper | Closes the dropdown and returns focus to the trigger. Handled in the capture phase, so it runs before an inner Dropdown’s onClose. |
| Choosing an item | Clicking an element with role menuitem, menuitemradio, or option closes the dropdown after the item’s own handlers run; keyboard choice refocuses the trigger. menuitemcheckbox items and popupRole="dialog" stay open. |
| Pointer down or focus outside the wrapper | Closes the dropdown. |
- Focus target on open: for
popupRole="dialog", the first tabbable element; otherwise the first enabledmenuitem/menuitemcheckbox/menuitemradio/option, falling back to the first tabbable element. - With
hasDropdown, the trigger setsaria-haspopup={popupRole},aria-expanded(open state), andaria-controls(generated dropdown id). labelis required and is applied as the trigger’saria-labelandtitle, so icon-only triggers always have an accessible name.
Variants & Notes
Section titled “Variants & Notes”variant:neutralordanger;squarevs. padded (square={false}).- The 100ms leave delay lets the pointer travel from the trigger into the dropdown.
- The dropdown appears with a 150ms fade transition.
- The outer wrapper is a
role="group"div handlingmouseenter/mouseleave,focusout, item clicks, and capture-phase Escape.
ActionsBar
Section titled “ActionsBar”Layout wrapper that groups action items (SearchAction, ActionButton, ViewToggle, etc.) into a single connected control strip with collapsed borders and auto-rounded outer edges.
import ActionsBar from '$ui/actions/ActionsBar.svelte';| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
className |
string |
'' |
No | Extra Tailwind class(es) appended to the bar’s root (e.g. md:justify-start to left-align). |
<script> import ActionsBar from '$ui/actions/ActionsBar.svelte'; import SearchAction from '$ui/actions/SearchAction.svelte'; import ActionButton from '$ui/actions/ActionButton.svelte'; import ViewToggle from '$ui/actions/ViewToggle.svelte'; import { Plus } from 'lucide-svelte';
let view = 'table';</script>
<ActionsBar className="md:justify-start"> <SearchAction searchStore={search} responsive /> <ActionButton icon={Plus} label="Add" /> <ViewToggle bind:value={view} /></ActionsBar>- Default slot — holds the child action items. Scoped
:globalstyles collapse inner borders (margin-left: -1px), strip inner rounding, and re-apply rounding to the first/last (or only) child’s bordered elements.
Variants & Notes
Section titled “Variants & Notes”- On mobile the bar is centered and full-width (
flex w-full justify-center), switching to auto-width left-aligned on md+ (md:w-auto md:mx-0). - The border/rounding orchestration relies on the DOM shape of children, so items must be direct children.
ViewToggle
Section titled “ViewToggle”Action-bar control that toggles a data page between 'cards' and 'table' view via an ActionButton dropdown of the two options. The dropdown opens on click, Enter, or Space (hover only previews it); see ActionButton keyboard behavior.
import ViewToggle from '$ui/actions/ViewToggle.svelte';| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
value |
ViewMode ('table' | 'cards') |
'table' |
No | Currently selected view mode; intended to be bound (bind:value) — dropdown items assign it on click. |
position |
'left' | 'right' | 'middle' |
'right' |
No | Dropdown positioning, forwarded to the inner Dropdown and the ActionButton’s dropdownPosition. |
<script> import ViewToggle from '$ui/actions/ViewToggle.svelte'; import type { ViewMode } from '$lib/client/stores/dataPage';
let view: ViewMode = 'table';</script>
<ViewToggle bind:value={view} position="right" />Events & Slots
Section titled “Events & Slots”- No custom dispatched events; state changes are surfaced by mutating the bindable
valueprop. - No slots exposed to consumers (internally composes
ActionButton+Dropdown+DropdownItemfor Cards/Table).
Variants & Notes
Section titled “Variants & Notes”- Two options: Cards (
LayoutGridicon) and Table (Tableicon); theEyeicon is the trigger, titled and labelled “Choose view”. - Built on
ActionButton(icon={Eye},hasDropdown) with aDropdownlabelled “View” containing twoDropdownItems withrole="menuitemradio". - Selection is reflected by each item’s
selectedstate, exposed asaria-checked. Choosing an option closes the dropdown. Use withbind:valueto observe changes.
SearchAction
Section titled “SearchAction”Search input designed for action bars, backed by a SearchStore. Desktop shows an inline bordered input; in responsive mode on mobile it collapses to a trigger button that opens a full-screen search modal. Supports an active-query badge.
import SearchAction from '$ui/actions/SearchAction.svelte';| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
searchStore |
SearchStore |
— | Yes | Required search store; the component reads $searchStore.query and calls setQuery()/clear(). |
placeholder |
string |
'Search...' |
No | Input placeholder text (suppressed on desktop while an activeQuery badge is shown). |
activeQuery |
string |
'' |
No | An already-applied query shown as an accent Badge inside the field; enables Backspace-to-clear. |
responsive |
boolean |
false |
No | When true, switches to the mobile button+modal presentation on screens below 768px. |
hideIcon |
boolean |
false |
No | Hides the leading Search icon and adjusts input padding accordingly. |
<script> import SearchAction from '$ui/actions/SearchAction.svelte'; import { createSearchStore } from '$lib/client/stores/search';
const search = createSearchStore();
function runSearch(query) { // ... }
function resetFilter() { // ... }</script>
<SearchAction searchStore={search} placeholder="Search components..." responsive on:submit={(e) => runSearch(e.detail)} on:clearQuery={resetFilter}/>Events & Slots
Section titled “Events & Slots”on:submit— dispatches the trimmed query string when Enter is pressed with a non-empty query.on:clearQuery— dispatched (void) when Backspace is pressed on an empty input while anactiveQueryexists, or when theactiveQueryclear button is clicked.- No slots.
Variants & Notes
Section titled “Variants & Notes”- Desktop inline mode vs. mobile modal mode (driven by
responsive+ viewport). - Clear (X) button state varies: clears current query, clears
activeQuery, or closes the modal. createEventDispatcheris typed as{ submit: string; clearQuery: void }.- Registers a
(max-width: 767px)matchMedialistener when responsive; closes the modal automatically when leaving mobile width. - Keydown handling: Enter submits, Backspace on empty clears
activeQuery, Escape closes the modal. - Uses
Badge($ui/badge/Badge.svelte) for the active query chip.
SourceFilterAction
Section titled “SourceFilterAction”Action-bar filter for selecting one or more PCD sources. Renders as inline segmented pill buttons for small source counts, or a labeled dropdown (with active count badge) when there are many sources, on mobile, or when forced. Supports single- and multi-select modes.
import SourceFilterAction from '$ui/actions/SourceFilterAction.svelte';| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
sources |
SourceRef[] |
[] |
No | Available sources; each maps to an option keyed ${source.type}:${source.id} with a Database or Trash2 icon. |
selectedKeys |
SourceFilterKey[] |
[] |
No | Currently selected source keys; normalized against available options. Bindable — updated before each change dispatch. |
selectionMode |
'single' | 'multi' |
'multi' |
No | single=exactly one source active (radio-like); multi=any subset, with an ‘All sources’ option and a last-source guard. |
position |
'left' | 'right' | 'middle' |
'right' |
No | Dropdown menu position (dropdown mode). |
responsive |
boolean |
true |
No | When true, forces dropdown mode on screens below 768px via matchMedia. |
hideWhenSingle |
boolean |
true |
No | When true, the component renders nothing if there is 0 or 1 source option. |
pillsThreshold |
number |
5 |
No | Source count at/above which the inline pills collapse into a dropdown. |
dropdownOnly |
boolean |
false |
No | Forces dropdown presentation regardless of source count or viewport. |
label |
string |
'Sources' |
No | Text label on the dropdown trigger button. |
ariaLabel |
string |
'Filter by source' |
No | Accessible label applied to the group and trigger button/title. |
disabled |
boolean |
false |
No | Disables all interaction (toggle, select-all, dropdown open). |
active |
boolean | undefined |
undefined |
No | Optional override of the computed ‘active’ (filtered) visual state; when undefined it is derived. |
<script> import SourceFilterAction from '$ui/actions/SourceFilterAction.svelte';
let selectedKeys = [];
function applyFilter(selectedSources) { // ... }</script>
<SourceFilterAction {sources} bind:selectedKeys selectionMode="multi" on:change={(e) => applyFilter(e.detail.selectedSources)}/>Events & Slots
Section titled “Events & Slots”on:change— dispatchesSourceFilterChangeDetail { selectedKeys, selectedSources, selectionMode, active }whenever the normalized selection changes.- No slots (internally composes
Dropdown+DropdownItemin dropdown mode; native pill buttons in inline mode).
Variants & Notes
Section titled “Variants & Notes”- Inline segmented pills (
aria-pressedtoggles) vs. dropdown trigger with Filter icon, active count badge (n/total), and ChevronDown. - Multi mode adds an ‘All sources’ item; items use
role="menuitemcheckbox"in multi mode androle="menuitemradio"in single mode. Single mode closes the dropdown on select and returns focus to the trigger. - Uses local types
SourceOptionandSourceFilterChangeDetail; keys are the template-literal type${SourceRef['type']}:${number}. normalizeSelection()filters invalid keys and defaults empty multi-selection to all options / empty single-selection to the first option.computeActiveState()marks the filter active when a strict subset is selected.- Dropdown uses
use:clickOutsideanduse:focusOutsideto close and also closes when the viewport crosses into mobile width; registers a(max-width: 767px)matchMedialistener when responsive. - The dropdown trigger sets
aria-haspopup="menu",aria-expanded, andaria-controls. Keyboard activation focuses the first menu item; Escape (on the trigger or inside the menu) closes it and refocuses the trigger. Arrow-key navigation comes fromDropdown.