Period filtering
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 aComputedRef<PeriodState>underidand returns a cleanup — call it inonBeforeUnmount. Registering the sameidfrom several producers is safe: the entry lives until the last one unregisters.usePeriodScope(id)returns the producer's current state, ornullwhenidis 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 aPeriodSelectorwith thatid.appendPeriodToUrl(url, state)serializes the state onto a URL as query params: alwaysfrom,to,preset, andcomparison(ISO dates), pluscompareFrom/compareTowhen a compare range is set. With a missingurlorstateit returnsurlunchanged.
PeriodState
The resolved, serializable value that flows from producer to consumer:
| Field | Type | Meaning |
|---|---|---|
preset | PeriodPreset | The chosen preset (or "custom"). |
comparison | PeriodComparison | The chosen comparison mode. |
range | { from: Date; to: Date } | The resolved primary date range. |
compareRange | { from: Date; to: Date } | null | The resolved comparison range, or null when comparison is none. |
key | string | A 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.
| Preset | Resolved range | In default menu |
|---|---|---|
last-hour | Exactly 60 minutes ending now — not snapped to a day | — |
last-24h | Exactly 24 hours ending now — not snapped to a day | — |
today | Start to end of today | — |
yesterday | The full previous day | — |
last-7-days | 7 days ending today | yes |
last-30-days | 30 days ending today | yes |
last-90-days | 90 days ending today | — |
this-month | 1st of this month → end of month | yes |
last-month | The full previous calendar month | yes |
this-quarter | Start → end of the current quarter | yes |
last-quarter | The full previous quarter | yes |
ytd | Jan 1 of this year → end of today | yes |
last-year | The full previous calendar year | yes |
custom | The range set via setCustomRange (fallback: last 30 days) | — |
| Comparison | Resolved compare range | In default menu |
|---|---|---|
none | null (no comparison) | yes |
previous-period | The equal-length span immediately before range, one millisecond clear of it | yes |
previous-year | The same dates one calendar year earlier | yes |
custom | The 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:
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-imported | Kind |
|---|---|
usePeriod, usePeriodScope, registerPeriodScope, appendPeriodToUrl | functions |
PeriodState, PeriodRange, PeriodPreset, PeriodComparison | types |
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:
| Member | Kind | Purpose |
|---|---|---|
preset, comparison | Ref | The selected preset / comparison literals. |
customRange, customCompareRange | Ref<PeriodRange | null> | User-picked ranges backing the custom modes. |
presets, comparisons | ComputedRef<[]> | The offered options (from options or the defaults). |
state | ComputedRef<PeriodState> | The resolved state to hand to registerPeriodScope. |
setPreset, setComparison | setters | Switch preset / comparison. |
setCustomRange, setCustomCompareRange | setters | Set 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:
<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:
<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.