{
  "name": "DatePicker",
  "category": "Forms",
  "subcategory": "Specialized",
  "description": "Calendar popover for picking a single date or range.",
  "thumbnail": "/preview-cards/date-picker.png",
  "hasDocsPage": true,
  "nugetPackage": "Lumeo",
  "files": [
    "UI/DatePicker/DatePicker.razor",
    "UI/DatePicker/DateRangePicker.razor",
    "UI/DatePicker/DateWheelPicker.razor"
  ],
  "dependencies": [
    "calendar",
    "popover",
    "time-picker"
  ],
  "packageDependencies": [
    "Blazicons.Lucide"
  ],
  "cssVars": [
    "--color-accent",
    "--color-accent-foreground",
    "--color-border",
    "--color-destructive",
    "--color-foreground",
    "--color-input",
    "--color-muted",
    "--color-muted-foreground",
    "--color-primary",
    "--radius"
  ],
  "api": {
    "name": "DatePicker",
    "category": "Forms",
    "subcategory": "Specialized",
    "description": "Calendar popover for picking a single date or range.",
    "nugetPackage": "Lumeo",
    "files": [
      "UI/DatePicker/DatePicker.razor",
      "UI/DatePicker/DateRangePicker.razor",
      "UI/DatePicker/DateWheelPicker.razor"
    ],
    "namespace": "Lumeo",
    "inheritsFrom": null,
    "implements": [],
    "parameters": [
      {
        "name": "Variant",
        "type": "DatePickerVariant",
        "default": "DatePickerVariant.Calendar",
        "description": "Picker visual style. Defaults to Calendar.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "FormField",
        "type": "FormField.FormFieldContext?",
        "default": null,
        "description": null,
        "isCascading": true,
        "captureUnmatched": false
      },
      {
        "name": "Required",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Invalid",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ErrorText",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "HelperText",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Label",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Name",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Value",
        "type": "DateOnly?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ValueChanged",
        "type": "EventCallback<DateOnly?>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "DateTimeValue",
        "type": "DateTime?",
        "default": null,
        "description": "Optional DateTime? binding. When set, takes precedence over Value. Changes fire both ValueChanged (DateOnly?) and DateTimeValueChanged (DateTime? at midnight).",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "DateTimeValueChanged",
        "type": "EventCallback<DateTime?>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Placeholder",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Format",
        "type": "string?",
        "default": null,
        "description": "Date format string. When null (default), the display falls back to the current culture's ShortDatePattern — so a de-DE user sees 15.03.2026 and an en-US user sees 3/15/2026 automatically.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Culture",
        "type": "System.Globalization.CultureInfo?",
        "default": null,
        "description": "Overrides CurrentCulture for parsing and formatting within this picker. Leave null to honour ASP.NET request localization.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "MinDate",
        "type": "DateOnly?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "MaxDate",
        "type": "DateOnly?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "IsDateDisabled",
        "type": "Func<DateTime, bool>?",
        "default": null,
        "description": "Custom predicate to disable specific dates (e.g. booked rooms, unavailable slots). Returns true for dates that should not be selectable.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "DateClass",
        "type": "Func<DateTime, string>?",
        "default": null,
        "description": "Custom CSS classes per day cell (e.g. status backgrounds for booking calendars).",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "DateTooltip",
        "type": "Func<DateTime, string?>?",
        "default": null,
        "description": "Per-date tooltip text rendered as the day cell's title attribute (native browser tooltip).",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "DateBadge",
        "type": "Func<DateTime, RenderFragment?>?",
        "default": null,
        "description": "Per-date indicator slot (status dots, count badges, icons) rendered absolutely positioned in the bottom-right of the day cell.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Presets",
        "type": "List<DatePickerPreset>?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Mode",
        "type": "DatePickerMode",
        "default": "DatePickerMode.Single",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "RangeStart",
        "type": "DateOnly?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "RangeStartChanged",
        "type": "EventCallback<DateOnly?>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "RangeEnd",
        "type": "DateOnly?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "RangeEndChanged",
        "type": "EventCallback<DateOnly?>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ShowTimePicker",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Time",
        "type": "TimeSpan?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "TimeChanged",
        "type": "EventCallback<TimeSpan?>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Disabled",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Clearable",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "TriggerContent",
        "type": "RenderFragment?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "NumberOfMonths",
        "type": "int",
        "default": "1",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Inline",
        "type": "bool",
        "default": null,
        "description": "Optional controlled-mode override for the popover open state. When non-null, the consumer holds the source of truth — the picker no longer mutates its internal state and instead raises OpenChanged. Use this when the picker lives inside a parent that re-renders heavily (e.g. an EditForm with active DataAnnotationsValidator) and the popover would otherwise close mid-click during a parameter cascade. When null (default), the picker falls back to its internal open state and behaves exactly as before.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "FirstDayOfWeek",
        "type": "DayOfWeek?",
        "default": null,
        "description": "Leftmost column day of week. Null means culture default.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ShowWeekNumbers",
        "type": "bool",
        "default": null,
        "description": "When true, renders an ISO week-number column.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Open",
        "type": "bool?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OpenChanged",
        "type": "EventCallback<bool>",
        "default": null,
        "description": "Fires whenever the popover would open or close. Pair with Open via @bind-Open for the controlled-mode pattern described above.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Class",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "AdditionalAttributes",
        "type": "Dictionary<string, object>?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": true
      }
    ],
    "events": [
      {
        "name": "ValueChanged",
        "type": "EventCallback<DateOnly?>",
        "description": null
      },
      {
        "name": "DateTimeValueChanged",
        "type": "EventCallback<DateTime?>",
        "description": null
      },
      {
        "name": "RangeStartChanged",
        "type": "EventCallback<DateOnly?>",
        "description": null
      },
      {
        "name": "RangeEndChanged",
        "type": "EventCallback<DateOnly?>",
        "description": null
      },
      {
        "name": "TimeChanged",
        "type": "EventCallback<TimeSpan?>",
        "description": null
      },
      {
        "name": "OpenChanged",
        "type": "EventCallback<bool>",
        "description": "Fires whenever the popover would open or close. Pair with Open via @bind-Open for the controlled-mode pattern described above."
      }
    ],
    "enums": [
      {
        "name": "DatePickerMode",
        "values": [
          "Single",
          "Range",
          "Month",
          "Year"
        ],
        "description": null
      },
      {
        "name": "DatePickerVariant",
        "values": [
          "Calendar",
          "Wheel"
        ],
        "description": "Visual style of the picker. Calendar renders the existing month-grid Calendar (default). Wheel renders three iOS-style snap-scrolling columns (day / month / year) — designed for mobile / touch UIs."
      }
    ],
    "records": [
      {
        "name": "DatePickerPreset",
        "signature": "DatePickerPreset(string Label, DateOnly Date)",
        "description": null
      }
    ],
    "cssVars": [
      "--color-accent",
      "--color-accent-foreground",
      "--color-border",
      "--color-destructive",
      "--color-foreground",
      "--color-input",
      "--color-muted",
      "--color-muted-foreground",
      "--color-primary",
      "--radius"
    ],
    "examples": [
      {
        "title": "Default",
        "code": "<DatePicker @bind-Value=\"_selectedDate\" />"
      },
      {
        "title": "With Label",
        "code": "<Label>Date of birth</Label>\n<DatePicker @bind-Value=\"_date\" Placeholder=\"Pick your date of birth\" />"
      },
      {
        "title": "Custom Format",
        "code": "<DatePicker @bind-Value=\"_date\" Format=\"yyyy-MM-dd\" Placeholder=\"YYYY-MM-DD\" />"
      },
      {
        "title": "Min & Max Date",
        "code": "<DatePicker @bind-Value=\"_date\" MinDate=\"@_minDate\" MaxDate=\"@_maxDate\" Placeholder=\"Select within range\" />\n\n@code {\n    private DateOnly? _date;\n    private DateOnly _minDate = DateOnly.FromDateTime(DateTime.Today);\n    private DateOnly _maxDate = DateOnly.FromDateTime(DateTime.Today.AddDays(30));\n}"
      }
    ],
    "subComponents": {
      "DateRangePicker": {
        "componentName": "DateRangePicker",
        "fileName": "DateRangePicker.razor",
        "namespace": "Lumeo",
        "inheritsFrom": null,
        "implements": [],
        "parameters": [
          {
            "name": "StartDate",
            "type": "DateOnly?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "StartDateChanged",
            "type": "EventCallback<DateOnly?>",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "EndDate",
            "type": "DateOnly?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "EndDateChanged",
            "type": "EventCallback<DateOnly?>",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "MinDate",
            "type": "DateOnly?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "MaxDate",
            "type": "DateOnly?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "IsDateDisabled",
            "type": "Func<DateTime, bool>?",
            "default": null,
            "description": "Custom predicate to disable specific dates (e.g. booked rooms). Returns true for dates that should not be selectable.",
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "DateClass",
            "type": "Func<DateTime, string>?",
            "default": null,
            "description": "Custom CSS classes per day cell — forwarded to inner Calendar.",
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "DateTooltip",
            "type": "Func<DateTime, string?>?",
            "default": null,
            "description": "Per-date tooltip text rendered as the day cell's title.",
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "DateBadge",
            "type": "Func<DateTime, RenderFragment?>?",
            "default": null,
            "description": "Per-date indicator slot rendered absolutely positioned in the bottom-right of the day cell.",
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "Placeholder",
            "type": "string?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "Format",
            "type": "string?",
            "default": null,
            "description": "Date format. When null (default) the inner DatePicker falls back to the effective culture's ShortDatePattern — so a de-DE user sees 15.03.2026 and an en-US user sees 3/15/2026 automatically.",
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "Presets",
            "type": "List<DateRangePreset>?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "Disabled",
            "type": "bool",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "Clearable",
            "type": "bool",
            "default": "true",
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "FirstDayOfWeek",
            "type": "DayOfWeek?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "ShowWeekNumbers",
            "type": "bool",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "Class",
            "type": "string?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "AdditionalAttributes",
            "type": "Dictionary<string, object>?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": true
          }
        ],
        "events": [
          {
            "name": "StartDateChanged",
            "type": "EventCallback<DateOnly?>",
            "description": null
          },
          {
            "name": "EndDateChanged",
            "type": "EventCallback<DateOnly?>",
            "description": null
          }
        ],
        "enums": [],
        "records": [],
        "parseFailed": false,
        "parseError": null
      },
      "DateWheelPicker": {
        "componentName": "DateWheelPicker",
        "fileName": "DateWheelPicker.razor",
        "namespace": "Lumeo",
        "inheritsFrom": null,
        "implements": [
          "IDisposable"
        ],
        "parameters": [
          {
            "name": "Value",
            "type": "DateOnly?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "ValueChanged",
            "type": "EventCallback<DateOnly?>",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "Min",
            "type": "DateOnly?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "Max",
            "type": "DateOnly?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "Culture",
            "type": "CultureInfo?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "Class",
            "type": "string?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "AdditionalAttributes",
            "type": "Dictionary<string, object>?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": true
          }
        ],
        "events": [
          {
            "name": "ValueChanged",
            "type": "EventCallback<DateOnly?>",
            "description": null
          }
        ],
        "enums": [],
        "records": [],
        "parseFailed": false,
        "parseError": null
      }
    },
    "parseFailed": false,
    "parseError": null
  },
  "$schema": "https://lumeo.nativ.sh/registry-component-schema.json",
  "key": "date-picker",
  "docsUrl": "https://lumeo.nativ.sh/components/date-picker",
  "sourceUrl": [
    {
      "file": "UI/DatePicker/DatePicker.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/DatePicker/DatePicker.razor"
    },
    {
      "file": "UI/DatePicker/DateRangePicker.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/DatePicker/DateRangePicker.razor"
    },
    {
      "file": "UI/DatePicker/DateWheelPicker.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/DatePicker/DateWheelPicker.razor"
    }
  ],
  "source": [
    {
      "path": "UI/DatePicker/DatePicker.razor",
      "content": "@namespace Lumeo\n\n@if (Label is not null && !InsideFormField)\n{\n    <label class=\"text-sm font-medium leading-none\">\n        @Label\n        @if (Required)\n        {\n            <span class=\"text-destructive ms-0.5\">*</span>\n        }\n    </label>\n}\n\n@if (Variant == DatePickerVariant.Wheel)\n{\n    @* iOS-style 3-column wheel picker. v1 limitations:\n       - Only honours single-date Mode (Range/Month/Year fall back to a day/month/year wheel).\n       - No Presets sidebar.\n       - ShowTimePicker is supported via a stacked TimeWheelPicker below the date wheel.\n    *@\n    @if (Inline)\n    {\n        <div>\n            <DateWheelPicker Value=\"@Value\" ValueChanged=\"HandleDateSelected\" Min=\"@MinDate\" Max=\"@MaxDate\" Culture=\"@Culture\" />\n            @if (ShowTimePicker)\n            {\n                <div class=\"border-t border-border/40 p-3\">\n                    <TimePicker Value=\"@Time\" ValueChanged=\"HandleTimeChanged\" Culture=\"@Culture\" Variant=\"TimePicker.TimePickerVariant.Wheel\" />\n                </div>\n            }\n        </div>\n    }\n    else\n    {\n        <Popover @bind-Open=\"_isOpen\">\n            <PopoverTrigger>\n                <button type=\"button\"\n                        name=\"@Name\"\n                        aria-required=\"@Required.ToString().ToLower()\"\n                        aria-invalid=\"@EffectiveInvalid.ToString().ToLower()\"\n                        class=\"@CssClass\"\n                        disabled=\"@Disabled\"\n                        @attributes=\"AdditionalAttributes\">\n                    <Blazicon Svg=\"Lucide.Calendar\" class=\"me-2 h-4 w-4 shrink-0\" />\n                    @if (TriggerContent != null)\n                    {\n                        @TriggerContent\n                    }\n                    else if (Value.HasValue)\n                    {\n                        <span>@Value.Value.ToString(EffectiveFormat, EffectiveCulture)</span>\n                    }\n                    else\n                    {\n                        <span class=\"text-muted-foreground\">@EffectivePlaceholder</span>\n                    }\n                    @if (Clearable && HasValue)\n                    {\n                        <button type=\"button\" class=\"ms-auto ps-1 hover:text-foreground\" aria-label=\"@L[\"Common.Clear\"]\" @onclick=\"HandleClear\" @onclick:stopPropagation=\"true\">\n                            <Blazicon Svg=\"Lucide.X\" class=\"h-3.5 w-3.5\" />\n                        </button>\n                    }\n                </button>\n            </PopoverTrigger>\n            <PopoverContent Align=\"PopoverContent.PopoverAlign.Start\" Class=\"w-[260px] p-3\">\n                <DateWheelPicker Value=\"@Value\" ValueChanged=\"HandleDateSelected\" Min=\"@MinDate\" Max=\"@MaxDate\" Culture=\"@Culture\" />\n                @if (ShowTimePicker)\n                {\n                    <div class=\"border-t border-border/40 mt-3 pt-3\">\n                        <TimePicker Value=\"@Time\" ValueChanged=\"HandleTimeChanged\" Culture=\"@Culture\" Variant=\"TimePicker.TimePickerVariant.Wheel\" />\n                    </div>\n                }\n            </PopoverContent>\n        </Popover>\n    }\n}\nelse if (Inline)\n{\n        @if (Mode == DatePickerMode.Month)\n        {\n            <Calendar Value=\"@Value\" ValueChanged=\"HandleDateSelected\" MinDate=\"@MinDate\" MaxDate=\"@MaxDate\" IsDateDisabled=\"@IsDateDisabled\" DateClass=\"@DateClass\" DateTooltip=\"@DateTooltip\" DateBadge=\"@DateBadge\" InitialView=\"@Calendar.CalendarView.Months\" FirstDayOfWeek=\"@FirstDayOfWeek\" ShowWeekNumbers=\"@ShowWeekNumbers\" />\n        }\n        else if (Mode == DatePickerMode.Year)\n        {\n            <Calendar Value=\"@Value\" ValueChanged=\"HandleDateSelected\" MinDate=\"@MinDate\" MaxDate=\"@MaxDate\" IsDateDisabled=\"@IsDateDisabled\" DateClass=\"@DateClass\" DateTooltip=\"@DateTooltip\" DateBadge=\"@DateBadge\" InitialView=\"@Calendar.CalendarView.Years\" FirstDayOfWeek=\"@FirstDayOfWeek\" ShowWeekNumbers=\"@ShowWeekNumbers\" />\n        }\n        else if (Mode == DatePickerMode.Range)\n        {\n            @if (Presets != null && Presets.Count > 0)\n            {\n                <div class=\"flex\">\n                    <div class=\"flex flex-col gap-1 border-e border-border/40 p-2 min-w-[120px]\">\n                        @foreach (var preset in Presets)\n                        {\n                            var p = preset;\n                            <button type=\"button\" class=\"text-sm px-3 py-1.5 rounded-md text-left hover:bg-accent hover:text-accent-foreground transition-colors\" @onclick=\"() => HandlePresetSelected(p)\">\n                                @p.Label\n                            </button>\n                        }\n                    </div>\n                    <Calendar IsRange=\"true\" RangeStart=\"@RangeStart\" RangeStartChanged=\"HandleRangeStartChanged\"\n                              RangeEnd=\"@RangeEnd\" RangeEndChanged=\"HandleRangeEndChanged\"\n                              MinDate=\"@MinDate\" MaxDate=\"@MaxDate\" IsDateDisabled=\"@IsDateDisabled\" DateClass=\"@DateClass\" DateTooltip=\"@DateTooltip\" DateBadge=\"@DateBadge\" NumberOfMonths=\"@EffectiveNumberOfMonths\" FirstDayOfWeek=\"@FirstDayOfWeek\" ShowWeekNumbers=\"@ShowWeekNumbers\" />\n                </div>\n            }\n            else\n            {\n                <Calendar IsRange=\"true\" RangeStart=\"@RangeStart\" RangeStartChanged=\"HandleRangeStartChanged\"\n                          RangeEnd=\"@RangeEnd\" RangeEndChanged=\"HandleRangeEndChanged\"\n                          MinDate=\"@MinDate\" MaxDate=\"@MaxDate\" IsDateDisabled=\"@IsDateDisabled\" DateClass=\"@DateClass\" DateTooltip=\"@DateTooltip\" DateBadge=\"@DateBadge\" NumberOfMonths=\"@EffectiveNumberOfMonths\" FirstDayOfWeek=\"@FirstDayOfWeek\" ShowWeekNumbers=\"@ShowWeekNumbers\" />\n            }\n        }\n        else\n        {\n            <div>\n                @if (Presets != null && Presets.Count > 0)\n                {\n                    <div class=\"flex\">\n                        <div class=\"flex flex-col gap-1 border-e border-border/40 p-2 min-w-[120px]\">\n                            @foreach (var preset in Presets)\n                            {\n                                var p = preset;\n                                <button type=\"button\" class=\"text-sm px-3 py-1.5 rounded-md text-left hover:bg-accent hover:text-accent-foreground transition-colors\" @onclick=\"() => HandlePresetSelected(p)\">\n                                    @p.Label\n                                </button>\n                            }\n                        </div>\n                        <Calendar Value=\"@Value\" ValueChanged=\"HandleDateSelected\" MinDate=\"@MinDate\" MaxDate=\"@MaxDate\" IsDateDisabled=\"@IsDateDisabled\" DateClass=\"@DateClass\" DateTooltip=\"@DateTooltip\" DateBadge=\"@DateBadge\" NumberOfMonths=\"@EffectiveNumberOfMonths\" FirstDayOfWeek=\"@FirstDayOfWeek\" ShowWeekNumbers=\"@ShowWeekNumbers\" />\n                    </div>\n                }\n                else\n                {\n                    <Calendar Value=\"@Value\" ValueChanged=\"HandleDateSelected\" MinDate=\"@MinDate\" MaxDate=\"@MaxDate\" IsDateDisabled=\"@IsDateDisabled\" DateClass=\"@DateClass\" DateTooltip=\"@DateTooltip\" DateBadge=\"@DateBadge\" NumberOfMonths=\"@EffectiveNumberOfMonths\" FirstDayOfWeek=\"@FirstDayOfWeek\" ShowWeekNumbers=\"@ShowWeekNumbers\" />\n                }\n\n                @if (ShowTimePicker)\n                {\n                    <div class=\"border-t border-border/40 p-3\">\n                        <TimePicker Value=\"@Time\" ValueChanged=\"HandleTimeChanged\" Culture=\"@Culture\" />\n                    </div>\n                }\n            </div>\n        }\n}\nelse\n{\n    <Popover @bind-Open=\"_isOpen\">\n        <PopoverTrigger>\n            <button type=\"button\"\n                    name=\"@Name\"\n                    aria-required=\"@Required.ToString().ToLower()\"\n                    aria-invalid=\"@EffectiveInvalid.ToString().ToLower()\"\n                    class=\"@CssClass\"\n                    disabled=\"@Disabled\"\n                    @attributes=\"AdditionalAttributes\">\n                <Blazicon Svg=\"Lucide.Calendar\" class=\"me-2 h-4 w-4 shrink-0\" />\n                @if (TriggerContent != null)\n                {\n                    @TriggerContent\n                }\n                else if (Mode == DatePickerMode.Range)\n                {\n                    @if (RangeStart.HasValue && RangeEnd.HasValue)\n                    {\n                        <span>@RangeStart.Value.ToString(EffectiveFormat, EffectiveCulture) - @RangeEnd.Value.ToString(EffectiveFormat, EffectiveCulture)</span>\n                    }\n                    else if (RangeStart.HasValue)\n                    {\n                        <span>@RangeStart.Value.ToString(EffectiveFormat, EffectiveCulture) - ...</span>\n                    }\n                    else\n                    {\n                        <span class=\"text-muted-foreground\">@EffectivePlaceholder</span>\n                    }\n                }\n                else if (Mode == DatePickerMode.Month)\n                {\n                    @if (Value.HasValue)\n                    {\n                        <span>@Value.Value.ToString(\"MMMM yyyy\", EffectiveCulture)</span>\n                    }\n                    else\n                    {\n                        <span class=\"text-muted-foreground\">@EffectivePlaceholder</span>\n                    }\n                }\n                else if (Mode == DatePickerMode.Year)\n                {\n                    @if (Value.HasValue)\n                    {\n                        <span>@Value.Value.Year</span>\n                    }\n                    else\n                    {\n                        <span class=\"text-muted-foreground\">@EffectivePlaceholder</span>\n                    }\n                }\n                else\n                {\n                    @if (Value.HasValue)\n                    {\n                        <span>@Value.Value.ToString(EffectiveFormat, EffectiveCulture)</span>\n                    }\n                    else\n                    {\n                        <span class=\"text-muted-foreground\">@EffectivePlaceholder</span>\n                    }\n                }\n    \n                @if (Clearable && HasValue)\n                {\n                    <button type=\"button\" class=\"ms-auto ps-1 hover:text-foreground\" aria-label=\"@L[\"Common.Clear\"]\" @onclick=\"HandleClear\" @onclick:stopPropagation=\"true\">\n                        <Blazicon Svg=\"Lucide.X\" class=\"h-3.5 w-3.5\" />\n                    </button>\n                }\n            </button>\n        </PopoverTrigger>\n        <PopoverContent Align=\"PopoverContent.PopoverAlign.Start\" Class=\"w-auto p-0\">\n            @if (Mode == DatePickerMode.Month)\n            {\n                <Calendar Value=\"@Value\" ValueChanged=\"HandleDateSelected\" MinDate=\"@MinDate\" MaxDate=\"@MaxDate\" IsDateDisabled=\"@IsDateDisabled\" DateClass=\"@DateClass\" DateTooltip=\"@DateTooltip\" DateBadge=\"@DateBadge\" InitialView=\"@Calendar.CalendarView.Months\" FirstDayOfWeek=\"@FirstDayOfWeek\" ShowWeekNumbers=\"@ShowWeekNumbers\" />\n            }\n            else if (Mode == DatePickerMode.Year)\n            {\n                <Calendar Value=\"@Value\" ValueChanged=\"HandleDateSelected\" MinDate=\"@MinDate\" MaxDate=\"@MaxDate\" IsDateDisabled=\"@IsDateDisabled\" DateClass=\"@DateClass\" DateTooltip=\"@DateTooltip\" DateBadge=\"@DateBadge\" InitialView=\"@Calendar.CalendarView.Years\" FirstDayOfWeek=\"@FirstDayOfWeek\" ShowWeekNumbers=\"@ShowWeekNumbers\" />\n            }\n            else if (Mode == DatePickerMode.Range)\n            {\n                @if (Presets != null && Presets.Count > 0)\n                {\n                    <div class=\"flex\">\n                        <div class=\"flex flex-col gap-1 border-e border-border/40 p-2 min-w-[120px]\">\n                            @foreach (var preset in Presets)\n                            {\n                                var p = preset;\n                                <button type=\"button\" class=\"text-sm px-3 py-1.5 rounded-md text-left hover:bg-accent hover:text-accent-foreground transition-colors\" @onclick=\"() => HandlePresetSelected(p)\">\n                                    @p.Label\n                                </button>\n                            }\n                        </div>\n                        <Calendar IsRange=\"true\" RangeStart=\"@RangeStart\" RangeStartChanged=\"HandleRangeStartChanged\"\n                                  RangeEnd=\"@RangeEnd\" RangeEndChanged=\"HandleRangeEndChanged\"\n                                  MinDate=\"@MinDate\" MaxDate=\"@MaxDate\" IsDateDisabled=\"@IsDateDisabled\" DateClass=\"@DateClass\" DateTooltip=\"@DateTooltip\" DateBadge=\"@DateBadge\" NumberOfMonths=\"@EffectiveNumberOfMonths\" FirstDayOfWeek=\"@FirstDayOfWeek\" ShowWeekNumbers=\"@ShowWeekNumbers\" />\n                    </div>\n                }\n                else\n                {\n                    <Calendar IsRange=\"true\" RangeStart=\"@RangeStart\" RangeStartChanged=\"HandleRangeStartChanged\"\n                              RangeEnd=\"@RangeEnd\" RangeEndChanged=\"HandleRangeEndChanged\"\n                              MinDate=\"@MinDate\" MaxDate=\"@MaxDate\" IsDateDisabled=\"@IsDateDisabled\" DateClass=\"@DateClass\" DateTooltip=\"@DateTooltip\" DateBadge=\"@DateBadge\" NumberOfMonths=\"@EffectiveNumberOfMonths\" FirstDayOfWeek=\"@FirstDayOfWeek\" ShowWeekNumbers=\"@ShowWeekNumbers\" />\n                }\n            }\n            else\n            {\n                <div>\n                    @if (Presets != null && Presets.Count > 0)\n                    {\n                        <div class=\"flex\">\n                            <div class=\"flex flex-col gap-1 border-e border-border/40 p-2 min-w-[120px]\">\n                                @foreach (var preset in Presets)\n                                {\n                                    var p = preset;\n                                    <button type=\"button\" class=\"text-sm px-3 py-1.5 rounded-md text-left hover:bg-accent hover:text-accent-foreground transition-colors\" @onclick=\"() => HandlePresetSelected(p)\">\n                                        @p.Label\n                                    </button>\n                                }\n                            </div>\n                            <Calendar Value=\"@Value\" ValueChanged=\"HandleDateSelected\" MinDate=\"@MinDate\" MaxDate=\"@MaxDate\" IsDateDisabled=\"@IsDateDisabled\" DateClass=\"@DateClass\" DateTooltip=\"@DateTooltip\" DateBadge=\"@DateBadge\" NumberOfMonths=\"@EffectiveNumberOfMonths\" FirstDayOfWeek=\"@FirstDayOfWeek\" ShowWeekNumbers=\"@ShowWeekNumbers\" />\n                        </div>\n                    }\n                    else\n                    {\n                        <Calendar Value=\"@Value\" ValueChanged=\"HandleDateSelected\" MinDate=\"@MinDate\" MaxDate=\"@MaxDate\" IsDateDisabled=\"@IsDateDisabled\" DateClass=\"@DateClass\" DateTooltip=\"@DateTooltip\" DateBadge=\"@DateBadge\" NumberOfMonths=\"@EffectiveNumberOfMonths\" FirstDayOfWeek=\"@FirstDayOfWeek\" ShowWeekNumbers=\"@ShowWeekNumbers\" />\n                    }\n    \n                    @if (ShowTimePicker)\n                    {\n                        <div class=\"border-t border-border/40 p-3\">\n                            <TimePicker Value=\"@Time\" ValueChanged=\"HandleTimeChanged\" Culture=\"@Culture\" />\n                        </div>\n                    }\n                </div>\n            }\n        </PopoverContent>\n    </Popover>\n}\n\n@if (!InsideFormField)\n{\n    @if (EffectiveInvalid && ErrorText is not null)\n    {\n        <p class=\"text-xs text-destructive\">@ErrorText</p>\n    }\n    else if (HelperText is not null)\n    {\n        <p class=\"text-xs text-muted-foreground\">@HelperText</p>\n    }\n}\n\n@inject Lumeo.Services.Localization.ILumeoLocalizer L\n\n@code {\n    public enum DatePickerMode { Single, Range, Month, Year }\n\n    /// <summary>\n    /// Visual style of the picker.\n    /// <see cref=\"DatePickerVariant.Calendar\"/> renders the existing month-grid Calendar (default).\n    /// <see cref=\"DatePickerVariant.Wheel\"/> renders three iOS-style snap-scrolling\n    /// columns (day / month / year) — designed for mobile / touch UIs.\n    /// </summary>\n    public enum DatePickerVariant { Calendar, Wheel }\n\n    /// <summary>Picker visual style. Defaults to <see cref=\"DatePickerVariant.Calendar\"/>.</summary>\n    [Parameter] public DatePickerVariant Variant { get; set; } = DatePickerVariant.Calendar;\n\n    [CascadingParameter] private FormField.FormFieldContext? FormField { get; set; }\n\n    [Parameter] public bool Required { get; set; }\n    [Parameter] public bool Invalid { get; set; }\n    [Parameter] public string? ErrorText { get; set; }\n    [Parameter] public string? HelperText { get; set; }\n    [Parameter] public string? Label { get; set; }\n    [Parameter] public string? Name { get; set; }\n\n    private bool EffectiveInvalid => Invalid || FormField?.HasError == true;\n    private bool InsideFormField => FormField is not null;\n\n    [Parameter] public DateOnly? Value { get; set; }\n    [Parameter] public EventCallback<DateOnly?> ValueChanged { get; set; }\n\n    /// <summary>\n    /// Optional DateTime? binding. When set, takes precedence over <see cref=\"Value\"/>.\n    /// Changes fire both <see cref=\"ValueChanged\"/> (DateOnly?) and <see cref=\"DateTimeValueChanged\"/> (DateTime? at midnight).\n    /// </summary>\n    [Parameter] public DateTime? DateTimeValue { get; set; }\n    [Parameter] public EventCallback<DateTime?> DateTimeValueChanged { get; set; }\n\n    [Parameter] public string? Placeholder { get; set; }\n\n    private string EffectivePlaceholder => Placeholder ?? (Mode == DatePickerMode.Range ? L[\"DateRange.Placeholder\"] : L[\"DatePicker.Placeholder\"]);\n    /// <summary>\n    /// Date format string. When null (default), the display falls back to the current\n    /// culture's <c>ShortDatePattern</c> — so a <c>de-DE</c> user sees <c>15.03.2026</c>\n    /// and an <c>en-US</c> user sees <c>3/15/2026</c> automatically.\n    /// </summary>\n    [Parameter] public string? Format { get; set; }\n\n    /// <summary>\n    /// Overrides <see cref=\"System.Globalization.CultureInfo.CurrentCulture\"/> for\n    /// parsing and formatting within this picker. Leave null to honour ASP.NET\n    /// request localization.\n    /// </summary>\n    [Parameter] public System.Globalization.CultureInfo? Culture { get; set; }\n\n    private System.Globalization.CultureInfo EffectiveCulture => Culture ?? System.Globalization.CultureInfo.CurrentCulture;\n    private string EffectiveFormat => Format ?? EffectiveCulture.DateTimeFormat.ShortDatePattern;\n    [Parameter] public DateOnly? MinDate { get; set; }\n    [Parameter] public DateOnly? MaxDate { get; set; }\n    /// <summary>Custom predicate to disable specific dates (e.g. booked rooms,\n    /// unavailable slots). Returns <c>true</c> for dates that should not be\n    /// selectable.</summary>\n    [Parameter] public Func<DateTime, bool>? IsDateDisabled { get; set; }\n    /// <summary>Custom CSS classes per day cell (e.g. status backgrounds for\n    /// booking calendars).</summary>\n    [Parameter] public Func<DateTime, string>? DateClass { get; set; }\n    /// <summary>Per-date tooltip text rendered as the day cell's <c>title</c>\n    /// attribute (native browser tooltip).</summary>\n    [Parameter] public Func<DateTime, string?>? DateTooltip { get; set; }\n    /// <summary>Per-date indicator slot (status dots, count badges, icons)\n    /// rendered absolutely positioned in the bottom-right of the day cell.</summary>\n    [Parameter] public Func<DateTime, RenderFragment?>? DateBadge { get; set; }\n    [Parameter] public List<DatePickerPreset>? Presets { get; set; }\n    [Parameter] public DatePickerMode Mode { get; set; } = DatePickerMode.Single;\n    [Parameter] public DateOnly? RangeStart { get; set; }\n    [Parameter] public EventCallback<DateOnly?> RangeStartChanged { get; set; }\n    [Parameter] public DateOnly? RangeEnd { get; set; }\n    [Parameter] public EventCallback<DateOnly?> RangeEndChanged { get; set; }\n    [Parameter] public bool ShowTimePicker { get; set; }\n    [Parameter] public TimeSpan? Time { get; set; }\n    [Parameter] public EventCallback<TimeSpan?> TimeChanged { get; set; }\n    [Parameter] public bool Disabled { get; set; }\n    [Parameter] public bool Clearable { get; set; }\n    [Parameter] public RenderFragment? TriggerContent { get; set; }\n    [Parameter] public int NumberOfMonths { get; set; } = 1;\n\n    /// <summary>\n    /// Optional controlled-mode override for the popover open state. When non-null,\n    /// the consumer holds the source of truth — the picker no longer mutates its\n    /// internal state and instead raises <see cref=\"OpenChanged\"/>. Use this when\n    /// the picker lives inside a parent that re-renders heavily (e.g. an\n    /// <c>EditForm</c> with active <c>DataAnnotationsValidator</c>) and the\n    /// popover would otherwise close mid-click during a parameter cascade.\n    /// When null (default), the picker falls back to its internal open state and\n    /// behaves exactly as before.\n    /// </summary>\n    /// <summary>When true, renders the calendar inline (always visible) without a popover wrapper.</summary>\n    [Parameter] public bool Inline { get; set; }\n\n    /// <summary>Leftmost column day of week. Null means culture default.</summary>\n    [Parameter] public DayOfWeek? FirstDayOfWeek { get; set; }\n\n    /// <summary>When true, renders an ISO week-number column.</summary>\n    [Parameter] public bool ShowWeekNumbers { get; set; }\n\n    [Parameter] public bool? Open { get; set; }\n\n    /// <summary>\n    /// Fires whenever the popover would open or close. Pair with\n    /// <see cref=\"Open\"/> via <c>@bind-Open</c> for the controlled-mode pattern\n    /// described above.\n    /// </summary>\n    [Parameter] public EventCallback<bool> OpenChanged { get; set; }\n\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    // Controlled-mode bridge: when Open is non-null, the parent owns the state and\n    // we never write to _internalOpen — every \"set\" just raises OpenChanged so the\n    // parent can decide. When Open is null, _internalOpen is the source of truth.\n    private bool _internalOpen;\n    private bool _isOpen\n    {\n        get => Open ?? _internalOpen;\n        set => SetIsOpen(value);\n    }\n\n    private void SetIsOpen(bool value)\n    {\n        if (Open is null)\n        {\n            _internalOpen = value;\n        }\n        if (OpenChanged.HasDelegate)\n        {\n            _ = OpenChanged.InvokeAsync(value);\n        }\n    }\n\n    private DateTime? _prevDateTimeValue;\n\n    protected override void OnParametersSet()\n    {\n        // DateTimeValue wins when it changes — sync Value from it.\n        if (DateTimeValue != _prevDateTimeValue)\n        {\n            _prevDateTimeValue = DateTimeValue;\n            Value = DateTimeValue.HasValue ? DateOnly.FromDateTime(DateTimeValue.Value) : null;\n        }\n    }\n\n    private int EffectiveNumberOfMonths => NumberOfMonths > 1 ? NumberOfMonths : (Mode == DatePickerMode.Range ? 2 : 1);\n\n    private bool HasValue => Mode == DatePickerMode.Range ? RangeStart.HasValue : Value.HasValue;\n\n    private string CssClass => $\"flex h-9 w-full items-center rounded-md border {(EffectiveInvalid ? \"border-destructive ring-1 ring-destructive/20\" : \"border-input\")} bg-transparent px-3 py-2 text-sm shadow-sm hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-50 {Class}\".Trim();\n\n    private async Task HandleDateSelected(DateOnly? date)\n    {\n        if (Mode == DatePickerMode.Month && date.HasValue)\n        {\n            Value = new DateOnly(date.Value.Year, date.Value.Month, 1);\n            await ValueChanged.InvokeAsync(Value);\n            await RaiseDateTimeValueChanged(Value);\n            _isOpen = false;\n        }\n        else if (Mode == DatePickerMode.Year && date.HasValue)\n        {\n            Value = new DateOnly(date.Value.Year, 1, 1);\n            await ValueChanged.InvokeAsync(Value);\n            await RaiseDateTimeValueChanged(Value);\n            _isOpen = false;\n        }\n        else\n        {\n            Value = date;\n            await ValueChanged.InvokeAsync(date);\n            await RaiseDateTimeValueChanged(date);\n            if (!ShowTimePicker)\n            {\n                _isOpen = false;\n            }\n        }\n    }\n\n    private async Task HandleRangeStartChanged(DateOnly? date)\n    {\n        RangeStart = date;\n        await RangeStartChanged.InvokeAsync(date);\n    }\n\n    private async Task HandleRangeEndChanged(DateOnly? date)\n    {\n        RangeEnd = date;\n        await RangeEndChanged.InvokeAsync(date);\n        if (date.HasValue)\n        {\n            _isOpen = false;\n        }\n    }\n\n    private async Task HandleTimeChanged(TimeSpan? time)\n    {\n        Time = time;\n        await TimeChanged.InvokeAsync(time);\n    }\n\n    private async Task HandlePresetSelected(DatePickerPreset preset)\n    {\n        Value = preset.Date;\n        await ValueChanged.InvokeAsync(preset.Date);\n        await RaiseDateTimeValueChanged(preset.Date);\n        _isOpen = false;\n    }\n\n    private async Task HandleClear()\n    {\n        if (Mode == DatePickerMode.Range)\n        {\n            RangeStart = null;\n            RangeEnd = null;\n            await RangeStartChanged.InvokeAsync(null);\n            await RangeEndChanged.InvokeAsync(null);\n        }\n        else\n        {\n            Value = null;\n            await ValueChanged.InvokeAsync(null);\n            await RaiseDateTimeValueChanged(null);\n        }\n        if (ShowTimePicker)\n        {\n            Time = null;\n            await TimeChanged.InvokeAsync(null);\n        }\n    }\n\n    private async Task RaiseDateTimeValueChanged(DateOnly? date)\n    {\n        if (DateTimeValueChanged.HasDelegate)\n        {\n            var dt = date.HasValue ? (DateTime?)date.Value.ToDateTime(TimeOnly.MinValue) : null;\n            _prevDateTimeValue = dt;\n            await DateTimeValueChanged.InvokeAsync(dt);\n        }\n    }\n\n    public record DatePickerPreset(string Label, DateOnly Date);\n}\n"
    },
    {
      "path": "UI/DatePicker/DateRangePicker.razor",
      "content": "@namespace Lumeo\n\n<DatePicker Mode=\"DatePicker.DatePickerMode.Range\" NumberOfMonths=\"2\"\n            RangeStart=\"@StartDate\" RangeStartChanged=\"@StartDateChanged\"\n            RangeEnd=\"@EndDate\" RangeEndChanged=\"@EndDateChanged\"\n            MinDate=\"@MinDate\" MaxDate=\"@MaxDate\"\n            IsDateDisabled=\"@IsDateDisabled\" DateClass=\"@DateClass\"\n            DateTooltip=\"@DateTooltip\" DateBadge=\"@DateBadge\"\n            Placeholder=\"@EffectivePlaceholder\" Format=\"@Format\"\n            Presets=\"@_datePickerPresets\"\n            Disabled=\"@Disabled\"\n            Clearable=\"@Clearable\"\n            Class=\"@Class\"\n            FirstDayOfWeek=\"@FirstDayOfWeek\" ShowWeekNumbers=\"@ShowWeekNumbers\"\n            @attributes=\"AdditionalAttributes\" />\n\n@inject Lumeo.Services.Localization.ILumeoLocalizer L\n\n@code {\n    [Parameter] public DateOnly? StartDate { get; set; }\n    [Parameter] public EventCallback<DateOnly?> StartDateChanged { get; set; }\n    [Parameter] public DateOnly? EndDate { get; set; }\n    [Parameter] public EventCallback<DateOnly?> EndDateChanged { get; set; }\n    [Parameter] public DateOnly? MinDate { get; set; }\n    [Parameter] public DateOnly? MaxDate { get; set; }\n    /// <summary>Custom predicate to disable specific dates (e.g. booked\n    /// rooms). Returns <c>true</c> for dates that should not be selectable.</summary>\n    [Parameter] public Func<DateTime, bool>? IsDateDisabled { get; set; }\n    /// <summary>Custom CSS classes per day cell — forwarded to inner Calendar.</summary>\n    [Parameter] public Func<DateTime, string>? DateClass { get; set; }\n    /// <summary>Per-date tooltip text rendered as the day cell's <c>title</c>.</summary>\n    [Parameter] public Func<DateTime, string?>? DateTooltip { get; set; }\n    /// <summary>Per-date indicator slot rendered absolutely positioned in the\n    /// bottom-right of the day cell.</summary>\n    [Parameter] public Func<DateTime, RenderFragment?>? DateBadge { get; set; }\n    [Parameter] public string? Placeholder { get; set; }\n\n    private string EffectivePlaceholder => Placeholder ?? L[\"DateRange.Placeholder\"];\n    /// <summary>\n    /// Date format. When null (default) the inner <see cref=\"DatePicker\"/> falls back\n    /// to the effective culture's <c>ShortDatePattern</c> — so a <c>de-DE</c> user sees\n    /// <c>15.03.2026</c> and an <c>en-US</c> user sees <c>3/15/2026</c> automatically.\n    /// </summary>\n    [Parameter] public string? Format { get; set; }\n    [Parameter] public List<DateRangePreset>? Presets { get; set; }\n    [Parameter] public bool Disabled { get; set; }\n    [Parameter] public bool Clearable { get; set; } = true;\n    [Parameter] public DayOfWeek? FirstDayOfWeek { get; set; }\n    [Parameter] public bool ShowWeekNumbers { get; set; }\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    private List<DatePicker.DatePickerPreset>? _datePickerPresets =>\n        Presets?.Select(p => new DatePicker.DatePickerPreset(p.Label, p.Start)).ToList();\n\n    public class DateRangePreset\n    {\n        public string Label { get; set; } = \"\";\n        public DateOnly Start { get; set; }\n        public DateOnly End { get; set; }\n    }\n}\n"
    },
    {
      "path": "UI/DatePicker/DateWheelPicker.razor",
      "content": "@namespace Lumeo\n@using System.Globalization\n@implements IDisposable\n\n<div class=\"@RootCssClass\" @attributes=\"AdditionalAttributes\" style=\"@RootStyle\">\n    @* Center selection indicator (overlays all columns) *@\n    <div class=\"absolute left-0 right-0 top-1/2 -translate-y-1/2 h-10 border-y border-primary/30 pointer-events-none\"></div>\n\n    @* Day column *@\n    <div class=\"flex-1 overflow-y-scroll snap-y snap-mandatory scrollbar-none h-full\"\n         @onscroll=\"OnDayScroll\"\n         @ref=\"_dayCol\">\n        <div class=\"h-[60px]\"></div>\n        @for (var d = 1; d <= _daysInMonth; d++)\n        {\n            var day = d;\n            <div class=\"h-10 flex items-center justify-center snap-center text-base select-none @(day == _day ? \"text-primary font-semibold\" : \"text-muted-foreground\")\">\n                @day\n            </div>\n        }\n        <div class=\"h-[60px]\"></div>\n    </div>\n\n    @* Month column *@\n    <div class=\"flex-1 overflow-y-scroll snap-y snap-mandatory scrollbar-none h-full\"\n         @onscroll=\"OnMonthScroll\"\n         @ref=\"_monthCol\">\n        <div class=\"h-[60px]\"></div>\n        @for (var m = 1; m <= 12; m++)\n        {\n            var month = m;\n            var name = MonthLabel(month);\n            <div class=\"h-10 flex items-center justify-center snap-center text-base select-none px-1 truncate @(month == _month ? \"text-primary font-semibold\" : \"text-muted-foreground\")\">\n                @name\n            </div>\n        }\n        <div class=\"h-[60px]\"></div>\n    </div>\n\n    @* Year column *@\n    <div class=\"flex-1 overflow-y-scroll snap-y snap-mandatory scrollbar-none h-full\"\n         @onscroll=\"OnYearScroll\"\n         @ref=\"_yearCol\">\n        <div class=\"h-[60px]\"></div>\n        @foreach (var yr in _years)\n        {\n            var year = yr;\n            <div class=\"h-10 flex items-center justify-center snap-center text-base select-none @(year == _year ? \"text-primary font-semibold\" : \"text-muted-foreground\")\">\n                @year\n            </div>\n        }\n        <div class=\"h-[60px]\"></div>\n    </div>\n</div>\n\n@inject Lumeo.Services.IComponentInteropService Interop\n\n@code {\n    // --- Public API ----------------------------------------------------------\n    [Parameter] public DateOnly? Value { get; set; }\n    [Parameter] public EventCallback<DateOnly?> ValueChanged { get; set; }\n    [Parameter] public DateOnly? Min { get; set; }\n    [Parameter] public DateOnly? Max { get; set; }\n    [Parameter] public CultureInfo? Culture { get; set; }\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    // --- Layout constants (must match markup) --------------------------------\n    private const int ItemHeight = 40;   // h-10\n    private const int Height = 160;      // overall column height\n\n    // --- Element refs --------------------------------------------------------\n    private ElementReference _dayCol;\n    private ElementReference _monthCol;\n    private ElementReference _yearCol;\n\n    // --- Selection state -----------------------------------------------------\n    private int _day = 1;\n    private int _month = 1;\n    private int _year = DateTime.Today.Year;\n    private int _daysInMonth = 31;\n    private List<int> _years = new();\n\n    // Debounce timer state — one per column so a scroll on one column doesn't\n    // cancel pending settle on another.\n    private System.Threading.Timer? _dayTimer;\n    private System.Threading.Timer? _monthTimer;\n    private System.Threading.Timer? _yearTimer;\n    private int _pendingDay;\n    private int _pendingMonth;\n    private int _pendingYear;\n    private bool _initialised;\n\n    private CultureInfo EffectiveCulture => Culture ?? CultureInfo.CurrentCulture;\n\n    private string RootCssClass => $\"flex gap-2 relative {Class}\".Trim();\n    private string RootStyle => $\"height: {Height}px;\";\n\n    protected override void OnParametersSet()\n    {\n        var seed = Value ?? DateOnly.FromDateTime(DateTime.Today);\n        _day = seed.Day;\n        _month = seed.Month;\n        _year = seed.Year;\n\n        var minYear = Min?.Year ?? DateTime.Today.Year - 50;\n        var maxYear = Max?.Year ?? DateTime.Today.Year + 50;\n        if (maxYear < minYear) maxYear = minYear;\n\n        _years = Enumerable.Range(minYear, maxYear - minYear + 1).ToList();\n        _daysInMonth = DateTime.DaysInMonth(_year, _month);\n        if (_day > _daysInMonth) _day = _daysInMonth;\n    }\n\n    private string MonthLabel(int month)\n    {\n        var names = EffectiveCulture.DateTimeFormat.AbbreviatedMonthNames;\n        if (month - 1 < names.Length && !string.IsNullOrWhiteSpace(names[month - 1]))\n        {\n            return names[month - 1];\n        }\n        return month.ToString(\"D2\");\n    }\n\n    // --- Scroll handlers -----------------------------------------------------\n    private async Task OnDayScroll()\n    {\n        if (!_initialised) return; // ignore the programmatic OnAfterRender scroll\n        var idx = await GetIndexAsync(_dayCol);\n        if (idx < 0) return;\n        var day = Math.Clamp(idx + 1, 1, _daysInMonth);\n        _pendingDay = day;\n        ResetTimer(ref _dayTimer, CommitDay);\n    }\n\n    private async Task OnMonthScroll()\n    {\n        if (!_initialised) return;\n        var idx = await GetIndexAsync(_monthCol);\n        if (idx < 0) return;\n        var month = Math.Clamp(idx + 1, 1, 12);\n        _pendingMonth = month;\n        ResetTimer(ref _monthTimer, CommitMonth);\n    }\n\n    private async Task OnYearScroll()\n    {\n        if (!_initialised) return;\n        var idx = await GetIndexAsync(_yearCol);\n        if (idx < 0 || idx >= _years.Count) return;\n        _pendingYear = _years[idx];\n        ResetTimer(ref _yearTimer, CommitYear);\n    }\n\n    private async Task<int> GetIndexAsync(ElementReference el)\n    {\n        var scrollTop = await Interop.WheelScrollTop(el);\n        // Each item is `ItemHeight` tall; the first item sits 60px below the top\n        // (top spacer). When item 0 is centered in the 160px viewport,\n        // scrollTop = (60 + ItemHeight/2) - 160/2 = 60 + 20 - 80 = 0. So\n        // index = round(scrollTop / ItemHeight).\n        return (int)Math.Round(scrollTop / ItemHeight);\n    }\n\n    private void ResetTimer(ref System.Threading.Timer? timer, Action commit)\n    {\n        timer?.Dispose();\n        timer = new System.Threading.Timer(_ => InvokeAsync(commit), null, 100, System.Threading.Timeout.Infinite);\n    }\n\n    private async void CommitDay()\n    {\n        if (_pendingDay == _day) return;\n        _day = _pendingDay;\n        await RaiseChange();\n        StateHasChanged();\n    }\n\n    private async void CommitMonth()\n    {\n        if (_pendingMonth == _month) return;\n        _month = _pendingMonth;\n        _daysInMonth = DateTime.DaysInMonth(_year, _month);\n        if (_day > _daysInMonth) _day = _daysInMonth;\n        await RaiseChange();\n        StateHasChanged();\n    }\n\n    private async void CommitYear()\n    {\n        if (_pendingYear == _year) return;\n        _year = _pendingYear;\n        _daysInMonth = DateTime.DaysInMonth(_year, _month);\n        if (_day > _daysInMonth) _day = _daysInMonth;\n        await RaiseChange();\n        StateHasChanged();\n    }\n\n    private async Task RaiseChange()\n    {\n        try\n        {\n            var d = new DateOnly(_year, _month, _day);\n            if (Min.HasValue && d < Min.Value) d = Min.Value;\n            if (Max.HasValue && d > Max.Value) d = Max.Value;\n            _year = d.Year; _month = d.Month; _day = d.Day;\n            await ValueChanged.InvokeAsync(d);\n        }\n        catch (ArgumentOutOfRangeException)\n        {\n            // Defensive — DateOnly ctor guards against invalid combos. We already\n            // re-clamp days on month/year change, so this branch is unreachable\n            // under normal flow.\n        }\n    }\n\n    protected override async Task OnAfterRenderAsync(bool firstRender)\n    {\n        if (firstRender)\n        {\n            await Interop.WheelScrollTo(_dayCol, (_day - 1) * ItemHeight);\n            await Interop.WheelScrollTo(_monthCol, (_month - 1) * ItemHeight);\n            var yearIdx = _years.IndexOf(_year);\n            if (yearIdx >= 0)\n            {\n                await Interop.WheelScrollTo(_yearCol, yearIdx * ItemHeight);\n            }\n            // Defer enabling scroll handlers until after the programmatic\n            // scrollTo writes have settled — otherwise the first @onscroll\n            // would interpret the seed scroll as a user gesture.\n            _ = Task.Delay(150).ContinueWith(_ => InvokeAsync(() => { _initialised = true; }));\n        }\n    }\n\n    public void Dispose()\n    {\n        _dayTimer?.Dispose();\n        _monthTimer?.Dispose();\n        _yearTimer?.Dispose();\n    }\n}\n"
    }
  ],
  "slots": [
    {
      "name": "TriggerContent",
      "owner": "DatePicker",
      "type": "RenderFragment?",
      "description": null
    }
  ],
  "serviceDependencies": [
    {
      "service": "ILumeoLocalizer",
      "namespace": "Lumeo.Services.Localization"
    },
    {
      "service": "IComponentInteropService",
      "namespace": "Lumeo.Services"
    }
  ],
  "cascadingDependencies": [
    {
      "name": "FormField",
      "type": "FormField.FormFieldContext?",
      "required": false
    }
  ],
  "relatedComponents": [
    {
      "name": "DateRangePicker",
      "reason": "sub-component"
    },
    {
      "name": "DateWheelPicker",
      "reason": "sub-component"
    },
    {
      "name": "TimePicker",
      "reason": "used internally"
    },
    {
      "name": "Popover",
      "reason": "used internally"
    },
    {
      "name": "Calendar",
      "reason": "used internally"
    }
  ],
  "keyboardInteractions": [],
  "tests": [
    "tests/Lumeo.Docs.Tests/Catalog/CatalogCardTests.cs",
    "tests/Lumeo.Tests/Components/DatePicker/DatePickerTests.cs"
  ],
  "mdSummary": "# DatePicker\n\nCalendar popover for picking a single date or range.\n\n**Package:** `Lumeo` · **Category:** Forms/Specialized\n**Docs:** https://lumeo.nativ.sh/components/date-picker\n\n## Parameters\n| Owner | Name | Type | Default | Description |\n|---|---|---|---|---|\n| DatePicker | Variant | `DatePickerVariant` | `DatePickerVariant.Calendar` | Picker visual style. Defaults to Calendar. |\n| DatePicker | FormField | `FormField.FormFieldContext?` | `` |  |\n| DatePicker | Required | `bool` | `` |  |\n| DatePicker | Invalid | `bool` | `` |  |\n| DatePicker | ErrorText | `string?` | `` |  |\n| DatePicker | HelperText | `string?` | `` |  |\n| DatePicker | Label | `string?` | `` |  |\n| DatePicker | Name | `string?` | `` |  |\n| DatePicker | Value | `DateOnly?` | `` |  |\n| DatePicker | ValueChanged | `EventCallback<DateOnly?>` | `` |  |\n| DatePicker | DateTimeValue | `DateTime?` | `` | Optional DateTime? binding. When set, takes precedence over Value. Changes fire both ValueChanged (DateOnly?) and DateTimeValueChanged (DateTime? at midnight). |\n| DatePicker | DateTimeValueChanged | `EventCallback<DateTime?>` | `` |  |\n| DatePicker | Placeholder | `string?` | `` |  |\n| DatePicker | Format | `string?` | `` | Date format string. When null (default), the display falls back to the current culture's ShortDatePattern — so a de-DE user sees 15.03.2026 and an en-US user sees 3/15/2026 automatically. |\n| DatePicker | Culture | `System.Globalization.CultureInfo?` | `` | Overrides CurrentCulture for parsing and formatting within this picker. Leave null to honour ASP.NET request localization. |\n| DatePicker | MinDate | `DateOnly?` | `` |  |\n| DatePicker | MaxDate | `DateOnly?` | `` |  |\n| DatePicker | IsDateDisabled | `Func<DateTime, bool>?` | `` | Custom predicate to disable specific dates (e.g. booked rooms, unavailable slots). Returns true for dates that should not be selectable. |\n| DatePicker | DateClass | `Func<DateTime, string>?` | `` | Custom CSS classes per day cell (e.g. status backgrounds for booking calendars). |\n| DatePicker | DateTooltip | `Func<DateTime, string?>?` | `` | Per-date tooltip text rendered as the day cell's title attribute (native browser tooltip). |\n| DatePicker | DateBadge | `Func<DateTime, RenderFragment?>?` | `` | Per-date indicator slot (status dots, count badges, icons) rendered absolutely positioned in the bottom-right of the day cell. |\n| DatePicker | Presets | `List<DatePickerPreset>?` | `` |  |\n| DatePicker | Mode | `DatePickerMode` | `DatePickerMode.Single` |  |\n| DatePicker | RangeStart | `DateOnly?` | `` |  |\n| DatePicker | RangeStartChanged | `EventCallback<DateOnly?>` | `` |  |\n| DatePicker | RangeEnd | `DateOnly?` | `` |  |\n| DatePicker | RangeEndChanged | `EventCallback<DateOnly?>` | `` |  |\n| DatePicker | ShowTimePicker | `bool` | `` |  |\n| DatePicker | Time | `TimeSpan?` | `` |  |\n| DatePicker | TimeChanged | `EventCallback<TimeSpan?>` | `` |  |\n| DatePicker | Disabled | `bool` | `` |  |\n| DatePicker | Clearable | `bool` | `` |  |\n| DatePicker | TriggerContent | `RenderFragment?` | `` |  |\n| DatePicker | NumberOfMonths | `int` | `1` |  |\n| DatePicker | Inline | `bool` | `` | Optional controlled-mode override for the popover open state. When non-null, the consumer holds the source of truth — the picker no longer mutates its internal state and instead raises OpenChanged. Use this when the picker lives inside a parent that re-renders heavily (e.g. an EditForm with active DataAnnotationsValidator) and the popover would otherwise close mid-click during a parameter cascade. When null (default), the picker falls back to its internal open state and behaves exactly as before. |\n| DatePicker | FirstDayOfWeek | `DayOfWeek?` | `` | Leftmost column day of week. Null means culture default. |\n| DatePicker | ShowWeekNumbers | `bool` | `` | When true, renders an ISO week-number column. |\n| DatePicker | Open | `bool?` | `` |  |\n| DatePicker | OpenChanged | `EventCallback<bool>` | `` | Fires whenever the popover would open or close. Pair with Open via @bind-Open for the controlled-mode pattern described above. |\n| DatePicker | Class | `string?` | `` |  |\n| DatePicker | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n| DateRangePicker | StartDate | `DateOnly?` | `` |  |\n| DateRangePicker | StartDateChanged | `EventCallback<DateOnly?>` | `` |  |\n| DateRangePicker | EndDate | `DateOnly?` | `` |  |\n| DateRangePicker | EndDateChanged | `EventCallback<DateOnly?>` | `` |  |\n| DateRangePicker | MinDate | `DateOnly?` | `` |  |\n| DateRangePicker | MaxDate | `DateOnly?` | `` |  |\n| DateRangePicker | IsDateDisabled | `Func<DateTime, bool>?` | `` | Custom predicate to disable specific dates (e.g. booked rooms). Returns true for dates that should not be selectable. |\n| DateRangePicker | DateClass | `Func<DateTime, string>?` | `` | Custom CSS classes per day cell — forwarded to inner Calendar. |\n| DateRangePicker | DateTooltip | `Func<DateTime, string?>?` | `` | Per-date tooltip text rendered as the day cell's title. |\n| DateRangePicker | DateBadge | `Func<DateTime, RenderFragment?>?` | `` | Per-date indicator slot rendered absolutely positioned in the bottom-right of the day cell. |\n| DateRangePicker | Placeholder | `string?` | `` |  |\n| DateRangePicker | Format | `string?` | `` | Date format. When null (default) the inner DatePicker falls back to the effective culture's ShortDatePattern — so a de-DE user sees 15.03.2026 and an en-US user sees 3/15/2026 automatically. |\n| DateRangePicker | Presets | `List<DateRangePreset>?` | `` |  |\n| DateRangePicker | Disabled | `bool` | `` |  |\n| DateRangePicker | Clearable | `bool` | `true` |  |\n| DateRangePicker | FirstDayOfWeek | `DayOfWeek?` | `` |  |\n| DateRangePicker | ShowWeekNumbers | `bool` | `` |  |\n| DateRangePicker | Class | `string?` | `` |  |\n| DateRangePicker | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n| DateWheelPicker | Value | `DateOnly?` | `` |  |\n| DateWheelPicker | ValueChanged | `EventCallback<DateOnly?>` | `` |  |\n| DateWheelPicker | Min | `DateOnly?` | `` |  |\n| DateWheelPicker | Max | `DateOnly?` | `` |  |\n| DateWheelPicker | Culture | `CultureInfo?` | `` |  |\n| DateWheelPicker | Class | `string?` | `` |  |\n| DateWheelPicker | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n\n## Slots\n- `TriggerContent` (DatePicker) — RenderFragment slot\n\n## Events\n- `ValueChanged` (`EventCallback<DateOnly?>`)\n- `DateTimeValueChanged` (`EventCallback<DateTime?>`)\n- `RangeStartChanged` (`EventCallback<DateOnly?>`)\n- `RangeEndChanged` (`EventCallback<DateOnly?>`)\n- `TimeChanged` (`EventCallback<TimeSpan?>`)\n- `OpenChanged` (`EventCallback<bool>`) — Fires whenever the popover would open or close. Pair with Open via @bind-Open for the controlled-mode pattern described above.\n\n## Sub-components\n- `DateRangePicker`, `DateWheelPicker`\n\n## Cascading values it expects\n- `FormField` (`FormField.FormFieldContext?`)\n\n## Services it injects\n- `ILumeoLocalizer` (Lumeo.Services.Localization)\n- `IComponentInteropService` (Lumeo.Services)\n\n## Keyboard\n—\n\n## Example\n```razor\n<DatePicker />\n```\n\n## CSS variables\n`--color-accent`, `--color-accent-foreground`, `--color-border`, `--color-destructive`, `--color-foreground`, `--color-input`, `--color-muted`, `--color-muted-foreground`, `--color-primary`, `--radius`\n\n## Files\n- `UI/DatePicker/DatePicker.razor`\n- `UI/DatePicker/DateRangePicker.razor`\n- `UI/DatePicker/DateWheelPicker.razor`\n"
}