fold-ng 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +344 -1
- package/README.md +50 -0
- package/fesm2022/fold-ng.mjs +2828 -6
- package/fesm2022/fold-ng.mjs.map +1 -1
- package/package.json +1 -1
- package/types/fold-ng.d.ts +1922 -2
- package/types/fold-ng.d.ts.map +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,347 @@ All notable changes to **fold-ng** are documented here. The format follows
|
|
|
8
8
|
|
|
9
9
|
_Nothing yet._
|
|
10
10
|
|
|
11
|
+
## [0.8.1] - 2026-08-02
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **The visual-snapshot tier is no longer a release gate.** Pixel screenshots
|
|
16
|
+
against a live dev server drift for reasons that are not a regression — a
|
|
17
|
+
scrollbar toggling or a font hint moves the frame ~20px — and the baselines
|
|
18
|
+
are per-platform, so gating on them turned the release red on Linux CI _and_
|
|
19
|
+
intermittently on the Mac it was cut from. The shots are now tagged `@visual`
|
|
20
|
+
and run only via `pnpm test:e2e:visual` (a local eyeball aid); the gate
|
|
21
|
+
(`test:e2e`, used by CI and `pnpm release`) excludes them. The geometry that
|
|
22
|
+
matters stays gated, and stably, by measurement in `calendar.spec.ts`. `0.8.0`
|
|
23
|
+
was tagged but never published because of this — `0.8.1` is the first published
|
|
24
|
+
cut of the 0.8 line, and carries the whole calendar family.
|
|
25
|
+
|
|
26
|
+
## [0.8.0] - 2026-08-02
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **A calendar family, starting with `fold-calendar-month` and the plain-date
|
|
31
|
+
model it stands on.** The grid is a date axis: seven columns of whole days,
|
|
32
|
+
and over them a layer of **bands**, each stretching from the column its event
|
|
33
|
+
starts on to the column it ends on. A span crossing a week is drawn once per
|
|
34
|
+
week with an open edge on the side that continues, so a three-week holiday
|
|
35
|
+
reads as one thing rather than twenty-one chips. Bands pack into lanes
|
|
36
|
+
(earliest start, then longest) under a `maxLanes` budget, and whatever will
|
|
37
|
+
not fit becomes an overflow chip **in the column of each day that lost
|
|
38
|
+
something** — a hidden span counts against every day it would have covered,
|
|
39
|
+
so the chip says _which_ day to open rather than only that the week is
|
|
40
|
+
crowded, and `overflowClick` emits that day. Events sharing a `groupId`
|
|
41
|
+
collapse into one chip spanning the union of their ranges and carrying the
|
|
42
|
+
count; a half-day edge is kept only on the segment holding the event's real
|
|
43
|
+
edge. Inputs cover `weekStartsOn` (any anchor, not just Monday), `fixedWeeks`,
|
|
44
|
+
`locale` and `labels`; `month` is a two-way `model`, so keyboard paging writes
|
|
45
|
+
back. Project an `<ng-template foldCalendarEvent>` to replace the built-in
|
|
46
|
+
chip. Generic over `T`, so an event's `data` survives the round trip to
|
|
47
|
+
`eventClick` without a cast.
|
|
48
|
+
- **`fold-calendar-agenda` — a rail of what is still ahead, grouped by day.**
|
|
49
|
+
The counterpart to the grids: they answer "what does this month look like",
|
|
50
|
+
it answers "what do I do next". Its `todo` slice keeps only the events asking
|
|
51
|
+
for attention — the `warning` and `alert` tones, the **same scale the chips
|
|
52
|
+
paint with**, so the rail needs no second notion of urgency — and carries the
|
|
53
|
+
count as a badge. An event already running is filed under the boundary rather
|
|
54
|
+
than its real start, so a three-week absence that began last week sits at the
|
|
55
|
+
top of what's next instead of in a past day the rail never shows. Days inside
|
|
56
|
+
the next week are named relatively ("Today", "Tomorrow", then the weekday),
|
|
57
|
+
which reads faster than a date at that distance. `mode` and `collapsed` are
|
|
58
|
+
two-way `model`s — persist the collapse if you want it to stick; the package
|
|
59
|
+
stores nothing. Pure `foldBuildAgenda` / `foldCountActionable` behind it.
|
|
60
|
+
- **`fold-calendar-source-filter` — chips that switch each feed of a merged
|
|
61
|
+
calendar on and off.** A calendar usually merges several feeds (a programme,
|
|
62
|
+
staff leave, contracts); an event names its own with `sourceKey`, a
|
|
63
|
+
`FoldCalendarSource` declares the label and dot, and the chips count what each
|
|
64
|
+
contributes. They own the **selection only** — the caller runs the pure
|
|
65
|
+
`foldFilterBySource()` over its own events, so the chips never learn how
|
|
66
|
+
anything is fetched. An event with no `sourceKey` belongs to no feed and no
|
|
67
|
+
chip can hide it. Each chip is a real toggle (`aria-pressed` in both states)
|
|
68
|
+
whose accessible name says which feed and whether it is showing, because the
|
|
69
|
+
tick and the dot are colour and colour cannot be the only carrier.
|
|
70
|
+
- **`fold-calendar-toolbar` — the chrome that makes the views one calendar.**
|
|
71
|
+
Jump to today, page back and forward, the period's name, and the view switch.
|
|
72
|
+
It owns no data: both pieces of state are two-way `model`s, so a page binds
|
|
73
|
+
the same `date` and `view` it hands the view on screen and paging works with
|
|
74
|
+
no output handler. The step matches the reading — a month under the month
|
|
75
|
+
view, a week under the week view, a day under the day view. The title carries
|
|
76
|
+
`aria-live="polite"`, so paging announces where a keyboard user landed instead
|
|
77
|
+
of changing silently. Backed by pure `foldShiftDate`, `foldRangeForView` (the
|
|
78
|
+
window a caller fetches — the whole painted grid for a month, not just its
|
|
79
|
+
days) and `foldViewTitle`.
|
|
80
|
+
- **The three column views that complete the drill-down: `fold-calendar-week`,
|
|
81
|
+
`fold-calendar-day` and `fold-calendar-list`.** Where the month grid packs
|
|
82
|
+
spans into lanes, these simply list what covers each day — so nothing is
|
|
83
|
+
clipped and no lane budget can hide anything. `week` is seven day columns;
|
|
84
|
+
`day` is one day in full, with a tag-qualified `button[empty]` slot for a "new request" action
|
|
85
|
+
when nothing sits on it; `list` is the flat chronological reading, each row
|
|
86
|
+
led by its span (formatted with `Intl`'s own range formatter, which collapses
|
|
87
|
+
a single day to one date and factors out a shared month). All three take the
|
|
88
|
+
same `foldCalendarEvent` template, labels and `locale` as the month view.
|
|
89
|
+
Their a11y is **simpler than the month's on purpose**: because a chip nests
|
|
90
|
+
inside the day it belongs to rather than spanning columns, every control is a
|
|
91
|
+
real child in the natural tab order — no roving tabindex, no `aria-hidden`,
|
|
92
|
+
every event reachable by keyboard. New pure builders `foldBuildWeek` and
|
|
93
|
+
`foldBuildDay` back them.
|
|
94
|
+
- **`FoldCalendarDate` — the package's date primitive is a plain
|
|
95
|
+
`YYYY-MM-DD` string, not a `Date`.** A calendar of all-day spans deals in
|
|
96
|
+
dates, not instants: `new Date("2026-05-18")` is UTC midnight, i.e. the 17th
|
|
97
|
+
anywhere west of Greenwich, and that off-by-one is the most common calendar
|
|
98
|
+
bug there is. Strings remove the class by construction, compare
|
|
99
|
+
lexicographically (`a <= b` _is_ "on or before"), are `===`-equal when they
|
|
100
|
+
mean the same day, and are already the wire format. Arithmetic runs through
|
|
101
|
+
`Date.UTC`, so no DST boundary can repeat or skip a day. Ships with
|
|
102
|
+
`foldToday`, `foldAddDays`, `foldAddMonths`, `foldStartOfWeek`,
|
|
103
|
+
`foldStartOfMonth`, `foldEndOfMonth`, `foldDaysBetween`, `foldWeekdayIndex`,
|
|
104
|
+
`foldWeekdayOf`, `foldIsWeekend`, `foldIsCalendarDate`, `foldToNativeDate` and
|
|
105
|
+
`foldFromNativeDate` (the inbound bridge — the conversion every consumer would
|
|
106
|
+
otherwise write as `toISOString().slice(0, 10)`, which is the very timezone bug
|
|
107
|
+
this design exists to remove), plus the layout entry points
|
|
108
|
+
`foldBuildMonthGrid`, `foldBuildWeek`, `foldBuildDay`, `foldEventsOnDay`,
|
|
109
|
+
`foldEventsInRange` and `foldFilterBySource`. Supported range is
|
|
110
|
+
`0001-01-01`–`9999-12-31`: the four-digit year is what makes the lexicographic
|
|
111
|
+
guarantee hold, so it is enforced rather than assumed.
|
|
112
|
+
`fold-timeline` keeps a native `Date` on purpose — it plots dated _instants_,
|
|
113
|
+
which is the other domain.
|
|
114
|
+
- **Month and weekday names come from `Intl`, not from a label token.** The
|
|
115
|
+
`locale` input drives `Intl.DateTimeFormat`, so every locale works without
|
|
116
|
+
hand-translating twelve month names; `FoldCalendarLabels` (with
|
|
117
|
+
`provideFoldCalendarLabels`) covers only what `Intl` cannot supply — the today
|
|
118
|
+
marker, the overflow chip and the event count.
|
|
119
|
+
|
|
120
|
+
- **Everything the calendar draws _around_ an event is a projectable template.**
|
|
121
|
+
The chip was replaceable from day one; the containers were not, which had it
|
|
122
|
+
backwards — an app could restyle the smallest unit and not the cell behind it.
|
|
123
|
+
Four more `<ng-template>` seams, each with a typed context and an
|
|
124
|
+
`ngTemplateContextGuard` (so `let-` variables are real types under
|
|
125
|
+
`strictTemplates`, not `any`): **`foldCalendarDay`** replaces the inside of a
|
|
126
|
+
month cell — the hook for a public holiday, a closure, "3/8 staffed";
|
|
127
|
+
**`foldCalendarHeading`** replaces the agenda's day heading;
|
|
128
|
+
**`foldCalendarTitle`** replaces the toolbar's `<h2>`, which also lets a page
|
|
129
|
+
give the title the heading level its outline needs; **`foldCalendarOverflow`**
|
|
130
|
+
replaces the `+N` chip. Alongside them, `dayModifiers: (day) => string[]`
|
|
131
|
+
emits an app's own names as one `data-fold-day-modifiers` attribute, matchable
|
|
132
|
+
with `[data-fold-day-modifiers~="holiday"]` — so nobody has to write CSS
|
|
133
|
+
against an internal class name. `foldCalendarEvent` is now generic too, so
|
|
134
|
+
`event.data` comes back as the app's own record.
|
|
135
|
+
- **`fold-calendar-timegrid` — the reading the other four cannot give: when
|
|
136
|
+
_inside_ a day.** Hour columns for a week or a single day (`dayCount`), with
|
|
137
|
+
the all-day strip on top. A meeting is a block whose height is its duration
|
|
138
|
+
and whose width is shared with whatever it collides with; an absence is a band
|
|
139
|
+
across the strip, spanning days through **the same packer the month grid
|
|
140
|
+
uses** — extracted rather than copied, so a three-day leave request reads
|
|
141
|
+
identically in both.
|
|
142
|
+
|
|
143
|
+
Time is modelled as wall-clock `HH:mm` (`FoldCalendarTime`), **not an
|
|
144
|
+
instant** — the same decision as the date, for the same reason: 09:00 is the
|
|
145
|
+
hour on the wall, and an instant re-derives that from a zone every render,
|
|
146
|
+
which is one wrong default away from drawing the wrong hour. The app converts
|
|
147
|
+
once at its own boundary.
|
|
148
|
+
|
|
149
|
+
Two details that are the difference between a real time grid and a demo: the
|
|
150
|
+
overlap test is **exclusive** at the boundary, so back-to-back meetings keep
|
|
151
|
+
the full width instead of each taking half; and events are grouped into
|
|
152
|
+
**clusters**, so one triple-booked morning does not narrow an unrelated
|
|
153
|
+
afternoon. A span crossing midnight becomes one block per day, each with the
|
|
154
|
+
right open edge, rather than one impossible block running off the bottom.
|
|
155
|
+
|
|
156
|
+
`now` is an input, never a clock the package reads — `today` already works
|
|
157
|
+
that way, and a server render that invented one would hydrate to a different
|
|
158
|
+
position. Positions are **fractions of the visible window**, so `dayStart` /
|
|
159
|
+
`dayEnd` and the CSS height stay independent. Backed by pure
|
|
160
|
+
`foldBuildTimeGrid` and `foldLayOutOverlaps`.
|
|
161
|
+
|
|
162
|
+
- **`foldFromTemporal` — the family is Temporal-native without depending on
|
|
163
|
+
it.** `Temporal.PlainDate.toString()` **is** `YYYY-MM-DD`: the primitive
|
|
164
|
+
chosen for correctness turns out to be exactly Temporal-shaped, which no
|
|
165
|
+
`Date`-based calendar can say. The bridge accepts a `PlainDate`,
|
|
166
|
+
`PlainDateTime` or `ZonedDateTime` — typed structurally, so it compiles and
|
|
167
|
+
runs on an engine that has none of them — and drops the time, which is what a
|
|
168
|
+
whole-day calendar wants **and** keeps the day the value itself means rather
|
|
169
|
+
than the one UTC would have picked. The other direction needs no helper:
|
|
170
|
+
`Temporal.PlainDate.from(foldDate)` already accepts one of ours.
|
|
171
|
+
- **The week now comes from the locale, not from a guess.** `Intl.Locale`
|
|
172
|
+
knows which day a locale's week opens on and which days it rests — most
|
|
173
|
+
calendars stop at the month names and hard-code Monday, which is right in
|
|
174
|
+
Paris and wrong in Chicago, Cairo and Malé. `weekStartsOn` and `weekendDays`
|
|
175
|
+
default to `foldLocaleWeekInfo(locale)` and stay overridable, so the common
|
|
176
|
+
case needs no input at all. Both `getWeekInfo()` and the older `weekInfo`
|
|
177
|
+
getter are probed, and a runtime with neither falls back to Monday + Sat/Sun.
|
|
178
|
+
- **ISO week numbers, as an optional leading column.** `showWeekNumbers` on
|
|
179
|
+
`fold-calendar-month`, backed by `foldIsoWeek` / `foldIsoWeekYear`. ISO weeks
|
|
180
|
+
start on Monday and belong to the year holding their **Thursday**, so they are
|
|
181
|
+
deliberately _not_ derived from the calendar's own anchor — 1 January is
|
|
182
|
+
sometimes week 53 of the year before, which is the whole point of the rule and
|
|
183
|
+
the reason European B2B reporting asks for it. The column is a real grid
|
|
184
|
+
track, so every placed element — cells, bands, overflow chips — shifts with it.
|
|
185
|
+
- **A `formats` input beside `labels`.** Labels let a locale translate; this
|
|
186
|
+
lets it **reformat**. Every `Intl` option bag the family uses lives in one
|
|
187
|
+
table (`FOLD_CALENDAR_FORMATS`, now exported); `formats` merges over it per
|
|
188
|
+
instance — a narrow weekday header, a numeric month, a four-digit year.
|
|
189
|
+
- **A print stylesheet for the month.** Browsers drop backgrounds but keep text
|
|
190
|
+
colour, so a dark theme printed as-is is light-on-white — unreadable. The
|
|
191
|
+
print block re-expresses everything in system colours (`Canvas`/`CanvasText`/
|
|
192
|
+
`GrayText`), which are neither theme tokens nor hard-coded values, and asks
|
|
193
|
+
for ink on exactly one thing: the tone bar, the last cue telling two bands
|
|
194
|
+
apart on paper.
|
|
195
|
+
- **The month layout is ~5× faster, and the benchmark ships with it.** The cost
|
|
196
|
+
was never the algorithm — it was the representation: every comparison in the
|
|
197
|
+
candidate sort called `foldDaysBetween`, which re-parsed two strings and built
|
|
198
|
+
two `Date`s, and the whole feed was re-filtered once per week row. Spans now
|
|
199
|
+
carry epoch-day bounds computed once, rows are bucketed in a single pass
|
|
200
|
+
(`O(rows × N)` → `O(N)`), and clipping is integer arithmetic. The public model
|
|
201
|
+
is unchanged — this is internal only. Measured on a month layout, mean of 20:
|
|
202
|
+
1 000 events 4.8 ms → **0.99 ms**, 5 000 28.9 → **5.0**, 20 000 124 → **22**.
|
|
203
|
+
`pnpm bench:calendar` re-runs it against a committed budget and exits non-zero
|
|
204
|
+
when a size blows it.
|
|
205
|
+
- **`foldCalendarNextFocus` is public — the geometry tier is now complete.**
|
|
206
|
+
The pure builders (`foldBuildMonthGrid`, `foldBuildWeek`, `foldBuildDay`,
|
|
207
|
+
`foldBuildAgenda`) and the period functions (`foldShiftDate`,
|
|
208
|
+
`foldRangeForView`, `foldViewTitle`) let an app lay a calendar out and draw it
|
|
209
|
+
its own way; the arrow-key map was the one piece missing, and a hand-rolled
|
|
210
|
+
date grid owes its users the same keyboard as the built-in one. Documented as
|
|
211
|
+
a **tier** in the README rather than left as an accident of what happened to
|
|
212
|
+
be exported. `foldFocusDayCell` deliberately stays internal: it reads a
|
|
213
|
+
`data-fold-day` attribute this package writes, and exporting it would freeze
|
|
214
|
+
that attribute into the public contract.
|
|
215
|
+
- **The view switch is open.** `FoldCalendarView` keeps the four built-ins as
|
|
216
|
+
autocompleting literals but accepts any string, and `views` takes
|
|
217
|
+
`{ value, label }` — so an app's own reading (a resource grid, a timeline) can
|
|
218
|
+
sit in the same toolbar without the library knowing about it. Paging and
|
|
219
|
+
titling an unrecognised view fall back to month semantics, which always lands
|
|
220
|
+
on a real date.
|
|
221
|
+
- **A `FoldCalendarDay` now carries its own `eventCount` and `hiddenCount`.**
|
|
222
|
+
Both were previously recomputed per cell per change-detection cycle (an O(N)
|
|
223
|
+
filter, with an allocation, 35 times a tick) or exposed as a positional
|
|
224
|
+
`hiddenByDay` array only readable when crossed with the row's dates. Counting
|
|
225
|
+
once while the grid is built is both cheaper and self-describing, and it is
|
|
226
|
+
what lets a day cell announce "5 events, 2 not shown".
|
|
227
|
+
|
|
228
|
+
### Changed
|
|
229
|
+
|
|
230
|
+
- **A standalone `fold-view-nav` now separates itself from the content it heads
|
|
231
|
+
by the same gap a `fold-nav-layout` applies.** A horizontal bar used on its own
|
|
232
|
+
(outside a layout) gained a `margin-block-end` equal to `--fold-nav-layout-gap`
|
|
233
|
+
(default `--fold-space-lg`, 16px) — the value is now shared through a
|
|
234
|
+
`nav-content-gap` mixin (`layout/_nav-gap.scss`) that both the layout and the
|
|
235
|
+
bar `@use`, so the two can't drift and consumers stop hand-rolling a margin.
|
|
236
|
+
Applied only to a **standalone horizontal** bar: inside a `fold-nav-layout` the
|
|
237
|
+
layout still owns the gap (no double space), and a vertical bar is a side rail,
|
|
238
|
+
not a header. `fold-tabs` is unaffected (an in-place widget, not a bar that
|
|
239
|
+
introduces following content). Purely additive spacing — the layout's own
|
|
240
|
+
rendering is unchanged.
|
|
241
|
+
|
|
242
|
+
- **The weekend is its own input, because it is its own fact.**
|
|
243
|
+
`foldIsWeekEnd(date, weekStartsOn)` defined the weekend as "the last two
|
|
244
|
+
columns", which shades **Friday and Saturday** on a Sunday-first calendar and
|
|
245
|
+
calls Sunday a working day. Replaced by `foldIsWeekend(date, weekendDays)`
|
|
246
|
+
with a `weekendDays` input (default `['sat', 'sun']`) on the month and week
|
|
247
|
+
views — the anchor moves the columns, it does not move the days people rest
|
|
248
|
+
on, and a Saturday-first calendar resting Fri+Sat is now expressible.
|
|
249
|
+
- **`foldRangeForView('list')` returns the month, not the painted grid.** It
|
|
250
|
+
shared the month branch, so a May list showed late-April events: a month view
|
|
251
|
+
paints padding days and needs their events, a list has none.
|
|
252
|
+
- **A projected template replaces a list row's whole inside**, as it already did
|
|
253
|
+
in the other four views. The bar and the date used to render outside the
|
|
254
|
+
branch, so the same template rendered differently depending on which view
|
|
255
|
+
hosted it, and a one-element template landed in a four-column grid.
|
|
256
|
+
- **The month day cell is a `gridcell`, not a `<button>` wearing the role.**
|
|
257
|
+
`role="gridcell"` on a button replaces its native role, so activation is now
|
|
258
|
+
wired explicitly (`Enter`/`Space`) and the cell is the focusable widget the
|
|
259
|
+
ARIA grid pattern asks for. `role="grid"` and its accessible name moved onto
|
|
260
|
+
the host, which also removes the second root the keyboard helper had to find.
|
|
261
|
+
- **`fold-calendar-week`'s `date` is a plain input.** It was a
|
|
262
|
+
`model.required` that the component never wrote to — a two-way binding
|
|
263
|
+
promising something it does not do. Pair it with the toolbar, which does own
|
|
264
|
+
the paging.
|
|
265
|
+
- **The five views share one host directive for their chrome.** Labels, locale
|
|
266
|
+
and the projected chip template were eight identical lines copied five times,
|
|
267
|
+
and the `Intl.DateTimeFormat` option bags were duplicated with small
|
|
268
|
+
divergences — which is how the day view came to ask for
|
|
269
|
+
`{ weekday: 'long', month: 'long' }` and print "Saturday May", a phrase in no
|
|
270
|
+
locale. There is now one table of formats (`FOLD_CALENDAR_FORMATS`) and one
|
|
271
|
+
cache keyed by locale.
|
|
272
|
+
- **Tones are written once.** The four roles were reimplemented in three places
|
|
273
|
+
(the month's bands, the shared chip, the list's rows, which had drifted into
|
|
274
|
+
its own copy); they are now one mixin parameterised by selector.
|
|
275
|
+
- **RTL decorations follow the reading direction.** Continuation edges, the
|
|
276
|
+
chevrons, the cell separators and the chip padding were physical properties,
|
|
277
|
+
so a right-to-left calendar squared off the wrong side. All logical now.
|
|
278
|
+
- **The label token drops `dateRange`** (declared, translated by consumers,
|
|
279
|
+
used nowhere — `Intl.formatRange` already orders a span per locale) and gains
|
|
280
|
+
`hiddenCount`, `agendaModes` and `agendaMore`.
|
|
281
|
+
|
|
282
|
+
### Fixed
|
|
283
|
+
|
|
284
|
+
- **Two events sharing an `id` no longer merge into one.** The layout keyed its
|
|
285
|
+
identity map on the id alone, so a duplicate was indistinguishable from an
|
|
286
|
+
explicit `groupId`: the second event was **never rendered**, both bands showed
|
|
287
|
+
the first one's label, a bogus group counter appeared, and the four column
|
|
288
|
+
views threw `NG0955` on their `track event.id`. Duplicates now stay separate,
|
|
289
|
+
with a dev-mode warning naming the id.
|
|
290
|
+
- **`maxLanes` is clamped in one place, and coerced from an attribute.** The
|
|
291
|
+
invariant lived in two and only one enforced it: `maxLanes="2"` as a string
|
|
292
|
+
built an overflow row of `"22"`, a negative value made `repeat(-5, …)` and the
|
|
293
|
+
CSS parser dropped the whole `grid-template-rows` rule, and `NaN` **disabled
|
|
294
|
+
the lane budget entirely** (`lane >= NaN` is never true) so nothing was ever
|
|
295
|
+
counted as hidden.
|
|
296
|
+
- **A reversed range (`end < start`) is put back in order.** Left alone it
|
|
297
|
+
rendered _twice, wrongly_: `grid-column: 5 / 2`, which CSS Grid silently
|
|
298
|
+
swaps, so a band covered four days that every other view — asking
|
|
299
|
+
`start <= day && end >= day` — reported as empty. Same input, two
|
|
300
|
+
contradictory renders, no error.
|
|
301
|
+
- **A month that is not a date yields no grid, and says so.** `foldIsCalendarDate`
|
|
302
|
+
existed, was tested, and was called by nothing: `month="not-a-date"` produced
|
|
303
|
+
a `role="grid"` with headers and zero rows, and `month="2026-13-45"` produced
|
|
304
|
+
a December calendar displayed with total confidence.
|
|
305
|
+
- **A collapsed group shows its most severe member.** It kept the first event in
|
|
306
|
+
document order and merged only the dates, so a cancelled first member greyed
|
|
307
|
+
out a whole group containing an alert, and an open-ended member inside a
|
|
308
|
+
closed group had its open edge dropped — a contract with no end drawn as
|
|
309
|
+
finished. Open edges are now the OR of the members, and the representative is
|
|
310
|
+
the most severe by tone (ties to document order), so tone, icon, label and
|
|
311
|
+
source all come from one event that really exists.
|
|
312
|
+
- **The agenda's `limit` can no longer empty the rail.** `limit: 0` printed
|
|
313
|
+
"Nothing to handle — all up to date." directly under a badge saying otherwise,
|
|
314
|
+
and `NaN` did the same (`slice(0, NaN)` returns nothing). It now clamps to at
|
|
315
|
+
least one day and reports what it cut off, so "there is more" stops rendering
|
|
316
|
+
as "there is nothing".
|
|
317
|
+
- **Paging with the keyboard twice in a row keeps the focus.** The deferred
|
|
318
|
+
focus target was a signal that was never reset, so repeating the _same_
|
|
319
|
+
transition wrote the same value, the effect did not re-run, and focus fell
|
|
320
|
+
onto `<body>`. The request is now consumed, and applied in an
|
|
321
|
+
`afterNextRender` rather than an `effect` — the ordering of an effect against
|
|
322
|
+
the DOM it wants to touch is not contracted, and has already changed between
|
|
323
|
+
Angular versions.
|
|
324
|
+
- **The day view's `[empty]` slot no longer swallows a child.** `empty` is also
|
|
325
|
+
an input on `fold-data-table` and `fold-field`; an unqualified selector
|
|
326
|
+
captured either of them, and with no default slot the child simply vanished
|
|
327
|
+
(rule 4.8). Tag-qualified, with a default slot behind it and a projection test.
|
|
328
|
+
- **Years below 100 no longer jump 1900 years.** `Date.UTC(99, …)` means 1999,
|
|
329
|
+
so `foldAddDays("0099-12-31", 1)` returned `"2000-01-01"`. Years are also
|
|
330
|
+
zero-padded to four digits, without which they sort before every other date.
|
|
331
|
+
- **`foldFilterBySource` accepts `null`** — the initial value of the very model
|
|
332
|
+
it exists to consume. Every caller was writing the same ternary; the gallery
|
|
333
|
+
did too.
|
|
334
|
+
- **The band icon takes the tone's colour**, and the collapsed agenda spine
|
|
335
|
+
names the slice it will open into rather than always saying "To handle". The
|
|
336
|
+
agenda's slice switch has its own accessible name instead of repeating the
|
|
337
|
+
rail's, and its badge honours a caller's `isActionable`, so it can no longer
|
|
338
|
+
disagree with the list under it.
|
|
339
|
+
- **The calendar is now honestly Gregorian in every locale.** Names were
|
|
340
|
+
formatted in the locale's _own_ calendar system, so `locale="ar-SA"` printed
|
|
341
|
+
Hijri month names over the Gregorian day numbers the grid counts, on rows that
|
|
342
|
+
break on Gregorian months — a calendar contradicting itself. The `Intl`
|
|
343
|
+
formatters are pinned to `calendar: 'gregory'`, so a locale now localises the
|
|
344
|
+
_language_ of the names, not the calendar behind them. A true non-Gregorian
|
|
345
|
+
layout is a separate widget this family does not pretend to be.
|
|
346
|
+
- **`foldIsCalendarTime` rejects an impossible minute field.** `"10:75"` and
|
|
347
|
+
`"23:60"` are `HH:mm`-shaped and under 1440 minutes, so a range check on the
|
|
348
|
+
total waved them through; the hour and minute fields are now validated
|
|
349
|
+
separately (`00:00`–`23:59`, plus `24:00`), the same rigour the date guard
|
|
350
|
+
applies to `2026-02-30`.
|
|
351
|
+
|
|
11
352
|
## [0.7.0] - 2026-07-29
|
|
12
353
|
|
|
13
354
|
### Added
|
|
@@ -715,7 +1056,9 @@ design-token stylesheet.
|
|
|
715
1056
|
`currentColor`; `prefers-reduced-motion` + `forced-colors` are respected;
|
|
716
1057
|
strings localise via inputs / providers (`provideFoldPanelLabels`).
|
|
717
1058
|
|
|
718
|
-
[unreleased]: https://github.com/hugoheynard/fold-ng/compare/v0.
|
|
1059
|
+
[unreleased]: https://github.com/hugoheynard/fold-ng/compare/v0.8.1...HEAD
|
|
1060
|
+
[0.8.1]: https://github.com/hugoheynard/fold-ng/releases/tag/v0.8.1
|
|
1061
|
+
[0.8.0]: https://github.com/hugoheynard/fold-ng/releases/tag/v0.8.0
|
|
719
1062
|
[0.7.0]: https://github.com/hugoheynard/fold-ng/releases/tag/v0.7.0
|
|
720
1063
|
[0.6.1]: https://github.com/hugoheynard/fold-ng/releases/tag/v0.6.1
|
|
721
1064
|
[0.6.0]: https://github.com/hugoheynard/fold-ng/releases/tag/v0.6.0
|
package/README.md
CHANGED
|
@@ -341,6 +341,56 @@ the package root.
|
|
|
341
341
|
| `FoldPopoverComponent` | `fold-popover` | Anchored floating layer — projected content in the native top layer (escapes `overflow`/`z-index`), positioned by a dependency-free **flip → size → shift** engine (`computePlacement`): a tall panel gets a `max-height` and scrolls inside the viewport. `[(open)]`; `autoUpdate` (ResizeObserver); optional `arrow`; native CSS enter/exit (`@starting-style` + `allow-discrete`); outside-click + `Escape` dismissal, focus-return, auto-wired `aria-haspopup`/`expanded`/`controls`. |
|
|
342
342
|
| `FoldDropdownComponent` | `fold-dropdown` | Actions menu on `fold-popover` — `role="menu"` with `<fold-dropdown-item>`s, ↑/↓ roving, `Home`/`End`, type-ahead; opens onto the first enabled item, closes returning focus to the trigger. Give the trigger `foldPopoverTrigger="menu"`. |
|
|
343
343
|
|
|
344
|
+
| `FoldCalendarMonthComponent` | `fold-calendar-month` | Month grid where events **span** the days they cover: a date-axis `role="grid"` with one roving tab stop, and over it a lane-packed layer of bands (one per week crossed, with open edges). `+N` chips sit on the crowded day, not at the end of the row. |
|
|
345
|
+
| `FoldCalendarWeekComponent` | `fold-calendar-week` | Seven day columns of stacked chips — nothing spans, so nothing is clipped and every chip is a real button in the tab order. Container-queried: labels and icons drop out before they truncate. |
|
|
346
|
+
| `FoldCalendarDayComponent` | `fold-calendar-day` | One day in full, with room for the subline — where a `dayClick` drill-down lands. Empty state takes a projected action (`button[empty]`). |
|
|
347
|
+
| `FoldCalendarListComponent` | `fold-calendar-list` | The flat reading of the same feed, in date order, each row leading with the span `Intl` formats. Nothing is ever hidden behind a lane budget. |
|
|
348
|
+
| `FoldCalendarTimegridComponent` | `fold-calendar-timegrid` | Hour columns for a week or a day, with the all-day strip on top. Timed events are placed on the clock and share their width only with what they actually collide with (exclusive boundaries, per-cluster widening); a span crossing midnight becomes one block per day. Times are wall-clock `HH:mm`, never instants; `now` is an input, not a clock. |
|
|
349
|
+
| `FoldCalendarAgendaComponent` | `fold-calendar-agenda` | "What's next" rail grouped by day, with a **to-handle** slice (the `warning`/`alert` tones — the same scale the chips paint with) and a live badge. Collapses to a spine; `mode`/`collapsed` are models, so the app owns persistence. |
|
|
350
|
+
| `FoldCalendarToolbarComponent` | `fold-calendar-toolbar` | Today / prev / next / period title / view switch. Owns no data — `date` and `view` are two-way, and the step matches the reading. `views` takes `{ value, label }` for an app's own view. |
|
|
351
|
+
| `FoldCalendarSourceFilterComponent` | `fold-calendar-source-filter` | Chips switching each feed of a merged calendar on and off. Owns the **selection** only; the caller runs the pure `foldFilterBySource()`. |
|
|
352
|
+
|
|
353
|
+
**Two tiers, on purpose.** The components are the first; under them, the same
|
|
354
|
+
geometry is exported as pure functions — `foldBuildMonthGrid` (week rows with
|
|
355
|
+
their events already packed into lanes), `foldBuildWeek` / `foldBuildDay`,
|
|
356
|
+
`foldBuildAgenda`, plus `foldShiftDate` / `foldRangeForView` / `foldViewTitle`
|
|
357
|
+
for paging and naming a period, and `foldCalendarNextFocus` for the arrow keys.
|
|
358
|
+
Lay a calendar out without rendering it with these components, and the hard part
|
|
359
|
+
— packing spans into lanes, clipping them at week boundaries, keeping an open
|
|
360
|
+
edge on the side that continues — is already solved and already tested. The
|
|
361
|
+
filters (`foldEventsOnDay`, `foldEventsInRange`, `foldFilterBySource`) are the
|
|
362
|
+
tier every page uses whichever rendering it picks.
|
|
363
|
+
|
|
364
|
+
**Measured, and kept measured.** Laying out a month (mean of 20 runs,
|
|
365
|
+
`pnpm bench:calendar`, committed with a budget so it cannot regress quietly):
|
|
366
|
+
|
|
367
|
+
| events in the window | 50 | 200 | 1 000 | 5 000 | 20 000 |
|
|
368
|
+
| -------------------- | ---- | ---- | ----- | ----- | ------ |
|
|
369
|
+
| ms per layout | 0.14 | 0.41 | 0.99 | 5.0 | 22 |
|
|
370
|
+
|
|
371
|
+
Calendar dates are plain `YYYY-MM-DD` **strings**, never `Date`. That is also
|
|
372
|
+
exactly what `Temporal.PlainDate.toString()` returns, so the family is
|
|
373
|
+
**Temporal-native without depending on it**: `foldFromTemporal()` accepts a
|
|
374
|
+
`PlainDate`, `PlainDateTime` or `ZonedDateTime` (structurally typed, so it
|
|
375
|
+
compiles on a runtime that has none of them), and going the other way needs no
|
|
376
|
+
helper — `Temporal.PlainDate.from(foldDate)` already takes one of ours. See
|
|
377
|
+
`foldToday` / `foldFromNativeDate` / `foldAddDays` and the reasoning in
|
|
378
|
+
[`/calendar-dates`](https://hugoheynard.github.io/fold-ng/calendar-dates). The
|
|
379
|
+
family plots on the **Gregorian** calendar and has no resource (staff × day)
|
|
380
|
+
view. Whole-day spans are the month, week, day, list and agenda views; the
|
|
381
|
+
**time grid** adds the hours, with time as wall-clock `HH:mm` rather than an
|
|
382
|
+
instant — the app converts at its own boundary, and no zone can move a meeting
|
|
383
|
+
after that.
|
|
384
|
+
|
|
385
|
+
It is a **pure display**, by decision and not by omission: it knows nothing
|
|
386
|
+
about what an event means or which layer produced it, so it never originates
|
|
387
|
+
one — no drag-to-create, no range-select. Tone is the caller's to compute,
|
|
388
|
+
which is what lets the same component paint a leave request and an account
|
|
389
|
+
sitting open between registration and activation, its band crossing into
|
|
390
|
+
`warning` then `alert` on a threshold the app owns. Everything drawn around an event is a projectable template:
|
|
391
|
+
`foldCalendarEvent`, `foldCalendarDay`, `foldCalendarHeading`,
|
|
392
|
+
`foldCalendarTitle`, `foldCalendarOverflow`.
|
|
393
|
+
|
|
344
394
|
Directives worth knowing: **`foldSurface`** (`page`·`chrome` — the seam a mixed
|
|
345
395
|
theme re-colours across), **`foldElevated`** (raise any bg-owning element into
|
|
346
396
|
an inset, rounded, shadowed card — the per-surface "floating" mechanism, driven
|