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/fesm2022/fold-ng.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DOCUMENT, NgTemplateOutlet, NgClass, DatePipe } from '@angular/common';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { input, inject, ElementRef, effect, Directive, Injectable, signal, DestroyRef, model, computed, HostListener, Component, InjectionToken, Service, booleanAttribute, linkedSignal, numberAttribute, ViewEncapsulation, output, viewChild, afterRenderEffect, afterNextRender, contentChildren, TemplateRef, isDevMode, contentChild, Renderer2, RendererStyleFlags2, viewChildren, ViewContainerRef
|
|
3
|
+
import { input, inject, ElementRef, effect, Directive, Injectable, signal, DestroyRef, model, computed, HostListener, Component, InjectionToken, Service, booleanAttribute, linkedSignal, numberAttribute, ViewEncapsulation, output, viewChild, afterRenderEffect, afterNextRender, contentChildren, TemplateRef, isDevMode, contentChild, untracked, Injector, Renderer2, RendererStyleFlags2, viewChildren, ViewContainerRef } from '@angular/core';
|
|
4
4
|
import { RouterLink, RouterLinkActive } from '@angular/router';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -4248,6 +4248,2292 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
4248
4248
|
}]
|
|
4249
4249
|
}], propDecorators: { sticky: [{ type: i0.Input, args: [{ isSignal: true, alias: "sticky", required: false }] }], stickyOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "stickyOffset", required: false }] }] } });
|
|
4250
4250
|
|
|
4251
|
+
/**
|
|
4252
|
+
* Marks an `<ng-template>` as the renderer for every event chip inside a
|
|
4253
|
+
* calendar view, replacing the built-in icon + label + count row:
|
|
4254
|
+
*
|
|
4255
|
+
* ```html
|
|
4256
|
+
* <fold-calendar-month [(month)]="month" [events]="events()">
|
|
4257
|
+
* <ng-template foldCalendarEvent let-event let-band="band">
|
|
4258
|
+
* <fold-avatar [name]="event.data.owner" size="xs" />
|
|
4259
|
+
* <span>{{ event.label }}</span>
|
|
4260
|
+
* </ng-template>
|
|
4261
|
+
* </fold-calendar-month>
|
|
4262
|
+
* ```
|
|
4263
|
+
*
|
|
4264
|
+
* `let-event` and `let-band` are **fully typed** under `strictTemplates`: the
|
|
4265
|
+
* directive is generic over the same `T` as the events it renders, and its
|
|
4266
|
+
* context guard carries that through, so `event.data` is the app's own record
|
|
4267
|
+
* with no cast.
|
|
4268
|
+
*
|
|
4269
|
+
* @selector `ng-template[foldCalendarEvent]`
|
|
4270
|
+
*/
|
|
4271
|
+
class FoldCalendarEventDirective {
|
|
4272
|
+
template = inject(TemplateRef);
|
|
4273
|
+
/** Teaches the template checker what `let-event` and `let-band` are. */
|
|
4274
|
+
static ngTemplateContextGuard(_directive, context) {
|
|
4275
|
+
// Compile-time only — Angular never calls this; the narrowing is the point.
|
|
4276
|
+
return context !== undefined;
|
|
4277
|
+
}
|
|
4278
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarEventDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
4279
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.8", type: FoldCalendarEventDirective, isStandalone: true, selector: "ng-template[foldCalendarEvent]", ngImport: i0 });
|
|
4280
|
+
}
|
|
4281
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarEventDirective, decorators: [{
|
|
4282
|
+
type: Directive,
|
|
4283
|
+
args: [{
|
|
4284
|
+
selector: "ng-template[foldCalendarEvent]",
|
|
4285
|
+
standalone: true,
|
|
4286
|
+
}]
|
|
4287
|
+
}] });
|
|
4288
|
+
|
|
4289
|
+
/**
|
|
4290
|
+
* Plain-date arithmetic for the calendar family — `YYYY-MM-DD` in, `YYYY-MM-DD`
|
|
4291
|
+
* out. Framework-free and side-effect-free, so every rule here is unit-testable
|
|
4292
|
+
* without a TestBed.
|
|
4293
|
+
*
|
|
4294
|
+
* ## Why a string and not a `Date`
|
|
4295
|
+
* A calendar of all-day spans deals in **plain dates** — "the 18th of May",
|
|
4296
|
+
* with no instant and no zone. A `Date` is a timestamp, so the moment one
|
|
4297
|
+
* crosses a zone or a DST boundary the same span lands on a different cell:
|
|
4298
|
+
* `new Date("2026-05-18")` is UTC midnight, which is the *17th* anywhere west
|
|
4299
|
+
* of Greenwich. Modelling the domain as `YYYY-MM-DD` removes that entire bug
|
|
4300
|
+
* class by construction, and buys three things for free: values compare
|
|
4301
|
+
* **lexicographically** (`a <= b` *is* "on or before"), they are `===`-equal
|
|
4302
|
+
* when they mean the same day (a `Date` never is), and they are already the
|
|
4303
|
+
* wire format every backend sends.
|
|
4304
|
+
*
|
|
4305
|
+
* {@link FoldTimelineNode} keeps a native `Date` on purpose — it plots dated
|
|
4306
|
+
* *instants* on a rail, which is the other domain.
|
|
4307
|
+
*
|
|
4308
|
+
* Arithmetic runs through `Date.UTC`, never local time: UTC has no DST, so a
|
|
4309
|
+
* day is always exactly 86 400 000 ms and "add 1 day" can never silently
|
|
4310
|
+
* return the same date twice (or skip one) around a clock change.
|
|
4311
|
+
*
|
|
4312
|
+
* ## Supported range
|
|
4313
|
+
* `0001-01-01` to `9999-12-31`. The four-digit year is what makes the
|
|
4314
|
+
* lexicographic guarantee hold, so it is enforced rather than assumed: years
|
|
4315
|
+
* below 1000 are zero-padded (`0099-12-31`, which still sorts correctly), and
|
|
4316
|
+
* arithmetic that walks past `9999-12-31` returns a five-digit string that
|
|
4317
|
+
* {@link foldIsCalendarDate} rejects — a value that fails the type's own
|
|
4318
|
+
* predicate rather than one that silently sorts before every other date.
|
|
4319
|
+
*/
|
|
4320
|
+
/** Milliseconds in a UTC day — exact, since UTC has no DST. */
|
|
4321
|
+
const MS_PER_DAY = 86_400_000;
|
|
4322
|
+
/** `FoldWeekday` → the index `Date.prototype.getUTCDay()` returns for it. */
|
|
4323
|
+
const WEEKDAY_INDEX = {
|
|
4324
|
+
sun: 0,
|
|
4325
|
+
mon: 1,
|
|
4326
|
+
tue: 2,
|
|
4327
|
+
wed: 3,
|
|
4328
|
+
thu: 4,
|
|
4329
|
+
fri: 5,
|
|
4330
|
+
sat: 6,
|
|
4331
|
+
};
|
|
4332
|
+
/** `getUTCDay()` index → the `FoldWeekday` it stands for. */
|
|
4333
|
+
const WEEKDAY_AT = [
|
|
4334
|
+
"sun",
|
|
4335
|
+
"mon",
|
|
4336
|
+
"tue",
|
|
4337
|
+
"wed",
|
|
4338
|
+
"thu",
|
|
4339
|
+
"fri",
|
|
4340
|
+
"sat",
|
|
4341
|
+
];
|
|
4342
|
+
/**
|
|
4343
|
+
* The days a calendar shades as the weekend by default. A separate fact from
|
|
4344
|
+
* the week's anchor day: a Sunday-first calendar still rests on Sat/Sun, and a
|
|
4345
|
+
* Saturday-first one (much of the Middle East) rests on Fri/Sat while starting
|
|
4346
|
+
* on Saturday — which is why the two are different inputs, not one derived
|
|
4347
|
+
* from the other.
|
|
4348
|
+
*/
|
|
4349
|
+
const FOLD_DEFAULT_WEEKEND_DAYS = [
|
|
4350
|
+
"sat",
|
|
4351
|
+
"sun",
|
|
4352
|
+
];
|
|
4353
|
+
/** `YYYY-MM-DD`, structurally. Rejects `2026-5-1`, `2026-05-1`, junk suffixes. */
|
|
4354
|
+
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
4355
|
+
/** Left-pads a 1–2 digit date part to the two digits the format requires. */
|
|
4356
|
+
function pad2(value) {
|
|
4357
|
+
return value < 10 ? `0${value}` : `${value}`;
|
|
4358
|
+
}
|
|
4359
|
+
/**
|
|
4360
|
+
* Left-pads a year to four digits. Year 99 has to render as `0099`, not `99`,
|
|
4361
|
+
* or it sorts before every other date and breaks the whole comparison model.
|
|
4362
|
+
*/
|
|
4363
|
+
function pad4(value) {
|
|
4364
|
+
if (value >= 1000) {
|
|
4365
|
+
return `${value}`;
|
|
4366
|
+
}
|
|
4367
|
+
if (value >= 100) {
|
|
4368
|
+
return `0${value}`;
|
|
4369
|
+
}
|
|
4370
|
+
return value >= 10 ? `00${value}` : `000${value}`;
|
|
4371
|
+
}
|
|
4372
|
+
/**
|
|
4373
|
+
* Whether `value` is a well-formed **and real** plain date. Catches both a
|
|
4374
|
+
* malformed shape (`"2026-5-1"`) and an impossible day (`"2026-02-30"`), the
|
|
4375
|
+
* latter by round-tripping through UTC and checking the parts survived.
|
|
4376
|
+
*/
|
|
4377
|
+
function foldIsCalendarDate(value) {
|
|
4378
|
+
if (!DATE_PATTERN.test(value)) {
|
|
4379
|
+
return false;
|
|
4380
|
+
}
|
|
4381
|
+
const year = Number(value.slice(0, 4));
|
|
4382
|
+
const month = Number(value.slice(5, 7));
|
|
4383
|
+
const day = Number(value.slice(8, 10));
|
|
4384
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) {
|
|
4385
|
+
return false;
|
|
4386
|
+
}
|
|
4387
|
+
const parsed = new Date(Date.UTC(year, month - 1, day));
|
|
4388
|
+
return parsed.getUTCMonth() === month - 1 && parsed.getUTCDate() === day;
|
|
4389
|
+
}
|
|
4390
|
+
/**
|
|
4391
|
+
* Epoch milliseconds at UTC midnight of `date` — the internal representation
|
|
4392
|
+
* every computation here runs on. A malformed date yields `NaN`, which
|
|
4393
|
+
* propagates visibly rather than silently shifting a result.
|
|
4394
|
+
*/
|
|
4395
|
+
function toUtcMs(date) {
|
|
4396
|
+
const year = Number(date.slice(0, 4));
|
|
4397
|
+
const month = Number(date.slice(5, 7));
|
|
4398
|
+
const day = Number(date.slice(8, 10));
|
|
4399
|
+
const at = new Date(Date.UTC(year, month - 1, day));
|
|
4400
|
+
// `Date.UTC` maps years 0–99 onto 1900–1999; only `setUTCFullYear` undoes it.
|
|
4401
|
+
if (year < 100) {
|
|
4402
|
+
at.setUTCFullYear(year, month - 1, day);
|
|
4403
|
+
}
|
|
4404
|
+
return at.getTime();
|
|
4405
|
+
}
|
|
4406
|
+
/** UTC midnight milliseconds back to `YYYY-MM-DD`. */
|
|
4407
|
+
function fromUtcMs(ms) {
|
|
4408
|
+
const at = new Date(ms);
|
|
4409
|
+
return `${pad4(at.getUTCFullYear())}-${pad2(at.getUTCMonth() + 1)}-${pad2(at.getUTCDate())}`;
|
|
4410
|
+
}
|
|
4411
|
+
/**
|
|
4412
|
+
* `date` as a whole number of days since the epoch.
|
|
4413
|
+
*
|
|
4414
|
+
* The internal currency of anything that sorts, compares or clips a lot of
|
|
4415
|
+
* dates. Strings are the right **model** — they are the wire format and they
|
|
4416
|
+
* cannot carry a zone — but they are the wrong thing to compare a hundred
|
|
4417
|
+
* thousand times: every `foldDaysBetween` in a sort comparator re-parses two
|
|
4418
|
+
* strings and builds two `Date`s. Converting once at the door and working on
|
|
4419
|
+
* integers is 6× faster on the month grid's hot path, with the same output.
|
|
4420
|
+
*
|
|
4421
|
+
* Not on the public surface: it is a representation, not a model.
|
|
4422
|
+
*/
|
|
4423
|
+
function foldToEpochDay(date) {
|
|
4424
|
+
return Math.round(toUtcMs(date) / MS_PER_DAY);
|
|
4425
|
+
}
|
|
4426
|
+
/** The inverse of {@link foldToEpochDay}. */
|
|
4427
|
+
function foldFromEpochDay(day) {
|
|
4428
|
+
return fromUtcMs(day * MS_PER_DAY);
|
|
4429
|
+
}
|
|
4430
|
+
/**
|
|
4431
|
+
* Today, read from the **local** clock — the day the user believes it is,
|
|
4432
|
+
* which is what a calendar highlights. Pass `now` to pin it (tests, a
|
|
4433
|
+
* workspace clock).
|
|
4434
|
+
*/
|
|
4435
|
+
function foldToday(now = new Date()) {
|
|
4436
|
+
return foldFromNativeDate(now);
|
|
4437
|
+
}
|
|
4438
|
+
/**
|
|
4439
|
+
* A native `Date` read as the plain date it shows on a **local** clock — the
|
|
4440
|
+
* inbound bridge, and the one conversion every consumer would otherwise write
|
|
4441
|
+
* by hand as `date.toISOString().slice(0, 10)`, which is UTC and therefore off
|
|
4442
|
+
* by a day for anyone west of Greenwich after 6pm. That is the exact bug this
|
|
4443
|
+
* whole module exists to remove, so the correct form ships with it.
|
|
4444
|
+
*
|
|
4445
|
+
* Pair with {@link foldToNativeDate} for the way out.
|
|
4446
|
+
*/
|
|
4447
|
+
function foldFromNativeDate(date) {
|
|
4448
|
+
return `${pad4(date.getFullYear())}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
|
|
4449
|
+
}
|
|
4450
|
+
/** `date` shifted by `days` (negative moves back). */
|
|
4451
|
+
function foldAddDays(date, days) {
|
|
4452
|
+
return fromUtcMs(toUtcMs(date) + days * MS_PER_DAY);
|
|
4453
|
+
}
|
|
4454
|
+
/**
|
|
4455
|
+
* Whole days from `from` to `to` — signed, and **exclusive** (same day → `0`),
|
|
4456
|
+
* so a span's length is `foldDaysBetween(start, end) + 1`.
|
|
4457
|
+
*/
|
|
4458
|
+
function foldDaysBetween(from, to) {
|
|
4459
|
+
return Math.round((toUtcMs(to) - toUtcMs(from)) / MS_PER_DAY);
|
|
4460
|
+
}
|
|
4461
|
+
/**
|
|
4462
|
+
* `date` shifted by `months`, clamping the day to the target month's length so
|
|
4463
|
+
* the result is always a real date (31 Jan + 1 month → 28/29 Feb, never 3 Mar).
|
|
4464
|
+
*/
|
|
4465
|
+
function foldAddMonths(date, months) {
|
|
4466
|
+
const day = Number(date.slice(8, 10));
|
|
4467
|
+
const target = new Date(utcMonthMs(date, months));
|
|
4468
|
+
const lastDay = new Date(lastDayOfMonthMs(target.getUTCFullYear(), target.getUTCMonth())).getUTCDate();
|
|
4469
|
+
return `${pad4(target.getUTCFullYear())}-${pad2(target.getUTCMonth() + 1)}-${pad2(Math.min(day, lastDay))}`;
|
|
4470
|
+
}
|
|
4471
|
+
/** The 1st of `date`'s month shifted by `months`, in epoch ms. */
|
|
4472
|
+
function utcMonthMs(date, months) {
|
|
4473
|
+
const year = Number(date.slice(0, 4));
|
|
4474
|
+
const month = Number(date.slice(5, 7));
|
|
4475
|
+
const at = new Date(Date.UTC(year, month - 1 + months, 1));
|
|
4476
|
+
if (year < 100) {
|
|
4477
|
+
at.setUTCFullYear(year, month - 1 + months, 1);
|
|
4478
|
+
}
|
|
4479
|
+
return at.getTime();
|
|
4480
|
+
}
|
|
4481
|
+
/** Day 0 of the following month — i.e. the last day of this one, in epoch ms. */
|
|
4482
|
+
function lastDayOfMonthMs(year, monthIndex) {
|
|
4483
|
+
const at = new Date(Date.UTC(year, monthIndex + 1, 0));
|
|
4484
|
+
if (year < 100) {
|
|
4485
|
+
at.setUTCFullYear(year, monthIndex + 1, 0);
|
|
4486
|
+
}
|
|
4487
|
+
return at.getTime();
|
|
4488
|
+
}
|
|
4489
|
+
/** The first day of `date`'s month. */
|
|
4490
|
+
function foldStartOfMonth(date) {
|
|
4491
|
+
return `${date.slice(0, 7)}-01`;
|
|
4492
|
+
}
|
|
4493
|
+
/** The last day of `date`'s month (28–31, leap years included). */
|
|
4494
|
+
function foldEndOfMonth(date) {
|
|
4495
|
+
const year = Number(date.slice(0, 4));
|
|
4496
|
+
const month = Number(date.slice(5, 7));
|
|
4497
|
+
return fromUtcMs(lastDayOfMonthMs(year, month - 1));
|
|
4498
|
+
}
|
|
4499
|
+
/**
|
|
4500
|
+
* The start of the week `date` falls in, for a calendar anchored on
|
|
4501
|
+
* `weekStartsOn`. Anchor-agnostic: a Monday-first and a Sunday-first calendar
|
|
4502
|
+
* ask the same question and get their own answer.
|
|
4503
|
+
*/
|
|
4504
|
+
function foldStartOfWeek(date, weekStartsOn = "mon") {
|
|
4505
|
+
const ms = toUtcMs(date);
|
|
4506
|
+
const dayOfWeek = new Date(ms).getUTCDay();
|
|
4507
|
+
const offset = (dayOfWeek - WEEKDAY_INDEX[weekStartsOn] + 7) % 7;
|
|
4508
|
+
return fromUtcMs(ms - offset * MS_PER_DAY);
|
|
4509
|
+
}
|
|
4510
|
+
/**
|
|
4511
|
+
* `date`'s position in its week, `0` on the anchor day — the column a day
|
|
4512
|
+
* occupies in a 7-wide grid.
|
|
4513
|
+
*/
|
|
4514
|
+
function foldWeekdayIndex(date, weekStartsOn = "mon") {
|
|
4515
|
+
return ((new Date(toUtcMs(date)).getUTCDay() - WEEKDAY_INDEX[weekStartsOn] + 7) % 7);
|
|
4516
|
+
}
|
|
4517
|
+
/**
|
|
4518
|
+
* The ISO-8601 week number `date` falls in, `1`–`53`.
|
|
4519
|
+
*
|
|
4520
|
+
* ISO weeks always start on Monday and belong to the year holding their
|
|
4521
|
+
* **Thursday**, which is why 1 January is sometimes week 52 of the year before
|
|
4522
|
+
* — the rule European B2B reporting runs on, and the reason this cannot be
|
|
4523
|
+
* derived from the calendar's own `weekStartsOn`.
|
|
4524
|
+
*/
|
|
4525
|
+
function foldIsoWeek(date) {
|
|
4526
|
+
const day = foldToEpochDay(date);
|
|
4527
|
+
// Epoch day 0 is a Thursday, so `+3` puts Monday at 0.
|
|
4528
|
+
const mondayOffset = (((day + 3) % 7) + 7) % 7;
|
|
4529
|
+
const thursday = day - mondayOffset + 3;
|
|
4530
|
+
const year = Number(foldFromEpochDay(thursday).slice(0, 4));
|
|
4531
|
+
const firstThursdayWeek = foldToEpochDay(`${pad4(year)}-01-01`);
|
|
4532
|
+
return Math.floor((thursday - firstThursdayWeek) / 7) + 1;
|
|
4533
|
+
}
|
|
4534
|
+
/** The ISO week-numbering year of `date` — not always its calendar year. */
|
|
4535
|
+
function foldIsoWeekYear(date) {
|
|
4536
|
+
const day = foldToEpochDay(date);
|
|
4537
|
+
const mondayOffset = (((day + 3) % 7) + 7) % 7;
|
|
4538
|
+
return Number(foldFromEpochDay(day - mondayOffset + 3).slice(0, 4));
|
|
4539
|
+
}
|
|
4540
|
+
/** Which day of the week `date` falls on, anchor-independent. */
|
|
4541
|
+
function foldWeekdayOf(date) {
|
|
4542
|
+
return WEEKDAY_AT[new Date(toUtcMs(date)).getUTCDay()] ?? "sun";
|
|
4543
|
+
}
|
|
4544
|
+
/**
|
|
4545
|
+
* Whether `date` is one of the days a calendar shades as the weekend.
|
|
4546
|
+
*
|
|
4547
|
+
* Takes the weekend **as data**, because it is not derivable from the week's
|
|
4548
|
+
* anchor: the two are independent locale facts. Deriving it — "the last two
|
|
4549
|
+
* columns" — shades Friday and Saturday on a Sunday-first calendar and calls
|
|
4550
|
+
* Sunday a working day, which is wrong everywhere it is used.
|
|
4551
|
+
*
|
|
4552
|
+
* @default FOLD_DEFAULT_WEEKEND_DAYS (`['sat', 'sun']`)
|
|
4553
|
+
*/
|
|
4554
|
+
function foldIsWeekend(date, weekendDays = FOLD_DEFAULT_WEEKEND_DAYS) {
|
|
4555
|
+
return weekendDays.includes(foldWeekdayOf(date));
|
|
4556
|
+
}
|
|
4557
|
+
/**
|
|
4558
|
+
* A `Temporal` value read as the plain date it starts with.
|
|
4559
|
+
*
|
|
4560
|
+
* `Temporal.PlainDate.toString()` **is** `YYYY-MM-DD` — the primitive this
|
|
4561
|
+
* module chose for correctness turns out to be exactly Temporal-shaped, which
|
|
4562
|
+
* no `Date`-based calendar can say. Going the other way needs no helper at all:
|
|
4563
|
+
* `Temporal.PlainDate.from(foldDate)` already accepts one of ours.
|
|
4564
|
+
*
|
|
4565
|
+
* A `PlainDateTime` or a `ZonedDateTime` drops its time here, which is the
|
|
4566
|
+
* point — this family plots whole days, and the day you mean is the one in the
|
|
4567
|
+
* value's own calendar and zone, not the one UTC would have given you.
|
|
4568
|
+
*
|
|
4569
|
+
* Returns `""` for anything that is not an ISO date, so a bad value fails
|
|
4570
|
+
* {@link foldIsCalendarDate} rather than silently rendering the wrong month.
|
|
4571
|
+
*/
|
|
4572
|
+
function foldFromTemporal(value) {
|
|
4573
|
+
const candidate = value.toString().slice(0, 10);
|
|
4574
|
+
return foldIsCalendarDate(candidate) ? candidate : "";
|
|
4575
|
+
}
|
|
4576
|
+
/**
|
|
4577
|
+
* A `Date` at **UTC midnight** of `date` — the bridge to `Intl.DateTimeFormat`
|
|
4578
|
+
* for month and weekday names. Format it with `timeZone: "UTC"`, or the local
|
|
4579
|
+
* zone will shift the label back a day west of Greenwich.
|
|
4580
|
+
*/
|
|
4581
|
+
function foldToNativeDate(date) {
|
|
4582
|
+
return new Date(toUtcMs(date));
|
|
4583
|
+
}
|
|
4584
|
+
|
|
4585
|
+
/**
|
|
4586
|
+
* Wall-clock time for the time grid — `HH:mm`, 24-hour, **no zone**.
|
|
4587
|
+
*
|
|
4588
|
+
* ## Why not an instant
|
|
4589
|
+
* The same decision as the date, for the same reason. A meeting at 09:00 is at
|
|
4590
|
+
* 09:00 on the wall the person is looking at; storing it as an instant means
|
|
4591
|
+
* every render re-derives that wall time from a zone, and the moment the zone
|
|
4592
|
+
* is wrong — a server default, a user travelling, a DST edge — the grid draws
|
|
4593
|
+
* the wrong hour. `"09:00"` cannot be wrong about which hour it means.
|
|
4594
|
+
*
|
|
4595
|
+
* The zone is the **app's** boundary, exactly like the date: convert once on
|
|
4596
|
+
* the way in ({@link foldFromTemporal} takes a `ZonedDateTime` and drops it to
|
|
4597
|
+
* the day and, here, the caller reads its `.hour`/`.minute`), and the calendar
|
|
4598
|
+
* never has to know.
|
|
4599
|
+
*
|
|
4600
|
+
* Times compare **lexicographically** like the dates do — `"09:00" < "14:30"` —
|
|
4601
|
+
* which is what makes overlap detection a string comparison rather than a
|
|
4602
|
+
* parse. `24:00` is legal and means the end of the day, so a span that runs to
|
|
4603
|
+
* midnight has an end greater than its start.
|
|
4604
|
+
*/
|
|
4605
|
+
/** Minutes in a day — the grid's own unit. */
|
|
4606
|
+
const FOLD_MINUTES_PER_DAY = 1440;
|
|
4607
|
+
/** `HH:mm`, structurally. Rejects `9:00`, `09:00:00`, junk suffixes. */
|
|
4608
|
+
const TIME_PATTERN = /^\d{2}:\d{2}$/;
|
|
4609
|
+
/**
|
|
4610
|
+
* Whether `value` is a well-formed **and real** wall-clock time.
|
|
4611
|
+
*
|
|
4612
|
+
* The shape check is not enough on its own: `"10:75"` and `"23:60"` both match
|
|
4613
|
+
* `HH:mm` and both sit under 1440 minutes, so a range check on the total would
|
|
4614
|
+
* wave them through. The fields are validated separately — minutes `00`–`59`,
|
|
4615
|
+
* hours `00`–`23`, plus the single `24:00` that means the end of the day — the
|
|
4616
|
+
* same rigour {@link foldIsCalendarDate} applies to `2026-02-30`.
|
|
4617
|
+
*/
|
|
4618
|
+
function foldIsCalendarTime(value) {
|
|
4619
|
+
if (!TIME_PATTERN.test(value)) {
|
|
4620
|
+
return false;
|
|
4621
|
+
}
|
|
4622
|
+
const hours = Number(value.slice(0, 2));
|
|
4623
|
+
const minutes = Number(value.slice(3, 5));
|
|
4624
|
+
if (minutes > 59 || hours > 24) {
|
|
4625
|
+
return false;
|
|
4626
|
+
}
|
|
4627
|
+
// Hour 24 exists only as `24:00`, the exclusive end of the day.
|
|
4628
|
+
return hours < 24 || minutes === 0;
|
|
4629
|
+
}
|
|
4630
|
+
/** Minutes since midnight — the number the grid positions on. */
|
|
4631
|
+
function foldToMinutes(time) {
|
|
4632
|
+
return Number(time.slice(0, 2)) * 60 + Number(time.slice(3, 5));
|
|
4633
|
+
}
|
|
4634
|
+
/** Minutes since midnight back to `HH:mm`, clamped to the day. */
|
|
4635
|
+
function foldFromMinutes(minutes) {
|
|
4636
|
+
const clamped = Math.max(0, Math.min(FOLD_MINUTES_PER_DAY, Math.round(minutes)));
|
|
4637
|
+
const hours = Math.floor(clamped / 60);
|
|
4638
|
+
const rest = clamped % 60;
|
|
4639
|
+
return `${hours < 10 ? "0" : ""}${hours}:${rest < 10 ? "0" : ""}${rest}`;
|
|
4640
|
+
}
|
|
4641
|
+
/** `time` shifted by `minutes`, clamped to the day rather than wrapping. */
|
|
4642
|
+
function foldAddMinutes(time, minutes) {
|
|
4643
|
+
return foldFromMinutes(foldToMinutes(time) + minutes);
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4646
|
+
/**
|
|
4647
|
+
* Every date format the calendar family renders, in one table.
|
|
4648
|
+
*
|
|
4649
|
+
* They used to be five components' worth of inline `Intl.DateTimeFormat`
|
|
4650
|
+
* option bags — which is how the day view ended up asking for
|
|
4651
|
+
* `{ weekday: 'long', month: 'long' }` and printing "Saturday May", a phrase in
|
|
4652
|
+
* no locale. Side by side they can be read against each other, and a consumer
|
|
4653
|
+
* can see what a locale is actually asked for.
|
|
4654
|
+
*
|
|
4655
|
+
* `timeZone: 'UTC'` is not optional: a plain date is bridged to `Date` at UTC
|
|
4656
|
+
* midnight, so formatting it in the local zone would print the day before for
|
|
4657
|
+
* anyone west of Greenwich.
|
|
4658
|
+
*
|
|
4659
|
+
* `calendar: 'gregory'` is pinned for the same reason `timeZone` is: the grid
|
|
4660
|
+
* counts days and groups months in the Gregorian calendar (that is what a
|
|
4661
|
+
* `YYYY-MM-DD` string *is*), so the names beside those numbers must be Gregorian
|
|
4662
|
+
* too. Left to the locale's own default, `ar-SA` would print Hijri month names
|
|
4663
|
+
* over Gregorian day numbers on rows that break on Gregorian months — a
|
|
4664
|
+
* calendar contradicting itself. The family is Gregorian, localised into the
|
|
4665
|
+
* language of the locale, not into its calendar system; a non-Gregorian layout
|
|
4666
|
+
* is a separate widget this one does not pretend to be.
|
|
4667
|
+
*/
|
|
4668
|
+
const FOLD_CALENDAR_FORMATS = {
|
|
4669
|
+
/** Whole date, spelled out — accessible names. */
|
|
4670
|
+
dateFull: { dateStyle: "long" },
|
|
4671
|
+
/** Whole date **with its weekday** — a period title, where it is the heading. */
|
|
4672
|
+
dateFullWeekday: { dateStyle: "full" },
|
|
4673
|
+
/** `18 May 2026` — the ends of a week, which `formatRange` then joins. */
|
|
4674
|
+
dayMonthYear: { day: "numeric", month: "long", year: "numeric" },
|
|
4675
|
+
/** `Mon` — grid column headers. */
|
|
4676
|
+
weekdayShort: { weekday: "short" },
|
|
4677
|
+
/** `Monday` — a day heading, where there is room. */
|
|
4678
|
+
weekdayLong: { weekday: "long" },
|
|
4679
|
+
/** `May` — beside a day number, never on its own with a weekday. */
|
|
4680
|
+
monthLong: { month: "long" },
|
|
4681
|
+
/** `May` (abbreviated) — the agenda rail, where width is scarce. */
|
|
4682
|
+
monthShort: { month: "short" },
|
|
4683
|
+
/** `May 2026` — a period title. */
|
|
4684
|
+
monthYear: { month: "long", year: "numeric" },
|
|
4685
|
+
/** `18 May` — a span's ends, which `formatRange` then joins. */
|
|
4686
|
+
dayMonthShort: { day: "numeric", month: "short" },
|
|
4687
|
+
/** `09:00` in France, `9:00 AM` in the US — an hour down the grid's gutter. */
|
|
4688
|
+
hourMark: { hour: "numeric", minute: "2-digit" },
|
|
4689
|
+
};
|
|
4690
|
+
/**
|
|
4691
|
+
* Formatters are expensive to build and perfectly reusable, so they are cached
|
|
4692
|
+
* per locale + format. The cache is module-level on purpose: two calendars in
|
|
4693
|
+
* the same locale should not each pay for their own.
|
|
4694
|
+
*/
|
|
4695
|
+
const CACHE$1 = new Map();
|
|
4696
|
+
/** The `Intl` formatter for one locale and format, built at most once. */
|
|
4697
|
+
function foldCalendarFormatter(locale, format, override) {
|
|
4698
|
+
const options = { ...FOLD_CALENDAR_FORMATS[format], ...override };
|
|
4699
|
+
const cacheKey = override === undefined
|
|
4700
|
+
? `${locale ?? ""}|${format}`
|
|
4701
|
+
: `${locale ?? ""}|${format}|${JSON.stringify(options)}`;
|
|
4702
|
+
const cached = CACHE$1.get(cacheKey);
|
|
4703
|
+
if (cached !== undefined) {
|
|
4704
|
+
return cached;
|
|
4705
|
+
}
|
|
4706
|
+
const built = new Intl.DateTimeFormat(locale, {
|
|
4707
|
+
...options,
|
|
4708
|
+
timeZone: "UTC",
|
|
4709
|
+
calendar: "gregory",
|
|
4710
|
+
});
|
|
4711
|
+
CACHE$1.set(cacheKey, built);
|
|
4712
|
+
return built;
|
|
4713
|
+
}
|
|
4714
|
+
/** One plain date, formatted. */
|
|
4715
|
+
function foldFormatDate(date, locale, format, override) {
|
|
4716
|
+
return foldCalendarFormatter(locale, format, override).format(foldToNativeDate(date));
|
|
4717
|
+
}
|
|
4718
|
+
/**
|
|
4719
|
+
* A span, collapsed to one date when it covers a single day — `Intl`'s own
|
|
4720
|
+
* range formatter does that, and picks the locale's separator.
|
|
4721
|
+
*/
|
|
4722
|
+
function foldFormatRange(from, to, locale, format, override) {
|
|
4723
|
+
return foldCalendarFormatter(locale, format, override).formatRange(foldToNativeDate(from), foldToNativeDate(to));
|
|
4724
|
+
}
|
|
4725
|
+
/** A day with no meaning of its own, used only to hang a wall-clock time on. */
|
|
4726
|
+
const TIME_ANCHOR = Date.UTC(2026, 0, 1);
|
|
4727
|
+
/**
|
|
4728
|
+
* A wall-clock time in the locale's own convention — `14:00` in France, `2 PM`
|
|
4729
|
+
* in the United States. Formatted at UTC on a throwaway date, because the time
|
|
4730
|
+
* has no zone and no day: only its hours and minutes are real.
|
|
4731
|
+
*/
|
|
4732
|
+
function foldFormatTime(time, locale, override) {
|
|
4733
|
+
const at = new Date(TIME_ANCHOR + foldToMinutes(time) * 60_000);
|
|
4734
|
+
return foldCalendarFormatter(locale, "hourMark", override).format(at);
|
|
4735
|
+
}
|
|
4736
|
+
|
|
4737
|
+
/** The English defaults — the base every override merges onto. */
|
|
4738
|
+
const FOLD_CALENDAR_DEFAULT_LABELS = {
|
|
4739
|
+
grid: "Calendar",
|
|
4740
|
+
today: "today",
|
|
4741
|
+
moreEvents: (count) => `+${count} more`,
|
|
4742
|
+
eventCount: (count) => (count === 1 ? "1 event" : `${count} events`),
|
|
4743
|
+
hiddenCount: (count) => `${count} not shown`,
|
|
4744
|
+
allDay: "All day",
|
|
4745
|
+
hours: "Hours",
|
|
4746
|
+
weekNumberShort: "Wk",
|
|
4747
|
+
weekNumber: (week) => `Week ${week}`,
|
|
4748
|
+
emptyDay: "Nothing on this day.",
|
|
4749
|
+
emptyRange: "Nothing in this range.",
|
|
4750
|
+
todayAction: "Today",
|
|
4751
|
+
previousPeriod: "Previous",
|
|
4752
|
+
nextPeriod: "Next",
|
|
4753
|
+
viewSwitch: "Calendar view",
|
|
4754
|
+
viewMonth: "Month",
|
|
4755
|
+
viewWeek: "Week",
|
|
4756
|
+
viewDay: "Day",
|
|
4757
|
+
viewList: "List",
|
|
4758
|
+
agenda: "Agenda",
|
|
4759
|
+
agendaModes: "Agenda slice",
|
|
4760
|
+
agendaTodo: "To handle",
|
|
4761
|
+
agendaUpcoming: "Upcoming",
|
|
4762
|
+
agendaEmptyTodo: "Nothing to handle — all up to date.",
|
|
4763
|
+
agendaEmptyUpcoming: "Nothing coming up.",
|
|
4764
|
+
agendaMore: (days) => (days === 1 ? "1 more day" : `${days} more days`),
|
|
4765
|
+
relativeToday: "Today",
|
|
4766
|
+
relativeTomorrow: "Tomorrow",
|
|
4767
|
+
collapse: "Collapse",
|
|
4768
|
+
expand: "Expand",
|
|
4769
|
+
sourceFilter: "Filter by source",
|
|
4770
|
+
sourceState: (label, shown) => `${label}, ${shown ? "shown" : "hidden"}`,
|
|
4771
|
+
};
|
|
4772
|
+
/** App-wide calendar labels; defaults to English. */
|
|
4773
|
+
const FOLD_CALENDAR_LABELS = new InjectionToken("FOLD_CALENDAR_LABELS", { factory: () => FOLD_CALENDAR_DEFAULT_LABELS });
|
|
4774
|
+
/** Provide calendar labels for the whole app, merged over the English defaults. */
|
|
4775
|
+
function provideFoldCalendarLabels(labels) {
|
|
4776
|
+
return {
|
|
4777
|
+
provide: FOLD_CALENDAR_LABELS,
|
|
4778
|
+
useValue: { ...FOLD_CALENDAR_DEFAULT_LABELS, ...labels },
|
|
4779
|
+
};
|
|
4780
|
+
}
|
|
4781
|
+
|
|
4782
|
+
/** ISO-8601 day numbers, `1` = Monday … `7` = Sunday — what `Intl` returns. */
|
|
4783
|
+
const ISO_DAY = [
|
|
4784
|
+
"mon",
|
|
4785
|
+
"tue",
|
|
4786
|
+
"wed",
|
|
4787
|
+
"thu",
|
|
4788
|
+
"fri",
|
|
4789
|
+
"sat",
|
|
4790
|
+
"sun",
|
|
4791
|
+
];
|
|
4792
|
+
/** What a runtime that cannot answer gets: the most common week in the world. */
|
|
4793
|
+
const FOLD_FALLBACK_WEEK_INFO = {
|
|
4794
|
+
weekStartsOn: "mon",
|
|
4795
|
+
weekendDays: ["sat", "sun"],
|
|
4796
|
+
};
|
|
4797
|
+
const CACHE = new Map();
|
|
4798
|
+
/** `1`–`7` → a weekday, ignoring anything out of range. */
|
|
4799
|
+
function toWeekday(isoDay) {
|
|
4800
|
+
if (typeof isoDay !== "number") {
|
|
4801
|
+
return null;
|
|
4802
|
+
}
|
|
4803
|
+
return ISO_DAY[isoDay - 1] ?? null;
|
|
4804
|
+
}
|
|
4805
|
+
/** A no-argument method, narrowed without an assertion. */
|
|
4806
|
+
function isThunk(value) {
|
|
4807
|
+
return typeof value === "function";
|
|
4808
|
+
}
|
|
4809
|
+
/** The `{ firstDay, weekend }` shape, whichever way the runtime exposes it. */
|
|
4810
|
+
function readWeekInfo(locale) {
|
|
4811
|
+
if ("getWeekInfo" in locale) {
|
|
4812
|
+
const method = locale.getWeekInfo;
|
|
4813
|
+
if (isThunk(method)) {
|
|
4814
|
+
return method.call(locale);
|
|
4815
|
+
}
|
|
4816
|
+
}
|
|
4817
|
+
if ("weekInfo" in locale) {
|
|
4818
|
+
return locale.weekInfo;
|
|
4819
|
+
}
|
|
4820
|
+
return null;
|
|
4821
|
+
}
|
|
4822
|
+
/** Narrows the probed value without trusting its shape. */
|
|
4823
|
+
function parseWeekInfo(raw) {
|
|
4824
|
+
if (typeof raw !== "object" || raw === null) {
|
|
4825
|
+
return null;
|
|
4826
|
+
}
|
|
4827
|
+
const firstDay = "firstDay" in raw ? toWeekday(raw.firstDay) : null;
|
|
4828
|
+
const weekendRaw = "weekend" in raw ? raw.weekend : null;
|
|
4829
|
+
const weekend = Array.isArray(weekendRaw)
|
|
4830
|
+
? weekendRaw
|
|
4831
|
+
.map(toWeekday)
|
|
4832
|
+
.filter((day) => day !== null)
|
|
4833
|
+
: [];
|
|
4834
|
+
if (firstDay === null) {
|
|
4835
|
+
return null;
|
|
4836
|
+
}
|
|
4837
|
+
return {
|
|
4838
|
+
weekStartsOn: firstDay,
|
|
4839
|
+
weekendDays: weekend.length > 0 ? weekend : FOLD_FALLBACK_WEEK_INFO.weekendDays,
|
|
4840
|
+
};
|
|
4841
|
+
}
|
|
4842
|
+
/**
|
|
4843
|
+
* The week conventions of `locale` — its first day and its weekend.
|
|
4844
|
+
*
|
|
4845
|
+
* `undefined` asks the runtime for its own locale, which is what a calendar
|
|
4846
|
+
* with no `locale` input is already formatting in.
|
|
4847
|
+
*/
|
|
4848
|
+
function foldLocaleWeekInfo(locale) {
|
|
4849
|
+
const tag = locale ?? new Intl.DateTimeFormat().resolvedOptions().locale;
|
|
4850
|
+
const cached = CACHE.get(tag);
|
|
4851
|
+
if (cached !== undefined) {
|
|
4852
|
+
return cached;
|
|
4853
|
+
}
|
|
4854
|
+
let info = FOLD_FALLBACK_WEEK_INFO;
|
|
4855
|
+
try {
|
|
4856
|
+
info = parseWeekInfo(readWeekInfo(new Intl.Locale(tag))) ?? info;
|
|
4857
|
+
}
|
|
4858
|
+
catch {
|
|
4859
|
+
// A malformed tag is the caller's bug, not a reason to render nothing.
|
|
4860
|
+
info = FOLD_FALLBACK_WEEK_INFO;
|
|
4861
|
+
}
|
|
4862
|
+
CACHE.set(tag, info);
|
|
4863
|
+
return info;
|
|
4864
|
+
}
|
|
4865
|
+
|
|
4866
|
+
/**
|
|
4867
|
+
* The three things every calendar view needs and none of them is about: its
|
|
4868
|
+
* labels, its locale, and the chip template a consumer may have projected.
|
|
4869
|
+
*
|
|
4870
|
+
* Applied through `hostDirectives`, so `locale` and `labels` are still ordinary
|
|
4871
|
+
* inputs on `<fold-calendar-month>`, `<fold-calendar-week>` and the rest — the
|
|
4872
|
+
* consumer sees no difference, and the five views stop carrying five identical
|
|
4873
|
+
* copies of the same eight lines.
|
|
4874
|
+
*
|
|
4875
|
+
* Internal — a view exposes it, it is never applied by hand.
|
|
4876
|
+
*
|
|
4877
|
+
* @example
|
|
4878
|
+
* ```ts
|
|
4879
|
+
* @Component({
|
|
4880
|
+
* hostDirectives: [
|
|
4881
|
+
* { directive: FoldCalendarChromeDirective, inputs: ['locale', 'labels'] },
|
|
4882
|
+
* ],
|
|
4883
|
+
* })
|
|
4884
|
+
* export class FoldCalendarWeekComponent<T = unknown> {
|
|
4885
|
+
* protected readonly chrome = inject(FoldCalendarChromeDirective);
|
|
4886
|
+
* }
|
|
4887
|
+
* ```
|
|
4888
|
+
*/
|
|
4889
|
+
class FoldCalendarChromeDirective {
|
|
4890
|
+
/** BCP-47 tag driving every `Intl` name; omitted, the runtime default. */
|
|
4891
|
+
locale = input(/* @ts-ignore */
|
|
4892
|
+
...(ngDevMode ? [undefined, { debugName: "locale" }] : /* istanbul ignore next */ []));
|
|
4893
|
+
/** Per-instance label overrides, merged over the app-wide (or English) set. */
|
|
4894
|
+
labels = input(/* @ts-ignore */
|
|
4895
|
+
...(ngDevMode ? [undefined, { debugName: "labels" }] : /* istanbul ignore next */ []));
|
|
4896
|
+
/**
|
|
4897
|
+
* Per-instance `Intl` option overrides, merged over
|
|
4898
|
+
* {@link FOLD_CALENDAR_FORMATS}. Labels let a locale translate; this lets it
|
|
4899
|
+
* **reformat** — a numeric weekday header, a four-digit year, a narrow month.
|
|
4900
|
+
*/
|
|
4901
|
+
formats = input(/* @ts-ignore */
|
|
4902
|
+
...(ngDevMode ? [undefined, { debugName: "formats" }] : /* istanbul ignore next */ []));
|
|
4903
|
+
injectedLabels = inject(FOLD_CALENDAR_LABELS);
|
|
4904
|
+
projectedEvent = contentChild(FoldCalendarEventDirective, /* @ts-ignore */
|
|
4905
|
+
...(ngDevMode ? [{ debugName: "projectedEvent" }] : /* istanbul ignore next */ []));
|
|
4906
|
+
/** Effective labels — the app-wide (or English) set, with `labels` on top. */
|
|
4907
|
+
l = computed(() => ({
|
|
4908
|
+
...this.injectedLabels,
|
|
4909
|
+
...this.labels(),
|
|
4910
|
+
}), /* @ts-ignore */
|
|
4911
|
+
...(ngDevMode ? [{ debugName: "l" }] : /* istanbul ignore next */ []));
|
|
4912
|
+
/** The custom chip template, when one is projected. */
|
|
4913
|
+
eventContent = computed(() => this.projectedEvent()?.template ?? null, /* @ts-ignore */
|
|
4914
|
+
...(ngDevMode ? [{ debugName: "eventContent" }] : /* istanbul ignore next */ []));
|
|
4915
|
+
/**
|
|
4916
|
+
* What this locale says its week looks like — the default behind
|
|
4917
|
+
* `weekStartsOn` and `weekendDays`, so a calendar is right in Cairo and
|
|
4918
|
+
* Chicago without the caller having to know either.
|
|
4919
|
+
*/
|
|
4920
|
+
weekInfo = computed(() => foldLocaleWeekInfo(this.locale()), /* @ts-ignore */
|
|
4921
|
+
...(ngDevMode ? [{ debugName: "weekInfo" }] : /* istanbul ignore next */ []));
|
|
4922
|
+
/** The anchor a view should use: the caller's, else the locale's. */
|
|
4923
|
+
anchor(explicit) {
|
|
4924
|
+
return explicit ?? this.weekInfo().weekStartsOn;
|
|
4925
|
+
}
|
|
4926
|
+
/** The weekend a view should shade: the caller's, else the locale's. */
|
|
4927
|
+
weekend(explicit) {
|
|
4928
|
+
return explicit ?? this.weekInfo().weekendDays;
|
|
4929
|
+
}
|
|
4930
|
+
/** One date, in this view's locale and its format overrides. */
|
|
4931
|
+
format(date, format) {
|
|
4932
|
+
return foldFormatDate(date, this.locale(), format, this.formats()?.[format]);
|
|
4933
|
+
}
|
|
4934
|
+
/** A wall-clock time, in this view's locale (24-hour, or AM/PM). */
|
|
4935
|
+
formatTime(time) {
|
|
4936
|
+
return foldFormatTime(time, this.locale(), this.formats()?.hourMark);
|
|
4937
|
+
}
|
|
4938
|
+
/** A span, in this view's locale and its format overrides. */
|
|
4939
|
+
formatRange(from, to, format) {
|
|
4940
|
+
return foldFormatRange(from, to, this.locale(), format, this.formats()?.[format]);
|
|
4941
|
+
}
|
|
4942
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarChromeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
4943
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.0.8", type: FoldCalendarChromeDirective, isStandalone: true, inputs: { locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, formats: { classPropertyName: "formats", publicName: "formats", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "projectedEvent", first: true, predicate: FoldCalendarEventDirective, descendants: true, isSignal: true }], ngImport: i0 });
|
|
4944
|
+
}
|
|
4945
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarChromeDirective, decorators: [{
|
|
4946
|
+
type: Directive,
|
|
4947
|
+
args: [{ standalone: true }]
|
|
4948
|
+
}], propDecorators: { locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], formats: [{ type: i0.Input, args: [{ isSignal: true, alias: "formats", required: false }] }], projectedEvent: [{ type: i0.ContentChild, args: [i0.forwardRef(() => FoldCalendarEventDirective), { isSignal: true }] }] } });
|
|
4949
|
+
|
|
4950
|
+
/**
|
|
4951
|
+
* Where a key press moves the focused day. Split out of the component because
|
|
4952
|
+
* "what does ArrowDown mean on a calendar" is a pure question about dates, and
|
|
4953
|
+
* answering it here means it can be tested without a grid, a DOM or a TestBed.
|
|
4954
|
+
*
|
|
4955
|
+
* {@link foldCalendarNextFocus} is public: it is the other half of the geometry
|
|
4956
|
+
* tier, and a hand-rolled date grid owes its users the same arrow keys as the
|
|
4957
|
+
* built-in one. {@link foldFocusDayCell} stays internal — it reads a
|
|
4958
|
+
* `data-fold-day` attribute this package writes, and exporting it would freeze
|
|
4959
|
+
* that attribute into the public contract.
|
|
4960
|
+
*/
|
|
4961
|
+
/**
|
|
4962
|
+
* The day `key` moves focus to from `from`, or `null` when the key is not a
|
|
4963
|
+
* grid-navigation key and the component should leave the event alone.
|
|
4964
|
+
*
|
|
4965
|
+
* The mapping is the ARIA grid pattern applied to a date axis: horizontal keys
|
|
4966
|
+
* step a day, vertical keys step a **week** (the column stays put, which is the
|
|
4967
|
+
* whole point of a 7-wide grid), `Home`/`End` snap to the week's bounds, and
|
|
4968
|
+
* `PageUp`/`PageDown` page by month.
|
|
4969
|
+
*/
|
|
4970
|
+
function foldCalendarNextFocus(key, from, weekStartsOn = "mon") {
|
|
4971
|
+
switch (key) {
|
|
4972
|
+
case "ArrowLeft":
|
|
4973
|
+
return foldAddDays(from, -1);
|
|
4974
|
+
case "ArrowRight":
|
|
4975
|
+
return foldAddDays(from, 1);
|
|
4976
|
+
case "ArrowUp":
|
|
4977
|
+
return foldAddDays(from, -7);
|
|
4978
|
+
case "ArrowDown":
|
|
4979
|
+
return foldAddDays(from, 7);
|
|
4980
|
+
case "Home":
|
|
4981
|
+
return foldStartOfWeek(from, weekStartsOn);
|
|
4982
|
+
case "End":
|
|
4983
|
+
return foldAddDays(foldStartOfWeek(from, weekStartsOn), 6);
|
|
4984
|
+
case "PageUp":
|
|
4985
|
+
return foldAddMonths(from, -1);
|
|
4986
|
+
case "PageDown":
|
|
4987
|
+
return foldAddMonths(from, 1);
|
|
4988
|
+
default:
|
|
4989
|
+
return null;
|
|
4990
|
+
}
|
|
4991
|
+
}
|
|
4992
|
+
/**
|
|
4993
|
+
* Focuses the cell for `date` inside `grid`, reporting whether it was there to
|
|
4994
|
+
* focus.
|
|
4995
|
+
*
|
|
4996
|
+
* A `false` is the signal that the target lies outside the rendered grid — the
|
|
4997
|
+
* caller pages the calendar and defers the focus until the new cells exist.
|
|
4998
|
+
* Taking the root as an argument keeps this free of `document` (so it is a
|
|
4999
|
+
* no-op under SSR) and means there is exactly **one** answer to "which element
|
|
5000
|
+
* is the grid" — looking it up from the event target as well gave two.
|
|
5001
|
+
*/
|
|
5002
|
+
function foldFocusDayCell(grid, date) {
|
|
5003
|
+
// A date can reach this from an input, and it is interpolated into a
|
|
5004
|
+
// selector: anything that is not a plain date is a `SyntaxError` waiting to
|
|
5005
|
+
// happen, so it is refused rather than escaped.
|
|
5006
|
+
if (grid === null || !foldIsCalendarDate(date)) {
|
|
5007
|
+
return false;
|
|
5008
|
+
}
|
|
5009
|
+
const cell = grid.querySelector(`[data-fold-day="${date}"]`);
|
|
5010
|
+
if (cell instanceof HTMLElement) {
|
|
5011
|
+
cell.focus();
|
|
5012
|
+
return true;
|
|
5013
|
+
}
|
|
5014
|
+
return false;
|
|
5015
|
+
}
|
|
5016
|
+
|
|
5017
|
+
class FoldCalendarRovingFocus {
|
|
5018
|
+
host;
|
|
5019
|
+
/** The day holding focus, once the user has moved it. */
|
|
5020
|
+
focused = signal(null, /* @ts-ignore */
|
|
5021
|
+
...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
|
|
5022
|
+
/** A day to focus once the grid has re-rendered around a new period. */
|
|
5023
|
+
deferred = signal(null, /* @ts-ignore */
|
|
5024
|
+
...(ngDevMode ? [{ debugName: "deferred" }] : /* istanbul ignore next */ []));
|
|
5025
|
+
constructor(host) {
|
|
5026
|
+
this.host = host;
|
|
5027
|
+
}
|
|
5028
|
+
/**
|
|
5029
|
+
* The cell carrying the tab stop: the focused day while it is on screen,
|
|
5030
|
+
* else the host's fallback — so paging never strands the grid without one.
|
|
5031
|
+
*/
|
|
5032
|
+
activeDate = computed(() => {
|
|
5033
|
+
const focused = this.focused();
|
|
5034
|
+
if (focused !== null && this.host.dates().has(focused)) {
|
|
5035
|
+
return focused;
|
|
5036
|
+
}
|
|
5037
|
+
return this.host.fallback();
|
|
5038
|
+
}, /* @ts-ignore */
|
|
5039
|
+
...(ngDevMode ? [{ debugName: "activeDate" }] : /* istanbul ignore next */ []));
|
|
5040
|
+
tabIndexFor(date) {
|
|
5041
|
+
return date === this.activeDate() ? 0 : -1;
|
|
5042
|
+
}
|
|
5043
|
+
/** A cell took focus — by keyboard, or by a click. */
|
|
5044
|
+
onFocus(date) {
|
|
5045
|
+
this.focused.set(date);
|
|
5046
|
+
}
|
|
5047
|
+
/**
|
|
5048
|
+
* Applies `key` to the date axis, reporting whether it was a navigation key.
|
|
5049
|
+
*
|
|
5050
|
+
* The move happens in place when the target is on screen; when it is not, the
|
|
5051
|
+
* host pages to it and the focus is deferred until the new cells exist.
|
|
5052
|
+
*/
|
|
5053
|
+
move(key, from) {
|
|
5054
|
+
const next = foldCalendarNextFocus(key, from, this.host.weekStartsOn());
|
|
5055
|
+
if (next === null) {
|
|
5056
|
+
return false;
|
|
5057
|
+
}
|
|
5058
|
+
this.focused.set(next);
|
|
5059
|
+
if (!foldFocusDayCell(this.host.root(), next)) {
|
|
5060
|
+
this.host.page(next);
|
|
5061
|
+
this.deferred.set(next);
|
|
5062
|
+
this.focusAfterRender();
|
|
5063
|
+
}
|
|
5064
|
+
return true;
|
|
5065
|
+
}
|
|
5066
|
+
/**
|
|
5067
|
+
* Focuses the deferred day once the grid has rendered around the new period.
|
|
5068
|
+
*
|
|
5069
|
+
* `afterNextRender` rather than an `effect`: the ordering of an effect against
|
|
5070
|
+
* the DOM it wants to touch is not contracted and has already changed between
|
|
5071
|
+
* Angular versions, and this way the SSR no-op is explicit. The request is
|
|
5072
|
+
* **consumed**, or repeating the exact same move would be a no-op on the
|
|
5073
|
+
* signal and drop focus onto `<body>`.
|
|
5074
|
+
*/
|
|
5075
|
+
focusAfterRender() {
|
|
5076
|
+
afterNextRender(() => {
|
|
5077
|
+
const target = untracked(this.deferred);
|
|
5078
|
+
if (target === null) {
|
|
5079
|
+
return;
|
|
5080
|
+
}
|
|
5081
|
+
this.deferred.set(null);
|
|
5082
|
+
foldFocusDayCell(this.host.root(), target);
|
|
5083
|
+
}, { injector: this.host.injector });
|
|
5084
|
+
}
|
|
5085
|
+
}
|
|
5086
|
+
|
|
5087
|
+
/** A day cell, with every flag a view needs to style it already resolved. */
|
|
5088
|
+
function foldMakeDay(epochDay, context) {
|
|
5089
|
+
const date = foldFromEpochDay(epochDay);
|
|
5090
|
+
return {
|
|
5091
|
+
date,
|
|
5092
|
+
dayOfMonth: Number(date.slice(8, 10)),
|
|
5093
|
+
inMonth: date.slice(0, 7) === context.month,
|
|
5094
|
+
isToday: date === context.today,
|
|
5095
|
+
isWeekend: foldIsWeekend(date, context.weekendDays),
|
|
5096
|
+
eventCount: context.counts.get(epochDay) ?? 0,
|
|
5097
|
+
hiddenCount: context.hidden.get(epochDay) ?? 0,
|
|
5098
|
+
};
|
|
5099
|
+
}
|
|
5100
|
+
/**
|
|
5101
|
+
* How many events cover each day of `[from, to]`, in a single pass.
|
|
5102
|
+
*
|
|
5103
|
+
* A view that asked each cell "which events are on you?" would re-scan the
|
|
5104
|
+
* whole feed once per cell — 35 filters per change-detection cycle on a month
|
|
5105
|
+
* grid. Walking the events instead costs one pass, and the count a cell needs
|
|
5106
|
+
* is then already there.
|
|
5107
|
+
*/
|
|
5108
|
+
function foldCountByDay(events, fromDay, toDay) {
|
|
5109
|
+
const counts = new Map();
|
|
5110
|
+
for (const event of events) {
|
|
5111
|
+
const a = foldToEpochDay(event.start);
|
|
5112
|
+
const b = foldToEpochDay(event.end);
|
|
5113
|
+
// Reversed ranges are normalised by the layout, so they are counted the
|
|
5114
|
+
// same way here — otherwise a band would render on days reporting zero.
|
|
5115
|
+
const start = Math.max(fromDay, Math.min(a, b));
|
|
5116
|
+
const end = Math.min(toDay, Math.max(a, b));
|
|
5117
|
+
for (let day = start; day <= end; day += 1) {
|
|
5118
|
+
counts.set(day, (counts.get(day) ?? 0) + 1);
|
|
5119
|
+
}
|
|
5120
|
+
}
|
|
5121
|
+
return counts;
|
|
5122
|
+
}
|
|
5123
|
+
/** No days dropped — the column views' `hidden` map. */
|
|
5124
|
+
const FOLD_NO_HIDDEN_DAYS = new Map();
|
|
5125
|
+
/** The weekend a caller left unset. */
|
|
5126
|
+
function foldWeekendOr(weekendDays) {
|
|
5127
|
+
return weekendDays ?? FOLD_DEFAULT_WEEKEND_DAYS;
|
|
5128
|
+
}
|
|
5129
|
+
|
|
5130
|
+
/**
|
|
5131
|
+
* Packing spans into lanes across a row of days — the month grid's week row,
|
|
5132
|
+
* and the time grid's all-day strip, which are the same problem: spans that may
|
|
5133
|
+
* cross the row, a lane budget, and an honest account of what did not fit.
|
|
5134
|
+
*
|
|
5135
|
+
* Shared rather than copied, because "a hidden span is charged to every day it
|
|
5136
|
+
* would have covered" is a rule that has to mean the same thing in both.
|
|
5137
|
+
*/
|
|
5138
|
+
/** The columns a span occupies once clipped to its week row. */
|
|
5139
|
+
function columnsOf(span, weekStartDay, dayCount) {
|
|
5140
|
+
return {
|
|
5141
|
+
startColumn: Math.max(0, span.startDay - weekStartDay),
|
|
5142
|
+
endColumn: Math.min(dayCount - 1, span.endDay - weekStartDay),
|
|
5143
|
+
};
|
|
5144
|
+
}
|
|
5145
|
+
/** One span placed in a lane, or `null` when the budget is spent. */
|
|
5146
|
+
function placeSpan(span, week, columns, laneEnds, maxLanes) {
|
|
5147
|
+
const { startColumn, endColumn } = columns;
|
|
5148
|
+
// First lane whose occupant ends before this span starts; else a fresh one.
|
|
5149
|
+
const reusable = laneEnds.findIndex((end) => end < startColumn);
|
|
5150
|
+
const lane = reusable === -1 ? laneEnds.length : reusable;
|
|
5151
|
+
if (lane >= maxLanes) {
|
|
5152
|
+
return null;
|
|
5153
|
+
}
|
|
5154
|
+
laneEnds[lane] = endColumn;
|
|
5155
|
+
const weekEndDay = week.startDay + week.dayCount - 1;
|
|
5156
|
+
const startsHere = span.startDay >= week.startDay && !span.openStart;
|
|
5157
|
+
const endsHere = span.endDay <= weekEndDay && !span.openEnd;
|
|
5158
|
+
return {
|
|
5159
|
+
key: `${span.key}@${week.start}`,
|
|
5160
|
+
event: span.event,
|
|
5161
|
+
startColumn,
|
|
5162
|
+
endColumn,
|
|
5163
|
+
lane,
|
|
5164
|
+
continuesBefore: !startsHere,
|
|
5165
|
+
continuesAfter: !endsHere,
|
|
5166
|
+
groupSize: span.groupSize,
|
|
5167
|
+
// A half-day edge only means something on the segment holding the real edge.
|
|
5168
|
+
...(startsHere && span.event.startHalfDay !== undefined
|
|
5169
|
+
? { startHalfDay: span.event.startHalfDay }
|
|
5170
|
+
: {}),
|
|
5171
|
+
...(endsHere && span.event.endHalfDay !== undefined
|
|
5172
|
+
? { endHalfDay: span.event.endHalfDay }
|
|
5173
|
+
: {}),
|
|
5174
|
+
};
|
|
5175
|
+
}
|
|
5176
|
+
/**
|
|
5177
|
+
* Packs a row's candidates into lanes, and records what overflowed **per day**
|
|
5178
|
+
* — a row-wide total would say a week is crowded without saying which day to
|
|
5179
|
+
* look at, so each date carries its own count.
|
|
5180
|
+
*/
|
|
5181
|
+
function foldPackRow(candidates, week, maxLanes, hidden) {
|
|
5182
|
+
const laneEnds = [];
|
|
5183
|
+
const bands = [];
|
|
5184
|
+
let hiddenCount = 0;
|
|
5185
|
+
for (const span of candidates) {
|
|
5186
|
+
const columns = columnsOf(span, week.startDay, week.dayCount);
|
|
5187
|
+
const band = placeSpan(span, week, columns, laneEnds, maxLanes);
|
|
5188
|
+
if (band !== null) {
|
|
5189
|
+
bands.push(band);
|
|
5190
|
+
continue;
|
|
5191
|
+
}
|
|
5192
|
+
hiddenCount += 1;
|
|
5193
|
+
// A hidden span is missing from every day it would have covered.
|
|
5194
|
+
for (let column = columns.startColumn; column <= columns.endColumn; column += 1) {
|
|
5195
|
+
const day = week.startDay + column;
|
|
5196
|
+
hidden.set(day, (hidden.get(day) ?? 0) + 1);
|
|
5197
|
+
}
|
|
5198
|
+
}
|
|
5199
|
+
return { bands, hiddenCount };
|
|
5200
|
+
}
|
|
5201
|
+
|
|
5202
|
+
/**
|
|
5203
|
+
* How loudly a tone asks to be looked at. A collapsed group shows its most
|
|
5204
|
+
* severe member, so an alert inside a group is never hidden behind a member
|
|
5205
|
+
* that happens to come first.
|
|
5206
|
+
*/
|
|
5207
|
+
const TONE_SEVERITY = {
|
|
5208
|
+
alert: 4,
|
|
5209
|
+
warning: 3,
|
|
5210
|
+
success: 2,
|
|
5211
|
+
neutral: 1,
|
|
5212
|
+
muted: 0,
|
|
5213
|
+
};
|
|
5214
|
+
/** Severity of an event's tone, `neutral` when it declares none. */
|
|
5215
|
+
function severityOf(event) {
|
|
5216
|
+
return TONE_SEVERITY[event.tone ?? "neutral"];
|
|
5217
|
+
}
|
|
5218
|
+
/**
|
|
5219
|
+
* The span a single event occupies, with a reversed range put back in order.
|
|
5220
|
+
*
|
|
5221
|
+
* `end < start` is a caller bug, and left alone it renders **twice, wrongly**:
|
|
5222
|
+
* the grid's `grid-column: 5 / 2` is silently swapped by CSS so a band covers
|
|
5223
|
+
* days it does not cover, while every other view — which asks
|
|
5224
|
+
* `start <= day && end >= day` — reports the event as absent. Swapping makes
|
|
5225
|
+
* the two agree, and the dev warning names the event rather than leaving the
|
|
5226
|
+
* contradiction to be discovered in a screenshot.
|
|
5227
|
+
*/
|
|
5228
|
+
function spanOf(event, key) {
|
|
5229
|
+
const reversed = event.end < event.start;
|
|
5230
|
+
if (reversed && isDevMode()) {
|
|
5231
|
+
console.warn(`[fold-calendar] event "${event.id}" ends (${event.end}) before it starts (${event.start}); treating the range as reversed.`);
|
|
5232
|
+
}
|
|
5233
|
+
const start = reversed ? event.end : event.start;
|
|
5234
|
+
const end = reversed ? event.start : event.end;
|
|
5235
|
+
return {
|
|
5236
|
+
key,
|
|
5237
|
+
event,
|
|
5238
|
+
start,
|
|
5239
|
+
end,
|
|
5240
|
+
startDay: foldToEpochDay(start),
|
|
5241
|
+
endDay: foldToEpochDay(end),
|
|
5242
|
+
groupSize: 1,
|
|
5243
|
+
openStart: event.openStart === true,
|
|
5244
|
+
openEnd: event.openEnd === true,
|
|
5245
|
+
};
|
|
5246
|
+
}
|
|
5247
|
+
/** Warns once per render when two events share an id. */
|
|
5248
|
+
function warnDuplicateIds(events) {
|
|
5249
|
+
const seen = new Set();
|
|
5250
|
+
for (const event of events) {
|
|
5251
|
+
if (seen.has(event.id)) {
|
|
5252
|
+
console.warn(`[fold-calendar] two events share the id "${event.id}". Ids must be unique per render — use \`groupId\` to collapse events on purpose.`);
|
|
5253
|
+
return;
|
|
5254
|
+
}
|
|
5255
|
+
seen.add(event.id);
|
|
5256
|
+
}
|
|
5257
|
+
}
|
|
5258
|
+
/**
|
|
5259
|
+
* Merges a member into the span standing for its group: the **union** of the
|
|
5260
|
+
* ranges (a group chip stopping short of a member it stands for would be a
|
|
5261
|
+
* lie), open on either side if **any** member is, and represented by the most
|
|
5262
|
+
* severe member so tone, icon, label and source all come from one event that
|
|
5263
|
+
* really exists.
|
|
5264
|
+
*/
|
|
5265
|
+
function mergeMember(span, member) {
|
|
5266
|
+
const takeover = severityOf(member.event) > severityOf(span.event);
|
|
5267
|
+
const opensFirst = span.startDay <= member.startDay;
|
|
5268
|
+
const endsLast = span.endDay >= member.endDay;
|
|
5269
|
+
return {
|
|
5270
|
+
key: span.key,
|
|
5271
|
+
event: takeover ? member.event : span.event,
|
|
5272
|
+
start: opensFirst ? span.start : member.start,
|
|
5273
|
+
end: endsLast ? span.end : member.end,
|
|
5274
|
+
startDay: opensFirst ? span.startDay : member.startDay,
|
|
5275
|
+
endDay: endsLast ? span.endDay : member.endDay,
|
|
5276
|
+
groupSize: span.groupSize + 1,
|
|
5277
|
+
openStart: span.openStart || member.openStart,
|
|
5278
|
+
openEnd: span.openEnd || member.openEnd,
|
|
5279
|
+
};
|
|
5280
|
+
}
|
|
5281
|
+
/**
|
|
5282
|
+
* One span per event, except where `groupId` asks for a collapsed chip.
|
|
5283
|
+
*
|
|
5284
|
+
* Identity keys carry the event's position, so two events that share an id stay
|
|
5285
|
+
* two spans: merging them would drop one silently, which is not something a
|
|
5286
|
+
* duplicated key should be able to do. Insertion order is preserved, so the
|
|
5287
|
+
* caller's ordering still decides ties.
|
|
5288
|
+
*/
|
|
5289
|
+
function foldCollapseGroups(events) {
|
|
5290
|
+
if (isDevMode()) {
|
|
5291
|
+
warnDuplicateIds(events);
|
|
5292
|
+
}
|
|
5293
|
+
const spans = new Map();
|
|
5294
|
+
events.forEach((event, index) => {
|
|
5295
|
+
const key = event.groupId === undefined
|
|
5296
|
+
? `event:${index}:${event.id}`
|
|
5297
|
+
: `group:${event.groupId}`;
|
|
5298
|
+
const incoming = spanOf(event, key);
|
|
5299
|
+
const existing = spans.get(key);
|
|
5300
|
+
spans.set(key, existing === undefined ? incoming : mergeMember(existing, incoming));
|
|
5301
|
+
});
|
|
5302
|
+
return [...spans.values()];
|
|
5303
|
+
}
|
|
5304
|
+
|
|
5305
|
+
/** Columns in a week row. */
|
|
5306
|
+
const DAYS_PER_WEEK$1 = 7;
|
|
5307
|
+
/** Rows a fixed-height month grid always emits. */
|
|
5308
|
+
const FIXED_WEEK_COUNT = 6;
|
|
5309
|
+
/** Lanes a week row stacks when the caller does not say. */
|
|
5310
|
+
const DEFAULT_MAX_LANES = 3;
|
|
5311
|
+
/**
|
|
5312
|
+
* Every row's candidates, in one pass over the feed.
|
|
5313
|
+
*
|
|
5314
|
+
* Filtering the whole feed once per row is `O(rows × N)` and, worse, compares
|
|
5315
|
+
* strings; walking the feed once and dropping each span into the rows it
|
|
5316
|
+
* touches is `O(N)` on integers. At 5 000 events this part of the layout went
|
|
5317
|
+
* from 12.6 ms to 1.9 ms.
|
|
5318
|
+
*
|
|
5319
|
+
* Each row is then ordered the way a reader scans it: earliest start first, and
|
|
5320
|
+
* on a tie the longest span first, so the bars crossing the most days settle
|
|
5321
|
+
* into the top lanes.
|
|
5322
|
+
*/
|
|
5323
|
+
function bucketByRow(spans, firstRowDay, rowCount) {
|
|
5324
|
+
const rows = Array.from({ length: rowCount }, () => []);
|
|
5325
|
+
const lastDay = firstRowDay + rowCount * DAYS_PER_WEEK$1 - 1;
|
|
5326
|
+
for (const span of spans) {
|
|
5327
|
+
if (span.endDay < firstRowDay || span.startDay > lastDay) {
|
|
5328
|
+
continue;
|
|
5329
|
+
}
|
|
5330
|
+
const from = Math.max(0, Math.floor((span.startDay - firstRowDay) / DAYS_PER_WEEK$1));
|
|
5331
|
+
const to = Math.min(rowCount - 1, Math.floor((span.endDay - firstRowDay) / DAYS_PER_WEEK$1));
|
|
5332
|
+
for (let row = from; row <= to; row += 1) {
|
|
5333
|
+
rows[row]?.push(span);
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
for (const row of rows) {
|
|
5337
|
+
row.sort((a, b) => a.startDay !== b.startDay
|
|
5338
|
+
? a.startDay - b.startDay
|
|
5339
|
+
: b.endDay - b.startDay - (a.endDay - a.startDay));
|
|
5340
|
+
}
|
|
5341
|
+
return rows;
|
|
5342
|
+
}
|
|
5343
|
+
/**
|
|
5344
|
+
* Lanes a row may stack, as one number the whole grid agrees on.
|
|
5345
|
+
*
|
|
5346
|
+
* The clamp lives here alone: a component that also clamped would be a second
|
|
5347
|
+
* copy of the invariant, and the copy that forgot `NaN` would disable the lane
|
|
5348
|
+
* budget entirely (`lane >= NaN` is never true) while the grid template turned
|
|
5349
|
+
* `repeat(NaN, …)` into a dropped rule.
|
|
5350
|
+
*/
|
|
5351
|
+
function foldClampLanes(value) {
|
|
5352
|
+
if (value === undefined || !Number.isFinite(value)) {
|
|
5353
|
+
return DEFAULT_MAX_LANES;
|
|
5354
|
+
}
|
|
5355
|
+
return Math.max(0, Math.trunc(value));
|
|
5356
|
+
}
|
|
5357
|
+
/** The grid's own window — whole weeks, padded out when `fixedWeeks` asks. */
|
|
5358
|
+
function gridBounds(options) {
|
|
5359
|
+
const weekStartsOn = options.weekStartsOn ?? "mon";
|
|
5360
|
+
const firstRowDay = foldToEpochDay(foldStartOfWeek(foldStartOfMonth(options.month), weekStartsOn));
|
|
5361
|
+
const lastRowDay = foldToEpochDay(foldStartOfWeek(foldEndOfMonth(options.month), weekStartsOn));
|
|
5362
|
+
const naturalRows = (lastRowDay - firstRowDay) / DAYS_PER_WEEK$1 + 1;
|
|
5363
|
+
return {
|
|
5364
|
+
firstRowDay,
|
|
5365
|
+
rowCount: options.fixedWeeks === true
|
|
5366
|
+
? Math.max(FIXED_WEEK_COUNT, naturalRows)
|
|
5367
|
+
: naturalRows,
|
|
5368
|
+
};
|
|
5369
|
+
}
|
|
5370
|
+
/**
|
|
5371
|
+
* Lays a month out as week rows with their events already packed into lanes.
|
|
5372
|
+
*
|
|
5373
|
+
* The grid runs from the week containing the 1st to the week containing the
|
|
5374
|
+
* last day, so it always starts and ends on whole weeks; `fixedWeeks` pads it
|
|
5375
|
+
* to six rows. Events outside that window are dropped, and an event crossing a
|
|
5376
|
+
* week boundary yields one band per week it touches.
|
|
5377
|
+
*
|
|
5378
|
+
* An unparseable `month` yields **no rows** rather than a grid of `Invalid
|
|
5379
|
+
* Date` cells, with a dev-mode warning naming the value.
|
|
5380
|
+
*/
|
|
5381
|
+
function foldBuildMonthGrid(events, options) {
|
|
5382
|
+
if (!foldIsCalendarDate(options.month)) {
|
|
5383
|
+
if (isDevMode()) {
|
|
5384
|
+
console.warn(`[fold-calendar] "${options.month}" is not a YYYY-MM-DD date; the grid is empty.`);
|
|
5385
|
+
}
|
|
5386
|
+
return [];
|
|
5387
|
+
}
|
|
5388
|
+
const maxLanes = foldClampLanes(options.maxLanes);
|
|
5389
|
+
const { firstRowDay, rowCount } = gridBounds(options);
|
|
5390
|
+
const rows = bucketByRow(foldCollapseGroups(events), firstRowDay, rowCount);
|
|
5391
|
+
const hidden = new Map();
|
|
5392
|
+
const packed = rows.map((candidates, row) => {
|
|
5393
|
+
const week = {
|
|
5394
|
+
startDay: firstRowDay + row * DAYS_PER_WEEK$1,
|
|
5395
|
+
start: foldFromEpochDay(firstRowDay + row * DAYS_PER_WEEK$1),
|
|
5396
|
+
dayCount: DAYS_PER_WEEK$1,
|
|
5397
|
+
};
|
|
5398
|
+
return { week, ...foldPackRow(candidates, week, maxLanes, hidden) };
|
|
5399
|
+
});
|
|
5400
|
+
const context = {
|
|
5401
|
+
month: options.month.slice(0, 7),
|
|
5402
|
+
today: options.today,
|
|
5403
|
+
weekendDays: foldWeekendOr(options.weekendDays),
|
|
5404
|
+
counts: foldCountByDay(events, firstRowDay, firstRowDay + rowCount * DAYS_PER_WEEK$1 - 1),
|
|
5405
|
+
hidden,
|
|
5406
|
+
};
|
|
5407
|
+
return packed.map((row) => ({
|
|
5408
|
+
start: row.week.start,
|
|
5409
|
+
days: buildDays(row.week.startDay, context),
|
|
5410
|
+
bands: row.bands,
|
|
5411
|
+
hiddenCount: row.hiddenCount,
|
|
5412
|
+
}));
|
|
5413
|
+
}
|
|
5414
|
+
/** The seven cells of one week row. */
|
|
5415
|
+
function buildDays(weekStartDay, context) {
|
|
5416
|
+
return Array.from({ length: DAYS_PER_WEEK$1 }, (_unused, offset) => foldMakeDay(weekStartDay + offset, context));
|
|
5417
|
+
}
|
|
5418
|
+
|
|
5419
|
+
/**
|
|
5420
|
+
* Replaces the **inside of a month day cell** — by default just the day number.
|
|
5421
|
+
*
|
|
5422
|
+
* This is the hook for everything the calendar cannot know: a holiday marker, a
|
|
5423
|
+
* closure, "3/8 staffed", a price. The cell itself (the button, its focus
|
|
5424
|
+
* behaviour, its accessible name) stays the calendar's job.
|
|
5425
|
+
*
|
|
5426
|
+
* ```html
|
|
5427
|
+
* <ng-template foldCalendarDay let-day>
|
|
5428
|
+
* <span>{{ day.dayOfMonth }}</span>
|
|
5429
|
+
* @if (closed(day.date)) { <fold-icon name="lock" size="xs" /> }
|
|
5430
|
+
* </ng-template>
|
|
5431
|
+
* ```
|
|
5432
|
+
*
|
|
5433
|
+
* @selector `ng-template[foldCalendarDay]`
|
|
5434
|
+
*/
|
|
5435
|
+
class FoldCalendarDayDirective {
|
|
5436
|
+
template = inject(TemplateRef);
|
|
5437
|
+
static ngTemplateContextGuard(_directive, context) {
|
|
5438
|
+
// Compile-time only — Angular never calls this; the narrowing is the point.
|
|
5439
|
+
return context !== undefined;
|
|
5440
|
+
}
|
|
5441
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarDayDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
5442
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.8", type: FoldCalendarDayDirective, isStandalone: true, selector: "ng-template[foldCalendarDay]", ngImport: i0 });
|
|
5443
|
+
}
|
|
5444
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarDayDirective, decorators: [{
|
|
5445
|
+
type: Directive,
|
|
5446
|
+
args: [{ selector: "ng-template[foldCalendarDay]", standalone: true }]
|
|
5447
|
+
}] });
|
|
5448
|
+
/**
|
|
5449
|
+
* Replaces the **day heading** of the agenda rail — by default a big day number
|
|
5450
|
+
* with a relative name beside it. Where an app puts a per-day count, a total,
|
|
5451
|
+
* or a quick action.
|
|
5452
|
+
*
|
|
5453
|
+
* @selector `ng-template[foldCalendarHeading]`
|
|
5454
|
+
*/
|
|
5455
|
+
class FoldCalendarHeadingDirective {
|
|
5456
|
+
template = inject(TemplateRef);
|
|
5457
|
+
static ngTemplateContextGuard(_directive, context) {
|
|
5458
|
+
// Compile-time only — Angular never calls this; the narrowing is the point.
|
|
5459
|
+
return context !== undefined;
|
|
5460
|
+
}
|
|
5461
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarHeadingDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
5462
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.8", type: FoldCalendarHeadingDirective, isStandalone: true, selector: "ng-template[foldCalendarHeading]", ngImport: i0 });
|
|
5463
|
+
}
|
|
5464
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarHeadingDirective, decorators: [{
|
|
5465
|
+
type: Directive,
|
|
5466
|
+
args: [{ selector: "ng-template[foldCalendarHeading]", standalone: true }]
|
|
5467
|
+
}] });
|
|
5468
|
+
/**
|
|
5469
|
+
* Replaces the toolbar's **period title** — by default an `<h2>` holding
|
|
5470
|
+
* `foldViewTitle()`. Project one to change the wording, or to give the title a
|
|
5471
|
+
* heading level that fits the page's outline.
|
|
5472
|
+
*
|
|
5473
|
+
* @selector `ng-template[foldCalendarTitle]`
|
|
5474
|
+
*/
|
|
5475
|
+
class FoldCalendarTitleDirective {
|
|
5476
|
+
template = inject(TemplateRef);
|
|
5477
|
+
static ngTemplateContextGuard(_directive, context) {
|
|
5478
|
+
// Compile-time only — Angular never calls this; the narrowing is the point.
|
|
5479
|
+
return context !== undefined;
|
|
5480
|
+
}
|
|
5481
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarTitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
5482
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.8", type: FoldCalendarTitleDirective, isStandalone: true, selector: "ng-template[foldCalendarTitle]", ngImport: i0 });
|
|
5483
|
+
}
|
|
5484
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarTitleDirective, decorators: [{
|
|
5485
|
+
type: Directive,
|
|
5486
|
+
args: [{ selector: "ng-template[foldCalendarTitle]", standalone: true }]
|
|
5487
|
+
}] });
|
|
5488
|
+
/**
|
|
5489
|
+
* Replaces the month view's **`+N` chip** — by default a small text button.
|
|
5490
|
+
* Where an app puts an avatar stack, or the trigger of a "see all" popover.
|
|
5491
|
+
*
|
|
5492
|
+
* @selector `ng-template[foldCalendarOverflow]`
|
|
5493
|
+
*/
|
|
5494
|
+
class FoldCalendarOverflowDirective {
|
|
5495
|
+
template = inject(TemplateRef);
|
|
5496
|
+
static ngTemplateContextGuard(_directive, context) {
|
|
5497
|
+
// Compile-time only — Angular never calls this; the narrowing is the point.
|
|
5498
|
+
return context !== undefined;
|
|
5499
|
+
}
|
|
5500
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarOverflowDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
5501
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.8", type: FoldCalendarOverflowDirective, isStandalone: true, selector: "ng-template[foldCalendarOverflow]", ngImport: i0 });
|
|
5502
|
+
}
|
|
5503
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarOverflowDirective, decorators: [{
|
|
5504
|
+
type: Directive,
|
|
5505
|
+
args: [{ selector: "ng-template[foldCalendarOverflow]", standalone: true }]
|
|
5506
|
+
}] });
|
|
5507
|
+
|
|
5508
|
+
/** A Monday, used only to walk seven weekday names out of `Intl`. */
|
|
5509
|
+
const WEEKDAY_SAMPLE = "2026-01-05";
|
|
5510
|
+
/** Keys that activate a day cell — the ones a `<button>` would have handled. */
|
|
5511
|
+
const ACTIVATION_KEYS = new Set(["Enter", " ", "Spacebar"]);
|
|
5512
|
+
/**
|
|
5513
|
+
* `<fold-calendar-month>` — a month grid where events span the days they cover.
|
|
5514
|
+
*
|
|
5515
|
+
* The grid is a **date axis**: seven columns of whole days, and over them a
|
|
5516
|
+
* layer of bands, each stretching from the column its event starts on to the
|
|
5517
|
+
* column it ends on. An event crossing a week boundary is drawn once per week
|
|
5518
|
+
* with an open edge on the side that continues, so a three-week holiday reads
|
|
5519
|
+
* as one thing rather than twenty-one chips.
|
|
5520
|
+
*
|
|
5521
|
+
* Dates are plain `YYYY-MM-DD` strings — see `calendar-date.ts` for why that,
|
|
5522
|
+
* and not `Date`.
|
|
5523
|
+
*
|
|
5524
|
+
* ## Inputs
|
|
5525
|
+
* | Input | Type | Default | Meaning |
|
|
5526
|
+
* |----------------|-------------------------------|---------------|---------|
|
|
5527
|
+
* | `month` | `FoldCalendarDate` | — | **Two-way.** Any date in the month on display; keyboard paging writes back. |
|
|
5528
|
+
* | `events` | `FoldCalendarEvent<T>[]` | `[]` | What to plot. Anything outside the grid is ignored. |
|
|
5529
|
+
* | `today` | `FoldCalendarDate` | — | The day to mark. Omitted, nothing is marked — pass `foldToday()`. |
|
|
5530
|
+
* | `weekStartsOn` | `FoldWeekday` | the locale's | Which day opens the row — `Intl.Locale` knows, so it is only an override. |
|
|
5531
|
+
* | `weekendDays` | `FoldWeekday[]` | the locale's | Which days are shaded — a separate fact from the anchor, and also in the locale. |
|
|
5532
|
+
* | `showWeekNumbers` | `boolean` | `false` | An ISO week number in a leading column. |
|
|
5533
|
+
* | `maxLanes` | `number` | `3` | Bands a week stacks before the rest collapse into overflow chips. |
|
|
5534
|
+
* | `fixedWeeks` | `boolean` | `false` | Always six rows, so the grid keeps one height across months. |
|
|
5535
|
+
* | `dayModifiers` | `(day) => string[]` | — | Extra names per cell, emitted as `data-fold-day-modifiers`. |
|
|
5536
|
+
* | `locale` | `string` | runtime | Drives month/weekday names through `Intl`. |
|
|
5537
|
+
* | `labels` | `Partial<FoldCalendarLabels>` | — | Per-instance overrides of the chrome strings. |
|
|
5538
|
+
*
|
|
5539
|
+
* ## Outputs
|
|
5540
|
+
* | Output | Payload | Fires on |
|
|
5541
|
+
* |-----------------|------------------------|----------|
|
|
5542
|
+
* | `dayClick` | `FoldCalendarDate` | A day cell activated by click, `Enter` or `Space`. |
|
|
5543
|
+
* | `eventClick` | `FoldCalendarEvent<T>` | A band clicked. Grouped bands emit their representative. |
|
|
5544
|
+
* | `overflowClick` | `FoldCalendarDate` | A `+N` chip — the **day** whose events overflowed, so the caller knows which one to open. |
|
|
5545
|
+
*
|
|
5546
|
+
* ## Templates
|
|
5547
|
+
* | Directive | Replaces |
|
|
5548
|
+
* |------------------------|----------|
|
|
5549
|
+
* | `foldCalendarEvent` | The inside of a band — icon + label + count. |
|
|
5550
|
+
* | `foldCalendarDay` | The inside of a day cell — the day number. |
|
|
5551
|
+
* | `foldCalendarOverflow` | The `+N` chip. |
|
|
5552
|
+
*
|
|
5553
|
+
* ## Theming
|
|
5554
|
+
* | CSS variable | Default | Sets |
|
|
5555
|
+
* |---------------------------------------|---------|------|
|
|
5556
|
+
* | `--fold-calendar-month-header-row` | `30px` | Height of the day-number row. |
|
|
5557
|
+
* | `--fold-calendar-month-lane` | `22px` | Height of one band lane. |
|
|
5558
|
+
* | `--fold-calendar-month-row-min-height`| `112px` | Minimum height of a week row. |
|
|
5559
|
+
* | `--fold-calendar-month-daynum-size` | `22px` | The day number's round box. |
|
|
5560
|
+
* | `--fold-calendar-month-cell-padding` | `--fold-space-sm` | Padding inside a day cell. |
|
|
5561
|
+
* | `--fold-calendar-month-week-number-width` | `34px` | The ISO week column, when shown. |
|
|
5562
|
+
* | `--fold-calendar-bar-width` | `3px` | The tone bar down a band's leading edge. |
|
|
5563
|
+
* | `--fold-calendar-band-radius` | `--fold-radius-sm` | Band and chip corners. |
|
|
5564
|
+
* | `--fold-calendar-band-gutter` | `--fold-space-xs` | Gap between a band and its cell edge. |
|
|
5565
|
+
*
|
|
5566
|
+
* ## Accessibility
|
|
5567
|
+
* The **date grid** is the ARIA structure: `role="grid"` over rows of
|
|
5568
|
+
* `gridcell` days, with one roving tab stop (the focused day, else today, else
|
|
5569
|
+
* the 1st) and the grid pattern on the arrow keys — horizontal steps a day,
|
|
5570
|
+
* vertical steps a week, `Home`/`End` snap to the week, and `PageUp`/`PageDown`
|
|
5571
|
+
* page the month, writing back through `month`.
|
|
5572
|
+
*
|
|
5573
|
+
* Bands cannot be `gridcell`s — they span columns, which is the whole point —
|
|
5574
|
+
* so they are **`aria-hidden` mouse affordances**, and each day cell instead
|
|
5575
|
+
* announces its date, the today marker, its **event count** and, when the lane
|
|
5576
|
+
* budget hid some, **how many it is not showing**.
|
|
5577
|
+
*
|
|
5578
|
+
* That makes `dayClick` **load-bearing, not optional**: it is the accessible
|
|
5579
|
+
* path into an individual event. A month that handles only `eventClick` is a
|
|
5580
|
+
* view whose events no keyboard can reach — wire `dayClick` to a day or list
|
|
5581
|
+
* view, as the gallery does.
|
|
5582
|
+
*
|
|
5583
|
+
* @selector `fold-calendar-month`
|
|
5584
|
+
*
|
|
5585
|
+
* @example
|
|
5586
|
+
* ```html
|
|
5587
|
+
* <fold-calendar-month
|
|
5588
|
+
* [(month)]="month"
|
|
5589
|
+
* [events]="events()"
|
|
5590
|
+
* [today]="today"
|
|
5591
|
+
* (dayClick)="openDay($event)"
|
|
5592
|
+
* (eventClick)="openEvent($event)"
|
|
5593
|
+
* />
|
|
5594
|
+
* ```
|
|
5595
|
+
*/
|
|
5596
|
+
class FoldCalendarMonthComponent {
|
|
5597
|
+
/** Any date inside the month on display. Two-way: keyboard paging writes back. */
|
|
5598
|
+
month = model.required(/* @ts-ignore */
|
|
5599
|
+
...(ngDevMode ? [{ debugName: "month" }] : /* istanbul ignore next */ []));
|
|
5600
|
+
/** Events to plot; those missing the grid entirely are ignored. */
|
|
5601
|
+
events = input([], /* @ts-ignore */
|
|
5602
|
+
...(ngDevMode ? [{ debugName: "events" }] : /* istanbul ignore next */ []));
|
|
5603
|
+
/** The day to mark as today. Omit and none is marked (an SSR-stable default). */
|
|
5604
|
+
today = input(/* @ts-ignore */
|
|
5605
|
+
...(ngDevMode ? [undefined, { debugName: "today" }] : /* istanbul ignore next */ []));
|
|
5606
|
+
/** Day the weeks start on. @default the locale's own first day */
|
|
5607
|
+
weekStartsOn = input(/* @ts-ignore */
|
|
5608
|
+
...(ngDevMode ? [undefined, { debugName: "weekStartsOn" }] : /* istanbul ignore next */ []));
|
|
5609
|
+
/** Days shaded as the weekend. @default the locale's own weekend */
|
|
5610
|
+
weekendDays = input(/* @ts-ignore */
|
|
5611
|
+
...(ngDevMode ? [undefined, { debugName: "weekendDays" }] : /* istanbul ignore next */ []));
|
|
5612
|
+
/** Show the ISO week number in a leading column. */
|
|
5613
|
+
showWeekNumbers = input(false, { ...(ngDevMode ? { debugName: "showWeekNumbers" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
5614
|
+
/** Bands a week stacks before the rest collapse into overflow chips. @default 3 */
|
|
5615
|
+
maxLanes = input(3, { ...(ngDevMode ? { debugName: "maxLanes" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
5616
|
+
/** Always emit six rows, so the grid keeps one height across months. */
|
|
5617
|
+
fixedWeeks = input(false, { ...(ngDevMode ? { debugName: "fixedWeeks" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
5618
|
+
/** Extra names for a cell, emitted as `data-fold-day-modifiers`. */
|
|
5619
|
+
dayModifiers = input(/* @ts-ignore */
|
|
5620
|
+
...(ngDevMode ? [undefined, { debugName: "dayModifiers" }] : /* istanbul ignore next */ []));
|
|
5621
|
+
/** A day cell was activated. */
|
|
5622
|
+
dayClick = output();
|
|
5623
|
+
/** A band was clicked; a grouped band emits its representative event. */
|
|
5624
|
+
eventClick = output();
|
|
5625
|
+
/** A `+N` chip was clicked, with the day whose events overflowed. */
|
|
5626
|
+
overflowClick = output();
|
|
5627
|
+
/** Labels, locale and the projected chip template — see the directive. */
|
|
5628
|
+
chrome = inject(FoldCalendarChromeDirective);
|
|
5629
|
+
host = inject(ElementRef);
|
|
5630
|
+
projectedDay = contentChild(FoldCalendarDayDirective, /* @ts-ignore */
|
|
5631
|
+
...(ngDevMode ? [{ debugName: "projectedDay" }] : /* istanbul ignore next */ []));
|
|
5632
|
+
projectedOverflow = contentChild(FoldCalendarOverflowDirective, /* @ts-ignore */
|
|
5633
|
+
...(ngDevMode ? [{ debugName: "projectedOverflow" }] : /* istanbul ignore next */ []));
|
|
5634
|
+
/** The custom day-cell template, when one is projected. */
|
|
5635
|
+
dayContent = computed(() => this.projectedDay()?.template ?? null, /* @ts-ignore */
|
|
5636
|
+
...(ngDevMode ? [{ debugName: "dayContent" }] : /* istanbul ignore next */ []));
|
|
5637
|
+
/** The custom overflow-chip template, when one is projected. */
|
|
5638
|
+
overflowContent = computed(() => this.projectedOverflow()?.template ?? null, /* @ts-ignore */
|
|
5639
|
+
...(ngDevMode ? [{ debugName: "overflowContent" }] : /* istanbul ignore next */ []));
|
|
5640
|
+
/**
|
|
5641
|
+
* Lanes this grid stacks — clamped once, by the layout, and read back here so
|
|
5642
|
+
* the geometry and the CSS row template can never disagree.
|
|
5643
|
+
*/
|
|
5644
|
+
lanes = computed(() => foldClampLanes(this.maxLanes()), /* @ts-ignore */
|
|
5645
|
+
...(ngDevMode ? [{ debugName: "lanes" }] : /* istanbul ignore next */ []));
|
|
5646
|
+
/** The laid-out grid: week rows, their days, and their packed bands. */
|
|
5647
|
+
/** The anchor in force: the caller's, else the one the locale declares. */
|
|
5648
|
+
anchor = computed(() => this.chrome.anchor(this.weekStartsOn()), /* @ts-ignore */
|
|
5649
|
+
...(ngDevMode ? [{ debugName: "anchor" }] : /* istanbul ignore next */ []));
|
|
5650
|
+
weeks = computed(() => foldBuildMonthGrid(this.events(), {
|
|
5651
|
+
month: this.month(),
|
|
5652
|
+
weekStartsOn: this.anchor(),
|
|
5653
|
+
weekendDays: this.chrome.weekend(this.weekendDays()),
|
|
5654
|
+
maxLanes: this.lanes(),
|
|
5655
|
+
fixedWeeks: this.fixedWeeks(),
|
|
5656
|
+
today: this.today(),
|
|
5657
|
+
}), /* @ts-ignore */
|
|
5658
|
+
...(ngDevMode ? [{ debugName: "weeks" }] : /* istanbul ignore next */ []));
|
|
5659
|
+
/** Column headers, walked out of `Intl` so every locale comes for free. */
|
|
5660
|
+
weekdayNames = computed(() => {
|
|
5661
|
+
const sample = foldStartOfWeek(WEEKDAY_SAMPLE, this.anchor());
|
|
5662
|
+
return Array.from({ length: 7 }, (_unused, offset) => this.chrome.format(foldAddDays(sample, offset), "weekdayShort"));
|
|
5663
|
+
}, /* @ts-ignore */
|
|
5664
|
+
...(ngDevMode ? [{ debugName: "weekdayNames" }] : /* istanbul ignore next */ []));
|
|
5665
|
+
/**
|
|
5666
|
+
* Columns before the first day. The week-number column is a real grid column,
|
|
5667
|
+
* so everything placed by index — cells, bands, overflow chips — shifts with
|
|
5668
|
+
* it, and nothing has to know why.
|
|
5669
|
+
*/
|
|
5670
|
+
columnOffset = computed(() => this.showWeekNumbers() ? 1 : 0, /* @ts-ignore */
|
|
5671
|
+
...(ngDevMode ? [{ debugName: "columnOffset" }] : /* istanbul ignore next */ []));
|
|
5672
|
+
/** The ISO week each row belongs to — always Monday-based, whatever the anchor. */
|
|
5673
|
+
weekNumber(week) {
|
|
5674
|
+
return foldIsoWeek(week.start);
|
|
5675
|
+
}
|
|
5676
|
+
/**
|
|
5677
|
+
* Row track sizes: the day-number row, one row per lane, then the overflow
|
|
5678
|
+
* row. Both heights are themeable knobs, which is why they are `var()`s.
|
|
5679
|
+
*/
|
|
5680
|
+
rowTemplate = computed(() => `var(--fold-calendar-month-header-row, 30px) repeat(${this.lanes()}, var(--fold-calendar-month-lane, 22px)) auto`, /* @ts-ignore */
|
|
5681
|
+
...(ngDevMode ? [{ debugName: "rowTemplate" }] : /* istanbul ignore next */ []));
|
|
5682
|
+
/** The overflow chips' row — always the one after the last lane. */
|
|
5683
|
+
overflowRow = computed(() => this.lanes() + 2, /* @ts-ignore */
|
|
5684
|
+
...(ngDevMode ? [{ debugName: "overflowRow" }] : /* istanbul ignore next */ []));
|
|
5685
|
+
/** Every date the grid currently shows — the bound focus may not leave. */
|
|
5686
|
+
gridDates = computed(() => new Set(this.weeks().flatMap((week) => week.days.map((day) => day.date))), /* @ts-ignore */
|
|
5687
|
+
...(ngDevMode ? [{ debugName: "gridDates" }] : /* istanbul ignore next */ []));
|
|
5688
|
+
/** Where the tab stop sits before the user moves it: today, else the 1st. */
|
|
5689
|
+
defaultStop = computed(() => {
|
|
5690
|
+
const today = this.today();
|
|
5691
|
+
if (today !== undefined && this.gridDates().has(today)) {
|
|
5692
|
+
return today;
|
|
5693
|
+
}
|
|
5694
|
+
return (this.weeks()
|
|
5695
|
+
.flatMap((week) => week.days)
|
|
5696
|
+
.find((day) => day.inMonth)?.date ?? null);
|
|
5697
|
+
}, /* @ts-ignore */
|
|
5698
|
+
...(ngDevMode ? [{ debugName: "defaultStop" }] : /* istanbul ignore next */ []));
|
|
5699
|
+
/** The grid's single tab stop and the arrow keys that move it. */
|
|
5700
|
+
focus = new FoldCalendarRovingFocus({
|
|
5701
|
+
dates: this.gridDates,
|
|
5702
|
+
fallback: this.defaultStop,
|
|
5703
|
+
weekStartsOn: this.anchor,
|
|
5704
|
+
root: () => this.rootElement(),
|
|
5705
|
+
page: (date) => this.month.set(date),
|
|
5706
|
+
injector: inject(Injector),
|
|
5707
|
+
});
|
|
5708
|
+
dayTabIndex(date) {
|
|
5709
|
+
return this.focus.tabIndexFor(date);
|
|
5710
|
+
}
|
|
5711
|
+
/** A cell's caller-supplied names, as one `~=`-matchable attribute. */
|
|
5712
|
+
dayModifierAttr(day) {
|
|
5713
|
+
const names = this.dayModifiers()?.(day) ?? [];
|
|
5714
|
+
return names.length === 0 ? null : names.join(" ");
|
|
5715
|
+
}
|
|
5716
|
+
/**
|
|
5717
|
+
* A day's accessible name: its date, the today marker, what sits on it, and
|
|
5718
|
+
* what the lane budget is keeping from it.
|
|
5719
|
+
*/
|
|
5720
|
+
dayLabel(day) {
|
|
5721
|
+
const labels = this.chrome.l();
|
|
5722
|
+
const parts = [this.chrome.format(day.date, "dateFull")];
|
|
5723
|
+
if (day.isToday) {
|
|
5724
|
+
parts.push(labels.today);
|
|
5725
|
+
}
|
|
5726
|
+
if (day.eventCount > 0) {
|
|
5727
|
+
parts.push(labels.eventCount(day.eventCount));
|
|
5728
|
+
}
|
|
5729
|
+
if (day.hiddenCount > 0) {
|
|
5730
|
+
parts.push(labels.hiddenCount(day.hiddenCount));
|
|
5731
|
+
}
|
|
5732
|
+
return parts.join(", ");
|
|
5733
|
+
}
|
|
5734
|
+
/** A band's text: the group's label when it stands for several events. */
|
|
5735
|
+
bandLabel(band) {
|
|
5736
|
+
if (band.groupSize > 1) {
|
|
5737
|
+
return band.event.groupLabel ?? band.event.label;
|
|
5738
|
+
}
|
|
5739
|
+
return band.event.label;
|
|
5740
|
+
}
|
|
5741
|
+
/** The leading glyph — suppressed on a continuation, which has no real start. */
|
|
5742
|
+
bandIcon(band) {
|
|
5743
|
+
if (band.continuesBefore) {
|
|
5744
|
+
return null;
|
|
5745
|
+
}
|
|
5746
|
+
return band.event.icon ?? null;
|
|
5747
|
+
}
|
|
5748
|
+
onDayFocus(date) {
|
|
5749
|
+
this.focus.onFocus(date);
|
|
5750
|
+
}
|
|
5751
|
+
/** A cell is not a `<button>`, so activation is wired by hand. */
|
|
5752
|
+
onDayKeydown(event, date) {
|
|
5753
|
+
if (event.target !== event.currentTarget) {
|
|
5754
|
+
return;
|
|
5755
|
+
}
|
|
5756
|
+
if (ACTIVATION_KEYS.has(event.key)) {
|
|
5757
|
+
event.preventDefault();
|
|
5758
|
+
this.dayClick.emit(date);
|
|
5759
|
+
return;
|
|
5760
|
+
}
|
|
5761
|
+
if (this.focus.move(event.key, date)) {
|
|
5762
|
+
event.preventDefault();
|
|
5763
|
+
}
|
|
5764
|
+
}
|
|
5765
|
+
rootElement() {
|
|
5766
|
+
const root = this.host.nativeElement;
|
|
5767
|
+
return root instanceof HTMLElement ? root : null;
|
|
5768
|
+
}
|
|
5769
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarMonthComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5770
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: FoldCalendarMonthComponent, isStandalone: true, selector: "fold-calendar-month", inputs: { month: { classPropertyName: "month", publicName: "month", isSignal: true, isRequired: true, transformFunction: null }, events: { classPropertyName: "events", publicName: "events", isSignal: true, isRequired: false, transformFunction: null }, today: { classPropertyName: "today", publicName: "today", isSignal: true, isRequired: false, transformFunction: null }, weekStartsOn: { classPropertyName: "weekStartsOn", publicName: "weekStartsOn", isSignal: true, isRequired: false, transformFunction: null }, weekendDays: { classPropertyName: "weekendDays", publicName: "weekendDays", isSignal: true, isRequired: false, transformFunction: null }, showWeekNumbers: { classPropertyName: "showWeekNumbers", publicName: "showWeekNumbers", isSignal: true, isRequired: false, transformFunction: null }, maxLanes: { classPropertyName: "maxLanes", publicName: "maxLanes", isSignal: true, isRequired: false, transformFunction: null }, fixedWeeks: { classPropertyName: "fixedWeeks", publicName: "fixedWeeks", isSignal: true, isRequired: false, transformFunction: null }, dayModifiers: { classPropertyName: "dayModifiers", publicName: "dayModifiers", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { month: "monthChange", dayClick: "dayClick", eventClick: "eventClick", overflowClick: "overflowClick" }, host: { attributes: { "data-fold-calendar": "", "role": "grid" }, properties: { "attr.aria-label": "chrome.l().grid", "class.has-week-numbers": "showWeekNumbers()" } }, queries: [{ propertyName: "projectedDay", first: true, predicate: FoldCalendarDayDirective, descendants: true, isSignal: true }, { propertyName: "projectedOverflow", first: true, predicate: FoldCalendarOverflowDirective, descendants: true, isSignal: true }], hostDirectives: [{ directive: FoldCalendarChromeDirective, inputs: ["locale", "locale", "labels", "labels", "formats", "formats"] }], ngImport: i0, template: "<div class=\"foldcal-weekdays\" role=\"row\">\n @if (showWeekNumbers()) {\n <span class=\"foldcal-weekday foldcal-weeknum-head\" role=\"columnheader\">\n {{ chrome.l().weekNumberShort }}\n </span>\n }\n @for (name of weekdayNames(); track $index) {\n <span class=\"foldcal-weekday\" role=\"columnheader\">{{ name }}</span>\n }\n</div>\n\n@for (week of weeks(); track week.start) {\n <div class=\"foldcal-week\" role=\"row\" [style.gridTemplateRows]=\"rowTemplate()\">\n <!--\n ISO weeks always start on Monday and belong to the year holding their\n Thursday, so this is not derivable from the calendar's own anchor.\n -->\n @if (showWeekNumbers()) {\n <span\n class=\"foldcal-weeknum\"\n role=\"rowheader\"\n [attr.aria-label]=\"chrome.l().weekNumber(weekNumber(week))\"\n >{{ weekNumber(week) }}</span\n >\n }\n <!--\n The cell is the focusable widget, as the ARIA grid pattern asks: a\n <button> here would have its native role overwritten by role=\"gridcell\",\n so activation is wired by hand instead.\n -->\n @for (day of week.days; track day.date; let column = $index) {\n <div\n class=\"foldcal-day\"\n role=\"gridcell\"\n [attr.data-fold-day]=\"day.date\"\n [attr.data-fold-day-modifiers]=\"dayModifierAttr(day)\"\n [class.is-outside]=\"!day.inMonth\"\n [class.is-today]=\"day.isToday\"\n [class.is-weekend]=\"day.isWeekend\"\n [style.gridColumn]=\"column + 1 + columnOffset()\"\n [attr.tabindex]=\"dayTabIndex(day.date)\"\n [attr.aria-label]=\"dayLabel(day)\"\n [attr.aria-current]=\"day.isToday ? 'date' : null\"\n (click)=\"dayClick.emit(day.date)\"\n (focus)=\"onDayFocus(day.date)\"\n (keydown)=\"onDayKeydown($event, day.date)\"\n >\n @if (dayContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{ $implicit: day }\"\n />\n } @else {\n <span class=\"foldcal-daynum\">{{ day.dayOfMonth }}</span>\n }\n </div>\n }\n\n <!--\n Bands span columns, so they cannot be gridcells; they are hidden from\n the a11y tree and each day cell announces its own event count instead.\n -->\n @for (band of week.bands; track band.key) {\n <button\n type=\"button\"\n class=\"foldcal-band\"\n aria-hidden=\"true\"\n tabindex=\"-1\"\n [attr.data-tone]=\"band.event.tone ?? 'neutral'\"\n [class.continues-before]=\"band.continuesBefore\"\n [class.continues-after]=\"band.continuesAfter\"\n [class.half-start]=\"band.startHalfDay !== undefined\"\n [class.half-end]=\"band.endHalfDay !== undefined\"\n [style.gridColumn]=\"\n band.startColumn +\n 1 +\n columnOffset() +\n ' / ' +\n (band.endColumn + 2 + columnOffset())\n \"\n [style.gridRow]=\"band.lane + 2\"\n [attr.title]=\"bandLabel(band)\"\n (click)=\"eventClick.emit(band.event)\"\n >\n @if (chrome.eventContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{ $implicit: band.event, band: band }\"\n />\n } @else {\n <span class=\"foldcal-band-bar\"></span>\n @if (bandIcon(band); as icon) {\n <fold-icon class=\"foldcal-band-icon\" [name]=\"icon\" size=\"sm\" />\n }\n <span class=\"foldcal-band-label\">{{ bandLabel(band) }}</span>\n @if (band.groupSize > 1) {\n <span class=\"foldcal-band-count\">{{ band.groupSize }}</span>\n }\n }\n </button>\n }\n\n <!-- One chip per crowded day, in that day's own column. -->\n @for (day of week.days; track day.date; let column = $index) {\n @if (day.hiddenCount > 0) {\n <button\n type=\"button\"\n class=\"foldcal-overflow\"\n aria-hidden=\"true\"\n tabindex=\"-1\"\n [style.gridColumn]=\"column + 1 + columnOffset()\"\n [style.gridRow]=\"overflowRow()\"\n (click)=\"overflowClick.emit(day.date)\"\n >\n @if (overflowContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{\n $implicit: day.hiddenCount,\n date: day.date,\n }\"\n />\n } @else {\n {{ chrome.l().moreEvents(day.hiddenCount) }}\n }\n </button>\n }\n }\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;min-height:0;border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-md);background:var(--fold-color-surface-card);overflow:clip}.foldcal-weekdays,.foldcal-week{grid-template-columns:repeat(7,1fr)}:host(.has-week-numbers) .foldcal-weekdays,:host(.has-week-numbers) .foldcal-week{grid-template-columns:var(--fold-calendar-month-week-number-width, 34px) repeat(7,1fr)}.foldcal-weekdays{display:grid;background:var(--fold-color-surface-subtle);border-bottom:1px solid var(--fold-color-border)}.foldcal-weekday{padding:var(--fold-space-sm) var(--fold-space-md);color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);font-weight:600;letter-spacing:.08em;text-transform:uppercase}.foldcal-week{display:grid;flex:1;min-height:var(--fold-calendar-month-row-min-height, 112px);border-bottom:1px solid var(--fold-color-border-subtle)}.foldcal-week:last-child{border-bottom:0}.foldcal-weeknum{grid-row:1/-1;display:flex;align-items:flex-start;justify-content:center;padding-top:var(--fold-space-sm);border-inline-end:1px solid var(--fold-color-border-subtle);background:var(--fold-color-surface-subtle);color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);font-variant-numeric:tabular-nums}.foldcal-weeknum-head{color:var(--fold-color-text-secondary);text-align:center}.foldcal-day{grid-row:1/-1;display:flex;flex-direction:column;align-items:flex-start;padding:var(--fold-calendar-month-cell-padding, var(--fold-space-sm)) var(--fold-calendar-month-cell-padding, var(--fold-space-sm)) var(--fold-space-xs);border-inline-end:1px solid var(--fold-color-border-subtle);color:inherit;font:inherit;text-align:start;cursor:pointer;transition:background-color var(--fold-motion-fast) ease}.foldcal-day:last-of-type{border-inline-end:0}.foldcal-day:hover:not(.is-outside){background:var(--fold-color-surface-hover)}.foldcal-day:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcal-day.is-weekend:not(.is-outside){background:var(--fold-color-surface-subtle)}.foldcal-day.is-outside{background:var(--fold-color-surface-sunken)}.foldcal-day.is-outside .foldcal-daynum{color:var(--fold-color-text-secondary)}.foldcal-day.is-today{background:linear-gradient(180deg,var(--fold-color-primary-surface) 0%,transparent 64%);box-shadow:inset 0 2px 0 0 var(--fold-color-primary)}.foldcal-day.is-today .foldcal-daynum{background:var(--fold-color-primary);color:var(--fold-color-on-primary);font-weight:700}.foldcal-daynum{display:inline-flex;align-items:center;justify-content:center;min-width:var(--fold-calendar-month-daynum-size, 22px);height:var(--fold-calendar-month-daynum-size, 22px);padding:0 var(--fold-space-xs);border-radius:var(--fold-radius-sm);color:var(--fold-color-text-secondary);font-size:var(--fold-text-sm);font-variant-numeric:tabular-nums}.foldcal-band{position:relative;z-index:1;display:flex;align-items:center;gap:var(--fold-space-xs);margin-block:1px;margin-inline:var(--fold-calendar-band-gutter, var(--fold-space-xs));padding:0;padding-inline-end:var(--fold-space-sm);border:0;border-radius:var(--fold-calendar-band-radius, var(--fold-radius-sm));background:var(--fold-color-surface-raised);color:var(--fold-color-text);font:inherit;font-size:var(--fold-text-xs);font-weight:500;line-height:var(--fold-calendar-month-lane, 22px);text-align:start;cursor:pointer;overflow:hidden;transition:filter var(--fold-motion-fast) ease,box-shadow var(--fold-motion-fast) ease}.foldcal-band:hover{z-index:2;filter:brightness(1.08);box-shadow:var(--fold-shadow-sm)}.foldcal-band.continues-before{margin-inline-start:0;border-start-start-radius:0;border-end-start-radius:0;padding-inline-start:var(--fold-space-xs)}.foldcal-band.continues-before .foldcal-band-bar{display:none}.foldcal-band.continues-before:before{content:\"\\2039\";flex:none;color:var(--fold-color-text-faded)}.foldcal-band.continues-after{margin-inline-end:0;border-start-end-radius:0;border-end-end-radius:0}.foldcal-band.continues-after:after{content:\"\\203a\";flex:none;color:var(--fold-color-text-faded)}.foldcal-band.half-start{border-end-start-radius:var(--fold-radius-lg)}.foldcal-band.half-end{border-start-end-radius:var(--fold-radius-lg)}[dir=rtl] .foldcal-band.continues-before:before{content:\"\\203a\"}[dir=rtl] .foldcal-band.continues-after:after{content:\"\\2039\"}.foldcal-band-bar{flex:0 0 var(--fold-calendar-bar-width, 3px);align-self:stretch;background:var(--fold-color-text-muted)}.foldcal-band-icon{flex:none;margin-inline-start:2px;opacity:.9}.foldcal-band-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.foldcal-band-count{flex:none;padding:0 var(--fold-space-xs);border-radius:var(--fold-radius-pill);background:var(--fold-color-surface-sunken);color:var(--fold-color-text-secondary);font-variant-numeric:tabular-nums}.foldcal-band[data-tone=success]{background:var(--fold-color-success-surface);color:var(--fold-color-success-text)}.foldcal-band[data-tone=success] .foldcal-band-bar{background:var(--fold-color-success)}.foldcal-band[data-tone=warning]{background:var(--fold-color-warning-surface);color:var(--fold-color-warning-text)}.foldcal-band[data-tone=warning] .foldcal-band-bar{background:var(--fold-color-warning)}.foldcal-band[data-tone=alert]{background:var(--fold-color-alert-surface);color:var(--fold-color-alert-text)}.foldcal-band[data-tone=alert] .foldcal-band-bar{background:var(--fold-color-alert)}.foldcal-band[data-tone=muted]{background:transparent;color:var(--fold-color-text-secondary)}.foldcal-band[data-tone=muted] .foldcal-band-bar{background:var(--fold-color-text-faded)}.foldcal-band[data-tone=muted] .foldcal-band-label{color:var(--fold-color-text-secondary);text-decoration:line-through;text-decoration-thickness:1px}@media(forced-colors:active){.foldcal-band{border:1px solid CanvasText}.foldcal-band-bar{forced-color-adjust:none}}.foldcal-overflow{justify-self:start;align-self:start;z-index:1;margin-inline:var(--fold-calendar-band-gutter, var(--fold-space-xs));padding:0 var(--fold-space-xs);border:0;border-radius:var(--fold-calendar-band-radius, var(--fold-radius-sm));background:transparent;color:var(--fold-color-text-secondary);font:inherit;font-size:var(--fold-text-xs);font-weight:500;cursor:pointer}.foldcal-overflow:hover{color:var(--fold-color-text);background:var(--fold-color-surface-hover)}@media(forced-colors:active){.foldcal-day.is-today{outline:2px solid Highlight;outline-offset:-2px}.foldcal-day:focus-visible{outline-color:Highlight}.foldcal-day.is-outside .foldcal-daynum{opacity:.6}.foldcal-day.is-weekend{border-block-end:2px solid GrayText}}@media(prefers-reduced-motion:reduce){.foldcal-day,.foldcal-band{transition:none}}@media print{:host{border-color:CanvasText;background:Canvas}.foldcal-weekdays,.foldcal-weekday,.foldcal-day,.foldcal-daynum,.foldcal-weeknum,.foldcal-overflow{background:Canvas;color:CanvasText}.foldcal-week,.foldcal-day,.foldcal-weeknum{border-color:GrayText}.foldcal-day.is-today{background:Canvas;box-shadow:inset 0 2px 0 0 CanvasText}.foldcal-day.is-today .foldcal-daynum{font-weight:700;text-decoration:underline}.foldcal-day.is-outside{opacity:.55}.foldcal-band{border:1px solid GrayText;background:Canvas;color:CanvasText;box-shadow:none}.foldcal-band-bar{-webkit-print-color-adjust:exact;print-color-adjust:exact}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: FoldIconComponent, selector: "fold-icon", inputs: ["name", "size", "tone", "title"] }] });
|
|
5771
|
+
}
|
|
5772
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarMonthComponent, decorators: [{
|
|
5773
|
+
type: Component,
|
|
5774
|
+
args: [{ selector: "fold-calendar-month", standalone: true, imports: [NgTemplateOutlet, FoldIconComponent], hostDirectives: [
|
|
5775
|
+
{
|
|
5776
|
+
directive: FoldCalendarChromeDirective,
|
|
5777
|
+
inputs: ["locale", "labels", "formats"],
|
|
5778
|
+
},
|
|
5779
|
+
], host: {
|
|
5780
|
+
"data-fold-calendar": "",
|
|
5781
|
+
role: "grid",
|
|
5782
|
+
"[attr.aria-label]": "chrome.l().grid",
|
|
5783
|
+
"[class.has-week-numbers]": "showWeekNumbers()",
|
|
5784
|
+
}, template: "<div class=\"foldcal-weekdays\" role=\"row\">\n @if (showWeekNumbers()) {\n <span class=\"foldcal-weekday foldcal-weeknum-head\" role=\"columnheader\">\n {{ chrome.l().weekNumberShort }}\n </span>\n }\n @for (name of weekdayNames(); track $index) {\n <span class=\"foldcal-weekday\" role=\"columnheader\">{{ name }}</span>\n }\n</div>\n\n@for (week of weeks(); track week.start) {\n <div class=\"foldcal-week\" role=\"row\" [style.gridTemplateRows]=\"rowTemplate()\">\n <!--\n ISO weeks always start on Monday and belong to the year holding their\n Thursday, so this is not derivable from the calendar's own anchor.\n -->\n @if (showWeekNumbers()) {\n <span\n class=\"foldcal-weeknum\"\n role=\"rowheader\"\n [attr.aria-label]=\"chrome.l().weekNumber(weekNumber(week))\"\n >{{ weekNumber(week) }}</span\n >\n }\n <!--\n The cell is the focusable widget, as the ARIA grid pattern asks: a\n <button> here would have its native role overwritten by role=\"gridcell\",\n so activation is wired by hand instead.\n -->\n @for (day of week.days; track day.date; let column = $index) {\n <div\n class=\"foldcal-day\"\n role=\"gridcell\"\n [attr.data-fold-day]=\"day.date\"\n [attr.data-fold-day-modifiers]=\"dayModifierAttr(day)\"\n [class.is-outside]=\"!day.inMonth\"\n [class.is-today]=\"day.isToday\"\n [class.is-weekend]=\"day.isWeekend\"\n [style.gridColumn]=\"column + 1 + columnOffset()\"\n [attr.tabindex]=\"dayTabIndex(day.date)\"\n [attr.aria-label]=\"dayLabel(day)\"\n [attr.aria-current]=\"day.isToday ? 'date' : null\"\n (click)=\"dayClick.emit(day.date)\"\n (focus)=\"onDayFocus(day.date)\"\n (keydown)=\"onDayKeydown($event, day.date)\"\n >\n @if (dayContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{ $implicit: day }\"\n />\n } @else {\n <span class=\"foldcal-daynum\">{{ day.dayOfMonth }}</span>\n }\n </div>\n }\n\n <!--\n Bands span columns, so they cannot be gridcells; they are hidden from\n the a11y tree and each day cell announces its own event count instead.\n -->\n @for (band of week.bands; track band.key) {\n <button\n type=\"button\"\n class=\"foldcal-band\"\n aria-hidden=\"true\"\n tabindex=\"-1\"\n [attr.data-tone]=\"band.event.tone ?? 'neutral'\"\n [class.continues-before]=\"band.continuesBefore\"\n [class.continues-after]=\"band.continuesAfter\"\n [class.half-start]=\"band.startHalfDay !== undefined\"\n [class.half-end]=\"band.endHalfDay !== undefined\"\n [style.gridColumn]=\"\n band.startColumn +\n 1 +\n columnOffset() +\n ' / ' +\n (band.endColumn + 2 + columnOffset())\n \"\n [style.gridRow]=\"band.lane + 2\"\n [attr.title]=\"bandLabel(band)\"\n (click)=\"eventClick.emit(band.event)\"\n >\n @if (chrome.eventContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{ $implicit: band.event, band: band }\"\n />\n } @else {\n <span class=\"foldcal-band-bar\"></span>\n @if (bandIcon(band); as icon) {\n <fold-icon class=\"foldcal-band-icon\" [name]=\"icon\" size=\"sm\" />\n }\n <span class=\"foldcal-band-label\">{{ bandLabel(band) }}</span>\n @if (band.groupSize > 1) {\n <span class=\"foldcal-band-count\">{{ band.groupSize }}</span>\n }\n }\n </button>\n }\n\n <!-- One chip per crowded day, in that day's own column. -->\n @for (day of week.days; track day.date; let column = $index) {\n @if (day.hiddenCount > 0) {\n <button\n type=\"button\"\n class=\"foldcal-overflow\"\n aria-hidden=\"true\"\n tabindex=\"-1\"\n [style.gridColumn]=\"column + 1 + columnOffset()\"\n [style.gridRow]=\"overflowRow()\"\n (click)=\"overflowClick.emit(day.date)\"\n >\n @if (overflowContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{\n $implicit: day.hiddenCount,\n date: day.date,\n }\"\n />\n } @else {\n {{ chrome.l().moreEvents(day.hiddenCount) }}\n }\n </button>\n }\n }\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;min-height:0;border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-md);background:var(--fold-color-surface-card);overflow:clip}.foldcal-weekdays,.foldcal-week{grid-template-columns:repeat(7,1fr)}:host(.has-week-numbers) .foldcal-weekdays,:host(.has-week-numbers) .foldcal-week{grid-template-columns:var(--fold-calendar-month-week-number-width, 34px) repeat(7,1fr)}.foldcal-weekdays{display:grid;background:var(--fold-color-surface-subtle);border-bottom:1px solid var(--fold-color-border)}.foldcal-weekday{padding:var(--fold-space-sm) var(--fold-space-md);color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);font-weight:600;letter-spacing:.08em;text-transform:uppercase}.foldcal-week{display:grid;flex:1;min-height:var(--fold-calendar-month-row-min-height, 112px);border-bottom:1px solid var(--fold-color-border-subtle)}.foldcal-week:last-child{border-bottom:0}.foldcal-weeknum{grid-row:1/-1;display:flex;align-items:flex-start;justify-content:center;padding-top:var(--fold-space-sm);border-inline-end:1px solid var(--fold-color-border-subtle);background:var(--fold-color-surface-subtle);color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);font-variant-numeric:tabular-nums}.foldcal-weeknum-head{color:var(--fold-color-text-secondary);text-align:center}.foldcal-day{grid-row:1/-1;display:flex;flex-direction:column;align-items:flex-start;padding:var(--fold-calendar-month-cell-padding, var(--fold-space-sm)) var(--fold-calendar-month-cell-padding, var(--fold-space-sm)) var(--fold-space-xs);border-inline-end:1px solid var(--fold-color-border-subtle);color:inherit;font:inherit;text-align:start;cursor:pointer;transition:background-color var(--fold-motion-fast) ease}.foldcal-day:last-of-type{border-inline-end:0}.foldcal-day:hover:not(.is-outside){background:var(--fold-color-surface-hover)}.foldcal-day:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcal-day.is-weekend:not(.is-outside){background:var(--fold-color-surface-subtle)}.foldcal-day.is-outside{background:var(--fold-color-surface-sunken)}.foldcal-day.is-outside .foldcal-daynum{color:var(--fold-color-text-secondary)}.foldcal-day.is-today{background:linear-gradient(180deg,var(--fold-color-primary-surface) 0%,transparent 64%);box-shadow:inset 0 2px 0 0 var(--fold-color-primary)}.foldcal-day.is-today .foldcal-daynum{background:var(--fold-color-primary);color:var(--fold-color-on-primary);font-weight:700}.foldcal-daynum{display:inline-flex;align-items:center;justify-content:center;min-width:var(--fold-calendar-month-daynum-size, 22px);height:var(--fold-calendar-month-daynum-size, 22px);padding:0 var(--fold-space-xs);border-radius:var(--fold-radius-sm);color:var(--fold-color-text-secondary);font-size:var(--fold-text-sm);font-variant-numeric:tabular-nums}.foldcal-band{position:relative;z-index:1;display:flex;align-items:center;gap:var(--fold-space-xs);margin-block:1px;margin-inline:var(--fold-calendar-band-gutter, var(--fold-space-xs));padding:0;padding-inline-end:var(--fold-space-sm);border:0;border-radius:var(--fold-calendar-band-radius, var(--fold-radius-sm));background:var(--fold-color-surface-raised);color:var(--fold-color-text);font:inherit;font-size:var(--fold-text-xs);font-weight:500;line-height:var(--fold-calendar-month-lane, 22px);text-align:start;cursor:pointer;overflow:hidden;transition:filter var(--fold-motion-fast) ease,box-shadow var(--fold-motion-fast) ease}.foldcal-band:hover{z-index:2;filter:brightness(1.08);box-shadow:var(--fold-shadow-sm)}.foldcal-band.continues-before{margin-inline-start:0;border-start-start-radius:0;border-end-start-radius:0;padding-inline-start:var(--fold-space-xs)}.foldcal-band.continues-before .foldcal-band-bar{display:none}.foldcal-band.continues-before:before{content:\"\\2039\";flex:none;color:var(--fold-color-text-faded)}.foldcal-band.continues-after{margin-inline-end:0;border-start-end-radius:0;border-end-end-radius:0}.foldcal-band.continues-after:after{content:\"\\203a\";flex:none;color:var(--fold-color-text-faded)}.foldcal-band.half-start{border-end-start-radius:var(--fold-radius-lg)}.foldcal-band.half-end{border-start-end-radius:var(--fold-radius-lg)}[dir=rtl] .foldcal-band.continues-before:before{content:\"\\203a\"}[dir=rtl] .foldcal-band.continues-after:after{content:\"\\2039\"}.foldcal-band-bar{flex:0 0 var(--fold-calendar-bar-width, 3px);align-self:stretch;background:var(--fold-color-text-muted)}.foldcal-band-icon{flex:none;margin-inline-start:2px;opacity:.9}.foldcal-band-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.foldcal-band-count{flex:none;padding:0 var(--fold-space-xs);border-radius:var(--fold-radius-pill);background:var(--fold-color-surface-sunken);color:var(--fold-color-text-secondary);font-variant-numeric:tabular-nums}.foldcal-band[data-tone=success]{background:var(--fold-color-success-surface);color:var(--fold-color-success-text)}.foldcal-band[data-tone=success] .foldcal-band-bar{background:var(--fold-color-success)}.foldcal-band[data-tone=warning]{background:var(--fold-color-warning-surface);color:var(--fold-color-warning-text)}.foldcal-band[data-tone=warning] .foldcal-band-bar{background:var(--fold-color-warning)}.foldcal-band[data-tone=alert]{background:var(--fold-color-alert-surface);color:var(--fold-color-alert-text)}.foldcal-band[data-tone=alert] .foldcal-band-bar{background:var(--fold-color-alert)}.foldcal-band[data-tone=muted]{background:transparent;color:var(--fold-color-text-secondary)}.foldcal-band[data-tone=muted] .foldcal-band-bar{background:var(--fold-color-text-faded)}.foldcal-band[data-tone=muted] .foldcal-band-label{color:var(--fold-color-text-secondary);text-decoration:line-through;text-decoration-thickness:1px}@media(forced-colors:active){.foldcal-band{border:1px solid CanvasText}.foldcal-band-bar{forced-color-adjust:none}}.foldcal-overflow{justify-self:start;align-self:start;z-index:1;margin-inline:var(--fold-calendar-band-gutter, var(--fold-space-xs));padding:0 var(--fold-space-xs);border:0;border-radius:var(--fold-calendar-band-radius, var(--fold-radius-sm));background:transparent;color:var(--fold-color-text-secondary);font:inherit;font-size:var(--fold-text-xs);font-weight:500;cursor:pointer}.foldcal-overflow:hover{color:var(--fold-color-text);background:var(--fold-color-surface-hover)}@media(forced-colors:active){.foldcal-day.is-today{outline:2px solid Highlight;outline-offset:-2px}.foldcal-day:focus-visible{outline-color:Highlight}.foldcal-day.is-outside .foldcal-daynum{opacity:.6}.foldcal-day.is-weekend{border-block-end:2px solid GrayText}}@media(prefers-reduced-motion:reduce){.foldcal-day,.foldcal-band{transition:none}}@media print{:host{border-color:CanvasText;background:Canvas}.foldcal-weekdays,.foldcal-weekday,.foldcal-day,.foldcal-daynum,.foldcal-weeknum,.foldcal-overflow{background:Canvas;color:CanvasText}.foldcal-week,.foldcal-day,.foldcal-weeknum{border-color:GrayText}.foldcal-day.is-today{background:Canvas;box-shadow:inset 0 2px 0 0 CanvasText}.foldcal-day.is-today .foldcal-daynum{font-weight:700;text-decoration:underline}.foldcal-day.is-outside{opacity:.55}.foldcal-band{border:1px solid GrayText;background:Canvas;color:CanvasText;box-shadow:none}.foldcal-band-bar{-webkit-print-color-adjust:exact;print-color-adjust:exact}}\n"] }]
|
|
5785
|
+
}], propDecorators: { month: [{ type: i0.Input, args: [{ isSignal: true, alias: "month", required: true }] }, { type: i0.Output, args: ["monthChange"] }], events: [{ type: i0.Input, args: [{ isSignal: true, alias: "events", required: false }] }], today: [{ type: i0.Input, args: [{ isSignal: true, alias: "today", required: false }] }], weekStartsOn: [{ type: i0.Input, args: [{ isSignal: true, alias: "weekStartsOn", required: false }] }], weekendDays: [{ type: i0.Input, args: [{ isSignal: true, alias: "weekendDays", required: false }] }], showWeekNumbers: [{ type: i0.Input, args: [{ isSignal: true, alias: "showWeekNumbers", required: false }] }], maxLanes: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLanes", required: false }] }], fixedWeeks: [{ type: i0.Input, args: [{ isSignal: true, alias: "fixedWeeks", required: false }] }], dayModifiers: [{ type: i0.Input, args: [{ isSignal: true, alias: "dayModifiers", required: false }] }], dayClick: [{ type: i0.Output, args: ["dayClick"] }], eventClick: [{ type: i0.Output, args: ["eventClick"] }], overflowClick: [{ type: i0.Output, args: ["overflowClick"] }], projectedDay: [{ type: i0.ContentChild, args: [i0.forwardRef(() => FoldCalendarDayDirective), { isSignal: true }] }], projectedOverflow: [{ type: i0.ContentChild, args: [i0.forwardRef(() => FoldCalendarOverflowDirective), { isSignal: true }] }] } });
|
|
5786
|
+
|
|
5787
|
+
/**
|
|
5788
|
+
* The three selections every view and every consumer makes over a feed.
|
|
5789
|
+
*
|
|
5790
|
+
* Deliberately the smallest, least coupled part of the family: an app that
|
|
5791
|
+
* wants the calendar's semantics — "covers this day", "overlaps this window",
|
|
5792
|
+
* "belongs to a live feed" — without any of its rendering imports only this.
|
|
5793
|
+
*/
|
|
5794
|
+
/**
|
|
5795
|
+
* Events covering `date`, in the caller's order — the selection the week, day
|
|
5796
|
+
* and list views render, where chips stack in a column and need no packing.
|
|
5797
|
+
*/
|
|
5798
|
+
function foldEventsOnDay(events, date) {
|
|
5799
|
+
return events.filter((event) => event.start <= date && event.end >= date);
|
|
5800
|
+
}
|
|
5801
|
+
/** Events overlapping `[from, to]` at all, in the caller's order. */
|
|
5802
|
+
function foldEventsInRange(events, from, to) {
|
|
5803
|
+
return events.filter((event) => event.end >= from && event.start <= to);
|
|
5804
|
+
}
|
|
5805
|
+
/**
|
|
5806
|
+
* Events from the feeds in `active`, in the caller's order — the other half of
|
|
5807
|
+
* `fold-calendar-source-filter`, which owns only the selection.
|
|
5808
|
+
*
|
|
5809
|
+
* `null` means nothing has been switched off yet and everything passes; it is
|
|
5810
|
+
* the filter chips' own initial value, so the one function built to consume
|
|
5811
|
+
* that model accepts it rather than making every caller write the ternary.
|
|
5812
|
+
* An empty `Set` is the opposite — every feed switched off — which is why the
|
|
5813
|
+
* two are not collapsed.
|
|
5814
|
+
*
|
|
5815
|
+
* An event with no `sourceKey` belongs to no feed, so no chip can hide it and
|
|
5816
|
+
* it always passes.
|
|
5817
|
+
*/
|
|
5818
|
+
function foldFilterBySource(events, active) {
|
|
5819
|
+
if (active === null) {
|
|
5820
|
+
return events;
|
|
5821
|
+
}
|
|
5822
|
+
return events.filter((event) => event.sourceKey === undefined || active.has(event.sourceKey));
|
|
5823
|
+
}
|
|
5824
|
+
|
|
5825
|
+
/** Columns in a week row. */
|
|
5826
|
+
const DAYS_PER_WEEK = 7;
|
|
5827
|
+
/** One day cell plus the events covering it. */
|
|
5828
|
+
function dayWithEvents(events, epochDay, month, options, counts) {
|
|
5829
|
+
const day = foldMakeDay(epochDay, {
|
|
5830
|
+
month,
|
|
5831
|
+
today: options.today,
|
|
5832
|
+
weekendDays: foldWeekendOr(options.weekendDays),
|
|
5833
|
+
counts,
|
|
5834
|
+
hidden: FOLD_NO_HIDDEN_DAYS,
|
|
5835
|
+
});
|
|
5836
|
+
return { day, events: foldEventsOnDay(events, day.date) };
|
|
5837
|
+
}
|
|
5838
|
+
/**
|
|
5839
|
+
* The seven days of `date`'s week, each with the events covering it — the
|
|
5840
|
+
* render model of the week view.
|
|
5841
|
+
*/
|
|
5842
|
+
function foldBuildWeek(events, options) {
|
|
5843
|
+
const startDay = foldToEpochDay(foldStartOfWeek(options.date, options.weekStartsOn ?? "mon"));
|
|
5844
|
+
const month = options.date.slice(0, 7);
|
|
5845
|
+
const counts = foldCountByDay(events, startDay, startDay + DAYS_PER_WEEK - 1);
|
|
5846
|
+
return Array.from({ length: DAYS_PER_WEEK }, (_unused, offset) => dayWithEvents(events, startDay + offset, month, options, counts));
|
|
5847
|
+
}
|
|
5848
|
+
/** A single day with its events — the render model of the day view. */
|
|
5849
|
+
function foldBuildDay(events, options) {
|
|
5850
|
+
const day = foldToEpochDay(options.date);
|
|
5851
|
+
return dayWithEvents(events, day, options.date.slice(0, 7), options, foldCountByDay(events, day, day));
|
|
5852
|
+
}
|
|
5853
|
+
|
|
5854
|
+
/**
|
|
5855
|
+
* `<fold-calendar-week>` — one week as seven day columns of stacked chips.
|
|
5856
|
+
*
|
|
5857
|
+
* Where the month view packs spans into lanes, a week simply lists what covers
|
|
5858
|
+
* each day: a three-day event appears under all three, which is the reading a
|
|
5859
|
+
* week is for. Nothing spans, so nothing is clipped and there is no overflow.
|
|
5860
|
+
*
|
|
5861
|
+
* ## Inputs
|
|
5862
|
+
* | Input | Type | Default | Meaning |
|
|
5863
|
+
* |----------------|--------------------------|---------|---------|
|
|
5864
|
+
* | `date` | `FoldCalendarDate` | — | Any date in the week on display. |
|
|
5865
|
+
* | `events` | `FoldCalendarEvent<T>[]` | `[]` | What to list. |
|
|
5866
|
+
* | `today` | `FoldCalendarDate` | — | The day to mark. |
|
|
5867
|
+
* | `weekStartsOn` | `FoldWeekday` | the locale's | Which day opens the week — `Intl.Locale` knows. |
|
|
5868
|
+
* | `weekendDays` | `FoldWeekday[]` | the locale's | Which days are shaded. |
|
|
5869
|
+
* | `locale` | `string` | runtime | Drives the column headers through `Intl`. |
|
|
5870
|
+
* | `labels` | `Partial<FoldCalendarLabels>` | — | Per-instance label overrides. |
|
|
5871
|
+
*
|
|
5872
|
+
* `date` is a plain input, not a `model`: this view never pages itself, so
|
|
5873
|
+
* writing back would be a promise it does not keep. Pair it with
|
|
5874
|
+
* `<fold-calendar-toolbar>`, which does own the paging.
|
|
5875
|
+
*
|
|
5876
|
+
* ## Theming
|
|
5877
|
+
* | CSS variable | Default | Sets |
|
|
5878
|
+
* |---------------------------------------|---------|------|
|
|
5879
|
+
* | `--fold-calendar-week-column-height` | `320px` | Height of the day columns. |
|
|
5880
|
+
* | `--fold-calendar-bar-width` | `3px` | The tone bar down a chip's leading edge. |
|
|
5881
|
+
* | `--fold-calendar-band-radius` | `--fold-radius-sm` | Chip corners. |
|
|
5882
|
+
*
|
|
5883
|
+
* ## Outputs
|
|
5884
|
+
* | Output | Payload | Fires on |
|
|
5885
|
+
* |--------------|------------------------|----------|
|
|
5886
|
+
* | `dayClick` | `FoldCalendarDate` | A column header activated. |
|
|
5887
|
+
* | `eventClick` | `FoldCalendarEvent<T>` | A chip activated. |
|
|
5888
|
+
*
|
|
5889
|
+
* ## Accessibility
|
|
5890
|
+
* Each column is a `<section>` named by its full date, holding a header button
|
|
5891
|
+
* and a list of chips. Because a chip sits **inside** the day it belongs to —
|
|
5892
|
+
* no spanning, no overlay — every control is a real child in the natural tab
|
|
5893
|
+
* order, so this view needs none of the month grid's roving-tabindex machinery
|
|
5894
|
+
* and every event is reachable by keyboard.
|
|
5895
|
+
*
|
|
5896
|
+
* @selector `fold-calendar-week`
|
|
5897
|
+
*
|
|
5898
|
+
* @example
|
|
5899
|
+
* ```html
|
|
5900
|
+
* <fold-calendar-week
|
|
5901
|
+
* [(date)]="date"
|
|
5902
|
+
* [events]="events()"
|
|
5903
|
+
* [today]="today"
|
|
5904
|
+
* (eventClick)="open($event)"
|
|
5905
|
+
* />
|
|
5906
|
+
* ```
|
|
5907
|
+
*/
|
|
5908
|
+
class FoldCalendarWeekComponent {
|
|
5909
|
+
/** Any date inside the week on display. */
|
|
5910
|
+
date = input.required(/* @ts-ignore */
|
|
5911
|
+
...(ngDevMode ? [{ debugName: "date" }] : /* istanbul ignore next */ []));
|
|
5912
|
+
/** Events to list under the days they cover. */
|
|
5913
|
+
events = input([], /* @ts-ignore */
|
|
5914
|
+
...(ngDevMode ? [{ debugName: "events" }] : /* istanbul ignore next */ []));
|
|
5915
|
+
/** The day to mark as today. */
|
|
5916
|
+
today = input(/* @ts-ignore */
|
|
5917
|
+
...(ngDevMode ? [undefined, { debugName: "today" }] : /* istanbul ignore next */ []));
|
|
5918
|
+
/** Day the week starts on. @default the locale's own first day */
|
|
5919
|
+
weekStartsOn = input(/* @ts-ignore */
|
|
5920
|
+
...(ngDevMode ? [undefined, { debugName: "weekStartsOn" }] : /* istanbul ignore next */ []));
|
|
5921
|
+
/** Days shaded as the weekend. @default ['sat', 'sun'] */
|
|
5922
|
+
weekendDays = input(/* @ts-ignore */
|
|
5923
|
+
...(ngDevMode ? [undefined, { debugName: "weekendDays" }] : /* istanbul ignore next */ []));
|
|
5924
|
+
/** A column header was activated. */
|
|
5925
|
+
dayClick = output();
|
|
5926
|
+
/** A chip was activated. */
|
|
5927
|
+
eventClick = output();
|
|
5928
|
+
/** Labels, locale and the projected chip template — see the directive. */
|
|
5929
|
+
chrome = inject(FoldCalendarChromeDirective);
|
|
5930
|
+
columns = computed(() => foldBuildWeek(this.events(), {
|
|
5931
|
+
date: this.date(),
|
|
5932
|
+
weekStartsOn: this.chrome.anchor(this.weekStartsOn()),
|
|
5933
|
+
weekendDays: this.chrome.weekend(this.weekendDays()),
|
|
5934
|
+
today: this.today(),
|
|
5935
|
+
}), /* @ts-ignore */
|
|
5936
|
+
...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
|
|
5937
|
+
weekdayName(column) {
|
|
5938
|
+
return this.chrome.format(column.day.date, "weekdayShort");
|
|
5939
|
+
}
|
|
5940
|
+
/** A column's accessible name: its date, the today marker, and what sits on it. */
|
|
5941
|
+
columnLabel(column) {
|
|
5942
|
+
const labels = this.chrome.l();
|
|
5943
|
+
const parts = [this.chrome.format(column.day.date, "dateFull")];
|
|
5944
|
+
if (column.day.isToday) {
|
|
5945
|
+
parts.push(labels.today);
|
|
5946
|
+
}
|
|
5947
|
+
if (column.day.eventCount > 0) {
|
|
5948
|
+
parts.push(labels.eventCount(column.day.eventCount));
|
|
5949
|
+
}
|
|
5950
|
+
return parts.join(", ");
|
|
5951
|
+
}
|
|
5952
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarWeekComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5953
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: FoldCalendarWeekComponent, isStandalone: true, selector: "fold-calendar-week", inputs: { date: { classPropertyName: "date", publicName: "date", isSignal: true, isRequired: true, transformFunction: null }, events: { classPropertyName: "events", publicName: "events", isSignal: true, isRequired: false, transformFunction: null }, today: { classPropertyName: "today", publicName: "today", isSignal: true, isRequired: false, transformFunction: null }, weekStartsOn: { classPropertyName: "weekStartsOn", publicName: "weekStartsOn", isSignal: true, isRequired: false, transformFunction: null }, weekendDays: { classPropertyName: "weekendDays", publicName: "weekendDays", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dayClick: "dayClick", eventClick: "eventClick" }, hostDirectives: [{ directive: FoldCalendarChromeDirective, inputs: ["locale", "locale", "labels", "labels", "formats", "formats"] }], ngImport: i0, template: "<div class=\"foldcalw\" [attr.aria-label]=\"chrome.l().grid\" role=\"group\">\n @for (column of columns(); track column.day.date) {\n <section\n class=\"foldcalw-col\"\n [class.is-today]=\"column.day.isToday\"\n [class.is-weekend]=\"column.day.isWeekend\"\n [attr.aria-label]=\"columnLabel(column)\"\n >\n <button\n type=\"button\"\n class=\"foldcalw-head\"\n [attr.aria-current]=\"column.day.isToday ? 'date' : null\"\n (click)=\"dayClick.emit(column.day.date)\"\n >\n <span class=\"foldcalw-weekday\">{{ weekdayName(column) }}</span>\n <span class=\"foldcalw-daynum\">{{ column.day.dayOfMonth }}</span>\n </button>\n\n <ul class=\"foldcalw-list\">\n @for (event of column.events; track event.id) {\n <li>\n <button\n type=\"button\"\n class=\"foldcal-chip\"\n [attr.data-tone]=\"event.tone ?? 'neutral'\"\n (click)=\"eventClick.emit(event)\"\n >\n @if (chrome.eventContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{ $implicit: event, band: null }\"\n />\n } @else {\n <span class=\"foldcal-chip-bar\"></span>\n @if (event.icon; as icon) {\n <fold-icon\n class=\"foldcal-chip-icon\"\n [name]=\"icon\"\n size=\"sm\"\n />\n }\n <span class=\"foldcal-chip-body\">\n <span class=\"foldcal-chip-label\">{{ event.label }}</span>\n @if (event.subline) {\n <span class=\"foldcal-chip-sub\">{{ event.subline }}</span>\n }\n </span>\n }\n </button>\n </li>\n }\n </ul>\n </section>\n }\n</div>\n", styles: [":host{display:block}.foldcal-chip{position:relative;display:flex;align-items:stretch;gap:var(--fold-space-sm);width:100%;padding:var(--fold-space-xs) var(--fold-space-sm);padding-inline-start:0;border:0;border-radius:var(--fold-calendar-band-radius, var(--fold-radius-sm));background:var(--fold-color-surface-raised);color:var(--fold-color-text);font:inherit;text-align:start;cursor:pointer;overflow:hidden;transition:background-color var(--fold-motion-fast) ease}.foldcal-chip:hover{background:var(--fold-color-surface-hover)}.foldcal-chip:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcal-chip-bar{flex:0 0 var(--fold-calendar-bar-width, 3px);align-self:stretch;background:var(--fold-color-text-muted)}.foldcal-chip-icon{flex:none;align-self:center;margin-inline-start:var(--fold-space-sm);color:var(--fold-color-text-secondary)}.foldcal-chip-body{display:flex;flex-direction:column;gap:1px;min-width:0}.foldcal-chip-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--fold-text-sm);font-weight:500}.foldcal-chip-sub{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs)}.foldcal-chip[data-tone=success] .foldcal-chip-bar{background:var(--fold-color-success)}.foldcal-chip[data-tone=warning] .foldcal-chip-bar{background:var(--fold-color-warning)}.foldcal-chip[data-tone=alert] .foldcal-chip-bar{background:var(--fold-color-alert)}.foldcal-chip[data-tone=muted] .foldcal-chip-bar{background:var(--fold-color-text-faded)}.foldcal-chip[data-tone=muted] .foldcal-chip-label{color:var(--fold-color-text-secondary);text-decoration:line-through;text-decoration-thickness:1px}@media(forced-colors:active){.foldcal-chip{border:1px solid CanvasText}.foldcal-chip-bar{forced-color-adjust:none}}@media(prefers-reduced-motion:reduce){.foldcal-chip{transition:none}}.foldcalw{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));min-height:var(--fold-calendar-week-column-height, 320px);border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-md);background:var(--fold-color-surface-card);overflow:clip}.foldcalw-col{container-type:inline-size;display:flex;flex-direction:column;gap:var(--fold-space-sm);min-width:0;padding:var(--fold-space-sm);border-inline-end:1px solid var(--fold-color-border-subtle)}.foldcalw-col:last-child{border-inline-end:0}.foldcalw-col.is-weekend{background:var(--fold-color-surface-subtle)}.foldcalw-col.is-today{background:linear-gradient(180deg,var(--fold-color-primary-surface) 0%,transparent 28%);box-shadow:inset 0 2px 0 0 var(--fold-color-primary)}.foldcalw-head{display:flex;flex-direction:column;align-items:flex-start;gap:1px;padding:0 0 var(--fold-space-xs);border:0;border-bottom:1px solid var(--fold-color-border-subtle);background:transparent;color:inherit;font:inherit;text-align:start;cursor:pointer}.foldcalw-head:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:2px}.foldcalw-weekday{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase}.foldcalw-daynum{color:var(--fold-color-text);font-size:var(--fold-text-lg);font-weight:600;font-variant-numeric:tabular-nums;line-height:1}.is-today .foldcalw-daynum{color:var(--fold-color-primary-text)}.foldcalw-list{display:flex;flex-direction:column;gap:var(--fold-space-xs);margin:0;padding:0;list-style:none}@container (max-width: 150px){.foldcal-chip-sub{display:none}}@container (max-width: 110px){.foldcal-chip-icon{display:none}.foldcal-chip{gap:var(--fold-space-xs);padding-inline-end:var(--fold-space-xs)}}@media(forced-colors:active){.foldcalw-col.is-today{outline:2px solid Highlight;outline-offset:-2px}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: FoldIconComponent, selector: "fold-icon", inputs: ["name", "size", "tone", "title"] }] });
|
|
5954
|
+
}
|
|
5955
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarWeekComponent, decorators: [{
|
|
5956
|
+
type: Component,
|
|
5957
|
+
args: [{ selector: "fold-calendar-week", standalone: true, imports: [NgTemplateOutlet, FoldIconComponent], hostDirectives: [
|
|
5958
|
+
{
|
|
5959
|
+
directive: FoldCalendarChromeDirective,
|
|
5960
|
+
inputs: ["locale", "labels", "formats"],
|
|
5961
|
+
},
|
|
5962
|
+
], template: "<div class=\"foldcalw\" [attr.aria-label]=\"chrome.l().grid\" role=\"group\">\n @for (column of columns(); track column.day.date) {\n <section\n class=\"foldcalw-col\"\n [class.is-today]=\"column.day.isToday\"\n [class.is-weekend]=\"column.day.isWeekend\"\n [attr.aria-label]=\"columnLabel(column)\"\n >\n <button\n type=\"button\"\n class=\"foldcalw-head\"\n [attr.aria-current]=\"column.day.isToday ? 'date' : null\"\n (click)=\"dayClick.emit(column.day.date)\"\n >\n <span class=\"foldcalw-weekday\">{{ weekdayName(column) }}</span>\n <span class=\"foldcalw-daynum\">{{ column.day.dayOfMonth }}</span>\n </button>\n\n <ul class=\"foldcalw-list\">\n @for (event of column.events; track event.id) {\n <li>\n <button\n type=\"button\"\n class=\"foldcal-chip\"\n [attr.data-tone]=\"event.tone ?? 'neutral'\"\n (click)=\"eventClick.emit(event)\"\n >\n @if (chrome.eventContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{ $implicit: event, band: null }\"\n />\n } @else {\n <span class=\"foldcal-chip-bar\"></span>\n @if (event.icon; as icon) {\n <fold-icon\n class=\"foldcal-chip-icon\"\n [name]=\"icon\"\n size=\"sm\"\n />\n }\n <span class=\"foldcal-chip-body\">\n <span class=\"foldcal-chip-label\">{{ event.label }}</span>\n @if (event.subline) {\n <span class=\"foldcal-chip-sub\">{{ event.subline }}</span>\n }\n </span>\n }\n </button>\n </li>\n }\n </ul>\n </section>\n }\n</div>\n", styles: [":host{display:block}.foldcal-chip{position:relative;display:flex;align-items:stretch;gap:var(--fold-space-sm);width:100%;padding:var(--fold-space-xs) var(--fold-space-sm);padding-inline-start:0;border:0;border-radius:var(--fold-calendar-band-radius, var(--fold-radius-sm));background:var(--fold-color-surface-raised);color:var(--fold-color-text);font:inherit;text-align:start;cursor:pointer;overflow:hidden;transition:background-color var(--fold-motion-fast) ease}.foldcal-chip:hover{background:var(--fold-color-surface-hover)}.foldcal-chip:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcal-chip-bar{flex:0 0 var(--fold-calendar-bar-width, 3px);align-self:stretch;background:var(--fold-color-text-muted)}.foldcal-chip-icon{flex:none;align-self:center;margin-inline-start:var(--fold-space-sm);color:var(--fold-color-text-secondary)}.foldcal-chip-body{display:flex;flex-direction:column;gap:1px;min-width:0}.foldcal-chip-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--fold-text-sm);font-weight:500}.foldcal-chip-sub{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs)}.foldcal-chip[data-tone=success] .foldcal-chip-bar{background:var(--fold-color-success)}.foldcal-chip[data-tone=warning] .foldcal-chip-bar{background:var(--fold-color-warning)}.foldcal-chip[data-tone=alert] .foldcal-chip-bar{background:var(--fold-color-alert)}.foldcal-chip[data-tone=muted] .foldcal-chip-bar{background:var(--fold-color-text-faded)}.foldcal-chip[data-tone=muted] .foldcal-chip-label{color:var(--fold-color-text-secondary);text-decoration:line-through;text-decoration-thickness:1px}@media(forced-colors:active){.foldcal-chip{border:1px solid CanvasText}.foldcal-chip-bar{forced-color-adjust:none}}@media(prefers-reduced-motion:reduce){.foldcal-chip{transition:none}}.foldcalw{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));min-height:var(--fold-calendar-week-column-height, 320px);border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-md);background:var(--fold-color-surface-card);overflow:clip}.foldcalw-col{container-type:inline-size;display:flex;flex-direction:column;gap:var(--fold-space-sm);min-width:0;padding:var(--fold-space-sm);border-inline-end:1px solid var(--fold-color-border-subtle)}.foldcalw-col:last-child{border-inline-end:0}.foldcalw-col.is-weekend{background:var(--fold-color-surface-subtle)}.foldcalw-col.is-today{background:linear-gradient(180deg,var(--fold-color-primary-surface) 0%,transparent 28%);box-shadow:inset 0 2px 0 0 var(--fold-color-primary)}.foldcalw-head{display:flex;flex-direction:column;align-items:flex-start;gap:1px;padding:0 0 var(--fold-space-xs);border:0;border-bottom:1px solid var(--fold-color-border-subtle);background:transparent;color:inherit;font:inherit;text-align:start;cursor:pointer}.foldcalw-head:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:2px}.foldcalw-weekday{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase}.foldcalw-daynum{color:var(--fold-color-text);font-size:var(--fold-text-lg);font-weight:600;font-variant-numeric:tabular-nums;line-height:1}.is-today .foldcalw-daynum{color:var(--fold-color-primary-text)}.foldcalw-list{display:flex;flex-direction:column;gap:var(--fold-space-xs);margin:0;padding:0;list-style:none}@container (max-width: 150px){.foldcal-chip-sub{display:none}}@container (max-width: 110px){.foldcal-chip-icon{display:none}.foldcal-chip{gap:var(--fold-space-xs);padding-inline-end:var(--fold-space-xs)}}@media(forced-colors:active){.foldcalw-col.is-today{outline:2px solid Highlight;outline-offset:-2px}}\n"] }]
|
|
5963
|
+
}], propDecorators: { date: [{ type: i0.Input, args: [{ isSignal: true, alias: "date", required: true }] }], events: [{ type: i0.Input, args: [{ isSignal: true, alias: "events", required: false }] }], today: [{ type: i0.Input, args: [{ isSignal: true, alias: "today", required: false }] }], weekStartsOn: [{ type: i0.Input, args: [{ isSignal: true, alias: "weekStartsOn", required: false }] }], weekendDays: [{ type: i0.Input, args: [{ isSignal: true, alias: "weekendDays", required: false }] }], dayClick: [{ type: i0.Output, args: ["dayClick"] }], eventClick: [{ type: i0.Output, args: ["eventClick"] }] } });
|
|
5964
|
+
|
|
5965
|
+
/**
|
|
5966
|
+
* `<fold-calendar-day>` — one day, its events listed in full.
|
|
5967
|
+
*
|
|
5968
|
+
* The end of the drill-down: where the month view has room only for a label,
|
|
5969
|
+
* this one has room for the subline too, so it is the view a `dayClick`
|
|
5970
|
+
* naturally lands on.
|
|
5971
|
+
*
|
|
5972
|
+
* ## Inputs
|
|
5973
|
+
* | Input | Type | Default | Meaning |
|
|
5974
|
+
* |----------|--------------------------|---------|---------|
|
|
5975
|
+
* | `date` | `FoldCalendarDate` | — | The day on display. |
|
|
5976
|
+
* | `events` | `FoldCalendarEvent<T>[]` | `[]` | Filtered to those covering `date`. |
|
|
5977
|
+
* | `today` | `FoldCalendarDate` | — | Marks the header when it matches. |
|
|
5978
|
+
* | `locale` | `string` | runtime | Drives the header through `Intl`. |
|
|
5979
|
+
* | `labels` | `Partial<FoldCalendarLabels>` | — | Per-instance label overrides. |
|
|
5980
|
+
*
|
|
5981
|
+
* ## Outputs
|
|
5982
|
+
* | Output | Payload | Fires on |
|
|
5983
|
+
* |--------------|------------------------|----------|
|
|
5984
|
+
* | `eventClick` | `FoldCalendarEvent<T>` | A chip activated. |
|
|
5985
|
+
*
|
|
5986
|
+
* ## Slots
|
|
5987
|
+
* | Selector | Region |
|
|
5988
|
+
* |--------------------------------|--------|
|
|
5989
|
+
* | `button[empty]`, `a[empty]`, `div[empty]` | Under the empty message — the place for a "new request" action. |
|
|
5990
|
+
* | *(default)* | After the list. |
|
|
5991
|
+
*
|
|
5992
|
+
* The `empty` slot is **tag-qualified** (rule 4.8): `empty` is also an input on
|
|
5993
|
+
* `fold-field` and `fold-data-table`, and a bare `[empty]` selector would
|
|
5994
|
+
* capture either of them whole.
|
|
5995
|
+
*
|
|
5996
|
+
* ## Theming
|
|
5997
|
+
* | CSS variable | Default | Sets |
|
|
5998
|
+
* |------------------------------------|---------|------|
|
|
5999
|
+
* | `--fold-calendar-day-number-size` | `--fold-text-3xl` | The big day number. |
|
|
6000
|
+
* | `--fold-calendar-bar-width` | `3px` | The tone bar down a chip's leading edge. |
|
|
6001
|
+
* | `--fold-calendar-band-radius` | `--fold-radius-sm` | Chip corners. |
|
|
6002
|
+
*
|
|
6003
|
+
* @selector `fold-calendar-day`
|
|
6004
|
+
*
|
|
6005
|
+
* @example
|
|
6006
|
+
* ```html
|
|
6007
|
+
* <fold-calendar-day [date]="date()" [events]="events()" [today]="today">
|
|
6008
|
+
* <button empty foldButton (click)="request()">New request</button>
|
|
6009
|
+
* </fold-calendar-day>
|
|
6010
|
+
* ```
|
|
6011
|
+
*/
|
|
6012
|
+
class FoldCalendarDayComponent {
|
|
6013
|
+
/** The day on display. */
|
|
6014
|
+
date = input.required(/* @ts-ignore */
|
|
6015
|
+
...(ngDevMode ? [{ debugName: "date" }] : /* istanbul ignore next */ []));
|
|
6016
|
+
/** Events to filter down to this day. */
|
|
6017
|
+
events = input([], /* @ts-ignore */
|
|
6018
|
+
...(ngDevMode ? [{ debugName: "events" }] : /* istanbul ignore next */ []));
|
|
6019
|
+
/** The day to mark as today. */
|
|
6020
|
+
today = input(/* @ts-ignore */
|
|
6021
|
+
...(ngDevMode ? [undefined, { debugName: "today" }] : /* istanbul ignore next */ []));
|
|
6022
|
+
/** A chip was activated. */
|
|
6023
|
+
eventClick = output();
|
|
6024
|
+
/** Labels, locale and the projected chip template — see the directive. */
|
|
6025
|
+
chrome = inject(FoldCalendarChromeDirective);
|
|
6026
|
+
dayEvents = computed(() => foldEventsOnDay(this.events(), this.date()), /* @ts-ignore */
|
|
6027
|
+
...(ngDevMode ? [{ debugName: "dayEvents" }] : /* istanbul ignore next */ []));
|
|
6028
|
+
isToday = computed(() => this.date() === this.today(), /* @ts-ignore */
|
|
6029
|
+
...(ngDevMode ? [{ debugName: "isToday" }] : /* istanbul ignore next */ []));
|
|
6030
|
+
/** `Saturday` — the weekday alone; the month sits beside the number. */
|
|
6031
|
+
weekdayName = computed(() => this.chrome.format(this.date(), "weekdayLong"), /* @ts-ignore */
|
|
6032
|
+
...(ngDevMode ? [{ debugName: "weekdayName" }] : /* istanbul ignore next */ []));
|
|
6033
|
+
/** `May` — read with the day number, which is why it is not in the line above. */
|
|
6034
|
+
monthName = computed(() => this.chrome.format(this.date(), "monthLong"), /* @ts-ignore */
|
|
6035
|
+
...(ngDevMode ? [{ debugName: "monthName" }] : /* istanbul ignore next */ []));
|
|
6036
|
+
dayOfMonth = computed(() => Number(this.date().slice(8, 10)), /* @ts-ignore */
|
|
6037
|
+
...(ngDevMode ? [{ debugName: "dayOfMonth" }] : /* istanbul ignore next */ []));
|
|
6038
|
+
/** The header's accessible name: the full date, plus the today marker. */
|
|
6039
|
+
headerLabel = computed(() => {
|
|
6040
|
+
const full = this.chrome.format(this.date(), "dateFull");
|
|
6041
|
+
return this.isToday() ? `${full}, ${this.chrome.l().today}` : full;
|
|
6042
|
+
}, /* @ts-ignore */
|
|
6043
|
+
...(ngDevMode ? [{ debugName: "headerLabel" }] : /* istanbul ignore next */ []));
|
|
6044
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarDayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6045
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: FoldCalendarDayComponent, isStandalone: true, selector: "fold-calendar-day", inputs: { date: { classPropertyName: "date", publicName: "date", isSignal: true, isRequired: true, transformFunction: null }, events: { classPropertyName: "events", publicName: "events", isSignal: true, isRequired: false, transformFunction: null }, today: { classPropertyName: "today", publicName: "today", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { eventClick: "eventClick" }, hostDirectives: [{ directive: FoldCalendarChromeDirective, inputs: ["locale", "locale", "labels", "labels", "formats", "formats"] }], ngImport: i0, template: "<section class=\"foldcald\" [attr.aria-label]=\"headerLabel()\">\n <header class=\"foldcald-head\">\n <span class=\"foldcald-weekday\">{{ weekdayName() }}</span>\n <span\n class=\"foldcald-daynum\"\n [class.is-today]=\"isToday()\"\n [attr.aria-current]=\"isToday() ? 'date' : null\"\n >{{ dayOfMonth() }}</span\n >\n <span class=\"foldcald-month\">{{ monthName() }}</span>\n </header>\n\n @if (dayEvents().length === 0) {\n <div class=\"foldcald-empty\">\n <p>{{ chrome.l().emptyDay }}</p>\n <!--\n Tag-qualified (rule 4.8): `empty` is also an input on fold-field and\n fold-data-table, and a bare [empty] selector would swallow either whole.\n -->\n <ng-content select=\"button[empty], a[empty], div[empty]\" />\n </div>\n } @else {\n <ul class=\"foldcald-list\">\n @for (event of dayEvents(); track event.id) {\n <li>\n <button\n type=\"button\"\n class=\"foldcal-chip foldcald-chip\"\n [attr.data-tone]=\"event.tone ?? 'neutral'\"\n (click)=\"eventClick.emit(event)\"\n >\n @if (chrome.eventContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{ $implicit: event, band: null }\"\n />\n } @else {\n <span class=\"foldcal-chip-bar\"></span>\n @if (event.icon; as icon) {\n <fold-icon class=\"foldcal-chip-icon\" [name]=\"icon\" size=\"md\" />\n }\n <span class=\"foldcal-chip-body\">\n <span class=\"foldcal-chip-label\">{{ event.label }}</span>\n @if (event.subline) {\n <span class=\"foldcal-chip-sub\">{{ event.subline }}</span>\n }\n </span>\n }\n </button>\n </li>\n }\n </ul>\n }\n\n <!-- Default slot: anything the qualified selector above did not claim still\n lands somewhere visible instead of disappearing. -->\n <ng-content />\n</section>\n", styles: ["@charset \"UTF-8\";:host{display:block}.foldcal-chip{position:relative;display:flex;align-items:stretch;gap:var(--fold-space-sm);width:100%;padding:var(--fold-space-xs) var(--fold-space-sm);padding-inline-start:0;border:0;border-radius:var(--fold-calendar-band-radius, var(--fold-radius-sm));background:var(--fold-color-surface-raised);color:var(--fold-color-text);font:inherit;text-align:start;cursor:pointer;overflow:hidden;transition:background-color var(--fold-motion-fast) ease}.foldcal-chip:hover{background:var(--fold-color-surface-hover)}.foldcal-chip:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcal-chip-bar{flex:0 0 var(--fold-calendar-bar-width, 3px);align-self:stretch;background:var(--fold-color-text-muted)}.foldcal-chip-icon{flex:none;align-self:center;margin-inline-start:var(--fold-space-sm);color:var(--fold-color-text-secondary)}.foldcal-chip-body{display:flex;flex-direction:column;gap:1px;min-width:0}.foldcal-chip-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--fold-text-sm);font-weight:500}.foldcal-chip-sub{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs)}.foldcal-chip[data-tone=success] .foldcal-chip-bar{background:var(--fold-color-success)}.foldcal-chip[data-tone=warning] .foldcal-chip-bar{background:var(--fold-color-warning)}.foldcal-chip[data-tone=alert] .foldcal-chip-bar{background:var(--fold-color-alert)}.foldcal-chip[data-tone=muted] .foldcal-chip-bar{background:var(--fold-color-text-faded)}.foldcal-chip[data-tone=muted] .foldcal-chip-label{color:var(--fold-color-text-secondary);text-decoration:line-through;text-decoration-thickness:1px}@media(forced-colors:active){.foldcal-chip{border:1px solid CanvasText}.foldcal-chip-bar{forced-color-adjust:none}}@media(prefers-reduced-motion:reduce){.foldcal-chip{transition:none}}.foldcald{display:flex;flex-direction:column;gap:var(--fold-space-lg);padding:var(--fold-space-lg);border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-md);background:var(--fold-color-surface-card)}.foldcald-head{display:flex;align-items:baseline;gap:var(--fold-space-md);padding-bottom:var(--fold-space-md);border-bottom:1px solid var(--fold-color-border)}.foldcald-weekday{color:var(--fold-color-text-secondary);font-size:var(--fold-text-md);font-weight:600;letter-spacing:.04em;text-transform:uppercase}.foldcald-daynum{color:var(--fold-color-text);font-size:var(--fold-calendar-day-number-size, 32px);font-weight:700;font-variant-numeric:tabular-nums;line-height:1}.foldcald-daynum.is-today{color:var(--fold-color-primary-text)}.foldcald-month{color:var(--fold-color-text-secondary);font-size:var(--fold-text-lg);font-weight:600}.foldcald-list{display:flex;flex-direction:column;gap:var(--fold-space-sm);margin:0;padding:0;list-style:none}.foldcald-chip{padding-block:var(--fold-space-sm)}.foldcald-empty{display:flex;flex-direction:column;align-items:flex-start;gap:var(--fold-space-sm);padding:var(--fold-space-lg);border:1px dashed var(--fold-color-border);border-radius:var(--fold-radius-md);background:var(--fold-color-surface-subtle)}.foldcald-empty p{margin:0;color:var(--fold-color-text-secondary);font-size:var(--fold-text-sm)}@media(forced-colors:active){.foldcald-daynum.is-today{outline:2px solid Highlight;outline-offset:2px}.foldcald-head{border-block-end-color:CanvasText}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: FoldIconComponent, selector: "fold-icon", inputs: ["name", "size", "tone", "title"] }] });
|
|
6046
|
+
}
|
|
6047
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarDayComponent, decorators: [{
|
|
6048
|
+
type: Component,
|
|
6049
|
+
args: [{ selector: "fold-calendar-day", standalone: true, imports: [NgTemplateOutlet, FoldIconComponent], hostDirectives: [
|
|
6050
|
+
{
|
|
6051
|
+
directive: FoldCalendarChromeDirective,
|
|
6052
|
+
inputs: ["locale", "labels", "formats"],
|
|
6053
|
+
},
|
|
6054
|
+
], template: "<section class=\"foldcald\" [attr.aria-label]=\"headerLabel()\">\n <header class=\"foldcald-head\">\n <span class=\"foldcald-weekday\">{{ weekdayName() }}</span>\n <span\n class=\"foldcald-daynum\"\n [class.is-today]=\"isToday()\"\n [attr.aria-current]=\"isToday() ? 'date' : null\"\n >{{ dayOfMonth() }}</span\n >\n <span class=\"foldcald-month\">{{ monthName() }}</span>\n </header>\n\n @if (dayEvents().length === 0) {\n <div class=\"foldcald-empty\">\n <p>{{ chrome.l().emptyDay }}</p>\n <!--\n Tag-qualified (rule 4.8): `empty` is also an input on fold-field and\n fold-data-table, and a bare [empty] selector would swallow either whole.\n -->\n <ng-content select=\"button[empty], a[empty], div[empty]\" />\n </div>\n } @else {\n <ul class=\"foldcald-list\">\n @for (event of dayEvents(); track event.id) {\n <li>\n <button\n type=\"button\"\n class=\"foldcal-chip foldcald-chip\"\n [attr.data-tone]=\"event.tone ?? 'neutral'\"\n (click)=\"eventClick.emit(event)\"\n >\n @if (chrome.eventContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{ $implicit: event, band: null }\"\n />\n } @else {\n <span class=\"foldcal-chip-bar\"></span>\n @if (event.icon; as icon) {\n <fold-icon class=\"foldcal-chip-icon\" [name]=\"icon\" size=\"md\" />\n }\n <span class=\"foldcal-chip-body\">\n <span class=\"foldcal-chip-label\">{{ event.label }}</span>\n @if (event.subline) {\n <span class=\"foldcal-chip-sub\">{{ event.subline }}</span>\n }\n </span>\n }\n </button>\n </li>\n }\n </ul>\n }\n\n <!-- Default slot: anything the qualified selector above did not claim still\n lands somewhere visible instead of disappearing. -->\n <ng-content />\n</section>\n", styles: ["@charset \"UTF-8\";:host{display:block}.foldcal-chip{position:relative;display:flex;align-items:stretch;gap:var(--fold-space-sm);width:100%;padding:var(--fold-space-xs) var(--fold-space-sm);padding-inline-start:0;border:0;border-radius:var(--fold-calendar-band-radius, var(--fold-radius-sm));background:var(--fold-color-surface-raised);color:var(--fold-color-text);font:inherit;text-align:start;cursor:pointer;overflow:hidden;transition:background-color var(--fold-motion-fast) ease}.foldcal-chip:hover{background:var(--fold-color-surface-hover)}.foldcal-chip:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcal-chip-bar{flex:0 0 var(--fold-calendar-bar-width, 3px);align-self:stretch;background:var(--fold-color-text-muted)}.foldcal-chip-icon{flex:none;align-self:center;margin-inline-start:var(--fold-space-sm);color:var(--fold-color-text-secondary)}.foldcal-chip-body{display:flex;flex-direction:column;gap:1px;min-width:0}.foldcal-chip-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--fold-text-sm);font-weight:500}.foldcal-chip-sub{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs)}.foldcal-chip[data-tone=success] .foldcal-chip-bar{background:var(--fold-color-success)}.foldcal-chip[data-tone=warning] .foldcal-chip-bar{background:var(--fold-color-warning)}.foldcal-chip[data-tone=alert] .foldcal-chip-bar{background:var(--fold-color-alert)}.foldcal-chip[data-tone=muted] .foldcal-chip-bar{background:var(--fold-color-text-faded)}.foldcal-chip[data-tone=muted] .foldcal-chip-label{color:var(--fold-color-text-secondary);text-decoration:line-through;text-decoration-thickness:1px}@media(forced-colors:active){.foldcal-chip{border:1px solid CanvasText}.foldcal-chip-bar{forced-color-adjust:none}}@media(prefers-reduced-motion:reduce){.foldcal-chip{transition:none}}.foldcald{display:flex;flex-direction:column;gap:var(--fold-space-lg);padding:var(--fold-space-lg);border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-md);background:var(--fold-color-surface-card)}.foldcald-head{display:flex;align-items:baseline;gap:var(--fold-space-md);padding-bottom:var(--fold-space-md);border-bottom:1px solid var(--fold-color-border)}.foldcald-weekday{color:var(--fold-color-text-secondary);font-size:var(--fold-text-md);font-weight:600;letter-spacing:.04em;text-transform:uppercase}.foldcald-daynum{color:var(--fold-color-text);font-size:var(--fold-calendar-day-number-size, 32px);font-weight:700;font-variant-numeric:tabular-nums;line-height:1}.foldcald-daynum.is-today{color:var(--fold-color-primary-text)}.foldcald-month{color:var(--fold-color-text-secondary);font-size:var(--fold-text-lg);font-weight:600}.foldcald-list{display:flex;flex-direction:column;gap:var(--fold-space-sm);margin:0;padding:0;list-style:none}.foldcald-chip{padding-block:var(--fold-space-sm)}.foldcald-empty{display:flex;flex-direction:column;align-items:flex-start;gap:var(--fold-space-sm);padding:var(--fold-space-lg);border:1px dashed var(--fold-color-border);border-radius:var(--fold-radius-md);background:var(--fold-color-surface-subtle)}.foldcald-empty p{margin:0;color:var(--fold-color-text-secondary);font-size:var(--fold-text-sm)}@media(forced-colors:active){.foldcald-daynum.is-today{outline:2px solid Highlight;outline-offset:2px}.foldcald-head{border-block-end-color:CanvasText}}\n"] }]
|
|
6055
|
+
}], propDecorators: { date: [{ type: i0.Input, args: [{ isSignal: true, alias: "date", required: true }] }], events: [{ type: i0.Input, args: [{ isSignal: true, alias: "events", required: false }] }], today: [{ type: i0.Input, args: [{ isSignal: true, alias: "today", required: false }] }], eventClick: [{ type: i0.Output, args: ["eventClick"] }] } });
|
|
6056
|
+
|
|
6057
|
+
/**
|
|
6058
|
+
* `<fold-calendar-list>` — everything in a range, in date order.
|
|
6059
|
+
*
|
|
6060
|
+
* The flat reading of the same data: no grid, no packing, so nothing is ever
|
|
6061
|
+
* hidden behind a lane budget. Each row leads with the span it covers, which
|
|
6062
|
+
* is what the grid views can only imply.
|
|
6063
|
+
*
|
|
6064
|
+
* For a sortable, column-configurable table instead, feed
|
|
6065
|
+
* `foldEventsInRange()` straight into {@link FoldDataTableComponent} — this
|
|
6066
|
+
* view stays deliberately small.
|
|
6067
|
+
*
|
|
6068
|
+
* ## Inputs
|
|
6069
|
+
* | Input | Type | Default | Meaning |
|
|
6070
|
+
* |----------|--------------------------|---------|---------|
|
|
6071
|
+
* | `events` | `FoldCalendarEvent<T>[]` | `[]` | Source events. |
|
|
6072
|
+
* | `from` | `FoldCalendarDate` | — | Range start; omit to list everything. |
|
|
6073
|
+
* | `to` | `FoldCalendarDate` | — | Range end; omit to list everything. |
|
|
6074
|
+
* | `locale` | `string` | runtime | Drives the span labels through `Intl`. |
|
|
6075
|
+
* | `labels` | `Partial<FoldCalendarLabels>` | — | Per-instance label overrides. |
|
|
6076
|
+
*
|
|
6077
|
+
* ## Outputs
|
|
6078
|
+
* | Output | Payload | Fires on |
|
|
6079
|
+
* |--------------|------------------------|----------|
|
|
6080
|
+
* | `eventClick` | `FoldCalendarEvent<T>` | A row activated. |
|
|
6081
|
+
*
|
|
6082
|
+
* ## Theming
|
|
6083
|
+
* | CSS variable | Default | Sets |
|
|
6084
|
+
* |------------------------------------|---------|------|
|
|
6085
|
+
* | `--fold-calendar-list-date-width` | `7rem` | The leading date column — widen it for verbose locales. |
|
|
6086
|
+
* | `--fold-calendar-bar-width` | `3px` | The tone bar down a row's leading edge. |
|
|
6087
|
+
*
|
|
6088
|
+
* A projected `foldCalendarEvent` template replaces a row's **whole** inside,
|
|
6089
|
+
* as in every other view: the built-in bar and date belong to the default
|
|
6090
|
+
* rendering, not to the frame around it.
|
|
6091
|
+
*
|
|
6092
|
+
* @selector `fold-calendar-list`
|
|
6093
|
+
*
|
|
6094
|
+
* @example
|
|
6095
|
+
* ```html
|
|
6096
|
+
* <fold-calendar-list
|
|
6097
|
+
* [events]="events()"
|
|
6098
|
+
* [from]="range().from"
|
|
6099
|
+
* [to]="range().to"
|
|
6100
|
+
* (eventClick)="open($event)"
|
|
6101
|
+
* />
|
|
6102
|
+
* ```
|
|
6103
|
+
*/
|
|
6104
|
+
class FoldCalendarListComponent {
|
|
6105
|
+
/** Source events. */
|
|
6106
|
+
events = input([], /* @ts-ignore */
|
|
6107
|
+
...(ngDevMode ? [{ debugName: "events" }] : /* istanbul ignore next */ []));
|
|
6108
|
+
/** Range start, inclusive. Omit (with `to`) to list every event given. */
|
|
6109
|
+
from = input(/* @ts-ignore */
|
|
6110
|
+
...(ngDevMode ? [undefined, { debugName: "from" }] : /* istanbul ignore next */ []));
|
|
6111
|
+
/** Range end, inclusive. */
|
|
6112
|
+
to = input(/* @ts-ignore */
|
|
6113
|
+
...(ngDevMode ? [undefined, { debugName: "to" }] : /* istanbul ignore next */ []));
|
|
6114
|
+
/** A row was activated. */
|
|
6115
|
+
eventClick = output();
|
|
6116
|
+
/** Labels, locale and the projected chip template — see the directive. */
|
|
6117
|
+
chrome = inject(FoldCalendarChromeDirective);
|
|
6118
|
+
/** In range (when one is given), earliest first. */
|
|
6119
|
+
rows = computed(() => {
|
|
6120
|
+
const from = this.from();
|
|
6121
|
+
const to = this.to();
|
|
6122
|
+
const scoped = from === undefined || to === undefined
|
|
6123
|
+
? this.events()
|
|
6124
|
+
: foldEventsInRange(this.events(), from, to);
|
|
6125
|
+
return [...scoped].sort((a, b) => a.start < b.start ? -1 : a.start > b.start ? 1 : 0);
|
|
6126
|
+
}, /* @ts-ignore */
|
|
6127
|
+
...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
|
|
6128
|
+
/**
|
|
6129
|
+
* The span, collapsed to one date when it covers a single day — `Intl`'s
|
|
6130
|
+
* own range formatter does that, and picks the locale's separator and its
|
|
6131
|
+
* order, which is why this is not a translatable label.
|
|
6132
|
+
*/
|
|
6133
|
+
rangeLabel(event) {
|
|
6134
|
+
return this.chrome.formatRange(event.start, event.end, "dayMonthShort");
|
|
6135
|
+
}
|
|
6136
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6137
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: FoldCalendarListComponent, isStandalone: true, selector: "fold-calendar-list", inputs: { events: { classPropertyName: "events", publicName: "events", isSignal: true, isRequired: false, transformFunction: null }, from: { classPropertyName: "from", publicName: "from", isSignal: true, isRequired: false, transformFunction: null }, to: { classPropertyName: "to", publicName: "to", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { eventClick: "eventClick" }, hostDirectives: [{ directive: FoldCalendarChromeDirective, inputs: ["locale", "locale", "labels", "labels", "formats", "formats"] }], ngImport: i0, template: "@if (rows().length === 0) {\n <p class=\"foldcall-empty\">{{ chrome.l().emptyRange }}</p>\n} @else {\n <ul class=\"foldcall\">\n @for (event of rows(); track event.id) {\n <li>\n <!--\n A projected template replaces the row's **whole** inside, exactly as\n it does in the other four views. Rendering the bar and the date\n outside the @if would make the same template mean something different\n here, and drop a one-element template into a four-column grid.\n -->\n <button\n type=\"button\"\n class=\"foldcall-row\"\n [class.is-custom]=\"chrome.eventContent() !== null\"\n [attr.data-tone]=\"event.tone ?? 'neutral'\"\n (click)=\"eventClick.emit(event)\"\n >\n @if (chrome.eventContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{ $implicit: event, band: null }\"\n />\n } @else {\n <span class=\"foldcall-bar\"></span>\n <span class=\"foldcall-when\">{{ rangeLabel(event) }}</span>\n <span class=\"foldcall-body\">\n @if (event.icon; as icon) {\n <fold-icon class=\"foldcall-icon\" [name]=\"icon\" size=\"sm\" />\n }\n <span class=\"foldcall-label\">{{ event.label }}</span>\n </span>\n <span class=\"foldcall-sub\">{{ event.subline ?? \"\" }}</span>\n }\n </button>\n </li>\n }\n </ul>\n}\n", styles: [":host{display:block;container-type:inline-size}.foldcall{display:flex;flex-direction:column;margin:0;padding:0;list-style:none;border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-md);background:var(--fold-color-surface-card);overflow:clip}.foldcall li+li .foldcall-row{border-top:1px solid var(--fold-color-border-subtle)}.foldcall-row{display:grid;grid-template-columns:var(--fold-calendar-bar-width, 3px) var(--fold-calendar-list-date-width, 7rem) minmax(0,1fr) auto;align-items:center;gap:var(--fold-space-md);width:100%;padding:var(--fold-space-sm) var(--fold-space-md);padding-inline-start:0;border:0;background:transparent;color:inherit;font:inherit;text-align:start;cursor:pointer;transition:background-color var(--fold-motion-fast) ease}.foldcall-row.is-custom{display:flex;align-items:center;gap:var(--fold-space-sm);padding-inline-start:var(--fold-space-md)}.foldcall-row:hover{background:var(--fold-color-surface-hover)}.foldcall-row:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcall-bar{align-self:stretch;background:var(--fold-color-text-muted)}.foldcall-when{color:var(--fold-color-text-secondary);font-size:var(--fold-text-sm);font-variant-numeric:tabular-nums;white-space:nowrap}.foldcall-body{display:flex;align-items:center;gap:var(--fold-space-sm);min-width:0}.foldcall-icon{flex:none;color:var(--fold-color-text-secondary)}.foldcall-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--fold-text-sm);font-weight:500}.foldcall-sub{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);white-space:nowrap}.foldcall-empty{margin:0;padding:var(--fold-space-xl);border:1px dashed var(--fold-color-border);border-radius:var(--fold-radius-md);background:var(--fold-color-surface-subtle);color:var(--fold-color-text-secondary);font-size:var(--fold-text-sm);text-align:center}.foldcall-row[data-tone=success] .foldcall-bar{background:var(--fold-color-success)}.foldcall-row[data-tone=warning] .foldcall-bar{background:var(--fold-color-warning)}.foldcall-row[data-tone=alert] .foldcall-bar{background:var(--fold-color-alert)}.foldcall-row[data-tone=muted] .foldcall-bar{background:var(--fold-color-text-faded)}.foldcall-row[data-tone=muted] .foldcall-label{color:var(--fold-color-text-secondary);text-decoration:line-through;text-decoration-thickness:1px}@media(forced-colors:active){.foldcall-row{border:1px solid CanvasText}.foldcall-bar{forced-color-adjust:none}}@container (max-width: 640px){.foldcall-row:not(.is-custom){grid-template-columns:var(--fold-calendar-bar-width, 3px) var(--fold-calendar-list-date-width, 7rem) minmax(0,1fr)}.foldcall-sub{grid-column:2/-1}}@media(forced-colors:active){.foldcall-row:focus-visible{outline-color:Highlight}}@media(prefers-reduced-motion:reduce){.foldcall-row{transition:none}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: FoldIconComponent, selector: "fold-icon", inputs: ["name", "size", "tone", "title"] }] });
|
|
6138
|
+
}
|
|
6139
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarListComponent, decorators: [{
|
|
6140
|
+
type: Component,
|
|
6141
|
+
args: [{ selector: "fold-calendar-list", standalone: true, imports: [NgTemplateOutlet, FoldIconComponent], hostDirectives: [
|
|
6142
|
+
{
|
|
6143
|
+
directive: FoldCalendarChromeDirective,
|
|
6144
|
+
inputs: ["locale", "labels", "formats"],
|
|
6145
|
+
},
|
|
6146
|
+
], template: "@if (rows().length === 0) {\n <p class=\"foldcall-empty\">{{ chrome.l().emptyRange }}</p>\n} @else {\n <ul class=\"foldcall\">\n @for (event of rows(); track event.id) {\n <li>\n <!--\n A projected template replaces the row's **whole** inside, exactly as\n it does in the other four views. Rendering the bar and the date\n outside the @if would make the same template mean something different\n here, and drop a one-element template into a four-column grid.\n -->\n <button\n type=\"button\"\n class=\"foldcall-row\"\n [class.is-custom]=\"chrome.eventContent() !== null\"\n [attr.data-tone]=\"event.tone ?? 'neutral'\"\n (click)=\"eventClick.emit(event)\"\n >\n @if (chrome.eventContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{ $implicit: event, band: null }\"\n />\n } @else {\n <span class=\"foldcall-bar\"></span>\n <span class=\"foldcall-when\">{{ rangeLabel(event) }}</span>\n <span class=\"foldcall-body\">\n @if (event.icon; as icon) {\n <fold-icon class=\"foldcall-icon\" [name]=\"icon\" size=\"sm\" />\n }\n <span class=\"foldcall-label\">{{ event.label }}</span>\n </span>\n <span class=\"foldcall-sub\">{{ event.subline ?? \"\" }}</span>\n }\n </button>\n </li>\n }\n </ul>\n}\n", styles: [":host{display:block;container-type:inline-size}.foldcall{display:flex;flex-direction:column;margin:0;padding:0;list-style:none;border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-md);background:var(--fold-color-surface-card);overflow:clip}.foldcall li+li .foldcall-row{border-top:1px solid var(--fold-color-border-subtle)}.foldcall-row{display:grid;grid-template-columns:var(--fold-calendar-bar-width, 3px) var(--fold-calendar-list-date-width, 7rem) minmax(0,1fr) auto;align-items:center;gap:var(--fold-space-md);width:100%;padding:var(--fold-space-sm) var(--fold-space-md);padding-inline-start:0;border:0;background:transparent;color:inherit;font:inherit;text-align:start;cursor:pointer;transition:background-color var(--fold-motion-fast) ease}.foldcall-row.is-custom{display:flex;align-items:center;gap:var(--fold-space-sm);padding-inline-start:var(--fold-space-md)}.foldcall-row:hover{background:var(--fold-color-surface-hover)}.foldcall-row:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcall-bar{align-self:stretch;background:var(--fold-color-text-muted)}.foldcall-when{color:var(--fold-color-text-secondary);font-size:var(--fold-text-sm);font-variant-numeric:tabular-nums;white-space:nowrap}.foldcall-body{display:flex;align-items:center;gap:var(--fold-space-sm);min-width:0}.foldcall-icon{flex:none;color:var(--fold-color-text-secondary)}.foldcall-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--fold-text-sm);font-weight:500}.foldcall-sub{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);white-space:nowrap}.foldcall-empty{margin:0;padding:var(--fold-space-xl);border:1px dashed var(--fold-color-border);border-radius:var(--fold-radius-md);background:var(--fold-color-surface-subtle);color:var(--fold-color-text-secondary);font-size:var(--fold-text-sm);text-align:center}.foldcall-row[data-tone=success] .foldcall-bar{background:var(--fold-color-success)}.foldcall-row[data-tone=warning] .foldcall-bar{background:var(--fold-color-warning)}.foldcall-row[data-tone=alert] .foldcall-bar{background:var(--fold-color-alert)}.foldcall-row[data-tone=muted] .foldcall-bar{background:var(--fold-color-text-faded)}.foldcall-row[data-tone=muted] .foldcall-label{color:var(--fold-color-text-secondary);text-decoration:line-through;text-decoration-thickness:1px}@media(forced-colors:active){.foldcall-row{border:1px solid CanvasText}.foldcall-bar{forced-color-adjust:none}}@container (max-width: 640px){.foldcall-row:not(.is-custom){grid-template-columns:var(--fold-calendar-bar-width, 3px) var(--fold-calendar-list-date-width, 7rem) minmax(0,1fr)}.foldcall-sub{grid-column:2/-1}}@media(forced-colors:active){.foldcall-row:focus-visible{outline-color:Highlight}}@media(prefers-reduced-motion:reduce){.foldcall-row{transition:none}}\n"] }]
|
|
6147
|
+
}], propDecorators: { events: [{ type: i0.Input, args: [{ isSignal: true, alias: "events", required: false }] }], from: [{ type: i0.Input, args: [{ isSignal: true, alias: "from", required: false }] }], to: [{ type: i0.Input, args: [{ isSignal: true, alias: "to", required: false }] }], eventClick: [{ type: i0.Output, args: ["eventClick"] }] } });
|
|
6148
|
+
|
|
6149
|
+
/**
|
|
6150
|
+
* Where overlapping timed events sit side by side — the time grid's only
|
|
6151
|
+
* genuinely algorithmic part, and the same shape as the month's lane packing
|
|
6152
|
+
* turned ninety degrees: intervals over **minutes** instead of days, laid out
|
|
6153
|
+
* across the column's width instead of down its height.
|
|
6154
|
+
*
|
|
6155
|
+
* Pure, so the rule can be exercised exhaustively without a grid.
|
|
6156
|
+
*/
|
|
6157
|
+
/**
|
|
6158
|
+
* The narrowest an event is allowed to get. Past this, three overlapping
|
|
6159
|
+
* meetings are three unreadable slivers, and a reader is better served by a
|
|
6160
|
+
* `+N` than by a column two pixels wide — which is the caller's call to make,
|
|
6161
|
+
* so this only reports the number.
|
|
6162
|
+
*/
|
|
6163
|
+
const MIN_VISIBLE_COLUMNS = 1;
|
|
6164
|
+
/** Whether two intervals actually share a minute. */
|
|
6165
|
+
function overlaps(a, b) {
|
|
6166
|
+
return a.startMinute < b.endMinute && b.startMinute < a.endMinute;
|
|
6167
|
+
}
|
|
6168
|
+
/**
|
|
6169
|
+
* Lays intervals out across the width, one slot per input, in input order.
|
|
6170
|
+
*
|
|
6171
|
+
* Two events that merely touch — one ends at 10:00, the next starts at 10:00 —
|
|
6172
|
+
* do **not** overlap and each keep the full width. That is the difference
|
|
6173
|
+
* between the month grid (inclusive days: touching means colliding) and a time
|
|
6174
|
+
* grid (exclusive minutes: touching means back-to-back), and getting it wrong
|
|
6175
|
+
* halves the width of every event in a packed morning.
|
|
6176
|
+
*
|
|
6177
|
+
* Events are grouped into **clusters** — runs connected by overlap — and each
|
|
6178
|
+
* cluster is widened only as far as it needs, so one triple-booked hour does
|
|
6179
|
+
* not narrow the rest of the day.
|
|
6180
|
+
*/
|
|
6181
|
+
function foldLayOutOverlaps(intervals) {
|
|
6182
|
+
const order = intervals
|
|
6183
|
+
.map((interval, index) => ({ interval, index }))
|
|
6184
|
+
.sort((a, b) => a.interval.startMinute !== b.interval.startMinute
|
|
6185
|
+
? a.interval.startMinute - b.interval.startMinute
|
|
6186
|
+
: b.interval.endMinute -
|
|
6187
|
+
b.interval.startMinute -
|
|
6188
|
+
(a.interval.endMinute - a.interval.startMinute));
|
|
6189
|
+
const slots = intervals.map(() => ({
|
|
6190
|
+
column: 0,
|
|
6191
|
+
columns: MIN_VISIBLE_COLUMNS,
|
|
6192
|
+
}));
|
|
6193
|
+
/** The cluster being filled: its members' indices and its lane end times. */
|
|
6194
|
+
let cluster = [];
|
|
6195
|
+
let laneEnds = [];
|
|
6196
|
+
const closeCluster = () => {
|
|
6197
|
+
for (const index of cluster) {
|
|
6198
|
+
slots[index] = {
|
|
6199
|
+
column: slots[index]?.column ?? 0,
|
|
6200
|
+
columns: laneEnds.length,
|
|
6201
|
+
};
|
|
6202
|
+
}
|
|
6203
|
+
cluster = [];
|
|
6204
|
+
laneEnds = [];
|
|
6205
|
+
};
|
|
6206
|
+
for (const { interval, index } of order) {
|
|
6207
|
+
// A cluster ends where nothing it holds is still running.
|
|
6208
|
+
const stillRunning = laneEnds.some((end) => end > interval.startMinute);
|
|
6209
|
+
if (!stillRunning && cluster.length > 0) {
|
|
6210
|
+
closeCluster();
|
|
6211
|
+
}
|
|
6212
|
+
const reusable = laneEnds.findIndex((end) => end <= interval.startMinute);
|
|
6213
|
+
const column = reusable === -1 ? laneEnds.length : reusable;
|
|
6214
|
+
laneEnds[column] = interval.endMinute;
|
|
6215
|
+
slots[index] = { column, columns: laneEnds.length };
|
|
6216
|
+
cluster.push(index);
|
|
6217
|
+
}
|
|
6218
|
+
if (cluster.length > 0) {
|
|
6219
|
+
closeCluster();
|
|
6220
|
+
}
|
|
6221
|
+
return slots;
|
|
6222
|
+
}
|
|
6223
|
+
/** Whether `a` and `b` would need separate columns — exported for tests. */
|
|
6224
|
+
function foldOverlaps(a, b) {
|
|
6225
|
+
return overlaps(a, b);
|
|
6226
|
+
}
|
|
6227
|
+
|
|
6228
|
+
const DEFAULT_DAY_COUNT = 7;
|
|
6229
|
+
const DEFAULT_ALL_DAY_LANES = 2;
|
|
6230
|
+
const MINUTES_PER_HOUR = 60;
|
|
6231
|
+
/**
|
|
6232
|
+
* Whether an event is placed on the clock or on the strip.
|
|
6233
|
+
*
|
|
6234
|
+
* **Both** ends are needed: an event with only a start has no height, and one
|
|
6235
|
+
* with only an end has no origin. Half a time is a data bug, and treating it as
|
|
6236
|
+
* all-day is the reading that loses the least.
|
|
6237
|
+
*/
|
|
6238
|
+
function foldIsTimed(event) {
|
|
6239
|
+
return event.startTime !== undefined && event.endTime !== undefined;
|
|
6240
|
+
}
|
|
6241
|
+
/** The window a timed event occupies on one particular day, or `null`. */
|
|
6242
|
+
function minutesOnDay(event, day, startMinute, endMinute) {
|
|
6243
|
+
const firstDay = foldToEpochDay(event.start);
|
|
6244
|
+
const lastDay = foldToEpochDay(event.end);
|
|
6245
|
+
if (day < firstDay || day > lastDay) {
|
|
6246
|
+
return null;
|
|
6247
|
+
}
|
|
6248
|
+
// A span crossing midnight runs to the end of each day it passes through:
|
|
6249
|
+
// Monday 14:00 → Wednesday 10:00 is three blocks, not one impossible one.
|
|
6250
|
+
const rawFrom = day === firstDay ? foldToMinutes(event.startTime ?? "00:00") : 0;
|
|
6251
|
+
const rawTo = day === lastDay
|
|
6252
|
+
? foldToMinutes(event.endTime ?? "24:00")
|
|
6253
|
+
: FOLD_MINUTES_PER_DAY;
|
|
6254
|
+
const from = Math.max(startMinute, Math.min(rawFrom, rawTo));
|
|
6255
|
+
const to = Math.min(endMinute, Math.max(rawFrom, rawTo));
|
|
6256
|
+
if (to <= from) {
|
|
6257
|
+
return null; // entirely outside the visible window
|
|
6258
|
+
}
|
|
6259
|
+
return {
|
|
6260
|
+
from,
|
|
6261
|
+
to,
|
|
6262
|
+
before: day > firstDay || rawFrom < startMinute,
|
|
6263
|
+
after: day < lastDay || rawTo > endMinute,
|
|
6264
|
+
};
|
|
6265
|
+
}
|
|
6266
|
+
/** The grid's own window: which days, and which minutes of them. */
|
|
6267
|
+
function bounds(options) {
|
|
6268
|
+
const dayCount = Math.max(1, Math.trunc(options.dayCount ?? DEFAULT_DAY_COUNT));
|
|
6269
|
+
const snap = options.snapToWeek !== false && dayCount === DEFAULT_DAY_COUNT;
|
|
6270
|
+
const firstDay = foldToEpochDay(snap
|
|
6271
|
+
? foldStartOfWeek(options.date, options.weekStartsOn ?? "mon")
|
|
6272
|
+
: options.date);
|
|
6273
|
+
const startMinute = foldToMinutes(options.dayStart ?? "00:00");
|
|
6274
|
+
const rawEnd = foldToMinutes(options.dayEnd ?? "24:00");
|
|
6275
|
+
return {
|
|
6276
|
+
firstDay,
|
|
6277
|
+
dayCount,
|
|
6278
|
+
startMinute,
|
|
6279
|
+
// A window with no height would divide by zero downstream.
|
|
6280
|
+
endMinute: rawEnd > startMinute ? rawEnd : FOLD_MINUTES_PER_DAY,
|
|
6281
|
+
};
|
|
6282
|
+
}
|
|
6283
|
+
/** The hour marks inside the window, including the one it opens on. */
|
|
6284
|
+
function hourMarks(startMinute, endMinute) {
|
|
6285
|
+
const marks = [];
|
|
6286
|
+
const first = Math.ceil(startMinute / MINUTES_PER_HOUR) * MINUTES_PER_HOUR;
|
|
6287
|
+
for (let minute = first; minute < endMinute; minute += MINUTES_PER_HOUR) {
|
|
6288
|
+
const hour = Math.floor(minute / MINUTES_PER_HOUR);
|
|
6289
|
+
marks.push(`${hour < 10 ? "0" : ""}${hour}:00`);
|
|
6290
|
+
}
|
|
6291
|
+
return marks;
|
|
6292
|
+
}
|
|
6293
|
+
/**
|
|
6294
|
+
* Lays out `dayCount` days as hour columns plus an all-day strip.
|
|
6295
|
+
*
|
|
6296
|
+
* Timed events are placed on the clock; everything else — no times, or only one
|
|
6297
|
+
* of them — goes on the strip, where it spans days exactly as it does in the
|
|
6298
|
+
* month grid.
|
|
6299
|
+
*/
|
|
6300
|
+
function foldBuildTimeGrid(events, options) {
|
|
6301
|
+
const { firstDay, dayCount, startMinute, endMinute } = bounds(options);
|
|
6302
|
+
const span = endMinute - startMinute;
|
|
6303
|
+
const timed = events.filter((event) => foldIsTimed(event));
|
|
6304
|
+
const allDayEvents = events.filter((event) => !foldIsTimed(event));
|
|
6305
|
+
const hidden = new Map();
|
|
6306
|
+
const strip = foldPackRow(foldCollapseGroups(allDayEvents).filter((candidate) => candidate.endDay >= firstDay &&
|
|
6307
|
+
candidate.startDay < firstDay + dayCount), {
|
|
6308
|
+
start: foldFromEpochDay(firstDay),
|
|
6309
|
+
startDay: firstDay,
|
|
6310
|
+
dayCount,
|
|
6311
|
+
}, Math.max(0, Math.trunc(options.maxAllDayLanes ?? DEFAULT_ALL_DAY_LANES)), hidden);
|
|
6312
|
+
const context = {
|
|
6313
|
+
month: foldFromEpochDay(firstDay).slice(0, 7),
|
|
6314
|
+
today: options.today,
|
|
6315
|
+
weekendDays: foldWeekendOr(options.weekendDays),
|
|
6316
|
+
counts: foldCountByDay(events, firstDay, firstDay + dayCount - 1),
|
|
6317
|
+
hidden: FOLD_NO_HIDDEN_DAYS,
|
|
6318
|
+
};
|
|
6319
|
+
const columns = Array.from({ length: dayCount }, (_unused, offset) => {
|
|
6320
|
+
const day = firstDay + offset;
|
|
6321
|
+
const date = foldFromEpochDay(day);
|
|
6322
|
+
const placed = timed
|
|
6323
|
+
.map((event) => ({
|
|
6324
|
+
event,
|
|
6325
|
+
window: minutesOnDay(event, day, startMinute, endMinute),
|
|
6326
|
+
}))
|
|
6327
|
+
.filter((candidate) => candidate.window !== null);
|
|
6328
|
+
const slots = foldLayOutOverlaps(placed.map(({ window }) => ({
|
|
6329
|
+
startMinute: window.from,
|
|
6330
|
+
endMinute: window.to,
|
|
6331
|
+
})));
|
|
6332
|
+
return {
|
|
6333
|
+
day: foldMakeDay(day, context),
|
|
6334
|
+
blocks: placed.map(({ event, window }, index) => ({
|
|
6335
|
+
key: `${event.id}@${date}`,
|
|
6336
|
+
event,
|
|
6337
|
+
top: (window.from - startMinute) / span,
|
|
6338
|
+
height: (window.to - window.from) / span,
|
|
6339
|
+
column: slots[index]?.column ?? 0,
|
|
6340
|
+
columns: slots[index]?.columns ?? 1,
|
|
6341
|
+
continuesBefore: window.before,
|
|
6342
|
+
continuesAfter: window.after,
|
|
6343
|
+
})),
|
|
6344
|
+
};
|
|
6345
|
+
});
|
|
6346
|
+
return {
|
|
6347
|
+
columns,
|
|
6348
|
+
allDay: strip.bands,
|
|
6349
|
+
allDayHiddenCount: strip.hiddenCount,
|
|
6350
|
+
hours: hourMarks(startMinute, endMinute),
|
|
6351
|
+
startMinute,
|
|
6352
|
+
endMinute,
|
|
6353
|
+
};
|
|
6354
|
+
}
|
|
6355
|
+
|
|
6356
|
+
/**
|
|
6357
|
+
* `<fold-calendar-timegrid>` — days as hour columns, with an all-day strip.
|
|
6358
|
+
*
|
|
6359
|
+
* The reading the other four cannot give: **when inside a day**. A meeting is a
|
|
6360
|
+
* block whose height is its duration and whose width is shared with whatever it
|
|
6361
|
+
* collides with; an absence is a band across the strip on top, spanning days
|
|
6362
|
+
* exactly as it does in the month view — the same packer draws both, so a
|
|
6363
|
+
* three-day leave request reads identically wherever it appears.
|
|
6364
|
+
*
|
|
6365
|
+
* Times are wall-clock `HH:mm`, never instants — the same decision as the date,
|
|
6366
|
+
* for the same reason. 09:00 is the hour on the wall; the app converts once at
|
|
6367
|
+
* its own boundary, and no zone can move a meeting after that.
|
|
6368
|
+
*
|
|
6369
|
+
* ## Inputs
|
|
6370
|
+
* | Input | Type | Default | Meaning |
|
|
6371
|
+
* |-------------------|--------------------------|-----------|---------|
|
|
6372
|
+
* | `date` | `FoldCalendarDate` | — | Any date in the range. |
|
|
6373
|
+
* | `events` | `FoldCalendarEvent<T>[]` | `[]` | Timed events go on the clock, the rest on the strip. |
|
|
6374
|
+
* | `dayCount` | `number` | `7` | `1` is a day view, `7` a week. |
|
|
6375
|
+
* | `today` | `FoldCalendarDate` | — | The column to mark. |
|
|
6376
|
+
* | `now` | `FoldCalendarTime` | — | Draws the current-time line on `today`'s column. Passed in, never read from a clock — the package has none, and an SSR render must not invent one. |
|
|
6377
|
+
* | `dayStart`/`dayEnd` | `FoldCalendarTime` | `00:00`/`24:00` | The visible window; `08:00`–`20:00` is a working day. |
|
|
6378
|
+
* | `weekStartsOn` | `FoldWeekday` | the locale's | Which day opens the week. |
|
|
6379
|
+
* | `weekendDays` | `FoldWeekday[]` | the locale's | Which columns are shaded. |
|
|
6380
|
+
* | `maxAllDayLanes` | `number` | `2` | Lanes the strip stacks before overflowing. |
|
|
6381
|
+
* | `locale`/`labels`/`formats` | — | — | See the other views. |
|
|
6382
|
+
*
|
|
6383
|
+
* ## Outputs
|
|
6384
|
+
* | Output | Payload | Fires on |
|
|
6385
|
+
* |--------------|------------------------|----------|
|
|
6386
|
+
* | `dayClick` | `FoldCalendarDate` | A column header activated. |
|
|
6387
|
+
* | `eventClick` | `FoldCalendarEvent<T>` | A block or an all-day band activated. |
|
|
6388
|
+
*
|
|
6389
|
+
* ## Theming
|
|
6390
|
+
* | CSS variable | Default | Sets |
|
|
6391
|
+
* |---------------------------------------|---------|------|
|
|
6392
|
+
* | `--fold-calendar-timegrid-height` | `640px` | Height of the hour area. |
|
|
6393
|
+
* | `--fold-calendar-timegrid-gutter` | `56px` | Width of the hour labels. |
|
|
6394
|
+
* | `--fold-calendar-timegrid-lane` | `20px` | Height of one all-day lane. |
|
|
6395
|
+
* | `--fold-calendar-bar-width` | `3px` | The tone bar down a block's leading edge. |
|
|
6396
|
+
*
|
|
6397
|
+
* ## Accessibility
|
|
6398
|
+
* Every block and every band is a real `<button>` inside the column it belongs
|
|
6399
|
+
* to, so the tab order follows the day and this view needs none of the month
|
|
6400
|
+
* grid's roving-tabindex machinery. A block's accessible name carries its
|
|
6401
|
+
* **times**, which its position on the clock conveys only visually.
|
|
6402
|
+
*
|
|
6403
|
+
* @selector `fold-calendar-timegrid`
|
|
6404
|
+
*
|
|
6405
|
+
* @example
|
|
6406
|
+
* ```html
|
|
6407
|
+
* <fold-calendar-timegrid
|
|
6408
|
+
* [date]="date()"
|
|
6409
|
+
* [events]="events()"
|
|
6410
|
+
* [today]="today"
|
|
6411
|
+
* [now]="now()"
|
|
6412
|
+
* dayStart="08:00"
|
|
6413
|
+
* dayEnd="20:00"
|
|
6414
|
+
* (eventClick)="open($event)"
|
|
6415
|
+
* />
|
|
6416
|
+
* ```
|
|
6417
|
+
*/
|
|
6418
|
+
class FoldCalendarTimegridComponent {
|
|
6419
|
+
/** Any date inside the range on display. */
|
|
6420
|
+
date = input.required(/* @ts-ignore */
|
|
6421
|
+
...(ngDevMode ? [{ debugName: "date" }] : /* istanbul ignore next */ []));
|
|
6422
|
+
/** Events to plot; timed ones on the clock, the rest on the strip. */
|
|
6423
|
+
events = input([], /* @ts-ignore */
|
|
6424
|
+
...(ngDevMode ? [{ debugName: "events" }] : /* istanbul ignore next */ []));
|
|
6425
|
+
/** Days to show: `1` is a day view, `7` a week. @default 7 */
|
|
6426
|
+
dayCount = input(7, { ...(ngDevMode ? { debugName: "dayCount" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
6427
|
+
/** The day to mark as today. */
|
|
6428
|
+
today = input(/* @ts-ignore */
|
|
6429
|
+
...(ngDevMode ? [undefined, { debugName: "today" }] : /* istanbul ignore next */ []));
|
|
6430
|
+
/**
|
|
6431
|
+
* The current wall-clock time, for the "now" line. Passed in rather than
|
|
6432
|
+
* read: this package owns no clock (`today` works the same way), and a
|
|
6433
|
+
* server render that invented one would hydrate to a different position.
|
|
6434
|
+
*/
|
|
6435
|
+
now = input(/* @ts-ignore */
|
|
6436
|
+
...(ngDevMode ? [undefined, { debugName: "now" }] : /* istanbul ignore next */ []));
|
|
6437
|
+
/** First hour on screen. @default '00:00' */
|
|
6438
|
+
dayStart = input("00:00", /* @ts-ignore */
|
|
6439
|
+
...(ngDevMode ? [{ debugName: "dayStart" }] : /* istanbul ignore next */ []));
|
|
6440
|
+
/** Last hour on screen. @default '24:00' */
|
|
6441
|
+
dayEnd = input("24:00", /* @ts-ignore */
|
|
6442
|
+
...(ngDevMode ? [{ debugName: "dayEnd" }] : /* istanbul ignore next */ []));
|
|
6443
|
+
/** Day the week starts on. @default the locale's own first day */
|
|
6444
|
+
weekStartsOn = input(/* @ts-ignore */
|
|
6445
|
+
...(ngDevMode ? [undefined, { debugName: "weekStartsOn" }] : /* istanbul ignore next */ []));
|
|
6446
|
+
/** Days shaded as the weekend. @default the locale's own weekend */
|
|
6447
|
+
weekendDays = input(/* @ts-ignore */
|
|
6448
|
+
...(ngDevMode ? [undefined, { debugName: "weekendDays" }] : /* istanbul ignore next */ []));
|
|
6449
|
+
/** Lanes the all-day strip stacks before events overflow. @default 2 */
|
|
6450
|
+
maxAllDayLanes = input(2, { ...(ngDevMode ? { debugName: "maxAllDayLanes" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
6451
|
+
/** A column header was activated. */
|
|
6452
|
+
dayClick = output();
|
|
6453
|
+
/** A block or an all-day band was activated. */
|
|
6454
|
+
eventClick = output();
|
|
6455
|
+
/** Labels, locale and the projected chip template — see the directive. */
|
|
6456
|
+
chrome = inject(FoldCalendarChromeDirective);
|
|
6457
|
+
grid = computed(() => foldBuildTimeGrid(this.events(), {
|
|
6458
|
+
date: this.date(),
|
|
6459
|
+
dayCount: this.dayCount(),
|
|
6460
|
+
weekStartsOn: this.chrome.anchor(this.weekStartsOn()),
|
|
6461
|
+
weekendDays: this.chrome.weekend(this.weekendDays()),
|
|
6462
|
+
today: this.today(),
|
|
6463
|
+
dayStart: this.dayStart(),
|
|
6464
|
+
dayEnd: this.dayEnd(),
|
|
6465
|
+
maxAllDayLanes: this.maxAllDayLanes(),
|
|
6466
|
+
}), /* @ts-ignore */
|
|
6467
|
+
...(ngDevMode ? [{ debugName: "grid" }] : /* istanbul ignore next */ []));
|
|
6468
|
+
/** Lane rows for the all-day strip, or none when nothing spans. */
|
|
6469
|
+
stripRows = computed(() => {
|
|
6470
|
+
const lanes = this.grid().allDay.reduce((highest, band) => Math.max(highest, band.lane + 1), 0);
|
|
6471
|
+
return `repeat(${lanes}, var(--fold-calendar-timegrid-lane, 20px))`;
|
|
6472
|
+
}, /* @ts-ignore */
|
|
6473
|
+
...(ngDevMode ? [{ debugName: "stripRows" }] : /* istanbul ignore next */ []));
|
|
6474
|
+
/** Where the "now" line sits, as a fraction of the window, or `null`. */
|
|
6475
|
+
nowOffset = computed(() => {
|
|
6476
|
+
const time = this.now();
|
|
6477
|
+
if (time === undefined) {
|
|
6478
|
+
return null;
|
|
6479
|
+
}
|
|
6480
|
+
const { startMinute, endMinute } = this.grid();
|
|
6481
|
+
const minute = foldToMinutes(time);
|
|
6482
|
+
if (minute < startMinute || minute > endMinute) {
|
|
6483
|
+
return null;
|
|
6484
|
+
}
|
|
6485
|
+
return (minute - startMinute) / (endMinute - startMinute);
|
|
6486
|
+
}, /* @ts-ignore */
|
|
6487
|
+
...(ngDevMode ? [{ debugName: "nowOffset" }] : /* istanbul ignore next */ []));
|
|
6488
|
+
weekdayName(column) {
|
|
6489
|
+
return this.chrome.format(column.day.date, "weekdayShort");
|
|
6490
|
+
}
|
|
6491
|
+
hourLabel(hour) {
|
|
6492
|
+
return this.chrome.formatTime(hour);
|
|
6493
|
+
}
|
|
6494
|
+
/** A column's accessible name: its date, the today marker, and its load. */
|
|
6495
|
+
columnLabel(column) {
|
|
6496
|
+
const labels = this.chrome.l();
|
|
6497
|
+
const parts = [this.chrome.format(column.day.date, "dateFull")];
|
|
6498
|
+
if (column.day.isToday) {
|
|
6499
|
+
parts.push(labels.today);
|
|
6500
|
+
}
|
|
6501
|
+
if (column.day.eventCount > 0) {
|
|
6502
|
+
parts.push(labels.eventCount(column.day.eventCount));
|
|
6503
|
+
}
|
|
6504
|
+
return parts.join(", ");
|
|
6505
|
+
}
|
|
6506
|
+
/**
|
|
6507
|
+
* A block's accessible name — its times first, because where it sits on the
|
|
6508
|
+
* clock is information a screen reader has no other way to get.
|
|
6509
|
+
*/
|
|
6510
|
+
blockLabel(block) {
|
|
6511
|
+
const { startTime, endTime, label } = block.event;
|
|
6512
|
+
if (startTime === undefined || endTime === undefined) {
|
|
6513
|
+
return label;
|
|
6514
|
+
}
|
|
6515
|
+
return `${this.chrome.formatTime(startTime)} – ${this.chrome.formatTime(endTime)}, ${label}`;
|
|
6516
|
+
}
|
|
6517
|
+
/** A band's text: the group's label when it stands for several events. */
|
|
6518
|
+
bandLabel(band) {
|
|
6519
|
+
if (band.groupSize > 1) {
|
|
6520
|
+
return band.event.groupLabel ?? band.event.label;
|
|
6521
|
+
}
|
|
6522
|
+
return band.event.label;
|
|
6523
|
+
}
|
|
6524
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarTimegridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6525
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: FoldCalendarTimegridComponent, isStandalone: true, selector: "fold-calendar-timegrid", inputs: { date: { classPropertyName: "date", publicName: "date", isSignal: true, isRequired: true, transformFunction: null }, events: { classPropertyName: "events", publicName: "events", isSignal: true, isRequired: false, transformFunction: null }, dayCount: { classPropertyName: "dayCount", publicName: "dayCount", isSignal: true, isRequired: false, transformFunction: null }, today: { classPropertyName: "today", publicName: "today", isSignal: true, isRequired: false, transformFunction: null }, now: { classPropertyName: "now", publicName: "now", isSignal: true, isRequired: false, transformFunction: null }, dayStart: { classPropertyName: "dayStart", publicName: "dayStart", isSignal: true, isRequired: false, transformFunction: null }, dayEnd: { classPropertyName: "dayEnd", publicName: "dayEnd", isSignal: true, isRequired: false, transformFunction: null }, weekStartsOn: { classPropertyName: "weekStartsOn", publicName: "weekStartsOn", isSignal: true, isRequired: false, transformFunction: null }, weekendDays: { classPropertyName: "weekendDays", publicName: "weekendDays", isSignal: true, isRequired: false, transformFunction: null }, maxAllDayLanes: { classPropertyName: "maxAllDayLanes", publicName: "maxAllDayLanes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dayClick: "dayClick", eventClick: "eventClick" }, hostDirectives: [{ directive: FoldCalendarChromeDirective, inputs: ["locale", "locale", "labels", "labels", "formats", "formats"] }], ngImport: i0, template: "<div class=\"foldcaltg\" [attr.aria-label]=\"chrome.l().grid\" role=\"group\">\n <div class=\"foldcaltg-head\">\n <span class=\"foldcaltg-corner\" aria-hidden=\"true\"></span>\n @for (column of grid().columns; track column.day.date) {\n <button\n type=\"button\"\n class=\"foldcaltg-dayhead\"\n [class.is-today]=\"column.day.isToday\"\n [class.is-weekend]=\"column.day.isWeekend\"\n [attr.aria-label]=\"columnLabel(column)\"\n [attr.aria-current]=\"column.day.isToday ? 'date' : null\"\n (click)=\"dayClick.emit(column.day.date)\"\n >\n <span class=\"foldcaltg-weekday\">{{ weekdayName(column) }}</span>\n <span class=\"foldcaltg-daynum\">{{ column.day.dayOfMonth }}</span>\n </button>\n }\n </div>\n\n <!-- The all-day strip: the same spanning bands the month grid draws. -->\n @if (grid().allDay.length > 0) {\n <div class=\"foldcaltg-allday\">\n <span class=\"foldcaltg-gutter-label\">{{ chrome.l().allDay }}</span>\n <div\n class=\"foldcaltg-strip\"\n [style.gridTemplateColumns]=\"\n 'repeat(' + grid().columns.length + ', 1fr)'\n \"\n [style.gridTemplateRows]=\"stripRows()\"\n >\n @for (band of grid().allDay; track band.key) {\n <button\n type=\"button\"\n class=\"foldcal-chip foldcaltg-band\"\n [attr.data-tone]=\"band.event.tone ?? 'neutral'\"\n [class.continues-before]=\"band.continuesBefore\"\n [class.continues-after]=\"band.continuesAfter\"\n [style.gridColumn]=\"\n band.startColumn + 1 + ' / ' + (band.endColumn + 2)\n \"\n [style.gridRow]=\"band.lane + 1\"\n (click)=\"eventClick.emit(band.event)\"\n >\n @if (chrome.eventContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{\n $implicit: band.event,\n band: band,\n }\"\n />\n } @else {\n <span class=\"foldcal-chip-bar\"></span>\n <span class=\"foldcal-chip-body\">\n <span class=\"foldcal-chip-label\">{{ bandLabel(band) }}</span>\n </span>\n }\n </button>\n }\n </div>\n </div>\n }\n\n <!--\n The hours scroll, so the region is a tab stop with a name: a scrollable\n area a keyboard cannot reach is a scrollable area a keyboard user cannot\n read (WCAG 2.1.1). Found by axe, not by looking at it.\n -->\n <div\n class=\"foldcaltg-body\"\n tabindex=\"0\"\n role=\"group\"\n [attr.aria-label]=\"chrome.l().hours\"\n >\n <!--\n Positioned, not flowed: the label has to land on the same line as the rule\n it names, and a flex column only gets there by coincidence.\n -->\n <div class=\"foldcaltg-gutter\" aria-hidden=\"true\">\n @for (hour of grid().hours; track hour; let index = $index) {\n <span\n class=\"foldcaltg-hour\"\n [class.is-first]=\"index === 0\"\n [style.top.%]=\"(index / grid().hours.length) * 100\"\n >{{ hourLabel(hour) }}</span\n >\n }\n </div>\n\n <div\n class=\"foldcaltg-cols\"\n [style.gridTemplateColumns]=\"'repeat(' + grid().columns.length + ', 1fr)'\"\n >\n @for (column of grid().columns; track column.day.date) {\n <section\n class=\"foldcaltg-col\"\n [class.is-weekend]=\"column.day.isWeekend\"\n [attr.aria-label]=\"columnLabel(column)\"\n >\n <!-- Hour rules, drawn per column so a column can be styled alone. -->\n <span class=\"foldcaltg-rules\" aria-hidden=\"true\">\n @for (hour of grid().hours; track hour) {\n <span class=\"foldcaltg-rule\"></span>\n }\n </span>\n\n @if (column.day.isToday && nowOffset() !== null) {\n <span\n class=\"foldcaltg-now\"\n aria-hidden=\"true\"\n [style.top.%]=\"(nowOffset() ?? 0) * 100\"\n ></span>\n }\n\n @for (block of column.blocks; track block.key) {\n <button\n type=\"button\"\n class=\"foldcaltg-block\"\n [attr.data-tone]=\"block.event.tone ?? 'neutral'\"\n [class.continues-before]=\"block.continuesBefore\"\n [class.continues-after]=\"block.continuesAfter\"\n [attr.aria-label]=\"blockLabel(block)\"\n [style.top.%]=\"block.top * 100\"\n [style.height.%]=\"block.height * 100\"\n [style.insetInlineStart.%]=\"(block.column / block.columns) * 100\"\n [style.width.%]=\"(1 / block.columns) * 100\"\n (click)=\"eventClick.emit(block.event)\"\n >\n @if (chrome.eventContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{\n $implicit: block.event,\n band: null,\n }\"\n />\n } @else {\n <span class=\"foldcaltg-block-bar\"></span>\n <!-- The glyph sits beside the label, never above it: a\n half-hour block is one line tall and the icon would take\n it. -->\n @if (block.event.icon; as icon) {\n <fold-icon\n class=\"foldcaltg-block-icon\"\n [name]=\"icon\"\n size=\"sm\"\n />\n }\n <span class=\"foldcaltg-block-body\">\n <span class=\"foldcaltg-block-label\">{{\n block.event.label\n }}</span>\n @if (block.event.subline) {\n <span class=\"foldcaltg-block-sub\">{{\n block.event.subline\n }}</span>\n }\n </span>\n }\n </button>\n }\n </section>\n }\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block}.foldcal-chip{position:relative;display:flex;align-items:stretch;gap:var(--fold-space-sm);width:100%;padding:var(--fold-space-xs) var(--fold-space-sm);padding-inline-start:0;border:0;border-radius:var(--fold-calendar-band-radius, var(--fold-radius-sm));background:var(--fold-color-surface-raised);color:var(--fold-color-text);font:inherit;text-align:start;cursor:pointer;overflow:hidden;transition:background-color var(--fold-motion-fast) ease}.foldcal-chip:hover{background:var(--fold-color-surface-hover)}.foldcal-chip:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcal-chip-bar{flex:0 0 var(--fold-calendar-bar-width, 3px);align-self:stretch;background:var(--fold-color-text-muted)}.foldcal-chip-icon{flex:none;align-self:center;margin-inline-start:var(--fold-space-sm);color:var(--fold-color-text-secondary)}.foldcal-chip-body{display:flex;flex-direction:column;gap:1px;min-width:0}.foldcal-chip-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--fold-text-sm);font-weight:500}.foldcal-chip-sub{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs)}.foldcal-chip[data-tone=success] .foldcal-chip-bar{background:var(--fold-color-success)}.foldcal-chip[data-tone=warning] .foldcal-chip-bar{background:var(--fold-color-warning)}.foldcal-chip[data-tone=alert] .foldcal-chip-bar{background:var(--fold-color-alert)}.foldcal-chip[data-tone=muted] .foldcal-chip-bar{background:var(--fold-color-text-faded)}.foldcal-chip[data-tone=muted] .foldcal-chip-label{color:var(--fold-color-text-secondary);text-decoration:line-through;text-decoration-thickness:1px}@media(forced-colors:active){.foldcal-chip{border:1px solid CanvasText}.foldcal-chip-bar{forced-color-adjust:none}}@media(prefers-reduced-motion:reduce){.foldcal-chip{transition:none}}.foldcaltg{display:flex;flex-direction:column;min-height:0;border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-md);background:var(--fold-color-surface-card);overflow:clip}.foldcaltg-head{display:grid;grid-template-columns:var(--fold-calendar-timegrid-gutter, 56px) repeat(auto-fit,minmax(0,1fr));grid-auto-flow:column;grid-auto-columns:minmax(0,1fr);background:var(--fold-color-surface-subtle);border-bottom:1px solid var(--fold-color-border)}.foldcaltg-corner{border-inline-end:1px solid var(--fold-color-border-subtle)}.foldcaltg-dayhead{display:flex;flex-direction:column;align-items:center;gap:1px;padding:var(--fold-space-sm) var(--fold-space-xs);border:0;border-inline-end:1px solid var(--fold-color-border-subtle);background:transparent;color:inherit;font:inherit;cursor:pointer}.foldcaltg-dayhead:last-child{border-inline-end:0}.foldcaltg-dayhead:hover{background:var(--fold-color-surface-hover)}.foldcaltg-dayhead:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcaltg-dayhead.is-weekend{background:var(--fold-color-surface-sunken)}.foldcaltg-weekday{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase}.foldcaltg-daynum{color:var(--fold-color-text-secondary);font-size:var(--fold-text-md);font-variant-numeric:tabular-nums}.foldcaltg-dayhead.is-today{box-shadow:inset 0 2px 0 0 var(--fold-color-primary)}.foldcaltg-dayhead.is-today .foldcaltg-daynum{color:var(--fold-color-primary-text);font-weight:700}.foldcaltg-allday{display:grid;grid-template-columns:var(--fold-calendar-timegrid-gutter, 56px) 1fr;padding-block:var(--fold-space-xs);border-bottom:1px solid var(--fold-color-border)}.foldcaltg-gutter-label{padding-inline:var(--fold-space-xs);border-inline-end:1px solid var(--fold-color-border-subtle);color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);text-align:end}.foldcaltg-strip{display:grid;gap:1px}.foldcaltg-band{min-width:0;margin-inline:var(--fold-calendar-band-gutter, var(--fold-space-xs));padding-block:0;font-size:var(--fold-text-xs)}.foldcaltg-band.continues-before{margin-inline-start:0;border-start-start-radius:0;border-end-start-radius:0}.foldcaltg-band.continues-after{margin-inline-end:0;border-start-end-radius:0;border-end-end-radius:0}.foldcaltg-body{display:grid}.foldcaltg-body:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcaltg-body{grid-template-columns:var(--fold-calendar-timegrid-gutter, 56px) 1fr;height:var(--fold-calendar-timegrid-height, 640px);overflow-y:auto}.foldcaltg-gutter{position:relative;border-inline-end:1px solid var(--fold-color-border-subtle)}.foldcaltg-hour{position:absolute;inset-inline:0;padding-inline-end:var(--fold-space-xs);color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);font-variant-numeric:tabular-nums;text-align:end;transform:translateY(-.5em)}.foldcaltg-hour.is-first{transform:none}.foldcaltg-cols{display:grid}.foldcaltg-col{position:relative;border-inline-end:1px solid var(--fold-color-border-subtle)}.foldcaltg-col:last-of-type{border-inline-end:0}.foldcaltg-col.is-weekend{background:var(--fold-color-surface-subtle)}.foldcaltg-rules{position:absolute;inset:0;display:flex;flex-direction:column}.foldcaltg-rule{flex:1;border-top:1px solid var(--fold-color-border-subtle)}.foldcaltg-now{position:absolute;inset-inline:0;z-index:2;height:2px;background:var(--fold-color-alert)}.foldcaltg-now:before{content:\"\";position:absolute;inset-inline-start:0;top:-3px;width:8px;height:8px;border-radius:50%;background:var(--fold-color-alert)}.foldcaltg-block{position:absolute;z-index:1;display:flex;align-items:stretch;gap:var(--fold-space-xs);min-height:14px;padding:0;padding-inline-end:var(--fold-space-xs);border:0;border-radius:var(--fold-calendar-band-radius, var(--fold-radius-sm));background:var(--fold-color-surface-raised);color:var(--fold-color-text);font:inherit;font-size:var(--fold-text-xs);text-align:start;cursor:pointer;overflow:hidden;transition:filter var(--fold-motion-fast) ease}.foldcaltg-block:hover{z-index:3;filter:brightness(1.08)}.foldcaltg-block:focus-visible{z-index:3;outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcaltg-block.continues-before{border-start-start-radius:0;border-start-end-radius:0}.foldcaltg-block.continues-after{border-end-start-radius:0;border-end-end-radius:0}.foldcaltg-block-bar{flex:0 0 var(--fold-calendar-bar-width, 3px);align-self:stretch;background:var(--fold-color-text-muted)}.foldcaltg-block-body{display:flex;flex-direction:column;flex:1 1 auto;min-width:0;padding-block:1px}.foldcaltg-block-icon{flex:none;align-self:flex-start;margin-block-start:1px}.foldcaltg-block-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}.foldcaltg-block-sub{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--fold-color-text-secondary)}.foldcaltg-block[data-tone=success]{background:var(--fold-color-success-surface);color:var(--fold-color-success-text)}.foldcaltg-block[data-tone=success] .foldcaltg-block-bar{background:var(--fold-color-success)}.foldcaltg-block[data-tone=warning]{background:var(--fold-color-warning-surface);color:var(--fold-color-warning-text)}.foldcaltg-block[data-tone=warning] .foldcaltg-block-bar{background:var(--fold-color-warning)}.foldcaltg-block[data-tone=alert]{background:var(--fold-color-alert-surface);color:var(--fold-color-alert-text)}.foldcaltg-block[data-tone=alert] .foldcaltg-block-bar{background:var(--fold-color-alert)}.foldcaltg-block[data-tone=muted]{background:transparent;color:var(--fold-color-text-secondary)}.foldcaltg-block[data-tone=muted] .foldcaltg-block-bar{background:var(--fold-color-text-faded)}.foldcaltg-block[data-tone=muted] .foldcaltg-block-label{color:var(--fold-color-text-secondary);text-decoration:line-through;text-decoration-thickness:1px}@media(forced-colors:active){.foldcaltg-block{border:1px solid CanvasText}.foldcaltg-block-bar{forced-color-adjust:none}}@media(forced-colors:active){.foldcaltg-dayhead.is-today{outline:2px solid Highlight;outline-offset:-2px}.foldcaltg-now{background:Highlight;forced-color-adjust:none}}@media(prefers-reduced-motion:reduce){.foldcaltg-block{transition:none}}@media print{.foldcaltg-body{height:auto;overflow:visible}.foldcaltg-block{border:1px solid GrayText;background:Canvas;color:CanvasText}.foldcaltg-block-bar{-webkit-print-color-adjust:exact;print-color-adjust:exact}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: FoldIconComponent, selector: "fold-icon", inputs: ["name", "size", "tone", "title"] }] });
|
|
6526
|
+
}
|
|
6527
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarTimegridComponent, decorators: [{
|
|
6528
|
+
type: Component,
|
|
6529
|
+
args: [{ selector: "fold-calendar-timegrid", standalone: true, imports: [NgTemplateOutlet, FoldIconComponent], hostDirectives: [
|
|
6530
|
+
{
|
|
6531
|
+
directive: FoldCalendarChromeDirective,
|
|
6532
|
+
inputs: ["locale", "labels", "formats"],
|
|
6533
|
+
},
|
|
6534
|
+
], template: "<div class=\"foldcaltg\" [attr.aria-label]=\"chrome.l().grid\" role=\"group\">\n <div class=\"foldcaltg-head\">\n <span class=\"foldcaltg-corner\" aria-hidden=\"true\"></span>\n @for (column of grid().columns; track column.day.date) {\n <button\n type=\"button\"\n class=\"foldcaltg-dayhead\"\n [class.is-today]=\"column.day.isToday\"\n [class.is-weekend]=\"column.day.isWeekend\"\n [attr.aria-label]=\"columnLabel(column)\"\n [attr.aria-current]=\"column.day.isToday ? 'date' : null\"\n (click)=\"dayClick.emit(column.day.date)\"\n >\n <span class=\"foldcaltg-weekday\">{{ weekdayName(column) }}</span>\n <span class=\"foldcaltg-daynum\">{{ column.day.dayOfMonth }}</span>\n </button>\n }\n </div>\n\n <!-- The all-day strip: the same spanning bands the month grid draws. -->\n @if (grid().allDay.length > 0) {\n <div class=\"foldcaltg-allday\">\n <span class=\"foldcaltg-gutter-label\">{{ chrome.l().allDay }}</span>\n <div\n class=\"foldcaltg-strip\"\n [style.gridTemplateColumns]=\"\n 'repeat(' + grid().columns.length + ', 1fr)'\n \"\n [style.gridTemplateRows]=\"stripRows()\"\n >\n @for (band of grid().allDay; track band.key) {\n <button\n type=\"button\"\n class=\"foldcal-chip foldcaltg-band\"\n [attr.data-tone]=\"band.event.tone ?? 'neutral'\"\n [class.continues-before]=\"band.continuesBefore\"\n [class.continues-after]=\"band.continuesAfter\"\n [style.gridColumn]=\"\n band.startColumn + 1 + ' / ' + (band.endColumn + 2)\n \"\n [style.gridRow]=\"band.lane + 1\"\n (click)=\"eventClick.emit(band.event)\"\n >\n @if (chrome.eventContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{\n $implicit: band.event,\n band: band,\n }\"\n />\n } @else {\n <span class=\"foldcal-chip-bar\"></span>\n <span class=\"foldcal-chip-body\">\n <span class=\"foldcal-chip-label\">{{ bandLabel(band) }}</span>\n </span>\n }\n </button>\n }\n </div>\n </div>\n }\n\n <!--\n The hours scroll, so the region is a tab stop with a name: a scrollable\n area a keyboard cannot reach is a scrollable area a keyboard user cannot\n read (WCAG 2.1.1). Found by axe, not by looking at it.\n -->\n <div\n class=\"foldcaltg-body\"\n tabindex=\"0\"\n role=\"group\"\n [attr.aria-label]=\"chrome.l().hours\"\n >\n <!--\n Positioned, not flowed: the label has to land on the same line as the rule\n it names, and a flex column only gets there by coincidence.\n -->\n <div class=\"foldcaltg-gutter\" aria-hidden=\"true\">\n @for (hour of grid().hours; track hour; let index = $index) {\n <span\n class=\"foldcaltg-hour\"\n [class.is-first]=\"index === 0\"\n [style.top.%]=\"(index / grid().hours.length) * 100\"\n >{{ hourLabel(hour) }}</span\n >\n }\n </div>\n\n <div\n class=\"foldcaltg-cols\"\n [style.gridTemplateColumns]=\"'repeat(' + grid().columns.length + ', 1fr)'\"\n >\n @for (column of grid().columns; track column.day.date) {\n <section\n class=\"foldcaltg-col\"\n [class.is-weekend]=\"column.day.isWeekend\"\n [attr.aria-label]=\"columnLabel(column)\"\n >\n <!-- Hour rules, drawn per column so a column can be styled alone. -->\n <span class=\"foldcaltg-rules\" aria-hidden=\"true\">\n @for (hour of grid().hours; track hour) {\n <span class=\"foldcaltg-rule\"></span>\n }\n </span>\n\n @if (column.day.isToday && nowOffset() !== null) {\n <span\n class=\"foldcaltg-now\"\n aria-hidden=\"true\"\n [style.top.%]=\"(nowOffset() ?? 0) * 100\"\n ></span>\n }\n\n @for (block of column.blocks; track block.key) {\n <button\n type=\"button\"\n class=\"foldcaltg-block\"\n [attr.data-tone]=\"block.event.tone ?? 'neutral'\"\n [class.continues-before]=\"block.continuesBefore\"\n [class.continues-after]=\"block.continuesAfter\"\n [attr.aria-label]=\"blockLabel(block)\"\n [style.top.%]=\"block.top * 100\"\n [style.height.%]=\"block.height * 100\"\n [style.insetInlineStart.%]=\"(block.column / block.columns) * 100\"\n [style.width.%]=\"(1 / block.columns) * 100\"\n (click)=\"eventClick.emit(block.event)\"\n >\n @if (chrome.eventContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{\n $implicit: block.event,\n band: null,\n }\"\n />\n } @else {\n <span class=\"foldcaltg-block-bar\"></span>\n <!-- The glyph sits beside the label, never above it: a\n half-hour block is one line tall and the icon would take\n it. -->\n @if (block.event.icon; as icon) {\n <fold-icon\n class=\"foldcaltg-block-icon\"\n [name]=\"icon\"\n size=\"sm\"\n />\n }\n <span class=\"foldcaltg-block-body\">\n <span class=\"foldcaltg-block-label\">{{\n block.event.label\n }}</span>\n @if (block.event.subline) {\n <span class=\"foldcaltg-block-sub\">{{\n block.event.subline\n }}</span>\n }\n </span>\n }\n </button>\n }\n </section>\n }\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block}.foldcal-chip{position:relative;display:flex;align-items:stretch;gap:var(--fold-space-sm);width:100%;padding:var(--fold-space-xs) var(--fold-space-sm);padding-inline-start:0;border:0;border-radius:var(--fold-calendar-band-radius, var(--fold-radius-sm));background:var(--fold-color-surface-raised);color:var(--fold-color-text);font:inherit;text-align:start;cursor:pointer;overflow:hidden;transition:background-color var(--fold-motion-fast) ease}.foldcal-chip:hover{background:var(--fold-color-surface-hover)}.foldcal-chip:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcal-chip-bar{flex:0 0 var(--fold-calendar-bar-width, 3px);align-self:stretch;background:var(--fold-color-text-muted)}.foldcal-chip-icon{flex:none;align-self:center;margin-inline-start:var(--fold-space-sm);color:var(--fold-color-text-secondary)}.foldcal-chip-body{display:flex;flex-direction:column;gap:1px;min-width:0}.foldcal-chip-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--fold-text-sm);font-weight:500}.foldcal-chip-sub{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs)}.foldcal-chip[data-tone=success] .foldcal-chip-bar{background:var(--fold-color-success)}.foldcal-chip[data-tone=warning] .foldcal-chip-bar{background:var(--fold-color-warning)}.foldcal-chip[data-tone=alert] .foldcal-chip-bar{background:var(--fold-color-alert)}.foldcal-chip[data-tone=muted] .foldcal-chip-bar{background:var(--fold-color-text-faded)}.foldcal-chip[data-tone=muted] .foldcal-chip-label{color:var(--fold-color-text-secondary);text-decoration:line-through;text-decoration-thickness:1px}@media(forced-colors:active){.foldcal-chip{border:1px solid CanvasText}.foldcal-chip-bar{forced-color-adjust:none}}@media(prefers-reduced-motion:reduce){.foldcal-chip{transition:none}}.foldcaltg{display:flex;flex-direction:column;min-height:0;border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-md);background:var(--fold-color-surface-card);overflow:clip}.foldcaltg-head{display:grid;grid-template-columns:var(--fold-calendar-timegrid-gutter, 56px) repeat(auto-fit,minmax(0,1fr));grid-auto-flow:column;grid-auto-columns:minmax(0,1fr);background:var(--fold-color-surface-subtle);border-bottom:1px solid var(--fold-color-border)}.foldcaltg-corner{border-inline-end:1px solid var(--fold-color-border-subtle)}.foldcaltg-dayhead{display:flex;flex-direction:column;align-items:center;gap:1px;padding:var(--fold-space-sm) var(--fold-space-xs);border:0;border-inline-end:1px solid var(--fold-color-border-subtle);background:transparent;color:inherit;font:inherit;cursor:pointer}.foldcaltg-dayhead:last-child{border-inline-end:0}.foldcaltg-dayhead:hover{background:var(--fold-color-surface-hover)}.foldcaltg-dayhead:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcaltg-dayhead.is-weekend{background:var(--fold-color-surface-sunken)}.foldcaltg-weekday{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase}.foldcaltg-daynum{color:var(--fold-color-text-secondary);font-size:var(--fold-text-md);font-variant-numeric:tabular-nums}.foldcaltg-dayhead.is-today{box-shadow:inset 0 2px 0 0 var(--fold-color-primary)}.foldcaltg-dayhead.is-today .foldcaltg-daynum{color:var(--fold-color-primary-text);font-weight:700}.foldcaltg-allday{display:grid;grid-template-columns:var(--fold-calendar-timegrid-gutter, 56px) 1fr;padding-block:var(--fold-space-xs);border-bottom:1px solid var(--fold-color-border)}.foldcaltg-gutter-label{padding-inline:var(--fold-space-xs);border-inline-end:1px solid var(--fold-color-border-subtle);color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);text-align:end}.foldcaltg-strip{display:grid;gap:1px}.foldcaltg-band{min-width:0;margin-inline:var(--fold-calendar-band-gutter, var(--fold-space-xs));padding-block:0;font-size:var(--fold-text-xs)}.foldcaltg-band.continues-before{margin-inline-start:0;border-start-start-radius:0;border-end-start-radius:0}.foldcaltg-band.continues-after{margin-inline-end:0;border-start-end-radius:0;border-end-end-radius:0}.foldcaltg-body{display:grid}.foldcaltg-body:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcaltg-body{grid-template-columns:var(--fold-calendar-timegrid-gutter, 56px) 1fr;height:var(--fold-calendar-timegrid-height, 640px);overflow-y:auto}.foldcaltg-gutter{position:relative;border-inline-end:1px solid var(--fold-color-border-subtle)}.foldcaltg-hour{position:absolute;inset-inline:0;padding-inline-end:var(--fold-space-xs);color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);font-variant-numeric:tabular-nums;text-align:end;transform:translateY(-.5em)}.foldcaltg-hour.is-first{transform:none}.foldcaltg-cols{display:grid}.foldcaltg-col{position:relative;border-inline-end:1px solid var(--fold-color-border-subtle)}.foldcaltg-col:last-of-type{border-inline-end:0}.foldcaltg-col.is-weekend{background:var(--fold-color-surface-subtle)}.foldcaltg-rules{position:absolute;inset:0;display:flex;flex-direction:column}.foldcaltg-rule{flex:1;border-top:1px solid var(--fold-color-border-subtle)}.foldcaltg-now{position:absolute;inset-inline:0;z-index:2;height:2px;background:var(--fold-color-alert)}.foldcaltg-now:before{content:\"\";position:absolute;inset-inline-start:0;top:-3px;width:8px;height:8px;border-radius:50%;background:var(--fold-color-alert)}.foldcaltg-block{position:absolute;z-index:1;display:flex;align-items:stretch;gap:var(--fold-space-xs);min-height:14px;padding:0;padding-inline-end:var(--fold-space-xs);border:0;border-radius:var(--fold-calendar-band-radius, var(--fold-radius-sm));background:var(--fold-color-surface-raised);color:var(--fold-color-text);font:inherit;font-size:var(--fold-text-xs);text-align:start;cursor:pointer;overflow:hidden;transition:filter var(--fold-motion-fast) ease}.foldcaltg-block:hover{z-index:3;filter:brightness(1.08)}.foldcaltg-block:focus-visible{z-index:3;outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcaltg-block.continues-before{border-start-start-radius:0;border-start-end-radius:0}.foldcaltg-block.continues-after{border-end-start-radius:0;border-end-end-radius:0}.foldcaltg-block-bar{flex:0 0 var(--fold-calendar-bar-width, 3px);align-self:stretch;background:var(--fold-color-text-muted)}.foldcaltg-block-body{display:flex;flex-direction:column;flex:1 1 auto;min-width:0;padding-block:1px}.foldcaltg-block-icon{flex:none;align-self:flex-start;margin-block-start:1px}.foldcaltg-block-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}.foldcaltg-block-sub{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--fold-color-text-secondary)}.foldcaltg-block[data-tone=success]{background:var(--fold-color-success-surface);color:var(--fold-color-success-text)}.foldcaltg-block[data-tone=success] .foldcaltg-block-bar{background:var(--fold-color-success)}.foldcaltg-block[data-tone=warning]{background:var(--fold-color-warning-surface);color:var(--fold-color-warning-text)}.foldcaltg-block[data-tone=warning] .foldcaltg-block-bar{background:var(--fold-color-warning)}.foldcaltg-block[data-tone=alert]{background:var(--fold-color-alert-surface);color:var(--fold-color-alert-text)}.foldcaltg-block[data-tone=alert] .foldcaltg-block-bar{background:var(--fold-color-alert)}.foldcaltg-block[data-tone=muted]{background:transparent;color:var(--fold-color-text-secondary)}.foldcaltg-block[data-tone=muted] .foldcaltg-block-bar{background:var(--fold-color-text-faded)}.foldcaltg-block[data-tone=muted] .foldcaltg-block-label{color:var(--fold-color-text-secondary);text-decoration:line-through;text-decoration-thickness:1px}@media(forced-colors:active){.foldcaltg-block{border:1px solid CanvasText}.foldcaltg-block-bar{forced-color-adjust:none}}@media(forced-colors:active){.foldcaltg-dayhead.is-today{outline:2px solid Highlight;outline-offset:-2px}.foldcaltg-now{background:Highlight;forced-color-adjust:none}}@media(prefers-reduced-motion:reduce){.foldcaltg-block{transition:none}}@media print{.foldcaltg-body{height:auto;overflow:visible}.foldcaltg-block{border:1px solid GrayText;background:Canvas;color:CanvasText}.foldcaltg-block-bar{-webkit-print-color-adjust:exact;print-color-adjust:exact}}\n"] }]
|
|
6535
|
+
}], propDecorators: { date: [{ type: i0.Input, args: [{ isSignal: true, alias: "date", required: true }] }], events: [{ type: i0.Input, args: [{ isSignal: true, alias: "events", required: false }] }], dayCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "dayCount", required: false }] }], today: [{ type: i0.Input, args: [{ isSignal: true, alias: "today", required: false }] }], now: [{ type: i0.Input, args: [{ isSignal: true, alias: "now", required: false }] }], dayStart: [{ type: i0.Input, args: [{ isSignal: true, alias: "dayStart", required: false }] }], dayEnd: [{ type: i0.Input, args: [{ isSignal: true, alias: "dayEnd", required: false }] }], weekStartsOn: [{ type: i0.Input, args: [{ isSignal: true, alias: "weekStartsOn", required: false }] }], weekendDays: [{ type: i0.Input, args: [{ isSignal: true, alias: "weekendDays", required: false }] }], maxAllDayLanes: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxAllDayLanes", required: false }] }], dayClick: [{ type: i0.Output, args: ["dayClick"] }], eventClick: [{ type: i0.Output, args: ["eventClick"] }] } });
|
|
6536
|
+
|
|
4251
6537
|
/**
|
|
4252
6538
|
* `<fold-choice-row>` — a horizontal single-select control.
|
|
4253
6539
|
*
|
|
@@ -7282,13 +9568,13 @@ class FoldNavLayoutComponent {
|
|
|
7282
9568
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldNavLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7283
9569
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.8", type: FoldNavLayoutComponent, isStandalone: true, selector: "fold-nav-layout", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, foldAt: { classPropertyName: "foldAt", publicName: "foldAt", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.is-row": "!stacked()" } }, providers: [
|
|
7284
9570
|
{ provide: FOLD_NAV_LAYOUT, useExisting: FoldNavLayoutComponent },
|
|
7285
|
-
], exportAs: ["foldNavLayout"], ngImport: i0, template: "<div class=\"tl-nav\"><ng-content select=\"[tabNav]\" /></div>\n<div class=\"tl-body\"><ng-content /></div>\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--fold-nav-layout-gap, 16px);min-width:0}:host(.is-row){flex-direction:row}.tl-nav{display:grid;min-width:0}:host(.is-row) .tl-nav{flex:0 0 var(--fold-nav-layout-rail-width, var(--fold-rail-tertiary, 200px))}.tl-body{flex:1 1 auto;min-width:0}\n"] });
|
|
9571
|
+
], exportAs: ["foldNavLayout"], ngImport: i0, template: "<div class=\"tl-nav\"><ng-content select=\"[tabNav]\" /></div>\n<div class=\"tl-body\"><ng-content /></div>\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--fold-nav-layout-gap, var(--fold-space-lg, 16px));min-width:0}:host(.is-row){flex-direction:row}.tl-nav{display:grid;min-width:0}:host(.is-row) .tl-nav{flex:0 0 var(--fold-nav-layout-rail-width, var(--fold-rail-tertiary, 200px))}.tl-body{flex:1 1 auto;min-width:0}\n"] });
|
|
7286
9572
|
}
|
|
7287
9573
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldNavLayoutComponent, decorators: [{
|
|
7288
9574
|
type: Component,
|
|
7289
9575
|
args: [{ selector: "fold-nav-layout", standalone: true, exportAs: "foldNavLayout", host: { "[class.is-row]": "!stacked()" }, providers: [
|
|
7290
9576
|
{ provide: FOLD_NAV_LAYOUT, useExisting: FoldNavLayoutComponent },
|
|
7291
|
-
], template: "<div class=\"tl-nav\"><ng-content select=\"[tabNav]\" /></div>\n<div class=\"tl-body\"><ng-content /></div>\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--fold-nav-layout-gap, 16px);min-width:0}:host(.is-row){flex-direction:row}.tl-nav{display:grid;min-width:0}:host(.is-row) .tl-nav{flex:0 0 var(--fold-nav-layout-rail-width, var(--fold-rail-tertiary, 200px))}.tl-body{flex:1 1 auto;min-width:0}\n"] }]
|
|
9577
|
+
], template: "<div class=\"tl-nav\"><ng-content select=\"[tabNav]\" /></div>\n<div class=\"tl-body\"><ng-content /></div>\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--fold-nav-layout-gap, var(--fold-space-lg, 16px));min-width:0}:host(.is-row){flex-direction:row}.tl-nav{display:grid;min-width:0}:host(.is-row) .tl-nav{flex:0 0 var(--fold-nav-layout-rail-width, var(--fold-rail-tertiary, 200px))}.tl-body{flex:1 1 auto;min-width:0}\n"] }]
|
|
7292
9578
|
}], ctorParameters: () => [], propDecorators: { placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], foldAt: [{ type: i0.Input, args: [{ isSignal: true, alias: "foldAt", required: false }] }] } });
|
|
7293
9579
|
|
|
7294
9580
|
/**
|
|
@@ -7871,6 +10157,13 @@ class FoldViewNavComponent {
|
|
|
7871
10157
|
...(ngDevMode ? [{ debugName: "background" }] : /* istanbul ignore next */ []));
|
|
7872
10158
|
/** The nearest layout, if any — lets `direction="auto"` follow it. */
|
|
7873
10159
|
layout = inject(FOLD_NAV_LAYOUT, { optional: true });
|
|
10160
|
+
/**
|
|
10161
|
+
* No wrapping `fold-nav-layout`. Standalone, the bar owns its gap to the
|
|
10162
|
+
* following content; inside a layout, the layout owns it (avoiding a double
|
|
10163
|
+
* gap).
|
|
10164
|
+
*/
|
|
10165
|
+
isStandalone = computed(() => this.layout === null, /* @ts-ignore */
|
|
10166
|
+
...(ngDevMode ? [{ debugName: "isStandalone" }] : /* istanbul ignore next */ []));
|
|
7874
10167
|
/** `direction` with `auto` resolved against the wrapping layout. */
|
|
7875
10168
|
resolvedDirection = computed(() => {
|
|
7876
10169
|
const d = this.direction();
|
|
@@ -7881,7 +10174,7 @@ class FoldViewNavComponent {
|
|
|
7881
10174
|
}, /* @ts-ignore */
|
|
7882
10175
|
...(ngDevMode ? [{ debugName: "resolvedDirection" }] : /* istanbul ignore next */ []));
|
|
7883
10176
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldViewNavComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7884
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: FoldViewNavComponent, isStandalone: true, selector: "fold-view-nav", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, activeKey: { classPropertyName: "activeKey", publicName: "activeKey", isSignal: true, isRequired: false, transformFunction: null }, activeStyle: { classPropertyName: "activeStyle", publicName: "activeStyle", isSignal: true, isRequired: false, transformFunction: null }, direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, background: { classPropertyName: "background", publicName: "background", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeKey: "activeKeyChange" }, ngImport: i0, template: "<nav\n class=\"tab-bar\"\n [class.style-underline]=\"activeStyle() === 'underline'\"\n [class.style-fill]=\"activeStyle() === 'fill'\"\n [class.dir-vertical]=\"resolvedDirection() === 'vertical'\"\n [class.size-comfortable]=\"size() === 'comfortable'\"\n [class.is-collapsed]=\"collapsed()\"\n [class.bg-surface]=\"background() === 'surface'\"\n>\n @for (item of items(); track item.key) {\n @if (item.disabled) {\n <button\n type=\"button\"\n class=\"tab-bar-item is-disabled\"\n disabled\n aria-disabled=\"true\"\n >\n <ng-container\n [ngTemplateOutlet]=\"body\"\n [ngTemplateOutletContext]=\"{ $implicit: item, active: false }\"\n />\n </button>\n } @else if (item.link !== undefined) {\n <a\n class=\"tab-bar-item\"\n [routerLink]=\"item.link\"\n routerLinkActive=\"is-active\"\n #rla=\"routerLinkActive\"\n [attr.aria-current]=\"rla.isActive ? 'page' : null\"\n >\n <ng-container\n [ngTemplateOutlet]=\"body\"\n [ngTemplateOutletContext]=\"{ $implicit: item, active: rla.isActive }\"\n />\n </a>\n } @else if (item.href !== undefined) {\n <a\n class=\"tab-bar-item\"\n [href]=\"item.href\"\n [class.is-active]=\"activeKey() === item.key\"\n [attr.aria-current]=\"activeKey() === item.key ? 'page' : null\"\n >\n <ng-container\n [ngTemplateOutlet]=\"body\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n active: activeKey() === item.key,\n }\"\n />\n </a>\n } @else {\n <button\n type=\"button\"\n class=\"tab-bar-item\"\n [class.is-active]=\"activeKey() === item.key\"\n [attr.aria-current]=\"activeKey() === item.key ? 'page' : null\"\n (click)=\"activeKey.set(item.key)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"body\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n active: activeKey() === item.key,\n }\"\n />\n </button>\n }\n }\n</nav>\n\n<!-- One item's inner content, shared by the link / button branches. -->\n<ng-template #body let-item let-active=\"active\">\n @if (item.icon; as ic) {\n <fold-icon class=\"tab-bar-icon\" [name]=\"ic\" size=\"sm\" />\n } @else if (collapsed()) {\n <!-- Collapsed needs a fixed glyph; an icon-less item falls back to its\n initial so the rail stays icon-width and the label still tooltips. -->\n <span class=\"tab-bar-icon tab-bar-initial\" aria-hidden=\"true\">{{\n item.label.charAt(0)\n }}</span>\n }\n <span class=\"tab-bar-label\">{{ item.label }}</span>\n @if (item.badge !== undefined && item.badge !== null) {\n <fold-badge\n class=\"tab-bar-badge\"\n [content]=\"item.badge + ''\"\n [variant]=\"active ? 'accent' : 'neutral'\"\n />\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:flex;width:100%;max-width:100%;overflow:hidden}.tab-bar{flex:1;display:flex;max-width:100%;gap:2px;-webkit-user-select:none;user-select:none}.bg-surface{background:var(--fold-color-bg-rail-tertiary);padding:var(--fold-space-xs) var(--fold-space-sm) 0}.bg-surface.dir-vertical{padding:var(--fold-space-md) var(--fold-space-sm)}.tab-bar-item{flex:1;display:flex;align-items:center;justify-content:center;gap:var(--fold-space-sm);padding:var(--fold-space-sm) var(--fold-space-sm);background:none;border:none;color:var(--fold-color-text-muted);font-family:inherit;font-size:10px;font-weight:600;cursor:pointer;text-align:center;text-decoration:none;transition:color .1s ease,background .1s ease,border-color .1s ease;white-space:nowrap}.tab-bar-item:hover{color:var(--fold-color-text-secondary)}.tab-bar-item.is-disabled{opacity:.5;cursor:default;pointer-events:none}.size-comfortable .tab-bar-item{flex:0 1 auto;gap:var(--fold-space-sm);padding:var(--fold-space-md) var(--fold-space-lg);font-size:var(--fold-text-sm)}.dir-vertical.size-comfortable .tab-bar-item{padding:var(--fold-space-md) var(--fold-space-md);font-size:var(--fold-text-md)}.style-underline{border-bottom:1px solid var(--fold-color-border)}.style-underline .tab-bar-item{border-bottom:2px solid transparent;margin-bottom:-1px}.style-underline .tab-bar-item.is-active{color:var(--fold-color-text);border-bottom-color:var(--fold-color-primary)}.style-fill .tab-bar-item{border-radius:var(--fold-radius-sm);border:1px solid transparent}.style-fill .tab-bar-item.is-active{background:var(--fold-color-primary-surface);color:var(--fold-color-primary-text);font-weight:600}.style-fill .tab-bar-item:hover:not(.is-active){background:var(--fold-color-surface-hover);color:var(--fold-color-text)}.dir-vertical{flex-direction:column}.dir-vertical .tab-bar-item{flex:none;justify-content:flex-start;text-align:left;padding:var(--fold-space-sm) var(--fold-space-md);gap:var(--fold-space-sm);border-radius:var(--fold-radius-sm);font-size:var(--fold-text-sm);font-weight:500}.dir-vertical.style-underline{border-bottom:none;border-right:1px solid var(--fold-color-border)}.dir-vertical.style-underline .tab-bar-item{border-bottom:none;border-left:2px solid transparent;margin-bottom:0;margin-right:-1px}.dir-vertical.style-underline .tab-bar-item.is-active{border-left-color:var(--fold-color-primary)}.dir-vertical.style-fill .tab-bar-item.is-active{background:var(--fold-color-primary-surface);color:var(--fold-color-primary-text);font-weight:600}.is-collapsed .tab-bar-item{position:relative;flex:0 0 auto;gap:0}.is-collapsed .tab-bar-item:not(.is-active) .tab-bar-badge{display:none}.is-collapsed .tab-bar-item.is-active{flex:1 1 auto;min-width:0;gap:var(--fold-space-sm)}.is-collapsed .tab-bar-item.is-active .tab-bar-label{overflow:hidden;text-overflow:ellipsis}.dir-vertical.is-collapsed .tab-bar-item .tab-bar-icon~.tab-bar-label,.is-collapsed:not(.dir-vertical) .tab-bar-item:not(.is-active) .tab-bar-icon~.tab-bar-label{position:absolute;padding:var(--fold-space-xs) var(--fold-space-sm);border-radius:var(--fold-radius-sm);background:var(--fold-color-glass);border:1px solid var(--fold-color-glass-border);color:var(--fold-color-text);font-size:var(--fold-text-xs);font-weight:500;white-space:nowrap;opacity:0;pointer-events:none;box-shadow:var(--fold-shadow-md);transition:opacity var(--fold-motion-fast);z-index:100}.dir-vertical.is-collapsed .tab-bar-item .tab-bar-icon~.tab-bar-label{left:calc(100% + 8px);top:50%;transform:translateY(-50%)}.is-collapsed:not(.dir-vertical) .tab-bar-item:not(.is-active) .tab-bar-icon~.tab-bar-label{top:calc(100% + 6px);left:50%;transform:translate(-50%)}.dir-vertical.is-collapsed .tab-bar-item:hover .tab-bar-icon~.tab-bar-label,.dir-vertical.is-collapsed .tab-bar-item:focus-visible .tab-bar-icon~.tab-bar-label,.is-collapsed:not(.dir-vertical) .tab-bar-item:not(.is-active):hover .tab-bar-icon~.tab-bar-label,.is-collapsed:not(.dir-vertical) .tab-bar-item:not(.is-active):focus-visible .tab-bar-icon~.tab-bar-label{opacity:1}.dir-vertical.is-collapsed{align-items:center}.dir-vertical.is-collapsed .tab-bar-item,.dir-vertical.is-collapsed .tab-bar-item.is-active{justify-content:center;gap:0}.dir-vertical.is-collapsed .tab-bar-item .tab-bar-badge{display:block;position:absolute;top:-2px;right:-2px;transform:scale(.85);transform-origin:top right;pointer-events:none}:host:has(.is-collapsed){overflow:visible}.tab-bar-icon{flex-shrink:0}.tab-bar-initial{display:grid;place-items:center;width:18px;height:18px;border-radius:var(--fold-radius-sm);font-size:11px;font-weight:700;line-height:1;text-transform:uppercase}.tab-bar-badge{flex-shrink:0;font-variant-numeric:tabular-nums}.tab-bar-item:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px;border-radius:var(--fold-radius-sm)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: FoldIconComponent, selector: "fold-icon", inputs: ["name", "size", "tone", "title"] }, { kind: "component", type: FoldBadgeComponent, selector: "fold-badge", inputs: ["content", "radius", "variant"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }] });
|
|
10177
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: FoldViewNavComponent, isStandalone: true, selector: "fold-view-nav", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, activeKey: { classPropertyName: "activeKey", publicName: "activeKey", isSignal: true, isRequired: false, transformFunction: null }, activeStyle: { classPropertyName: "activeStyle", publicName: "activeStyle", isSignal: true, isRequired: false, transformFunction: null }, direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, background: { classPropertyName: "background", publicName: "background", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeKey: "activeKeyChange" }, host: { properties: { "class.is-standalone": "isStandalone()", "class.is-horizontal": "resolvedDirection() === 'horizontal'" } }, ngImport: i0, template: "<nav\n class=\"tab-bar\"\n [class.style-underline]=\"activeStyle() === 'underline'\"\n [class.style-fill]=\"activeStyle() === 'fill'\"\n [class.dir-vertical]=\"resolvedDirection() === 'vertical'\"\n [class.size-comfortable]=\"size() === 'comfortable'\"\n [class.is-collapsed]=\"collapsed()\"\n [class.bg-surface]=\"background() === 'surface'\"\n>\n @for (item of items(); track item.key) {\n @if (item.disabled) {\n <button\n type=\"button\"\n class=\"tab-bar-item is-disabled\"\n disabled\n aria-disabled=\"true\"\n >\n <ng-container\n [ngTemplateOutlet]=\"body\"\n [ngTemplateOutletContext]=\"{ $implicit: item, active: false }\"\n />\n </button>\n } @else if (item.link !== undefined) {\n <a\n class=\"tab-bar-item\"\n [routerLink]=\"item.link\"\n routerLinkActive=\"is-active\"\n #rla=\"routerLinkActive\"\n [attr.aria-current]=\"rla.isActive ? 'page' : null\"\n >\n <ng-container\n [ngTemplateOutlet]=\"body\"\n [ngTemplateOutletContext]=\"{ $implicit: item, active: rla.isActive }\"\n />\n </a>\n } @else if (item.href !== undefined) {\n <a\n class=\"tab-bar-item\"\n [href]=\"item.href\"\n [class.is-active]=\"activeKey() === item.key\"\n [attr.aria-current]=\"activeKey() === item.key ? 'page' : null\"\n >\n <ng-container\n [ngTemplateOutlet]=\"body\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n active: activeKey() === item.key,\n }\"\n />\n </a>\n } @else {\n <button\n type=\"button\"\n class=\"tab-bar-item\"\n [class.is-active]=\"activeKey() === item.key\"\n [attr.aria-current]=\"activeKey() === item.key ? 'page' : null\"\n (click)=\"activeKey.set(item.key)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"body\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n active: activeKey() === item.key,\n }\"\n />\n </button>\n }\n }\n</nav>\n\n<!-- One item's inner content, shared by the link / button branches. -->\n<ng-template #body let-item let-active=\"active\">\n @if (item.icon; as ic) {\n <fold-icon class=\"tab-bar-icon\" [name]=\"ic\" size=\"sm\" />\n } @else if (collapsed()) {\n <!-- Collapsed needs a fixed glyph; an icon-less item falls back to its\n initial so the rail stays icon-width and the label still tooltips. -->\n <span class=\"tab-bar-icon tab-bar-initial\" aria-hidden=\"true\">{{\n item.label.charAt(0)\n }}</span>\n }\n <span class=\"tab-bar-label\">{{ item.label }}</span>\n @if (item.badge !== undefined && item.badge !== null) {\n <fold-badge\n class=\"tab-bar-badge\"\n [content]=\"item.badge + ''\"\n [variant]=\"active ? 'accent' : 'neutral'\"\n />\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:flex;width:100%;max-width:100%;overflow:hidden}.tab-bar{flex:1;display:flex;max-width:100%;gap:2px;-webkit-user-select:none;user-select:none}.bg-surface{background:var(--fold-color-bg-rail-tertiary);padding:var(--fold-space-xs) var(--fold-space-sm) 0}.bg-surface.dir-vertical{padding:var(--fold-space-md) var(--fold-space-sm)}.tab-bar-item{flex:1;display:flex;align-items:center;justify-content:center;gap:var(--fold-space-sm);padding:var(--fold-space-sm) var(--fold-space-sm);background:none;border:none;color:var(--fold-color-text-muted);font-family:inherit;font-size:10px;font-weight:600;cursor:pointer;text-align:center;text-decoration:none;transition:color .1s ease,background .1s ease,border-color .1s ease;white-space:nowrap}.tab-bar-item:hover{color:var(--fold-color-text-secondary)}.tab-bar-item.is-disabled{opacity:.5;cursor:default;pointer-events:none}.size-comfortable .tab-bar-item{flex:0 1 auto;gap:var(--fold-space-sm);padding:var(--fold-space-md) var(--fold-space-lg);font-size:var(--fold-text-sm)}.dir-vertical.size-comfortable .tab-bar-item{padding:var(--fold-space-md) var(--fold-space-md);font-size:var(--fold-text-md)}.style-underline{border-bottom:1px solid var(--fold-color-border)}.style-underline .tab-bar-item{border-bottom:2px solid transparent;margin-bottom:-1px}.style-underline .tab-bar-item.is-active{color:var(--fold-color-text);border-bottom-color:var(--fold-color-primary)}.style-fill .tab-bar-item{border-radius:var(--fold-radius-sm);border:1px solid transparent}.style-fill .tab-bar-item.is-active{background:var(--fold-color-primary-surface);color:var(--fold-color-primary-text);font-weight:600}.style-fill .tab-bar-item:hover:not(.is-active){background:var(--fold-color-surface-hover);color:var(--fold-color-text)}.dir-vertical{flex-direction:column}.dir-vertical .tab-bar-item{flex:none;justify-content:flex-start;text-align:left;padding:var(--fold-space-sm) var(--fold-space-md);gap:var(--fold-space-sm);border-radius:var(--fold-radius-sm);font-size:var(--fold-text-sm);font-weight:500}.dir-vertical.style-underline{border-bottom:none;border-right:1px solid var(--fold-color-border)}.dir-vertical.style-underline .tab-bar-item{border-bottom:none;border-left:2px solid transparent;margin-bottom:0;margin-right:-1px}.dir-vertical.style-underline .tab-bar-item.is-active{border-left-color:var(--fold-color-primary)}.dir-vertical.style-fill .tab-bar-item.is-active{background:var(--fold-color-primary-surface);color:var(--fold-color-primary-text);font-weight:600}.is-collapsed .tab-bar-item{position:relative;flex:0 0 auto;gap:0}.is-collapsed .tab-bar-item:not(.is-active) .tab-bar-badge{display:none}.is-collapsed .tab-bar-item.is-active{flex:1 1 auto;min-width:0;gap:var(--fold-space-sm)}.is-collapsed .tab-bar-item.is-active .tab-bar-label{overflow:hidden;text-overflow:ellipsis}.dir-vertical.is-collapsed .tab-bar-item .tab-bar-icon~.tab-bar-label,.is-collapsed:not(.dir-vertical) .tab-bar-item:not(.is-active) .tab-bar-icon~.tab-bar-label{position:absolute;padding:var(--fold-space-xs) var(--fold-space-sm);border-radius:var(--fold-radius-sm);background:var(--fold-color-glass);border:1px solid var(--fold-color-glass-border);color:var(--fold-color-text);font-size:var(--fold-text-xs);font-weight:500;white-space:nowrap;opacity:0;pointer-events:none;box-shadow:var(--fold-shadow-md);transition:opacity var(--fold-motion-fast);z-index:100}.dir-vertical.is-collapsed .tab-bar-item .tab-bar-icon~.tab-bar-label{left:calc(100% + 8px);top:50%;transform:translateY(-50%)}.is-collapsed:not(.dir-vertical) .tab-bar-item:not(.is-active) .tab-bar-icon~.tab-bar-label{top:calc(100% + 6px);left:50%;transform:translate(-50%)}.dir-vertical.is-collapsed .tab-bar-item:hover .tab-bar-icon~.tab-bar-label,.dir-vertical.is-collapsed .tab-bar-item:focus-visible .tab-bar-icon~.tab-bar-label,.is-collapsed:not(.dir-vertical) .tab-bar-item:not(.is-active):hover .tab-bar-icon~.tab-bar-label,.is-collapsed:not(.dir-vertical) .tab-bar-item:not(.is-active):focus-visible .tab-bar-icon~.tab-bar-label{opacity:1}.dir-vertical.is-collapsed{align-items:center}.dir-vertical.is-collapsed .tab-bar-item,.dir-vertical.is-collapsed .tab-bar-item.is-active{justify-content:center;gap:0}.dir-vertical.is-collapsed .tab-bar-item .tab-bar-badge{display:block;position:absolute;top:-2px;right:-2px;transform:scale(.85);transform-origin:top right;pointer-events:none}:host:has(.is-collapsed){overflow:visible}.tab-bar-icon{flex-shrink:0}.tab-bar-initial{display:grid;place-items:center;width:18px;height:18px;border-radius:var(--fold-radius-sm);font-size:11px;font-weight:700;line-height:1;text-transform:uppercase}.tab-bar-badge{flex-shrink:0;font-variant-numeric:tabular-nums}.tab-bar-item:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px;border-radius:var(--fold-radius-sm)}:host(.is-standalone.is-horizontal){margin-block-end:var(--fold-nav-layout-gap, var(--fold-space-lg, 16px))}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: FoldIconComponent, selector: "fold-icon", inputs: ["name", "size", "tone", "title"] }, { kind: "component", type: FoldBadgeComponent, selector: "fold-badge", inputs: ["content", "radius", "variant"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }] });
|
|
7885
10178
|
}
|
|
7886
10179
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldViewNavComponent, decorators: [{
|
|
7887
10180
|
type: Component,
|
|
@@ -7891,7 +10184,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
7891
10184
|
FoldBadgeComponent,
|
|
7892
10185
|
RouterLink,
|
|
7893
10186
|
RouterLinkActive,
|
|
7894
|
-
],
|
|
10187
|
+
], host: {
|
|
10188
|
+
// A standalone horizontal bar owns the gap to the content it heads, matching
|
|
10189
|
+
// fold-nav-layout. Inside a layout, the layout owns that gap (these two
|
|
10190
|
+
// classes never both apply there); the SCSS keys the margin on their
|
|
10191
|
+
// conjunction. Vertical is a side rail, not a header — no block-end margin.
|
|
10192
|
+
"[class.is-standalone]": "isStandalone()",
|
|
10193
|
+
"[class.is-horizontal]": "resolvedDirection() === 'horizontal'",
|
|
10194
|
+
}, template: "<nav\n class=\"tab-bar\"\n [class.style-underline]=\"activeStyle() === 'underline'\"\n [class.style-fill]=\"activeStyle() === 'fill'\"\n [class.dir-vertical]=\"resolvedDirection() === 'vertical'\"\n [class.size-comfortable]=\"size() === 'comfortable'\"\n [class.is-collapsed]=\"collapsed()\"\n [class.bg-surface]=\"background() === 'surface'\"\n>\n @for (item of items(); track item.key) {\n @if (item.disabled) {\n <button\n type=\"button\"\n class=\"tab-bar-item is-disabled\"\n disabled\n aria-disabled=\"true\"\n >\n <ng-container\n [ngTemplateOutlet]=\"body\"\n [ngTemplateOutletContext]=\"{ $implicit: item, active: false }\"\n />\n </button>\n } @else if (item.link !== undefined) {\n <a\n class=\"tab-bar-item\"\n [routerLink]=\"item.link\"\n routerLinkActive=\"is-active\"\n #rla=\"routerLinkActive\"\n [attr.aria-current]=\"rla.isActive ? 'page' : null\"\n >\n <ng-container\n [ngTemplateOutlet]=\"body\"\n [ngTemplateOutletContext]=\"{ $implicit: item, active: rla.isActive }\"\n />\n </a>\n } @else if (item.href !== undefined) {\n <a\n class=\"tab-bar-item\"\n [href]=\"item.href\"\n [class.is-active]=\"activeKey() === item.key\"\n [attr.aria-current]=\"activeKey() === item.key ? 'page' : null\"\n >\n <ng-container\n [ngTemplateOutlet]=\"body\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n active: activeKey() === item.key,\n }\"\n />\n </a>\n } @else {\n <button\n type=\"button\"\n class=\"tab-bar-item\"\n [class.is-active]=\"activeKey() === item.key\"\n [attr.aria-current]=\"activeKey() === item.key ? 'page' : null\"\n (click)=\"activeKey.set(item.key)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"body\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n active: activeKey() === item.key,\n }\"\n />\n </button>\n }\n }\n</nav>\n\n<!-- One item's inner content, shared by the link / button branches. -->\n<ng-template #body let-item let-active=\"active\">\n @if (item.icon; as ic) {\n <fold-icon class=\"tab-bar-icon\" [name]=\"ic\" size=\"sm\" />\n } @else if (collapsed()) {\n <!-- Collapsed needs a fixed glyph; an icon-less item falls back to its\n initial so the rail stays icon-width and the label still tooltips. -->\n <span class=\"tab-bar-icon tab-bar-initial\" aria-hidden=\"true\">{{\n item.label.charAt(0)\n }}</span>\n }\n <span class=\"tab-bar-label\">{{ item.label }}</span>\n @if (item.badge !== undefined && item.badge !== null) {\n <fold-badge\n class=\"tab-bar-badge\"\n [content]=\"item.badge + ''\"\n [variant]=\"active ? 'accent' : 'neutral'\"\n />\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:flex;width:100%;max-width:100%;overflow:hidden}.tab-bar{flex:1;display:flex;max-width:100%;gap:2px;-webkit-user-select:none;user-select:none}.bg-surface{background:var(--fold-color-bg-rail-tertiary);padding:var(--fold-space-xs) var(--fold-space-sm) 0}.bg-surface.dir-vertical{padding:var(--fold-space-md) var(--fold-space-sm)}.tab-bar-item{flex:1;display:flex;align-items:center;justify-content:center;gap:var(--fold-space-sm);padding:var(--fold-space-sm) var(--fold-space-sm);background:none;border:none;color:var(--fold-color-text-muted);font-family:inherit;font-size:10px;font-weight:600;cursor:pointer;text-align:center;text-decoration:none;transition:color .1s ease,background .1s ease,border-color .1s ease;white-space:nowrap}.tab-bar-item:hover{color:var(--fold-color-text-secondary)}.tab-bar-item.is-disabled{opacity:.5;cursor:default;pointer-events:none}.size-comfortable .tab-bar-item{flex:0 1 auto;gap:var(--fold-space-sm);padding:var(--fold-space-md) var(--fold-space-lg);font-size:var(--fold-text-sm)}.dir-vertical.size-comfortable .tab-bar-item{padding:var(--fold-space-md) var(--fold-space-md);font-size:var(--fold-text-md)}.style-underline{border-bottom:1px solid var(--fold-color-border)}.style-underline .tab-bar-item{border-bottom:2px solid transparent;margin-bottom:-1px}.style-underline .tab-bar-item.is-active{color:var(--fold-color-text);border-bottom-color:var(--fold-color-primary)}.style-fill .tab-bar-item{border-radius:var(--fold-radius-sm);border:1px solid transparent}.style-fill .tab-bar-item.is-active{background:var(--fold-color-primary-surface);color:var(--fold-color-primary-text);font-weight:600}.style-fill .tab-bar-item:hover:not(.is-active){background:var(--fold-color-surface-hover);color:var(--fold-color-text)}.dir-vertical{flex-direction:column}.dir-vertical .tab-bar-item{flex:none;justify-content:flex-start;text-align:left;padding:var(--fold-space-sm) var(--fold-space-md);gap:var(--fold-space-sm);border-radius:var(--fold-radius-sm);font-size:var(--fold-text-sm);font-weight:500}.dir-vertical.style-underline{border-bottom:none;border-right:1px solid var(--fold-color-border)}.dir-vertical.style-underline .tab-bar-item{border-bottom:none;border-left:2px solid transparent;margin-bottom:0;margin-right:-1px}.dir-vertical.style-underline .tab-bar-item.is-active{border-left-color:var(--fold-color-primary)}.dir-vertical.style-fill .tab-bar-item.is-active{background:var(--fold-color-primary-surface);color:var(--fold-color-primary-text);font-weight:600}.is-collapsed .tab-bar-item{position:relative;flex:0 0 auto;gap:0}.is-collapsed .tab-bar-item:not(.is-active) .tab-bar-badge{display:none}.is-collapsed .tab-bar-item.is-active{flex:1 1 auto;min-width:0;gap:var(--fold-space-sm)}.is-collapsed .tab-bar-item.is-active .tab-bar-label{overflow:hidden;text-overflow:ellipsis}.dir-vertical.is-collapsed .tab-bar-item .tab-bar-icon~.tab-bar-label,.is-collapsed:not(.dir-vertical) .tab-bar-item:not(.is-active) .tab-bar-icon~.tab-bar-label{position:absolute;padding:var(--fold-space-xs) var(--fold-space-sm);border-radius:var(--fold-radius-sm);background:var(--fold-color-glass);border:1px solid var(--fold-color-glass-border);color:var(--fold-color-text);font-size:var(--fold-text-xs);font-weight:500;white-space:nowrap;opacity:0;pointer-events:none;box-shadow:var(--fold-shadow-md);transition:opacity var(--fold-motion-fast);z-index:100}.dir-vertical.is-collapsed .tab-bar-item .tab-bar-icon~.tab-bar-label{left:calc(100% + 8px);top:50%;transform:translateY(-50%)}.is-collapsed:not(.dir-vertical) .tab-bar-item:not(.is-active) .tab-bar-icon~.tab-bar-label{top:calc(100% + 6px);left:50%;transform:translate(-50%)}.dir-vertical.is-collapsed .tab-bar-item:hover .tab-bar-icon~.tab-bar-label,.dir-vertical.is-collapsed .tab-bar-item:focus-visible .tab-bar-icon~.tab-bar-label,.is-collapsed:not(.dir-vertical) .tab-bar-item:not(.is-active):hover .tab-bar-icon~.tab-bar-label,.is-collapsed:not(.dir-vertical) .tab-bar-item:not(.is-active):focus-visible .tab-bar-icon~.tab-bar-label{opacity:1}.dir-vertical.is-collapsed{align-items:center}.dir-vertical.is-collapsed .tab-bar-item,.dir-vertical.is-collapsed .tab-bar-item.is-active{justify-content:center;gap:0}.dir-vertical.is-collapsed .tab-bar-item .tab-bar-badge{display:block;position:absolute;top:-2px;right:-2px;transform:scale(.85);transform-origin:top right;pointer-events:none}:host:has(.is-collapsed){overflow:visible}.tab-bar-icon{flex-shrink:0}.tab-bar-initial{display:grid;place-items:center;width:18px;height:18px;border-radius:var(--fold-radius-sm);font-size:11px;font-weight:700;line-height:1;text-transform:uppercase}.tab-bar-badge{flex-shrink:0;font-variant-numeric:tabular-nums}.tab-bar-item:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px;border-radius:var(--fold-radius-sm)}:host(.is-standalone.is-horizontal){margin-block-end:var(--fold-nav-layout-gap, var(--fold-space-lg, 16px))}\n"] }]
|
|
7895
10195
|
}], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], activeKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeKey", required: false }] }, { type: i0.Output, args: ["activeKeyChange"] }], activeStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeStyle", required: false }] }], direction: [{ type: i0.Input, args: [{ isSignal: true, alias: "direction", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], background: [{ type: i0.Input, args: [{ isSignal: true, alias: "background", required: false }] }] } });
|
|
7896
10196
|
|
|
7897
10197
|
/**
|
|
@@ -9025,6 +11325,528 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
9025
11325
|
args: [{ selector: "fold-dropdown", standalone: true, imports: [FoldPopoverComponent], template: "<fold-popover\n [placement]=\"placement()\"\n [offset]=\"offset()\"\n [autoFocus]=\"false\"\n [(open)]=\"open\"\n>\n <ng-content select=\"[foldPopoverTrigger]\" />\n <div\n foldPopoverPanel\n class=\"fdrop-menu\"\n role=\"menu\"\n [attr.aria-label]=\"ariaLabel() ?? null\"\n (keydown)=\"onMenuKeydown($event)\"\n (click)=\"onMenuActivate($event)\"\n >\n <ng-content />\n </div>\n</fold-popover>\n", styles: [":host{display:contents}.fdrop-menu{display:flex;flex-direction:column;gap:1px;min-width:11rem;padding:var(--fold-space-xs)}\n"] }]
|
|
9026
11326
|
}], ctorParameters: () => [], propDecorators: { placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], items: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => FoldDropdownItemComponent), { isSignal: true }] }] } });
|
|
9027
11327
|
|
|
11328
|
+
/**
|
|
11329
|
+
* `date` moved by `delta` periods **in the units the view reads in** — a month
|
|
11330
|
+
* at a time in the month view, a week in the week view, a day in the day view.
|
|
11331
|
+
* The list view follows the month, since that is the window it lists.
|
|
11332
|
+
*
|
|
11333
|
+
* An unrecognised view pages by month, the safest of the four to be wrong
|
|
11334
|
+
* about: it always lands on a real date the other views can also show.
|
|
11335
|
+
*/
|
|
11336
|
+
function foldShiftDate(view, date, delta, weekStartsOn = "mon") {
|
|
11337
|
+
if (view === "day") {
|
|
11338
|
+
return foldAddDays(date, delta);
|
|
11339
|
+
}
|
|
11340
|
+
if (view === "week") {
|
|
11341
|
+
return foldAddDays(foldStartOfWeek(date, weekStartsOn), delta * 7);
|
|
11342
|
+
}
|
|
11343
|
+
return foldAddMonths(foldStartOfMonth(date), delta);
|
|
11344
|
+
}
|
|
11345
|
+
/**
|
|
11346
|
+
* The days `view` covers around `date` — what a caller passes to its data
|
|
11347
|
+
* source, and what the list view scopes itself to.
|
|
11348
|
+
*
|
|
11349
|
+
* The **month** view's range is wider than its month: the grid paints the days
|
|
11350
|
+
* either side of it, and they carry their events too. The **list** view's is
|
|
11351
|
+
* not, because a list has no padding days — an April row in a May list is an
|
|
11352
|
+
* event the reader did not ask for.
|
|
11353
|
+
*/
|
|
11354
|
+
function foldRangeForView(view, date, weekStartsOn = "mon") {
|
|
11355
|
+
if (view === "day") {
|
|
11356
|
+
return { from: date, to: date };
|
|
11357
|
+
}
|
|
11358
|
+
if (view === "week") {
|
|
11359
|
+
const start = foldStartOfWeek(date, weekStartsOn);
|
|
11360
|
+
return { from: start, to: foldAddDays(start, 6) };
|
|
11361
|
+
}
|
|
11362
|
+
if (view === "list") {
|
|
11363
|
+
return { from: foldStartOfMonth(date), to: foldEndOfMonth(date) };
|
|
11364
|
+
}
|
|
11365
|
+
const first = foldStartOfWeek(foldStartOfMonth(date), weekStartsOn);
|
|
11366
|
+
const last = foldAddDays(foldStartOfWeek(foldEndOfMonth(date), weekStartsOn), 6);
|
|
11367
|
+
return { from: first, to: last };
|
|
11368
|
+
}
|
|
11369
|
+
/**
|
|
11370
|
+
* The heading for the period on display, localised through `Intl` — "May 2026",
|
|
11371
|
+
* "18–24 May 2026", "Monday 18 May 2026". A week spanning two months or two
|
|
11372
|
+
* years names both ends, which `Intl`'s range formatter handles per locale.
|
|
11373
|
+
*/
|
|
11374
|
+
function foldViewTitle(view, date, locale, weekStartsOn = "mon") {
|
|
11375
|
+
if (view === "day") {
|
|
11376
|
+
return foldCalendarFormatter(locale, "dateFullWeekday").format(foldToNativeDate(date));
|
|
11377
|
+
}
|
|
11378
|
+
if (view === "week") {
|
|
11379
|
+
const start = foldStartOfWeek(date, weekStartsOn);
|
|
11380
|
+
return foldFormatRange(start, foldAddDays(start, 6), locale, "dayMonthYear");
|
|
11381
|
+
}
|
|
11382
|
+
return foldCalendarFormatter(locale, "monthYear").format(foldToNativeDate(date));
|
|
11383
|
+
}
|
|
11384
|
+
|
|
11385
|
+
/**
|
|
11386
|
+
* `<fold-calendar-toolbar>` — the chrome above a calendar: jump to today, page
|
|
11387
|
+
* back and forward, the period's name, and the view switch.
|
|
11388
|
+
*
|
|
11389
|
+
* It owns no data. Both pieces of state it touches are two-way `model`s, so
|
|
11390
|
+
* the page binds the same `date` and `view` it hands to whichever view is on
|
|
11391
|
+
* screen, and paging keeps working without a single output handler.
|
|
11392
|
+
*
|
|
11393
|
+
* The step matches the reading: a month at a time under the month view, a week
|
|
11394
|
+
* under the week view, a day under the day view — see `foldShiftDate`.
|
|
11395
|
+
*
|
|
11396
|
+
* ## Inputs
|
|
11397
|
+
* | Input | Type | Default | Meaning |
|
|
11398
|
+
* |----------------|--------------------------|---------|---------|
|
|
11399
|
+
* | `date` | `FoldCalendarDate` | — | **Two-way.** The period on display. |
|
|
11400
|
+
* | `view` | `FoldCalendarView` | `'month'` | **Two-way.** Which reading is on screen. |
|
|
11401
|
+
* | `views` | `(FoldCalendarView \| FoldCalendarViewOption)[]` | all four | Which switches to offer; pass fewer to drop one, or `{ value, label }` to add your own. |
|
|
11402
|
+
* | `today` | `FoldCalendarDate` | local clock | Where the "today" button jumps to. |
|
|
11403
|
+
* | `weekStartsOn` | `FoldWeekday` | the locale's | How the week view snaps and names itself. |
|
|
11404
|
+
* | `locale` | `string` | runtime | Drives the title through `Intl`. |
|
|
11405
|
+
* | `labels` | `Partial<FoldCalendarLabels>` | — | Per-instance label overrides. |
|
|
11406
|
+
*
|
|
11407
|
+
* ## Slots & templates
|
|
11408
|
+
* | Selector | Region |
|
|
11409
|
+
* |------------------------|--------|
|
|
11410
|
+
* | `[actions]` | Trailing edge — the page's own buttons ("New event", filters). |
|
|
11411
|
+
* | `ng-template[foldCalendarTitle]` | Replaces the `<h2>` title — reword it, or give it the heading level the page's outline needs. |
|
|
11412
|
+
*
|
|
11413
|
+
* ## Theming
|
|
11414
|
+
* | CSS variable | Default | Sets |
|
|
11415
|
+
* |------------------------------------|---------|------|
|
|
11416
|
+
* | `--fold-calendar-toolbar-step-size`| `28px` | The back/forward buttons. |
|
|
11417
|
+
*
|
|
11418
|
+
* ## Extending
|
|
11419
|
+
* `views` accepts an app's own reading — `[views]="['month', {value: 'rooms', label: 'Rooms'}]"`.
|
|
11420
|
+
* Paging and titling an unrecognised view fall back to month semantics, so a
|
|
11421
|
+
* custom view still lands on real dates.
|
|
11422
|
+
*
|
|
11423
|
+
* ## Accessibility
|
|
11424
|
+
* The title is the toolbar's live heading: it carries `aria-live="polite"`, so
|
|
11425
|
+
* paging announces the period a keyboard user just moved to instead of leaving
|
|
11426
|
+
* the change silent.
|
|
11427
|
+
*
|
|
11428
|
+
* @selector `fold-calendar-toolbar`
|
|
11429
|
+
*
|
|
11430
|
+
* @example
|
|
11431
|
+
* ```html
|
|
11432
|
+
* <fold-calendar-toolbar [(date)]="date" [(view)]="view" [today]="today">
|
|
11433
|
+
* <button actions foldButton (click)="create()">New event</button>
|
|
11434
|
+
* </fold-calendar-toolbar>
|
|
11435
|
+
* ```
|
|
11436
|
+
*/
|
|
11437
|
+
class FoldCalendarToolbarComponent {
|
|
11438
|
+
/** The period on display. */
|
|
11439
|
+
date = model.required(/* @ts-ignore */
|
|
11440
|
+
...(ngDevMode ? [{ debugName: "date" }] : /* istanbul ignore next */ []));
|
|
11441
|
+
/** Which reading is on screen. @default 'month' */
|
|
11442
|
+
view = model("month", /* @ts-ignore */
|
|
11443
|
+
...(ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []));
|
|
11444
|
+
/**
|
|
11445
|
+
* Which switches to offer — a built-in name, or `{ value, label }` for a view
|
|
11446
|
+
* the app owns. @default every built-in view
|
|
11447
|
+
*/
|
|
11448
|
+
views = input(["month", "week", "day", "list"], /* @ts-ignore */
|
|
11449
|
+
...(ngDevMode ? [{ debugName: "views" }] : /* istanbul ignore next */ []));
|
|
11450
|
+
/** The day the "today" button jumps to; defaults to the local clock. */
|
|
11451
|
+
today = input(/* @ts-ignore */
|
|
11452
|
+
...(ngDevMode ? [undefined, { debugName: "today" }] : /* istanbul ignore next */ []));
|
|
11453
|
+
/** Day the week starts on. @default the locale's own first day */
|
|
11454
|
+
weekStartsOn = input(/* @ts-ignore */
|
|
11455
|
+
...(ngDevMode ? [undefined, { debugName: "weekStartsOn" }] : /* istanbul ignore next */ []));
|
|
11456
|
+
/** Labels and locale — see the directive. */
|
|
11457
|
+
chrome = inject(FoldCalendarChromeDirective);
|
|
11458
|
+
/** The anchor in force: the caller's, else the one the locale declares. */
|
|
11459
|
+
anchor = computed(() => this.chrome.anchor(this.weekStartsOn()), /* @ts-ignore */
|
|
11460
|
+
...(ngDevMode ? [{ debugName: "anchor" }] : /* istanbul ignore next */ []));
|
|
11461
|
+
projectedTitle = contentChild(FoldCalendarTitleDirective, /* @ts-ignore */
|
|
11462
|
+
...(ngDevMode ? [{ debugName: "projectedTitle" }] : /* istanbul ignore next */ []));
|
|
11463
|
+
/** The custom title template, when one is projected. */
|
|
11464
|
+
titleContent = computed(() => this.projectedTitle()?.template ?? null, /* @ts-ignore */
|
|
11465
|
+
...(ngDevMode ? [{ debugName: "titleContent" }] : /* istanbul ignore next */ []));
|
|
11466
|
+
/** The period's name — "May 2026", "18–24 May 2026", "Wednesday 20 May 2026". */
|
|
11467
|
+
title = computed(() => foldViewTitle(this.view(), this.date(), this.chrome.locale(), this.anchor()), /* @ts-ignore */
|
|
11468
|
+
...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
|
|
11469
|
+
/** The period the title stands for — handed to a projected title template. */
|
|
11470
|
+
range = computed(() => foldRangeForView(this.view(), this.date(), this.anchor()), /* @ts-ignore */
|
|
11471
|
+
...(ngDevMode ? [{ debugName: "range" }] : /* istanbul ignore next */ []));
|
|
11472
|
+
options = computed(() => {
|
|
11473
|
+
const labels = this.chrome.l();
|
|
11474
|
+
const named = {
|
|
11475
|
+
month: labels.viewMonth,
|
|
11476
|
+
week: labels.viewWeek,
|
|
11477
|
+
day: labels.viewDay,
|
|
11478
|
+
list: labels.viewList,
|
|
11479
|
+
};
|
|
11480
|
+
return this.views().map((view) => typeof view === "string"
|
|
11481
|
+
? { value: view, label: named[view] ?? view }
|
|
11482
|
+
: { value: view.value, label: view.label });
|
|
11483
|
+
}, /* @ts-ignore */
|
|
11484
|
+
...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
11485
|
+
/** Pages by one period in the units the current view reads in. */
|
|
11486
|
+
shift(delta) {
|
|
11487
|
+
this.date.set(foldShiftDate(this.view(), this.date(), delta, this.anchor()));
|
|
11488
|
+
}
|
|
11489
|
+
goToday() {
|
|
11490
|
+
this.date.set(this.today() ?? foldToday());
|
|
11491
|
+
}
|
|
11492
|
+
/** The switch emits a bare `string`; only a view it actually offers is kept. */
|
|
11493
|
+
onViewChange(value) {
|
|
11494
|
+
const offered = this.options().some((option) => option.value === value);
|
|
11495
|
+
if (offered) {
|
|
11496
|
+
this.view.set(value);
|
|
11497
|
+
}
|
|
11498
|
+
}
|
|
11499
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11500
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: FoldCalendarToolbarComponent, isStandalone: true, selector: "fold-calendar-toolbar", inputs: { date: { classPropertyName: "date", publicName: "date", isSignal: true, isRequired: true, transformFunction: null }, view: { classPropertyName: "view", publicName: "view", isSignal: true, isRequired: false, transformFunction: null }, views: { classPropertyName: "views", publicName: "views", isSignal: true, isRequired: false, transformFunction: null }, today: { classPropertyName: "today", publicName: "today", isSignal: true, isRequired: false, transformFunction: null }, weekStartsOn: { classPropertyName: "weekStartsOn", publicName: "weekStartsOn", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { date: "dateChange", view: "viewChange" }, queries: [{ propertyName: "projectedTitle", first: true, predicate: FoldCalendarTitleDirective, descendants: true, isSignal: true }], hostDirectives: [{ directive: FoldCalendarChromeDirective, inputs: ["locale", "locale", "labels", "labels", "formats", "formats"] }], ngImport: i0, template: "<div class=\"foldcalt\">\n <div class=\"foldcalt-lead\">\n <button type=\"button\" class=\"foldcalt-today\" (click)=\"goToday()\">\n {{ chrome.l().todayAction }}\n </button>\n\n <div class=\"foldcalt-steps\">\n <button\n type=\"button\"\n class=\"foldcalt-step\"\n [attr.aria-label]=\"chrome.l().previousPeriod\"\n (click)=\"shift(-1)\"\n >\n <fold-icon name=\"chevron-left\" size=\"sm\" />\n </button>\n <button\n type=\"button\"\n class=\"foldcalt-step\"\n [attr.aria-label]=\"chrome.l().nextPeriod\"\n (click)=\"shift(1)\"\n >\n <fold-icon name=\"chevron-right\" size=\"sm\" />\n </button>\n </div>\n\n @if (titleContent(); as projected) {\n <div class=\"foldcalt-title\" aria-live=\"polite\">\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{\n $implicit: title(),\n from: range().from,\n to: range().to,\n view: view(),\n }\"\n />\n </div>\n } @else {\n <h2 class=\"foldcalt-title\" aria-live=\"polite\">{{ title() }}</h2>\n }\n </div>\n\n <div class=\"foldcalt-trail\">\n @if (options().length > 1) {\n <fold-view-toggle\n size=\"sm\"\n [ariaLabel]=\"chrome.l().viewSwitch\"\n [options]=\"options()\"\n [value]=\"view()\"\n (valueChange)=\"onViewChange($event)\"\n />\n }\n <ng-content select=\"[actions]\" />\n </div>\n</div>\n", styles: [":host{display:block;container-type:inline-size}.foldcalt{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--fold-space-md)}.foldcalt-lead,.foldcalt-trail{display:flex;align-items:center;gap:var(--fold-space-sm);min-width:0}.foldcalt-today{padding:var(--fold-space-xs) var(--fold-space-md);border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-sm);background:var(--fold-color-surface-raised);color:var(--fold-color-text);font:inherit;font-size:var(--fold-text-sm);font-weight:500;cursor:pointer;white-space:nowrap;transition:background-color var(--fold-motion-fast, .12s) ease}.foldcalt-today:hover{background:var(--fold-color-surface-hover)}.foldcalt-today:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:2px}.foldcalt-steps{display:inline-flex;gap:1px}.foldcalt-step{display:inline-flex;align-items:center;justify-content:center;width:var(--fold-calendar-toolbar-step-size, 28px);height:var(--fold-calendar-toolbar-step-size, 28px);padding:0;border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-sm);background:var(--fold-color-surface-raised);color:var(--fold-color-text-secondary);cursor:pointer;transition:background-color var(--fold-motion-fast, .12s) ease,color var(--fold-motion-fast, .12s) ease}.foldcalt-step:hover{background:var(--fold-color-surface-hover);color:var(--fold-color-text)}.foldcalt-step:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:2px}.foldcalt-title{margin:0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--fold-color-text);font-size:var(--fold-text-lg);font-weight:700;letter-spacing:-.01em}@container (max-width: 560px){.foldcalt-lead{flex-wrap:wrap}.foldcalt-title{order:-1;flex:1 0 100%}}@media(forced-colors:active){.foldcalt-today:focus-visible,.foldcalt-step:focus-visible{outline-color:Highlight}}@media(prefers-reduced-motion:reduce){.foldcalt-today,.foldcalt-step{transition:none}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: FoldIconComponent, selector: "fold-icon", inputs: ["name", "size", "tone", "title"] }, { kind: "component", type: FoldViewToggleComponent, selector: "fold-view-toggle", inputs: ["options", "value", "ariaLabel", "size", "activeStyle", "iconOnly"], outputs: ["valueChange"] }] });
|
|
11501
|
+
}
|
|
11502
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarToolbarComponent, decorators: [{
|
|
11503
|
+
type: Component,
|
|
11504
|
+
args: [{ selector: "fold-calendar-toolbar", standalone: true, imports: [NgTemplateOutlet, FoldIconComponent, FoldViewToggleComponent], hostDirectives: [
|
|
11505
|
+
{
|
|
11506
|
+
directive: FoldCalendarChromeDirective,
|
|
11507
|
+
inputs: ["locale", "labels", "formats"],
|
|
11508
|
+
},
|
|
11509
|
+
], template: "<div class=\"foldcalt\">\n <div class=\"foldcalt-lead\">\n <button type=\"button\" class=\"foldcalt-today\" (click)=\"goToday()\">\n {{ chrome.l().todayAction }}\n </button>\n\n <div class=\"foldcalt-steps\">\n <button\n type=\"button\"\n class=\"foldcalt-step\"\n [attr.aria-label]=\"chrome.l().previousPeriod\"\n (click)=\"shift(-1)\"\n >\n <fold-icon name=\"chevron-left\" size=\"sm\" />\n </button>\n <button\n type=\"button\"\n class=\"foldcalt-step\"\n [attr.aria-label]=\"chrome.l().nextPeriod\"\n (click)=\"shift(1)\"\n >\n <fold-icon name=\"chevron-right\" size=\"sm\" />\n </button>\n </div>\n\n @if (titleContent(); as projected) {\n <div class=\"foldcalt-title\" aria-live=\"polite\">\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{\n $implicit: title(),\n from: range().from,\n to: range().to,\n view: view(),\n }\"\n />\n </div>\n } @else {\n <h2 class=\"foldcalt-title\" aria-live=\"polite\">{{ title() }}</h2>\n }\n </div>\n\n <div class=\"foldcalt-trail\">\n @if (options().length > 1) {\n <fold-view-toggle\n size=\"sm\"\n [ariaLabel]=\"chrome.l().viewSwitch\"\n [options]=\"options()\"\n [value]=\"view()\"\n (valueChange)=\"onViewChange($event)\"\n />\n }\n <ng-content select=\"[actions]\" />\n </div>\n</div>\n", styles: [":host{display:block;container-type:inline-size}.foldcalt{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--fold-space-md)}.foldcalt-lead,.foldcalt-trail{display:flex;align-items:center;gap:var(--fold-space-sm);min-width:0}.foldcalt-today{padding:var(--fold-space-xs) var(--fold-space-md);border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-sm);background:var(--fold-color-surface-raised);color:var(--fold-color-text);font:inherit;font-size:var(--fold-text-sm);font-weight:500;cursor:pointer;white-space:nowrap;transition:background-color var(--fold-motion-fast, .12s) ease}.foldcalt-today:hover{background:var(--fold-color-surface-hover)}.foldcalt-today:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:2px}.foldcalt-steps{display:inline-flex;gap:1px}.foldcalt-step{display:inline-flex;align-items:center;justify-content:center;width:var(--fold-calendar-toolbar-step-size, 28px);height:var(--fold-calendar-toolbar-step-size, 28px);padding:0;border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-sm);background:var(--fold-color-surface-raised);color:var(--fold-color-text-secondary);cursor:pointer;transition:background-color var(--fold-motion-fast, .12s) ease,color var(--fold-motion-fast, .12s) ease}.foldcalt-step:hover{background:var(--fold-color-surface-hover);color:var(--fold-color-text)}.foldcalt-step:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:2px}.foldcalt-title{margin:0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--fold-color-text);font-size:var(--fold-text-lg);font-weight:700;letter-spacing:-.01em}@container (max-width: 560px){.foldcalt-lead{flex-wrap:wrap}.foldcalt-title{order:-1;flex:1 0 100%}}@media(forced-colors:active){.foldcalt-today:focus-visible,.foldcalt-step:focus-visible{outline-color:Highlight}}@media(prefers-reduced-motion:reduce){.foldcalt-today,.foldcalt-step{transition:none}}\n"] }]
|
|
11510
|
+
}], propDecorators: { date: [{ type: i0.Input, args: [{ isSignal: true, alias: "date", required: true }] }, { type: i0.Output, args: ["dateChange"] }], view: [{ type: i0.Input, args: [{ isSignal: true, alias: "view", required: false }] }, { type: i0.Output, args: ["viewChange"] }], views: [{ type: i0.Input, args: [{ isSignal: true, alias: "views", required: false }] }], today: [{ type: i0.Input, args: [{ isSignal: true, alias: "today", required: false }] }], weekStartsOn: [{ type: i0.Input, args: [{ isSignal: true, alias: "weekStartsOn", required: false }] }], projectedTitle: [{ type: i0.ContentChild, args: [i0.forwardRef(() => FoldCalendarTitleDirective), { isSignal: true }] }] } });
|
|
11511
|
+
|
|
11512
|
+
/** Days the agenda returns when the caller does not say. */
|
|
11513
|
+
const DEFAULT_LIMIT = 8;
|
|
11514
|
+
/**
|
|
11515
|
+
* Whether an event is asking for something.
|
|
11516
|
+
*
|
|
11517
|
+
* `warning` and `alert` are the two tones that mean "attention" in this
|
|
11518
|
+
* system, so they are what `todo` keeps — the agenda reads the same semantic
|
|
11519
|
+
* scale the chips paint with, rather than inventing a second one. Pass
|
|
11520
|
+
* `isActionable` to define it otherwise.
|
|
11521
|
+
*/
|
|
11522
|
+
function foldIsActionable(event) {
|
|
11523
|
+
return event.tone === "warning" || event.tone === "alert";
|
|
11524
|
+
}
|
|
11525
|
+
/** At least one day, and never `NaN` — `slice(0, NaN)` returns nothing at all. */
|
|
11526
|
+
function clampLimit(limit) {
|
|
11527
|
+
if (limit === undefined || !Number.isFinite(limit)) {
|
|
11528
|
+
return DEFAULT_LIMIT;
|
|
11529
|
+
}
|
|
11530
|
+
return Math.max(1, Math.trunc(limit));
|
|
11531
|
+
}
|
|
11532
|
+
/**
|
|
11533
|
+
* Events still ahead of `from`, grouped by day and capped to `limit` days.
|
|
11534
|
+
*
|
|
11535
|
+
* An event already running is grouped under `from` rather than its real start:
|
|
11536
|
+
* a three-week absence that began last Monday belongs at the top of "what's
|
|
11537
|
+
* next", not buried in a past day the rail never shows.
|
|
11538
|
+
*/
|
|
11539
|
+
function foldBuildAgenda(events, options) {
|
|
11540
|
+
const { from } = options;
|
|
11541
|
+
const mode = options.mode ?? "all";
|
|
11542
|
+
const limit = clampLimit(options.limit);
|
|
11543
|
+
const actionable = options.isActionable ?? foldIsActionable;
|
|
11544
|
+
const ahead = events
|
|
11545
|
+
.filter((event) => event.end >= from)
|
|
11546
|
+
.filter((event) => mode !== "todo" || actionable(event));
|
|
11547
|
+
const ordered = [...ahead].sort((a, b) => a.start < b.start ? -1 : a.start > b.start ? 1 : 0);
|
|
11548
|
+
const byDay = new Map();
|
|
11549
|
+
for (const event of ordered) {
|
|
11550
|
+
const key = event.start < from ? from : event.start;
|
|
11551
|
+
const bucket = byDay.get(key);
|
|
11552
|
+
if (bucket === undefined) {
|
|
11553
|
+
byDay.set(key, [event]);
|
|
11554
|
+
continue;
|
|
11555
|
+
}
|
|
11556
|
+
bucket.push(event);
|
|
11557
|
+
}
|
|
11558
|
+
const days = [...byDay.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
11559
|
+
return {
|
|
11560
|
+
groups: days
|
|
11561
|
+
.slice(0, limit)
|
|
11562
|
+
.map(([date, group]) => ({ date, events: group })),
|
|
11563
|
+
truncated: Math.max(0, days.length - limit),
|
|
11564
|
+
};
|
|
11565
|
+
}
|
|
11566
|
+
/**
|
|
11567
|
+
* How many events are asking for attention from `from` on — the rail's badge.
|
|
11568
|
+
*
|
|
11569
|
+
* Takes the same `isActionable` the agenda does, so a badge can never disagree
|
|
11570
|
+
* with the list under it.
|
|
11571
|
+
*/
|
|
11572
|
+
function foldCountActionable(events, from, isActionable = foldIsActionable) {
|
|
11573
|
+
return events.filter((event) => event.end >= from && isActionable(event))
|
|
11574
|
+
.length;
|
|
11575
|
+
}
|
|
11576
|
+
|
|
11577
|
+
/** Days ahead within which a weekday name still reads faster than a date. */
|
|
11578
|
+
const NEAR_HORIZON = 7;
|
|
11579
|
+
/**
|
|
11580
|
+
* `<fold-calendar-agenda>` — a rail of what is still ahead, grouped by day.
|
|
11581
|
+
*
|
|
11582
|
+
* The counterpart to the grids: they answer "what does this month look like",
|
|
11583
|
+
* this answers "what do I do next". It carries a **`todo` slice** that keeps
|
|
11584
|
+
* only the events asking for attention (the `warning` and `alert` tones — the
|
|
11585
|
+
* same scale the chips paint with), so the rail doubles as a work queue with a
|
|
11586
|
+
* count.
|
|
11587
|
+
*
|
|
11588
|
+
* ## Inputs
|
|
11589
|
+
* | Input | Type | Default | Meaning |
|
|
11590
|
+
* |-------------|--------------------------|---------|---------|
|
|
11591
|
+
* | `from` | `FoldCalendarDate` | — | Where "ahead" starts; anything ending before it is past. |
|
|
11592
|
+
* | `events` | `FoldCalendarEvent<T>[]` | `[]` | Source events. |
|
|
11593
|
+
* | `mode` | `'todo' \| 'all'` | `'todo'` | **Two-way.** Which slice is shown. |
|
|
11594
|
+
* | `collapsed` | `boolean` | `false` | **Two-way.** Collapsed to a spine — persist it if you want it to stick. |
|
|
11595
|
+
* | `limit` | `number` | `8` | Days to show at most; what it cuts off is announced, not swallowed. |
|
|
11596
|
+
* | `isActionable` | `(event) => boolean` | tones | What the `todo` slice and the badge both count. |
|
|
11597
|
+
* | `locale` | `string` | runtime | Drives the day names through `Intl`. |
|
|
11598
|
+
* | `labels` | `Partial<FoldCalendarLabels>` | — | Per-instance label overrides. |
|
|
11599
|
+
*
|
|
11600
|
+
* ## Outputs
|
|
11601
|
+
* | Output | Payload | Fires on |
|
|
11602
|
+
* |--------------|------------------------|----------|
|
|
11603
|
+
* | `dayClick` | `FoldCalendarDate` | A day heading activated. |
|
|
11604
|
+
* | `eventClick` | `FoldCalendarEvent<T>` | A row activated. |
|
|
11605
|
+
*
|
|
11606
|
+
* ## Templates
|
|
11607
|
+
* | Directive | Replaces |
|
|
11608
|
+
* |-----------------------|----------|
|
|
11609
|
+
* | `foldCalendarEvent` | The inside of a row. |
|
|
11610
|
+
* | `foldCalendarHeading` | The day heading — where a per-day count or action goes. |
|
|
11611
|
+
*
|
|
11612
|
+
* ## Theming
|
|
11613
|
+
* | CSS variable | Default | Sets |
|
|
11614
|
+
* |-----------------------------------------|---------|------|
|
|
11615
|
+
* | `--fold-calendar-agenda-width` | `320px` | Width of the expanded rail. |
|
|
11616
|
+
* | `--fold-calendar-agenda-height` | `100%` | Height — match a grid beside it, or pin it. |
|
|
11617
|
+
* | `--fold-calendar-agenda-spine-width` | `44px` | Width once collapsed. |
|
|
11618
|
+
* | `--fold-calendar-agenda-badge-size` | `20px` | The count badge. |
|
|
11619
|
+
* | `--fold-calendar-agenda-control-size` | `26px` | The collapse control. |
|
|
11620
|
+
*
|
|
11621
|
+
* A day within the next week is named relatively — "Today", "Tomorrow", then
|
|
11622
|
+
* its weekday — because that reads faster than a date at this distance.
|
|
11623
|
+
*
|
|
11624
|
+
* @selector `fold-calendar-agenda`
|
|
11625
|
+
*
|
|
11626
|
+
* @example
|
|
11627
|
+
* ```html
|
|
11628
|
+
* <fold-calendar-agenda
|
|
11629
|
+
* [from]="today"
|
|
11630
|
+
* [events]="events()"
|
|
11631
|
+
* [(mode)]="agendaMode"
|
|
11632
|
+
* [(collapsed)]="agendaCollapsed"
|
|
11633
|
+
* (eventClick)="open($event)"
|
|
11634
|
+
* />
|
|
11635
|
+
* ```
|
|
11636
|
+
*/
|
|
11637
|
+
class FoldCalendarAgendaComponent {
|
|
11638
|
+
/** The day "ahead" starts from. */
|
|
11639
|
+
from = input.required(/* @ts-ignore */
|
|
11640
|
+
...(ngDevMode ? [{ debugName: "from" }] : /* istanbul ignore next */ []));
|
|
11641
|
+
/** Source events. */
|
|
11642
|
+
events = input([], /* @ts-ignore */
|
|
11643
|
+
...(ngDevMode ? [{ debugName: "events" }] : /* istanbul ignore next */ []));
|
|
11644
|
+
/** Which slice is shown. @default 'todo' */
|
|
11645
|
+
mode = model("todo", /* @ts-ignore */
|
|
11646
|
+
...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
11647
|
+
/** Collapsed to a spine. @default false */
|
|
11648
|
+
collapsed = model(false, /* @ts-ignore */
|
|
11649
|
+
...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
|
|
11650
|
+
/** Days to show at most. @default 8 */
|
|
11651
|
+
limit = input(8, { ...(ngDevMode ? { debugName: "limit" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
11652
|
+
/**
|
|
11653
|
+
* What counts as asking for attention — the `todo` slice **and** the badge,
|
|
11654
|
+
* from one definition, so the two can never disagree.
|
|
11655
|
+
* @default the `warning` and `alert` tones
|
|
11656
|
+
*/
|
|
11657
|
+
isActionable = input(foldIsActionable, /* @ts-ignore */
|
|
11658
|
+
...(ngDevMode ? [{ debugName: "isActionable" }] : /* istanbul ignore next */ []));
|
|
11659
|
+
/** A day heading was activated. */
|
|
11660
|
+
dayClick = output();
|
|
11661
|
+
/** A row was activated. */
|
|
11662
|
+
eventClick = output();
|
|
11663
|
+
/** Labels, locale and the projected chip template — see the directive. */
|
|
11664
|
+
chrome = inject(FoldCalendarChromeDirective);
|
|
11665
|
+
projectedHeading = contentChild(FoldCalendarHeadingDirective, /* @ts-ignore */
|
|
11666
|
+
...(ngDevMode ? [{ debugName: "projectedHeading" }] : /* istanbul ignore next */ []));
|
|
11667
|
+
/** The custom day-heading template, when one is projected. */
|
|
11668
|
+
headingContent = computed(() => this.projectedHeading()?.template ?? null, /* @ts-ignore */
|
|
11669
|
+
...(ngDevMode ? [{ debugName: "headingContent" }] : /* istanbul ignore next */ []));
|
|
11670
|
+
agenda = computed(() => foldBuildAgenda(this.events(), {
|
|
11671
|
+
from: this.from(),
|
|
11672
|
+
mode: this.mode(),
|
|
11673
|
+
limit: this.limit(),
|
|
11674
|
+
isActionable: this.isActionable(),
|
|
11675
|
+
}), /* @ts-ignore */
|
|
11676
|
+
...(ngDevMode ? [{ debugName: "agenda" }] : /* istanbul ignore next */ []));
|
|
11677
|
+
groups = computed(() => this.agenda().groups, /* @ts-ignore */
|
|
11678
|
+
...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
|
|
11679
|
+
/** Days `limit` cut off — "there is more" is not the same as "there is none". */
|
|
11680
|
+
truncated = computed(() => this.agenda().truncated, /* @ts-ignore */
|
|
11681
|
+
...(ngDevMode ? [{ debugName: "truncated" }] : /* istanbul ignore next */ []));
|
|
11682
|
+
/** How much is asking for attention — the rail's badge, in either mode. */
|
|
11683
|
+
todoCount = computed(() => foldCountActionable(this.events(), this.from(), this.isActionable()), /* @ts-ignore */
|
|
11684
|
+
...(ngDevMode ? [{ debugName: "todoCount" }] : /* istanbul ignore next */ []));
|
|
11685
|
+
/** The rail's own name, and the one the collapsed spine repeats. */
|
|
11686
|
+
title = computed(() => this.mode() === "todo"
|
|
11687
|
+
? this.chrome.l().agendaTodo
|
|
11688
|
+
: this.chrome.l().agendaUpcoming, /* @ts-ignore */
|
|
11689
|
+
...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
|
|
11690
|
+
emptyMessage = computed(() => this.mode() === "todo"
|
|
11691
|
+
? this.chrome.l().agendaEmptyTodo
|
|
11692
|
+
: this.chrome.l().agendaEmptyUpcoming, /* @ts-ignore */
|
|
11693
|
+
...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
|
|
11694
|
+
/** Day of the month, for the rail's number. */
|
|
11695
|
+
dayNumber(group) {
|
|
11696
|
+
return group.date.slice(8, 10);
|
|
11697
|
+
}
|
|
11698
|
+
/**
|
|
11699
|
+
* "Today", "Tomorrow", then the weekday for the rest of the week — a name
|
|
11700
|
+
* beats a date while the day is still near.
|
|
11701
|
+
*/
|
|
11702
|
+
relativeName(group) {
|
|
11703
|
+
const offset = foldDaysBetween(this.from(), group.date);
|
|
11704
|
+
if (offset === 0) {
|
|
11705
|
+
return this.chrome.l().relativeToday;
|
|
11706
|
+
}
|
|
11707
|
+
if (offset === 1) {
|
|
11708
|
+
return this.chrome.l().relativeTomorrow;
|
|
11709
|
+
}
|
|
11710
|
+
return this.chrome.format(group.date, "weekdayLong");
|
|
11711
|
+
}
|
|
11712
|
+
/** The month, shown only once the weekday alone stops being enough. */
|
|
11713
|
+
monthName(group) {
|
|
11714
|
+
if (foldDaysBetween(this.from(), group.date) < NEAR_HORIZON) {
|
|
11715
|
+
return "";
|
|
11716
|
+
}
|
|
11717
|
+
return this.chrome.format(group.date, "monthShort");
|
|
11718
|
+
}
|
|
11719
|
+
toggleMode(mode) {
|
|
11720
|
+
this.mode.set(mode);
|
|
11721
|
+
}
|
|
11722
|
+
toggleCollapsed() {
|
|
11723
|
+
this.collapsed.set(!this.collapsed());
|
|
11724
|
+
}
|
|
11725
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarAgendaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11726
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: FoldCalendarAgendaComponent, isStandalone: true, selector: "fold-calendar-agenda", inputs: { from: { classPropertyName: "from", publicName: "from", isSignal: true, isRequired: true, transformFunction: null }, events: { classPropertyName: "events", publicName: "events", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, limit: { classPropertyName: "limit", publicName: "limit", isSignal: true, isRequired: false, transformFunction: null }, isActionable: { classPropertyName: "isActionable", publicName: "isActionable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { mode: "modeChange", collapsed: "collapsedChange", dayClick: "dayClick", eventClick: "eventClick" }, queries: [{ propertyName: "projectedHeading", first: true, predicate: FoldCalendarHeadingDirective, descendants: true, isSignal: true }], hostDirectives: [{ directive: FoldCalendarChromeDirective, inputs: ["locale", "locale", "labels", "labels", "formats", "formats"] }], ngImport: i0, template: "@if (collapsed()) {\n <aside class=\"foldcala is-collapsed\" [attr.aria-label]=\"chrome.l().agenda\">\n <button\n type=\"button\"\n class=\"foldcala-spine\"\n [attr.aria-expanded]=\"false\"\n [attr.aria-label]=\"chrome.l().expand\"\n (click)=\"toggleCollapsed()\"\n >\n <fold-icon name=\"inbox\" size=\"sm\" />\n @if (todoCount() > 0) {\n <span class=\"foldcala-spine-count\">{{ todoCount() }}</span>\n }\n <!-- The spine names what opening the rail will show, not always \"todo\". -->\n <span class=\"foldcala-spine-label\">{{ title() }}</span>\n </button>\n </aside>\n} @else {\n <aside class=\"foldcala\" [attr.aria-label]=\"chrome.l().agenda\">\n <header class=\"foldcala-head\">\n <span class=\"foldcala-title\">\n {{ title() }}\n @if (todoCount() > 0) {\n <span class=\"foldcala-count\">{{ todoCount() }}</span>\n }\n </span>\n\n <span class=\"foldcala-head-right\">\n <!-- Its own name: reusing the rail's would nest two regions under one. -->\n <span\n class=\"foldcala-modes\"\n role=\"group\"\n [attr.aria-label]=\"chrome.l().agendaModes\"\n >\n <button\n type=\"button\"\n [class.is-on]=\"mode() === 'todo'\"\n [attr.aria-pressed]=\"mode() === 'todo'\"\n (click)=\"toggleMode('todo')\"\n >\n {{ chrome.l().agendaTodo }}\n </button>\n <button\n type=\"button\"\n [class.is-on]=\"mode() === 'all'\"\n [attr.aria-pressed]=\"mode() === 'all'\"\n (click)=\"toggleMode('all')\"\n >\n {{ chrome.l().agendaUpcoming }}\n </button>\n </span>\n <button\n type=\"button\"\n class=\"foldcala-collapse\"\n [attr.aria-expanded]=\"true\"\n [attr.aria-label]=\"chrome.l().collapse\"\n (click)=\"toggleCollapsed()\"\n >\n <fold-icon name=\"chevron-right\" size=\"sm\" />\n </button>\n </span>\n </header>\n\n <div class=\"foldcala-body\">\n @if (groups().length === 0) {\n <p class=\"foldcala-empty\">{{ emptyMessage() }}</p>\n } @else {\n @for (group of groups(); track group.date) {\n <section class=\"foldcala-group\">\n <button\n type=\"button\"\n class=\"foldcala-date\"\n (click)=\"dayClick.emit(group.date)\"\n >\n @if (headingContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{\n $implicit: group.date,\n events: group.events,\n }\"\n />\n } @else {\n <span class=\"foldcala-daynum\">{{ dayNumber(group) }}</span>\n <span class=\"foldcala-dmeta\">\n <span class=\"foldcala-dname\">{{ relativeName(group) }}</span>\n @if (monthName(group)) {\n <span class=\"foldcala-dmonth\">{{ monthName(group) }}</span>\n }\n </span>\n }\n </button>\n\n <ul class=\"foldcala-list\">\n @for (event of group.events; track event.id) {\n <li>\n <button\n type=\"button\"\n class=\"foldcal-chip\"\n [attr.data-tone]=\"event.tone ?? 'neutral'\"\n (click)=\"eventClick.emit(event)\"\n >\n @if (chrome.eventContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{\n $implicit: event,\n band: null,\n }\"\n />\n } @else {\n <span class=\"foldcal-chip-bar\"></span>\n @if (event.icon; as icon) {\n <fold-icon\n class=\"foldcal-chip-icon\"\n [name]=\"icon\"\n size=\"sm\"\n />\n }\n <span class=\"foldcal-chip-body\">\n <span class=\"foldcal-chip-label\">{{\n event.label\n }}</span>\n @if (event.subline) {\n <span class=\"foldcal-chip-sub\">{{\n event.subline\n }}</span>\n }\n </span>\n }\n </button>\n </li>\n }\n </ul>\n </section>\n }\n\n <!-- `limit` stopped short of what is ahead: say so, rather than\n letting the rail look like the end of the story. -->\n @if (truncated(); as more) {\n <p class=\"foldcala-more\">{{ chrome.l().agendaMore(more) }}</p>\n }\n }\n </div>\n </aside>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block}.foldcal-chip{position:relative;display:flex;align-items:stretch;gap:var(--fold-space-sm);width:100%;padding:var(--fold-space-xs) var(--fold-space-sm);padding-inline-start:0;border:0;border-radius:var(--fold-calendar-band-radius, var(--fold-radius-sm));background:var(--fold-color-surface-raised);color:var(--fold-color-text);font:inherit;text-align:start;cursor:pointer;overflow:hidden;transition:background-color var(--fold-motion-fast) ease}.foldcal-chip:hover{background:var(--fold-color-surface-hover)}.foldcal-chip:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcal-chip-bar{flex:0 0 var(--fold-calendar-bar-width, 3px);align-self:stretch;background:var(--fold-color-text-muted)}.foldcal-chip-icon{flex:none;align-self:center;margin-inline-start:var(--fold-space-sm);color:var(--fold-color-text-secondary)}.foldcal-chip-body{display:flex;flex-direction:column;gap:1px;min-width:0}.foldcal-chip-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--fold-text-sm);font-weight:500}.foldcal-chip-sub{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs)}.foldcal-chip[data-tone=success] .foldcal-chip-bar{background:var(--fold-color-success)}.foldcal-chip[data-tone=warning] .foldcal-chip-bar{background:var(--fold-color-warning)}.foldcal-chip[data-tone=alert] .foldcal-chip-bar{background:var(--fold-color-alert)}.foldcal-chip[data-tone=muted] .foldcal-chip-bar{background:var(--fold-color-text-faded)}.foldcal-chip[data-tone=muted] .foldcal-chip-label{color:var(--fold-color-text-secondary);text-decoration:line-through;text-decoration-thickness:1px}@media(forced-colors:active){.foldcal-chip{border:1px solid CanvasText}.foldcal-chip-bar{forced-color-adjust:none}}@media(prefers-reduced-motion:reduce){.foldcal-chip{transition:none}}.foldcala{display:flex;flex-direction:column;min-height:0;width:var(--fold-calendar-agenda-width, 320px);height:var(--fold-calendar-agenda-height, 100%);border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-lg);background:var(--fold-color-surface-card);overflow:clip}.foldcala.is-collapsed{width:var(--fold-calendar-agenda-spine-width, 44px)}.foldcala-spine{flex:1;display:flex;flex-direction:column;align-items:center;gap:var(--fold-space-md);padding:var(--fold-space-md) 0;border:0;background:transparent;color:var(--fold-color-text-secondary);font:inherit;cursor:pointer}.foldcala-spine:hover{background:var(--fold-color-surface-hover);color:var(--fold-color-text)}.foldcala-spine:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcala-spine-label{writing-mode:vertical-rl;font-size:var(--fold-text-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase}.foldcala-spine-count{display:inline-flex;align-items:center;justify-content:center;min-width:var(--fold-calendar-agenda-badge-size, 20px);padding:0 var(--fold-space-xs);border-radius:var(--fold-radius-pill);background:var(--fold-color-warning-surface);color:var(--fold-color-warning-text);font-size:var(--fold-text-xs);font-weight:700;font-variant-numeric:tabular-nums}.foldcala-head{display:flex;align-items:center;justify-content:space-between;gap:var(--fold-space-sm);padding:var(--fold-space-md);border-bottom:1px solid var(--fold-color-border-subtle)}.foldcala-title{display:inline-flex;align-items:center;gap:var(--fold-space-sm);color:var(--fold-color-text);font-size:var(--fold-text-sm);font-weight:700}.foldcala-count{display:inline-flex;align-items:center;justify-content:center;min-width:var(--fold-calendar-agenda-badge-size, 20px);padding:0 var(--fold-space-xs);border-radius:var(--fold-radius-pill);background:var(--fold-color-warning-surface);color:var(--fold-color-warning-text);font-size:var(--fold-text-xs);font-weight:700;font-variant-numeric:tabular-nums}.foldcala-head-right{display:inline-flex;align-items:center;gap:var(--fold-space-xs)}.foldcala-modes{display:inline-flex;padding:2px;border-radius:var(--fold-radius-sm);background:var(--fold-color-surface-sunken)}.foldcala-modes button{padding:2px var(--fold-space-sm);border:0;border-radius:var(--fold-radius-xs);background:transparent;color:var(--fold-color-text-secondary);font:inherit;font-size:var(--fold-text-xs);font-weight:500;cursor:pointer}.foldcala-modes button.is-on{background:var(--fold-color-surface-raised);color:var(--fold-color-text)}.foldcala-modes button:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:1px}.foldcala-collapse{display:inline-flex;align-items:center;justify-content:center;width:var(--fold-calendar-agenda-control-size, 26px);height:var(--fold-calendar-agenda-control-size, 26px);padding:0;border:1px solid var(--fold-color-border-subtle);border-radius:var(--fold-radius-sm);background:var(--fold-color-surface-subtle);color:var(--fold-color-text-secondary);cursor:pointer}.foldcala-collapse:hover{background:var(--fold-color-surface-hover);color:var(--fold-color-text)}.foldcala-collapse:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:1px}.foldcala-body{flex:1;min-height:0;overflow-y:auto;padding:var(--fold-space-sm)}.foldcala-group{display:grid;grid-template-columns:auto minmax(0,1fr);gap:var(--fold-space-sm);padding:var(--fold-space-sm) 0}.foldcala-group+.foldcala-group{border-top:1px solid var(--fold-color-border-subtle)}.foldcala-date{display:flex;align-items:baseline;gap:var(--fold-space-xs);padding:0;border:0;background:transparent;color:inherit;font:inherit;text-align:start;cursor:pointer}.foldcala-date:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:2px}.foldcala-daynum{color:var(--fold-color-text);font-size:var(--fold-text-lg);font-weight:700;font-variant-numeric:tabular-nums;line-height:1}.foldcala-dmeta{display:flex;flex-direction:column}.foldcala-dname{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);font-weight:600;letter-spacing:.04em;text-transform:uppercase}.foldcala-dmonth{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);text-transform:uppercase}.foldcala-list{display:flex;flex-direction:column;gap:var(--fold-space-xs);margin:0;padding:0;list-style:none}.foldcala-empty{margin:0;padding:var(--fold-space-xl) var(--fold-space-md);color:var(--fold-color-text-secondary);font-size:var(--fold-text-sm);text-align:center}@media(forced-colors:active){.foldcala-modes button.is-on{outline:1px solid Highlight}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: FoldIconComponent, selector: "fold-icon", inputs: ["name", "size", "tone", "title"] }] });
|
|
11727
|
+
}
|
|
11728
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarAgendaComponent, decorators: [{
|
|
11729
|
+
type: Component,
|
|
11730
|
+
args: [{ selector: "fold-calendar-agenda", standalone: true, imports: [NgTemplateOutlet, FoldIconComponent], hostDirectives: [
|
|
11731
|
+
{
|
|
11732
|
+
directive: FoldCalendarChromeDirective,
|
|
11733
|
+
inputs: ["locale", "labels", "formats"],
|
|
11734
|
+
},
|
|
11735
|
+
], template: "@if (collapsed()) {\n <aside class=\"foldcala is-collapsed\" [attr.aria-label]=\"chrome.l().agenda\">\n <button\n type=\"button\"\n class=\"foldcala-spine\"\n [attr.aria-expanded]=\"false\"\n [attr.aria-label]=\"chrome.l().expand\"\n (click)=\"toggleCollapsed()\"\n >\n <fold-icon name=\"inbox\" size=\"sm\" />\n @if (todoCount() > 0) {\n <span class=\"foldcala-spine-count\">{{ todoCount() }}</span>\n }\n <!-- The spine names what opening the rail will show, not always \"todo\". -->\n <span class=\"foldcala-spine-label\">{{ title() }}</span>\n </button>\n </aside>\n} @else {\n <aside class=\"foldcala\" [attr.aria-label]=\"chrome.l().agenda\">\n <header class=\"foldcala-head\">\n <span class=\"foldcala-title\">\n {{ title() }}\n @if (todoCount() > 0) {\n <span class=\"foldcala-count\">{{ todoCount() }}</span>\n }\n </span>\n\n <span class=\"foldcala-head-right\">\n <!-- Its own name: reusing the rail's would nest two regions under one. -->\n <span\n class=\"foldcala-modes\"\n role=\"group\"\n [attr.aria-label]=\"chrome.l().agendaModes\"\n >\n <button\n type=\"button\"\n [class.is-on]=\"mode() === 'todo'\"\n [attr.aria-pressed]=\"mode() === 'todo'\"\n (click)=\"toggleMode('todo')\"\n >\n {{ chrome.l().agendaTodo }}\n </button>\n <button\n type=\"button\"\n [class.is-on]=\"mode() === 'all'\"\n [attr.aria-pressed]=\"mode() === 'all'\"\n (click)=\"toggleMode('all')\"\n >\n {{ chrome.l().agendaUpcoming }}\n </button>\n </span>\n <button\n type=\"button\"\n class=\"foldcala-collapse\"\n [attr.aria-expanded]=\"true\"\n [attr.aria-label]=\"chrome.l().collapse\"\n (click)=\"toggleCollapsed()\"\n >\n <fold-icon name=\"chevron-right\" size=\"sm\" />\n </button>\n </span>\n </header>\n\n <div class=\"foldcala-body\">\n @if (groups().length === 0) {\n <p class=\"foldcala-empty\">{{ emptyMessage() }}</p>\n } @else {\n @for (group of groups(); track group.date) {\n <section class=\"foldcala-group\">\n <button\n type=\"button\"\n class=\"foldcala-date\"\n (click)=\"dayClick.emit(group.date)\"\n >\n @if (headingContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{\n $implicit: group.date,\n events: group.events,\n }\"\n />\n } @else {\n <span class=\"foldcala-daynum\">{{ dayNumber(group) }}</span>\n <span class=\"foldcala-dmeta\">\n <span class=\"foldcala-dname\">{{ relativeName(group) }}</span>\n @if (monthName(group)) {\n <span class=\"foldcala-dmonth\">{{ monthName(group) }}</span>\n }\n </span>\n }\n </button>\n\n <ul class=\"foldcala-list\">\n @for (event of group.events; track event.id) {\n <li>\n <button\n type=\"button\"\n class=\"foldcal-chip\"\n [attr.data-tone]=\"event.tone ?? 'neutral'\"\n (click)=\"eventClick.emit(event)\"\n >\n @if (chrome.eventContent(); as projected) {\n <ng-container\n [ngTemplateOutlet]=\"projected\"\n [ngTemplateOutletContext]=\"{\n $implicit: event,\n band: null,\n }\"\n />\n } @else {\n <span class=\"foldcal-chip-bar\"></span>\n @if (event.icon; as icon) {\n <fold-icon\n class=\"foldcal-chip-icon\"\n [name]=\"icon\"\n size=\"sm\"\n />\n }\n <span class=\"foldcal-chip-body\">\n <span class=\"foldcal-chip-label\">{{\n event.label\n }}</span>\n @if (event.subline) {\n <span class=\"foldcal-chip-sub\">{{\n event.subline\n }}</span>\n }\n </span>\n }\n </button>\n </li>\n }\n </ul>\n </section>\n }\n\n <!-- `limit` stopped short of what is ahead: say so, rather than\n letting the rail look like the end of the story. -->\n @if (truncated(); as more) {\n <p class=\"foldcala-more\">{{ chrome.l().agendaMore(more) }}</p>\n }\n }\n </div>\n </aside>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block}.foldcal-chip{position:relative;display:flex;align-items:stretch;gap:var(--fold-space-sm);width:100%;padding:var(--fold-space-xs) var(--fold-space-sm);padding-inline-start:0;border:0;border-radius:var(--fold-calendar-band-radius, var(--fold-radius-sm));background:var(--fold-color-surface-raised);color:var(--fold-color-text);font:inherit;text-align:start;cursor:pointer;overflow:hidden;transition:background-color var(--fold-motion-fast) ease}.foldcal-chip:hover{background:var(--fold-color-surface-hover)}.foldcal-chip:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcal-chip-bar{flex:0 0 var(--fold-calendar-bar-width, 3px);align-self:stretch;background:var(--fold-color-text-muted)}.foldcal-chip-icon{flex:none;align-self:center;margin-inline-start:var(--fold-space-sm);color:var(--fold-color-text-secondary)}.foldcal-chip-body{display:flex;flex-direction:column;gap:1px;min-width:0}.foldcal-chip-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--fold-text-sm);font-weight:500}.foldcal-chip-sub{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs)}.foldcal-chip[data-tone=success] .foldcal-chip-bar{background:var(--fold-color-success)}.foldcal-chip[data-tone=warning] .foldcal-chip-bar{background:var(--fold-color-warning)}.foldcal-chip[data-tone=alert] .foldcal-chip-bar{background:var(--fold-color-alert)}.foldcal-chip[data-tone=muted] .foldcal-chip-bar{background:var(--fold-color-text-faded)}.foldcal-chip[data-tone=muted] .foldcal-chip-label{color:var(--fold-color-text-secondary);text-decoration:line-through;text-decoration-thickness:1px}@media(forced-colors:active){.foldcal-chip{border:1px solid CanvasText}.foldcal-chip-bar{forced-color-adjust:none}}@media(prefers-reduced-motion:reduce){.foldcal-chip{transition:none}}.foldcala{display:flex;flex-direction:column;min-height:0;width:var(--fold-calendar-agenda-width, 320px);height:var(--fold-calendar-agenda-height, 100%);border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-lg);background:var(--fold-color-surface-card);overflow:clip}.foldcala.is-collapsed{width:var(--fold-calendar-agenda-spine-width, 44px)}.foldcala-spine{flex:1;display:flex;flex-direction:column;align-items:center;gap:var(--fold-space-md);padding:var(--fold-space-md) 0;border:0;background:transparent;color:var(--fold-color-text-secondary);font:inherit;cursor:pointer}.foldcala-spine:hover{background:var(--fold-color-surface-hover);color:var(--fold-color-text)}.foldcala-spine:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:-2px}.foldcala-spine-label{writing-mode:vertical-rl;font-size:var(--fold-text-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase}.foldcala-spine-count{display:inline-flex;align-items:center;justify-content:center;min-width:var(--fold-calendar-agenda-badge-size, 20px);padding:0 var(--fold-space-xs);border-radius:var(--fold-radius-pill);background:var(--fold-color-warning-surface);color:var(--fold-color-warning-text);font-size:var(--fold-text-xs);font-weight:700;font-variant-numeric:tabular-nums}.foldcala-head{display:flex;align-items:center;justify-content:space-between;gap:var(--fold-space-sm);padding:var(--fold-space-md);border-bottom:1px solid var(--fold-color-border-subtle)}.foldcala-title{display:inline-flex;align-items:center;gap:var(--fold-space-sm);color:var(--fold-color-text);font-size:var(--fold-text-sm);font-weight:700}.foldcala-count{display:inline-flex;align-items:center;justify-content:center;min-width:var(--fold-calendar-agenda-badge-size, 20px);padding:0 var(--fold-space-xs);border-radius:var(--fold-radius-pill);background:var(--fold-color-warning-surface);color:var(--fold-color-warning-text);font-size:var(--fold-text-xs);font-weight:700;font-variant-numeric:tabular-nums}.foldcala-head-right{display:inline-flex;align-items:center;gap:var(--fold-space-xs)}.foldcala-modes{display:inline-flex;padding:2px;border-radius:var(--fold-radius-sm);background:var(--fold-color-surface-sunken)}.foldcala-modes button{padding:2px var(--fold-space-sm);border:0;border-radius:var(--fold-radius-xs);background:transparent;color:var(--fold-color-text-secondary);font:inherit;font-size:var(--fold-text-xs);font-weight:500;cursor:pointer}.foldcala-modes button.is-on{background:var(--fold-color-surface-raised);color:var(--fold-color-text)}.foldcala-modes button:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:1px}.foldcala-collapse{display:inline-flex;align-items:center;justify-content:center;width:var(--fold-calendar-agenda-control-size, 26px);height:var(--fold-calendar-agenda-control-size, 26px);padding:0;border:1px solid var(--fold-color-border-subtle);border-radius:var(--fold-radius-sm);background:var(--fold-color-surface-subtle);color:var(--fold-color-text-secondary);cursor:pointer}.foldcala-collapse:hover{background:var(--fold-color-surface-hover);color:var(--fold-color-text)}.foldcala-collapse:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:1px}.foldcala-body{flex:1;min-height:0;overflow-y:auto;padding:var(--fold-space-sm)}.foldcala-group{display:grid;grid-template-columns:auto minmax(0,1fr);gap:var(--fold-space-sm);padding:var(--fold-space-sm) 0}.foldcala-group+.foldcala-group{border-top:1px solid var(--fold-color-border-subtle)}.foldcala-date{display:flex;align-items:baseline;gap:var(--fold-space-xs);padding:0;border:0;background:transparent;color:inherit;font:inherit;text-align:start;cursor:pointer}.foldcala-date:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:2px}.foldcala-daynum{color:var(--fold-color-text);font-size:var(--fold-text-lg);font-weight:700;font-variant-numeric:tabular-nums;line-height:1}.foldcala-dmeta{display:flex;flex-direction:column}.foldcala-dname{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);font-weight:600;letter-spacing:.04em;text-transform:uppercase}.foldcala-dmonth{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);text-transform:uppercase}.foldcala-list{display:flex;flex-direction:column;gap:var(--fold-space-xs);margin:0;padding:0;list-style:none}.foldcala-empty{margin:0;padding:var(--fold-space-xl) var(--fold-space-md);color:var(--fold-color-text-secondary);font-size:var(--fold-text-sm);text-align:center}@media(forced-colors:active){.foldcala-modes button.is-on{outline:1px solid Highlight}}\n"] }]
|
|
11736
|
+
}], propDecorators: { from: [{ type: i0.Input, args: [{ isSignal: true, alias: "from", required: true }] }], events: [{ type: i0.Input, args: [{ isSignal: true, alias: "events", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }, { type: i0.Output, args: ["modeChange"] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }, { type: i0.Output, args: ["collapsedChange"] }], limit: [{ type: i0.Input, args: [{ isSignal: true, alias: "limit", required: false }] }], isActionable: [{ type: i0.Input, args: [{ isSignal: true, alias: "isActionable", required: false }] }], dayClick: [{ type: i0.Output, args: ["dayClick"] }], eventClick: [{ type: i0.Output, args: ["eventClick"] }], projectedHeading: [{ type: i0.ContentChild, args: [i0.forwardRef(() => FoldCalendarHeadingDirective), { isSignal: true }] }] } });
|
|
11737
|
+
|
|
11738
|
+
/**
|
|
11739
|
+
* `<fold-calendar-source-filter>` — the chips that switch each feed of a
|
|
11740
|
+
* merged calendar on and off.
|
|
11741
|
+
*
|
|
11742
|
+
* A calendar usually shows several feeds at once (a programme, staff leave,
|
|
11743
|
+
* contracts). This owns the **selection** only: it counts what each feed
|
|
11744
|
+
* contributes and toggles it, and the caller runs `foldFilterBySource()` over
|
|
11745
|
+
* its own events. Keeping the two apart means the chips never need to know how
|
|
11746
|
+
* the events are fetched, and the filtering stays a pure function.
|
|
11747
|
+
*
|
|
11748
|
+
* ## Inputs
|
|
11749
|
+
* | Input | Type | Default | Meaning |
|
|
11750
|
+
* |-----------|--------------------------|---------|---------|
|
|
11751
|
+
* | `sources` | `FoldCalendarSource[]` | `[]` | The feeds to offer. |
|
|
11752
|
+
* | `events` | `FoldCalendarEvent[]` | `[]` | Counted per feed — pass the window on screen for live counts. |
|
|
11753
|
+
* | `active` | `ReadonlySet<string> \| null` | `null` | **Two-way.** The keys currently shown; `null` means "all". |
|
|
11754
|
+
* | `labels` | `Partial<FoldCalendarLabels>` | — | Per-instance label overrides. |
|
|
11755
|
+
*
|
|
11756
|
+
* ## Outputs
|
|
11757
|
+
* | Output | Payload | Fires on |
|
|
11758
|
+
* |-------------------|-----------------------|----------|
|
|
11759
|
+
* | `selectionChange` | `ReadonlySet<string>` | A chip toggled — never `null`. |
|
|
11760
|
+
*
|
|
11761
|
+
* `null` and an empty `Set` mean opposite things — "nothing switched off yet"
|
|
11762
|
+
* versus "every feed switched off" — which is why they are not collapsed.
|
|
11763
|
+
* `foldFilterBySource()` takes either.
|
|
11764
|
+
*
|
|
11765
|
+
* ## Accessibility
|
|
11766
|
+
* Each chip is a **toggle**, so it carries `aria-pressed` in both states, and
|
|
11767
|
+
* its accessible name says which feed and whether it is showing — the tick and
|
|
11768
|
+
* the dot are colour, which cannot be the only carrier.
|
|
11769
|
+
*
|
|
11770
|
+
* @selector `fold-calendar-source-filter`
|
|
11771
|
+
*
|
|
11772
|
+
* @example
|
|
11773
|
+
* ```html
|
|
11774
|
+
* <fold-calendar-source-filter
|
|
11775
|
+
* [sources]="sources"
|
|
11776
|
+
* [events]="events()"
|
|
11777
|
+
* [(active)]="activeSources"
|
|
11778
|
+
* />
|
|
11779
|
+
* <fold-calendar-month [events]="visibleEvents()" [(month)]="month" />
|
|
11780
|
+
* ```
|
|
11781
|
+
*/
|
|
11782
|
+
class FoldCalendarSourceFilterComponent {
|
|
11783
|
+
/** The feeds to offer. */
|
|
11784
|
+
sources = input([], /* @ts-ignore */
|
|
11785
|
+
...(ngDevMode ? [{ debugName: "sources" }] : /* istanbul ignore next */ []));
|
|
11786
|
+
/** Events to count per feed; pass the window on screen for live counts. */
|
|
11787
|
+
events = input([], /* @ts-ignore */
|
|
11788
|
+
...(ngDevMode ? [{ debugName: "events" }] : /* istanbul ignore next */ []));
|
|
11789
|
+
/**
|
|
11790
|
+
* The keys currently shown. Left unset every feed shows, so the filter is
|
|
11791
|
+
* useful before the caller has any selection state of its own.
|
|
11792
|
+
*/
|
|
11793
|
+
active = model(null, /* @ts-ignore */
|
|
11794
|
+
...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
|
|
11795
|
+
/**
|
|
11796
|
+
* The selection after a chip was toggled — **never `null`**, unlike `active`,
|
|
11797
|
+
* whose unset state exists only as a starting value. Handle this one and you
|
|
11798
|
+
* never have to eliminate a case that cannot happen.
|
|
11799
|
+
*/
|
|
11800
|
+
selectionChange = output();
|
|
11801
|
+
/** Labels and locale — see the directive. */
|
|
11802
|
+
chrome = inject(FoldCalendarChromeDirective);
|
|
11803
|
+
/** The effective selection — every key while `active` is unset. */
|
|
11804
|
+
shownKeys = computed(() => {
|
|
11805
|
+
const selected = this.active();
|
|
11806
|
+
if (selected !== null) {
|
|
11807
|
+
return selected;
|
|
11808
|
+
}
|
|
11809
|
+
return new Set(this.sources().map((source) => source.key));
|
|
11810
|
+
}, /* @ts-ignore */
|
|
11811
|
+
...(ngDevMode ? [{ debugName: "shownKeys" }] : /* istanbul ignore next */ []));
|
|
11812
|
+
chips = computed(() => {
|
|
11813
|
+
const shown = this.shownKeys();
|
|
11814
|
+
const events = this.events();
|
|
11815
|
+
return this.sources().map((source) => ({
|
|
11816
|
+
source,
|
|
11817
|
+
count: events.filter((event) => event.sourceKey === source.key).length,
|
|
11818
|
+
shown: shown.has(source.key),
|
|
11819
|
+
}));
|
|
11820
|
+
}, /* @ts-ignore */
|
|
11821
|
+
...(ngDevMode ? [{ debugName: "chips" }] : /* istanbul ignore next */ []));
|
|
11822
|
+
chipLabel(chip) {
|
|
11823
|
+
return this.chrome.l().sourceState(chip.source.label, chip.shown);
|
|
11824
|
+
}
|
|
11825
|
+
/** Writes the next selection rather than mutating the set in place. */
|
|
11826
|
+
toggle(chip) {
|
|
11827
|
+
const next = new Set(this.shownKeys());
|
|
11828
|
+
if (next.has(chip.source.key)) {
|
|
11829
|
+
next.delete(chip.source.key);
|
|
11830
|
+
}
|
|
11831
|
+
else {
|
|
11832
|
+
next.add(chip.source.key);
|
|
11833
|
+
}
|
|
11834
|
+
this.active.set(next);
|
|
11835
|
+
this.selectionChange.emit(next);
|
|
11836
|
+
}
|
|
11837
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarSourceFilterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11838
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: FoldCalendarSourceFilterComponent, isStandalone: true, selector: "fold-calendar-source-filter", inputs: { sources: { classPropertyName: "sources", publicName: "sources", isSignal: true, isRequired: false, transformFunction: null }, events: { classPropertyName: "events", publicName: "events", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { active: "activeChange", selectionChange: "selectionChange" }, hostDirectives: [{ directive: FoldCalendarChromeDirective, inputs: ["locale", "locale", "labels", "labels", "formats", "formats"] }], ngImport: i0, template: "<nav class=\"foldcalsf\" [attr.aria-label]=\"chrome.l().sourceFilter\">\n @for (chip of chips(); track chip.source.key) {\n <button\n type=\"button\"\n class=\"foldcalsf-chip\"\n [class.is-on]=\"chip.shown\"\n [attr.data-tone]=\"chip.source.tone ?? 'neutral'\"\n [attr.aria-pressed]=\"chip.shown\"\n [attr.aria-label]=\"chipLabel(chip)\"\n (click)=\"toggle(chip)\"\n >\n @if (chip.shown) {\n <fold-icon class=\"foldcalsf-check\" name=\"check\" size=\"sm\" />\n }\n <span class=\"foldcalsf-dot\" aria-hidden=\"true\"></span>\n <span class=\"foldcalsf-label\">{{ chip.source.label }}</span>\n <span class=\"foldcalsf-count\">{{ chip.count }}</span>\n </button>\n }\n</nav>\n", styles: [":host{display:block}.foldcalsf{display:flex;flex-wrap:wrap;gap:var(--fold-space-sm)}.foldcalsf-chip{display:inline-flex;align-items:center;gap:var(--fold-space-xs);padding:var(--fold-space-xs) var(--fold-space-md);border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-pill);background:var(--fold-color-surface-subtle);color:var(--fold-color-text-secondary);font:inherit;font-size:var(--fold-text-sm);font-weight:500;cursor:pointer;transition:background-color var(--fold-motion-fast, .12s) ease,color var(--fold-motion-fast, .12s) ease,border-color var(--fold-motion-fast, .12s) ease}.foldcalsf-chip:hover{background:var(--fold-color-surface-hover)}.foldcalsf-chip:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:2px}.foldcalsf-chip:not(.is-on) .foldcalsf-dot{background:var(--fold-color-text-faded)}.foldcalsf-chip:not(.is-on) .foldcalsf-label{text-decoration:line-through;text-decoration-color:var(--fold-color-text-faded)}.foldcalsf-chip.is-on{background:var(--fold-color-surface-raised);border-color:var(--fold-color-border);color:var(--fold-color-text)}.foldcalsf-check{flex:none;color:var(--fold-color-primary-text)}.foldcalsf-dot{flex:none;width:8px;height:8px;border-radius:var(--fold-radius-round);background:var(--fold-color-text-muted)}.foldcalsf-chip[data-tone=success].is-on .foldcalsf-dot{background:var(--fold-color-success)}.foldcalsf-chip[data-tone=warning].is-on .foldcalsf-dot{background:var(--fold-color-warning)}.foldcalsf-chip[data-tone=alert].is-on .foldcalsf-dot{background:var(--fold-color-alert)}.foldcalsf-chip[data-tone=muted].is-on .foldcalsf-dot{background:var(--fold-color-text-faded)}.foldcalsf-count{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);font-variant-numeric:tabular-nums}@media(forced-colors:active){.foldcalsf-chip.is-on{border:2px solid Highlight}}@media(prefers-reduced-motion:reduce){.foldcalsf-chip{transition:none}}\n"], dependencies: [{ kind: "component", type: FoldIconComponent, selector: "fold-icon", inputs: ["name", "size", "tone", "title"] }] });
|
|
11839
|
+
}
|
|
11840
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCalendarSourceFilterComponent, decorators: [{
|
|
11841
|
+
type: Component,
|
|
11842
|
+
args: [{ selector: "fold-calendar-source-filter", standalone: true, imports: [FoldIconComponent], hostDirectives: [
|
|
11843
|
+
{
|
|
11844
|
+
directive: FoldCalendarChromeDirective,
|
|
11845
|
+
inputs: ["locale", "labels", "formats"],
|
|
11846
|
+
},
|
|
11847
|
+
], template: "<nav class=\"foldcalsf\" [attr.aria-label]=\"chrome.l().sourceFilter\">\n @for (chip of chips(); track chip.source.key) {\n <button\n type=\"button\"\n class=\"foldcalsf-chip\"\n [class.is-on]=\"chip.shown\"\n [attr.data-tone]=\"chip.source.tone ?? 'neutral'\"\n [attr.aria-pressed]=\"chip.shown\"\n [attr.aria-label]=\"chipLabel(chip)\"\n (click)=\"toggle(chip)\"\n >\n @if (chip.shown) {\n <fold-icon class=\"foldcalsf-check\" name=\"check\" size=\"sm\" />\n }\n <span class=\"foldcalsf-dot\" aria-hidden=\"true\"></span>\n <span class=\"foldcalsf-label\">{{ chip.source.label }}</span>\n <span class=\"foldcalsf-count\">{{ chip.count }}</span>\n </button>\n }\n</nav>\n", styles: [":host{display:block}.foldcalsf{display:flex;flex-wrap:wrap;gap:var(--fold-space-sm)}.foldcalsf-chip{display:inline-flex;align-items:center;gap:var(--fold-space-xs);padding:var(--fold-space-xs) var(--fold-space-md);border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-pill);background:var(--fold-color-surface-subtle);color:var(--fold-color-text-secondary);font:inherit;font-size:var(--fold-text-sm);font-weight:500;cursor:pointer;transition:background-color var(--fold-motion-fast, .12s) ease,color var(--fold-motion-fast, .12s) ease,border-color var(--fold-motion-fast, .12s) ease}.foldcalsf-chip:hover{background:var(--fold-color-surface-hover)}.foldcalsf-chip:focus-visible{outline:2px solid var(--fold-color-primary);outline-offset:2px}.foldcalsf-chip:not(.is-on) .foldcalsf-dot{background:var(--fold-color-text-faded)}.foldcalsf-chip:not(.is-on) .foldcalsf-label{text-decoration:line-through;text-decoration-color:var(--fold-color-text-faded)}.foldcalsf-chip.is-on{background:var(--fold-color-surface-raised);border-color:var(--fold-color-border);color:var(--fold-color-text)}.foldcalsf-check{flex:none;color:var(--fold-color-primary-text)}.foldcalsf-dot{flex:none;width:8px;height:8px;border-radius:var(--fold-radius-round);background:var(--fold-color-text-muted)}.foldcalsf-chip[data-tone=success].is-on .foldcalsf-dot{background:var(--fold-color-success)}.foldcalsf-chip[data-tone=warning].is-on .foldcalsf-dot{background:var(--fold-color-warning)}.foldcalsf-chip[data-tone=alert].is-on .foldcalsf-dot{background:var(--fold-color-alert)}.foldcalsf-chip[data-tone=muted].is-on .foldcalsf-dot{background:var(--fold-color-text-faded)}.foldcalsf-count{color:var(--fold-color-text-secondary);font-size:var(--fold-text-xs);font-variant-numeric:tabular-nums}@media(forced-colors:active){.foldcalsf-chip.is-on{border:2px solid Highlight}}@media(prefers-reduced-motion:reduce){.foldcalsf-chip{transition:none}}\n"] }]
|
|
11848
|
+
}], propDecorators: { sources: [{ type: i0.Input, args: [{ isSignal: true, alias: "sources", required: false }] }], events: [{ type: i0.Input, args: [{ isSignal: true, alias: "events", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }, { type: i0.Output, args: ["activeChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }] } });
|
|
11849
|
+
|
|
9028
11850
|
// Public TypeScript API for fold-ng.
|
|
9029
11851
|
//
|
|
9030
11852
|
// The tokens themselves ship as CSS (import 'fold-ng/tokens.css').
|
|
@@ -9035,5 +11857,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
9035
11857
|
* Generated bundle index. Do not edit.
|
|
9036
11858
|
*/
|
|
9037
11859
|
|
|
9038
|
-
export { FOLD_AUTO_PALETTES, FOLD_BLUR_TOKENS, FOLD_BUILTIN_ICONS, FOLD_DATA_TABLE_DEFAULT_LABELS, FOLD_DATA_TABLE_LABELS, FOLD_INLINE_CONFIRM_DEFAULT_LABELS, FOLD_INLINE_CONFIRM_LABELS, FOLD_MOTION_TOKENS, FOLD_PAGINATOR_DEFAULT_LABELS, FOLD_PAGINATOR_LABELS, FOLD_PALETTE_DEFAULT, FOLD_PANEL_CLOSE_LABEL, FOLD_PANEL_DEFAULTS, FOLD_RADIUS_TOKENS, FOLD_SEMANTIC_COLOR_TOKENS, FOLD_SHADOW_TOKENS, FOLD_SPACE_TOKENS, FOLD_TEXT_TOKENS, FOLD_TOAST_CONFIG, FoldAppShellComponent, FoldAsideLayoutComponent, FoldAvatarComponent, FoldAvatarDetailComponent, FoldAvatarListComponent, FoldBadgeComponent, FoldButtonComponent, FoldButtonIconComponent, FoldCalloutComponent, FoldCardComponent, FoldCheckboxComponent, FoldChoiceRowComponent, FoldContextCardComponent, FoldDataTableCellDirective, FoldDataTableComponent, FoldDataTableRowCardDirective, FoldDisclosureComponent, FoldDropdownComponent, FoldDropdownItemComponent, FoldElementTitleComponent, FoldElevatedDirective, FoldEmptyStateComponent, FoldFieldComponent, FoldFieldIdDirective, FoldFieldListComponent, FoldFileDropzoneComponent, FoldHeroCardComponent, FoldHeroSectionComponent, FoldIconComponent, FoldIconRegistry, FoldIdService, FoldInlineConfirmComponent, FoldInputComponent, FoldLinkComponent, FoldListboxComponent, FoldLoadingStateComponent, FoldMenuComponent, FoldMenuItemComponent, FoldMenuSectionComponent, FoldMenuSeparatorComponent, FoldMultiselectComponent, FoldNavLauncherComponent, FoldNavLayoutComponent, FoldNavTileComponent, FoldNumberInputComponent, FoldOptionComponent, FoldPageLayoutComponent, FoldPageSectionComponent, FoldPageTitleDirective, FoldPaginatorComponent, FoldPaletteRegistry, FoldPanelComponentOutletDirective, FoldPanelHeaderComponent, FoldPanelHostComponent, FoldPanelHostService, FoldPanelRef, FoldPanelToggle, FoldPasswordFieldComponent, FoldPopoverComponent, FoldPopoverTriggerDirective, FoldRangeSliderComponent, FoldRepeatPressDirective, FoldSearchComponent, FoldSelectComponent, FoldSliderComponent, FoldSpinnerComponent, FoldStatusBadgeComponent, FoldStickyColumnDirective, FoldSurfaceDirective, FoldTabPanelComponent, FoldTabsComponent, FoldTimelineComponent, FoldToastComponent, FoldToastContainerComponent, FoldToastService, FoldToggleIconComponent, FoldViewNavComponent, FoldViewToggleComponent, autoUpdate, computePlacement, foldBlurVar, foldCardBandChrome, foldColorProperty, foldColorVar, foldDefaultPasswordRules, foldHashSeed, foldMotionVar, foldRadiusVar, foldRegexRule, foldResolvePalette, foldShadowVar, foldSpaceVar, foldTextVar, observeElementWidth, provideFoldDataTableLabels, provideFoldIcons, provideFoldInlineConfirmLabels, provideFoldPaginatorLabels, provideFoldPalette, provideFoldPanelDefaults, provideFoldPanelLabels, provideFoldToasts };
|
|
11860
|
+
export { FOLD_AUTO_PALETTES, FOLD_BLUR_TOKENS, FOLD_BUILTIN_ICONS, FOLD_CALENDAR_DEFAULT_LABELS, FOLD_CALENDAR_FORMATS, FOLD_CALENDAR_LABELS, FOLD_DATA_TABLE_DEFAULT_LABELS, FOLD_DATA_TABLE_LABELS, FOLD_DEFAULT_WEEKEND_DAYS, FOLD_FALLBACK_WEEK_INFO, FOLD_INLINE_CONFIRM_DEFAULT_LABELS, FOLD_INLINE_CONFIRM_LABELS, FOLD_MINUTES_PER_DAY, FOLD_MOTION_TOKENS, FOLD_PAGINATOR_DEFAULT_LABELS, FOLD_PAGINATOR_LABELS, FOLD_PALETTE_DEFAULT, FOLD_PANEL_CLOSE_LABEL, FOLD_PANEL_DEFAULTS, FOLD_RADIUS_TOKENS, FOLD_SEMANTIC_COLOR_TOKENS, FOLD_SHADOW_TOKENS, FOLD_SPACE_TOKENS, FOLD_TEXT_TOKENS, FOLD_TOAST_CONFIG, FoldAppShellComponent, FoldAsideLayoutComponent, FoldAvatarComponent, FoldAvatarDetailComponent, FoldAvatarListComponent, FoldBadgeComponent, FoldButtonComponent, FoldButtonIconComponent, FoldCalendarAgendaComponent, FoldCalendarChromeDirective, FoldCalendarDayComponent, FoldCalendarDayDirective, FoldCalendarEventDirective, FoldCalendarHeadingDirective, FoldCalendarListComponent, FoldCalendarMonthComponent, FoldCalendarOverflowDirective, FoldCalendarSourceFilterComponent, FoldCalendarTimegridComponent, FoldCalendarTitleDirective, FoldCalendarToolbarComponent, FoldCalendarWeekComponent, FoldCalloutComponent, FoldCardComponent, FoldCheckboxComponent, FoldChoiceRowComponent, FoldContextCardComponent, FoldDataTableCellDirective, FoldDataTableComponent, FoldDataTableRowCardDirective, FoldDisclosureComponent, FoldDropdownComponent, FoldDropdownItemComponent, FoldElementTitleComponent, FoldElevatedDirective, FoldEmptyStateComponent, FoldFieldComponent, FoldFieldIdDirective, FoldFieldListComponent, FoldFileDropzoneComponent, FoldHeroCardComponent, FoldHeroSectionComponent, FoldIconComponent, FoldIconRegistry, FoldIdService, FoldInlineConfirmComponent, FoldInputComponent, FoldLinkComponent, FoldListboxComponent, FoldLoadingStateComponent, FoldMenuComponent, FoldMenuItemComponent, FoldMenuSectionComponent, FoldMenuSeparatorComponent, FoldMultiselectComponent, FoldNavLauncherComponent, FoldNavLayoutComponent, FoldNavTileComponent, FoldNumberInputComponent, FoldOptionComponent, FoldPageLayoutComponent, FoldPageSectionComponent, FoldPageTitleDirective, FoldPaginatorComponent, FoldPaletteRegistry, FoldPanelComponentOutletDirective, FoldPanelHeaderComponent, FoldPanelHostComponent, FoldPanelHostService, FoldPanelRef, FoldPanelToggle, FoldPasswordFieldComponent, FoldPopoverComponent, FoldPopoverTriggerDirective, FoldRangeSliderComponent, FoldRepeatPressDirective, FoldSearchComponent, FoldSelectComponent, FoldSliderComponent, FoldSpinnerComponent, FoldStatusBadgeComponent, FoldStickyColumnDirective, FoldSurfaceDirective, FoldTabPanelComponent, FoldTabsComponent, FoldTimelineComponent, FoldToastComponent, FoldToastContainerComponent, FoldToastService, FoldToggleIconComponent, FoldViewNavComponent, FoldViewToggleComponent, autoUpdate, computePlacement, foldAddDays, foldAddMinutes, foldAddMonths, foldBlurVar, foldBuildAgenda, foldBuildDay, foldBuildMonthGrid, foldBuildTimeGrid, foldBuildWeek, foldCalendarNextFocus, foldCardBandChrome, foldColorProperty, foldColorVar, foldCountActionable, foldDaysBetween, foldDefaultPasswordRules, foldEndOfMonth, foldEventsInRange, foldEventsOnDay, foldFilterBySource, foldFromMinutes, foldFromNativeDate, foldFromTemporal, foldHashSeed, foldIsActionable, foldIsCalendarDate, foldIsCalendarTime, foldIsTimed, foldIsWeekend, foldIsoWeek, foldIsoWeekYear, foldLayOutOverlaps, foldLocaleWeekInfo, foldMotionVar, foldRadiusVar, foldRangeForView, foldRegexRule, foldResolvePalette, foldShadowVar, foldShiftDate, foldSpaceVar, foldStartOfMonth, foldStartOfWeek, foldTextVar, foldToMinutes, foldToNativeDate, foldToday, foldViewTitle, foldWeekdayIndex, foldWeekdayOf, observeElementWidth, provideFoldCalendarLabels, provideFoldDataTableLabels, provideFoldIcons, provideFoldInlineConfirmLabels, provideFoldPaginatorLabels, provideFoldPalette, provideFoldPanelDefaults, provideFoldPanelLabels, provideFoldToasts };
|
|
9039
11861
|
//# sourceMappingURL=fold-ng.mjs.map
|