[data-reveal]{opacity:1!important;transform:none!important}
Extending The Dashboard

Period filtering

The producer / consumer / registry that links a PeriodSelector to data widgets by a shared string scope id.

A dashboard date-range is shared, not passed. A PeriodSelector resolves the chosen preset and comparison into a PeriodState and publishes it under a string id; every widget naming that same string in its periodScope reads the state and refetches when it changes. A client-side registry keyed by id mediates between them — no provide/inject, no prop drilling, producer and consumers anywhere on the page.

This page covers the linking mechanism; the page-level components — PeriodSelector and the chart/card widgets — are in Charts & widgets.

The contract

const cleanup = registerPeriodScope(id, state); // producer side
const scope = usePeriodScope(id); // consumer side → ComputedRef<PeriodState | null>
const url = appendPeriodToUrl(baseUrl, scope.value); // fold state into a request
  • registerPeriodScope(id, state) registers a ComputedRef<PeriodState> under id and returns a cleanup — call it in onBeforeUnmount. Registering the same id from several producers is safe: the entry lives until the last one unregisters.
  • usePeriodScope(id) returns the producer's current state, or null when id is falsy or no producer is registered. The lookup is reactive, so a consumer rendered before its producer updates automatically once the selector registers. A lookup that stays unmatched logs a one-shot console warning (after 250 ms, once per id) telling you to add a PeriodSelector with that id.
  • appendPeriodToUrl(url, state) serializes the state onto a URL as query params: always from, to, preset, and comparison (ISO dates), plus compareFrom / compareTo when a compare range is set. With a missing url or state it returns url unchanged.
The registry is client-only — registering is a no-op during SSR. A scoped widget therefore defers its first fetch to client mount.

PeriodState

The resolved, serializable value that flows from producer to consumer:

FieldTypeMeaning
presetPeriodPresetThe chosen preset (or "custom").
comparisonPeriodComparisonThe chosen comparison mode.
range{ from: Date; to: Date }The resolved primary date range.
compareRange{ from: Date; to: Date } | nullThe resolved comparison range, or null when comparison is none.
keystringA stable digest of all of the above.

Consumers watch key rather than the object, so a refetch fires once per real change and not on every re-render.

Presets & comparisons

preset and comparison are closed unions — there is no runtime "register a preset" hook; the DMS resolves each literal's range internally. Calendar presets use day boundaries (start-of-day to end-of-day) and day-count presets include today; the two relative presets are the exception and keep instant bounds, so an ops dashboard can look at the last hour rather than the last calendar day. While a relative preset is active its published state re-resolves every minute, so the window actually slides and bound widgets refetch; calendar presets resolve once per selection.

PresetResolved rangeIn default menu
last-hourExactly 60 minutes ending now — not snapped to a day
last-24hExactly 24 hours ending now — not snapped to a day
todayStart to end of today
yesterdayThe full previous day
last-7-days7 days ending todayyes
last-30-days30 days ending todayyes
last-90-days90 days ending today
this-month1st of this month → end of monthyes
last-monthThe full previous calendar monthyes
this-quarterStart → end of the current quarteryes
last-quarterThe full previous quarteryes
ytdJan 1 of this year → end of todayyes
last-yearThe full previous calendar yearyes
customThe range set via setCustomRange (fallback: last 30 days)
ComparisonResolved compare rangeIn default menu
nonenull (no comparison)yes
previous-periodThe equal-length span immediately before range, one millisecond clear of ityes
previous-yearThe same dates one calendar year earlieryes
customThe range set via setCustomCompareRange

The defaults are this-month and none. The relative presets, today, yesterday, last-90-days and the two custom literals are valid states but only offered when you pass them in presets / comparisons — the default menu stays the business-reporting set.

Wiring a selector to widgets

At the page-builder level you never touch the registry — you set two matching strings. Give the PeriodSelector an id, and give each widget the same value as its periodScope:

src/pages/dashboard.ts
import { PeriodSelector } from "@antelopejs-private/cms/interfaces/cms-base/period-selector";
import { ChartCard } from "@antelopejs-private/cms/interfaces/cms-base/chart-card";
import { KpiCard } from "@antelopejs-private/cms/interfaces/cms-base/kpi-card";
import { ChartArea } from "@antelopejs-private/cms/interfaces/cms-base/chart";

// Producer: publishes a PeriodState under "sales"
PeriodSelector({ id: "sales", defaultPreset: "this-month" });

// Consumers: read the same scope and refetch on change
KpiCard({ title: "Revenue", fetchUrl: "/dashboard/revenue", periodScope: "sales" });
ChartCard({
  title: "Sales",
  fetchUrl: "/dashboard/sales",
  periodScope: "sales",
  chart: ChartArea({ smooth: true }),
});

Under the hood each widget's fetch watches the scope's key, appends the range and comparison params to its fetchUrl, and refetches on change (debounced — the first fetch runs immediately). While a periodScope is set but unresolved the fetch is skipped: a typo'd id leaves the widget empty, plus the console warning, until a selector with that id mounts.

Use multiple distinct ids on one page to drive disjoint widget groups independently; reuse one id to move a group together. A widget with no periodScope ignores the registry entirely and fetches its raw fetchUrl.

Public layer API

Everything on this page is part of the layer's public surface, auto-imported in any frontend layer — your module's components included. No import statement, no deep path:

Auto-importedKind
usePeriod, usePeriodScope, registerPeriodScope, appendPeriodToUrlfunctions
PeriodState, PeriodRange, PeriodPreset, PeriodComparisontypes
DEFAULT_PRESETS, DEFAULT_COMPARISONS, MS_PER_HOUR, MS_PER_DAY, END_OF_DAY_*constants

That is what lets a module own a period scope without a PeriodSelector anywhere on the page — see the section below.

usePeriod

When the stock PeriodSelector UI doesn't fit, build your own control on usePeriod — the reactive state machine behind it. It owns the preset/comparison/custom refs and derives the resolved state you publish.

usePeriod(options?) returns:

MemberKindPurpose
preset, comparisonRefThe selected preset / comparison literals.
customRange, customCompareRangeRef<PeriodRange | null>User-picked ranges backing the custom modes.
presets, comparisonsComputedRef<[]>The offered options (from options or the defaults).
stateComputedRef<PeriodState>The resolved state to hand to registerPeriodScope.
setPreset, setComparisonsettersSwitch preset / comparison.
setCustomRange, setCustomCompareRangesettersSet a custom range; a non-null value auto-flips preset / comparison to "custom".

options accepts defaultPreset, defaultComparison, presets, and comparisons (the last two narrow the offered menus).

To act as a producer, register state and clean up on unmount — exactly what PeriodSelector does:

app/components/MyPeriodControl.vue
<script setup lang="ts">
import { onBeforeUnmount } from "vue";

const props = defineProps<{ id: string }>();
const period = usePeriod({ defaultPreset: "this-quarter" });

// Publish under the scope id, and release it on unmount.
const cleanup = registerPeriodScope(props.id, period.state);
onBeforeUnmount(cleanup);
</script>

<template>
  <button @click="period.setPreset('last-month')">Last month</button>
  <button @click="period.setComparison('previous-year')">vs last year</button>
</template>

Any widget pointing its periodScope at props.id now follows your control just as it would the stock selector.

A module control is free to bypass presets entirely and publish arbitrary ranges through setCustomRange — a billing-month picker with its own labels, for instance:

app/components/BillingMonthPicker.vue
<script setup lang="ts">
const props = defineProps<{ scopeId: string }>();

const period = usePeriod();
const cleanup = registerPeriodScope(props.scopeId, period.state);
onBeforeUnmount(cleanup);

function selectMonth(range: PeriodRange) {
  period.setCustomRange(range);
  period.setComparison("previous-period");
}
</script>

Mount it with CustomComponent("BillingMonthPicker").options({ scopeId: "billing" }) and give the surrounding KpiCards periodScope: "billing"; they pick up the range and their comparison deltas through the same registry as any other scope. The playground's PeriodSelector Variants page runs exactly this setup.