@0k/vue-period-picker
@0k/vue-period-picker
Purpose
@0k/vue-period-picker is a compact Vue 3 time-range picker for narrow
sidebars. One full-width input opens a body-appended
vue-datepicker-next range calendar with preset chips, previous/next
controls, a Now button, and a current-period label. Hosts supply presets
and own data fetching, refresh scheduling, routing, and theme values.
Install
npm install @0k/vue-period-picker
Peer dependencies: vue ^3.5.0 and vue3-gettext ^4.0.1.
vue-datepicker-next 1.0.3 is a dependency, external to the package
bundle. The package entry imports its CSS, which the host bundler
resolves: the host imports only @0k/vue-period-picker/style.css
explicitly, in any order relative to the package.
The type declarations use extensionless relative imports: host
TypeScript configurations need "moduleResolution": "bundler", the
setting of Vite projects. The entry's import of the datepicker CSS is a
side-effect import; TypeScript 6 and later projects that check libraries
(skipLibCheck: false) need CSS module types, such as Vite's
vite/client.
Host tests running under Vitest must let Vite process the package, so that its CSS import is resolved rather than handed to Node:
// vite.config.ts (or vitest.config.ts)
test: { server: { deps: { inline: ['@0k/vue-period-picker'] } } }
Usage and gettext
Install the host's gettext plugin once. Merge the package's French messages with host messages by language, not just with a top-level object spread. Host messages take precedence in this example.
// main.ts
import { createApp } from 'vue';
import { createGettext } from 'vue3-gettext';
import { translations as periodTranslations } from '@0k/vue-period-picker';
import '@0k/vue-period-picker/style.css';
import App from './App.vue';
import hostTranslations from './locales/translations.json';
const gettext = createGettext({
availableLanguages: { en: 'English', fr: 'Français' },
defaultLanguage: 'fr',
sourceCodeLanguage: 'en',
translations: {
...periodTranslations,
...hostTranslations,
fr: { ...periodTranslations.fr, ...hostTranslations.fr },
},
});
createApp(App).use(gettext).mount('#app');
<script setup lang="ts">
import { ref } from 'vue';
import { PeriodPicker, type PeriodPreset, type PeriodValue } from '@0k/vue-period-picker';
const period = ref<PeriodValue>({ preset: '6h' });
const presets: PeriodPreset[] = [
{ id: '1h', seconds: 3600 },
{ id: '6h', seconds: 21600 },
{ id: 'quarter', unit: 'quarter' },
{ id: 'year', unit: 'year' },
];
</script>
<template>
<PeriodPicker v-model="period" :presets="presets" />
</template>
The component is also the default export. Every generated string uses
gettext; optional preset label strings are already translated by the
host and override both the short chip text and long input/title label.
Duration chips use compact labels such as 6h (6 h in French); their
title retains the long label, such as Last 6 hours. Singular long
labels are natural: Last hour / Dernière heure, not Last 1 hour.
Calendar month and weekday names (English and French, weeks starting on
Monday) are built into the package and passed to each picker: importing
the package never changes vue-datepicker-next's global default locale,
which other datepickers of the host rely on. Regional language codes
(e.g. fr-FR) select the French calendar; unsupported calendar
languages fall back to English. Hosts using regional gettext keys should
also provide the package messages under that key.
Value contract and presets
All timestamps are Unix seconds, not milliseconds. to is
exclusive.
type CalendarUnit = 'day' | 'week' | 'month' | 'quarter' | 'year';
interface DurationPreset { id: string; seconds: number; label?: string | undefined }
interface CalendarPreset { id: string; unit: CalendarUnit; label?: string | undefined }
type PeriodPreset = DurationPreset | CalendarPreset;
type PeriodValue = { preset: string } | { from: number; to: number };
interface PeriodWindow { start: number; end: number; live: boolean }
- Preset ids must be unique; durations must be positive, finite seconds.
- A duration emits
{ preset: id }and remains live. Each host refresh callsresolvePeriod(period, presets, now)with the current Unix time. The picker does not fetch data or start a host refresh timer. - A calendar chip emits the local calendar unit containing the current window's start, not today's unit. Weeks start Monday; quarters start January, April, July, and October. Calendar selections are fixed ranges.
- Previous/next shifts an exactly calendar-aligned fixed range by its configured calendar unit. Otherwise it shifts by the range's width in seconds. Shifting a live duration produces a fixed range.
- Now returns to the last duration selected by a chip or received
through
modelValue, including the value supplied on mount. Navigation to a fixed range preserves that remembered duration. If no duration has been selected or received, Now uses the first duration preset. It is hidden when no duration presets exist. - Datetime selection applies immediately, has hour/minute columns, and
emits its selected end instant as exclusive. No Apply button is
present. Picks emit whole minutes: seconds carried over from a live
value are dropped, and
min/maxapply to those emitted minutes. - In datetime mode the footer's
Select time/Select datetoggle opens the current range's time panels without requiring new date selections (Choisir l'heure/Choisir la datein French). Selecting both dates opens the time panels automatically; closing resets to the date panel. - Date selection shows an inclusive end day, then emits the following
local midnight as exclusive
to. Conversion uses calendar days across DST, not an assumed 86400-second day. Incoming date-mode ranges display every local day intersecting the range; picking them normalizes to whole days. - Invalid/incomplete selections and
from >= todo not emit. - Calendar bounds disable days with no allowed instant, and datetime bounds disable times outside the bounds. Bounds are inclusive for selection; date-mode choices cover whole intersecting days. Preset and navigation actions are intentionally not clamped to calendar bounds.
Component props and events
| Prop | Type | Default |
|---|---|---|
modelValue |
PeriodValue |
required |
presets |
PeriodPreset[] |
required |
type |
"date" or "datetime" |
'datetime' |
min, max |
number (Unix seconds) |
no bound |
now |
() => number |
floored Date.now/1000 |
Event: update:modelValue(value: PeriodValue), for standard v-model.
modelValue must be valid for presets: like resolvePeriod, the
component throws on a { preset } id that presets does not hold as a
duration preset, or on a range with from >= to. Hosts restoring a
saved value (URL, storage) after changing their preset list should check
it with resolvePeriod first.
Test ids: period-picker, period-prev, period-next, period-now,
period-preset-<id>, period-label, and period-time-toggle (datetime
only).
Pure helpers
resolvePeriod(value, presets, now): PeriodWindow: unknown preset ids and calendar ids used as live preset values throw clear errors.calendarRange(unit, at): { from, to }: local unit boundaries.alignedUnit(range, presets): CalendarUnit | null: only configured calendar units count; largest exact match wins.shiftPeriod(value, presets, now, direction: -1 | 1): { from, to }.formatDuration(seconds, gettext)andformatPreset(preset, gettext).formatPresetShort(preset, gettext): translated compact chip label usings,m,h,d,w,mo, oryin English; French usess,min,h,j,sem,mois, and pluralizedan/ans. Calendar unit labels and host-provided overrides are unchanged.formatPeriod(value, presets, now, type, gettext): generated display label.toPickerRange(range, type): [Date, Date]andfromPickerRange(dates, type): { from, to } | null: picker/API conversion.
PeriodGettext describes the helper's $gettext, $ngettext, and
optional current language field. The helpers are only reachable
through the package entry, which imports Vue, gettext and the
datepicker: like the component, they need a browser (or a DOM such as
jsdom), not plain Node or server rendering. translations exports
compiled locale messages, including French plurals. Durations display
the largest exactly divisible unit; months mean 30 days and years 365
days for duration labels, never for calendar arithmetic.
Theming and popup layout
Set custom properties on :root (or body): the popup teleports to
body, so theme values set only on a sidebar ancestor do not reach it.
:root {
--period-picker-accent: #2563eb;
--period-picker-accent-soft: #e8efff;
--period-picker-border: #cbd0d6;
--period-picker-surface: #fff;
--period-picker-text: #222;
--period-picker-muted: #667085;
--period-picker-radius: 4px;
--period-picker-font-size: 13px;
--period-picker-popup-z-index: 8600;
}
The input fills its container, including 260-320px sidebars. The upstream range calendar always renders two panels: no single-panel range option exists. Above 750px viewport width they sit side by side in a body-appended popup. At 750px and below they stack in a 264px popup, bounded by the viewport with vertical scrolling. Both datetime time panels remain usable; the package corrects upstream's fixed 496px datetime wrapper and 270px height.
Hour/minute columns preserve every minute and keep the popup open while
editing either endpoint. Fixed time-picker-options lists are
deliberately not used: they limit choices and close the popup on
selection. Close by clicking outside.
The datepicker accesses document when imported: the public component
entry is browser-only, not an SSR entry. The package is ESM only, like
its vue3-gettext peer.
Development
npm install
npm run gettext:extract
npm run gettext:compile
npm run check
npm run test:browser
gettext:extract needs the GNU gettext tools (msgmerge, and msginit
for a new language) on the PATH.
To try unreleased changes in a host, run ./autogen.sh, build, then
link the checkout from the host with
npm install --no-save /path/to/vue-period-picker. A linked checkout
brings its own vue3-gettext: add
resolve: { dedupe: ['vue3-gettext'] } to the host's Vite
configuration, or the component fails with "Failed to inject gettext".
./autogen.sh writes the version into the tracked package.json and
package-lock.json: restore their %%version%% placeholder before
committing.
npm run check runs specification tests (in Europe/Paris, then the
calendar tests again in America/Santiago, where DST skips midnight),
strict typechecking, the Vite library build, and test:package. The
build emits dist/index.js, dist/index.d.ts, and dist/style.css,
plus support declarations and maps. test:package checks those
artifacts and creates a real host-like Vite production bundle that
consumes dist/index.js with its dependencies re-bundled, not
externalized. test:browser builds that same production consumer and
serves only its static output, asserting mount, interaction, and zero
console/page errors. It requires Linux, system Chrome and a Python
environment with Playwright. The temporary bundle and owned static HTTP
server are cleaned up after the run; no development-server transform is
used.
French PO, POT, and compiled JSON live under src/locales/. Regenerate
JSON after modifying a catalog. Release scripts are vendored with
pkg vendor. No release or publishing is performed by the check
commands.
License
MIT, see LICENSE.
Changelog
0.1.0 (2026-10-09)
New
-
Provide a compact Vue period picker with presets and time selection. [Valentin Lab]
One input fits a narrow side panel; its popup holds the preset chips, the previous and next buttons, Now, a range calendar and, in
datetimemode, hour and minute columns. Presets are host-configured: live durations ending now (6h) or calendar units (day, week, month, quarter, year) that the previous and next buttons step through. Values are Unix seconds with an exclusiveto;datemode shows an inclusive last day.Labels are English and translated with
vue3-gettext; a French catalog is exported astranslations, and calendar month and weekday names follow the gettext language.
Related Packages
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.1.0 | ^3.5.0 | MIT |
Pre-releases (1)
| Version | Released | License |
|---|---|---|
| 0.0.0-stage |