Skip to content
Navigation

Calendar

Added 14 October 2026

React

Codecomponents/react/calendar-demo.tsx
import {useState} from 'react';
import type {DateRange} from 'react-day-picker';
import {Calendar} from '@stroeer-labs/design-library/react';

export function CalendarDemo() {
    const [range, setRange] = useState<DateRange | undefined>();
    const [single, setSingle] = useState<Date | undefined>();

    return (
        <div className="space-y-6">
            <div className="space-y-2">
                <p className="text-xs font-medium text-muted-foreground">Range (2 months)</p>
                <Calendar
                    mode="range"
                    numberOfMonths={2}
                    selected={range}
                    onSelect={setRange}
                    weekStartsOn={1}
                    showOutsideDays={false}
                    className="rounded-md border border-border"
                />
            </div>

            <div className="space-y-2">
                <p className="text-xs font-medium text-muted-foreground">Single date</p>
                <Calendar mode="single" selected={single} onSelect={setSingle} weekStartsOn={1} className="rounded-md border border-border" />
            </div>
        </div>
    );
}

Angular

Codecomponents/angular/calendar-demo.ts
import {Component, computed, signal} from '@angular/core';
import {NgIcon, provideIcons} from '@ng-icons/core';
import {lucideChevronLeft, lucideChevronRight} from '@ng-icons/lucide';
import {HlmButton, HlmCalendarImports, buildCalendarI18n, provideBrnCalendarI18n} from '@stroeer-labs/design-library/angular';

// spartan has no native multi-month range calendar, so the "Range (2 months)" demo
// composes two hlm-calendar-range instances sharing one start/end with linked navigation
// (the same approach the DateRangePicker uses internally).
@Component({
    selector: 'dl-calendar-demo',
    standalone: true,
    imports: [HlmCalendarImports, HlmButton, NgIcon],
    viewProviders: [provideIcons({lucideChevronLeft, lucideChevronRight})],
    // English calendar locale (matches React), independent of the OS/browser locale.
    providers: [provideBrnCalendarI18n(buildCalendarI18n('en'))],
    template: `
    <div class="space-y-6">
      <div class="space-y-2">
        <p class="text-xs font-medium text-muted-foreground">Range (2 months)</p>
        <div class="relative w-fit rounded-md border border-border">
          <div
            class="pointer-events-none absolute inset-x-0 top-0 z-10 flex items-center justify-between p-3"
          >
            <button
              hlmBtn
              variant="ghost"
              class="pointer-events-auto size-8 p-0"
              (click)="shift(-1)"
            >
              <ng-icon name="lucideChevronLeft" />
            </button>
            <button
              hlmBtn
              variant="ghost"
              class="pointer-events-auto size-8 p-0"
              (click)="shift(1)"
            >
              <ng-icon name="lucideChevronRight" />
            </button>
          </div>
          <div class="flex">
            @for (month of months(); track key(month)) {
              <hlm-calendar-range
                class="p-3 [&_.order-first]:invisible [&_.order-last]:invisible [&_[data-outside-month]]:invisible"
                [defaultFocusedDate]="month"
                [startDate]="start()"
                [endDate]="end()"
                [weekStartsOn]="1"
                (startDateChange)="onStart($event)"
                (endDateChange)="onEnd($event)"
              />
            }
          </div>
        </div>
      </div>

      <div class="space-y-2">
        <p class="text-xs font-medium text-muted-foreground">Single date</p>
        <hlm-calendar
          class="w-fit rounded-md border border-border"
          [date]="single()"
          (dateChange)="single.set($event)"
          [weekStartsOn]="1"
        />
      </div>
    </div>
  `,
})
export class CalendarDemoComponent {
    protected readonly single = signal<Date | undefined>(undefined);
    protected readonly start = signal<Date | undefined>(undefined);
    protected readonly end = signal<Date | undefined>(undefined);

    protected readonly anchorMonth = signal(new Date(new Date().getFullYear(), new Date().getMonth(), 1));
    protected readonly months = computed(() => [this.anchorMonth(), this.addMonths(this.anchorMonth(), 1)]);

    protected shift(delta: number): void {
        this.anchorMonth.update((m) => this.addMonths(m, delta));
    }

    protected onStart(date: Date | undefined): void {
        this.start.set(date);
        this.end.set(undefined);
    }

    protected onEnd(date: Date | undefined): void {
        if (date) {
            this.end.set(date);
        }
    }

    protected key(date: Date): number {
        return date.getFullYear() * 12 + date.getMonth();
    }

    private addMonths(date: Date, count: number): Date {
        return new Date(date.getFullYear(), date.getMonth() + count, 1);
    }
}