mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-04-18 21:08:05 -04:00
688 lines
22 KiB
Markdown
688 lines
22 KiB
Markdown
# UI Components
|
|
|
|
**Source:** `src/lib/client/ui/`
|
|
**Import alias:** `$ui/*` (maps to `src/lib/client/ui/`)
|
|
**Lint rule:** `scripts/lint/ui.ts` (`deno task lint:ui`)
|
|
|
|
Profilarr ships a small in-house component library that wraps the raw HTML
|
|
primitives the app needs: buttons, inputs, selects, textareas, dialogs, and
|
|
tables. Every page in `src/routes/` is expected to consume these wrappers
|
|
instead of raw elements. Compliance is enforced by a custom Deno lint script
|
|
(`no-raw-ui`) that parses every `.svelte` file with the Svelte 5 compiler and
|
|
flags banned tags.
|
|
|
|
## Table of Contents
|
|
|
|
- [The `no-raw-ui` Lint Rule](#the-no-raw-ui-lint-rule)
|
|
- [Component Catalog](#component-catalog)
|
|
- [Inputs](#inputs)
|
|
- [Buttons and Toggles](#buttons-and-toggles)
|
|
- [Layout and Containers](#layout-and-containers)
|
|
- [Modals and Overlays](#modals-and-overlays)
|
|
- [Data Display](#data-display)
|
|
- [Navigation](#navigation)
|
|
- [Feedback and Info](#feedback-and-info)
|
|
- [Filters](#filters)
|
|
- [Actions Bar](#actions-bar)
|
|
- [Arr-Specific](#arr-specific)
|
|
- [Semantic Tokens and Theming (Planned)](#semantic-tokens-and-theming-planned)
|
|
|
|
## The `no-raw-ui` Lint Rule
|
|
|
|
`scripts/lint/ui.ts` is a script that enforces the "wrappers
|
|
only" convention. It walks the Svelte 5 AST produced by
|
|
`npm:svelte/compiler`, reports each banned tag with a suggested replacement,
|
|
and exits non-zero on any violation.
|
|
|
|
```bash
|
|
deno task lint:ui # run the no-raw-ui rule alone
|
|
```
|
|
|
|
### Exemptions
|
|
|
|
**`<input type="hidden">`** is always allowed. Roughly 140 hidden inputs
|
|
exist in the codebase as SvelteKit form-action plumbing; they are not visible
|
|
UI and wrapping them would add noise. The exemption only applies when `type`
|
|
is a static string literal equal to `"hidden"`. Dynamic `type={foo}`
|
|
expressions are flagged.
|
|
|
|
**Escape-hatch comment.** For the rare case where a wrapper is genuinely
|
|
wrong (or doesn't exist yet), an HTML comment placed immediately before the
|
|
element disables the rule for that line:
|
|
|
|
```svelte
|
|
<!-- lint-disable-next-line no-raw-ui -- svelte-dnd-action needs a real <button> host -->
|
|
<button use:dragHandle>...</button>
|
|
```
|
|
|
|
The directive format is strict:
|
|
|
|
```
|
|
<!-- lint-disable-next-line no-raw-ui -- <non-empty reason> -->
|
|
```
|
|
|
|
A **malformed** directive (missing `--`, empty reason, wrong rule name) is
|
|
itself reported as a violation, so you cannot accidentally "pass" the linter
|
|
by writing a broken directive.
|
|
|
|
## Component Catalog
|
|
|
|
Components are organized by functional category. Each major component (the
|
|
ones that directly replace banned HTML, plus the most heavily used wrappers)
|
|
is documented with three representative call sites. Minor components get a
|
|
one-line description.
|
|
|
|
### Inputs
|
|
|
|
#### FormInput
|
|
|
|
**`$ui/form/FormInput.svelte`** wraps `<input>` (and, with `textarea={true}`,
|
|
`<textarea>`). It owns the label, description text, password visibility
|
|
toggle, monospace mode, and the size scale (`xs` / `sm` / `md` / `lg`).
|
|
|
|
A typical text field:
|
|
|
|
```svelte
|
|
<!-- src/routes/settings/general/+page.svelte:616 -->
|
|
<FormInput
|
|
label="API Read Access Token"
|
|
name="tmdb_api_key"
|
|
value={tmdbApiKey}
|
|
private_
|
|
mono
|
|
placeholder={data.tmdbSettings.hasApiKey ? '••••••••••••••••' : ''}
|
|
description={data.tmdbSettings.hasApiKey
|
|
? 'Leave blank to keep existing key'
|
|
: 'Use the API Read Access Token (not API Key) from themoviedb.org'}
|
|
on:input={(e) => {
|
|
tmdbApiKey = e.detail;
|
|
update('tmdb_api_key', e.detail);
|
|
}}
|
|
/>
|
|
```
|
|
|
|
`private_` turns the input into a password field with an eye toggle; `mono`
|
|
switches to the monospace font stack (useful for API keys and URLs).
|
|
|
|
A plain URL input:
|
|
|
|
```svelte
|
|
<!-- src/routes/settings/general/+page.svelte:659 -->
|
|
<FormInput
|
|
label="API URL"
|
|
name="ai_api_url"
|
|
value={aiApiUrl}
|
|
type="url"
|
|
mono
|
|
description="OpenAI-compatible endpoint (e.g., Ollama: http://localhost:11434/v1)"
|
|
on:input={(e) => {
|
|
aiApiUrl = e.detail;
|
|
update('ai_api_url', e.detail);
|
|
}}
|
|
/>
|
|
```
|
|
|
|
Inside a modal with compact sizing, `FormInput` sheds its label and runs
|
|
flush with surrounding content when `label` is omitted.
|
|
|
|
#### NumberInput
|
|
|
|
**`$ui/form/NumberInput.svelte`** wraps `<input type="number">` and adds
|
|
increment/decrement buttons, min/max validation, and a `compact` mode for
|
|
dense form grids. Typical use: retention days, timeouts, thresholds.
|
|
|
|
```svelte
|
|
<!-- src/routes/settings/general/+page.svelte:472 -->
|
|
<NumberInput
|
|
name="backup_retention_days"
|
|
id="backup_retention_days"
|
|
value={backupRetentionDays}
|
|
min={1}
|
|
max={365}
|
|
onchange={(v) => {
|
|
backupRetentionDays = v;
|
|
update('backup_retention_days', v);
|
|
}}
|
|
/>
|
|
```
|
|
|
|
#### SearchDropdown and DropdownSelect
|
|
|
|
Two related components:
|
|
|
|
- **`$ui/form/SearchDropdown.svelte`** is a searchable combobox. The user
|
|
types to filter, keyboard arrows navigate, Enter selects. Supports fixed
|
|
positioning so it renders above modals and other overlay context.
|
|
- **`$ui/dropdown/DropdownSelect.svelte`** is the plain `<select>` replacement:
|
|
no search box, just click and pick.
|
|
|
|
`DropdownSelect` for a small fixed option list:
|
|
|
|
```svelte
|
|
<!-- src/routes/settings/general/+page.svelte:337 -->
|
|
<DropdownSelect
|
|
value={uiAlertPosition}
|
|
options={alertPositionOptions}
|
|
fullWidth
|
|
fixed
|
|
on:change={(e) => {
|
|
uiAlertPosition = e.detail as AlertPosition;
|
|
update('ui_alert_position', uiAlertPosition);
|
|
}}
|
|
/>
|
|
```
|
|
|
|
`SearchDropdown` used as a sub-component inside `DateInput` to pick a year:
|
|
|
|
```svelte
|
|
<!-- src/lib/client/ui/form/DateInput.svelte:159 -->
|
|
<SearchDropdown
|
|
value={year}
|
|
options={yearOptions}
|
|
label="Year"
|
|
hideLabel
|
|
on:change={(event) => onYearChange(event.detail)}
|
|
/>
|
|
```
|
|
|
|
Use `SearchDropdown` when the option list is long (movies, profiles,
|
|
timezones). Use `DropdownSelect` when it's short and enumerable (log levels,
|
|
sort order).
|
|
|
|
#### MarkdownInput
|
|
|
|
**`$ui/form/MarkdownInput.svelte`** is the `<textarea>` replacement. It adds
|
|
a markdown toolbar, a live preview toggle, and auto-grow behavior. Pass
|
|
`markdown={false}` for plain multi-line text without the toolbar.
|
|
|
|
#### DateInput and TimeInput
|
|
|
|
**`$ui/form/DateInput.svelte`** composes three `SearchDropdown`s
|
|
(month / day / year) with days-in-month validation so February never accepts
|
|
day 30. Value format is `YYYY-MM-DD`.
|
|
|
|
**`$ui/form/TimeInput.svelte`** is two dropdowns (`HH` / `MM`).
|
|
|
|
Both exist because cross-browser native date/time pickers are a usability
|
|
nightmare on mobile.
|
|
|
|
#### TagInput
|
|
|
|
**`$ui/form/TagInput.svelte`** is a chips-style input. Enter adds a tag;
|
|
Backspace on an empty field removes the last one. Supports a `NOT:` prefix
|
|
convention for exclusions (used by custom-format language filters and the
|
|
release-title parser).
|
|
|
|
#### IconCheckbox
|
|
|
|
**`$ui/form/IconCheckbox.svelte`** is a checkbox with an icon baked in and
|
|
configurable colors (hex or CSS variable), plus filled / outline variants.
|
|
Used for qualifiers like "exclude", "required", "boost".
|
|
|
|
#### KeyValueList, RangeScale, CronInput
|
|
|
|
Minor but worth knowing:
|
|
|
|
- **`$ui/form/KeyValueList.svelte`**: dynamic list of string key / string
|
|
value pairs. Add / remove rows; used for header maps and env-var style
|
|
config.
|
|
- **`$ui/form/RangeScale.svelte`**: slider for bounded numeric values
|
|
(score weights, percentages).
|
|
- **`$ui/cron/CronInput.svelte`**: structured cron-expression builder with
|
|
common presets (hourly, daily, weekly) plus a raw-expression escape hatch.
|
|
|
|
### Buttons and Toggles
|
|
|
|
#### Button
|
|
|
|
**`$ui/button/Button.svelte`** is the single replacement for every
|
|
`<button>` and action link in the app. Props cover everything raw buttons
|
|
need: `text`, `icon` (a Lucide component), `variant`
|
|
(`primary` / `secondary` / `danger` / `ghost` / others), `size`
|
|
(`xs` / `sm` / `md` / `lg`), `loading`, `disabled`, `href` (renders as `<a>`),
|
|
and `type` (for form submits). If a `tooltip` prop is provided, the button
|
|
wraps itself in a `Tooltip`.
|
|
|
|
A form-submit button with a loading spinner:
|
|
|
|
```svelte
|
|
<!-- src/routes/settings/general/+page.svelte:270 -->
|
|
<Button
|
|
text={saving ? 'Saving...' : 'Save'}
|
|
icon={saving ? Loader2 : Save}
|
|
iconColor="text-blue-600 dark:text-blue-400"
|
|
loading={saving}
|
|
disabled={saving || !$isDirty}
|
|
type="submit"
|
|
/>
|
|
```
|
|
|
|
A compact icon-only action (alert preview in a section header):
|
|
|
|
```svelte
|
|
<!-- src/routes/settings/general/+page.svelte:290 -->
|
|
<Button
|
|
icon={CheckCircle}
|
|
iconColor="text-green-600 dark:text-green-400"
|
|
size="xs"
|
|
on:click={() => alertStore.add('success', 'Success alert example.')}
|
|
/>
|
|
```
|
|
|
|
A row action inside a table body:
|
|
|
|
```svelte
|
|
<!-- src/routes/settings/logs/+page.svelte:359 -->
|
|
<Button icon={Copy} size="xs" variant="secondary" on:click={() => copyToClipboard(row.message)} />
|
|
```
|
|
|
|
#### Toggle
|
|
|
|
**`$ui/toggle/Toggle.svelte`** is the boolean switch. Emits a `change` event
|
|
with `detail` being the new boolean. Supports inline label text, a
|
|
`fullWidth` mode for grid cells, and custom colors.
|
|
|
|
```svelte
|
|
<!-- src/routes/settings/general/+page.svelte:322 -->
|
|
<Toggle
|
|
label={uiNavIconStyle === 'emoji' ? 'Enabled' : 'Disabled'}
|
|
checked={uiNavIconStyle === 'emoji'}
|
|
fullWidth
|
|
on:change={(e) => {
|
|
uiNavIconStyle = e.detail ? 'emoji' : 'lucide';
|
|
update('ui_nav_icon_style', uiNavIconStyle);
|
|
}}
|
|
/>
|
|
```
|
|
|
|
The toggle is almost always paired with a matching `<input type="hidden">`
|
|
so the form action receives a value for unchecked state (unchecked
|
|
checkboxes submit nothing by default):
|
|
|
|
```svelte
|
|
<Toggle checked={backupEnabled} on:change={...} />
|
|
<input type="hidden" name="backup_enabled" value={backupEnabled ? 'on' : ''} />
|
|
```
|
|
|
|
Hidden inputs are exempt from the `no-raw-ui` rule specifically to support
|
|
this pattern.
|
|
|
|
And a standalone toggle that just drives local state:
|
|
|
|
```svelte
|
|
<!-- src/routes/settings/general/+page.svelte:648 -->
|
|
<Toggle
|
|
label="Enable AI Features"
|
|
checked={aiEnabled}
|
|
on:change={(e) => {
|
|
aiEnabled = e.detail;
|
|
update('ai_enabled', e.detail);
|
|
}}
|
|
/>
|
|
```
|
|
|
|
#### SelectableContainer and SelectableRow
|
|
|
|
**`$ui/toggle/SelectableContainer.svelte`** + **`SelectableRow.svelte`** are
|
|
the bulk-selection pair: a container that manages selection state across a
|
|
list of rows, each row showing a checkbox indicator. Used in the import
|
|
modal and release-selection flows.
|
|
|
|
### Layout and Containers
|
|
|
|
#### Card, CardGrid, ExpandableCard, StickyCard
|
|
|
|
The card family is the primary content container:
|
|
|
|
- **`$ui/card/Card.svelte`** is a surface with padding, optional hover,
|
|
optional `href` (renders as a link), and a `flush` mode that removes
|
|
internal padding when the card is used as a grid child.
|
|
- **`$ui/card/CardGrid.svelte`** is a responsive grid (1 / 2 / 3 / 4 / 5
|
|
column breakpoints) that lays cards out.
|
|
- **`$ui/card/ExpandableCard.svelte`** is a card with a collapsible body
|
|
and a header that supports a right-slot for inline actions.
|
|
- **`$ui/card/StickyCard.svelte`** is a card that pins to the bottom of
|
|
the viewport, typically used as a save/discard action bar.
|
|
|
|
A settings page uses `ExpandableCard` extensively to group related fields
|
|
under collapsible sections:
|
|
|
|
```svelte
|
|
<!-- src/routes/settings/general/+page.svelte:282 -->
|
|
<ExpandableCard
|
|
title="Interface"
|
|
description="Customize the look and feel of the application"
|
|
>
|
|
<svelte:fragment slot="header-right">
|
|
<div class="flex items-center gap-1" on:click|stopPropagation>
|
|
<Button icon={CheckCircle} size="xs" on:click={...} />
|
|
<!-- more header-right actions -->
|
|
</div>
|
|
</svelte:fragment>
|
|
<div class="px-6 py-4">
|
|
<!-- section content -->
|
|
</div>
|
|
</ExpandableCard>
|
|
```
|
|
|
|
A `Card` as a filter-group container:
|
|
|
|
```svelte
|
|
<!-- src/routes/arr/[id]/upgrades/components/FilterGroup.svelte:68 -->
|
|
<Card padding="md" flush={depth === 0}>
|
|
<div class="mb-3 flex items-center justify-between">
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-xs font-medium">Match</span>
|
|
<DropdownSelect value={group.match} options={matchOptions} />
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
```
|
|
|
|
A `CardGrid` of repository-style entity tiles is the standard layout for
|
|
list pages (databases, profiles, regex library, custom formats).
|
|
|
|
#### Dropdown family
|
|
|
|
Sub-components for building custom menu UIs:
|
|
|
|
- **`Dropdown.svelte`**: positioning container with fixed-positioning and
|
|
hover-bridge support.
|
|
- **`DropdownHeader.svelte`**, **`DropdownFooter.svelte`**,
|
|
**`DropdownItem.svelte`**: styled section header, footer, and menu item.
|
|
`DropdownFooter` mirrors `DropdownHeader` but uses `border-top` instead
|
|
of `border-bottom` and sits at the end of a menu (e.g. "and 12 more").
|
|
- **`DropdownSelect.svelte`**: the `<select>` replacement (covered under
|
|
[Inputs](#searchdropdown-and-dropdownselect)).
|
|
- **`CustomGroupManager.svelte`**: specialized dropdown used by the custom
|
|
format group editor.
|
|
|
|
Most route code consumes `DropdownSelect`; the lower-level pieces are for
|
|
building bespoke menus.
|
|
|
|
#### DraggableCard
|
|
|
|
**`$ui/list/DraggableCard.svelte`** wraps a card with the drag-and-drop
|
|
affordance used for reordering ordered collections (profile qualities,
|
|
custom format conditions).
|
|
|
|
### Modals and Overlays
|
|
|
|
#### Modal
|
|
|
|
**`$ui/modal/Modal.svelte`** is the `<dialog>` replacement. It owns
|
|
overlay / backdrop handling, Escape-to-close, focus management, and
|
|
scroll-lock on `<body>`. Body content is a slot; footer actions can be the
|
|
built-in confirm / cancel pair (via `confirmText` / `cancelText` /
|
|
`confirmDanger` props) or custom markup via the footer slot.
|
|
|
|
A confirm-to-delete modal with built-in footer:
|
|
|
|
```svelte
|
|
<!-- src/routes/databases/components/InstanceForm.svelte:506 -->
|
|
<Modal
|
|
open={showDeleteModal}
|
|
header="Unlink Database"
|
|
bodyMessage={`Are you sure you want to unlink "${instance?.name}"? This action cannot be undone and all local data will be permanently removed.`}
|
|
confirmText="Unlink"
|
|
cancelText="Cancel"
|
|
confirmDanger={true}
|
|
on:confirm={() => {
|
|
showDeleteModal = false;
|
|
const deleteForm = document.getElementById('delete-form');
|
|
if (deleteForm instanceof HTMLFormElement) {
|
|
deleteForm.requestSubmit();
|
|
}
|
|
}}
|
|
/>
|
|
```
|
|
|
|
A multi-step import modal with custom body content (opens in
|
|
`ImportReleasesModal` around line 342) uses `<div slot="body">` with
|
|
conditional step rendering driven by a local `step` variable.
|
|
|
|
An add-entity modal that contains a `SearchDropdown` for picking between
|
|
existing entities is at
|
|
`src/routes/quality-profiles/entity-testing/[databaseId]/components/AddEntityModal.svelte`.
|
|
|
|
#### DirtyModal, InfoModal, ImportModal, CloneModal, SyncPromptModal
|
|
|
|
Pre-composed modals for recurring flows:
|
|
|
|
- **`DirtyModal.svelte`** is the nav-guard prompt wired to the dirty store;
|
|
drop one instance per form and it auto-triggers. See
|
|
[`dirty.md`](./dirty.md) for details.
|
|
- **`InfoModal.svelte`** is a single-action alert ("OK") for success and
|
|
info messages.
|
|
- **`ImportModal.svelte`**, **`CloneModal.svelte`**,
|
|
**`SyncPromptModal.svelte`** are specialized wrappers for those specific
|
|
flows (DB import, entity clone, sync prompt).
|
|
|
|
### Data Display
|
|
|
|
#### Table and ExpandableTable
|
|
|
|
**`$ui/table/Table.svelte`** is the `<table>` replacement. It owns sortable
|
|
headers, hover rows, responsive card fallback on narrow viewports, an empty
|
|
state, progressive loading, page size, and a per-row `actions` slot. Columns
|
|
are described declaratively.
|
|
|
|
A log table with sort, hover, compact rows, and row actions:
|
|
|
|
```svelte
|
|
<!-- src/routes/settings/logs/+page.svelte:347 -->
|
|
<Table
|
|
data={paginatedLogs}
|
|
{columns}
|
|
emptyMessage="No logs found"
|
|
hoverable={true}
|
|
compact={true}
|
|
responsive
|
|
initialSort={{ key: 'timestamp', direction: defaultSortDirection }}
|
|
onSortChange={handleSortChange}
|
|
>
|
|
<svelte:fragment slot="actions" let:row>
|
|
<div class="flex items-center justify-end gap-1">
|
|
<Button icon={Copy} size="xs" variant="secondary" on:click={...} />
|
|
</div>
|
|
</svelte:fragment>
|
|
</Table>
|
|
```
|
|
|
|
Additional call sites worth skimming:
|
|
|
|
- `src/routes/settings/security/+page.svelte`: activity log with dense rows.
|
|
- `src/routes/databases/[id]/conflicts/+page.svelte`: conflict table that
|
|
pairs sort with an expand-detail slot.
|
|
- `src/routes/arr/[id]/logs/+page.svelte`: arr-specific log table mirroring
|
|
the Profilarr logs layout.
|
|
|
|
**`$ui/table/ExpandableTable.svelte`** extends `Table` with a per-row
|
|
expand region (click a chevron, get a nested detail view). Used in
|
|
entity-testing tables where each row has nested per-release detail.
|
|
|
|
**`$ui/table/TableActionButton.svelte`** is a button styled to match the
|
|
dense vertical rhythm of table action cells; prefer it over `Button` when
|
|
the button lives in a table column.
|
|
|
|
#### Badge
|
|
|
|
**`$ui/badge/Badge.svelte`** is an inline pill label. Variants cover the
|
|
usual palette (`accent` / `neutral` / `success` / `warning` / `danger` /
|
|
`info`) plus product-specific (`radarr` / `sonarr`). Supports an inline
|
|
icon and `mono` text.
|
|
|
|
#### Label
|
|
|
|
**`$ui/label/Label.svelte`** is the lighter-weight text label; use it for
|
|
inline tags that don't need a badge's visual weight.
|
|
|
|
#### CodeBlock and InlineCode
|
|
|
|
- **`$ui/code/CodeBlock.svelte`**: syntax-highlighted multi-line code with
|
|
theme selection and a copy button.
|
|
- **`$ui/code/InlineCode.svelte`**: inline monospace span for references in
|
|
prose.
|
|
|
|
A second `CodeBlock` exists at `$ui/display/CodeBlock.svelte` alongside
|
|
`$ui/display/Markdown.svelte`; the `display/` variants are used by the
|
|
help / docs surface while `code/` is the general-purpose block. Treat the
|
|
`code/` pair as the canonical entry point for now.
|
|
|
|
### Navigation
|
|
|
|
The navigation family is consumed mostly by the app shell (root layout),
|
|
not by individual routes.
|
|
|
|
- **`navigation/navbar/navbar.svelte`**: top bar with logo,
|
|
`themeToggle.svelte`, and `accentPicker.svelte`.
|
|
- **`navigation/pageNav/pageNav.svelte`**: sidebar navigation composed of
|
|
`group.svelte`, `groupHeader.svelte`, `groupItem.svelte`, plus
|
|
`jobStatus.svelte` and `version.svelte` footer widgets.
|
|
- **`navigation/bottomNav/BottomNav.svelte`**: mobile bottom tab bar.
|
|
|
|
Route-level navigation primitives:
|
|
|
|
- **`navigation/tabs/Tabs.svelte`**: horizontal tab bar with a breadcrumb
|
|
and an optional back button. Tabs are described as `{ label, href, active,
|
|
icon }` objects.
|
|
|
|
```svelte
|
|
<!-- src/routes/regular-expressions/[databaseId]/+page.svelte:148 -->
|
|
<Tabs {tabs} responsive />
|
|
```
|
|
|
|
- **`navigation/breadcrumb/Breadcrumb.svelte`**: standalone breadcrumb for
|
|
pages that don't need tabs.
|
|
- **`navigation/pagination/Pagination.svelte`**: page-number pagination
|
|
consumed by tables and card grids.
|
|
|
|
#### InlineLink
|
|
|
|
**`$ui/link/InlineLink.svelte`** is a lightweight inline `<a>` for text-level
|
|
links within content. It renders as accent-colored text with an underline on
|
|
hover. Use it to link entity names to their detail pages.
|
|
|
|
Props: `href` (required), `text` (required), `external` (boolean, defaults to
|
|
false; adds `target="_blank" rel="noopener noreferrer"`).
|
|
|
|
```svelte
|
|
<!-- src/routes/quality-profiles/[databaseId]/[id]/scoring/components/ScoringTableDesktop.svelte:87 -->
|
|
<InlineLink href="/custom-formats/{databaseId}/{row.id}/general" text={row.name} external />
|
|
```
|
|
|
|
### Feedback and Info
|
|
|
|
#### Tooltip
|
|
|
|
**`$ui/tooltip/Tooltip.svelte`** wraps any child in a hover-activated
|
|
tooltip. It handles viewport edge detection (flipping position when the
|
|
tooltip would overflow) and supports a `fullWidth` mode for stretching to
|
|
fit a parent container.
|
|
|
|
A badge with conditional tooltip text:
|
|
|
|
```svelte
|
|
<!-- src/routes/arr/[id]/library/components/MovieCard.svelte:107 -->
|
|
<Tooltip text={movie.isProfilarrProfile ? '' : 'Not managed by Profilarr'} position="top">
|
|
<Badge
|
|
variant={movie.isProfilarrProfile ? 'accent' : 'warning'}
|
|
icon={movie.isProfilarrProfile ? null : CircleAlert}
|
|
mono
|
|
/>
|
|
</Tooltip>
|
|
```
|
|
|
|
Empty `text` disables the tooltip without an `{#if}` wrapper. The tooltip
|
|
is also baked into `Button` via the `tooltip` prop; prefer that over
|
|
manually wrapping a `<Button>`.
|
|
|
|
#### EmptyState
|
|
|
|
**`$ui/state/EmptyState.svelte`** is the "no data yet" empty-view surface:
|
|
icon, title, description, plus an optional CTA button (with `buttonText`,
|
|
`buttonHref`, `buttonIcon`, `onboarding` hook).
|
|
|
|
```svelte
|
|
<!-- src/routes/databases/+page.svelte:49 -->
|
|
<EmptyState
|
|
icon={Database}
|
|
title="No Databases Linked"
|
|
description="Link a Profilarr Compliant Database to get started with profile management."
|
|
buttonText="Link Database"
|
|
buttonHref="/databases/new"
|
|
buttonIcon={Plus}
|
|
onboarding="db-add"
|
|
/>
|
|
```
|
|
|
|
Used on every list page when the underlying collection is empty. The
|
|
`onboarding` prop ties the CTA into the cutscene system (see
|
|
[`./cutscene.md`](./cutscene.md)).
|
|
|
|
#### HelpButton
|
|
|
|
**`$ui/help/HelpButton.svelte`** is the floating / navbar help button that
|
|
opens the help modal. Variants: `fab` (floating) or `navbar` (inline).
|
|
|
|
### Filters
|
|
|
|
- **`$ui/filter/FilterTag.svelte`**: active-filter chip with a clear button.
|
|
- **`$ui/filter/SmartFilterBar.svelte`**: multi-facet filter toolbar that
|
|
combines search, filter chips, and saved-filter presets. Used on
|
|
list-heavy pages (arr library, custom formats).
|
|
|
|
### Actions Bar
|
|
|
|
The `actions/` family builds merged-border toolbar groups, typically at the
|
|
top of a list page:
|
|
|
|
- **`ActionsBar.svelte`**: horizontal container that merges children's
|
|
borders so they look like a single unit.
|
|
- **`ActionButton.svelte`**, **`ActionInput.svelte`**: button and input
|
|
styled to fit inside the bar.
|
|
- **`SearchAction.svelte`**: search field optimized for the bar (with
|
|
clear / submit handling).
|
|
- **`SearchFilterAction.svelte`**: filter button with an indicator for
|
|
active filters.
|
|
- **`SearchModeToggle.svelte`**: toggle between search modes (e.g.,
|
|
"fuzzy" vs. "exact").
|
|
- **`ViewToggle.svelte`**: list / grid view switcher used on pages that
|
|
support both layouts.
|
|
|
|
### Arr-Specific
|
|
|
|
Components scoped to Radarr / Sonarr semantics rather than generic UI:
|
|
|
|
- **`$ui/arr/Score.svelte`**: custom-format score display with positive /
|
|
negative coloring.
|
|
- **`$ui/arr/CustomFormatBadge.svelte`**: badge styled specifically for
|
|
custom formats (tight sizing, color-coded by score).
|
|
- **`$ui/arr/ProgressIndicator.svelte`**: progress bar for long-running
|
|
arr operations (sync, bulk upgrade).
|
|
|
|
## Semantic Tokens and Theming (Planned)
|
|
|
|
**Status:** not started. Tracked in
|
|
[issue #298](https://github.com/Dictionarry-Hub/profilarr/issues/298).
|
|
|
|
Today, every component in `src/lib/client/ui/` ships with hardcoded
|
|
Tailwind palette classes paired with `dark:` variants:
|
|
|
|
```svelte
|
|
<button class="bg-white border-neutral-300 text-neutral-700
|
|
dark:bg-neutral-800 dark:border-neutral-700 dark:text-neutral-200">
|
|
```
|
|
|
|
This works, but it pins the app to exactly two themes (light and dark) and
|
|
duplicates every color decision across two class strings.
|
|
|
|
The plan is to migrate to **semantic CSS variable tokens** so components
|
|
reference intent-level names (`bg-surface`, `text-text`, `border-border`)
|
|
instead of palette values. Each theme then lives in its own CSS file and
|
|
swaps the token values:
|
|
|
|
```svelte
|
|
<button class="bg-surface border-border text-text">
|
|
```
|