ujs_date_picker

A Flutter package for picking dates as DD/MM/YYYY and times as HH:mm.

It ships five date views — spin wheels, a calendar grid, dropdowns, a typed field, and a start/end range — and three time views: spin wheels, dropdowns, and a typed field. Two themes style all of them. Every date view uses the same selection engine, so leap years, month lengths, and the allowed range behave the same no matter which picker is on screen.

import 'package:ujs_date_picker/ujs_date_picker.dart';

CustomDateField(
  label: 'Travel date',
  type: DatePickerType.calendar,
  theme: CustomDatePickerThemeData.material,
  firstDate: DateTime(2000),
  lastDate: DateTime.now(),
  onChanged: (date) {
    if (date == null) return;
    print(formatDdMmYyyy(date)); // 09/10/2026
  },
);

CustomTimeField(
  label: 'Departure',
  type: TimePickerType.spin,
  use24Hour: true,
  minuteInterval: 15,
  onChanged: (time) {
    if (time == null) return;
    print(formatHhMm(time)); // 14:30
  },
);

How to run

The package itself has no main.dart. You run the demo app in example/. That app depends on this package with a path dependency, so changes in lib/ show up in the demo.

What you need

  • Flutter stable, with Dart ^3.11.1
  • A device: an Android emulator, an iOS simulator, macOS, or Chrome

Check the toolchain:

flutter doctor

Run the demo

From the package root (date Picker/):

cd example
flutter pub get
flutter devices
flutter run

flutter devices lists what is connected. flutter run picks one. To choose it yourself:

flutter run -d chrome
flutter run -d macos
flutter run -d <device-id>

The first launch compiles the app. After it is open:

  • Press r in the terminal for a hot reload
  • Press R for a hot restart
  • Press q to quit

What you should see

The demo title is Date picker. Chips along the top switch the view:

Chip What opens
spin Day, month, and year wheels
calendar Month grid
dropdown Three menus
input A field you type into as DD/MM/YYYY
range Start date and end date

Minimal and Material switch the colors. A Time section below the date uses the same themes. Its chips are spin, dropdown, and input, plus 24-hour and 12-hour.

Pick a date and a time, then tap Save. The formatted values appear under the fields and in a snackbar, for example 09/10/2026 14:30.

Run the tests

From the package root, not from example/:

flutter test

To run the demo's own smoke test:

cd example
flutter test

Those tests cover leap years, February clamping, the DD/MM/YYYY format, a reversed range, and each date picker: spin confirm, a calendar day tap, the January-to-February dropdown clamp, the input mask and the 31/02/2026 error, and a range that swaps its ends. The time tests cover 24-hour and 12-hour formatting, minute snapping, spin confirm, the Now button, a dropdown hour change, the HH:mm mask, 25:00, an off-step minute, and the AM/PM control.

How to use this in a project

Copy the dependency into the other app. Copy the widgets into that app's Dart files. Leave this folder where it is.

1. Add the dependency

After the package is on pub.dev, open the other app's pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  ujs_date_picker: ^0.1.1

To use this folder before it is published, point path at it. If that app sits next to this folder:

dependencies:
  flutter:
    sdk: flutter
  ujs_date_picker:
    path: ../date Picker

path is relative to the app. If the package lives somewhere else, point path at that folder. The folder name contains a space, so quote it in the shell (cd "date Picker"). In YAML the path works with or without quotes.

From the other app:

flutter pub get

2. Import the public library

import 'package:ujs_date_picker/ujs_date_picker.dart';

That import is the whole public API: the fields, the sheet functions, the themes, and the format helpers. The widgets use Material, so the page sits under a MaterialApp.

3. Put date and time on a page

This page is enough to save a travel date and a departure time. onChanged receives the new value, or null when the user taps the clear icon. validator runs when Save calls validate.

import 'package:ujs_date_picker/ujs_date_picker.dart';
import 'package:flutter/material.dart';

class BookingPage extends StatefulWidget {
  const BookingPage({super.key});

  @override
  State<BookingPage> createState() => _BookingPageState();
}

class _BookingPageState extends State<BookingPage> {
  final _formKey = GlobalKey<FormState>();
  DateTime? _date;
  TimeOfDay? _time;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Form(
          key: _formKey,
          child: ListView(
            padding: const EdgeInsets.all(24),
            children: [
              CustomDateField(
                label: 'Travel date',
                type: DatePickerType.calendar,
                firstDate: DateTime.now(),
                lastDate: DateTime(DateTime.now().year + 1, 12, 31),
                theme: CustomDatePickerThemeData.minimal,
                onChanged: (date) => setState(() => _date = date),
                validator: (value) => value == null ? 'Select a date' : null,
              ),
              const SizedBox(height: 20),
              CustomTimeField(
                label: 'Departure',
                type: TimePickerType.spin,
                use24Hour: true,
                minuteInterval: 15,
                theme: CustomDatePickerThemeData.minimal,
                onChanged: (time) => setState(() => _time = time),
                validator: (value) => value == null ? 'Select a time' : null,
              ),
              const SizedBox(height: 20),
              Text(_date == null ? 'No date yet' : formatDdMmYyyy(_date!)),
              Text(_time == null ? 'No time yet' : formatHhMm(_time!)),
              const SizedBox(height: 20),
              FilledButton(
                onPressed: () {
                  if (!_formKey.currentState!.validate()) return;
                  debugPrint('${formatDdMmYyyy(_date!)} ${formatHhMm(_time!)}');
                },
                child: const Text('Save'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

firstDate and lastDate are inclusive. Leaving them out allows 1 January 1900 through 31 December 2100. A returned date is date-only: the time component is midnight. A returned time is a TimeOfDay with an hour from 0 to 23 and a minute from 0 to 59.

4. One date: CustomDateField

CustomDateField is a FormField<DateTime>. type chooses the view.

Argument Default What it does
label 'Date' Text above the field
type DatePickerType.spin spin, calendar, dropdown, or input
initialValue null Date already filled in when the field is created
firstDate 1 Jan 1900 Earliest allowed day, inclusive
lastDate 31 Dec 2100 Latest allowed day, inclusive
theme CustomDatePickerThemeData.minimal Colors, sizes, and button labels
onChanged null Called with the DateTime, or null when cleared
validator null Returns an error string, or null when the value is valid
autovalidateMode null When Flutter runs validator
CustomDateField(
  label: 'Date of birth',
  type: DatePickerType.calendar,
  initialValue: DateTime(1998, 4, 12),
  firstDate: DateTime(1900, 1, 1),
  lastDate: DateTime.now(),
  autovalidateMode: AutovalidateMode.onUserInteraction,
  onChanged: (date) => setState(() => _date = date),
  validator: (value) => value == null ? 'Select a date' : null,
)

Switch the view by changing type only:

type What the user gets Value
DatePickerType.spin Day, month, and year wheels. Today, Confirm, Cancel. DateTime
DatePickerType.calendar A Monday-first month grid. DateTime
DatePickerType.dropdown Three menus: day, month, year. DateTime
DatePickerType.input A text field. 09102026 becomes 09/10/2026. DateTime

DatePickerType.range belongs on CustomDateRangeField. Putting it on CustomDateField fails an assert.

The input field shows Enter a real date for 31/02/2026 and Date is out of range when the day is outside firstDate / lastDate. Spin, calendar, and dropdown snap an impossible day back onto a real day inside the range. February 31 becomes 28 February, or 29 in a leap year.

5. A start and an end: CustomDateRangeField

CustomDateRangeField is a FormField<DateTimeRange>. It always opens the range calendar.

Argument Default What it does
label 'Date range' Text above the field
initialValue null A DateTimeRange already filled in
firstDate 1 Jan 1900 Earliest allowed day, inclusive
lastDate 31 Dec 2100 Latest allowed day, inclusive
theme Minimal Same theme object as the date field
onChanged null Called with the range, or null when cleared
validator null Form error text
autovalidateMode null When Flutter runs validator
DateTimeRange? _trip;

CustomDateRangeField(
  label: 'Trip',
  initialValue: DateTimeRange(
    start: DateTime(2026, 10, 9),
    end: DateTime(2026, 10, 12),
  ),
  firstDate: DateTime.now(),
  lastDate: DateTime(DateTime.now().year + 1, 12, 31),
  onChanged: (range) => setState(() => _trip = range),
  validator: (value) => value == null ? 'Select a range' : null,
)

Read the two ends with formatDdMmYyyy:

Text(
  _trip == null
      ? 'Pick a trip'
      : '${formatDdMmYyyy(_trip!.start)} – ${formatDdMmYyyy(_trip!.end)}',
)

The first tap sets the start. The second tap sets the end. Confirm stays disabled until both days are set. If the second tap is earlier than the first, start is the earlier day and end is the later day.

6. A time: CustomTimeField

CustomTimeField is a FormField<TimeOfDay>.

Argument Default What it does
label 'Time' Text above the field
type TimePickerType.spin spin, dropdown, or input
initialValue null TimeOfDay already filled in
use24Hour true false shows a 12-hour clock with AM/PM
minuteInterval 1 Step size. Must be a positive divisor of 60: 1, 5, 10, 15, 20, 30
theme Minimal Same theme object as the date fields
onChanged null Called with the TimeOfDay, or null when cleared
validator null Form error text
autovalidateMode null When Flutter runs validator
CustomTimeField(
  label: 'Departure',
  type: TimePickerType.dropdown,
  initialValue: const TimeOfDay(hour: 9, minute: 30),
  use24Hour: false,
  minuteInterval: 15,
  onChanged: (time) => setState(() => _time = time),
  validator: (value) => value == null ? 'Select a time' : null,
)
type What the user gets Shown as
TimePickerType.spin Hour and minute wheels. Now, Confirm, Cancel. 14:30 or 02:30 PM
TimePickerType.dropdown Hour and minute menus. A third menu is AM/PM when use24Hour is false. same
TimePickerType.input A text field. 0930 becomes 09:30. In 12-hour mode, AM/PM sits under the field. same

formatHhMm(_time!) prints 14:30. formatHhMm(_time!, use24Hour: false) prints 02:30 PM. The stored hour is still 0–23. Midnight is 00:00 / 12:00 AM. Noon is 12:00 / 12:00 PM.

Spin and dropdown snap to minuteInterval. The typed field keeps an off-step minute on screen and shows Use a 15-minute step. 25:00 shows Enter a real time. Any other interval, such as 7, throws ArgumentError.

7. Open a sheet from your own button

Use these when the page already has a button and only needs the sheet. Cancel returns null.

One date. type is spin, calendar, or dropdown:

final date = await showCustomDatePicker(
  context: context,
  type: DatePickerType.calendar,
  initialDate: DateTime(2026, 10, 9),
  firstDate: DateTime(2020, 1, 1),
  lastDate: DateTime(2030, 12, 31),
  theme: CustomDatePickerThemeData.material,
);

if (date != null) {
  debugPrint(formatDdMmYyyy(date));
}
Argument Default What it does
context required The button's BuildContext
type DatePickerType.spin spin, calendar, or dropdown
initialDate today Day the sheet opens on
firstDate 1 Jan 1900 Earliest allowed day
lastDate 31 Dec 2100 Latest allowed day
theme Minimal Sheet colors and button labels

showCustomDatePicker throws ArgumentError when type is input or range. Typed dates use CustomDateField with DatePickerType.input. A range uses the next function.

A start and an end:

final range = await showCustomDateRangePicker(
  context: context,
  initialStart: DateTime(2026, 10, 1),
  initialEnd: DateTime(2026, 10, 9),
  firstDate: DateTime(2026, 1, 1),
  lastDate: DateTime(2026, 12, 31),
  theme: CustomDatePickerThemeData.minimal,
);

initialStart and initialEnd are optional. Pass theme, firstDate, and lastDate the same way as the single-date sheet.

A time. type is spin or dropdown:

final time = await showCustomTimePicker(
  context: context,
  type: TimePickerType.spin,
  initialTime: const TimeOfDay(hour: 9, minute: 30),
  use24Hour: true,
  minuteInterval: 5,
  theme: CustomDatePickerThemeData.material,
);

if (time != null) {
  debugPrint(formatHhMm(time));
}
Argument Default What it does
context required The button's BuildContext
type TimePickerType.spin spin or dropdown
initialTime now Time the sheet opens on, snapped to minuteInterval
use24Hour true false adds AM/PM
minuteInterval 1 Positive divisor of 60
theme Minimal Sheet colors and the Now label

showCustomTimePicker throws ArgumentError when type is TimePickerType.input.

On the web, a field waits briefly before opening its sheet so the same click does not land on the barrier and dismiss it.

8. Validate date, range, and time together

All three fields are Flutter form fields. One Form and one GlobalKey<FormState> check every validator.

final _formKey = GlobalKey<FormState>();
DateTime? _date;
DateTimeRange? _range;
TimeOfDay? _time;

Form(
  key: _formKey,
  child: Column(
    children: [
      CustomDateField(
        label: 'Date of birth',
        type: DatePickerType.input,
        lastDate: DateTime.now(),
        onChanged: (value) => _date = value,
        validator: (value) => value == null ? 'Select a date' : null,
      ),
      CustomDateRangeField(
        label: 'Stay',
        onChanged: (value) => _range = value,
        validator: (value) => value == null ? 'Select a range' : null,
      ),
      CustomTimeField(
        label: 'Check-in time',
        type: TimePickerType.input,
        use24Hour: true,
        onChanged: (value) => _time = value,
        validator: (value) => value == null ? 'Select a time' : null,
      ),
      FilledButton(
        onPressed: () {
          if (!_formKey.currentState!.validate()) return;
          debugPrint(formatDdMmYyyy(_date!));
          debugPrint(
            '${formatDdMmYyyy(_range!.start)} – ${formatDdMmYyyy(_range!.end)}',
          );
          debugPrint(formatHhMm(_time!));
        },
        child: const Text('Save'),
      ),
    ],
  ),
)

autovalidateMode: AutovalidateMode.onUserInteraction shows the error as soon as the user changes the field. The clear icon sets the value to null and calls onChanged(null), so a required-field validator fails again until a new value is chosen.

9. Format, parse, and snap

Dates:

formatDdMmYyyy(DateTime(2026, 10, 9)); // 09/10/2026
formatDdMmYyyy(DateTime(2026, 10, 9), separator: '-'); // 09-10-2026

tryParseDdMmYyyy('09/10/2026'); // DateTime(2026, 10, 9)
tryParseDdMmYyyy('31/02/2026'); // null

daysInMonth(2026, 2); // 28
isLeapYear(2000); // true
isLeapYear(1900); // false
dateOnly(DateTime.now()); // today at midnight

clampDate and clampDateParts pull a raw day, month, and year onto a real day inside a range. monthNames and monthAbbreviations are the English month lists. DdMmYyyyInputFormatter is the slash mask the input field uses. Put it on your own TextField when you want the same mask.

Times:

const afternoon = TimeOfDay(hour: 14, minute: 30);

formatHhMm(afternoon); // 14:30
formatHhMm(afternoon, use24Hour: false); // 02:30 PM

tryParseHhMm('14:30'); // 14:30
tryParseHhMm('02:30 PM'); // 14:30
tryParseHhMm('12:00 AM'); // 00:00
tryParseHhMm('25:00'); // null

normalizeTime(const TimeOfDay(hour: 10, minute: 7), minuteInterval: 15);
// 10:00

normalizeTime(const TimeOfDay(hour: 23, minute: 58), minuteInterval: 15);
// 00:00

clockHour(14) is 2. hour24FromClock(2, DayPeriod.pm) is 14. HhMmInputFormatter is the colon mask. Typing 1430 through that formatter yields 14:30.

10. Colors, size, and labels

Pass the same theme to every field and to every show* function. Start from a preset and override the parts you care about. Anything left out keeps the preset. The full list of knobs is in Themes.

final theme = CustomDatePickerThemeData.minimal.copyWith(
  accent: const Color(0xFF0B6E4F),
  confirmBackground: const Color(0xFF0B6E4F),
  confirmForeground: Colors.white,
  buttonHeight: 56,
  buttonRadius: 8,
  confirmText: 'Done',
  cancelText: 'Close',
  todayText: 'Today',
  nowText: 'Current',
);

CustomDateField(theme: theme, onChanged: (date) {});
CustomTimeField(theme: theme, onChanged: (time) {});

CustomDatePickerThemeData.material is the Material 3 skin. todayText is the date sheet's shortcut. nowText is the time sheet's shortcut.

Picker types

DatePickerType chooses the view. A single date is a DateTime. A range is a DateTimeRange. Both are date-only values: the time component is midnight.

Type Widget What the user does Result
spin CustomDateField Scroll day, month, and year wheels DateTime
calendar CustomDateField Tap a day on a month grid DateTime
dropdown CustomDateField Choose day, month, and year from menus DateTime
input CustomDateField Type the date DateTime
range CustomDateRangeField Tap a start day, then an end day DateTimeRange

DatePickerType.range is not a valid type for CustomDateField. Use CustomDateRangeField for a range.

Spin

Three wheels labeled DD, MM, and YYYY. Month rows read 05 May. The highlighted row is the current choice, shown above the wheels as 09 / 10 / 2026.

Scrolling onto a day the month does not have, such as 31 while February is selected, snaps back to the last real day. Dates outside firstDate and lastDate snap back into range. Today jumps all three wheels to the current day, clamped to that range. Confirm returns the date. Cancel returns null.

Calendar

A month grid. The week starts on Monday (Mo through Su). Previous and next arrows move the month, and they stop at the first and last allowed months.

Days that belong to the neighboring month are dimmed. Days outside firstDate and lastDate cannot be tapped. Tap a day, then Confirm. Today selects the current day and shows its month.

Three menus in one sheet: DD (01–31), MM (01–12, with the short month name), and YYYY.

The day menu only lists days that exist. Choosing February while the day is 31 changes the day to 28, or 29 in a leap year. Menus also omit months and days outside the allowed range.

Input

The field itself is the picker. There is no sheet. Typing 09102026 is masked to 09/10/2026. A finished value that is not a real day, such as 31/02/2026, stays in the field and shows Enter a real date. A real day outside firstDate / lastDate shows Date is out of range. Only a valid in-range date is reported through onChanged.

Range

A calendar used twice. The first tap sets the start and clears any previous end. The second tap sets the end. Days between the two are highlighted, and the sheet title area shows both dates as DD/MM/YYYY – DD/MM/YYYY.

If the second tap is earlier than the first, the engine swaps them so start is always on or before end. Confirm stays disabled until both days are set, then returns a DateTimeRange.

Time picker

A time is a TimeOfDay. The default clock is 24-hour (14:30). Pass use24Hour: false for 02:30 PM.

Type Widget What the user does Result
spin CustomTimeField Scroll hour and minute wheels TimeOfDay
dropdown CustomTimeField Choose hour and minute from menus TimeOfDay
input CustomTimeField Type the time TimeOfDay

minuteInterval defaults to 1. Use 5, 15, or 30 when the choice should land on those minutes. It must divide 60.

Spin

Two wheels labeled HH and MM. In 12-hour mode a third wheel is AM / PM. The highlighted row is the current choice, shown above the wheels as 14:30 or 02:30 PM.

Now jumps the wheels to the current time, snapped to minuteInterval. Confirm returns the time. Cancel returns null.

Menus for HH and MM. The minute menu only lists steps of minuteInterval. In 12-hour mode a third menu chooses AM or PM. Now selects the current time.

Input

The field itself is the picker. There is no sheet. Typing 0930 is masked to 09:30. 25:00 stays in the field and shows Enter a real time. A real time whose minute is off the interval, such as 14:10 with a 15-minute step, shows Use a 15-minute step.

In 12-hour mode the field still takes HH:MM from 01 to 12, and an AM / PM control sits under it. 02:30 with PM selected is 14:30.

Themes

Themes are skins. They are not picker types. The same view can be Minimal or Material.

Theme Look
CustomDatePickerThemeData.minimal Warm paper sheet, green accent. This is the default.
CustomDatePickerThemeData.material Material 3 purple accent and tonal surfaces.

Pass theme on the field or on showCustomDatePicker / showCustomDateRangePicker. Start from a preset and override any color, size, or text style with copyWith. A full ButtonStyle replaces the Confirm, Cancel, or Today button when you need more than the built-in knobs.

The class is named CustomDatePickerThemeData so it does not clash with Flutter's DatePickerTheme.

final theme = CustomDatePickerThemeData.minimal.copyWith(
  accent: const Color(0xFF0B6E4F),
  confirmBackground: const Color(0xFF0B6E4F),
  confirmForeground: Colors.white,
  cancelForeground: const Color(0xFF0B6E4F),
  todayForeground: const Color(0xFF0B6E4F),
  buttonHeight: 56,
  buttonRadius: 8,
  buttonFontSize: 16,
  valueFontSize: 24,
  previewFontSize: 32,
  dayCellSize: 40,
  wheelHeight: 220,
  iconSize: 28,
  sheetRadius: 20,
  confirmText: 'Done',
  cancelText: 'Close',
);

CustomDateField(
  type: DatePickerType.spin,
  theme: theme,
)

Anything you do not pass keeps the preset. Useful knobs:

What you want to change Theme fields
Confirm, Cancel, and Today colors confirmBackground, confirmForeground, cancelForeground, cancelBorder, todayForeground, disabledButton
Button size and shape buttonHeight, buttonRadius, buttonFontSize, buttonPadding, buttonGap
Button label confirmText, cancelText, todayText, nowText
Replace a button's whole style confirmButtonStyle, cancelButtonStyle, todayButtonStyle
Field text and padding valueFontSize, labelFontSize, captionFontSize, fieldPadding, radius, borderWidth
Sheet sheet, sheetRadius, sheetMargin, sheetPadding
Calendar days dayCellSize, dayGridExtent, dayFontSize, selectedDay, selectedDayText, highlight
Spin wheels wheelHeight, wheelItemExtent, wheelFontSize, wheelSelectedFontSize
Icons iconSize, clearIconSize, iconColor

labelTextStyle, valueTextStyle, previewTextStyle, buttonTextStyle, and errorTextStyle override the matching font size when you need a full TextStyle.

Use in a form

CustomDateField is a FormField<DateTime>. CustomDateRangeField is a FormField<DateTimeRange>. CustomTimeField is a FormField<TimeOfDay>. The copy-paste page in How to use this in a project puts all three in one Form, with validator, initialValue, and autovalidateMode.

A selected value can be cleared with the close icon on the field. Clearing calls onChanged with null.

showCustomDatePicker, showCustomDateRangePicker, and showCustomTimePicker open a sheet from a button you already have. Argument lists and the values that throw ArgumentError are in step 7. Canceling a sheet returns null.

On the web, the field waits briefly before opening the sheet so the same click does not land on the barrier and dismiss it.

Date rules

The default allowed range is 1 January 1900 through 31 December 2100. Pass firstDate and lastDate to narrow it. Both ends are inclusive.

Rules applied by every view:

  • February has 29 days in leap years and 28 otherwise. Years divisible by 100 are not leap years unless they are also divisible by 400 (1900 is not, 2000 is).
  • A day past the end of the month is shortened before the range check. 31/02/2026 becomes 28/02/2026. It does not roll into March.
  • A date before firstDate becomes firstDate. A date after lastDate becomes lastDate.
  • Returned values are date-only (DateTime(year, month, day)).

Helpers exported from the package:

Function Purpose
formatDdMmYyyy(date) 09/10/2026. Optional separator.
tryParseDdMmYyyy(text) Parses dd/MM/yyyy, or returns null when it is not a real day.
daysInMonth(year, month) Length of that month.
isLeapYear(year) Leap-year test.
dateOnly(date) Strips the time.
clampDate / clampDateParts Pull a date onto a real day inside a range.
DdMmYyyyInputFormatter The slash mask used by the date input field.
formatHhMm(time) 14:30, or 02:30 PM when use24Hour is false.
tryParseHhMm(text) Parses HH:mm or hh:mm AM / hh:mm PM, or returns null.
normalizeTime(time) Snaps to minuteInterval. Near midnight this can roll to 00:00.
clockHour(hour24) 12-hour face. 14 becomes 2. Midnight and noon are 12.
hour24FromClock(clock, period) 2 plus DayPeriod.pm becomes 14.
HhMmInputFormatter The colon mask used by the time input field.

monthNames and monthAbbreviations are also exported.

Architecture

Application
  └── Public widgets          CustomDateField, CustomDateRangeField, CustomTimeField
        ├── Selection engine  first/last constraints, single date or range, normalization
        └── Presentation      Minimal or Material theme
              └── Views       date views, time spin / dropdown / input, sheet chrome

The example app imports only package:ujs_date_picker/ujs_date_picker.dart. A view reports a raw day, month, and year. The engine normalizes that and applies the allowed range. The presentation layer paints the result as DD/MM/YYYY.

lib/
  ujs_date_picker.dart      Public exports
  date_parts.dart           Leap years, month lengths, date formatting
  time_parts.dart           12/24-hour formatting, minute snapping
  src/engine/               Constraints, selection state, selection engine
  src/presentation/         Themes, sheet, and the five views
example/                    Demo app

Libraries

date_parts
Calendar helpers for a day / month / year picker.
time_parts
ujs_date_picker