@feugene/granularity-chrono

v0.10.02026-08-27

Changed

  • Peer floors on @feugene/* raised to the current minor. Every peer this package declares on the ecosystem now starts at the version the monorepo actually ships:

    • @feugene/fint-i18n>=0.7.0 <1.0.0
    • @feugene/granularity>=0.36.0 <1.0.0
    • @feugene/unocss-preset-granular>=0.13.0 <1.0.0
    • @feugene/unplugin-granularity>=0.7.0 <1.0.0

    The floors had drifted far behind — some still admitted releases from a year of development ago — and a range that claims support it was never tested against is worse than a narrow one: the install succeeds and the breakage surfaces later, in the consumer’s app.

    This is breaking for anyone below a floor. Installing against an older @feugene/granularity now produces a peer conflict instead of silence. The fix is to move the core up; nothing in this package’s own API changed.

v0.9.12026-08-23

Fixed

  • The i18n docblock named the wrong block. It said keys live under gr, «like the core», while the package declares grChrono (GR_CHRONO_I18N_BLOCK) and everything else — README, loaders, the integration gate — agrees with that. A consumer who trusted the comment registered gr and got no chrono translations, with a failure quiet enough to miss: the calendar grid still reads correctly because it comes from Intl, only panel labels and aria-labels stay English.

v0.9.02026-08-20

Added

  • Manual input in GrDateTimePicker and GrDateRangePicker. editable now works on all four pickers. One string describes two values here — a date with a time, or two bounds — and that is a parser of its own, which is why these two came last.

    Parsing is driven by Intl, not by a pattern string. Which half of the string is the date and which is the time, in what order the parts come and what separates them is known by the locale: vi puts the time first (15:30 12/8/26), ko puts the day period before the hour (오후 3:30), en-US after it. Separators themselves are never matched — digit groups are counted instead. That is what makes en-CA, where the date is itself written with hyphens (2026-08-12 - 2026-08-14), split correctly; a list of range separators would have broken on the first such locale.

    For the range, the string is split in half by digit-group count and each half is parsed as a bound. Reversed order is normalised, exactly as clicking backwards is. A single date is rejected: one date is not a period, and inventing the other bound would make up something the user never entered. Time is accepted precisely when the picker shows it — required on both bounds with enable-time, refused without it. A parsed but disallowed period (disabledDates, min, max, minRange, maxRange) is not applied and is announced: the input was well formed, and silence would read as a lost Enter.

    For the datetime picker, a bare date keeps the model’s time — what was not typed does not change. Typed text commits the model directly, bypassing autoApply: that prop governs the panel, where selection is multi-step, while Enter in the field is already a finished action.

    Neither field masks input: a mask would have to guess where the date ends and the time begins, and fight the caret on deletion.

  • The panel follows what is being typed. In all four editable pickers, a fully typed date is highlighted in the grid and the grid moves to its month; a typed hour is highlighted in the hours column, minutes in the minutes column. The model is untouched until Enter or blur — this is a preview, not a commit. Parts that were not typed keep their current value.

    Typing into a panel that still shows the previous value is exactly the blind spot a text field was supposed to remove, so the draft is parsed on every keystroke: completely for the date, part by part for the time.

Fixed

  • An editable field now shows what it can parse back. With editable, values are rendered as digits (08/12/2026) instead of Aug 12, 2026. Editing a number in place used to leave the parser two digit groups instead of three, so the edit was silently rolled back although the user had done nothing wrong. An explicit format still wins. This also affects GrDatePicker, where the same defect had been present since editable was introduced.
  • A disallowed date is no longer accepted as text. disabledDates, min and max now apply to Enter in the field the same way they apply to a click, in all three editable pickers. The grid refuses such a day; the field used to take it.
  • A time column now scrolls to a selection that changed while it was open. Scrolling happened only when the panel opened, so a value set from anywhere else — typing in the field, an external v-model write — stayed off-screen and the highlight was useless. Jumping to a new value also centres it instead of stopping at the nearest edge: an hour pinned to the bottom of the column reads as “nothing below”. Stepping with arrow keys still stops at the edge, so the list does not jump under the hand.
  • Clicking an editable field no longer moves focus into the panel. The panel opens, as before, but focus stays where the user put it: they came to type. Keyboard opening () still moves into the grid — that is what it means.
  • Quick ranges honour enable-time. A footer preset committed two midnights while the same two dates picked by hand got 00:00 and the end of day, so “last 7 days” quietly dropped the final day.

v0.8.02026-08-20

Added

  • enable-time on GrDateRangePicker — a window, not two midnights. A shift from 8:00 Monday to 20:00 Wednesday, a maintenance window, a two-day room booking: until now this had to be assembled from separate fields, because the range picker gave two midnights and the datetime picker a single moment.

    A freshly picked pair gets 00:00 and 23:59 — the whole span of days. The end lands on the same grid as the columns, so minute-step="15" gives 23:45: otherwise the minutes column would have no selected option and the end could not be read where it is edited. At the default step of a minute it is exactly 23:59. Two midnights would look symmetrical, but “1 to 3 August” means all of the third to a person, and [1 Aug 00:00, 3 Aug 00:00) quietly drops almost all of it: the classic reporting bug. With enable-seconds the end gets 23:59:59. Both values are visible and editable.

    No four-step wizard, which is what the spec had assumed this feature would need. Dates are picked with the same two clicks as before and the times are edited whenever, in any order. Stepping would introduce hidden “which step are we on” state that a person who mis-clicks can only escape by walking the whole thing again.

    The panel no longer closes on the second date when time is on: selection is not finished there, and closing would take away the very columns the prop was enabled for.

    Inside a single day only the time keeps the ends in order. An edit that would put the end before the start is not applied and is announced — the same way a disallowed length is refused today: the user simply mis-aimed, and there is nothing to reset.

    minRange and maxRange still count days. Changing the unit of existing props would break consumers; a minute-level limit is minDuration/maxDuration, and those arrive when asked for.

    minuteStep, secondStep, enableSeconds and use12Hours carry the same names they have on GrDateTimePicker, so the two pickers do not drift apart.

v0.7.02026-08-20

Added

  • multiple on GrDatePicker — a set of dates, not a range. Class schedules, exception days, booked dates: an arbitrary set where adjacency means nothing. Until now this had no expression at all — GrDatePicker held one date and GrDateRangePicker two ends of a continuous span, leaving the application with its own grid or a list of fields.

    Clicking a selected date removes it: a set is a toggle, not an accumulator, or there would be no way to undo a mistaken pick. The panel stays open — a set is accumulated, whereas a single date is chosen once, and closing after the first click would turn ten dates into ten openings.

    The model always arrives sorted ascending, wherever you clicked. It has to be comparable: reordering must not read as a change, or “unsaved edits” fires on nothing.

    Manual entry is off in this mode even with editable. One string describing N dates is a separate parser with its own behaviour on partial input, and the package does not have it yet.

    The field shows the first three dates and the rest as a count (“and 2 more”) — without a cap the label overflows by the fifth. The form receives one hidden field per date, sharing a name, as FormData.getAll reads them. min, max and disabledDates apply per date.

  • selectedDates on GrCalendar — painting only, exactly like rangeStart/rangeEnd. The grid answers one question, how to colour a cell; adding, removing, sorting and length rules belong to the picker. Membership is tested against a Set rebuilt once per set change: walking the array for each of the forty-two cells would make highlighting quadratic.

  • separator on GrDatePicker — what joins the dates of a set in the field.

v0.6.02026-08-20

Added

  • GrTimePicker’s footer hands the selection inward. The footer slot now receives select, canSelect and close — the same trio the date pickers already pass — so an application can finally put a “now” button there. Until now the slot took no props at all: there was neither a way to pick a time nor a way to ask whether one was allowed.

    Time snaps up to the step: 14:37 with a 15-minute step gives 14:45, not 14:30. A time in a picker almost always means “starting from this moment” — a booking, a reminder, an appointment — and a value rounded down has already passed. This is the mirror of the columns, which round down when displaying an existing value; there the task is the opposite, not to invent a slot the column does not have.

    The coarsest declared step wins and finer units zero out, so minuteStep: 15 yields 14:45:00 rather than 14:45:37. With seconds switched off they stay out of the value too — otherwise the model would carry what was never on screen.

    Bounds are checked after snapping, not before. With max at 14:40 and a 15-minute step, a “now” of 14:37 snaps to 14:45 — already past the bound — and canSelect returns false. The button arrives disabled instead of silently doing nothing; checking before the snap would have let it through.

  • quarter mode in GrCalendar and GrDatePicker — four cells in two columns (three would leave one cell alone on the second row). The value is the first day of the quarter. Labels come from the package’s own locale strings: Intl does not name quarters at all, and Q1 vs 1 кв. is interface text rather than locale-dependent data.

  • week mode — drawn with the day grid, not the period grid. Twelve cells in three columns would be twelve weeks on screen: a quarter of a year without a single month label, with nothing to aim at. Instead, clicking any day selects the week it belongs to and the whole row highlights.

    The value is the start of the week, so the shape of the model stays the same across all five modes and valueAdapter keeps working as before. The first day comes from the locale, as everywhere else — in the US the same date falls in a week starting on Sunday. Week numbers are not shown: ISO and US number them differently and Intl does not provide them.

  • startOfWeek(date, firstDayOfWeek) and ceilToStep(time, stepSeconds) are exported — the twins of leadingOffset (which was internal) and floorToStep.

Fixed

  • GrTimePicker’s footer had no container of its own. Slot content sat flush against the columns and read as their continuation rather than a separate action. It now gets the same wrapper the date pickers’ footer uses — a hairline rule and spacing — so the two pickers look alike, which was the point of giving the slot the same props.

v0.5.02026-08-19

Added

  • Quick ranges live in the picker panel. GrDateRangePicker and GrDatePicker gained a presets prop — a row of shortcuts in the panel footer: «Today», «Last 7 days», «This month». Bounds may be given as a function, so «last 7 days» counts from today rather than from the day the prop was declared. A shortcut whose range falls outside min/max, touches disabledDates or breaks minRange/maxRange arrives disabled: a button that does nothing lies about what it does. The docs used to redirect this to GrSegmented next to the field, and for a single date that still holds — but a range shortcut has to set both bounds and respect the length limits, which is knowledge only the picker has.
  • The footer slot now carries the selection. GrDateRangePicker passes setRange, canSetRange and close; GrDatePicker and GrCalendar pass select, canSelect (and close for the picker). A custom footer replaces the preset row entirely and gets exactly the same rules, so it cannot select a value the grid would refuse.
  • GrDuration — how long it lasted, not when it happened. «2 h 30 min» from a number of seconds, from a pair of dates, or counted live from a starting moment. Unit names and numeral agreement come from Intl, so the component ships no strings of its own; the markup is <time> with an exact ISO 8601 duration in datetime even when the text is shortened.
  • Duration arithmetic is public: selectDurationParts, formatDuration, durationToIso and resetDurationFormatCache. Units stop at days — months and years are calendar-bound, and deriving them from a count of seconds cannot be done without lying. maxUnits is a ceiling rather than a quota (exactly two hours reads «2 h», not «2 h 0 min»), and the smallest shown unit is truncated rather than rounded, so the display never runs ahead of the time that actually passed. Where Intl.DurationFormat is missing — the package declares Node >=22, which has no such API, and the SSR harness runs in Node — the same string is assembled from Intl.NumberFormat with style: 'unit'.

Changed

  • The panel footer moved out of the calendar grid into the picker panel. Rendered inside [data-gr-calendar] it had no padding of its own: the panel hands its background and spacing to the calendar. Anything already using the footer slot of GrDatePicker or GrDateRangePicker now sits one level higher in the DOM.

v0.4.02026-08-19

Changed

  • Control-scale font sizes now ship a paired line height. The calendar, the time picker and the shared picker field set the matching leading-* next to every control font size, from the core’s new --gr-control-leading-* steps. Before this the line height came from the host application’s body as an absolute value, so a 12px picker caption inherited whatever the host had set. Requires core >=0.27.0.

Added

  • weekStart can now be set once for the whole application through <GrConfigProvider :component-defaults="{ GrCalendar: { weekStart: 7 } }">. The prop existed on the calendar and on all three pickers, but only per instance — an application whose working week starts on Sunday regardless of interface language had to repeat it at every call site, and miss one to get two different calendars on one screen. The key is GrCalendar and there is deliberately no per-picker one: the pickers render that same calendar, so a second key would be two names for one setting. Resolution order is the usual one — a prop on the spot beats the config, the config beats the locale — and with neither set Intl still decides, which is what makes the default right in every country without anybody configuring anything.

Fixed

  • showWeekNumbers was typed as configurable and was not. GrCalendar declared it in its defaults registry, so the key type-checked and the IDE suggested it, but the component read props.showWeekNumbers ?? false and never looked at the provider. Nothing failed: the setting was simply ignored, silently, which is the worst way for a contract to be broken. The gate did not catch it either — defineComponentDefaultsGate checks that a declaration exists, not that the component reads it.
  • The pickers overrode that setting even once it worked. GrDatePicker, GrDateRangePicker and GrDateTimePicker defaulted showWeekNumbers to false rather than undefined and passed that value straight down, so an explicit “no” from the picker beat the application’s “yes” from the provider. They now pass the prop through untouched and let the calendar resolve it.

Earlier versions

The bodies of these releases are not shown: the full feed weighs more than a page is allowed to. The file in the repository is always complete.

  • v0.3.12026-08-18
  • v0.3.02026-08-18

The package CHANGELOG in full