cerxos-ui 0.0.17 → 0.0.19

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Component, EventEmitter, ElementRef, HostListener, ViewChildren, ViewChild, Output, Input, ChangeDetectionStrategy, Attribute, forwardRef, Directive, ContentChildren, ContentChild, HostBinding, signal, Injectable, inject, Pipe } from '@angular/core';
2
+ import { Component, EventEmitter, ElementRef, HostListener, ViewChildren, ViewChild, Output, Input, ChangeDetectionStrategy, Attribute, forwardRef, Directive, ContentChildren, inject, ContentChild, HostBinding, signal, Injectable, Pipe } from '@angular/core';
3
3
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
4
4
  import * as i1 from '@angular/common';
5
5
  import { NgTemplateOutlet, CommonModule } from '@angular/common';
@@ -1922,6 +1922,7 @@ class CxsDialogComponent {
1922
1922
  dialogPanel;
1923
1923
  titleId = `cxs-dialog-title-${CxsDialogComponent.nextId++}`;
1924
1924
  descriptionId = `cxs-dialog-description-${CxsDialogComponent.nextId++}`;
1925
+ pressStartedOnBackdrop = false;
1925
1926
  constructor(hostClass) {
1926
1927
  this.hostClass = hostClass;
1927
1928
  }
@@ -1968,11 +1969,18 @@ class CxsDialogComponent {
1968
1969
  this.openChange.emit(false);
1969
1970
  this.closed.emit(reason);
1970
1971
  }
1972
+ onBackdropPointerDown(event) {
1973
+ this.pressStartedOnBackdrop = event.target === event.currentTarget;
1974
+ }
1971
1975
  onBackdropClick(event) {
1976
+ // A click only counts as a backdrop click when the press also started on the backdrop,
1977
+ // so dragging a text selection out of the panel does not close the dialog.
1978
+ const pressStartedOnBackdrop = this.pressStartedOnBackdrop;
1979
+ this.pressStartedOnBackdrop = false;
1972
1980
  if (!this.closeOnBackdrop || !this.open) {
1973
1981
  return;
1974
1982
  }
1975
- if (event.target === event.currentTarget) {
1983
+ if (pressStartedOnBackdrop && event.target === event.currentTarget) {
1976
1984
  this.requestClose('backdrop');
1977
1985
  }
1978
1986
  }
@@ -1991,11 +1999,11 @@ class CxsDialogComponent {
1991
1999
  });
1992
2000
  }
1993
2001
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: CxsDialogComponent, deps: [{ token: 'class', attribute: true }], target: i0.ɵɵFactoryTarget.Component });
1994
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.10", type: CxsDialogComponent, isStandalone: true, selector: "cxs-dialog", inputs: { open: "open", title: "title", description: "description", ariaLabel: "ariaLabel", size: "size", dismissible: "dismissible", closeOnBackdrop: "closeOnBackdrop", closeOnEscape: "closeOnEscape" }, outputs: { openChange: "openChange", closed: "closed" }, viewQueries: [{ propertyName: "dialogPanel", first: true, predicate: ["dialogPanel"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "@if (open) {\n <div [class]=\"overlayClass\" (click)=\"onBackdropClick($event)\">\n <div\n #dialogPanel\n [class]=\"panelClass\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"title ? titleId : null\"\n [attr.aria-describedby]=\"description ? descriptionId : null\"\n [attr.aria-label]=\"ariaLabelValue\"\n tabindex=\"-1\"\n (keydown)=\"onKeydown($event)\"\n >\n <div class=\"flex items-start gap-3\">\n <div class=\"flex-1\">\n @if (title) {\n <h2 [id]=\"titleId\" [class]=\"titleClass\">{{ title }}</h2>\n }\n @if (description) {\n <p [id]=\"descriptionId\" [class]=\"descriptionClass\">{{ description }}</p>\n }\n </div>\n @if (dismissible) {\n <button\n type=\"button\"\n [class]=\"closeButtonClass\"\n aria-label=\"Close dialog\"\n (click)=\"requestClose('dismiss')\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"size-6\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18 18 6M6 6l12 12\" />\n </svg>\n </button>\n }\n </div>\n\n <div [class]=\"bodyClass\">\n <ng-content></ng-content>\n </div>\n\n <div [class]=\"actionsClass\">\n <ng-content select=\"[cxsDialogActions]\"></ng-content>\n </div>\n </div>\n </div>\n}\n", styles: [":host{display:contents}.cxs-dialog-overlay{animation:cxs-dialog-overlay-appear .22s ease-out}.cxs-dialog-panel{animation:cxs-dialog-panel-appear .26s cubic-bezier(.16,1,.3,1);transform-origin:center;will-change:opacity,transform}@keyframes cxs-dialog-overlay-appear{0%{opacity:0}to{opacity:1}}@keyframes cxs-dialog-panel-appear{0%{opacity:0;transform:scale(.82)}to{opacity:1;transform:scale(1)}}@media(prefers-reduced-motion:reduce){.cxs-dialog-overlay,.cxs-dialog-panel{animation:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2002
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.10", type: CxsDialogComponent, isStandalone: true, selector: "cxs-dialog", inputs: { open: "open", title: "title", description: "description", ariaLabel: "ariaLabel", size: "size", dismissible: "dismissible", closeOnBackdrop: "closeOnBackdrop", closeOnEscape: "closeOnEscape" }, outputs: { openChange: "openChange", closed: "closed" }, viewQueries: [{ propertyName: "dialogPanel", first: true, predicate: ["dialogPanel"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "@if (open) {\n <div\n [class]=\"overlayClass\"\n (pointerdown)=\"onBackdropPointerDown($event)\"\n (click)=\"onBackdropClick($event)\"\n >\n <div\n #dialogPanel\n [class]=\"panelClass\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"title ? titleId : null\"\n [attr.aria-describedby]=\"description ? descriptionId : null\"\n [attr.aria-label]=\"ariaLabelValue\"\n tabindex=\"-1\"\n (keydown)=\"onKeydown($event)\"\n >\n <div class=\"flex items-start gap-3\">\n <div class=\"flex-1\">\n @if (title) {\n <h2 [id]=\"titleId\" [class]=\"titleClass\">{{ title }}</h2>\n }\n @if (description) {\n <p [id]=\"descriptionId\" [class]=\"descriptionClass\">{{ description }}</p>\n }\n </div>\n @if (dismissible) {\n <button\n type=\"button\"\n [class]=\"closeButtonClass\"\n aria-label=\"Close dialog\"\n (click)=\"requestClose('dismiss')\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"size-6\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18 18 6M6 6l12 12\" />\n </svg>\n </button>\n }\n </div>\n\n <div [class]=\"bodyClass\">\n <ng-content></ng-content>\n </div>\n\n <div [class]=\"actionsClass\">\n <ng-content select=\"[cxsDialogActions]\"></ng-content>\n </div>\n </div>\n </div>\n}\n", styles: [":host{display:contents}.cxs-dialog-overlay{animation:cxs-dialog-overlay-appear .22s ease-out}.cxs-dialog-panel{animation:cxs-dialog-panel-appear .26s cubic-bezier(.16,1,.3,1);transform-origin:center;will-change:opacity,transform}@keyframes cxs-dialog-overlay-appear{0%{opacity:0}to{opacity:1}}@keyframes cxs-dialog-panel-appear{0%{opacity:0;transform:scale(.82)}to{opacity:1;transform:scale(1)}}@media(prefers-reduced-motion:reduce){.cxs-dialog-overlay,.cxs-dialog-panel{animation:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1995
2003
  }
1996
2004
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: CxsDialogComponent, decorators: [{
1997
2005
  type: Component,
1998
- args: [{ selector: 'cxs-dialog', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (open) {\n <div [class]=\"overlayClass\" (click)=\"onBackdropClick($event)\">\n <div\n #dialogPanel\n [class]=\"panelClass\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"title ? titleId : null\"\n [attr.aria-describedby]=\"description ? descriptionId : null\"\n [attr.aria-label]=\"ariaLabelValue\"\n tabindex=\"-1\"\n (keydown)=\"onKeydown($event)\"\n >\n <div class=\"flex items-start gap-3\">\n <div class=\"flex-1\">\n @if (title) {\n <h2 [id]=\"titleId\" [class]=\"titleClass\">{{ title }}</h2>\n }\n @if (description) {\n <p [id]=\"descriptionId\" [class]=\"descriptionClass\">{{ description }}</p>\n }\n </div>\n @if (dismissible) {\n <button\n type=\"button\"\n [class]=\"closeButtonClass\"\n aria-label=\"Close dialog\"\n (click)=\"requestClose('dismiss')\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"size-6\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18 18 6M6 6l12 12\" />\n </svg>\n </button>\n }\n </div>\n\n <div [class]=\"bodyClass\">\n <ng-content></ng-content>\n </div>\n\n <div [class]=\"actionsClass\">\n <ng-content select=\"[cxsDialogActions]\"></ng-content>\n </div>\n </div>\n </div>\n}\n", styles: [":host{display:contents}.cxs-dialog-overlay{animation:cxs-dialog-overlay-appear .22s ease-out}.cxs-dialog-panel{animation:cxs-dialog-panel-appear .26s cubic-bezier(.16,1,.3,1);transform-origin:center;will-change:opacity,transform}@keyframes cxs-dialog-overlay-appear{0%{opacity:0}to{opacity:1}}@keyframes cxs-dialog-panel-appear{0%{opacity:0;transform:scale(.82)}to{opacity:1;transform:scale(1)}}@media(prefers-reduced-motion:reduce){.cxs-dialog-overlay,.cxs-dialog-panel{animation:none}}\n"] }]
2006
+ args: [{ selector: 'cxs-dialog', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (open) {\n <div\n [class]=\"overlayClass\"\n (pointerdown)=\"onBackdropPointerDown($event)\"\n (click)=\"onBackdropClick($event)\"\n >\n <div\n #dialogPanel\n [class]=\"panelClass\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"title ? titleId : null\"\n [attr.aria-describedby]=\"description ? descriptionId : null\"\n [attr.aria-label]=\"ariaLabelValue\"\n tabindex=\"-1\"\n (keydown)=\"onKeydown($event)\"\n >\n <div class=\"flex items-start gap-3\">\n <div class=\"flex-1\">\n @if (title) {\n <h2 [id]=\"titleId\" [class]=\"titleClass\">{{ title }}</h2>\n }\n @if (description) {\n <p [id]=\"descriptionId\" [class]=\"descriptionClass\">{{ description }}</p>\n }\n </div>\n @if (dismissible) {\n <button\n type=\"button\"\n [class]=\"closeButtonClass\"\n aria-label=\"Close dialog\"\n (click)=\"requestClose('dismiss')\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"size-6\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18 18 6M6 6l12 12\" />\n </svg>\n </button>\n }\n </div>\n\n <div [class]=\"bodyClass\">\n <ng-content></ng-content>\n </div>\n\n <div [class]=\"actionsClass\">\n <ng-content select=\"[cxsDialogActions]\"></ng-content>\n </div>\n </div>\n </div>\n}\n", styles: [":host{display:contents}.cxs-dialog-overlay{animation:cxs-dialog-overlay-appear .22s ease-out}.cxs-dialog-panel{animation:cxs-dialog-panel-appear .26s cubic-bezier(.16,1,.3,1);transform-origin:center;will-change:opacity,transform}@keyframes cxs-dialog-overlay-appear{0%{opacity:0}to{opacity:1}}@keyframes cxs-dialog-panel-appear{0%{opacity:0;transform:scale(.82)}to{opacity:1;transform:scale(1)}}@media(prefers-reduced-motion:reduce){.cxs-dialog-overlay,.cxs-dialog-panel{animation:none}}\n"] }]
1999
2007
  }], ctorParameters: () => [{ type: undefined, decorators: [{
2000
2008
  type: Attribute,
2001
2009
  args: ['class']
@@ -2380,6 +2388,251 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
2380
2388
  args: ['document:keydown.escape']
2381
2389
  }] } });
2382
2390
 
2391
+ const DAY_MS = 86_400_000;
2392
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
2393
+ const ISO_DAYS = [1, 2, 3, 4, 5, 6, 7];
2394
+ const STATE_TEXT = {
2395
+ done: 'done',
2396
+ empty: 'not done',
2397
+ off: 'not scheduled',
2398
+ 'out-of-range': 'unavailable',
2399
+ padding: ''
2400
+ };
2401
+ const CELL_BASE_CLASSES = 'block h-[var(--cxs-habit-grid-cell-size,0.875rem)] w-[var(--cxs-habit-grid-cell-size,0.875rem)] shrink-0 rounded-[3px] ' +
2402
+ 'outline-none focus-visible:ring-2 focus-visible:ring-[var(--cxs-color-focus)] focus-visible:ring-offset-1';
2403
+ const CELL_STATE_CLASSES = {
2404
+ done: 'bg-[var(--cxs-habit-grid-accent,var(--cxs-color-primary))]',
2405
+ empty: 'border border-[var(--cxs-color-border)] bg-[var(--cxs-color-surface-hover)]',
2406
+ off: 'border border-dashed border-[var(--cxs-color-border)] bg-transparent opacity-50',
2407
+ 'out-of-range': 'bg-[var(--cxs-color-surface-hover)] opacity-30',
2408
+ padding: 'invisible'
2409
+ };
2410
+ const INTERACTIVE_CLASSES = 'cursor-pointer hover:opacity-80';
2411
+ function parseIsoDate(value) {
2412
+ if (!ISO_DATE.test(value)) {
2413
+ return null;
2414
+ }
2415
+ const [year, month, day] = value.split('-').map(Number);
2416
+ const time = Date.UTC(year, month - 1, day);
2417
+ return formatIsoDate(time) === value ? time : null;
2418
+ }
2419
+ function formatIsoDate(time) {
2420
+ return new Date(time).toISOString().slice(0, 10);
2421
+ }
2422
+ function isoDayOf(time) {
2423
+ return (((new Date(time).getUTCDay() + 6) % 7) + 1);
2424
+ }
2425
+ /**
2426
+ * GitHub-style grid of days for habit tracking. Columns are Monday–Sunday weeks; rows are days of the week.
2427
+ * Presentational only: it renders the dates it is given and emits `dayToggle`; the parent owns the data.
2428
+ */
2429
+ class CxsHabitGridComponent {
2430
+ elementRef = inject(ElementRef);
2431
+ /** First date shown, as `yyyy-MM-dd`. */
2432
+ from = '';
2433
+ /** Last date shown, as `yyyy-MM-dd`. */
2434
+ to = '';
2435
+ /** The user's local today; later dates are out of range. */
2436
+ today = '';
2437
+ /** The habit's start date; earlier dates are out of range. */
2438
+ startDate = null;
2439
+ /** Dates marked done, as `yyyy-MM-dd`. */
2440
+ doneDates = [];
2441
+ /** Scheduled ISO days of week; `null` means every day is scheduled. */
2442
+ scheduledDays = null;
2443
+ /** Weekly target; when set, each week shows `done/target`. */
2444
+ weeklyTarget = null;
2445
+ /** Optional colour for done cells; defaults to `--cxs-color-primary`. */
2446
+ accentColor = null;
2447
+ /** Whether done and empty cells can be toggled. */
2448
+ interactive = false;
2449
+ /** Accessible name of the grid. */
2450
+ ariaLabel = 'Habit history';
2451
+ /** Locale for accessible date labels. */
2452
+ locale = 'en-GB';
2453
+ /** Emitted when the user toggles a `done` or `empty` cell in interactive mode. */
2454
+ dayToggle = new EventEmitter();
2455
+ dayRows = ISO_DAYS;
2456
+ weeks = [];
2457
+ focusedDate = null;
2458
+ rangeStart = null;
2459
+ rangeEnd = null;
2460
+ get hasWeeklyProgress() {
2461
+ return this.weeklyTarget != null && this.weeklyTarget > 0;
2462
+ }
2463
+ ngOnChanges() {
2464
+ this.weeks = this.buildWeeks();
2465
+ if (!this.focusedDate || !this.isWithinRange(this.focusedDate)) {
2466
+ this.focusedDate = this.initialFocusDate();
2467
+ }
2468
+ }
2469
+ weekdayLabel(isoDay) {
2470
+ // 2024-01-01 is a Monday.
2471
+ return new Intl.DateTimeFormat(this.locale, { weekday: 'short', timeZone: 'UTC' }).format(new Date(Date.UTC(2024, 0, isoDay)));
2472
+ }
2473
+ dayAt(week, isoDay) {
2474
+ return week.days[isoDay - 1];
2475
+ }
2476
+ cellClass(day) {
2477
+ const classes = [CELL_BASE_CLASSES, CELL_STATE_CLASSES[day.state]];
2478
+ if (this.isToggleable(day)) {
2479
+ classes.push(INTERACTIVE_CLASSES);
2480
+ }
2481
+ return classes.join(' ');
2482
+ }
2483
+ tabIndexFor(day) {
2484
+ if (day.state === 'padding') {
2485
+ return null;
2486
+ }
2487
+ return day.date === this.focusedDate ? 0 : -1;
2488
+ }
2489
+ isToggleable(day) {
2490
+ return this.interactive && (day.state === 'done' || day.state === 'empty');
2491
+ }
2492
+ onCellClick(day) {
2493
+ if (day.state === 'padding') {
2494
+ return;
2495
+ }
2496
+ this.focusedDate = day.date;
2497
+ this.toggle(day);
2498
+ }
2499
+ onCellKeydown(event, day) {
2500
+ const moves = { ArrowUp: -1, ArrowDown: 1, ArrowLeft: -7, ArrowRight: 7 };
2501
+ if (event.key === 'Enter' || event.key === ' ') {
2502
+ event.preventDefault();
2503
+ this.toggle(day);
2504
+ return;
2505
+ }
2506
+ const delta = moves[event.key];
2507
+ if (delta === undefined) {
2508
+ return;
2509
+ }
2510
+ event.preventDefault();
2511
+ const current = parseIsoDate(day.date);
2512
+ if (current === null || this.rangeStart === null || this.rangeEnd === null) {
2513
+ return;
2514
+ }
2515
+ const target = Math.min(Math.max(current + delta * DAY_MS, this.rangeStart), this.rangeEnd);
2516
+ this.focusedDate = formatIsoDate(target);
2517
+ queueMicrotask(() => this.focusCell(this.focusedDate));
2518
+ }
2519
+ toggle(day) {
2520
+ if (!this.isToggleable(day)) {
2521
+ return;
2522
+ }
2523
+ this.dayToggle.emit({ date: day.date, done: day.state === 'empty' });
2524
+ }
2525
+ focusCell(date) {
2526
+ if (!date) {
2527
+ return;
2528
+ }
2529
+ this.elementRef.nativeElement.querySelector(`[data-date="${date}"]`)?.focus();
2530
+ }
2531
+ isWithinRange(date) {
2532
+ const time = parseIsoDate(date);
2533
+ return time !== null && this.rangeStart !== null && this.rangeEnd !== null
2534
+ && time >= this.rangeStart && time <= this.rangeEnd;
2535
+ }
2536
+ initialFocusDate() {
2537
+ if (this.rangeStart === null || this.rangeEnd === null) {
2538
+ return null;
2539
+ }
2540
+ const today = parseIsoDate(this.today);
2541
+ const target = today === null ? this.rangeEnd : Math.min(Math.max(today, this.rangeStart), this.rangeEnd);
2542
+ return formatIsoDate(target);
2543
+ }
2544
+ buildWeeks() {
2545
+ this.rangeStart = parseIsoDate(this.from);
2546
+ this.rangeEnd = parseIsoDate(this.to);
2547
+ if (this.rangeStart === null || this.rangeEnd === null || this.rangeStart > this.rangeEnd) {
2548
+ this.rangeStart = null;
2549
+ this.rangeEnd = null;
2550
+ return [];
2551
+ }
2552
+ const done = new Set(this.doneDates);
2553
+ const scheduled = this.scheduledDays ? new Set(this.scheduledDays) : null;
2554
+ const today = parseIsoDate(this.today);
2555
+ const start = this.startDate ? parseIsoDate(this.startDate) : null;
2556
+ const labelFormat = new Intl.DateTimeFormat(this.locale, {
2557
+ weekday: 'long',
2558
+ day: 'numeric',
2559
+ month: 'long',
2560
+ year: 'numeric',
2561
+ timeZone: 'UTC'
2562
+ });
2563
+ const weekFormat = new Intl.DateTimeFormat(this.locale, { day: 'numeric', month: 'short', timeZone: 'UTC' });
2564
+ const firstMonday = this.rangeStart - (isoDayOf(this.rangeStart) - 1) * DAY_MS;
2565
+ const weeks = [];
2566
+ for (let weekStart = firstMonday; weekStart <= this.rangeEnd; weekStart += 7 * DAY_MS) {
2567
+ const days = ISO_DAYS.map((isoDay) => {
2568
+ const time = weekStart + (isoDay - 1) * DAY_MS;
2569
+ const date = formatIsoDate(time);
2570
+ const state = this.stateOf(time, date, isoDay, done, scheduled, start, today);
2571
+ const label = state === 'padding' ? '' : `${labelFormat.format(time)}, ${STATE_TEXT[state]}`;
2572
+ return { date, isoDay, state, label };
2573
+ });
2574
+ const doneCount = days.filter((d) => d.state === 'done').length;
2575
+ const target = this.hasWeeklyProgress ? this.weeklyTarget : null;
2576
+ weeks.push({
2577
+ start: formatIsoDate(weekStart),
2578
+ days,
2579
+ doneCount,
2580
+ progressLabel: target === null ? null : `${doneCount}/${target}`,
2581
+ progressAriaLabel: target === null ? null : `Week of ${weekFormat.format(weekStart)}: ${doneCount} of ${target}`
2582
+ });
2583
+ }
2584
+ return weeks;
2585
+ }
2586
+ stateOf(time, date, isoDay, done, scheduled, start, today) {
2587
+ if (time < this.rangeStart || time > this.rangeEnd) {
2588
+ return 'padding';
2589
+ }
2590
+ if ((start !== null && time < start) || (today !== null && time > today)) {
2591
+ return 'out-of-range';
2592
+ }
2593
+ if (scheduled && !scheduled.has(isoDay)) {
2594
+ return 'off';
2595
+ }
2596
+ return done.has(date) ? 'done' : 'empty';
2597
+ }
2598
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: CxsHabitGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2599
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.10", type: CxsHabitGridComponent, isStandalone: true, selector: "cxs-habit-grid", inputs: { from: "from", to: "to", today: "today", startDate: "startDate", doneDates: "doneDates", scheduledDays: "scheduledDays", weeklyTarget: "weeklyTarget", accentColor: "accentColor", interactive: "interactive", ariaLabel: "ariaLabel", locale: "locale" }, outputs: { dayToggle: "dayToggle" }, host: { properties: { "style.--cxs-habit-grid-accent": "accentColor || null" }, classAttribute: "block" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"inline-flex max-w-full flex-col gap-1 overflow-x-auto\">\n <div\n role=\"grid\"\n class=\"flex flex-col gap-[var(--cxs-habit-grid-gap,3px)]\"\n [attr.aria-label]=\"ariaLabel\"\n [attr.aria-readonly]=\"interactive ? null : 'true'\"\n >\n @for (isoDay of dayRows; track isoDay) {\n <div role=\"row\" class=\"flex items-center gap-[var(--cxs-habit-grid-gap,3px)]\">\n <span\n role=\"rowheader\"\n class=\"w-8 shrink-0 text-[10px] leading-none text-[var(--cxs-color-on-surface-muted)]\"\n >\n {{ weekdayLabel(isoDay) }}\n </span>\n @for (week of weeks; track week.start) {\n @let day = dayAt(week, isoDay);\n @if (day.state === 'padding') {\n <span role=\"gridcell\" aria-hidden=\"true\" [class]=\"cellClass(day)\"></span>\n } @else {\n <span\n role=\"gridcell\"\n [class]=\"cellClass(day)\"\n [attr.data-date]=\"day.date\"\n [attr.data-state]=\"day.state\"\n [attr.aria-label]=\"day.label\"\n [attr.aria-selected]=\"day.state === 'done'\"\n [attr.aria-disabled]=\"interactive && !isToggleable(day) ? 'true' : null\"\n [attr.tabindex]=\"tabIndexFor(day)\"\n (click)=\"onCellClick(day)\"\n (keydown)=\"onCellKeydown($event, day)\"\n ></span>\n }\n }\n </div>\n }\n </div>\n\n @if (hasWeeklyProgress) {\n <div class=\"flex items-center gap-[var(--cxs-habit-grid-gap,3px)]\" data-testid=\"cxs-habit-grid-progress\">\n <span class=\"w-8 shrink-0\" aria-hidden=\"true\"></span>\n @for (week of weeks; track week.start) {\n <span\n class=\"w-[var(--cxs-habit-grid-cell-size,0.875rem)] shrink-0 overflow-visible whitespace-nowrap text-center text-[9px] leading-none text-[var(--cxs-color-on-surface-muted)]\"\n [class.font-semibold]=\"week.doneCount >= (weeklyTarget ?? 0)\"\n [attr.aria-label]=\"week.progressAriaLabel\"\n [attr.title]=\"week.progressAriaLabel\"\n >\n {{ week.progressLabel }}\n </span>\n }\n </div>\n }\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
2600
+ }
2601
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: CxsHabitGridComponent, decorators: [{
2602
+ type: Component,
2603
+ args: [{ selector: 'cxs-habit-grid', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: {
2604
+ class: 'block',
2605
+ '[style.--cxs-habit-grid-accent]': 'accentColor || null'
2606
+ }, template: "<div class=\"inline-flex max-w-full flex-col gap-1 overflow-x-auto\">\n <div\n role=\"grid\"\n class=\"flex flex-col gap-[var(--cxs-habit-grid-gap,3px)]\"\n [attr.aria-label]=\"ariaLabel\"\n [attr.aria-readonly]=\"interactive ? null : 'true'\"\n >\n @for (isoDay of dayRows; track isoDay) {\n <div role=\"row\" class=\"flex items-center gap-[var(--cxs-habit-grid-gap,3px)]\">\n <span\n role=\"rowheader\"\n class=\"w-8 shrink-0 text-[10px] leading-none text-[var(--cxs-color-on-surface-muted)]\"\n >\n {{ weekdayLabel(isoDay) }}\n </span>\n @for (week of weeks; track week.start) {\n @let day = dayAt(week, isoDay);\n @if (day.state === 'padding') {\n <span role=\"gridcell\" aria-hidden=\"true\" [class]=\"cellClass(day)\"></span>\n } @else {\n <span\n role=\"gridcell\"\n [class]=\"cellClass(day)\"\n [attr.data-date]=\"day.date\"\n [attr.data-state]=\"day.state\"\n [attr.aria-label]=\"day.label\"\n [attr.aria-selected]=\"day.state === 'done'\"\n [attr.aria-disabled]=\"interactive && !isToggleable(day) ? 'true' : null\"\n [attr.tabindex]=\"tabIndexFor(day)\"\n (click)=\"onCellClick(day)\"\n (keydown)=\"onCellKeydown($event, day)\"\n ></span>\n }\n }\n </div>\n }\n </div>\n\n @if (hasWeeklyProgress) {\n <div class=\"flex items-center gap-[var(--cxs-habit-grid-gap,3px)]\" data-testid=\"cxs-habit-grid-progress\">\n <span class=\"w-8 shrink-0\" aria-hidden=\"true\"></span>\n @for (week of weeks; track week.start) {\n <span\n class=\"w-[var(--cxs-habit-grid-cell-size,0.875rem)] shrink-0 overflow-visible whitespace-nowrap text-center text-[9px] leading-none text-[var(--cxs-color-on-surface-muted)]\"\n [class.font-semibold]=\"week.doneCount >= (weeklyTarget ?? 0)\"\n [attr.aria-label]=\"week.progressAriaLabel\"\n [attr.title]=\"week.progressAriaLabel\"\n >\n {{ week.progressLabel }}\n </span>\n }\n </div>\n }\n</div>\n" }]
2607
+ }], propDecorators: { from: [{
2608
+ type: Input,
2609
+ args: [{ required: true }]
2610
+ }], to: [{
2611
+ type: Input,
2612
+ args: [{ required: true }]
2613
+ }], today: [{
2614
+ type: Input,
2615
+ args: [{ required: true }]
2616
+ }], startDate: [{
2617
+ type: Input
2618
+ }], doneDates: [{
2619
+ type: Input
2620
+ }], scheduledDays: [{
2621
+ type: Input
2622
+ }], weeklyTarget: [{
2623
+ type: Input
2624
+ }], accentColor: [{
2625
+ type: Input
2626
+ }], interactive: [{
2627
+ type: Input
2628
+ }], ariaLabel: [{
2629
+ type: Input
2630
+ }], locale: [{
2631
+ type: Input
2632
+ }], dayToggle: [{
2633
+ type: Output
2634
+ }] } });
2635
+
2383
2636
  const BASE_CLASSES$3 = 'w-full rounded-[var(--cxs-radius-md)] border border-[var(--cxs-color-border)] ' +
2384
2637
  'bg-[var(--cxs-color-surface)] text-[var(--cxs-color-on-surface)] ' +
2385
2638
  'placeholder:text-[var(--cxs-color-on-surface-muted)] transition-colors ' +
@@ -4620,5 +4873,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
4620
4873
  * Generated bundle index. Do not edit.
4621
4874
  */
4622
4875
 
4623
- export { CXS_USER_PREFERENCE_CHANGED_EVENT, CXS_USER_SESSION_STORAGE_KEY, CerxosUi, CxsAccordionComponent, CxsAccordionItemComponent, CxsActionMenuComponent, CxsAlertComponent, CxsAvatarComponent, CxsBadgeComponent, CxsBreadcrumbComponent, CxsButtonComponent, CxsCardComponent, CxsCarouselComponent, CxsCheckboxComponent, CxsCurrencyPipe, CxsDataTableCellDirective, CxsDataTableComponent, CxsDatePipe, CxsDatepickerComponent, CxsDialogComponent, CxsIconComponent, CxsInputComponent, CxsMenubarComponent, CxsMultiSelectComponent, CxsPreferenceFormattingService, CxsProgressBarComponent, CxsRadioComponent, CxsSelectComponent, CxsSkeletonComponent, CxsSliderComponent, CxsTabLabelDirective, CxsTabPanelComponent, CxsTableComponent, CxsTabsComponent, CxsThemeDirective, CxsToastComponent, CxsToggleComponent, CxsTooltipComponent };
4876
+ export { CXS_USER_PREFERENCE_CHANGED_EVENT, CXS_USER_SESSION_STORAGE_KEY, CerxosUi, CxsAccordionComponent, CxsAccordionItemComponent, CxsActionMenuComponent, CxsAlertComponent, CxsAvatarComponent, CxsBadgeComponent, CxsBreadcrumbComponent, CxsButtonComponent, CxsCardComponent, CxsCarouselComponent, CxsCheckboxComponent, CxsCurrencyPipe, CxsDataTableCellDirective, CxsDataTableComponent, CxsDatePipe, CxsDatepickerComponent, CxsDialogComponent, CxsHabitGridComponent, CxsIconComponent, CxsInputComponent, CxsMenubarComponent, CxsMultiSelectComponent, CxsPreferenceFormattingService, CxsProgressBarComponent, CxsRadioComponent, CxsSelectComponent, CxsSkeletonComponent, CxsSliderComponent, CxsTabLabelDirective, CxsTabPanelComponent, CxsTableComponent, CxsTabsComponent, CxsThemeDirective, CxsToastComponent, CxsToggleComponent, CxsTooltipComponent };
4624
4877
  //# sourceMappingURL=cerxos-ui.mjs.map