Skip to content

Customization

This page covers how to customize the admin panel's appearance and behavior.

Theme Customization

The admin panel supports multiple daisyUI themes, changed via the theme selector (ThemeSelector.svelte, packages/tracker-shared/src/components/ThemeSelector.svelte) in the sidebar footer. The theme preference is persisted to local storage (see persistTheme/resolveStoredTheme in tracker-shared/theme) under the key configured by PUBLIC_THEME_STORAGE_KEY, so it carries over across sessions.

Theme Implementation

Themes are daisyUI theme definitions (themeDefinitions in tracker-shared/theme) applied via the data-theme attribute. Because daisyUI themes map semantic Tailwind classes (bg-base-100, text-base-content, bg-primary, and so on) to CSS custom properties per theme, most components never need theme-specific styling — they just use the semantic utility classes and follow whichever theme is active.

Dark Mode Support

Dark/light behavior comes from choosing an appropriate daisyUI theme rather than a single dedicated "dark mode" toggle. The theme picker includes:

  • A theme selector button that opens a popover listing every available theme, with a live color preview swatch for each
  • Automatic detection of the user's system preference on first load (getSystemTheme())
  • Persistent theme preference storage in local storage
  • A checkmark indicating the currently active theme

All shared components are built with daisyUI's semantic color classes so they render correctly regardless of which theme is selected.

Shared Field/Display Components

Rather than framework-specific "field" components, the admin panel and tracker-frontend-svelte share plain Svelte components from packages/tracker-shared/src/components/. The most relevant ones for admin data display are:

  • StatusBadge.svelte: Renders a label with a tone-based color (used for role badges, campaign status, filter chips, and more)
  • EntityColumn.svelte / EntityCard.svelte (in tracker-shared/components/clients/): The building blocks behind the Campaign Management drill-down UI — EntityColumn renders a titled, filterable, add/edit/delete-enabled list of EntityCards, with optional "trackers" and "locations" cross-links per item
  • EntityColumnGroup.svelte: Lays out several EntityColumns side by side on wide viewports and stacks them as scrollable rows on mobile, auto-scrolling to the next column when a selection is made
  • Pagination.svelte, SortableHeaderButton.svelte: Shared pagination controls and sortable column headers used across the users/trackers/locations list views

These components enhance the user experience by providing consistent visual representations of relationships (client → brand → campaign → child run) and enabling easy navigation between related resources — for example, a campaign card exposes direct links to that run's trackers and locations.

Layout Customization

The admin panel's layout is built from shared shell components in tracker-shared/components/:

AppShell

AppShell.svelte defines the overall page structure: a persistent sidebar on desktop (lg: breakpoint and up), a collapsible mobile drawer with a top bar and hamburger button below that, and a max-w-7xl centered content area rendered via a Svelte snippet ({@render children()}).

Sidebar.svelte renders the navigation rail (collapsible to icon-only), the theme selector, and the account menu. The admin panel's navigation items are defined in tracker-admin-svelte/src/routes/(app)/+layout.svelte: Dashboard (/), Campaign Management (/clients), and Users (/users). Tracker and location management are reached by drilling into a campaign or child run from Campaign Management, not via top-level nav items.

Dashboard Layout

The dashboard route (tracker-admin-svelte/src/routes/(app)/+page.svelte) shows:

  • Stat cards (active trackers, trackers moved in the last hour/24h/7d, gone-dark count)
  • A movement calendar heatmap ($lib/components/dashboard/MovementCalendarHeatmap.svelte)

Campaign Management Layout (List Layout Equivalent)

The /clients route uses EntityColumnGroup to show four linked columns — Clients, Brands, Campaigns, and Child runs — each with its own filter input, add button, and pagination (on the Clients column). Selecting an item in one column loads and filters the next.

Detail Layouts

Per-campaign tracker management lives at /trackers/[id] (tracker-admin-svelte/src/routes/(app)/trackers/[id]/+page.svelte), which includes a header with campaign/child-run context, a list/card view toggle, sortable columns, a radial "move" wheel and paste-select for moving trackers between runs, and CSV import/export actions.

Custom Styling

The admin panel uses Tailwind CSS v4 with the daisyUI plugin for styling, which allows for easy customization of component styles without writing custom CSS.

Global Styles

Global styles live in tracker-admin-svelte/src/app.css (Tailwind/daisyUI entry point plus any app-wide base styles).

Component-Specific Styles

Component-specific styles are applied with utility classes directly in each component's markup. This approach allows for:

  • Easy customization of individual components
  • Consistent styling across the application
  • Reduced CSS bundle size
  • Better performance

Responsive Design

The admin panel is responsive down to mobile. For example, the Campaign Management page's four-column layout is side-by-side at the lg breakpoint and stacks into full-width, independently scrollable rows below it (see EntityColumnGroup.svelte), and the sidebar collapses into a slide-out drawer on small screens.

Custom Components

The admin panel can be extended with custom components to add new functionality or modify existing behavior.

Creating Custom Components

To create a custom, admin-only component:

  1. Create a new .svelte file under tracker-admin-svelte/src/lib/components/
  2. Define your component using Svelte 5 runes ($props, $state, $derived) and TypeScript
  3. Import and use it from the relevant route

If the component is generic enough to be useful from tracker-frontend-svelte too, put it in packages/tracker-shared/src/components/ instead.

Example: Custom Display Component

<script lang="ts">
  let {
    label,
    value,
  }: {
    label: string;
    value: string | number;
  } = $props();
</script>

<div class="custom-field flex items-center gap-2 text-sm">
  <span class="font-medium text-base-content/70">{label}:</span>
  <span class="text-base-content">{value}</span>
</div>

Example: Custom Action Button

<script lang="ts">
  import { showToast } from "tracker-shared/state/toast";

  let {
    label,
    onaction,
  }: {
    label: string;
    onaction: () => Promise<void>;
  } = $props();

  let isSubmitting = $state(false);

  async function handleClick(): Promise<void> {
    isSubmitting = true;
    try {
      await onaction();
      showToast("Custom action performed", "success");
    } catch (error) {
      showToast(error instanceof Error ? error.message : "Action failed", "error");
    } finally {
      isSubmitting = false;
    }
  }
</script>

<button type="button" class="btn btn-primary btn-sm" disabled={isSubmitting} onclick={handleClick}>
  {isSubmitting ? "Working..." : label}
</button>

This mirrors the pattern used throughout the app: props (including callback props like onaction) are declared with $props(), local UI state uses $state, and user feedback goes through the shared showToast helper (packages/tracker-shared/src/state/toast.ts) instead of a framework-provided notification hook.