cerxos-ui 0.0.17 → 0.0.18

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';
@@ -2380,6 +2380,251 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
2380
2380
  args: ['document:keydown.escape']
2381
2381
  }] } });
2382
2382
 
2383
+ const DAY_MS = 86_400_000;
2384
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
2385
+ const ISO_DAYS = [1, 2, 3, 4, 5, 6, 7];
2386
+ const STATE_TEXT = {
2387
+ done: 'done',
2388
+ empty: 'not done',
2389
+ off: 'not scheduled',
2390
+ 'out-of-range': 'unavailable',
2391
+ padding: ''
2392
+ };
2393
+ 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] ' +
2394
+ 'outline-none focus-visible:ring-2 focus-visible:ring-[var(--cxs-color-focus)] focus-visible:ring-offset-1';
2395
+ const CELL_STATE_CLASSES = {
2396
+ done: 'bg-[var(--cxs-habit-grid-accent,var(--cxs-color-primary))]',
2397
+ empty: 'border border-[var(--cxs-color-border)] bg-[var(--cxs-color-surface-hover)]',
2398
+ off: 'border border-dashed border-[var(--cxs-color-border)] bg-transparent opacity-50',
2399
+ 'out-of-range': 'bg-[var(--cxs-color-surface-hover)] opacity-30',
2400
+ padding: 'invisible'
2401
+ };
2402
+ const INTERACTIVE_CLASSES = 'cursor-pointer hover:opacity-80';
2403
+ function parseIsoDate(value) {
2404
+ if (!ISO_DATE.test(value)) {
2405
+ return null;
2406
+ }
2407
+ const [year, month, day] = value.split('-').map(Number);
2408
+ const time = Date.UTC(year, month - 1, day);
2409
+ return formatIsoDate(time) === value ? time : null;
2410
+ }
2411
+ function formatIsoDate(time) {
2412
+ return new Date(time).toISOString().slice(0, 10);
2413
+ }
2414
+ function isoDayOf(time) {
2415
+ return (((new Date(time).getUTCDay() + 6) % 7) + 1);
2416
+ }
2417
+ /**
2418
+ * GitHub-style grid of days for habit tracking. Columns are Monday–Sunday weeks; rows are days of the week.
2419
+ * Presentational only: it renders the dates it is given and emits `dayToggle`; the parent owns the data.
2420
+ */
2421
+ class CxsHabitGridComponent {
2422
+ elementRef = inject(ElementRef);
2423
+ /** First date shown, as `yyyy-MM-dd`. */
2424
+ from = '';
2425
+ /** Last date shown, as `yyyy-MM-dd`. */
2426
+ to = '';
2427
+ /** The user's local today; later dates are out of range. */
2428
+ today = '';
2429
+ /** The habit's start date; earlier dates are out of range. */
2430
+ startDate = null;
2431
+ /** Dates marked done, as `yyyy-MM-dd`. */
2432
+ doneDates = [];
2433
+ /** Scheduled ISO days of week; `null` means every day is scheduled. */
2434
+ scheduledDays = null;
2435
+ /** Weekly target; when set, each week shows `done/target`. */
2436
+ weeklyTarget = null;
2437
+ /** Optional colour for done cells; defaults to `--cxs-color-primary`. */
2438
+ accentColor = null;
2439
+ /** Whether done and empty cells can be toggled. */
2440
+ interactive = false;
2441
+ /** Accessible name of the grid. */
2442
+ ariaLabel = 'Habit history';
2443
+ /** Locale for accessible date labels. */
2444
+ locale = 'en-GB';
2445
+ /** Emitted when the user toggles a `done` or `empty` cell in interactive mode. */
2446
+ dayToggle = new EventEmitter();
2447
+ dayRows = ISO_DAYS;
2448
+ weeks = [];
2449
+ focusedDate = null;
2450
+ rangeStart = null;
2451
+ rangeEnd = null;
2452
+ get hasWeeklyProgress() {
2453
+ return this.weeklyTarget != null && this.weeklyTarget > 0;
2454
+ }
2455
+ ngOnChanges() {
2456
+ this.weeks = this.buildWeeks();
2457
+ if (!this.focusedDate || !this.isWithinRange(this.focusedDate)) {
2458
+ this.focusedDate = this.initialFocusDate();
2459
+ }
2460
+ }
2461
+ weekdayLabel(isoDay) {
2462
+ // 2024-01-01 is a Monday.
2463
+ return new Intl.DateTimeFormat(this.locale, { weekday: 'short', timeZone: 'UTC' }).format(new Date(Date.UTC(2024, 0, isoDay)));
2464
+ }
2465
+ dayAt(week, isoDay) {
2466
+ return week.days[isoDay - 1];
2467
+ }
2468
+ cellClass(day) {
2469
+ const classes = [CELL_BASE_CLASSES, CELL_STATE_CLASSES[day.state]];
2470
+ if (this.isToggleable(day)) {
2471
+ classes.push(INTERACTIVE_CLASSES);
2472
+ }
2473
+ return classes.join(' ');
2474
+ }
2475
+ tabIndexFor(day) {
2476
+ if (day.state === 'padding') {
2477
+ return null;
2478
+ }
2479
+ return day.date === this.focusedDate ? 0 : -1;
2480
+ }
2481
+ isToggleable(day) {
2482
+ return this.interactive && (day.state === 'done' || day.state === 'empty');
2483
+ }
2484
+ onCellClick(day) {
2485
+ if (day.state === 'padding') {
2486
+ return;
2487
+ }
2488
+ this.focusedDate = day.date;
2489
+ this.toggle(day);
2490
+ }
2491
+ onCellKeydown(event, day) {
2492
+ const moves = { ArrowUp: -1, ArrowDown: 1, ArrowLeft: -7, ArrowRight: 7 };
2493
+ if (event.key === 'Enter' || event.key === ' ') {
2494
+ event.preventDefault();
2495
+ this.toggle(day);
2496
+ return;
2497
+ }
2498
+ const delta = moves[event.key];
2499
+ if (delta === undefined) {
2500
+ return;
2501
+ }
2502
+ event.preventDefault();
2503
+ const current = parseIsoDate(day.date);
2504
+ if (current === null || this.rangeStart === null || this.rangeEnd === null) {
2505
+ return;
2506
+ }
2507
+ const target = Math.min(Math.max(current + delta * DAY_MS, this.rangeStart), this.rangeEnd);
2508
+ this.focusedDate = formatIsoDate(target);
2509
+ queueMicrotask(() => this.focusCell(this.focusedDate));
2510
+ }
2511
+ toggle(day) {
2512
+ if (!this.isToggleable(day)) {
2513
+ return;
2514
+ }
2515
+ this.dayToggle.emit({ date: day.date, done: day.state === 'empty' });
2516
+ }
2517
+ focusCell(date) {
2518
+ if (!date) {
2519
+ return;
2520
+ }
2521
+ this.elementRef.nativeElement.querySelector(`[data-date="${date}"]`)?.focus();
2522
+ }
2523
+ isWithinRange(date) {
2524
+ const time = parseIsoDate(date);
2525
+ return time !== null && this.rangeStart !== null && this.rangeEnd !== null
2526
+ && time >= this.rangeStart && time <= this.rangeEnd;
2527
+ }
2528
+ initialFocusDate() {
2529
+ if (this.rangeStart === null || this.rangeEnd === null) {
2530
+ return null;
2531
+ }
2532
+ const today = parseIsoDate(this.today);
2533
+ const target = today === null ? this.rangeEnd : Math.min(Math.max(today, this.rangeStart), this.rangeEnd);
2534
+ return formatIsoDate(target);
2535
+ }
2536
+ buildWeeks() {
2537
+ this.rangeStart = parseIsoDate(this.from);
2538
+ this.rangeEnd = parseIsoDate(this.to);
2539
+ if (this.rangeStart === null || this.rangeEnd === null || this.rangeStart > this.rangeEnd) {
2540
+ this.rangeStart = null;
2541
+ this.rangeEnd = null;
2542
+ return [];
2543
+ }
2544
+ const done = new Set(this.doneDates);
2545
+ const scheduled = this.scheduledDays ? new Set(this.scheduledDays) : null;
2546
+ const today = parseIsoDate(this.today);
2547
+ const start = this.startDate ? parseIsoDate(this.startDate) : null;
2548
+ const labelFormat = new Intl.DateTimeFormat(this.locale, {
2549
+ weekday: 'long',
2550
+ day: 'numeric',
2551
+ month: 'long',
2552
+ year: 'numeric',
2553
+ timeZone: 'UTC'
2554
+ });
2555
+ const weekFormat = new Intl.DateTimeFormat(this.locale, { day: 'numeric', month: 'short', timeZone: 'UTC' });
2556
+ const firstMonday = this.rangeStart - (isoDayOf(this.rangeStart) - 1) * DAY_MS;
2557
+ const weeks = [];
2558
+ for (let weekStart = firstMonday; weekStart <= this.rangeEnd; weekStart += 7 * DAY_MS) {
2559
+ const days = ISO_DAYS.map((isoDay) => {
2560
+ const time = weekStart + (isoDay - 1) * DAY_MS;
2561
+ const date = formatIsoDate(time);
2562
+ const state = this.stateOf(time, date, isoDay, done, scheduled, start, today);
2563
+ const label = state === 'padding' ? '' : `${labelFormat.format(time)}, ${STATE_TEXT[state]}`;
2564
+ return { date, isoDay, state, label };
2565
+ });
2566
+ const doneCount = days.filter((d) => d.state === 'done').length;
2567
+ const target = this.hasWeeklyProgress ? this.weeklyTarget : null;
2568
+ weeks.push({
2569
+ start: formatIsoDate(weekStart),
2570
+ days,
2571
+ doneCount,
2572
+ progressLabel: target === null ? null : `${doneCount}/${target}`,
2573
+ progressAriaLabel: target === null ? null : `Week of ${weekFormat.format(weekStart)}: ${doneCount} of ${target}`
2574
+ });
2575
+ }
2576
+ return weeks;
2577
+ }
2578
+ stateOf(time, date, isoDay, done, scheduled, start, today) {
2579
+ if (time < this.rangeStart || time > this.rangeEnd) {
2580
+ return 'padding';
2581
+ }
2582
+ if ((start !== null && time < start) || (today !== null && time > today)) {
2583
+ return 'out-of-range';
2584
+ }
2585
+ if (scheduled && !scheduled.has(isoDay)) {
2586
+ return 'off';
2587
+ }
2588
+ return done.has(date) ? 'done' : 'empty';
2589
+ }
2590
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: CxsHabitGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2591
+ 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 });
2592
+ }
2593
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: CxsHabitGridComponent, decorators: [{
2594
+ type: Component,
2595
+ args: [{ selector: 'cxs-habit-grid', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: {
2596
+ class: 'block',
2597
+ '[style.--cxs-habit-grid-accent]': 'accentColor || null'
2598
+ }, 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" }]
2599
+ }], propDecorators: { from: [{
2600
+ type: Input,
2601
+ args: [{ required: true }]
2602
+ }], to: [{
2603
+ type: Input,
2604
+ args: [{ required: true }]
2605
+ }], today: [{
2606
+ type: Input,
2607
+ args: [{ required: true }]
2608
+ }], startDate: [{
2609
+ type: Input
2610
+ }], doneDates: [{
2611
+ type: Input
2612
+ }], scheduledDays: [{
2613
+ type: Input
2614
+ }], weeklyTarget: [{
2615
+ type: Input
2616
+ }], accentColor: [{
2617
+ type: Input
2618
+ }], interactive: [{
2619
+ type: Input
2620
+ }], ariaLabel: [{
2621
+ type: Input
2622
+ }], locale: [{
2623
+ type: Input
2624
+ }], dayToggle: [{
2625
+ type: Output
2626
+ }] } });
2627
+
2383
2628
  const BASE_CLASSES$3 = 'w-full rounded-[var(--cxs-radius-md)] border border-[var(--cxs-color-border)] ' +
2384
2629
  'bg-[var(--cxs-color-surface)] text-[var(--cxs-color-on-surface)] ' +
2385
2630
  'placeholder:text-[var(--cxs-color-on-surface-muted)] transition-colors ' +
@@ -4620,5 +4865,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
4620
4865
  * Generated bundle index. Do not edit.
4621
4866
  */
4622
4867
 
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 };
4868
+ 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
4869
  //# sourceMappingURL=cerxos-ui.mjs.map