├── .changeset ├── README.md └── config.json ├── .eslintrc.js ├── .github └── workflows │ ├── deploy-docs.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .vscode ├── extensions.json ├── settings.json └── useful.code-snippets ├── LICENSE ├── README.md ├── apps ├── docs │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── docs │ │ ├── fundamentals │ │ │ ├── _category_.json │ │ │ ├── assets │ │ │ │ ├── anatomy.png │ │ │ │ ├── backup.png │ │ │ │ ├── basic-calendar-list.png │ │ │ │ ├── basic-calendar.png │ │ │ │ ├── bundle-size.png │ │ │ │ ├── calendar-list-compact.png │ │ │ │ ├── custom-formatting.png │ │ │ │ ├── custom-locale.png │ │ │ │ ├── date-range-calendar-list.png │ │ │ │ ├── disabled-dates.png │ │ │ │ ├── linear-theme.png │ │ │ │ └── two-calendars-mounted.png │ │ │ ├── customization.mdx │ │ │ ├── limitations.mdx │ │ │ ├── principles.mdx │ │ │ ├── tips-and-tricks.mdx │ │ │ ├── troubleshooting.mdx │ │ │ └── usage.mdx │ │ └── index.mdx │ ├── docusaurus.config.ts │ ├── package.json │ ├── sidebars.ts │ ├── src │ │ ├── components │ │ │ ├── HStack.tsx │ │ │ ├── HomepageFeatures │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.css │ │ │ └── VStack.tsx │ │ └── css │ │ │ └── custom.css │ ├── static │ │ ├── .nojekyll │ │ ├── img │ │ │ ├── cover.png │ │ │ ├── docusaurus-social-card.jpg │ │ │ ├── docusaurus.png │ │ │ ├── favicon.ico │ │ │ ├── hero.png │ │ │ ├── logo.svg │ │ │ ├── shots.png │ │ │ ├── social-card.png │ │ │ ├── undraw_docusaurus_mountain.svg │ │ │ ├── undraw_docusaurus_react.svg │ │ │ └── undraw_docusaurus_tree.svg │ │ └── videos │ │ │ ├── bottom-sheet-android.mp4 │ │ │ ├── example-app-demo.mp4 │ │ │ ├── imperative-scroll.mp4 │ │ │ ├── limitation-backward-scrolling-workaround.mp4 │ │ │ ├── limitation-backward-scrolling.mp4 │ │ │ ├── moni-demo.mp4 │ │ │ ├── perf-calendar.mp4 │ │ │ ├── rerender-demo.mp4 │ │ │ ├── scroll-performance-demo.mp4 │ │ │ ├── troubleshooting │ │ │ ├── slow-after.mp4 │ │ │ └── slow-before.mp4 │ │ │ └── windows-xp-calendar.mp4 │ └── tsconfig.json └── example │ ├── .eslintrc.js │ ├── .gitignore │ ├── .storybook │ ├── index.ts │ ├── main.ts │ ├── preview.tsx │ └── storybook.requires.ts │ ├── .watchmanconfig │ ├── app.json │ ├── assets │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash.png │ ├── babel.config.js │ ├── index.js │ ├── metro.config.js │ ├── package.json │ ├── src │ ├── App.tsx │ └── components │ │ ├── GithubIssues │ │ ├── CalendarGithubIssues.stories.tsx │ │ └── CalendarListGithubIssues.stories.tsx │ │ ├── PerfTestCalendar │ │ ├── PerfTestCalendar.stories.tsx │ │ ├── PerfTestCalendar.tsx │ │ ├── PerfTestCalendarItemDay.tsx │ │ ├── PerfTestCalendarList.stories.tsx │ │ └── useRenderCount.ts │ │ └── ThemeableCalendar │ │ ├── Calendar.stories.tsx │ │ ├── LinearCalendar.tsx │ │ └── WindowsXpCalendar │ │ ├── WindowXpButton.tsx │ │ ├── WindowXpCalendar.tsx │ │ ├── WindowsXpText.tsx │ │ ├── WindowsXpWindow.tsx │ │ ├── index.ts │ │ └── utils.ts │ └── tsconfig.json ├── bun.lockb ├── kitchen-sink └── expo │ ├── .eslintrc.js │ ├── .gitignore │ ├── CHANGELOG.md │ ├── app.json │ ├── assets │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash.png │ ├── babel.config.js │ ├── index.js │ ├── metro.config.js │ ├── package.json │ ├── src │ ├── App.tsx │ ├── BottomSheetCalendar.tsx │ ├── Calendar.tsx │ ├── CalendarCustomFormatting.tsx │ ├── CalendarList.tsx │ ├── CalendarListCustomSpacing.tsx │ ├── ImperativeScroll.tsx │ ├── SlowExampleAddressed.tsx │ └── components │ │ └── BottomSheetFlashList │ │ ├── BottomSheetFlashList.tsx │ │ └── index.ts │ └── tsconfig.json ├── package.json ├── packages ├── eslint-config │ ├── index.js │ └── package.json ├── flash-calendar │ ├── .eslintrc.js │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── Calendar.stories.tsx │ │ │ ├── Calendar.tsx │ │ │ ├── CalendarItemDay.tsx │ │ │ ├── CalendarItemEmpty.tsx │ │ │ ├── CalendarItemWeekName.tsx │ │ │ ├── CalendarList.stories.tsx │ │ │ ├── CalendarList.tsx │ │ │ ├── CalendarRowMonth.tsx │ │ │ ├── CalendarRowWeek.tsx │ │ │ ├── CalendarThemeProvider.tsx │ │ │ ├── HStack.tsx │ │ │ ├── VStack.tsx │ │ │ └── index.ts │ │ ├── developer │ │ │ ├── decorators.tsx │ │ │ ├── loggginHandler.tsx │ │ │ └── useRenderCount.ts │ │ ├── helpers │ │ │ ├── dates.test.ts │ │ │ ├── dates.ts │ │ │ ├── functions.ts │ │ │ ├── numbers.ts │ │ │ ├── strings.ts │ │ │ ├── tokens.ts │ │ │ └── types.ts │ │ ├── hooks │ │ │ ├── useCalendar.test.ts │ │ │ ├── useCalendar.ts │ │ │ ├── useCalendarList.test.ts │ │ │ ├── useCalendarList.tsx │ │ │ ├── useDateRange.test.ts │ │ │ ├── useDateRange.ts │ │ │ ├── useOptimizedDayMetadata.test.ts │ │ │ ├── useOptimizedDayMetadata.ts │ │ │ └── useTheme.ts │ │ └── index.ts │ ├── tsconfig.json │ └── tsup.config.ts └── tsconfig │ ├── base.json │ └── package.json ├── scripts └── analyze-bundle.sh ├── tsconfig.json └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch" 10 | } 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // This configuration only applies to the package manager root. 2 | /** @type {import("eslint").Linter.Config} */ 3 | module.exports = { 4 | ignorePatterns: ["apps/**", "packages/**"], 5 | extends: ["@marceloterreiro/eslint-config"], 6 | parser: "@typescript-eslint/parser", 7 | parserOptions: { 8 | project: true, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "apps/docs/**" 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | deploy: 15 | name: Deploy to GitHub Pages 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - uses: oven-sh/setup-bun@v1 22 | with: 23 | bun-version: "1.1.17" 24 | - run: bun install 25 | 26 | - name: Build website 27 | run: bun run build --filter docs 28 | 29 | # Popular action to deploy to GitHub Pages: 30 | # Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus 31 | - name: Deploy to GitHub Pages 32 | uses: peaceiris/actions-gh-pages@v3 33 | with: 34 | github_token: ${{ secrets.GITHUB_TOKEN }} 35 | # Build output to publish to the `gh-pages` branch: 36 | publish_dir: apps/docs/build 37 | # The following lines assign commit authorship to the official 38 | # GH-Actions bot for deploys to `gh-pages` branch: 39 | # https://github.com/actions/checkout/issues/13#issuecomment-724415212 40 | # The GH actions bot is used by default if you didn't specify the two fields. 41 | # You can swap them out with your own user credentials. 42 | user_name: github-actions[bot] 43 | user_email: 41898282+github-actions[bot]@users.noreply.github.com 44 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repo 16 | uses: actions/checkout@v3 17 | 18 | - uses: oven-sh/setup-bun@v1 19 | with: 20 | bun-version: "1.1.17" 21 | 22 | - name: Install Dependencies 23 | run: bun install 24 | 25 | - name: Build project 26 | run: bun run build --filter @marceloterreiro/flash-calendar 27 | 28 | - name: Create Release Pull Request or Publish to npm 29 | id: changesets 30 | uses: changesets/action@v1 31 | with: 32 | # This expects you to have a script called release which does a build for your packages and calls changeset publish 33 | publish: bun run publish-packages 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test (PR) 2 | on: 3 | pull_request: 4 | branches-ignore: 5 | - "changeset-release/main" 6 | 7 | jobs: 8 | test: 9 | name: test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: oven-sh/setup-bun@v1 14 | with: 15 | bun-version: "1.1.17" 16 | 17 | - name: Install Dependencies 18 | run: bun install 19 | 20 | - name: Build 21 | run: bun run build 22 | 23 | - name: Lint, Test, Typecheck 24 | run: bun turbo build lint test typecheck 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Monorepo 2 | apps/*/credentials.json 3 | apps/*/dist 4 | packages/*/dist 5 | 6 | # Turborepo 7 | .turbo 8 | 9 | # Expo 10 | .expo 11 | __generated__ 12 | web-build 13 | 14 | # macOS 15 | .DS_Store 16 | 17 | # Node 18 | node_modules 19 | npm-debug.log 20 | yarn-error.log 21 | package-lock.json 22 | 23 | # Ruby 24 | .direnv 25 | 26 | # Emacs 27 | *~ 28 | 29 | # Vim 30 | .*.swp 31 | .*.swo 32 | .*.swn 33 | .*.swm 34 | 35 | # Sublime Text 36 | *.sublime-project 37 | *.sublime-workspace 38 | 39 | # Xcode 40 | *.pbxuser 41 | !default.pbxuser 42 | *.xccheckout 43 | *.xcscmblueprint 44 | xcuserdata 45 | 46 | # Android Studio 47 | *.iml 48 | .gradle 49 | .idea/libraries 50 | .idea/workspace.xml 51 | .idea/gradle.xml 52 | .idea/misc.xml 53 | .idea/modules.xml 54 | .idea/vcs.xml 55 | 56 | # Eclipse 57 | .project 58 | .settings 59 | 60 | # VSCode 61 | .history/ -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "[typescriptreact]": { 4 | "editor.defaultFormatter": "esbenp.prettier-vscode" 5 | }, 6 | "[json]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode" 8 | }, 9 | "[typescript]": { 10 | "editor.defaultFormatter": "esbenp.prettier-vscode" 11 | }, 12 | "editor.formatOnSave": true, 13 | "editor.codeActionsOnSave": { 14 | "source.fixAll.eslint": "explicit" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/useful.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Stylesheet usage": { 3 | "prefix": "snip-stylesheet", 4 | "body": [ 5 | "import { StyleSheet } from \"react-native\"", 6 | "", 7 | "const styles = StyleSheet.create({", 8 | " container: { ", 9 | " padding: themeGlobals.spacings[12],", 10 | " }", 11 | " })" 12 | ] 13 | }, 14 | "Inline stylesheet": { 15 | "prefix": "snip-stylesheet-inline", 16 | "body": [ 17 | "useMemo(() => ({", 18 | "\t...styles.$1", 19 | "\t $2", 20 | "}), [$3]);" 21 | ] 22 | }, 23 | "Story": { 24 | "prefix": "snip-story", 25 | "body": [ 26 | "import { paddingDecorator } from \"@/developer/decorators\";", 27 | "import type { Meta, StoryObj } from \"@storybook/react\";", 28 | "import { ${TM_FILENAME_BASE/(.*)\\..+$/$1/} } from \"./${TM_FILENAME_BASE/(.*)\\..+$/$1/}\"", 29 | "import type { ${TM_FILENAME_BASE/(.*)\\..+$/$1/}Props } from \"./${TM_FILENAME_BASE/(.*)\\..+$/$1/}\"", 30 | "", 31 | "const ${TM_FILENAME_BASE/(.*)\\..+$/$1/}Meta: Meta = {", 32 | " title: \"${TM_FILENAME_BASE/(.*)\\..+$/$1/}\",", 33 | " component: ${TM_FILENAME_BASE/(.*)\\..+$/$1/},", 34 | " argTypes: {", 35 | " onPress: { action: \"onPress\" },", 36 | " },", 37 | " args: {", 38 | " children: \"hello world\",", 39 | " }," 40 | " decorators: [paddingDecorator],", 41 | "}", 42 | "", 43 | "export default ${TM_FILENAME_BASE/(.*)\\..+$/$1/}Meta", 44 | "", 45 | "export const Default: StoryObj = {}", 46 | "" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Marcelo T Prado 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Flash Calendar 2 | 3 | [![npm](https://img.shields.io/npm/l/@marceloterreiro/flash-calendar?style=flat-square)](https://www.npmjs.com/package/@marceloterreiro/flash-calendar) [![npm](https://img.shields.io/badge/types-included-blue?style=flat-square)](https://www.npmjs.com/package/@marceloterreiro/flash-calendar) [![runs with expo](https://img.shields.io/badge/Runs%20with%20Expo-4630EB.svg?style=flat-square&logo=EXPO&labelColor=f3f3f3&logoColor=000)](https://expo.io/) 4 | 5 | This is the monorepo for Flash Calendar, an incredibly fast and flexible library to build calendars in React Native. 6 | 7 | 8 | 9 | ## Documentation & Examples 10 | 11 | You can find the Flash Calendar documentation [on the website](https://marceloprado.github.io/flash-calendar/). 12 | 13 | ## Installation 14 | 15 | This project uses [Bun](https://bun.sh/) as its package manager. The first thing you'll need is to [install Bun](https://bun.sh/). 16 | 17 | To install dependencies, run at the root: 18 | 19 | ```bash 20 | bun install 21 | ``` 22 | 23 | To build (required for a fresh install) 24 | 25 | ```bash 26 | bun run build 27 | ``` 28 | 29 | To develop or run the example app: 30 | 31 | ```bash 32 | bun dev 33 | ``` 34 | 35 | To run the documentation website: 36 | 37 | ```bash 38 | bun docs 39 | ``` 40 | 41 | ## Contributing 42 | 43 | Ensure your changes are unit-tested. To improve DX, run the tests in watch mode with `bun test --watch`. You can also run the tests for a specific file with `bun test --watch {filename}`. 44 | 45 | ### Package structure 46 | 47 | #### /apps 48 | 49 | - `/apps/example`: Storybook host for Flash Calendar, runs with the latest uncompiled code. 50 | - `/apps/docs`: The documentation website for Flash Calendar. 51 | 52 | #### /kitchen-sink 53 | 54 | A place to test the published Flash Calendar in a real environment. 55 | 56 | - `/kitchen-sink/expo`: Scaffolded expo project to test the flash calendar in a real environment. 57 | 58 | #### /packages 59 | 60 | The actual src code for Flash Calendar: 61 | 62 | - `/packages/flash-calendar`: The flash calendar package itself. 63 | - `/packages/eslint-config`: Shared eslint config for the project. 64 | - `/packages/tsconfig`: Shared tsconfig for the project. 65 | 66 | 67 | ### License 68 | 69 | Flash Calendar is [MIT licensed](./LICENSE). -------------------------------------------------------------------------------- /apps/docs/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ["@marceloterreiro/eslint-config"], 5 | }; 6 | -------------------------------------------------------------------------------- /apps/docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /apps/docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /apps/docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /apps/docs/docs/fundamentals/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Fundamentals", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "All you need to know about Flash Calendar" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/docs/docs/fundamentals/assets/anatomy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/docs/fundamentals/assets/anatomy.png -------------------------------------------------------------------------------- /apps/docs/docs/fundamentals/assets/backup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/docs/fundamentals/assets/backup.png -------------------------------------------------------------------------------- /apps/docs/docs/fundamentals/assets/basic-calendar-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/docs/fundamentals/assets/basic-calendar-list.png -------------------------------------------------------------------------------- /apps/docs/docs/fundamentals/assets/basic-calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/docs/fundamentals/assets/basic-calendar.png -------------------------------------------------------------------------------- /apps/docs/docs/fundamentals/assets/bundle-size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/docs/fundamentals/assets/bundle-size.png -------------------------------------------------------------------------------- /apps/docs/docs/fundamentals/assets/calendar-list-compact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/docs/fundamentals/assets/calendar-list-compact.png -------------------------------------------------------------------------------- /apps/docs/docs/fundamentals/assets/custom-formatting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/docs/fundamentals/assets/custom-formatting.png -------------------------------------------------------------------------------- /apps/docs/docs/fundamentals/assets/custom-locale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/docs/fundamentals/assets/custom-locale.png -------------------------------------------------------------------------------- /apps/docs/docs/fundamentals/assets/date-range-calendar-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/docs/fundamentals/assets/date-range-calendar-list.png -------------------------------------------------------------------------------- /apps/docs/docs/fundamentals/assets/disabled-dates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/docs/fundamentals/assets/disabled-dates.png -------------------------------------------------------------------------------- /apps/docs/docs/fundamentals/assets/linear-theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/docs/fundamentals/assets/linear-theme.png -------------------------------------------------------------------------------- /apps/docs/docs/fundamentals/assets/two-calendars-mounted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/docs/fundamentals/assets/two-calendars-mounted.png -------------------------------------------------------------------------------- /apps/docs/docs/fundamentals/limitations.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | import { HStack } from "@site/src/components/HStack"; 6 | import limitationBackwardScrolling from "@site/static/videos/limitation-backward-scrolling.mp4"; 7 | import limitationBackwardScrollingWorkaround from "@site/static/videos/limitation-backward-scrolling-workaround.mp4"; 8 | 9 | # Limitations 10 | 11 | The following are the current known limitations of Flash Calendar. Please, feel free to contribute and help address them. 12 | 13 | ## Infinite scrolling doesn't work backwards 14 | 15 | We currently use `onEndReached` to append new months, but also need a `onStartReached` to prepend months. We need a reliable way to keep the scroll position intact when the user scrolls 16 | backwards. Functionality-wise, `prependMonths` is already built. We just need to hook it up to the scroll event. 17 | 18 | Waiting for Flash List's [#824](https://github.com/Shopify/flash-list/pull/824). 19 | 20 | Here's what happens when you try to scroll backwards: 21 | 22 | - **Expected**: The calendar scrolls to previous months infinitely at `calendarPastScrollRangeInMonths` increments. 23 | - **Actual**: The calendar doesn't scroll past the initial month. 24 | 25 | 26 | 27 |
28 | 29 | ```tsx 30 | export const ScrollingBackwardsLimitation = () => { 31 | return ( 32 | 33 | Notice how it doesn't scroll past January 34 | 35 | 42 | 43 | ); 44 | }; 45 | ``` 46 | 47 |
48 | 49 | 52 | 53 |
54 | 55 | ### Work-around 56 | 57 | As a work-around, you can use `calendarPastScrollRangeInMonths` and 58 | `calendarMinDateId` to preload all the months you need to scroll back to. 59 | 60 | This can have a performance impact since the month list is held in memory. However, we have an 61 | [example](https://github.com/MarceloPrado/flash-calendar/blob/70bbf3f5e19a6c10592a6ee606c57d7c880ff3d9/apps/example/src/components/PerfTestCalendar/PerfTestCalendarList.stories.tsx#L69-L102) 62 | rendering 2.000 months (166 years) just fine. Flash List performs really well with large lists, so it's likely that the main bottelneck will be how many items can be held in memory. 63 | 64 | 65 | 66 |
67 | 68 | ```tsx 69 | export const ScrollingBackwardsWorkaround = () => { 70 | return ( 71 | 72 | This preloads all past months between Jan 1st 2020 and today 73 | 74 | 82 | 83 | ); 84 | }; 85 | ``` 86 | 87 |
88 | 89 | 92 | 93 |
94 | -------------------------------------------------------------------------------- /apps/docs/docs/fundamentals/principles.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | import Image from "@theme/IdealImage"; 6 | import bundleSize from "./assets/bundle-size.png"; 7 | import rerenderVideo from "@site/static/videos/rerender-demo.mp4"; 8 | import scrollPerfVideo from "@site/static/videos/scroll-performance-demo.mp4"; 9 | 10 | # Principles 11 | 12 | Flash Calendar was initially built for 13 | [Moni](https://apps.apple.com/br/app/moni-finan%C3%A7as-pessoais/id6462422147), 14 | the product I work on during my free time. I saw a need for a fast and 15 | easy-to-customize package that allowed me to build high-quality and polished 16 | date pickers. 17 | 18 | The existing solutions at that time didn't cover my needs. The most popular one, 19 | [`react-native-calendars`](https://github.com/wix/react-native-calendars) by 20 | Wix, offerred great features, but there were a few things that bothered me: no 21 | bottom sheet support, polish gaps (e.g. months with 6 and 5 weeks had 22 | incosistent spacing between each other), bundle size (and extra dependencies 23 | like `lodash`) and difficulty to customize. 24 | 25 | After mantaining a patch for that library for more than 3 years, I decided to do 26 | things differently this time and build my own solution. 27 | 28 | The following are the guiding principles that helps me make decisions about the library. 29 | 30 | ## Solve fewer needs better 31 | 32 | After working with `react-native-calendars`, I realized that library tried to do 33 | too many things at once. Flash Calendar goes the opposite way. It offers an 34 | elegant way of building calendars and calendars lists. Features like an agenda 35 | mode are out of scope for this package. 36 | 37 | ## Fast 38 | 39 | Flash Calendar, as the name implies, works at lightening speeds for the use 40 | cases it was built for: 41 | 42 | - infinite lists 43 | - date picker 44 | - date range picker 45 | 46 | As such, it's heavily optimized to avoid unnecessary re-renders: 47 | 48 | 51 | 52 | From the video above, notice how **only the affected dates re-render**. This 53 | means your component stays responsive no matter how lengthy the calendar list 54 | is. 55 | 56 | Besides re-render performance, it's also heavily optimized for scroll 57 | performance, all thanks to the excellent 58 | [FlashList](https://shopify.github.io/flash-list) foundation (kudos to 59 | [Shopify](https://www.shopify.com/) for their incredible OSS work 🙏): 60 | 61 | 64 | 65 | Given the `` component is [just a 66 | wrapper](https://github.com/marceloprado/flash-calendar/blob/06c813b26b2f4527b3eaacaddfa424df26d42e61/packages/flash-calendar/src/components/CalendarList.tsx#L266-L281) 67 | around `FlashList`, it inherits all of its performance characteristics. 68 | Additionally, all of FlashList's performance tuning guides apply to Flash 69 | Calendar as well. You'll fell right at home. 70 | 71 | Finally, Flash Calendar also honors this principle for local development. 72 | Developing and writing tests for Flash Calendar should be so fast it makes you 73 | smile - in fact, this was the key reason [Bun](https://bun.sh/) was chosen as 74 | its test runner and [Turbo](https://turbo.build/) as its monorepo tool. 75 | 76 | ## Tiny footprint 77 | 78 | I work for a large company. I know we're not willing to substantially increase 79 | our bundle-size. In fact, we seize every oppotunity to decrease it. 80 | 81 | Besides, most companies already have their preferred date formatting library. 82 | Companies use `moment`, `date-fns`, `luxon`, `dayjs` and others. Flash Calendar 83 | shouldn't make an assumption about a particular library - this would add 84 | unnecessary bloat to every consumer. Flash Calendar allows you to **bring your 85 | own date formatting library**, giving you full control over how dates are 86 | formatted and localized. 87 | 88 | As a result, the library weights just 18.8kb in size (**6kb gzip**), with a 89 | [single 200 bytes](https://github.com/developit/mitt) external dependency: 90 | 91 | 92 | 93 | ## Easy to use and customizable 94 | 95 | With Flash Calendar, it's easy to do the right thing. The library offers a 96 | simple and intuitive API, and give you full control over the look and feel of 97 | your calendars. 98 | 99 | Read the [Customization](/fundamentals/customization) section for more details. 100 | -------------------------------------------------------------------------------- /apps/docs/docs/fundamentals/tips-and-tricks.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | import { HStack } from "@site/src/components/HStack"; 6 | import imperativeScrollVideo from "@site/static/videos/imperative-scroll.mp4"; 7 | 8 | # Tips and Tricks 9 | 10 | ## Exploring the libary and props 11 | 12 | All props from Flash Calendar are documented and prefixed with `calendar` to 13 | increase IDE discoverability. After installing the package, just type ` 56 | 57 |
58 | 59 | ```tsx 60 | import { addMonths, subMonths, startOfMonth } from "date-fns"; 61 | import type { CalendarListRef } from "@marceloterreiro/flash-calendar"; 62 | import { Calendar, toDateId } from "@marceloterreiro/flash-calendar"; 63 | import { useRef, useState } from "react"; 64 | import { Button, Text, View } from "react-native"; 65 | 66 | export function ImperativeScrolling() { 67 | const [currentMonth, setCurrentMonth] = useState(startOfMonth(new Date())); 68 | const ref = useRef(null); 69 | 70 | const onCalendarDayPress = useCallback((dateId: string) => { 71 | ref.current?.scrollToDate(fromDateId(dateId), true); 72 | }, []); 73 | 74 | return ( 75 | 76 | 77 |
108 | 109 | 112 | 113 | 114 | 115 | You can also pass an `additionalOffset` to fine-tune the scroll position: 116 | 117 | ```tsx 118 | .scrollToDate(fromDateId("2024-07-01"), true, { additionalOffset: 4 }) 119 | ``` 120 | 121 | ## Setting Border Radius to `Calendar.Item.Day` 122 | 123 | To apply a border radius to the `Calendar.Item.Day` component, it's necessary to 124 | specify the radius for all four corners. Here's an example of how to achieve 125 | this: 126 | 127 | ```tsx 128 | itemDay: { 129 | base: () => ({ 130 | container: { 131 | borderTopRightRadius: 10, 132 | borderBottomRightRadius: 10, 133 | borderTopLeftRadius: 10, 134 | borderBottomLeftRadius: 10, 135 | }, 136 | }), 137 | } 138 | ``` 139 | 140 | ## Avoiding dark mode 141 | 142 | If your app doesn't support dynamic themes, you can override Flash Calendar's 143 | color scheme by passing a `calendarColorScheme` prop: 144 | 145 | ```tsx 146 | export const LightModeOnly = () => { 147 | const { calendarActiveDateRanges, onCalendarDayPress } = useDateRange({ 148 | startId: "2024-02-04", 149 | endId: "2024-02-09", 150 | }); 151 | 152 | return ( 153 | 159 | ); 160 | }; 161 | ``` 162 | 163 | When set, Flash Calendar's theming system will use this scheme instead of the 164 | user system's theme. 165 | 166 | **Note**: you should avoid using this prop. Instead, your app should 167 | support dynamic themes that react to the user's system preferences. The prop is 168 | provided as an escape hatch for apps that doesn't support dynamic themes yet. 169 | 170 | ## Listening to the visible months 171 | 172 | You can listen to which months are currently visible by hooking into the [`onViewableItemsChange`](https://shopify.github.io/flash-list/docs/usage#onviewableitemschanged) prop: 173 | 174 | ```tsx 175 | export const ListenToVisibleMonth = () => { 176 | const [selectedDate, setSelectedDate] = useState(today); 177 | const [visibleMonth, setVisibleMonth] = useState(today); 178 | 179 | const handleViewableItemsChanged = useCallback< 180 | NonNullable["onViewableItemsChanged"]> 181 | >(({ viewableItems }) => { 182 | const firstVisibleItem = viewableItems.find((item) => item.isViewable); 183 | 184 | if (firstVisibleItem) { 185 | setVisibleMonth(firstVisibleItem.item.id); 186 | } 187 | }, []); 188 | 189 | return ( 190 | 191 | 192 | Selected date: {selectedDate} 193 | Visible month: {visibleMonth} 194 | 195 | 201 | 202 | ); 203 | }; 204 | ``` 205 | -------------------------------------------------------------------------------- /apps/docs/docs/fundamentals/troubleshooting.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | import { HStack } from "@site/src/components/HStack"; 6 | import { VStack } from "@site/src/components/VStack"; 7 | import slowBefore from "@site/static/videos/troubleshooting/slow-before.mp4"; 8 | import slowAfter from "@site/static/videos/troubleshooting/slow-after.mp4"; 9 | 10 | # Troubleshooting 11 | 12 | The following are the most common issues found when using Flash Calendar and a 13 | guide on how to fix them. 14 | 15 | ## `FlashList's rendered size is not usable.` 16 | 17 | Check out [Flash List 18 | docs](https://shopify.github.io/flash-list/docs/known-issues/#1-flashlists-rendered-size-is-not-usable-warning) 19 | for instructions on how to fix this warning. 20 | 21 | **TLDR**: ensure the parent of `` has a fixed height or a 22 | `flex: 1` style. 23 | 24 | ## `ReferenceError: Can't find variable: Intl` (Android) 25 | 26 | Flash Calendar uses `Intl` primitives to format dates in a locale-aware way. 27 | JavaScriptCore (`jsc`), the legacy React Native JS runtime, doesn't support 28 | `Intl` out of the box on Android. 29 | 30 | It's highly advised to upgrade to `Hermes`, the new default JS runtime ([more on 31 | that](https://reactnative.dev/docs/hermes)). Besides better performance, it also 32 | supports the `Intl` primitives Flash Calendar uses. For Expo, update your 33 | `app.json` to use `hermes`: 34 | 35 | ```json 36 | { 37 | "expo": { 38 | "jsEngine": "hermes" 39 | } 40 | } 41 | ``` 42 | 43 | If your company is stuck with `jsc`, you can either use a polyfill, or bypass 44 | `Intl` entirely by [providing your own date formatting 45 | functions](/fundamentals/usage#custom-date-formatting). 46 | 47 | ## Calendar.List is slow when using date ranges 48 | 49 | If you're seeing frame drops when using `Calendar.List`, there's a high chance 50 | you're suffering from too many re-renders. If you're not careful with 51 | memoization, the entire list re-renders whenever the `calendarActiveDateRanges` 52 | prop changes. **Notice the frame drops and how each `BaseCalendar` re-renders in 53 | the React DevTools profiler (this is bad 👎)**: 54 | 55 | 56 | 57 | 60 | 61 |
62 | 63 | ```tsx 64 | import { Calendar, toDateId } from "@marceloterreiro/flash-calendar"; 65 | import { addMonths } from "date-fns"; 66 | import { Text } from "react-native"; 67 | 68 | const todayId = toDateId(new Date()); 69 | const maxDateId = toDateId(addMonths(new Date(), 12)); 70 | 71 | export const SlowExample = () => { 72 | const [dateIds, setDateIds] = useState([]); 73 | const dateRanges = dateIds.map((dateId) => ({ 74 | startId: dateId, 75 | endId: dateId, 76 | })); 77 | 78 | return ( 79 | 80 | ⚠️ Don't copy-paste this example, it has performance issues 81 | 82 | { 88 | if (dateIds.includes(dateId)) { 89 | setDateIds(dateIds.filter((id) => id !== dateId)); 90 | } else { 91 | setDateIds([...dateIds, dateId]); 92 | } 93 | }} 94 | /> 95 | 96 | ); 97 | }; 98 | ``` 99 | 100 |
101 | 102 |
103 | 104 | The easiest fix is to use the provided [`useDateRange` 105 | hook](/fundamentals/usage#date-range-picker). The hook is optimized by default 106 | and works perfectly with `Calendar.List`. 107 | 108 | However, there might be cases where you need to control the `onCalendarDayPress` 109 | event and decide how the date range logic works. In those cases, make sure you 110 | memoize the `onCalendarDayPress` and use the [updater function 111 | pattern](https://react.dev/reference/react/useState#updating-state-based-on-the-previous-state). 112 | **Notice it runs on 60 FPS and the `BaseCalendar` components don't re-render 113 | anymore (this is good 👍)**: 114 | 115 | 116 | 117 | 120 | 121 |
122 | 123 | ```tsx 124 | import type { CalendarOnDayPress } from "@marceloterreiro/flash-calendar"; 125 | import { Calendar, toDateId } from "@marceloterreiro/flash-calendar"; 126 | import { addMonths } from "date-fns"; 127 | import { useCallback, useState } from "react"; 128 | import { Text } from "react-native"; 129 | 130 | const todayId = toDateId(new Date()); 131 | const maxDateId = toDateId(addMonths(new Date(), 12)); 132 | 133 | export const SlowExampleAddressed = () => { 134 | const [dateIds, setDateIds] = useState([]); 135 | const dateRanges = dateIds.map((dateId) => ({ 136 | startId: dateId, 137 | endId: dateId, 138 | })); 139 | 140 | // highlight-start 141 | // This is the fix: memoized onCalendarDayPress and updater function pattern 142 | // It keeps `BaseCalendar` props stable, allowing each month to skip re-renders 143 | const handleCalendarDayPress = useCallback((dateId) => { 144 | setDateIds((dateIds) => { 145 | if (dateIds.includes(dateId)) { 146 | return dateIds.filter((id) => id !== dateId); 147 | } else { 148 | return [...dateIds, dateId]; 149 | } 150 | }); 151 | }, []); 152 | // highlight-end 153 | 154 | return ( 155 | 156 | ✅ This is safe to copy, perf issues addressed 157 | 158 | 165 | 166 | ); 167 | }; 168 | ``` 169 | 170 |
171 | 172 |
173 | -------------------------------------------------------------------------------- /apps/docs/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | import Image from "@theme/IdealImage"; 6 | import playgroundVideo from "@site/static/videos/example-app-demo.mp4"; 7 | import moniVideo from "@site/static/videos/moni-demo.mp4"; 8 | 9 | # Introduction 10 | 11 | **An incredibly fast and flexible way of building calendars in React Native.** 12 | 13 | 14 | 15 | ## Features 16 | 17 | - iOS and Android support 18 | - Expo compatible, no binary updates required 19 | - Localization built-in 20 | - Customizable and composable UI 21 | - Dark-mode out of the box 22 | - Infinite scroll 23 | - Date range support 24 | - Tiny bundle size _(18kb minified, 6kb gzip)_ 25 | - Just a [single](https://github.com/developit/mitt) third-party dependency _(200 bytes)_ 26 | - [Bottom sheet](https://github.com/gorhom/react-native-bottom-sheet) compatible 27 | 28 | ## Installation 29 | 30 | Add the package to your project: 31 | 32 | ```bash npm2yarn 33 | npm add @marceloterreiro/flash-calendar 34 | ``` 35 | 36 | #### Requirements 37 | 38 | To use this package, **you also need to install its peer dependencies**. Check out their documentation for more information: 39 | 40 | - [`@shopify/flash-list`](https://shopify.github.io/flash-list) 41 | 42 | ## Getting started 43 | 44 | **To learn more about Flash Calendar, start [here](/fundamentals/principles).** 45 | 46 | ## Playground & examples 47 | 48 | There are a bunch of examples, including interactable stories, available in the [`apps/example`](https://github.com/MarceloPrado/flash-calendar/tree/285bcec42efa067455bcf44862eeb0f74985caba/apps/example) package. Read the Contributing section from the README for instructions on how to run it. Here's a sneak peek: 49 | 50 | 53 | 54 | ## Who's using it? 55 | 56 | - 🇧🇷 [Moni](https://apps.apple.com/br/app/moni-finan%C3%A7as-pessoais/id6462422147), a Brazilian personal finance app that helps users track their finances effortlessly ([iOS](https://apps.apple.com/br/app/moni-finan%C3%A7as-pessoais/id6462422147), [Android](https://play.google.com/store/apps/details?id=com.marceloterreiro.moni)) 57 | - 🇺🇸 [Brex](https://brex.com), the AI-powered spend platform ([iOS](https://apps.apple.com/us/app/brex/id1472905508), [Android](https://play.google.com/store/apps/details?id=com.brex.mobile&hl=en&pli=1)) 58 | - 🇺🇸 [Dysperse](https://dysperse.com), an all-in-one unified productivity platform designed to enhance efficiency and streamline tasks ([Windows](https://click.dysperse.com/xP1iJKE), [Web](https://click.dysperse.com/b1PFaNM)) 59 | 60 | Here's a sneak peek of what you can build with Flash Calendar (from Moni's production app): 61 | 62 | 65 | 66 | In this demo, there are several features working together: 67 | 68 | - [date range picker](/fundamentals/usage#date-range-picker) 69 | - [theme prop](/fundamentals/customization.mdx#theme-prop) 70 | - [bottom-sheet integration](/fundamentals/usage#bottom-sheet) 71 | - infinite scroll 72 | - [localization](/fundamentals/usage#localization-and-date-formatting) 73 | 74 | ## Resources 75 | 76 | - [Build fast, flexible calendars with Flash 77 | Calendar](https://expo.dev/blog/build-fast-flexible-calendars-in-react-native-with-flash-calendar) | 78 | Expo Blog 79 | -------------------------------------------------------------------------------- /apps/docs/docusaurus.config.ts: -------------------------------------------------------------------------------- 1 | import { themes as prismThemes } from "prism-react-renderer"; 2 | import type { Config } from "@docusaurus/types"; 3 | import type * as Preset from "@docusaurus/preset-classic"; 4 | 5 | const config: Config = { 6 | title: "Flash Calendar", 7 | tagline: "The fastest calendar component for React Native", 8 | favicon: "img/favicon.ico", 9 | 10 | // Set the production url of your site here 11 | url: "https://marceloprado.github.io", 12 | // Set the // pathname under which your site is served 13 | // For GitHub pages deployment, it is often '//' 14 | baseUrl: "/flash-calendar/", 15 | 16 | // GitHub pages deployment config. 17 | // If you aren't using GitHub pages, you don't need these. 18 | organizationName: "marceloprado", // Usually your GitHub org/user name. 19 | projectName: "flash-calendar", // Usually your repo name. 20 | trailingSlash: false, 21 | 22 | onBrokenLinks: "throw", 23 | onBrokenMarkdownLinks: "warn", 24 | 25 | // Even if you don't use internationalization, you can use this field to set 26 | // useful metadata like html lang. For example, if your site is Chinese, you 27 | // may want to replace "en" with "zh-Hans". 28 | i18n: { 29 | defaultLocale: "en", 30 | locales: ["en"], 31 | }, 32 | 33 | presets: [ 34 | [ 35 | "classic", 36 | { 37 | docs: { 38 | routeBasePath: "/", // Serve the docs at the site's root. 39 | }, 40 | blog: false, 41 | theme: { 42 | customCss: "./src/css/custom.css", 43 | }, 44 | } satisfies Preset.Options, 45 | ], 46 | ], 47 | 48 | plugins: ["@docusaurus/plugin-ideal-image"], 49 | 50 | themeConfig: { 51 | algolia: { 52 | appId: "XZ9MYUH844", 53 | apiKey: "f23abd1fe4bf9bf91c59d7ccd526269d", 54 | indexName: "marcelopradoio", 55 | }, 56 | image: "img/social-card.png", 57 | navbar: { 58 | title: "Flash Calendar", 59 | logo: { 60 | alt: "Flash Calendar Logo", 61 | src: "img/logo.svg", 62 | }, 63 | items: [ 64 | { 65 | type: "docSidebar", 66 | sidebarId: "defaultSidebar", 67 | position: "left", 68 | label: "Docs", 69 | }, 70 | { 71 | href: "https://github.com/marceloprado/flash-calendar", 72 | label: "GitHub", 73 | position: "right", 74 | }, 75 | ], 76 | }, 77 | footer: { 78 | style: "dark", 79 | links: [ 80 | { 81 | title: "More", 82 | 83 | items: [ 84 | { 85 | label: "GitHub", 86 | href: "https://github.com/MarceloPrado/flash-calendar", 87 | }, 88 | { 89 | label: "Twitter", 90 | href: "https://twitter.com/marceloterreiro", 91 | }, 92 | ], 93 | }, 94 | ], 95 | copyright: `Copyright © ${new Date().getFullYear()} Marcelo Prado. Built with Docusaurus.`, 96 | }, 97 | prism: { 98 | theme: prismThemes.github, 99 | darkTheme: prismThemes.dracula, 100 | }, 101 | } satisfies Preset.ThemeConfig, 102 | }; 103 | 104 | export default config; 105 | -------------------------------------------------------------------------------- /apps/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "dev": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "docs": "docusaurus start", 13 | "serve": "docusaurus serve", 14 | "clean": "rm -rf .docusaurus && rm -rf node_modules && rm -rf build", 15 | "write-translations": "docusaurus write-translations", 16 | "write-heading-ids": "docusaurus write-heading-ids", 17 | "typecheck": "tsc --noEmit" 18 | }, 19 | "dependencies": { 20 | "@docusaurus/core": "^3.4.0", 21 | "@docusaurus/plugin-ideal-image": "^3.4.0", 22 | "@docusaurus/preset-classic": "^3.4.0", 23 | "@mdx-js/react": "^3.0.0", 24 | "clsx": "^2.0.0", 25 | "prism-react-renderer": "^2.3.0", 26 | "react": "18.2.0", 27 | "react-dom": "^18.2.0", 28 | "react-loadable": "npm:@docusaurus/react-loadable@5.5.2" 29 | }, 30 | "devDependencies": { 31 | "@docusaurus/module-type-aliases": "^3.4.0", 32 | "@docusaurus/tsconfig": "^3.4.0", 33 | "@docusaurus/types": "^3.4.0", 34 | "@marceloterreiro/eslint-config": "*", 35 | "typescript": "^5.3.3" 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.5%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 3 chrome version", 45 | "last 3 firefox version", 46 | "last 5 safari version" 47 | ] 48 | }, 49 | "engines": { 50 | "node": ">=18.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /apps/docs/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; 2 | 3 | /** 4 | * Creating a sidebar enables you to: 5 | - create an ordered group of docs 6 | - render a sidebar for each doc of that group 7 | - provide next/previous navigation 8 | 9 | The sidebars can be generated from the filesystem, or explicitly defined here. 10 | 11 | Create as many sidebars as you want. 12 | */ 13 | const sidebars: SidebarsConfig = { 14 | // By default, Docusaurus generates a sidebar from the docs folder structure 15 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 16 | 17 | // But you can create a sidebar manually 18 | /* 19 | tutorialSidebar: [ 20 | 'intro', 21 | 'hello', 22 | { 23 | type: 'category', 24 | label: 'Tutorial', 25 | items: ['tutorial-basics/create-a-document'], 26 | }, 27 | ], 28 | */ 29 | }; 30 | 31 | export default sidebars; 32 | -------------------------------------------------------------------------------- /apps/docs/src/components/HStack.tsx: -------------------------------------------------------------------------------- 1 | import { type ReactNode } from "react"; 2 | 3 | export interface HStackProps { 4 | alignItems?: HTMLDivElement["style"]["alignItems"]; 5 | justifyContent?: HTMLDivElement["style"]["justifyContent"]; 6 | children: ReactNode; 7 | grow?: boolean; 8 | shrink?: boolean; 9 | spacing?: number; 10 | wrap?: HTMLDivElement["style"]["flexWrap"]; 11 | backgroundColor?: string; 12 | style?: HTMLDivElement["style"]; 13 | width?: HTMLDivElement["style"]["width"]; 14 | } 15 | 16 | export const HStack = ({ 17 | alignItems, 18 | children, 19 | justifyContent = "flex-start", 20 | spacing = 0, 21 | }: HStackProps) => { 22 | return ( 23 |
32 | {children} 33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /apps/docs/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Heading from '@theme/Heading'; 3 | import styles from './styles.module.css'; 4 | 5 | type FeatureItem = { 6 | title: string; 7 | Svg: React.ComponentType>; 8 | description: JSX.Element; 9 | }; 10 | 11 | const FeatureList: FeatureItem[] = [ 12 | { 13 | title: 'Easy to Use', 14 | Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, 15 | description: ( 16 | <> 17 | Docusaurus was designed from the ground up to be easily installed and 18 | used to get your website up and running quickly. 19 | 20 | ), 21 | }, 22 | { 23 | title: 'Focus on What Matters', 24 | Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, 25 | description: ( 26 | <> 27 | Docusaurus lets you focus on your docs, and we'll do the chores. Go 28 | ahead and move your docs into the docs directory. 29 | 30 | ), 31 | }, 32 | { 33 | title: 'Powered by React', 34 | Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, 35 | description: ( 36 | <> 37 | Extend or customize your website layout by reusing React. Docusaurus can 38 | be extended while reusing the same header and footer. 39 | 40 | ), 41 | }, 42 | ]; 43 | 44 | function Feature({title, Svg, description}: FeatureItem) { 45 | return ( 46 |
47 |
48 | 49 |
50 |
51 | {title} 52 |

{description}

53 |
54 |
55 | ); 56 | } 57 | 58 | export default function HomepageFeatures(): JSX.Element { 59 | return ( 60 |
61 |
62 |
63 | {FeatureList.map((props, idx) => ( 64 | 65 | ))} 66 |
67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /apps/docs/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /apps/docs/src/components/VStack.tsx: -------------------------------------------------------------------------------- 1 | import { type ReactNode } from "react"; 2 | 3 | export interface VStackProps { 4 | alignItems?: HTMLDivElement["style"]["alignItems"]; 5 | justifyContent?: HTMLDivElement["style"]["justifyContent"]; 6 | children: ReactNode; 7 | grow?: boolean; 8 | shrink?: boolean; 9 | spacing?: number; 10 | wrap?: HTMLDivElement["style"]["flexWrap"]; 11 | backgroundColor?: string; 12 | style?: HTMLDivElement["style"]; 13 | width?: HTMLDivElement["style"]["width"]; 14 | } 15 | 16 | export const VStack = ({ 17 | alignItems, 18 | children, 19 | justifyContent = "flex-start", 20 | spacing = 0, 21 | }: VStackProps) => { 22 | return ( 23 |
32 | {children} 33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /apps/docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #2e8555; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #25c2a0; 23 | --ifm-color-primary-dark: #21af90; 24 | --ifm-color-primary-darker: #1fa588; 25 | --ifm-color-primary-darkest: #1a8870; 26 | --ifm-color-primary-light: #29d5b0; 27 | --ifm-color-primary-lighter: #32d8b4; 28 | --ifm-color-primary-lightest: #4fddbf; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | -------------------------------------------------------------------------------- /apps/docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/.nojekyll -------------------------------------------------------------------------------- /apps/docs/static/img/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/img/cover.png -------------------------------------------------------------------------------- /apps/docs/static/img/docusaurus-social-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/img/docusaurus-social-card.jpg -------------------------------------------------------------------------------- /apps/docs/static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/img/docusaurus.png -------------------------------------------------------------------------------- /apps/docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /apps/docs/static/img/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/img/hero.png -------------------------------------------------------------------------------- /apps/docs/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/docs/static/img/shots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/img/shots.png -------------------------------------------------------------------------------- /apps/docs/static/img/social-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/img/social-card.png -------------------------------------------------------------------------------- /apps/docs/static/videos/bottom-sheet-android.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/videos/bottom-sheet-android.mp4 -------------------------------------------------------------------------------- /apps/docs/static/videos/example-app-demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/videos/example-app-demo.mp4 -------------------------------------------------------------------------------- /apps/docs/static/videos/imperative-scroll.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/videos/imperative-scroll.mp4 -------------------------------------------------------------------------------- /apps/docs/static/videos/limitation-backward-scrolling-workaround.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/videos/limitation-backward-scrolling-workaround.mp4 -------------------------------------------------------------------------------- /apps/docs/static/videos/limitation-backward-scrolling.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/videos/limitation-backward-scrolling.mp4 -------------------------------------------------------------------------------- /apps/docs/static/videos/moni-demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/videos/moni-demo.mp4 -------------------------------------------------------------------------------- /apps/docs/static/videos/perf-calendar.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/videos/perf-calendar.mp4 -------------------------------------------------------------------------------- /apps/docs/static/videos/rerender-demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/videos/rerender-demo.mp4 -------------------------------------------------------------------------------- /apps/docs/static/videos/scroll-performance-demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/videos/scroll-performance-demo.mp4 -------------------------------------------------------------------------------- /apps/docs/static/videos/troubleshooting/slow-after.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/videos/troubleshooting/slow-after.mp4 -------------------------------------------------------------------------------- /apps/docs/static/videos/troubleshooting/slow-before.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/videos/troubleshooting/slow-before.mp4 -------------------------------------------------------------------------------- /apps/docs/static/videos/windows-xp-calendar.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/docs/static/videos/windows-xp-calendar.mp4 -------------------------------------------------------------------------------- /apps/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/example/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ["@marceloterreiro/eslint-config"], 5 | rules: { 6 | "import/order": [ 7 | "error", 8 | { 9 | "newlines-between": "always", 10 | pathGroups: [ 11 | { 12 | pattern: "@/**", 13 | group: "external", 14 | position: "after", 15 | }, 16 | ], 17 | distinctGroup: false, 18 | }, 19 | ], 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /apps/example/.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /apps/example/.storybook/index.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from "@react-native-async-storage/async-storage"; 2 | import { view } from "./storybook.requires"; 3 | 4 | const StorybookUIRoot = view.getStorybookUI({ 5 | storage: { 6 | getItem: AsyncStorage.getItem, 7 | setItem: AsyncStorage.setItem, 8 | }, 9 | }); 10 | 11 | export default StorybookUIRoot; 12 | -------------------------------------------------------------------------------- /apps/example/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import { StorybookConfig } from "@storybook/react-native"; 2 | import * as path from "path"; 3 | 4 | const flashCalendarStories = path.resolve( 5 | __dirname, 6 | "../../../packages/flash-calendar/src/components/**/*.stories.?(ts|tsx|js|jsx)" 7 | ); 8 | 9 | const exampleStories = path.resolve( 10 | __dirname, 11 | "../src/components/**/*.stories.?(ts|tsx|js|jsx)" 12 | ); 13 | 14 | const main: StorybookConfig = { 15 | stories: [flashCalendarStories, exampleStories], 16 | addons: [ 17 | "@storybook/addon-ondevice-controls", 18 | "@storybook/addon-ondevice-actions", 19 | ], 20 | }; 21 | 22 | export default main; 23 | -------------------------------------------------------------------------------- /apps/example/.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react"; 2 | import { backgroundDecorator } from "@/developer/decorators"; 3 | 4 | const preview: Preview = { 5 | parameters: { 6 | controls: { 7 | matchers: { 8 | color: /(background|color)$/i, 9 | date: /Date$/, 10 | }, 11 | }, 12 | }, 13 | }; 14 | 15 | export default preview; 16 | 17 | export const decorators = [backgroundDecorator]; 18 | -------------------------------------------------------------------------------- /apps/example/.storybook/storybook.requires.ts: -------------------------------------------------------------------------------- 1 | /* do not change this file, it is auto generated by storybook. */ 2 | 3 | import { 4 | start, 5 | prepareStories, 6 | getProjectAnnotations, 7 | } from "@storybook/react-native"; 8 | 9 | import "@storybook/addon-ondevice-controls/register"; 10 | import "@storybook/addon-ondevice-actions/register"; 11 | 12 | const normalizedStories = [ 13 | { 14 | titlePrefix: "", 15 | directory: "../../packages/flash-calendar/src/components", 16 | files: "**/*.stories.?(ts|tsx|js|jsx)", 17 | importPathMatcher: 18 | /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/, 19 | // @ts-ignore 20 | req: require.context( 21 | "../../../packages/flash-calendar/src/components", 22 | true, 23 | /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/ 24 | ), 25 | }, 26 | { 27 | titlePrefix: "", 28 | directory: "./src/components", 29 | files: "**/*.stories.?(ts|tsx|js|jsx)", 30 | importPathMatcher: 31 | /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/, 32 | // @ts-ignore 33 | req: require.context( 34 | "../src/components", 35 | true, 36 | /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/ 37 | ), 38 | }, 39 | ]; 40 | 41 | declare global { 42 | var view: ReturnType; 43 | var STORIES: typeof normalizedStories; 44 | } 45 | 46 | const annotations = [ 47 | require("./preview"), 48 | require("@storybook/react-native/dist/preview"), 49 | require("@storybook/addon-actions/preview"), 50 | ]; 51 | 52 | global.STORIES = normalizedStories; 53 | 54 | // @ts-ignore 55 | module?.hot?.accept?.(); 56 | 57 | if (!global.view) { 58 | global.view = start({ 59 | annotations, 60 | storyEntries: normalizedStories, 61 | }); 62 | } else { 63 | const { importMap } = prepareStories({ storyEntries: normalizedStories }); 64 | 65 | global.view._preview.onStoriesChanged({ 66 | importFn: async (importPath: string) => importMap[importPath], 67 | }); 68 | 69 | global.view._preview.onGetProjectAnnotationsChanged({ 70 | getProjectAnnotations: getProjectAnnotations(global.view, annotations), 71 | }); 72 | } 73 | 74 | export const view = global.view; 75 | -------------------------------------------------------------------------------- /apps/example/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /apps/example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "example", 4 | "slug": "example", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "automatic", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "assetBundlePatterns": ["**/*"], 15 | "ios": { 16 | "supportsTablet": true 17 | }, 18 | "android": { 19 | "adaptiveIcon": { 20 | "foregroundImage": "./assets/adaptive-icon.png", 21 | "backgroundColor": "#ffffff" 22 | } 23 | }, 24 | "web": { 25 | "favicon": "./assets/favicon.png" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/example/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/example/assets/adaptive-icon.png -------------------------------------------------------------------------------- /apps/example/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/example/assets/favicon.png -------------------------------------------------------------------------------- /apps/example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/example/assets/icon.png -------------------------------------------------------------------------------- /apps/example/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/apps/example/assets/splash.png -------------------------------------------------------------------------------- /apps/example/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ["babel-preset-expo"], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /apps/example/index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from "expo"; 2 | 3 | import App from "./src/App"; 4 | 5 | registerRootComponent(App); 6 | -------------------------------------------------------------------------------- /apps/example/metro.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const path = require("path"); 4 | 5 | const { getDefaultConfig } = require("expo/metro-config"); 6 | const { generate } = require("@storybook/react-native/scripts/generate"); 7 | 8 | // Storybook setup 9 | generate({ 10 | configPath: path.resolve(__dirname, "./.storybook"), 11 | }); 12 | 13 | // Find the project and workspace directories 14 | const projectRoot = __dirname; 15 | // This can be replaced with `find-yarn-workspace-root` 16 | const monorepoRoot = path.resolve(projectRoot, "../.."); 17 | 18 | /** @type {import('expo/metro-config').MetroConfig} */ 19 | const config = getDefaultConfig(projectRoot); 20 | 21 | // 1. Watch all files within the monorepo 22 | config.watchFolders = [monorepoRoot]; 23 | // 2. Let Metro know where to resolve packages and in what order 24 | config.resolver.nodeModulesPaths = [ 25 | path.resolve(projectRoot, "node_modules"), 26 | path.resolve(monorepoRoot, "node_modules"), 27 | ]; 28 | 29 | // Storybook setup 30 | config.transformer.unstable_allowRequireContext = true; 31 | 32 | module.exports = config; 33 | -------------------------------------------------------------------------------- /apps/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@marceloterreiro/example", 3 | "version": "0.0.1", 4 | "private": true, 5 | "main": "./index.js", 6 | "scripts": { 7 | "android": "expo start --android", 8 | "clean": "rm -rf dist && rm -rf node_modules", 9 | "dev": "expo start", 10 | "ios": "expo start --ios", 11 | "lint": "eslint src", 12 | "start": "expo start", 13 | "typecheck": "tsc --noEmit", 14 | "storybook-generate": "sb-rn-get-stories", 15 | "web": "expo start --web" 16 | }, 17 | "dependencies": { 18 | "@marceloterreiro/flash-calendar": "*", 19 | "@shopify/flash-list": "^1.6.3", 20 | "date-fns": "^3.3.1", 21 | "expo": "~50.0.7", 22 | "expo-status-bar": "~1.11.1", 23 | "polished": "^4.2.2", 24 | "react": "18.2.0", 25 | "react-native": "0.73.4" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.20.0", 29 | "@marceloterreiro/eslint-config": "*", 30 | "@marceloterreiro/tsconfig": "*", 31 | "@react-native-async-storage/async-storage": "^1.21.0", 32 | "@react-native-community/datetimepicker": "^7.6.2", 33 | "@react-native-community/slider": "^4.5.0", 34 | "@storybook/addon-ondevice-actions": "^7.6.17", 35 | "@storybook/addon-ondevice-controls": "^7.6.17", 36 | "@storybook/react-native": "^7.6.17", 37 | "@types/react": "~18.2.45", 38 | "babel-loader": "^8.3.0", 39 | "eslint": "^8.56.0", 40 | "react-dom": "^18.2.0", 41 | "react-native-safe-area-context": "^4.9.0", 42 | "typescript": "^5.3.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/example/src/App.tsx: -------------------------------------------------------------------------------- 1 | export { default } from "../.storybook"; 2 | -------------------------------------------------------------------------------- /apps/example/src/components/GithubIssues/CalendarGithubIssues.stories.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Calendar, 3 | toDateId, 4 | useDateRange, 5 | } from "@marceloterreiro/flash-calendar"; 6 | import type { Meta } from "@storybook/react"; 7 | import { addDays, format } from "date-fns"; 8 | import { useEffect, useState } from "react"; 9 | import { View } from "react-native"; 10 | 11 | const CalendarMeta: Meta = { 12 | title: "Calendar/Github Issues", 13 | decorators: [], 14 | }; 15 | 16 | export default CalendarMeta; 17 | 18 | const today = toDateId(new Date()); 19 | const todayPlusFive = toDateId(addDays(new Date(), 5)); 20 | 21 | // See more: https://github.com/MarceloPrado/flash-calendar/issues/69 22 | export const Issue69 = () => { 23 | // This state is simplified for the example. In my case it would come from a context. 24 | const [start, setStart] = useState(today); 25 | const [end, setEnd] = useState(todayPlusFive); 26 | 27 | const { onCalendarDayPress, isDateRangeValid, calendarActiveDateRanges } = 28 | useDateRange({ startId: start, endId: end }); // I am setting the default range here because I want the data from my context to be pre-selected here 29 | 30 | useEffect(() => { 31 | // Every time the selected range changed I want to update my context (in this example the state) 32 | if (isDateRangeValid && calendarActiveDateRanges.length > 0) { 33 | const range = calendarActiveDateRanges[0]; 34 | setStart(range.startId); 35 | setEnd(range?.endId); 36 | } 37 | }, [calendarActiveDateRanges, isDateRangeValid]); 38 | 39 | return ( 40 | 41 | format(date, "EEEEEE")} 48 | onCalendarDayPress={onCalendarDayPress} 49 | /> 50 | 51 | ); 52 | }; 53 | 54 | const formatWeekDay = (date: Date) => format(date, "EEEEEE"); 55 | -------------------------------------------------------------------------------- /apps/example/src/components/GithubIssues/CalendarListGithubIssues.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | CalendarMonth, 3 | CalendarOnDayPress, 4 | CalendarTheme, 5 | } from "@marceloterreiro/flash-calendar"; 6 | import { Calendar, toDateId } from "@marceloterreiro/flash-calendar"; 7 | import type { FlashListProps } from "@shopify/flash-list"; 8 | import type { Meta } from "@storybook/react"; 9 | import { useCallback, useMemo, useState } from "react"; 10 | import { Alert, Text, View } from "react-native"; 11 | import { useTheme } from "@/hooks/useTheme"; 12 | 13 | const CalendarMeta: Meta = { 14 | title: "Calendar.List/Github Issues", 15 | decorators: [], 16 | }; 17 | 18 | export default CalendarMeta; 19 | 20 | // See more: https://github.com/MarceloPrado/flash-calendar/issues/11 21 | export const Issue11 = () => { 22 | const act = [ 23 | { endId: "2024-01-31", startId: "2024-01-30" }, 24 | { endId: "2024-01-12", startId: "2024-01-10" }, 25 | { endId: "2024-03-07", startId: "2024-02-28" }, 26 | { endId: "2024-04-10", startId: "2024-04-01" }, 27 | { endId: "2024-01-19", startId: "2024-01-18" }, 28 | { endId: "2024-02-06", startId: "2024-02-02" }, 29 | { endId: "2024-01-26", startId: "2024-01-25" }, 30 | { endId: "2024-01-05", startId: "2024-01-02" }, 31 | ]; 32 | const dis = ["2024-02-02", "2024-02-06", "2024-02-19", "2024-02-27"]; 33 | 34 | return ( 35 | { 40 | console.log("pressed"); 41 | }} 42 | /> 43 | ); 44 | }; 45 | 46 | const today = toDateId(new Date()); 47 | 48 | // See more: https://github.com/MarceloPrado/flash-calendar/issues/16 49 | export const Issue16 = () => { 50 | const [selectedDate, setSelectedDate] = useState(today); 51 | 52 | return ( 53 | 54 | Selected date: {selectedDate} 55 | 66 | 67 | ); 68 | }; 69 | 70 | const disabledRange = { 71 | start: "2024-06-01", 72 | end: "2024-06-15", 73 | }; 74 | 75 | function isDisabled(dateId: string) { 76 | return dateId >= disabledRange.start && dateId <= disabledRange.end; 77 | } 78 | 79 | // See more: https://github.com/MarceloPrado/flash-calendar/issues/38 80 | export const Issue38 = () => { 81 | const [selectedDate, setSelectedDate] = useState(today); 82 | const { colors } = useTheme(); 83 | 84 | const customTheme = useMemo(() => { 85 | const theme: CalendarTheme = { 86 | itemDay: { 87 | idle: ({ id }) => { 88 | if (isDisabled(id)) { 89 | return { 90 | container: {}, 91 | content: { 92 | color: colors.content.disabled, 93 | }, 94 | }; 95 | } 96 | return {}; 97 | }, 98 | }, 99 | }; 100 | 101 | return theme; 102 | }, [colors.content.disabled]); 103 | 104 | const handleCalendarDayPress: CalendarOnDayPress = useCallback((dateId) => { 105 | if (isDisabled(dateId)) { 106 | Alert.alert("This date is disabled"); 107 | return; 108 | } 109 | setSelectedDate(dateId); 110 | }, []); 111 | 112 | const calendarActiveDateRanges = useMemo(() => { 113 | if (!selectedDate) return []; 114 | return [{ startId: selectedDate, endId: selectedDate }]; 115 | }, [selectedDate]); 116 | 117 | return ( 118 | 119 | Selected date: {selectedDate} 120 | 126 | 127 | ); 128 | }; 129 | 130 | // See more at: https://github.com/MarceloPrado/flash-calendar/issues/65 131 | export const ListenToVisibleMonth = () => { 132 | const [selectedDate, setSelectedDate] = useState(today); 133 | const [visibleMonth, setVisibleMonth] = useState(today); 134 | 135 | const handleViewableItemsChanged = useCallback< 136 | NonNullable["onViewableItemsChanged"]> 137 | >(({ viewableItems }) => { 138 | const firstVisibleItem = viewableItems.find((item) => item.isViewable); 139 | 140 | if (firstVisibleItem) { 141 | setVisibleMonth(firstVisibleItem.item.id); 142 | } 143 | }, []); 144 | 145 | return ( 146 | 147 | 148 | Selected date: {selectedDate} 149 | Visible month: {visibleMonth} 150 | 151 | 157 | 158 | ); 159 | }; 160 | -------------------------------------------------------------------------------- /apps/example/src/components/PerfTestCalendar/PerfTestCalendar.stories.tsx: -------------------------------------------------------------------------------- 1 | import { toDateId, useDateRange } from "@marceloterreiro/flash-calendar"; 2 | import type { Meta } from "@storybook/react"; 3 | import { addDays, startOfMonth } from "date-fns"; 4 | import { useState } from "react"; 5 | import { paddingDecorator } from "@/developer/decorators"; 6 | 7 | import { PerfTestCalendar } from "./PerfTestCalendar"; 8 | 9 | const PerfTestCalendarMeta: Meta = { 10 | title: "Performance Test/Calendar", 11 | component: PerfTestCalendar, 12 | decorators: [paddingDecorator], 13 | }; 14 | 15 | export default PerfTestCalendarMeta; 16 | 17 | const startOfThisMonth = startOfMonth(new Date()); 18 | 19 | export const DatePicker = () => { 20 | const [activeDateId, setActiveDateId] = useState( 21 | toDateId(addDays(startOfThisMonth, 3)) 22 | ); 23 | 24 | return ( 25 | 32 | ); 33 | }; 34 | 35 | export const DateRangePicker = () => { 36 | const { calendarActiveDateRanges, onCalendarDayPress } = useDateRange({ 37 | startId: toDateId(addDays(startOfThisMonth, 3)), 38 | endId: toDateId(addDays(startOfThisMonth, 8)), 39 | }); 40 | 41 | return ( 42 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /apps/example/src/components/PerfTestCalendar/PerfTestCalendar.tsx: -------------------------------------------------------------------------------- 1 | import type { CalendarProps } from "@marceloterreiro/flash-calendar"; 2 | import { 3 | Calendar, 4 | activeDateRangesEmitter, 5 | useCalendar, 6 | } from "@marceloterreiro/flash-calendar"; 7 | import { memo, useEffect } from "react"; 8 | import { Text } from "react-native"; 9 | import { uppercaseFirstLetter } from "@/helpers/strings"; 10 | 11 | import { PerfTestCalendarItemDayWithContainer } from "./PerfTestCalendarItemDay"; 12 | import { useRenderCount } from "./useRenderCount"; 13 | 14 | const BasePerfTestCalendar = memo( 15 | ({ 16 | onCalendarDayPress, 17 | calendarRowVerticalSpacing = 8, 18 | calendarRowHorizontalSpacing = 8, 19 | theme, 20 | calendarDayHeight = 48, 21 | calendarMonthHeaderHeight = 20, 22 | calendarWeekHeaderHeight = calendarDayHeight, 23 | 24 | ...buildCalendarParams 25 | }: CalendarProps) => { 26 | const { calendarRowMonth, weeksList, weekDaysList } = 27 | useCalendar(buildCalendarParams); 28 | 29 | const renderCounter = useRenderCount(calendarRowMonth); 30 | 31 | return ( 32 | 33 | 37 | {uppercaseFirstLetter(calendarRowMonth)} 38 | 39 | {" "} 40 | (render: {renderCounter}x ⚡) 41 | 42 | 43 | 44 | {weekDaysList.map((weekDay, i) => ( 45 | 50 | {weekDay} 51 | 52 | ))} 53 | 54 | {weeksList.map((week, index) => ( 55 | 56 | {week.map((dayProps) => { 57 | if (dayProps.isDifferentMonth) { 58 | return ( 59 | 66 | 70 | 71 | ); 72 | } 73 | 74 | return ( 75 | 83 | {dayProps.displayLabel} 84 | 85 | ); 86 | })} 87 | 88 | ))} 89 | 90 | ); 91 | } 92 | ); 93 | 94 | BasePerfTestCalendar.displayName = "BasePerfTestCalendar"; 95 | 96 | export const PerfTestCalendar = memo( 97 | ({ calendarActiveDateRanges, calendarMonthId, ...props }: CalendarProps) => { 98 | useEffect(() => { 99 | activeDateRangesEmitter.emit("onSetActiveDateRanges", { 100 | ranges: calendarActiveDateRanges ?? [], 101 | }); 102 | /** 103 | * While `calendarMonthId` is not used by the effect, we still need it in 104 | * the dependency array since [FlashList uses recycling 105 | * internally](https://shopify.github.io/flash-list/docs/recycling). 106 | * 107 | * This means `Calendar` can re-render with different props instead of 108 | * getting re-mounted. Without it, we would see staled/invalid data, as 109 | * reported by 110 | * [#11](https://github.com/MarceloPrado/flash-calendar/issues/11). 111 | */ 112 | }, [calendarActiveDateRanges, calendarMonthId]); 113 | 114 | return ( 115 | 116 | ); 117 | } 118 | ); 119 | PerfTestCalendar.displayName = "PerfTestCalendar"; 120 | -------------------------------------------------------------------------------- /apps/example/src/components/PerfTestCalendar/PerfTestCalendarItemDay.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Calendar, 3 | useOptimizedDayMetadata, 4 | } from "@marceloterreiro/flash-calendar"; 5 | import { Text } from "react-native"; 6 | import type { CalendarItemDayWithContainerProps } from "@/components/CalendarItemDay"; 7 | 8 | import { useRenderCount } from "./useRenderCount"; 9 | 10 | export const PerfTestCalendarItemDayWithContainer = ({ 11 | children, 12 | metadata: baseMetadata, 13 | onPress, 14 | theme, 15 | dayHeight, 16 | daySpacing, 17 | containerTheme, 18 | }: CalendarItemDayWithContainerProps) => { 19 | const metadata = useOptimizedDayMetadata(baseMetadata); 20 | const renderCounter = useRenderCount(); 21 | 22 | return ( 23 | 34 | 40 | {children} 41 | 49 | {"\n"}render: {renderCounter}x 50 | 51 | 52 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /apps/example/src/components/PerfTestCalendar/PerfTestCalendarList.stories.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Calendar, 3 | toDateId, 4 | useDateRange, 5 | } from "@marceloterreiro/flash-calendar"; 6 | import type { Meta } from "@storybook/react"; 7 | import { addDays, startOfMonth } from "date-fns"; 8 | import { useState } from "react"; 9 | import { View, StyleSheet, Text } from "react-native"; 10 | import { paddingDecorator } from "@/developer/decorators"; 11 | 12 | import { PerfTestCalendar } from "./PerfTestCalendar"; 13 | 14 | const PerfTestCalendarMeta: Meta = { 15 | title: "Performance Test/Calendar.List", 16 | component: PerfTestCalendar, 17 | decorators: [paddingDecorator], 18 | }; 19 | 20 | export default PerfTestCalendarMeta; 21 | 22 | const startOfThisMonth = startOfMonth(new Date()); 23 | 24 | export const DatePicker = () => { 25 | const [activeDateId, setActiveDateId] = useState( 26 | toDateId(addDays(startOfThisMonth, 3)) 27 | ); 28 | 29 | return ( 30 | ( 39 | 40 | 41 | 42 | )} 43 | /> 44 | ); 45 | }; 46 | 47 | export const DateRangePicker = () => { 48 | const { calendarActiveDateRanges, onCalendarDayPress } = useDateRange({ 49 | startId: toDateId(addDays(startOfThisMonth, 3)), 50 | endId: toDateId(addDays(startOfThisMonth, 8)), 51 | }); 52 | 53 | return ( 54 | ( 61 | 62 | 63 | 64 | )} 65 | /> 66 | ); 67 | }; 68 | 69 | export const LenghtyDateRangePicker = () => { 70 | const { calendarActiveDateRanges, onCalendarDayPress } = useDateRange({ 71 | startId: toDateId(addDays(startOfThisMonth, 3)), 72 | endId: toDateId(addDays(startOfThisMonth, 8)), 73 | }); 74 | 75 | return ( 76 | 77 | 78 | This renders a list of 2.000 calendars at once, to stress-test 79 | Calendar.List's performance. 80 | 81 | 82 | ( 91 | 92 | 96 | 97 | )} 98 | /> 99 | 100 | 101 | ); 102 | }; 103 | 104 | const styles = StyleSheet.create({ 105 | container: { 106 | flex: 1, 107 | backgroundColor: "#fff", 108 | alignItems: "center", 109 | justifyContent: "center", 110 | }, 111 | text: { 112 | fontWeight: "bold", 113 | }, 114 | }); 115 | -------------------------------------------------------------------------------- /apps/example/src/components/PerfTestCalendar/useRenderCount.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | 3 | export const useRenderCount = (id?: string) => { 4 | const renderCount = useRef(0); 5 | renderCount.current += 1; 6 | 7 | const lastItemId = useRef(id); 8 | 9 | /** 10 | * See more at: https://shopify.github.io/flash-list/docs/recycling 11 | */ 12 | if (lastItemId.current !== id) { 13 | lastItemId.current = id; 14 | renderCount.current = 1; 15 | } 16 | 17 | return renderCount.current; 18 | }; 19 | -------------------------------------------------------------------------------- /apps/example/src/components/ThemeableCalendar/Calendar.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | CalendarActiveDateRange, 3 | CalendarOnDayPress, 4 | } from "@marceloterreiro/flash-calendar"; 5 | import { 6 | Calendar, 7 | fromDateId, 8 | toDateId, 9 | } from "@marceloterreiro/flash-calendar"; 10 | import type { Meta } from "@storybook/react"; 11 | import { add, sub } from "date-fns"; 12 | import { format } from "date-fns/fp"; 13 | import { useCallback, useMemo, useState } from "react"; 14 | import { StyleSheet, Text, View } from "react-native"; 15 | 16 | import { 17 | WindowsXpButton, 18 | WindowsXpCalendar, 19 | WindowsXpWindow, 20 | windowsXpTokens, 21 | } from "./WindowsXpCalendar"; 22 | import { LinearCalendar } from "./LinearCalendar"; 23 | 24 | const styles = StyleSheet.create({ 25 | windowsXpBackground: { 26 | backgroundColor: windowsXpTokens.colors.background, 27 | padding: 12, 28 | flex: 1, 29 | }, 30 | }); 31 | 32 | const CalendarMeta: Meta = { 33 | title: "Calendar/Themes", 34 | decorators: [], 35 | }; 36 | 37 | export default CalendarMeta; 38 | 39 | export const Linear = () => ; 40 | 41 | export const WindowsXP = () => { 42 | const [isPickerVisible, setIsPickerVisible] = useState(true); 43 | 44 | const [currentCalendarMonth, setCurrentCalendarMonth] = useState(new Date()); 45 | const [selectedDate, setSelectedDate] = useState( 46 | sub(new Date(), { days: 1 }) 47 | ); 48 | 49 | const handleOpenPicker = useCallback(() => { 50 | setIsPickerVisible((p) => !p); 51 | }, []); 52 | 53 | const handleDayPress = useCallback((dateId) => { 54 | setCurrentCalendarMonth(fromDateId(dateId)); 55 | setSelectedDate(fromDateId(dateId)); 56 | setIsPickerVisible(false); 57 | }, []); 58 | 59 | const calendarActiveDateRanges = useMemo( 60 | () => [ 61 | { 62 | startId: toDateId(selectedDate), 63 | endId: toDateId(selectedDate), 64 | }, 65 | ], 66 | [selectedDate] 67 | ); 68 | 69 | const handlePreviousMonth = useCallback(() => { 70 | setCurrentCalendarMonth(sub(currentCalendarMonth, { months: 1 })); 71 | }, [currentCalendarMonth]); 72 | 73 | const handleNextMonth = useCallback(() => { 74 | setCurrentCalendarMonth(add(currentCalendarMonth, { months: 1 })); 75 | }, [currentCalendarMonth]); 76 | 77 | return ( 78 | 79 | 80 | 81 | 82 | This is a Windows's XP themed calendar, using the composable API 83 | pattern to fully customize the calendar's appearance. 84 | 85 | 86 | 87 | {format("dd/MM/yyyy")(selectedDate)} 88 | 89 | 90 | {isPickerVisible && ( 91 | 101 | )} 102 | 103 | 104 | 105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /apps/example/src/components/ThemeableCalendar/LinearCalendar.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | CalendarActiveDateRange, 3 | CalendarTheme, 4 | } from "@marceloterreiro/flash-calendar"; 5 | import { Calendar, toDateId } from "@marceloterreiro/flash-calendar"; 6 | import { loggingHandler } from "@marceloterreiro/flash-calendar/src/developer/loggginHandler"; 7 | import { add, startOfMonth } from "date-fns"; 8 | import { format } from "date-fns/fp"; 9 | import { memo } from "react"; 10 | import { StyleSheet, View } from "react-native"; 11 | 12 | const today = new Date(); 13 | 14 | const startOfThisMonth = startOfMonth(today); 15 | 16 | const styles = StyleSheet.create({ 17 | linearContainer: { 18 | flex: 1, 19 | backgroundColor: "#252630", 20 | padding: 12, 21 | }, 22 | }); 23 | 24 | const linearAccent = "#585ABF"; 25 | 26 | const linearTheme: CalendarTheme = { 27 | rowMonth: { 28 | content: { 29 | textAlign: "left", 30 | color: "rgba(255, 255, 255, 0.5)", 31 | fontWeight: "700", 32 | }, 33 | }, 34 | rowWeek: { 35 | container: { 36 | borderBottomWidth: 1, 37 | borderBottomColor: "rgba(255, 255, 255, 0.1)", 38 | borderStyle: "solid", 39 | }, 40 | }, 41 | itemWeekName: { content: { color: "rgba(255, 255, 255, 0.5)" } }, 42 | itemDayContainer: { 43 | activeDayFiller: { 44 | backgroundColor: linearAccent, 45 | }, 46 | }, 47 | itemDay: { 48 | idle: ({ isPressed, isWeekend }) => ({ 49 | container: { 50 | backgroundColor: isPressed ? linearAccent : "transparent", 51 | borderRadius: 4, 52 | }, 53 | content: { 54 | color: isWeekend && !isPressed ? "rgba(255, 255, 255, 0.5)" : "#ffffff", 55 | }, 56 | }), 57 | today: ({ isPressed }) => ({ 58 | container: { 59 | borderColor: "rgba(255, 255, 255, 0.5)", 60 | borderRadius: isPressed ? 4 : 30, 61 | backgroundColor: isPressed ? linearAccent : "transparent", 62 | }, 63 | content: { 64 | color: isPressed ? "#ffffff" : "rgba(255, 255, 255, 0.5)", 65 | }, 66 | }), 67 | active: ({ isEndOfRange, isStartOfRange }) => ({ 68 | container: { 69 | backgroundColor: linearAccent, 70 | borderTopLeftRadius: isStartOfRange ? 4 : 0, 71 | borderBottomLeftRadius: isStartOfRange ? 4 : 0, 72 | borderTopRightRadius: isEndOfRange ? 4 : 0, 73 | borderBottomRightRadius: isEndOfRange ? 4 : 0, 74 | }, 75 | content: { 76 | color: "#ffffff", 77 | }, 78 | }), 79 | }, 80 | }; 81 | 82 | const calendarActiveDateRanges: CalendarActiveDateRange[] = [ 83 | { 84 | startId: toDateId(add(startOfThisMonth, { days: 3 })), 85 | endId: toDateId(add(startOfThisMonth, { days: 8 })), 86 | }, 87 | ]; 88 | 89 | export const LinearCalendar = memo(() => { 90 | return ( 91 | 92 | 103 | 104 | ); 105 | }); 106 | -------------------------------------------------------------------------------- /apps/example/src/components/ThemeableCalendar/WindowsXpCalendar/WindowXpButton.tsx: -------------------------------------------------------------------------------- 1 | import { darken, size } from "polished"; 2 | import { memo } from "react"; 3 | import { Pressable, StyleSheet, Text, View } from "react-native"; 4 | 5 | import { windowsXpTokens } from "./utils"; 6 | 7 | const styles = StyleSheet.create({ 8 | chevronContainer: { 9 | backgroundColor: windowsXpTokens.colors.button.secondaryBackground, 10 | alignItems: "center", 11 | justifyContent: "center", 12 | borderColor: windowsXpTokens.colors.accent, 13 | borderWidth: 1, 14 | borderStyle: "solid", 15 | }, 16 | chevronText: { 17 | color: windowsXpTokens.colors.button.content, 18 | textAlignVertical: "center", 19 | textAlign: "center", 20 | }, 21 | 22 | buttonContainer: { 23 | borderColor: windowsXpTokens.colors.accent, 24 | borderStyle: "solid", 25 | borderWidth: 1, 26 | backgroundColor: "white", 27 | padding: 2, 28 | gap: 4, 29 | alignItems: "center", 30 | justifyContent: "center", 31 | // FIXME: can't make this fit the content automatically 32 | width: 110, 33 | flexDirection: "row", 34 | }, 35 | buttonContent: { 36 | color: windowsXpTokens.colors.content.inverse.primary, 37 | backgroundColor: windowsXpTokens.colors.accent, 38 | fontStyle: "italic", 39 | }, 40 | 41 | chevronButtonContainer: { 42 | borderColor: windowsXpTokens.colors.accent, 43 | borderStyle: "solid", 44 | borderWidth: 1, 45 | backgroundColor: "white", 46 | alignItems: "center", 47 | justifyContent: "center", 48 | padding: 2, 49 | }, 50 | }); 51 | 52 | const BASE_CHEVRON = "▼"; 53 | 54 | const WindowsXPChevron = memo( 55 | ({ type }: { type: "left" | "bottom" | "top" | "right" }) => { 56 | return ( 57 | 69 | {BASE_CHEVRON} 70 | 71 | ); 72 | } 73 | ); 74 | 75 | export const WindowsXpButton = memo( 76 | ({ children, onPress }: { children: string; onPress: () => void }) => { 77 | return ( 78 | ({ 81 | ...styles.buttonContainer, 82 | backgroundColor: pressed 83 | ? darken(0.05, windowsXpTokens.colors.button.primaryBackground) 84 | : windowsXpTokens.colors.button.primaryBackground, 85 | })} 86 | > 87 | {children} 88 | 89 | 90 | 91 | 92 | ); 93 | } 94 | ); 95 | 96 | export const WindowsXpChevronButton = memo( 97 | ({ 98 | type, 99 | onPress, 100 | size: sizeProp, 101 | }: { 102 | type: "left" | "bottom" | "top" | "right"; 103 | size: number; 104 | onPress: () => void; 105 | }) => { 106 | return ( 107 | ({ 110 | ...styles.chevronButtonContainer, 111 | ...size(sizeProp), 112 | backgroundColor: pressed 113 | ? darken(0.1, windowsXpTokens.colors.button.primaryBackground) 114 | : windowsXpTokens.colors.button.primaryBackground, 115 | })} 116 | > 117 | 118 | 119 | 120 | 121 | ); 122 | } 123 | ); 124 | -------------------------------------------------------------------------------- /apps/example/src/components/ThemeableCalendar/WindowsXpCalendar/WindowXpCalendar.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | CalendarProps, 3 | CalendarTheme, 4 | } from "@marceloterreiro/flash-calendar"; 5 | import { Calendar, useCalendar } from "@marceloterreiro/flash-calendar"; 6 | import { format } from "date-fns"; 7 | import { memo, useMemo } from "react"; 8 | import { StyleSheet, Text, View } from "react-native"; 9 | 10 | import { WindowsXpChevronButton } from "./WindowXpButton"; 11 | import { windowsXpTokens } from "./utils"; 12 | 13 | const DAY_HEIGHT = 25; 14 | const MONTH_HEADER_HEIGHT = 40; 15 | const WEEK_DAYS_HEIGHT = 25; 16 | const FOOTER_HEIGHT = 30; 17 | 18 | const BORDER_WIDTH = 1; 19 | 20 | const styles = StyleSheet.create({ 21 | weekDivider: { 22 | height: 1, 23 | backgroundColor: windowsXpTokens.colors.content.primary, 24 | position: "absolute", 25 | left: 8, 26 | right: 8, 27 | bottom: 0, 28 | }, 29 | calendarContainer: { 30 | backgroundColor: "white", 31 | borderStyle: "solid", 32 | borderWidth: BORDER_WIDTH, 33 | borderColor: windowsXpTokens.colors.accent, 34 | }, 35 | calendarFooter: { 36 | height: FOOTER_HEIGHT, 37 | flexDirection: "row", 38 | justifyContent: "flex-start", 39 | alignItems: "center", 40 | gap: 4, 41 | paddingHorizontal: 8, 42 | }, 43 | calendarFooterLegend: { 44 | width: 20, 45 | height: 20, 46 | borderColor: windowsXpTokens.colors.secondary, 47 | borderWidth: 2, 48 | }, 49 | calendarFooterText: { 50 | fontWeight: "bold", 51 | fontStyle: "italic", 52 | }, 53 | }); 54 | 55 | const calendarTheme: CalendarTheme = { 56 | rowMonth: { 57 | container: { 58 | backgroundColor: windowsXpTokens.colors.accent, 59 | height: MONTH_HEADER_HEIGHT, 60 | }, 61 | content: { 62 | color: windowsXpTokens.colors.content.inverse.primary, 63 | fontSize: 17, 64 | width: 200, 65 | textAlign: "center", 66 | }, 67 | }, 68 | itemWeekName: { content: { color: windowsXpTokens.colors.accent } }, 69 | itemDay: { 70 | base: () => ({ 71 | container: { 72 | padding: 0, 73 | borderRadius: 0, 74 | }, 75 | }), 76 | today: () => ({ 77 | container: { 78 | borderWidth: 2, 79 | borderColor: windowsXpTokens.colors.secondary, 80 | }, 81 | }), 82 | idle: ({ isDifferentMonth }) => ({ 83 | content: isDifferentMonth 84 | ? { 85 | color: windowsXpTokens.colors.content.disabled, 86 | } 87 | : undefined, 88 | }), 89 | active: () => ({ 90 | container: { 91 | backgroundColor: windowsXpTokens.colors.accent, 92 | borderTopLeftRadius: 0, 93 | borderTopRightRadius: 0, 94 | borderBottomLeftRadius: 0, 95 | borderBottomRightRadius: 0, 96 | }, 97 | content: { 98 | color: windowsXpTokens.colors.content.inverse.primary, 99 | }, 100 | }), 101 | }, 102 | }; 103 | 104 | interface WindowsXpCalendarProps extends CalendarProps { 105 | onPreviousMonthPress: () => void; 106 | onNextMonthPress: () => void; 107 | } 108 | export const WindowsXpCalendar = memo((props: WindowsXpCalendarProps) => { 109 | const { calendarRowMonth, weekDaysList, weeksList } = useCalendar(props); 110 | 111 | const today = useMemo(() => { 112 | return weeksList.flatMap((week) => week).find((day) => day.isToday); 113 | }, [weeksList]); 114 | 115 | return ( 116 | 117 | 118 | {/* Replaces `Calendar.Row.Month` with a custom implementation */} 119 | 125 | 130 | 131 | {calendarRowMonth} 132 | 133 | 138 | 139 | 140 | 141 | {weekDaysList.map((day, i) => ( 142 | 147 | {day} 148 | 149 | ))} 150 | 151 | 152 | 153 | {weeksList.map((week, i) => ( 154 | 155 | {week.map((day) => ( 156 | 162 | 168 | {day.displayLabel} 169 | 170 | 171 | ))} 172 | 173 | ))} 174 | 175 | 176 | 177 | 178 | Today: {format(today?.date ?? new Date(), "dd/MM/yyyy")} 179 | 180 | 181 | 182 | 183 | ); 184 | }); 185 | -------------------------------------------------------------------------------- /apps/example/src/components/ThemeableCalendar/WindowsXpCalendar/WindowsXpText.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | import type { TextStyle } from "react-native"; 3 | import { Text, StyleSheet } from "react-native"; 4 | 5 | export const textStyles = StyleSheet.create({ 6 | windowsXpText: { 7 | color: "#ffffff", 8 | width: "100%", 9 | textShadowColor: "rgba(0, 0, 0, 1)", 10 | textShadowOffset: { width: 1, height: 1 }, 11 | textShadowRadius: 1, 12 | }, 13 | }); 14 | 15 | export const WindowsXpText = memo( 16 | ({ children, style }: { children: string; style?: TextStyle }) => { 17 | return {children}; 18 | } 19 | ); 20 | -------------------------------------------------------------------------------- /apps/example/src/components/ThemeableCalendar/WindowsXpCalendar/WindowsXpWindow.tsx: -------------------------------------------------------------------------------- 1 | import { View, StyleSheet } from "react-native"; 2 | 3 | import { WindowsXpText } from "./WindowsXpText"; 4 | import { windowsXpTokens } from "./utils"; 5 | 6 | const styles = StyleSheet.create({ 7 | windowContent: { 8 | backgroundColor: windowsXpTokens.colors.background, 9 | borderWidth: 2, 10 | borderColor: windowsXpTokens.colors.accent, 11 | borderStyle: "solid", 12 | borderBottomLeftRadius: 8, 13 | borderBottomRightRadius: 8, 14 | padding: 12, 15 | }, 16 | 17 | windowHeaderContainer: { 18 | paddingVertical: 8, 19 | paddingHorizontal: 8, 20 | backgroundColor: windowsXpTokens.colors.accent, 21 | alignItems: "center", 22 | justifyContent: "center", 23 | borderTopLeftRadius: 8, 24 | borderTopRightRadius: 8, 25 | }, 26 | windowsXpText: { 27 | color: "#ffffff", 28 | width: "100%", 29 | textShadowColor: "rgba(0, 0, 0, 1)", 30 | textShadowOffset: { width: 1, height: 1 }, 31 | textShadowRadius: 1, 32 | }, 33 | 34 | weekDivider: { 35 | height: 2, 36 | backgroundColor: "#000000", 37 | position: "absolute", 38 | left: 4, 39 | right: 4, 40 | bottom: 0, 41 | }, 42 | }); 43 | 44 | export const WindowsXpWindowHeader = ({ children }: { children: string }) => { 45 | return ( 46 | 47 | {children} 48 | 49 | ); 50 | }; 51 | 52 | export const WindowsXpWindow = ({ 53 | children, 54 | title, 55 | }: { 56 | children: React.ReactNode; 57 | title: string; 58 | }) => { 59 | return ( 60 | 61 | {title} 62 | {children} 63 | 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /apps/example/src/components/ThemeableCalendar/WindowsXpCalendar/index.ts: -------------------------------------------------------------------------------- 1 | export { WindowsXpButton } from "./WindowXpButton"; 2 | export { WindowsXpCalendar } from "./WindowXpCalendar"; 3 | export { WindowsXpWindow } from "./WindowsXpWindow"; 4 | export { windowsXpTokens } from "./utils"; 5 | -------------------------------------------------------------------------------- /apps/example/src/components/ThemeableCalendar/WindowsXpCalendar/utils.ts: -------------------------------------------------------------------------------- 1 | export const windowsXpTokens = { 2 | colors: { 3 | background: "#ECE9DA", 4 | accent: "#2559DF", 5 | secondary: "#EA3424", 6 | 7 | content: { 8 | primary: "#000000", 9 | disabled: "rgba(0, 0, 0, .3)", 10 | inverse: { 11 | primary: "#ffffff", 12 | }, 13 | }, 14 | button: { 15 | primaryBackground: "#ffffff", 16 | secondaryBackground: "#D1DDF9", 17 | content: "#6D7B9E", 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /apps/example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@marceloterreiro/tsconfig/base", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@marceloterreiro/flash-calendar": ["../../packages/flash-calendar/src"], 7 | "@/*": ["../../packages/flash-calendar/src/*"] 8 | }, 9 | "esModuleInterop": true 10 | }, 11 | 12 | "include": ["src", ".storybook"], 13 | "exclude": ["node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/bun.lockb -------------------------------------------------------------------------------- /kitchen-sink/expo/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ["@marceloterreiro/eslint-config"], 5 | }; 6 | -------------------------------------------------------------------------------- /kitchen-sink/expo/.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /kitchen-sink/expo/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @marceloterreiro/kitchen-sink-expo 2 | 3 | ## null 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [cbc7728] 8 | - @marceloterreiro/flash-calendar@0.0.5 9 | 10 | ## null 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [801bc18] 15 | - @marceloterreiro/flash-calendar@0.0.4 16 | 17 | ## null 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [e680a96] 22 | - @marceloterreiro/flash-calendar@0.0.3 23 | -------------------------------------------------------------------------------- /kitchen-sink/expo/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "expo-app", 4 | "slug": "expo-app", 5 | "jsEngine": "hermes", 6 | "version": "1.0.0", 7 | "orientation": "portrait", 8 | "icon": "./assets/icon.png", 9 | "userInterfaceStyle": "automatic", 10 | "splash": { 11 | "image": "./assets/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "assetBundlePatterns": ["**/*"], 16 | "ios": { 17 | "supportsTablet": true 18 | }, 19 | "android": { 20 | "adaptiveIcon": { 21 | "foregroundImage": "./assets/adaptive-icon.png", 22 | "backgroundColor": "#ffffff" 23 | } 24 | }, 25 | "web": { 26 | "favicon": "./assets/favicon.png" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /kitchen-sink/expo/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/kitchen-sink/expo/assets/adaptive-icon.png -------------------------------------------------------------------------------- /kitchen-sink/expo/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/kitchen-sink/expo/assets/favicon.png -------------------------------------------------------------------------------- /kitchen-sink/expo/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/kitchen-sink/expo/assets/icon.png -------------------------------------------------------------------------------- /kitchen-sink/expo/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceloPrado/flash-calendar/a6d8c03e70b0ac40e28e8c3f95cff37b100ecc4f/kitchen-sink/expo/assets/splash.png -------------------------------------------------------------------------------- /kitchen-sink/expo/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /kitchen-sink/expo/index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from "expo"; 2 | 3 | import App from "./src/App"; 4 | 5 | registerRootComponent(App); 6 | -------------------------------------------------------------------------------- /kitchen-sink/expo/metro.config.js: -------------------------------------------------------------------------------- 1 | // Learn more https://docs.expo.io/guides/customizing-metro 2 | const { getDefaultConfig } = require('expo/metro-config'); 3 | 4 | /** @type {import('expo/metro-config').MetroConfig} */ 5 | const config = getDefaultConfig(__dirname); 6 | 7 | module.exports = config; 8 | -------------------------------------------------------------------------------- /kitchen-sink/expo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@marceloterreiro/kitchen-sink-expo", 3 | "private": true, 4 | "main": "./index.js", 5 | "scripts": { 6 | "analyze:android": "source-map-explorer 'dist/_expo/static/js/android/*.js' 'dist/_expo/static/js/android/*.js.map'", 7 | "analyze:ios": "source-map-explorer 'dist/_expo/static/js/ios/*.js' 'dist/_expo/static/js/ios/*.js.map'", 8 | "android": "expo start --android", 9 | "build:android": "bunx expo export --source-maps --platform android", 10 | "build:ios": "bunx expo export --source-maps --platform ios", 11 | "clean": "rm -rf dist && rm -rf node_modules", 12 | "ios": "expo start --ios", 13 | "lint": "eslint src", 14 | "start": "expo start", 15 | "typecheck": "tsc --noEmit", 16 | "web": "expo start --web" 17 | }, 18 | "dependencies": { 19 | "@gorhom/bottom-sheet": "^4", 20 | "@marceloterreiro/flash-calendar": "*", 21 | "@shopify/flash-list": "^1.6.3", 22 | "expo": "~50.0.7", 23 | "expo-status-bar": "~1.11.1", 24 | "react": "18.2.0", 25 | "react-native": "0.73.4", 26 | "react-native-gesture-handler": "~2.14.0", 27 | "react-native-reanimated": "~3.6.2" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.20.0", 31 | "@marceloterreiro/eslint-config": "*", 32 | "@marceloterreiro/tsconfig": "*", 33 | "@types/react": "~18.2.45", 34 | "eslint": "^8.56.0", 35 | "source-map-explorer": "^2.5.3", 36 | "typescript": "^5.3.3" 37 | }, 38 | "version": null 39 | } 40 | -------------------------------------------------------------------------------- /kitchen-sink/expo/src/App.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { GestureHandlerRootView } from "react-native-gesture-handler"; 3 | import { StatusBar } from "expo-status-bar"; 4 | import { useState } from "react"; 5 | import { SafeAreaView, StyleSheet } from "react-native"; 6 | 7 | import { CalendarDemo } from "./Calendar"; 8 | import { CalendarListDemo } from "./CalendarList"; 9 | import { BottomSheetCalendar } from "./BottomSheetCalendar"; 10 | import { CalendarCustomFormatting } from "./CalendarCustomFormatting"; 11 | import { ImperativeScrolling } from "./ImperativeScroll"; 12 | // import { SlowExampleAddressed } from "./SlowExampleAddressed"; 13 | 14 | export default function App() { 15 | const [demo, setDemo] = useState<"calendar" | "calendarList">("calendar"); 16 | 17 | return ( 18 | 19 | 20 | 21 | {/* 22 | 23 | 24 | Demo: {demo === "calendar" ? "Calendar" : "Calendar List"} 25 | 26 |