diff --git a/.github/workflows/docs_deploy.yaml b/.github/workflows/docs_deploy.yaml index 173445b89..f0a670270 100644 --- a/.github/workflows/docs_deploy.yaml +++ b/.github/workflows/docs_deploy.yaml @@ -65,7 +65,7 @@ jobs: working-directory: ./samples steps: - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2.17.0 + - uses: subosito/flutter-action@v2.18.0 with: flutter-version: 3.24.x cache: true diff --git a/.github/workflows/docs_preview_deploy.yaml b/.github/workflows/docs_preview_deploy.yaml index 4ca2f8d37..0bab5d76e 100644 --- a/.github/workflows/docs_preview_deploy.yaml +++ b/.github/workflows/docs_preview_deploy.yaml @@ -25,7 +25,7 @@ jobs: ref: ${{ github.event.pull_request.head.ref }} # Publish samples - - uses: subosito/flutter-action@v2.17.0 + - uses: subosito/flutter-action@v2.18.0 with: flutter-version: 3.24.x cache: true diff --git a/.github/workflows/forui_build.yaml b/.github/workflows/forui_build.yaml index 6b5b54e31..04fbe217d 100644 --- a/.github/workflows/forui_build.yaml +++ b/.github/workflows/forui_build.yaml @@ -22,7 +22,7 @@ jobs: flutter-version: [ 3.x ] steps: - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2.17.0 + - uses: subosito/flutter-action@v2.18.0 with: flutter-version: ${{ matrix.flutter-version }} cache: true @@ -51,7 +51,7 @@ jobs: with: distribution: 'temurin' java-version: 17 - - uses: subosito/flutter-action@v2.17.0 + - uses: subosito/flutter-action@v2.18.0 with: flutter-version: ${{ matrix.flutter-version }} cache: true @@ -71,7 +71,7 @@ jobs: flutter-version: [ 3.x ] steps: - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2.17.0 + - uses: subosito/flutter-action@v2.18.0 with: flutter-version: ${{ matrix.flutter-version }} cache: true diff --git a/.github/workflows/forui_hooks_build.yaml b/.github/workflows/forui_hooks_build.yaml index 1143daea1..bfb23aca2 100644 --- a/.github/workflows/forui_hooks_build.yaml +++ b/.github/workflows/forui_hooks_build.yaml @@ -22,7 +22,7 @@ jobs: flutter-version: [ 3.x ] steps: - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2.16.0 + - uses: subosito/flutter-action@v2.18.0 with: flutter-version: ${{ matrix.flutter-version }} cache: true diff --git a/.github/workflows/forui_hooks_presubmit.yaml b/.github/workflows/forui_hooks_presubmit.yaml index 24a3fd32d..2bc0cdc50 100644 --- a/.github/workflows/forui_hooks_presubmit.yaml +++ b/.github/workflows/forui_hooks_presubmit.yaml @@ -37,7 +37,7 @@ jobs: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.ref }} - - uses: subosito/flutter-action@v2.16.0 + - uses: subosito/flutter-action@v2.18.0 with: cache: true diff --git a/.github/workflows/forui_presubmit.yaml b/.github/workflows/forui_presubmit.yaml index 029f792fa..1bf72231f 100644 --- a/.github/workflows/forui_presubmit.yaml +++ b/.github/workflows/forui_presubmit.yaml @@ -37,7 +37,7 @@ jobs: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.ref }} - - uses: subosito/flutter-action@v2.17.0 + - uses: subosito/flutter-action@v2.18.0 with: cache: true diff --git a/.github/workflows/samples_build.yaml b/.github/workflows/samples_build.yaml index 46c00ca80..1a9439f09 100644 --- a/.github/workflows/samples_build.yaml +++ b/.github/workflows/samples_build.yaml @@ -24,7 +24,7 @@ jobs: flutter-version: [ 3.x ] steps: - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2.17.0 + - uses: subosito/flutter-action@v2.18.0 with: flutter-version: ${{ matrix.flutter-version }} cache: true diff --git a/.github/workflows/samples_presubmit.yaml b/.github/workflows/samples_presubmit.yaml index a5cce048f..5c45b26b8 100644 --- a/.github/workflows/samples_presubmit.yaml +++ b/.github/workflows/samples_presubmit.yaml @@ -37,7 +37,7 @@ jobs: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.ref }} - - uses: subosito/flutter-action@v2.17.0 + - uses: subosito/flutter-action@v2.18.0 with: cache: true diff --git a/docs/pages/docs/form/calendar.mdx b/docs/pages/docs/form/calendar.mdx index bef652a18..ba5b9f315 100644 --- a/docs/pages/docs/form/calendar.mdx +++ b/docs/pages/docs/form/calendar.mdx @@ -1,4 +1,4 @@ -import { Tabs } from 'nextra/components'; +import { Callout, Tabs } from 'nextra/components'; import { Widget } from "../../../components/widget.tsx"; import LinkBadge from "../../../components/link-badge/link-badge.tsx"; import LinkBadgeGroup from "../../../components/link-badge/link-badge-group.tsx"; @@ -9,13 +9,16 @@ A date field component that allows users to enter and edit date. The calendar pages are designed to be navigable through swipe gestures on mobile platforms, allowing left and right swipes to transition between pages. -An [`FCalendarController`](https://pub.dev/documentation/forui/latest/forui.widgets.calendar/FCalendarController-class.html) is used -to customize the date selection behavior. +A [`FCalendarController`](https://pub.dev/documentation/forui/latest/forui.widgets.calendar/FCalendarController-class.html) +is used to customize the date selection behavior. + + `FCalendar` and all `FCalendarController`s return `DateTime`s in UTC timezone, truncated to the nearest day. + @@ -25,8 +28,8 @@ to customize the date selection behavior. ```dart FCalendar( controller: FCalendarController.date(initialSelection: selected), - start: DateTime.utc(2000), - end: DateTime.utc(2030), + start: DateTime(2000), + end: DateTime(2030), ); ``` @@ -39,14 +42,15 @@ to customize the date selection behavior. ```dart FCalendar( controller: FCalendarController.date( - initialSelection: DateTime.utc(2024, 9, 13), + initialSelection: DateTime(2024, 9, 13), selectable: (date) => allowedDates.contains(date), ), - start: DateTime.utc(2024), - end: DateTime.utc(2030), - today: DateTime.utc(2024, 7, 14), + dayBuilder: (context, data, child) => !child, + start: DateTime(2024), + end: DateTime(2030), + today: DateTime(2024, 7, 14), initialType = FCalendarPickerType.yearMonth, - initialMonth = DateTime.utc(2024, 9), + initialMonth = DateTime(2024, 9), onMonthChange: (date) => print(date), onPress: (date) => print(date), onLongPress: (date) => print(date), @@ -63,8 +67,8 @@ FCalendar( ```dart {2} FCalendar( controller: FCalendarController.date(), - start: DateTime.utc(2000), - end: DateTime.utc(2030), + start: DateTime(2000), + end: DateTime(2030), ); ``` @@ -79,11 +83,11 @@ FCalendar( ```dart {2-4} FCalendar( controller: FCalendarController.dates( - initialSelections: {DateTime.utc(2024, 7, 17), DateTime.utc(2024, 7, 20)}, + initialSelections: {DateTime(2024, 7, 17), DateTime(2024, 7, 20)}, ), - start: DateTime.utc(2000), - today: DateTime.utc(2024, 7, 15), - end: DateTime.utc(2030), + start: DateTime(2000), + today: DateTime(2024, 7, 15), + end: DateTime(2030), ); ``` @@ -98,12 +102,12 @@ FCalendar( ```dart {4} FCalendar( controller: FCalendarController.dates( - initialSelections: {DateTime.utc(2024, 7, 17), DateTime.utc(2024, 7, 20)}, - selectable: (date) => !{DateTime.utc(2024, 7, 18), DateTime.utc(2024, 7, 19)}.contains(date), + initialSelections: {DateTime(2024, 7, 17), DateTime(2024, 7, 20)}, + selectable: (date) => !{DateTime(2024, 7, 18), DateTime(2024, 7, 19)}.contains(date), ), - start: DateTime.utc(2000), - today: DateTime.utc(2024, 7, 15), - end: DateTime.utc(2030), + start: DateTime(2000), + today: DateTime(2024, 7, 15), + end: DateTime(2030), ); ``` @@ -118,11 +122,11 @@ FCalendar( ```dart {2-4} FCalendar( controller: FCalendarController.range( - initialSelection: (DateTime.utc(2024, 7, 17), DateTime.utc(2024, 7, 20)), + initialSelection: (DateTime(2024, 7, 17), DateTime(2024, 7, 20)), ), - start: DateTime.utc(2000), - today: DateTime.utc(2024, 7, 15), - end: DateTime.utc(2030), + start: DateTime(2000), + today: DateTime(2024, 7, 15), + end: DateTime(2030), ); ``` diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index 6efbef739..b34fa25e2 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -16,13 +16,13 @@ importers: version: 0.462.0(react@18.3.1) next: specifier: ^14.2.14 - version: 14.2.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 14.2.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nextra: specifier: ^3.0.8 - version: 3.2.4(@types/react@18.3.3)(acorn@8.14.0)(next@14.2.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2) + version: 3.2.4(@types/react@18.3.3)(acorn@8.14.0)(next@14.2.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2) nextra-theme-docs: specifier: ^3.0.8 - version: 3.2.4(next@14.2.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.2.4(@types/react@18.3.3)(acorn@8.14.0)(next@14.2.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.2.4(next@14.2.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.2.4(@types/react@18.3.3)(acorn@8.14.0)(next@14.2.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: 18.3.1 version: 18.3.1 @@ -44,7 +44,7 @@ importers: version: 8.4.49 tailwindcss: specifier: ^3.4.13 - version: 3.4.15 + version: 3.4.16 typescript: specifier: ^5.6.2 version: 5.7.2 @@ -238,59 +238,59 @@ packages: resolution: {integrity: sha512-jMxvwzkKzd3cXo2EB9GM2ic0eYo2rP/BS6gJt6HnWbsDO1O8GSD4k7o2Cpr2YERtMpGF/MGcDfsfj2EbQPtrXw==} engines: {node: '>= 10'} - '@next/env@14.2.18': - resolution: {integrity: sha512-2vWLOUwIPgoqMJKG6dt35fVXVhgM09tw4tK3/Q34GFXDrfiHlG7iS33VA4ggnjWxjiz9KV5xzfsQzJX6vGAekA==} + '@next/env@14.2.20': + resolution: {integrity: sha512-JfDpuOCB0UBKlEgEy/H6qcBSzHimn/YWjUHzKl1jMeUO+QVRdzmTTl8gFJaNO87c8DXmVKhFCtwxQ9acqB3+Pw==} - '@next/swc-darwin-arm64@14.2.18': - resolution: {integrity: sha512-tOBlDHCjGdyLf0ube/rDUs6VtwNOajaWV+5FV/ajPgrvHeisllEdymY/oDgv2cx561+gJksfMUtqf8crug7sbA==} + '@next/swc-darwin-arm64@14.2.20': + resolution: {integrity: sha512-WDfq7bmROa5cIlk6ZNonNdVhKmbCv38XteVFYsxea1vDJt3SnYGgxLGMTXQNfs5OkFvAhmfKKrwe7Y0Hs+rWOg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@14.2.18': - resolution: {integrity: sha512-uJCEjutt5VeJ30jjrHV1VIHCsbMYnEqytQgvREx+DjURd/fmKy15NaVK4aR/u98S1LGTnjq35lRTnRyygglxoA==} + '@next/swc-darwin-x64@14.2.20': + resolution: {integrity: sha512-XIQlC+NAmJPfa2hruLvr1H1QJJeqOTDV+v7tl/jIdoFvqhoihvSNykLU/G6NMgoeo+e/H7p/VeWSOvMUHKtTIg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@14.2.18': - resolution: {integrity: sha512-IL6rU8vnBB+BAm6YSWZewc+qvdL1EaA+VhLQ6tlUc0xp+kkdxQrVqAnh8Zek1ccKHlTDFRyAft0e60gteYmQ4A==} + '@next/swc-linux-arm64-gnu@14.2.20': + resolution: {integrity: sha512-pnzBrHTPXIMm5QX3QC8XeMkpVuoAYOmyfsO4VlPn+0NrHraNuWjdhe+3xLq01xR++iCvX+uoeZmJDKcOxI201Q==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@14.2.18': - resolution: {integrity: sha512-RCaENbIZqKKqTlL8KNd+AZV/yAdCsovblOpYFp0OJ7ZxgLNbV5w23CUU1G5On+0fgafrsGcW+GdMKdFjaRwyYA==} + '@next/swc-linux-arm64-musl@14.2.20': + resolution: {integrity: sha512-WhJJAFpi6yqmUx1momewSdcm/iRXFQS0HU2qlUGlGE/+98eu7JWLD5AAaP/tkK1mudS/rH2f9E3WCEF2iYDydQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@14.2.18': - resolution: {integrity: sha512-3kmv8DlyhPRCEBM1Vavn8NjyXtMeQ49ID0Olr/Sut7pgzaQTo4h01S7Z8YNE0VtbowyuAL26ibcz0ka6xCTH5g==} + '@next/swc-linux-x64-gnu@14.2.20': + resolution: {integrity: sha512-ao5HCbw9+iG1Kxm8XsGa3X174Ahn17mSYBQlY6VGsdsYDAbz/ZP13wSLfvlYoIDn1Ger6uYA+yt/3Y9KTIupRg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@14.2.18': - resolution: {integrity: sha512-mliTfa8seVSpTbVEcKEXGjC18+TDII8ykW4a36au97spm9XMPqQTpdGPNBJ9RySSFw9/hLuaCMByluQIAnkzlw==} + '@next/swc-linux-x64-musl@14.2.20': + resolution: {integrity: sha512-CXm/kpnltKTT7945np6Td3w7shj/92TMRPyI/VvveFe8+YE+/YOJ5hyAWK5rpx711XO1jBCgXl211TWaxOtkaA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@14.2.18': - resolution: {integrity: sha512-J5g0UFPbAjKYmqS3Cy7l2fetFmWMY9Oao32eUsBPYohts26BdrMUyfCJnZFQkX9npYaHNDOWqZ6uV9hSDPw9NA==} + '@next/swc-win32-arm64-msvc@14.2.20': + resolution: {integrity: sha512-upJn2HGQgKNDbXVfIgmqT2BN8f3z/mX8ddoyi1I565FHbfowVK5pnMEwauvLvaJf4iijvuKq3kw/b6E9oIVRWA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-ia32-msvc@14.2.18': - resolution: {integrity: sha512-Ynxuk4ZgIpdcN7d16ivJdjsDG1+3hTvK24Pp8DiDmIa2+A4CfhJSEHHVndCHok6rnLUzAZD+/UOKESQgTsAZGg==} + '@next/swc-win32-ia32-msvc@14.2.20': + resolution: {integrity: sha512-igQW/JWciTGJwj3G1ipalD2V20Xfx3ywQy17IV0ciOUBbFhNfyU1DILWsTi32c8KmqgIDviUEulW/yPb2FF90w==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@next/swc-win32-x64-msvc@14.2.18': - resolution: {integrity: sha512-dtRGMhiU9TN5nyhwzce+7c/4CCeykYS+ipY/4mIrGzJ71+7zNo55ZxCB7cAVuNqdwtYniFNR2c9OFQ6UdFIMcg==} + '@next/swc-win32-x64-msvc@14.2.20': + resolution: {integrity: sha512-AFmqeLW6LtxeFTuoB+MXFeM5fm5052i3MU6xD0WzJDOwku6SkZaxb1bxjBaRC8uNqTRTSPl0yMFtjNowIVI67w==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -630,8 +630,8 @@ packages: caniuse-lite@1.0.30001666: resolution: {integrity: sha512-gD14ICmoV5ZZM1OdzPWmpx+q4GyefaK06zi8hmfHV5xe4/2nOQX3+Dw5o+fSqOws2xVwL9j+anOPFwHzdEdV4g==} - caniuse-lite@1.0.30001680: - resolution: {integrity: sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==} + caniuse-lite@1.0.30001686: + resolution: {integrity: sha512-Y7deg0Aergpa24M3qLC5xjNklnKnhsmSyR/V89dLZ1n0ucJIFNs7PgR2Yfa/Zf6W79SbBicgtGxZr2juHkEUIA==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1251,12 +1251,8 @@ packages: layout-base@2.0.1: resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} - lilconfig@2.1.0: - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} - engines: {node: '>=10'} - - lilconfig@3.1.2: - resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} lines-and-columns@1.2.4: @@ -1510,6 +1506,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -1520,8 +1521,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@14.2.18: - resolution: {integrity: sha512-H9qbjDuGivUDEnK6wa+p2XKO+iMzgVgyr9Zp/4Iv29lKa+DYaxJGjOeEA+5VOvJh/M7HLiskehInSa0cWxVXUw==} + next@14.2.20: + resolution: {integrity: sha512-yPvIiWsiyVYqJlSQxwmzMIReXn5HxFNq4+tlVQ812N1FbvhmE+fDpIAD7bcS2mGYQwPJ5vAsQouyme2eKsxaug==} engines: {node: '>=18.17.0'} hasBin: true peerDependencies: @@ -1952,8 +1953,8 @@ packages: tailwind-merge@2.5.5: resolution: {integrity: sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==} - tailwindcss@3.4.15: - resolution: {integrity: sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==} + tailwindcss@3.4.16: + resolution: {integrity: sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==} engines: {node: '>=14.0.0'} hasBin: true @@ -2348,33 +2349,33 @@ snapshots: '@napi-rs/simple-git-win32-arm64-msvc': 0.1.19 '@napi-rs/simple-git-win32-x64-msvc': 0.1.19 - '@next/env@14.2.18': {} + '@next/env@14.2.20': {} - '@next/swc-darwin-arm64@14.2.18': + '@next/swc-darwin-arm64@14.2.20': optional: true - '@next/swc-darwin-x64@14.2.18': + '@next/swc-darwin-x64@14.2.20': optional: true - '@next/swc-linux-arm64-gnu@14.2.18': + '@next/swc-linux-arm64-gnu@14.2.20': optional: true - '@next/swc-linux-arm64-musl@14.2.18': + '@next/swc-linux-arm64-musl@14.2.20': optional: true - '@next/swc-linux-x64-gnu@14.2.18': + '@next/swc-linux-x64-gnu@14.2.20': optional: true - '@next/swc-linux-x64-musl@14.2.18': + '@next/swc-linux-x64-musl@14.2.20': optional: true - '@next/swc-win32-arm64-msvc@14.2.18': + '@next/swc-win32-arm64-msvc@14.2.20': optional: true - '@next/swc-win32-ia32-msvc@14.2.18': + '@next/swc-win32-ia32-msvc@14.2.20': optional: true - '@next/swc-win32-x64-msvc@14.2.18': + '@next/swc-win32-x64-msvc@14.2.20': optional: true '@nodelib/fs.scandir@2.1.5': @@ -2756,7 +2757,7 @@ snapshots: caniuse-lite@1.0.30001666: {} - caniuse-lite@1.0.30001680: {} + caniuse-lite@1.0.30001686: {} ccount@2.0.1: {} @@ -3474,9 +3475,7 @@ snapshots: layout-base@2.0.1: {} - lilconfig@2.1.0: {} - - lilconfig@3.1.2: {} + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -4045,6 +4044,8 @@ snapshots: nanoid@3.3.7: {} + nanoid@3.3.8: {} + negotiator@1.0.0: {} next-themes@0.4.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -4052,46 +4053,46 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next@14.2.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@14.2.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@next/env': 14.2.18 + '@next/env': 14.2.20 '@swc/helpers': 0.5.5 busboy: 1.6.0 - caniuse-lite: 1.0.30001680 + caniuse-lite: 1.0.30001686 graceful-fs: 4.2.11 postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) styled-jsx: 5.1.1(react@18.3.1) optionalDependencies: - '@next/swc-darwin-arm64': 14.2.18 - '@next/swc-darwin-x64': 14.2.18 - '@next/swc-linux-arm64-gnu': 14.2.18 - '@next/swc-linux-arm64-musl': 14.2.18 - '@next/swc-linux-x64-gnu': 14.2.18 - '@next/swc-linux-x64-musl': 14.2.18 - '@next/swc-win32-arm64-msvc': 14.2.18 - '@next/swc-win32-ia32-msvc': 14.2.18 - '@next/swc-win32-x64-msvc': 14.2.18 + '@next/swc-darwin-arm64': 14.2.20 + '@next/swc-darwin-x64': 14.2.20 + '@next/swc-linux-arm64-gnu': 14.2.20 + '@next/swc-linux-arm64-musl': 14.2.20 + '@next/swc-linux-x64-gnu': 14.2.20 + '@next/swc-linux-x64-musl': 14.2.20 + '@next/swc-win32-arm64-msvc': 14.2.20 + '@next/swc-win32-ia32-msvc': 14.2.20 + '@next/swc-win32-x64-msvc': 14.2.20 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - nextra-theme-docs@3.2.4(next@14.2.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.2.4(@types/react@18.3.3)(acorn@8.14.0)(next@14.2.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + nextra-theme-docs@3.2.4(next@14.2.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.2.4(@types/react@18.3.3)(acorn@8.14.0)(next@14.2.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@headlessui/react': 2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) clsx: 2.1.1 escape-string-regexp: 5.0.0 flexsearch: 0.7.43 - next: 14.2.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: 0.4.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - nextra: 3.2.4(@types/react@18.3.3)(acorn@8.14.0)(next@14.2.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2) + nextra: 3.2.4(@types/react@18.3.3)(acorn@8.14.0)(next@14.2.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) scroll-into-view-if-needed: 3.1.0 zod: 3.23.8 - nextra@3.2.4(@types/react@18.3.3)(acorn@8.14.0)(next@14.2.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2): + nextra@3.2.4(@types/react@18.3.3)(acorn@8.14.0)(next@14.2.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2): dependencies: '@formatjs/intl-localematcher': 0.5.8 '@headlessui/react': 2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -4114,7 +4115,7 @@ snapshots: mdast-util-gfm: 3.0.0 mdast-util-to-hast: 13.2.0 negotiator: 1.0.0 - next: 14.2.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1) p-limit: 6.1.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -4256,7 +4257,7 @@ snapshots: postcss-load-config@4.0.2(postcss@8.4.49): dependencies: - lilconfig: 3.1.2 + lilconfig: 3.1.3 yaml: 2.6.1 optionalDependencies: postcss: 8.4.49 @@ -4275,7 +4276,7 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.7 + nanoid: 3.3.8 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -4622,7 +4623,7 @@ snapshots: tailwind-merge@2.5.5: {} - tailwindcss@3.4.15: + tailwindcss@3.4.16: dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -4633,7 +4634,7 @@ snapshots: glob-parent: 6.0.2 is-glob: 4.0.3 jiti: 1.21.6 - lilconfig: 2.1.0 + lilconfig: 3.1.3 micromatch: 4.0.8 normalize-path: 3.0.0 object-hash: 3.0.0 diff --git a/forui/CHANGELOG.md b/forui/CHANGELOG.md index 552b95285..5f5a2f043 100644 --- a/forui/CHANGELOG.md +++ b/forui/CHANGELOG.md @@ -4,6 +4,14 @@ * Add `FLineCalendar`. +* Add `truncateAndStripTimezone` to `FCalendarController.date(...)`. + +* Add `truncateAndStripTimezone` to `FCalendarController.dates(...)`. + +* Add `truncateAndStripTimezone` to `FCalendarController.range(...)`. + +* Add `FCalendar.dayBuilder`. + * Add `FTileGroup.builder`. * Add `FSelectTileGroup.builder`. @@ -12,6 +20,16 @@ ### Changes +* Change `FCalendarController.date(...)` to automatically strip and truncate all DateTimes to dates in UTC timezone. + +* Change `FCalendarController.dates(...)` to automatically strip and truncate all DateTimes to dates in UTC timezone. + +* Change `FCalendarController.ranges(...)` to automatically strip and truncate all DateTimes to dates in UTC timezone. + +* Change `FCalendar.start` to be optional and default to 1st January 1900. + +* Change `FCalendar.end` to be optional and default to 1st January 2100. + * Change `FTheme` to internally extend `InheritedTheme`. * Change `FTileGroup` to be scrollable. diff --git a/forui/example/android/settings.gradle b/forui/example/android/settings.gradle index 9db74ac3c..ffbfcf941 100644 --- a/forui/example/android/settings.gradle +++ b/forui/example/android/settings.gradle @@ -19,7 +19,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.7.2' apply false + id "com.android.application" version '8.7.3' apply false id "org.jetbrains.kotlin.android" version "2.1.0" apply false } diff --git a/forui/example/lib/sandbox.dart b/forui/example/lib/sandbox.dart index 75dc34cb3..b5de1b285 100644 --- a/forui/example/lib/sandbox.dart +++ b/forui/example/lib/sandbox.dart @@ -14,7 +14,7 @@ class Sandbox extends StatefulWidget { class _SandboxState extends State with SingleTickerProviderStateMixin { final FCalendarController controller = FCalendarController.date(); late FPopoverController popoverController; - + @override void initState() { super.initState(); diff --git a/forui/lib/src/widgets/calendar/calendar.dart b/forui/lib/src/widgets/calendar/calendar.dart index 830569ffd..704304f3c 100644 --- a/forui/lib/src/widgets/calendar/calendar.dart +++ b/forui/lib/src/widgets/calendar/calendar.dart @@ -23,19 +23,27 @@ import 'package:forui/src/widgets/calendar/year_month_picker.dart'; /// * [FCalendarController] for customizing a calendar's date selection behavior. /// * [FCalendarStyle] for customizing a calendar's appearance. class FCalendar extends StatelessWidget { + static Widget _dayBuilder(BuildContext context, FCalendarDayData data, Widget? child) => child!; + /// The style. Defaults to [FThemeData.calendarStyle]. final FCalendarStyle? style; /// A controller that determines if a date is selected. final FCalendarController controller; - /// The start date, inclusive. It is truncated to the nearest date. + /// The builder used to build a day in the day picker. Defaults to returning the given child. + /// + /// The `child` is the default content with no alterations. Consider wrapping the `child` and other custom decoration + /// in a [Stack] to avoid re-creating the custom day content from scratch. + final ValueWidgetBuilder dayBuilder; + + /// The start date, inclusive. It is truncated to the nearest date. Defaults to 1st January, 1900. /// /// ## Contract /// Throws [AssertionError] if [end] <= [start]. final DateTime start; - /// The end date, exclusive. It is truncated to the nearest date. + /// The end date, exclusive. It is truncated to the nearest date. Defaults to 1st January, 2100. /// /// ## Contract /// Throws [AssertionError] if [end] <= [start]. @@ -61,20 +69,24 @@ class FCalendar extends StatelessWidget { /// [initialMonth] defaults to [today]. It is truncated to the nearest date. FCalendar({ required this.controller, - required this.start, - required this.end, this.style, + this.dayBuilder = _dayBuilder, this.onMonthChange, this.onPress, this.onLongPress, FCalendarPickerType initialType = FCalendarPickerType.day, + DateTime? start, + DateTime? end, DateTime? today, DateTime? initialMonth, super.key, - }) : assert(start.toLocalDate() < end.toLocalDate(), 'end date must be greater than start date'), + }) : start = start ?? DateTime(1990), + end = end ?? DateTime(2100), today = today ?? DateTime.now(), _type = ValueNotifier(initialType), - _month = ValueNotifier((initialMonth ?? today ?? DateTime.now()).toLocalDate().truncate(to: DateUnit.months)); + _month = ValueNotifier((initialMonth ?? today ?? DateTime.now()).toLocalDate().truncate(to: DateUnit.months)) { + assert(this.start.toLocalDate() < this.end.toLocalDate(), 'end date must be greater than start date'); + } @override Widget build(BuildContext context) { @@ -102,6 +114,7 @@ class FCalendar extends StatelessWidget { builder: (context, value, child) => switch (value) { FCalendarPickerType.day => PagedDayPicker( style: style, + dayBuilder: dayBuilder, start: start.toLocalDate(), end: end.toLocalDate(), today: today.toLocalDate(), @@ -142,6 +155,7 @@ class FCalendar extends StatelessWidget { properties ..add(DiagnosticsProperty('style', style)) ..add(DiagnosticsProperty('controller', controller)) + ..add(ObjectFlagProperty.has('dayBuilder', dayBuilder)) ..add(DiagnosticsProperty('start', start)) ..add(DiagnosticsProperty('end', end)) ..add(DiagnosticsProperty('today', today)) diff --git a/forui/lib/src/widgets/calendar/calendar_controller.dart b/forui/lib/src/widgets/calendar/calendar_controller.dart index c96131e12..8a9b8c09a 100644 --- a/forui/lib/src/widgets/calendar/calendar_controller.dart +++ b/forui/lib/src/widgets/calendar/calendar_controller.dart @@ -4,9 +4,11 @@ import 'package:forui/forui.dart'; bool _true(DateTime _) => true; +DateTime _stripTimezone(DateTime date) => DateTime.utc(date.year, date.month, date.day); + /// A controller that controls date selection in a calendar. /// -/// The [DateTime]s are always in UTC timezone and truncated to the nearest day. +/// All returned [DateTime]s are in UTC timezone with no time component. /// /// This class should be extended to customize date selection. By default, the following controllers are provided: /// * [FCalendarController.date] for selecting a single date. @@ -18,66 +20,129 @@ abstract class FCalendarController extends FValueNotifier { /// /// [selectable] will always return true if not given. /// + /// [truncateAndStripTimezone] determines whether the controller should truncate and convert all given [DateTime]s to + /// dates in UTC timezone. Defaults to true. + /// + /// ```dart + /// DateTime truncateAndStripTimezone(DateTime date) => DateTime.utc(date.year, date.month, date.day); + /// ``` + /// + /// [truncateAndStripTimezone] should be set to false if you can guarantee that all dates are in UTC timezone (with + /// the help of a 3rd party library), which will improve performance. **Warning:** Giving a [DateTime] in local + /// timezone or with a time component when [truncateAndStripTimezone] is false is undefined behavior. + /// /// ## Contract - /// Throws [AssertionError] if [initialSelection] is not in UTC timezone. + /// Throws [AssertionError] if [initialSelection] is not in UTC timezone and [truncateAndStripTimezone] is false. static FCalendarController date({ DateTime? initialSelection, Predicate? selectable, + bool truncateAndStripTimezone = true, }) => - _DateController(initialSelection: initialSelection, selectable: selectable); + truncateAndStripTimezone + ? _AutoDateController(initialSelection: initialSelection, selectable: selectable) + : _DateController(initialSelection: initialSelection, selectable: selectable); /// Creates a [FCalendarController] that allows multiple dates to be selected, with the given initial selected dates. /// /// [selectable] will always return true if not given. /// + /// [truncateAndStripTimezone] determines whether the controller should truncate and convert all given [DateTime]s to + /// dates in UTC timezone. Defaults to true. + /// + /// ```dart + /// DateTime truncateAndStripTimezone(DateTime date) => DateTime.utc(date.year, date.month, date.day); + /// ``` + /// + /// [truncateAndStripTimezone] should be set to false if you can guarantee that all dates are in UTC timezone (with + /// the help of an 3rd party library), which will improve performance. **Warning:** Giving a [DateTime] in local + /// timezone or with a time component when [truncateAndStripTimezone] is false is undefined behavior. + /// /// ## Contract - /// Throws [AssertionError] if the dates in [initialSelections] are not in UTC timezone. + /// Throws [AssertionError] if the dates in [initialSelections] are not in UTC timezone and [truncateAndStripTimezone] + /// is false. static FCalendarController> dates({ Set initialSelections = const {}, Predicate? selectable, + bool truncateAndStripTimezone = true, }) => - _DatesController(initialSelections: initialSelections, selectable: selectable); + truncateAndStripTimezone + ? _AutoDatesController(initialSelections: initialSelections, selectable: selectable) + : _DatesController(initialSelections: initialSelections, selectable: selectable); /// Creates a [FCalendarController] that allows a single range to be selected, with the given initial range. /// /// [selectable] will always return true if not given. /// - /// Both the start and end dates of the range is inclusive. The selected dates are always in UTC timezone and - /// truncated to the nearest day. Unselectable dates within the selected range are selected regardless. + /// [truncateAndStripTimezone] determines whether the controller should truncate and convert all given [DateTime]s to + /// dates in UTC timezone. Defaults to true. + /// + /// ```dart + /// DateTime truncateAndStripTimezone(DateTime date) => DateTime.utc(date.year, date.month, date.day); + /// ``` + /// + /// [truncateAndStripTimezone] should be set to false if you can guarantee that all dates are in UTC timezone (with + /// the help of an 3rd party library), which will improve performance. **Warning:** Giving a [DateTime] in local + /// timezone or with a time component when [truncateAndStripTimezone] is false is undefined behavior. + /// + /// Both the start and end dates of the range is inclusive. Unselectable dates within the selected range are selected + /// regardless. /// /// ## Contract /// Throws [AssertionError] if: - /// * the given dates in [value] is not in UTC timezone. + /// * the given dates in [initialSelection] is not in UTC timezone and [truncateAndStripTimezone] is false. /// * the end date is less than start date. static FCalendarController<(DateTime, DateTime)?> range({ (DateTime, DateTime)? initialSelection, Predicate? selectable, + bool truncateAndStripTimezone = true, }) => - _RangeController(initialSelection: initialSelection, selectable: selectable); + truncateAndStripTimezone + ? _AutoRangeController(initialSelection: initialSelection, selectable: selectable) + : _RangeController(initialSelection: initialSelection, selectable: selectable); /// Creates a [FCalendarController] with the given initial [value]. FCalendarController(super._value); /// Returns true if the given [date] can be selected/unselected. /// - /// [date] should always in UTC timezone and truncated to the nearest day. - /// /// ## Note /// It is unsafe for this function to have side effects since it may be called more than once for a single date. As it - /// is called frequently, it should not be computationally expensive. + /// is called frequently, it should also not be computationally expensive. bool selectable(DateTime date); /// Returns true if the given [date] is selected. - /// - /// [date] should always in UTC timezone and truncated to the nearest day. bool selected(DateTime date); /// Selects the given [date]. - /// - /// [date] should always in UTC timezone and truncated to the nearest day. void select(DateTime date); } +// The single date controllers. +class _AutoDateController extends FCalendarController { + final Predicate _selectable; + + _AutoDateController({ + DateTime? initialSelection, + Predicate? selectable, + }) : _selectable = selectable ?? _true, + super(initialSelection = initialSelection == null ? null : _stripTimezone(initialSelection)); + + @override + bool selectable(DateTime date) => _selectable(_stripTimezone(date)); + + @override + bool selected(DateTime date) => value == _stripTimezone(date); + + @override + void select(DateTime date) { + date = _stripTimezone(date); + super.value = value == date ? null : date; + } + + @override + set value(DateTime? value) => super.value = value == null ? null : _stripTimezone(value); +} + class _DateController extends FCalendarController { final Predicate _selectable; @@ -92,12 +157,36 @@ class _DateController extends FCalendarController { bool selectable(DateTime date) => _selectable(date); @override - bool selected(DateTime date) => value?.toLocalDate() == date.toLocalDate(); + bool selected(DateTime date) => value == date; + + @override + void select(DateTime date) => value = value == date ? null : date; +} + +// The multiple dates controllers. +final class _AutoDatesController extends FCalendarController> { + final Predicate _selectable; + + _AutoDatesController({ + Set initialSelections = const {}, + Predicate? selectable, + }) : _selectable = selectable ?? _true, + super(initialSelections.map(_stripTimezone).toSet()); + + @override + bool selectable(DateTime date) => _selectable(_stripTimezone(date)); + + @override + bool selected(DateTime date) => value.contains(_stripTimezone(date)); @override void select(DateTime date) { - value = value?.toLocalDate() == date.toLocalDate() ? null : date; + final copy = {...value}; + super.value = copy..toggle(_stripTimezone(date)); } + + @override + set value(Set value) => super.value = value.map(_stripTimezone).toSet(); } final class _DatesController extends FCalendarController> { @@ -116,11 +205,68 @@ final class _DatesController extends FCalendarController> { @override bool selected(DateTime date) => value.contains(date); + @override + void select(DateTime date) => value = value..toggle(date); +} + +// The range controllers. +final class _AutoRangeController extends FCalendarController<(DateTime, DateTime)?> { + final Predicate _selectable; + + _AutoRangeController({ + (DateTime, DateTime)? initialSelection, + Predicate? selectable, + }) : _selectable = selectable ?? _true, + super( + initialSelection = initialSelection == null + ? null + : (_stripTimezone(initialSelection.$1), _stripTimezone(initialSelection.$2)), + ) { + final range = value; + assert( + range == null || (range.$1.isBefore(range.$2) || range.$1.isAtSameMomentAs(range.$2)), + 'end date must be greater than or equal to start date', + ); + } + + @override + bool selectable(DateTime date) => _selectable(_stripTimezone(date)); + + @override + bool selected(DateTime date) { + if (value case (final first, final last)) { + final current = date.toLocalDate(); + return first.toLocalDate() <= current && current <= last.toLocalDate(); + } + + return false; + } + @override void select(DateTime date) { - final copy = {...value}; - value = copy..toggle(date); + date = _stripTimezone(date); + if (value == null) { + super.value = (date, date); + return; + } + + final (first, last) = value!; + + switch ((first, last)) { + case (final first, final last) when date == first || date == last: + super.value = null; + + case (final first, final last) when date.isBefore(first): + super.value = (date, last); + + case (final first, _): + super.value = (first, date); + } } + + @override + set value((DateTime, DateTime)? value) => + super.value = value == null ? null : (_stripTimezone(value.$1), _stripTimezone(value.$2)); } final class _RangeController extends FCalendarController<(DateTime, DateTime)?> { @@ -158,22 +304,21 @@ final class _RangeController extends FCalendarController<(DateTime, DateTime)?> @override void select(DateTime date) { if (value == null) { - value = (date, date); + super.value = (date, date); return; } final (first, last) = value!; - final pressed = date.toLocalDate(); - switch ((first.toLocalDate(), last.toLocalDate())) { - case (final first, final last) when pressed == first || pressed == last: - value = null; + switch ((first, last)) { + case (final first, final last) when date == first || date == last: + super.value = null; - case (final first, final last) when pressed < first: - value = (pressed.toNative(), last.toNative()); + case (final first, final last) when date.isBefore(first): + super.value = (date, last); case (final first, _): - value = (first.toNative(), pressed.toNative()); + super.value = (first, date); } } } diff --git a/forui/lib/src/widgets/calendar/day/day_picker.dart b/forui/lib/src/widgets/calendar/day/day_picker.dart index 4287e196d..de9316fcd 100644 --- a/forui/lib/src/widgets/calendar/day/day_picker.dart +++ b/forui/lib/src/widgets/calendar/day/day_picker.dart @@ -16,6 +16,7 @@ class DayPicker extends StatefulWidget { final FCalendarDayPickerStyle style; final FLocalizations localization; + final ValueWidgetBuilder dayBuilder; final LocalDate month; final LocalDate today; final LocalDate? focused; @@ -27,6 +28,7 @@ class DayPicker extends StatefulWidget { const DayPicker({ required this.style, required this.localization, + required this.dayBuilder, required this.month, required this.today, required this.focused, @@ -46,6 +48,7 @@ class DayPicker extends StatefulWidget { properties ..add(DiagnosticsProperty('style', style)) ..add(DiagnosticsProperty('localization', localization)) + ..add(ObjectFlagProperty.has('dayBuilder', dayBuilder)) ..add(DiagnosticsProperty('month', month)) ..add(DiagnosticsProperty('today', today)) ..add(DiagnosticsProperty('focused', focused)) @@ -111,6 +114,7 @@ class _DayPickerState extends State { Entry.day( style: widget.style, localizations: widget.localization, + dayBuilder: widget.dayBuilder, date: date, focusNode: focusNode, current: date.month == widget.month.month, diff --git a/forui/lib/src/widgets/calendar/day/paged_day_picker.dart b/forui/lib/src/widgets/calendar/day/paged_day_picker.dart index e5ce27025..617f2ed26 100644 --- a/forui/lib/src/widgets/calendar/day/paged_day_picker.dart +++ b/forui/lib/src/widgets/calendar/day/paged_day_picker.dart @@ -15,12 +15,14 @@ class PagedDayPicker extends PagedPicker { final ValueChanged? onMonthChange; final ValueChanged onPress; final ValueChanged onLongPress; + final ValueWidgetBuilder dayBuilder; PagedDayPicker({ required this.selected, required this.onMonthChange, required this.onPress, required this.onLongPress, + required this.dayBuilder, required super.style, required super.start, required super.end, @@ -40,7 +42,8 @@ class PagedDayPicker extends PagedPicker { ..add(DiagnosticsProperty('selected', selected)) ..add(DiagnosticsProperty('onMonthChange', onMonthChange)) ..add(DiagnosticsProperty('onPress', onPress)) - ..add(DiagnosticsProperty('onLongPress', onLongPress)); + ..add(DiagnosticsProperty('onLongPress', onLongPress)) + ..add(DiagnosticsProperty('dayBuilder', dayBuilder)); } } @@ -51,6 +54,7 @@ class _PagedDayPickerState extends PagedPickerState { Widget buildItem(BuildContext context, int page) => DayPicker( style: widget.style.dayPickerStyle, localization: FLocalizations.of(context), + dayBuilder: widget.dayBuilder, month: widget.start.truncate(to: DateUnit.months).plus(months: page), today: widget.today, focused: focusedDate, diff --git a/forui/lib/src/widgets/calendar/shared/entry.dart b/forui/lib/src/widgets/calendar/shared/entry.dart index 514c588a3..0d95da286 100644 --- a/forui/lib/src/widgets/calendar/shared/entry.dart +++ b/forui/lib/src/widgets/calendar/shared/entry.dart @@ -6,6 +6,16 @@ import 'package:sugar/sugar.dart'; import 'package:forui/forui.dart'; +/// A calendar day's data. +typedef FCalendarDayData = ({ + FCalendarDayPickerStyle style, + DateTime date, + bool current, + bool today, + bool selectable, + bool selected, +}); + @internal abstract class Entry extends StatelessWidget { final FCalendarEntryStyle style; @@ -14,6 +24,7 @@ abstract class Entry extends StatelessWidget { factory Entry.day({ required FCalendarDayPickerStyle style, required FLocalizations localizations, + required ValueWidgetBuilder dayBuilder, required LocalDate date, required FocusNode focusNode, required bool current, @@ -34,7 +45,16 @@ abstract class Entry extends StatelessWidget { final yesterday = isSelected && selected(date.yesterday) ? Radius.zero : entryStyle.radius; final tomorrow = isSelected && selected(date.tomorrow) ? Radius.zero : entryStyle.radius; - return _Content( + final dayData = ( + style: style, + date: date.toNative(), + current: current, + today: today, + selectable: canSelect, + selected: isSelected, + ); + + final child = _Content( style: entryStyle, borderRadius: switch (Directionality.maybeOf(context)) { TextDirection.ltr || null => BorderRadius.horizontal(left: yesterday, right: tomorrow), @@ -44,6 +64,8 @@ abstract class Entry extends StatelessWidget { data: data, current: today, ); + + return dayBuilder(context, dayData, child); } if (canSelect) { diff --git a/forui/lib/widgets/calendar.dart b/forui/lib/widgets/calendar.dart index 76d0c8c58..4f5319511 100644 --- a/forui/lib/widgets/calendar.dart +++ b/forui/lib/widgets/calendar.dart @@ -7,7 +7,7 @@ library forui.widgets.calendar; export '../src/widgets/calendar/calendar.dart'; export '../src/widgets/calendar/day/day_picker.dart' show FCalendarDayPickerStyle, FCalendarDayStyle; -export '../src/widgets/calendar/shared/entry.dart' show FCalendarEntryStyle; +export '../src/widgets/calendar/shared/entry.dart' show FCalendarDayData, FCalendarEntryStyle; export '../src/widgets/calendar/shared/header.dart' show FCalendarHeaderStyle, FCalendarPickerType; export '../src/widgets/calendar/calendar_controller.dart'; export '../src/widgets/calendar/year_month_picker.dart' show FCalendarYearMonthPickerStyle; diff --git a/forui/test/src/widgets/calendar/calendar_controller_test.dart b/forui/test/src/widgets/calendar/calendar_controller_test.dart index 239196bd1..ba74ddfbd 100644 --- a/forui/test/src/widgets/calendar/calendar_controller_test.dart +++ b/forui/test/src/widgets/calendar/calendar_controller_test.dart @@ -4,17 +4,77 @@ import 'package:forui/forui.dart'; void main() { group('FCalendarController.date(...)', () { + test( + 'constructor converts date time', + () => expect( + FCalendarController.date(initialSelection: DateTime(2024, 11, 30, 12)).value, + DateTime.utc(2024, 11, 30), + ), + ); + + test('selectable(...)', () { + FCalendarController.date( + selectable: (date) { + expect(date, DateTime.utc(2024)); + return true; + }, + ).selectable(DateTime(2024, 1, 1, 1)); + }); + + for (final (date, expected) in [ + (DateTime(2024, 5, 4, 3), true), + (DateTime(2024, 5, 5, 3), false), + ]) { + test('selected(...) contains date', () { + final controller = FCalendarController.date(initialSelection: DateTime(2024, 5, 4)); + expect(controller.selected(date), expected); + }); + } + + for (final (initial, date, expected) in [ + (null, DateTime(2024), DateTime.utc(2024)), + (null, DateTime(2025), DateTime.utc(2025)), + (DateTime(2024), DateTime(2025), DateTime.utc(2025)), + (DateTime(2024), DateTime(2024), null), + ]) { + test('select(...)', () { + final controller = FCalendarController.date(initialSelection: initial)..select(date); + expect(controller.value, expected); + }); + } + + test('value', () { + final controller = FCalendarController.date()..value = DateTime(2024, 11, 30, 12); + expect(controller.value, DateTime.utc(2024, 11, 30)); + }); + }); + + group('FCalendarController.date(...) no auto-convert', () { test( 'constructor throws error', - () => expect(() => FCalendarController.date(initialSelection: DateTime.now()), throwsAssertionError), + () => expect( + () => FCalendarController.date(initialSelection: DateTime.now(), truncateAndStripTimezone: false), + throwsAssertionError, + ), ); + test('selectable(...)', () { + FCalendarController.date( + truncateAndStripTimezone: false, + selectable: (date) { + expect(date, DateTime(2024, 1, 1, 1)); + return true; + }, + ).selectable(DateTime(2024, 1, 1, 1)); + }); + for (final (date, expected) in [ (DateTime.utc(2024, 5, 4), true), (DateTime.utc(2024, 5, 5), false), ]) { test('selected(...) contains date', () { - final controller = FCalendarController.date(initialSelection: DateTime.utc(2024, 5, 4)); + final controller = + FCalendarController.date(initialSelection: DateTime.utc(2024, 5, 4), truncateAndStripTimezone: false); expect(controller.selected(date), expected); }); } @@ -26,19 +86,84 @@ void main() { (DateTime.utc(2024), DateTime.utc(2024), null), ]) { test('select(...)', () { - final controller = FCalendarController.date(initialSelection: initial)..select(date); + final controller = FCalendarController.date(initialSelection: initial, truncateAndStripTimezone: false) + ..select(date); + expect(controller.value, expected); + }); + } + }); + + group('FCalendarController.dates(...)', () { + test( + 'constructor converts date time', + () => expect( + FCalendarController.dates(initialSelections: {DateTime(2024, 11, 30, 12)}).value, + {DateTime.utc(2024, 11, 30)}, + ), + ); + + test('selectable(...)', () { + FCalendarController.dates( + selectable: (date) { + expect(date, DateTime.utc(2024)); + return true; + }, + ).selectable(DateTime(2024, 1, 1, 1)); + }); + + for (final (date, expected) in [ + (DateTime(2024), true), + (DateTime(2025), false), + ]) { + test('selected(...)', () { + final controller = FCalendarController.dates(initialSelections: {DateTime.utc(2024)}); + expect(controller.selected(date), expected); + }); + } + + for (final (initial, date, expected) in [ + ({DateTime(2024)}, DateTime(2024), {}), + ({}, DateTime.utc(2024), {DateTime.utc(2024)}), + ({DateTime(2024)}, DateTime(2025), {DateTime.utc(2024), DateTime.utc(2025)}), + ]) { + test('select(...)', () { + final controller = FCalendarController.dates(initialSelections: initial)..select(date); expect(controller.value, expected); }); } + + test('value', () { + final controller = FCalendarController.dates()..value = {DateTime(2024, 11, 30, 12)}; + expect(controller.value, {DateTime.utc(2024, 11, 30)}); + }); }); - group('FCalendarController.dates', () { + group('FCalendarController.dates(...) no auto-convert', () { + test( + 'constructor throws error', + () => expect( + () => FCalendarController.dates(initialSelections: {DateTime.now()}, truncateAndStripTimezone: false), + throwsAssertionError, + ), + ); + + test('selectable(...)', () { + FCalendarController.dates( + truncateAndStripTimezone: false, + selectable: (date) { + expect(date, DateTime(2024, 1, 1, 1)); + return true; + }, + ).selectable(DateTime(2024, 1, 1, 1)); + }); + for (final (date, expected) in [ (DateTime.utc(2024), true), (DateTime.utc(2025), false), ]) { test('selected(...)', () { - final controller = FCalendarController.dates(initialSelections: {DateTime.utc(2024)}); + final controller = + FCalendarController.dates(initialSelections: {DateTime.utc(2024)}, truncateAndStripTimezone: false); expect(controller.selected(date), expected); }); } @@ -49,17 +174,72 @@ void main() { ({DateTime.utc(2024)}, DateTime.utc(2025), {DateTime.utc(2024), DateTime.utc(2025)}), ]) { test('select(...)', () { - final controller = FCalendarController.dates(initialSelections: initial)..select(date); + final controller = FCalendarController.dates(initialSelections: initial, truncateAndStripTimezone: false) + ..select(date); expect(controller.value, expected); }); } }); group('FCalendarController.range(...)', () { + test( + 'constructor converts date time', + () => expect( + FCalendarController.range(initialSelection: (DateTime(2024, 11, 30, 12), DateTime(2024, 12, 12, 12))).value, + (DateTime.utc(2024, 11, 30), DateTime.utc(2024, 12, 12)), + ), + ); + + test('selectable(...)', () { + FCalendarController.range( + selectable: (date) { + expect(date, DateTime.utc(2024)); + return true; + }, + ).selectable(DateTime(2024, 1, 1, 1)); + }); + + for (final (initial, date, expected) in [ + ((DateTime(2024), DateTime(2025)), DateTime.utc(2024), true), + ((DateTime(2024), DateTime(2025)), DateTime.utc(2025), true), + ((DateTime(2024), DateTime(2025)), DateTime.utc(2023), false), + ((DateTime(2024), DateTime(2025)), DateTime.utc(2026), false), + (null, DateTime.utc(2023), false), + ]) { + test('selected(...)', () { + final controller = FCalendarController.range(initialSelection: initial); + expect(controller.selected(date), expected); + }); + } + + for (final (initial, date, expected) in [ + ((DateTime(2024), DateTime(2025)), DateTime(2024), null), + ((DateTime(2024), DateTime(2025)), DateTime(2025), null), + ((DateTime(2024), DateTime(2025)), DateTime(2023), (DateTime.utc(2023), DateTime.utc(2025))), + ((DateTime(2024), DateTime(2025)), DateTime(2026), (DateTime.utc(2024), DateTime.utc(2026))), + ((DateTime(2024), DateTime(2027)), DateTime(2025), (DateTime.utc(2024), DateTime.utc(2025))), + (null, DateTime(2023), (DateTime.utc(2023), DateTime.utc(2023))), + ]) { + test('select(...)', () { + final controller = FCalendarController.range(initialSelection: initial)..select(date); + expect(controller.value, expected); + }); + } + + test('value', () { + final controller = FCalendarController.range()..value = (DateTime(2024, 11, 30, 12), DateTime(2024, 12, 12, 12)); + expect(controller.value, (DateTime.utc(2024, 11, 30), DateTime.utc(2024, 12, 12))); + }); + }); + + group('FCalendarController.range(...) no auto-convert', () { test( 'constructor throws error', () => expect( - () => FCalendarController.range(initialSelection: (DateTime(2025), DateTime(2024))), + () => FCalendarController.range( + initialSelection: (DateTime(2025), DateTime(2024)), + truncateAndStripTimezone: false, + ), throwsAssertionError, ), ); @@ -72,7 +252,7 @@ void main() { (null, DateTime.utc(2023), false), ]) { test('selected(...)', () { - final controller = FCalendarController.range(initialSelection: initial); + final controller = FCalendarController.range(initialSelection: initial, truncateAndStripTimezone: false); expect(controller.selected(date), expected); }); } @@ -86,7 +266,8 @@ void main() { (null, DateTime.utc(2023), (DateTime.utc(2023), DateTime.utc(2023))), ]) { test('select(...)', () { - final controller = FCalendarController.range(initialSelection: initial)..select(date); + final controller = FCalendarController.range(initialSelection: initial, truncateAndStripTimezone: false) + ..select(date); expect(controller.value, expected); }); } diff --git a/forui_hooks/CHANGELOG.md b/forui_hooks/CHANGELOG.md index ef4ca8830..815505be4 100644 --- a/forui_hooks/CHANGELOG.md +++ b/forui_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.7.0+1 + +Updated the README file. + ## 0.7.0 Initial release! diff --git a/forui_hooks/README.md b/forui_hooks/README.md index 45b8c6db7..3251e334c 100644 --- a/forui_hooks/README.md +++ b/forui_hooks/README.md @@ -1,7 +1,58 @@ # Forui Hooks -Companion hooks for [Forui](../forui), a UI library for Flutter that provides a set of a set of beautifully designed, -minimalistic widgets. +Forui provides first class integration with [Flutter Hooks](https://pub.dev/packages/flutter_hooks). All controllers +are exposed as hooks in the companion `forui_hooks` package. + +## Installation + +From your Flutter project directory, run the following command to install `flutter_hooks` and `forui_hooks`. + +```bash filename="bash" +flutter pub add flutter_hooks +flutter pub add forui_hooks +``` + +## Usage + +To use Forui hooks in your Flutter app, import the `forui_hooks` package and initialize a hook inside a `HookWidget`. + +```dart {5, 7, 10} +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'package:forui/forui.dart'; +import 'package:forui_hooks/forui_hooks.dart'; + +class Example extends HookWidget { + @override + Widget build(BuildContext context) { + final controller = useFAccordionController(); + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FAccordion( + controller: controller, + items: [ + FAccordionItem( + title: const Text('Is it accessible?'), + child: const Text('Yes. It adheres to the WAI-ARIA design pattern.'), + ), + FAccordionItem( + initiallyExpanded: true, + title: const Text('Is it Styled?'), + child: const Text("Yes. It comes with default styles that matches the other components' aesthetics"), + ), + FAccordionItem( + title: const Text('Is it Animated?'), + child: const Text('Yes. It is animated by default, but you can disable it if you prefer'), + ), + ], + ), + ], + ); + } +} +``` ## Documentation @@ -13,4 +64,4 @@ Please read the [contributing guide](../CONTRIBUTING.md). ## License -Licensed under the [MIT License](/LICENSE) & [ISC license](/LICENSE). \ No newline at end of file +Licensed under the [MIT License](/LICENSE). diff --git a/forui_hooks/lib/src/calendar_controller_hook.dart b/forui_hooks/lib/src/calendar_controller_hook.dart index ee0494bcd..e94ed0483 100644 --- a/forui_hooks/lib/src/calendar_controller_hook.dart +++ b/forui_hooks/lib/src/calendar_controller_hook.dart @@ -9,9 +9,24 @@ typedef _Create = FCalendarController Function(_CalendarControllerHook) /// Creates a [FCalendarController] that allows only a single date to be selected and is automatically disposed. /// /// [selectable] will always return true if not given. +/// +/// [autoConvert] determines whether the controller should truncate and convert all given [DateTime]s to dates in +/// UTC timezone. Defaults to true. +/// +/// ```dart +/// DateTime convert(DateTime date) => DateTime.utc(date.year, date.month, date.day); +/// ``` +/// +/// [autoConvert] should be set to false if you can guarantee that all dates are in UTC timezone (with the help of an +/// 3rd party library), which will improve performance. **Warning:** Giving a [DateTime] in local timezone or with a +/// time component when [autoConvert] is false is undefined behavior. +/// +/// ## Contract +/// Throws [AssertionError] if [initialSelection] is not in UTC timezone and [autoConvert] is false. FCalendarController useFDateCalendarController({ DateTime? initialSelection, bool Function(DateTime)? selectable, + bool autoConvert = true, List? keys, }) => use(_CalendarControllerHook( @@ -21,15 +36,31 @@ FCalendarController useFDateCalendarController({ create: (hook) => FCalendarController.date( initialSelection: hook.value, selectable: hook.selectable, + truncateAndStripTimezone: autoConvert, ), )); /// Creates a [FCalendarController] that allows only multiple dates to be selected and is automatically disposed. /// /// [selectable] will always return true if not given. +/// +/// [autoConvert] determines whether the controller should truncate and convert all given [DateTime]s to dates in +/// UTC timezone. Defaults to true. +/// +/// ```dart +/// DateTime convert(DateTime date) => DateTime.utc(date.year, date.month, date.day); +/// ``` +/// +/// [autoConvert] should be set to false if you can guarantee that all dates are in UTC timezone (with the help of an +/// 3rd party library), which will improve performance. **Warning:** Giving a [DateTime] in local timezone or with a +/// time component when [autoConvert] is false is undefined behavior. +/// +/// ## Contract +/// Throws [AssertionError] if the dates in [initialSelections] are not in UTC timezone and [autoConvert] is false. FCalendarController> useFDatesCalendarController({ Set initialSelections = const {}, bool Function(DateTime)? selectable, + bool autoConvert = true, List? keys, }) => use(_CalendarControllerHook>( @@ -39,6 +70,7 @@ FCalendarController> useFDatesCalendarController({ create: (hook) => FCalendarController.dates( initialSelections: hook.value, selectable: hook.selectable, + truncateAndStripTimezone: autoConvert, ), )); @@ -47,11 +79,28 @@ FCalendarController> useFDatesCalendarController({ /// /// [selectable] will always return true if not given. /// -/// Both the start and end dates of the range is inclusive. The selected dates are always in UTC timezone and truncated -/// to the nearest day. Unselectable dates within the selected range are selected regardless. +/// [autoConvert] determines whether the controller should truncate and convert all given [DateTime]s to dates in +/// UTC timezone. Defaults to true. +/// +/// ```dart +/// DateTime convert(DateTime date) => DateTime.utc(date.year, date.month, date.day); +/// ``` +/// +/// [autoConvert] should be set to false if you can guarantee that all dates are in UTC timezone (with the help of an +/// 3rd party library), which will improve performance. **Warning:** Giving a [DateTime] in local timezone or with a +/// time component when [autoConvert] is false is undefined behavior. +/// +/// Both the start and end dates of the range is inclusive. Unselectable dates within the selected range are +/// selected regardless. +/// +/// ## Contract +/// Throws [AssertionError] if: +/// * the given dates in [initialSelection] is not in UTC timezone and [autoConvert] is false. +/// * the end date is less than start date. FCalendarController<(DateTime, DateTime)?> useFRangeCalendarController({ (DateTime, DateTime)? initialSelection, bool Function(DateTime)? selectable, + bool autoConvert = true, List? keys, }) => use(_CalendarControllerHook<(DateTime, DateTime)?>( @@ -61,6 +110,7 @@ FCalendarController<(DateTime, DateTime)?> useFRangeCalendarController({ create: (hook) => FCalendarController.range( initialSelection: hook.value, selectable: hook.selectable, + truncateAndStripTimezone: autoConvert, ), )); diff --git a/forui_hooks/pubspec.yaml b/forui_hooks/pubspec.yaml index d13475cba..ba445cedd 100644 --- a/forui_hooks/pubspec.yaml +++ b/forui_hooks/pubspec.yaml @@ -1,6 +1,6 @@ name: forui_hooks description: "Companion hooks for Forui, a set of beautifully designed, minimalistic widgets for desktop & touch devices." -version: 0.7.0 +version: 0.7.0+1 homepage: https://forui.dev/ documentation: https://forui.dev/docs repository: https://github.com/forus-labs/forui/tree/main/forui_hooks diff --git a/samples/lib/widgets/calendar.dart b/samples/lib/widgets/calendar.dart index 2693b3238..228f70017 100644 --- a/samples/lib/widgets/calendar.dart +++ b/samples/lib/widgets/calendar.dart @@ -25,8 +25,8 @@ class CalendarPage extends Sample { @override Widget sample(BuildContext context) => FCalendar( controller: FCalendarController.date(initialSelection: selected), - start: DateTime.utc(2000), - end: DateTime.utc(2030), + start: DateTime(2000), + end: DateTime(2030), ); } @@ -39,11 +39,11 @@ class DatesCalendarPage extends Sample { @override Widget sample(BuildContext context) => FCalendar( controller: FCalendarController.dates( - initialSelections: {DateTime.utc(2024, 7, 17), DateTime.utc(2024, 7, 20)}, + initialSelections: {DateTime(2024, 7, 17), DateTime(2024, 7, 20)}, ), - start: DateTime.utc(2000), - today: DateTime.utc(2024, 7, 15), - end: DateTime.utc(2030), + start: DateTime(2000), + today: DateTime(2024, 7, 15), + end: DateTime(2030), ); } @@ -56,12 +56,12 @@ class UnselectableCalendarPage extends Sample { @override Widget sample(BuildContext context) => FCalendar( controller: FCalendarController.dates( - initialSelections: {DateTime.utc(2024, 7, 17), DateTime.utc(2024, 7, 20)}, - selectable: (date) => !{DateTime.utc(2024, 7, 18), DateTime.utc(2024, 7, 19)}.contains(date), + initialSelections: {DateTime(2024, 7, 17), DateTime(2024, 7, 20)}, + selectable: (date) => !{DateTime(2024, 7, 18), DateTime(2024, 7, 19)}.contains(date), ), - start: DateTime.utc(2000), - today: DateTime.utc(2024, 7, 15), - end: DateTime.utc(2030), + start: DateTime(2000), + today: DateTime(2024, 7, 15), + end: DateTime(2030), ); } @@ -73,11 +73,9 @@ class RangeCalendarPage extends Sample { @override Widget sample(BuildContext context) => FCalendar( - controller: FCalendarController.range( - initialSelection: (DateTime.utc(2024, 7, 17), DateTime.utc(2024, 7, 20)), - ), - start: DateTime.utc(2000), - today: DateTime.utc(2024, 7, 15), - end: DateTime.utc(2030), + controller: FCalendarController.range(initialSelection: (DateTime(2024, 7, 17), DateTime(2024, 7, 20))), + start: DateTime(2000), + today: DateTime(2024, 7, 15), + end: DateTime(2030), ); }