[
  {
    "path": ".eslintignore",
    "content": "build/\ndist/\nnode_modules/\nperf/\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "const defaultExtends = [\n  'tui',\n  'prettier',\n  'plugin:@typescript-eslint/recommended',\n  'plugin:react/recommended',\n  'plugin:prettier/recommended',\n];\n\nmodule.exports = {\n  root: true,\n  env: {\n    browser: true,\n    es6: true,\n    node: true,\n  },\n  parser: '@typescript-eslint/parser',\n  parserOptions: {\n    parser: 'typescript-eslint-parser',\n    ecmaVersion: 2018,\n    sourceType: 'module',\n  },\n  plugins: [\n    'unused-imports',\n    'simple-import-sort',\n    'prettier',\n    'react',\n    'react-hooks',\n    '@typescript-eslint',\n    'jest',\n  ],\n  extends: defaultExtends,\n  settings: {\n    react: {\n      pragma: 'h',\n      version: '16.13',\n    },\n  },\n  globals: {\n    fixture: true,\n  },\n  ignorePatterns: ['**/*.d.ts'],\n  rules: {\n    '@typescript-eslint/explicit-module-boundary-types': 0,\n    '@typescript-eslint/ban-types': 0,\n    '@typescript-eslint/no-use-before-define': 0,\n    '@typescript-eslint/explicit-function-return-type': 0,\n    '@typescript-eslint/no-explicit-any': 0,\n    '@typescript-eslint/no-shadow': 'error',\n    '@typescript-eslint/consistent-type-imports': 'error',\n    'no-duplicate-imports': 'off',\n    '@typescript-eslint/no-duplicate-imports': 'error',\n    'no-shadow': 'off',\n    'no-use-before-define': 0,\n    // use unused-imports plugin\n    '@typescript-eslint/no-unused-vars': 'off',\n    'unused-imports/no-unused-imports': 'error',\n    'unused-imports/no-unused-vars': [\n      'warn',\n      { args: 'after-used', argsIgnorePattern: '^_', ignoreRestSiblings: true },\n    ],\n    'react/prop-types': 0,\n    'react-hooks/rules-of-hooks': 'error',\n    'react-hooks/exhaustive-deps': 'error',\n    'jest/no-conditional-expect': 0,\n    'simple-import-sort/imports': [\n      'warn',\n      {\n        groups: [\n          // Side effect imports.\n          ['^\\\\u0000'],\n          // Preact.\n          ['^preact'],\n          // Any other packages.\n          ['^(\\\\w|@)(?!src/|test/|stories/|t/)'],\n          // Source files.\n          ['^@src/'],\n          // stories files\n          ['^@stories/'],\n          // Types.\n          ['^@t/'],\n          // Absolute imports and other imports such as Vue-style `@/foo`.\n          // Anything not matched in another group.\n          ['^'],\n          // Relative imports.\n          // Anything that starts with a dot.\n          ['^\\\\.'],\n        ],\n      },\n    ],\n    'simple-import-sort/exports': 'warn',\n    complexity: ['error', { max: 8 }],\n    'no-warning-comments': 0,\n  },\n  overrides: [\n    {\n      files: ['*.spec.ts', '*.spec.tsx'],\n      extends: ['plugin:jest/recommended'],\n      rules: {\n        'max-nested-callbacks': ['error', { max: 5 }],\n        'jest/expect-expect': [\n          'warn',\n          {\n            assertFunctionNames: ['expect', 'assert*'],\n          },\n        ],\n        'jest/no-conditional-expect': 'warn',\n      },\n    },\n    {\n      files: ['apps/calendar/playwright/**/*.ts'],\n      extends: ['plugin:playwright/playwright-test'],\n      rules: {\n        'playwright/no-force-option': 'off',\n        'max-nested-callbacks': ['error', { max: 5 }],\n        'dot-notation': ['error', { allowKeywords: true }],\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": ".github/composite-actions/install-dependencies/action.yml",
    "content": "name: 'Install root dependencies using cache'\ndescription: 'Set Node.js and install dependencies using cache'\nruns:\n  using: 'composite'\n  steps:\n    - name: Setup timezone\n      uses: szenius/set-timezone@v1.0\n      with:\n        timezoneLinux: 'Asia/Seoul'\n    - name: Use Node.js 16\n      uses: actions/setup-node@v3\n      with:\n        node-version: '16'\n        registry-url: https://registry.npmjs.org/\n        cache: 'npm'\n    - name: Install dependencies\n      run: npm ci\n      shell: bash\n"
  },
  {
    "path": ".github/workflows/publish-calendar.yml",
    "content": "name: Publish calendar\non: [workflow_dispatch]\nenv:\n  WORKING_DIRECTORY: ./apps/calendar\njobs:\n  check-version:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout branch\n        uses: actions/checkout@v3\n      - name: Check the package version\n        id: cpv\n        uses: PostHog/check-package-version@v2\n        with:\n          path: ${{ env.WORKING_DIRECTORY }}\n      - name: Log when unchanged\n        if: steps.cpv.outputs.is-new-version == 'false'\n        run: 'echo \"No version change\"'\n      - name: Cancel workflow\n        if: steps.cpv.outputs.is-new-version == 'false'\n        uses: andymckay/cancel-action@0.2\n\n  publish-npm:\n    needs: check-version\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - name: Install dependencies\n        uses: ./.github/composite-actions/install-dependencies\n      - name: Build calendar\n        run: |\n          npm run build:calendar\n      - name: Check package version\n        id: cpv\n        uses: PostHog/check-package-version@v2\n        with:\n          path: ${{ env.WORKING_DIRECTORY }}\n      - name: Create new version tag\n        run: |\n          git tag calendar@${{ steps.cpv.outputs.committed-version }}\n      - name: Push new version tag\n        run: |\n          git push origin calendar@${{ steps.cpv.outputs.committed-version }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      - name: Update the relative links in README\n        run: |\n          PACKAGES=core npm run update:readme\n      - name: Publish the package to npm (Calendar)\n        working-directory: ${{ env.WORKING_DIRECTORY }}\n        run: |\n          npm publish --access public\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n\n  publish-cdn:\n    needs: publish-npm\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout branch\n        uses: actions/checkout@v3\n      - uses: ./.github/composite-actions/install-dependencies\n      - name: Upload files to CDN\n        working-directory: ${{ env.WORKING_DIRECTORY }}\n        run: |\n          npm run build\n          npm run publish:cdn\n        env:\n          TOAST_CLOUD_TENANTID: ${{ secrets.TOAST_CLOUD_TENANTID }}\n          TOAST_CLOUD_STORAGEID: ${{ secrets.TOAST_CLOUD_STORAGEID }}\n          TOAST_CLOUD_USERNAME: ${{ secrets.TOAST_CLOUD_USERNAME }}\n          TOAST_CLOUD_PASSWORD: ${{ secrets.TOAST_CLOUD_PASSWORD }}\n"
  },
  {
    "path": ".github/workflows/publish-docs.yml",
    "content": "name: Publish docs\non: [workflow_dispatch]\nenv:\n  WORKING_DIRECTORY: ./apps/calendar\njobs:\n  publish-docs:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n      - name: Get the package version\n        id: cpv\n        uses: PostHog/check-package-version@v2\n        with:\n          path: ${{ env.WORKING_DIRECTORY }}\n      - name: Install dependencies\n        uses: ./.github/composite-actions/install-dependencies\n      - name: Update the relative links in README\n        run: |\n          PACKAGES=core npm run update:readme\n      - name: Prebuild docs\n        working-directory: ${{ env.WORKING_DIRECTORY }}\n        run: |\n          npm run docs:prebuild\n      - name: Change node version to 10\n        uses: actions/setup-node@v3\n        with:\n          node-version: '10'\n          registry-url: https://registry.npmjs.org/\n      - name: Install tuidoc\n        run: |\n          npm install -g @toast-ui/doc\n        shell: bash\n      - name: Build docs\n        working-directory: ${{ env.WORKING_DIRECTORY }}\n        run: |\n          tuidoc\n        shell: bash\n      - name: Switch branch\n        run: |\n          git stash\n          git switch gh-pages\n      - name: Remove previous docs\n        run: |\n          rm -rf latest\n          rm -rf ${{ steps.cpv.outputs.committed-version }}\n      - name: Commit docs\n        working-directory: ${{ env.WORKING_DIRECTORY }}\n        run: |\n          git config --local user.email \"dohyung.ahn@nhn.com\"\n          git config --local user.name \"Dohyung Ahn\"\n          mv -i _latest ../../latest\n          mv _${{ steps.cpv.outputs.committed-version }} ../../${{ steps.cpv.outputs.committed-version }}\n          git add -A\n          git commit -m ${{ steps.cpv.outputs.committed-version }}\n      - name: Publish new docs\n        uses: ad-m/github-push-action@master\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          branch: gh-pages\n      - name: Back to main branch\n        run: |\n          git switch main\n      - name: Restore Node.js version to 16\n        uses: actions/setup-node@v3\n        with:\n          node-version: '16'\n          registry-url: https://registry.npmjs.org/\n          cache: 'npm'\n"
  },
  {
    "path": ".github/workflows/publish-wrappers.yml",
    "content": "name: Publish wrapper\non: [workflow_dispatch]\n\nenv:\n  WORKING_DIRECTORY: ./apps/calendar\n  REACT_WRAPPER_DIRECTORY: ./apps/react-calendar\n  VUE_WRAPPER_DIRECTORY: ./apps/vue-calendar\n\njobs:\n  publish-wrapper:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout branch\n        uses: actions/checkout@v3\n      - name: Install root dependencies\n        uses: ./.github/composite-actions/install-dependencies\n      - name: Use Node.js 16.x\n        uses: actions/setup-node@v1\n        with:\n          node-version: '16.x'\n          registry-url: https://registry.npmjs.org/\n      - name: Build core\n        working-directory: ${{ env.WORKING_DIRECTORY }}\n        run: |\n          npm run build\n      - name: Get the package version\n        id: version\n        uses: PostHog/check-package-version@v2\n        with:\n          path: ${{ env.WORKING_DIRECTORY }}/\n      - name: Update version of wrappers in package.json\n        working-directory: ${{ env.WORKING_DIRECTORY }}\n        run: |\n          npm run update:wrapper\n      - name: Update lock file of react wrapper\n        working-directory: ${{ env.REACT_WRAPPER_DIRECTORY }}\n        run: |\n          npm install\n      - name: Build react wrapper\n        working-directory: ${{ env.REACT_WRAPPER_DIRECTORY }}\n        run: |\n          npm run build\n      - name: Update lock file of Vue wrapper\n        working-directory: ${{ env.VUE_WRAPPER_DIRECTORY }}\n        run: |\n          npm install\n      - name: Build vue wrapper\n        working-directory: ${{ env.VUE_WRAPPER_DIRECTORY }}\n        run: |\n          npm run build\n      - name: Commit files\n        run: |\n          rm -rf ./apps/react-calendar/package-lock.json\n          rm -rf ./apps/vue-calendar/package-lock.json\n          git config --local user.name \"lja1018\"\n          git config --local user.email \"jaeeon.lim@nhn.com\"\n          git add .\n          git commit -m \"chore: update version of wrappers to v${{ steps.version.outputs.committed-version }}\"\n      - name: Push version update commit\n        uses: ad-m/github-push-action@master\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          branch: main\n      - name: Push new version tags\n        run: |\n          git tag react-calendar@${{ steps.version.outputs.committed-version }}\n          git tag vue-calendar@${{ steps.version.outputs.committed-version }}\n          git push origin react-calendar@${{ steps.version.outputs.committed-version }}\n          git push origin vue-calendar@${{ steps.version.outputs.committed-version }}\n      - name: Update the relative links in README\n        run: |\n          PACKAGES=react,vue npm run update:readme\n      - name: Publish react wrapper\n        working-directory: ${{ env.REACT_WRAPPER_DIRECTORY }}\n        run: |\n          npm publish\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n      - name: Publish vue wrapper\n        working-directory: ${{ env.VUE_WRAPPER_DIRECTORY }}\n        run: |\n          npm publish\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Lint and test\non:\n  pull_request:\n    branches: [main]\n  workflow_call:\njobs:\n  lint:\n    name: Lint & Type Checking\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout next branch\n        uses: actions/checkout@v3\n      - name: Install dependencies\n        uses: ./.github/composite-actions/install-dependencies\n      - name: Lint & Type Checking\n        run: |\n          npm run lint\n\n  test-jest:\n    name: Test jest\n    needs: [lint]\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout next branch\n        uses: actions/checkout@v3\n      - name: Install dependencies\n        uses: ./.github/composite-actions/install-dependencies\n      - name: run unit test\n        run: |\n          npm run test\n\n  test-playwright:\n    name: Test playwright\n    needs: [lint]\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout next branch\n        uses: actions/checkout@v3\n      - name: Install dependencies\n        uses: ./.github/composite-actions/install-dependencies\n      - name: Install playwright dependencies\n        run: |\n          npx playwright install --with-deps\n      - name: Build storybook\n        run: |\n          npm run storybook:build -w \"@toast-ui/calendar\"\n      - name: Run E2E test\n        run: |\n         npm run test:playwright\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\nscreenshots\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directory\nnode_modules\n\n# Bower Components\nbower_components\nlib\n\n# IDEA\n.idea\n*.iml\n\n# Window\nThumbs.db\nDesktop.ini\n\n# MAC\n.DS_Store\n\n# SVN\n.svn\n\n# eclipse\n.project\n.metadata\n\n# build\nbuild/\nstorybook-static/\ndist/\napps/calendar/types/\ntmpdoc/\n\n# etc\n*.swp\netc\ntemp\napi\ndoc\nreport\nkarma.conf.local.js\n.tern-project\n.tern-port\n*.vim\n.\\#*\n.vscode/\ntest-results/\nstats.json\n"
  },
  {
    "path": ".husky/.gitignore",
    "content": "_\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpx lint-staged\n"
  },
  {
    "path": ".prettierignore",
    "content": "*.md\n*.html\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"singleQuote\": true,\n  \"printWidth\": 100,\n  \"tabWidth\": 2,\n  \"useTabs\": false,\n  \"semi\": true,\n  \"quoteProps\": \"as-needed\",\n  \"jsxSingleQuote\": false,\n  \"trailingComma\": \"es5\",\n  \"arrowParens\": \"always\",\n  \"endOfLine\": \"lf\",\n  \"bracketSpacing\": true,\n  \"requirePragma\": false,\n  \"insertPragma\": false,\n  \"proseWrap\": \"preserve\",\n  \"vueIndentScriptAndStyle\": false\n}\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, gender identity and expression, level of experience,\neducation, socio-economic status, nationality, personal appearance, race,\nreligion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n  advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at dl_javascript@nhn.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to TOAST UI\n\nFirst off, thanks for taking the time to contribute! 🎉 😘 ✨\n\nThe following is a set of guidelines for contributing to TOAST UI. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.\n\n## Reporting Bugs\n\nBugs are tracked as [GitHub issues](https://github.com/nhn/tui.calendar/issues). Search the issue list and try to reproduce on [demo](https://nhn.github.io/tui.calendar/latest/tutorial-00-calendar-app) before you create an issue. When you create an issue, please provide the following information by filling in the template.\n\nExplain the problem and include additional details to help maintainers reproduce the problem:\n\n* **Use a clear and descriptive title** for the issue to identify the problem.\n* **Describe the exact steps which reproduce the problem** in as many details as possible. Don't just say what you did, but explain how you did it. For example, if you moved the cursor to the end of a line, explain if you used a mouse or a keyboard.\n* **Provide specific examples to demonstrate the steps.** Include links to files or GitHub projects, or copy/paste-able snippets, which you use in those examples. If you're providing snippets on the issue, use Markdown code blocks.\n* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.\n* **Explain which behavior you expected to see instead and why.**\n* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem.\n\n## Suggesting Enhancements\n\nIn case you want to suggest for TOAST UI Calendar, please follow this guideline to help maintainers and the community understand your suggestion.\nBefore creating suggestions, please check [issue list](https://github.com/nhn/tui.calendar/labels/Type:%20Enhancement) if there's already a request.\n\nCreate an issue and provide the following information:\n\n* **Use a clear and descriptive title** for the issue to identify the suggestion.\n* **Provide a step-by-step description of the suggested enhancement** in as many details as possible.\n* **Provide specific examples to demonstrate the steps.** Include copy/paste-able snippets which you use in those examples, as Markdown code blocks.\n* **Include screenshots and animated GIFs** which helps demonstrate the steps or point out the part of TOAST UI Calendar which the suggestion is related to.\n* **Explain why this enhancement would be useful** to most TOAST UI users.\n* **List some other applications where this enhancement exists.**\n\n## First Code Contribution\n\nUnsure where to begin contributing to TOAST UI? You can start by looking through these `document`, `good first issue` and `help wanted` issues:\n\n* **document issues**: issues which should be reviewed or improved.\n* **good first issues**: issues which should only require a few lines of code, and a test or two.\n* **help wanted issues**: issues which should be a bit more involved than beginner issues.\n\n## Pull Requests\n\n### Development WorkFlow\n\n- Set up your development environment\n- Make change from a right branch\n- Be sure the code passes `npm run lint`, `npm run test`, `npm run test:playwright`\n- Make a pull request\n\n### Development environment\n\n- Prepare your machine Node.js 16+ and it's packages installed.\n- Checkout to the right branch\n- Install dependencies by `npm install`\n- Start development by `npm run develop`\n  - For wrappers, `npm run develop --workspace @toast-ui/react-calendar` or `npm run develop --workspace @toast-ui/vue-calendar`\n\n### Make changes\n\n#### Checkout a branch\n\n- **main**: PR base branch. merge features, updates for next minor or major release.\n- **v1**: Legacy version of the project.\n- **gh-pages**: API docs, examples and demo\n\n#### Check Code Style\n\nRun `npm run lint` and make sure all the tests pass.  \n\n#### Test\n\nRun `npm run test` and verify all the tests pass.\nIf you are adding new commands or features, they must include tests.\nIf you are changing functionality, update the tests if you need to.\n\n#### Commit\n\nFollow our [commit message conventions](./docs/COMMIT_MESSAGE_CONVENTION.md).\n\n### Yes! Pull request\n\nMake your pull request, then describe your changes.\n\n#### Title\n\nFollow other PR title format on below.\n\n```\n    <Type>: Short Description (fix #111)\n    <Type>: Short Description (fix #123, #111, #122)\n    <Type>: Short Description (ref #111)\n```\n\n* capitalize first letter of Type\n* use present tense: 'change' not 'changed' or 'changes'\n\n#### Description\n\nIf it has related to issues, add links to the issues(like `#123`) in the description.\nFill in the [Pull Request Template](./docs/PULL_REQUEST_TEMPLATE.md) by check your case.\n\n## Code of Conduct\n\nThis project and everyone participating in it is governed by the [Code of Conduct](./CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to dl_javascript@github.com.\n\n> This Guide is base on [atom contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md), [CocoaPods](http://guides.cocoapods.org/contributing/contribute-to-cocoapods.html) and [ESLint](http://eslint.org/docs/developer-guide/contributing/pull-requests)\n\n[demo]:https://nhn.github.io/tui.calendar/latest\n"
  },
  {
    "path": "LICENSE",
    "content": "\nMIT License\n\nCopyright (c) 2021 NHN Corp.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# ![TOAST UI Calendar](https://user-images.githubusercontent.com/26706716/39230183-7f8ff186-48a0-11e8-8d9c-9699d2d0e471.png)\n\n> 🍞📅 A JavaScript calendar that is full featured. Now your service just got the customizable calendar.\n\n[![npm](https://img.shields.io/npm/v/@toast-ui/calendar.svg)](https://www.npmjs.com/package/@toast-ui/calendar)\n[![GitHub license](https://img.shields.io/github/license/nhn/tui.calendar.svg)](https://github.com/nhn/tui.calendar/blob/main/LICENSE)\n[![PRs welcome](https://img.shields.io/badge/PRs-welcome-ff69b4.svg)](https://github.com/nhn/tui.calendar/labels/help%20wanted)\n[![code with hearth by NHN Cloud](https://img.shields.io/badge/%3C%2F%3E%20with%20%E2%99%A5%20by-NHN_Cloud-ff1414.svg)](https://github.com/nhn)\n\n## 🚩 Table of Contents\n\n- [📦 Packages](#-packages)\n- [📙 Documents](#-documents)\n- [Collect statistics on the use of open source](#collect-statistics-on-the-use-of-open-source)\n- [📅 Features](#-features)\n  - [✨ Monthly, Weekly, Daily and Various View Types](#-monthly-weekly-daily-and-various-view-types)\n  - [Easy to Use: Dragging and Resizing a Schedule](#easy-to-use-dragging-and-resizing-a-schedule)\n  - [Ready to Use: Default Popups](#ready-to-use-default-popups)\n- [🎨 Other Features](#-other-features)\n- [💬 Contributing](#-contributing)\n- [🌏 Browser Support](#-browser-support)\n- [🔩 Dependencies](#-dependencies)\n- [🍞 TOAST UI Family](#-toast-ui-family)\n- [🚀 Used By](#-used-by)\n- [📜 License](#-license)\n\n## 📦 Packages\n\nThe functionality of TOAST UI Calendar is available when using the Plain JavaScript, React, Vue Component.\n\n- [@toast-ui/calendar](/apps/calendar) - Plain JavaScript component implemented by [NHN Cloud](https://github.com/nhn).\n- [@toast-ui/react-calendar](/apps/react-calendar) - React wrapper component implemented by [NHN Cloud](https://github.com/nhn).\n- [@toast-ui/vue-calendar](/apps/vue-calendar) - Vue wrapper component implemented by [NHN Cloud](https://github.com/nhn).\n\n## 📙 Documents\n\n- [English](./docs/README.md)\n- [Korean](./docs/ko/README.md)\n\n## Collect statistics on the use of open source\n\nTOAST UI Calendar applies Google Analytics (GA) to collect statistics on the use of open source, in order to identify how widely TOAST UI Calendar is used throughout the world. It also serves as important index to determine the future course of projects. location.hostname (e.g. > “ui.toast.com\") is to be collected and the sole purpose is nothing but to measure statistics on the usage.\n\nTo disable GA, refer to the docs below.\n\n- [TOAST UI Calendar](/docs/en/guide/getting-started.md#disable-to-collect-hostname-for-google-analyticsga)\n- [TOAST UI Calendar for React](/apps/react-calendar/docs/en/guide/getting-started.md#disable-to-collect-hostname-for-google-analyticsga)\n- [TOAST UI Calendar for Vue](/apps/vue-calendar/docs/en/guide/getting-started.md#disable-to-collect-hostname-for-google-analyticsga)\n\n## 📅 Features\n\n### ✨ Monthly, Weekly, Daily and Various View Types\n\n| Monthly | Weekly |\n| --- | --- |\n| ![image](https://user-images.githubusercontent.com/26706716/39230396-4d79a592-48a1-11e8-9849-08e80f1bedf6.png) | ![image](https://user-images.githubusercontent.com/26706716/39230459-83beac38-48a1-11e8-8cd4-11b97817f1f8.png) |\n\n| Daily | 2 Weeks |\n| --- | --- |\n| ![image](https://user-images.githubusercontent.com/26706716/39230685-60a2a1d6-48a2-11e8-9d46-ce5693277a64.png) | ![image](https://user-images.githubusercontent.com/26706716/39230638-281d5266-48a2-11e8-84d8-ab289f372051.png) |\n\n### Easy to Use: Dragging and Resizing a Schedule\n\n| Dragging | Resizing |\n| --- | --- |\n| ![image](https://user-images.githubusercontent.com/26706716/39230930-591031f8-48a3-11e8-8f62-e12e6c19920c.gif) | ![image](https://user-images.githubusercontent.com/26706716/39231671-c926d0da-48a5-11e8-959d-35fd32f2c522.gif) |\n\n### Ready to Use: Default Popups\n\n| Creation Popup | Detail Popup |\n| --- | --- |\n| ![image](https://user-images.githubusercontent.com/26706716/39230798-d151a9ae-48a2-11e8-842d-b19b40432f48.png) | ![image](https://user-images.githubusercontent.com/26706716/39230820-e73fa11c-48a2-11e8-9348-8e3d81979a78.png) |\n\n## 🎨 Other Features\n\n- Supports various view types: daily, weekly, monthly(6 weeks, 2 weeks, 3 weeks)\n- Supports efficient management of milestone and task schedules\n- Supports the narrow width of weekend\n- Supports changing start day of week\n- Supports customizing the date and schedule information UI(including a header and a footer of grid cell)\n- Supports adjusting a schedule by mouse dragging\n- Supports customizing UI by theme\n\n## 💬 Contributing\n\n- [Code of Conduct](/CODE_OF_CONDUCT.md)\n- [Contributing Guidelines](/CONTRIBUTING.md)\n- [Commit Message Convention](/docs/COMMIT_MESSAGE_CONVENTION.md)\n- [Issue Guidelines](/docs/ISSUE_TEMPLATE.md)\n\n## 🌏 Browser Support\n\n| <img src=\"https://user-images.githubusercontent.com/1215767/34348387-a2e64588-ea4d-11e7-8267-a43365103afe.png\" alt=\"Chrome\" width=\"16px\" height=\"16px\" /> Chrome | <img src=\"https://user-images.githubusercontent.com/1215767/34348590-250b3ca2-ea4f-11e7-9efb-da953359321f.png\" alt=\"IE\" width=\"16px\" height=\"16px\" /> Internet Explorer | <img src=\"https://user-images.githubusercontent.com/1215767/34348380-93e77ae8-ea4d-11e7-8696-9a989ddbbbf5.png\" alt=\"Edge\" width=\"16px\" height=\"16px\" /> Edge | <img src=\"https://user-images.githubusercontent.com/1215767/34348394-a981f892-ea4d-11e7-9156-d128d58386b9.png\" alt=\"Safari\" width=\"16px\" height=\"16px\" /> Safari | <img src=\"https://user-images.githubusercontent.com/1215767/34348383-9e7ed492-ea4d-11e7-910c-03b39d52f496.png\" alt=\"Firefox\" width=\"16px\" height=\"16px\" /> Firefox |\n| :---------: | :---------: | :---------: | :---------: | :---------: |\n| Latest | 11+ | Latest | Latest | Latest |\n\n## 🔩 Dependencies\n\n- [Preact](https://github.com/preactjs/preact)\n- [Immer](https://github.com/immerjs/immer)\n- [DOMPurify](https://github.com/cure53/DOMPurify)\n- (Optional) [tui-date-picker](https://github.com/nhn/tui.date-picker)\n- (Optional) [tui-time-picker](https://github.com/nhn/tui.time-picker)\n\n## 🍞 TOAST UI Family\n\n- [TOAST UI Grid](https://github.com/nhn/tui.grid)\n- [TOAST UI Chart](https://github.com/nhn/tui.chart)\n- [TOAST UI Editor](https://github.com/nhn/tui.editor)\n- [TOAST UI Image-Editor](https://github.com/nhn/tui.image-editor)\n- [TOAST UI Components](https://github.com/nhn?q=tui)\n\n## 🚀 Used By\n\n- [NHN Dooray! - Collaboration Service (Project, Messenger, Mail, Calendar, Drive, Wiki, Contacts)](https://dooray.com)\n- [NCP - Commerce Platform](https://www.e-ncp.com/)\n- [shopby](https://www.godo.co.kr/shopby/main.gd)\n- [payco-shopping](https://shopping.payco.com/)\n- [iamTeacher](https://teacher.iamservice.net)\n- [linder](https://www.linder.kr)\n\n## 📜 License\n\nThis software is licensed under the [MIT](/LICENSE) © [NHN Cloud](https://github.com/nhn).\n"
  },
  {
    "path": "apps/calendar/.browserslistrc",
    "content": "> 1%\nlast 2 versions\nnot ie <= 10\n"
  },
  {
    "path": "apps/calendar/.lintstagedrc.js",
    "content": "module.exports = {\n  '**/*.js': 'eslint --fix',\n  '**/*.{ts,tsx}': [() => 'npm run check-types', 'eslint --fix', 'jest --bail --findRelatedTests'],\n  '**/*.css': 'stylelint',\n};\n"
  },
  {
    "path": "apps/calendar/.storybook/main.js",
    "content": "const path = require('path');\n\nmodule.exports = {\n  core: {\n    builder: 'webpack5',\n  },\n  stories: ['../**/*.stories.@(ts|tsx)'],\n  babel: async (config) => {\n    // Replace storybook babel preset & plugins with custom ones\n    config.presets.splice(config.presets.length - 1, 1, [\n      require.resolve('@babel/preset-typescript'),\n      { jsxPragma: 'h', jsxPragmaFrag: 'Fragment' },\n    ]);\n    config.plugins.splice(config.plugins.length - 1, 1, [\n      require.resolve('@babel/plugin-transform-react-jsx'),\n      { pragma: 'h', pragmaFrag: 'Fragment' },\n      'preset',\n    ]);\n\n    return config;\n  },\n  webpackFinal: async (config) => {\n    config.module.rules = config.module.rules\n      .filter((rule) => !(rule?.test?.test('.css') ?? false))\n      .concat([\n        {\n          test: /\\.css$/,\n          include: /node_modules/,\n          use: [\n            require.resolve('style-loader'),\n            {\n              loader: require.resolve('css-loader'),\n              options: {\n                importLoaders: 1,\n              },\n            },\n          ],\n        },\n        {\n          test: /\\.css$/,\n          exclude: /node_modules/,\n          sideEffects: true,\n          use: [\n            require.resolve('style-loader'),\n            {\n              loader: require.resolve('css-loader'),\n              options: {\n                importLoaders: 1,\n              },\n            },\n            require.resolve('postcss-loader'),\n          ],\n        },\n      ]);\n\n    Object.assign(config.resolve.alias, {\n      'core-js/modules': path.resolve(__dirname, '../../../node_modules/core-js/modules'),\n      '@src': path.resolve(__dirname, '../src/'),\n      '@t': path.resolve(__dirname, '../src/types/'),\n      '@stories': path.resolve(__dirname, '../stories/'),\n    });\n\n    return config;\n  },\n};\n"
  },
  {
    "path": "apps/calendar/.storybook/manager.js",
    "content": "import { addons } from '@storybook/addons';\nimport calendarTheme from './theme';\n\naddons.setConfig({\n  theme: calendarTheme,\n});\n"
  },
  {
    "path": "apps/calendar/.storybook/preview.js",
    "content": "import 'preact/debug';\nimport '@src/css/index.css';\nimport 'tui-date-picker/dist/tui-date-picker.css';\nimport 'tui-time-picker/dist/tui-time-picker.css';\n\nexport const parameters = {\n  layout: 'fullscreen',\n};\n"
  },
  {
    "path": "apps/calendar/.storybook/theme.js",
    "content": "import { create } from '@storybook/theming';\n\nexport default create({\n  base: 'light',\n  brandTitle: 'TOAST UI Calendar',\n  brandUrl: 'https://ui.toast.com/tui-calendar',\n  brandImage: 'https://user-images.githubusercontent.com/26706716/39230183-7f8ff186-48a0-11e8-8d9c-9699d2d0e471.png',\n});\n"
  },
  {
    "path": "apps/calendar/README.md",
    "content": "# ![TOAST UI Calendar](https://user-images.githubusercontent.com/26706716/39230183-7f8ff186-48a0-11e8-8d9c-9699d2d0e471.png)\n\n> A JavaScript calendar that is full featured. Now your service just got the customizable calendar.\n\n[![npm](https://img.shields.io/npm/v/@toast-ui/calendar.svg)](https://www.npmjs.com/package/@toast-ui/calendar)\n[![license](https://img.shields.io/github/license/nhn/tui.calendar.svg)](https://github.com/nhn/tui.calendar/blob/master/LICENSE)\n[![PRs welcome](https://img.shields.io/badge/PRs-welcome-ff69b4.svg)](https://github.com/nhn/tui.calendar/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)\n[![code with hearth by NHN Cloud](https://img.shields.io/badge/%3C%2F%3E%20with%20%E2%99%A5%20by-NHN_Cloud-ff1414.svg)](https://github.com/nhn)\n\n## 🚩 Table of Contents\n\n- [📙 Documents](#-documents)\n- [Collect statistics on the use of open source](#collect-statistics-on-the-use-of-open-source)\n- [💾 Install](#-install)\n  - [Using npm](#using-npm)\n  - [Via Contents Delivery Network (CDN)](#via-contents-delivery-network-cdn)\n  - [Download Source Files](#download-source-files)\n- [📅 Usage](#-usage)\n  - [Load](#load)\n  - [Implement](#implement)\n- [🔧 Pull Request Steps](#-pull-request-steps)\n  - [Setup](#setup)\n  - [Develop](#develop)\n  - [Pull Request](#pull-request)\n- [💬 Contributing](#-contributing)\n- [📜 License](#-license)\n\n## 📙 Documents\n\n- [English](/docs/README.md)\n- [Korean](/docs/ko/README.md)\n\n## Collect statistics on the use of open source\n\nTOAST UI Calendar applies Google Analytics (GA) to collect statistics on the use of open source, in order to identify how widely TOAST UI Calendar is used throughout the world. It also serves as important index to determine the future course of projects. location.hostname (e.g. > “ui.toast.com\") is to be collected and the sole purpose is nothing but to measure statistics on the usage.\n\nTo disable GA, set the [`usageStatistics` option](/docs/en/apis/options.md#usagestatistics) to `false`:\n\n```js\nconst calendar = new Calendar('#calendar', {\n  usageStatistics: false\n});\n```\n\n## 💾 Install\n\n### Using npm\n\n```sh\nnpm install --save @toast-ui/calendar\n```\n\n### Via Contents Delivery Network (CDN)\n\nTOAST UI products are available over the CDN powered by [NHN Cloud](https://www.toast.com).\n\n```html\n<link rel=\"stylesheet\" href=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css\" />\n<script src=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.min.js\"></script>\n\n<!-- To get bundle file for legacy browser -->\n<!-- <script src=\"https://uicdn.toast.com/calendar /latest/toastui-calendar.ie11.min.js\"></script> -->\n\n<!-- Import as es module -->\n<!-- <script type=\"module\" src=\"https:// uicdn.toast.com/calendar/latest/toastui-calendar.mjs\"></script> -->\n```\n\nIf you want to use a specific version, use the tag name instead of `latest` in the url's path.\n\nThe CDN directory has the following structure.\n\n```\n- uicdn.toast.com/\n  ├─ calendar/\n  │  ├─ latest\n  │  │  ├─ toastui-calendar.css\n  │  │  ├─ toastui-calendar.js\n  │  │  ├─ toastui-calendar.min.css\n  │  │  ├─ toastui-calendar.min.js\n  │  │  ├─ toastui-calendar.ie11.js\n  │  │  ├─ toastui-calendar.ie11.min.js\n  │  │  │  toastui-calendar.mjs\n  │  ├─ v2.0.0/\n```\n\n### Download Source Files\n\n- [Download all sources for each version](https://github.com/nhn/tui.calendar/releases)\n\n## 📅 Usage\n\n### Load\n\nTOAST UI Calendar can be instantiated through the constructor function. There are three ways to access the constructor function depending on the environment.\n\n```js\n/* ES6 module in Node.js environment */\nimport Calendar from '@toast-ui/calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n```\n\n```js\n/* CommonJS in Node.js environment */\nconst Calendar = require('@toast-ui/calendar');\nrequire('@toast-ui/calendar/dist/toastui-calendar.min.css');\n```\n\n```js\n/* in the browser environment namespace */\nconst Calendar = tui.Calendar;\n```\n\n### Implement\n\n```html\n<div id=\"calendar\" style=\"height: 800px\"></div>\n```\n\n```javascript\nconst calendar = new Calendar('#calendar', {\n  defaultView: 'week',\n  template: {\n    time(event) {\n      const { start, end, title } = event;\n\n      return `<span style=\"color: white;\">${formatTime(start)}~${formatTime(end)} ${title}</span>`;\n    },\n    allday(event) {\n      return `<span style=\"color: gray;\">${event.title}</span>`;\n    },\n  },\n  calendars: [\n    {\n      id: 'cal1',\n      name: 'Personal',\n      backgroundColor: '#03bd9e',\n    },\n    {\n      id: 'cal2',\n      name: 'Work',\n      backgroundColor: '#00a9ff',\n    },\n  ],\n});\n```\n\n## 🔧 Pull Request Steps\n\nTOAST UI products are open source, so you can create a pull request(PR) after you fix issues.\nRun npm scripts and develop yourself with the following process.\n\n### Setup\n\nFork `main` branch into your personal repository.\nClone it to local computer. Install node modules.\nBefore starting development, you should check to have any errors.\n\n``` sh\ngit clone https://github.com/{your-personal-repo}/[[repo name]].git\ncd [[repo name]]\nnpm install\n```\n\n### Develop\n\nLet's start development!\n\n### Pull Request\n\nBefore PR, check to test lastly and then check any errors.\nIf it has no error, commit and then push it!\n\nFor more information on PR's step, please see links of Contributing section.\n\n## 💬 Contributing\n\n- [Code of Conduct](/CODE_OF_CONDUCT.md)\n- [Contributing Guidelines](/CONTRIBUTING.md)\n- [Commit Message Convention](/docs/COMMIT_MESSAGE_CONVENTION.md)\n\n## 📜 License\n\nThis software is licensed under the [MIT](/LICENSE) © [NHN Cloud](https://github.com/nhn).\n"
  },
  {
    "path": "apps/calendar/examples/00-calendar-app.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\"\n    />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n    <title>TOAST UI Calendar App Demo</title>\n    <link\n      rel=\"stylesheet\"\n      href=\"https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.min.css\"\n    />\n    <link\n      rel=\"stylesheet\"\n      href=\"https://uicdn.toast.com/tui.date-picker/latest/tui-date-picker.min.css\"\n    />\n    <link rel=\"stylesheet\" href=\"../dist/toastui-calendar.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/reset.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/app.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/icons.css\" />\n  </head>\n  <body>\n    <div class=\"app-container code-html\">\n      <header class=\"header\">\n        <a href=\"https://github.com/nhn/tui.calendar\">\n          <img\n            alt=\"TOAST UI Calendar Brand Image\"\n            src=\"./images/img-bi.png\"\n            srcset=\"./images/img-bi@2x.png 2x, ./images/img-bi@3x.png 3x\"\n          />\n        </a>\n      </header>\n      <article class=\"content\">\n        <aside class=\"sidebar\">\n          <div class=\"sidebar-item\">\n            <input class=\"checkbox-all\" type=\"checkbox\" id=\"all\" value=\"all\" checked />\n            <label class=\"checkbox checkbox-all\" for=\"all\">View all</label>\n          </div>\n          <hr />\n          <div class=\"sidebar-item\">\n            <input type=\"checkbox\" id=\"1\" value=\"1\" checked />\n            <label class=\"checkbox checkbox-calendar checkbox-1\" for=\"1\">My Calendar</label>\n          </div>\n          <div class=\"sidebar-item\">\n            <input type=\"checkbox\" id=\"2\" value=\"2\" checked />\n            <label class=\"checkbox checkbox-calendar checkbox-2\" for=\"2\">Work</label>\n          </div>\n          <div class=\"sidebar-item\">\n            <input type=\"checkbox\" id=\"3\" value=\"3\" checked />\n            <label class=\"checkbox checkbox-calendar checkbox-3\" for=\"3\">Family</label>\n          </div>\n          <div class=\"sidebar-item\">\n            <input type=\"checkbox\" id=\"4\" value=\"4\" checked />\n            <label class=\"checkbox checkbox-calendar checkbox-4\" for=\"4\">Friends</label>\n          </div>\n          <div class=\"sidebar-item\">\n            <input type=\"checkbox\" id=\"5\" value=\"5\" checked />\n            <label class=\"checkbox checkbox-calendar checkbox-5\" for=\"5\">Travel</label>\n          </div>\n          <hr />\n          <div class=\"app-footer\">© NHN Cloud Corp.</div>\n        </aside>\n        <section class=\"app-column\">\n          <nav class=\"navbar\">\n            <div class=\"dropdown\">\n              <div class=\"dropdown-trigger\">\n                <button\n                  class=\"button is-rounded\"\n                  aria-haspopup=\"true\"\n                  aria-controls=\"dropdown-menu\"\n                >\n                  <span class=\"button-text\"></span>\n                  <span\n                    class=\"dropdown-icon toastui-calendar-icon toastui-calendar-ic-dropdown-arrow\"\n                  ></span>\n                </button>\n              </div>\n              <div class=\"dropdown-menu\">\n                <div class=\"dropdown-content\">\n                  <a href=\"#\" class=\"dropdown-item\" data-view-name=\"month\">Monthly</a>\n                  <a href=\"#\" class=\"dropdown-item\" data-view-name=\"week\">Weekly</a>\n                  <a href=\"#\" class=\"dropdown-item\" data-view-name=\"day\">Daily</a>\n                </div>\n              </div>\n            </div>\n            <button class=\"button is-rounded today\">Today</button>\n            <button class=\"button is-rounded prev\">\n              <img\n                alt=\"prev\"\n                src=\"./images/ic-arrow-line-left.png\"\n                srcset=\"\n                  ./images/ic-arrow-line-left@2x.png 2x,\n                  ./images/ic-arrow-line-left@3x.png 3x\n                \"\n              />\n            </button>\n            <button class=\"button is-rounded next\">\n              <img\n                alt=\"prev\"\n                src=\"./images/ic-arrow-line-right.png\"\n                srcset=\"\n                  ./images/ic-arrow-line-right@2x.png 2x,\n                  ./images/ic-arrow-line-right@3x.png 3x\n                \"\n              />\n            </button>\n            <span class=\"navbar--range\"></span>\n            <div class=\"nav-checkbox\">\n              <input class=\"checkbox-collapse\" type=\"checkbox\" id=\"collapse\" value=\"collapse\" />\n              <label for=\"collapse\">Collapse duplicate events and disable the detail popup</label>\n            </div>\n          </nav>\n          <main id=\"app\"></main>\n        </section>\n      </article>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js\"></script>\n    <script src=\"https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.min.js\"></script>\n    <script src=\"https://uicdn.toast.com/tui.date-picker/latest/tui-date-picker.min.js\"></script>\n    <script src=\"../dist/toastui-calendar.ie11.min.js\"></script>\n    <script src=\"./scripts/mock-data.js\"></script>\n    <script src=\"./scripts/utils.js\"></script>\n    <script src=\"./scripts/app.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/calendar/examples/01-monthly-view-basic.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\"\n    />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n    <title>TOAST UI Calendar Example - Monthly View Basic</title>\n    <link rel=\"stylesheet\" href=\"../dist/toastui-calendar.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/reset.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/app.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/icons.css\" />\n    <style>\n      .navbar {\n        padding: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"app-container code-html\">\n      <header class=\"header\">\n        <nav class=\"navbar\">\n          <button class=\"button is-rounded today\">Today</button>\n          <button class=\"button is-rounded prev\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-left.png\"\n              srcset=\"./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x\"\n            />\n          </button>\n          <button class=\"button is-rounded next\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-right.png\"\n              srcset=\"\n                ./images/ic-arrow-line-right@2x.png 2x,\n                ./images/ic-arrow-line-right@3x.png 3x\n              \"\n            />\n          </button>\n          <span class=\"navbar--range\"></span>\n        </nav>\n      </header>\n      <main id=\"app\"></main>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js\"></script>\n    <script src=\"../dist/toastui-calendar.ie11.min.js\"></script>\n    <script src=\"./scripts/mock-data.js\"></script>\n    <script src=\"./scripts/utils.js\"></script>\n    <script type=\"text/javascript\" class=\"code-js\">\n      var Calendar = window.tui.Calendar;\n\n      var cal = new Calendar('#app', {\n        defaultView: 'month',\n        calendars: MOCK_CALENDARS,\n      });\n    </script>\n    <script type=\"text/javascript\">\n      var todayButton = $('.today');\n      var prevButton = $('.prev');\n      var nextButton = $('.next');\n      var range = $('.navbar--range');\n\n      function displayEvents() {\n        var events = generateRandomEvents(\n          cal.getViewName(),\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd()\n        );\n        cal.clear();\n        cal.createEvents(events);\n      }\n\n      function displayRenderRange() {\n        range.textContent = getNavbarRange(cal.getDateRangeStart(), cal.getDateRangeEnd(), 'month');\n      }\n\n      todayButton.addEventListener('click', function () {\n        cal.today();\n        displayEvents();\n        displayRenderRange();\n      });\n      prevButton.addEventListener('click', function () {\n        cal.prev();\n        displayEvents();\n        displayRenderRange();\n      });\n      nextButton.addEventListener('click', function () {\n        cal.next();\n        displayEvents();\n        displayRenderRange();\n      });\n\n      displayEvents();\n      displayRenderRange();\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/calendar/examples/02-monthly-view-2weeks.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\"\n    />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n    <title>TOAST UI Calendar Example - Monthly View Basic</title>\n    <link rel=\"stylesheet\" href=\"../dist/toastui-calendar.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/reset.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/app.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/icons.css\" />\n    <style>\n      .navbar {\n        padding: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"app-container code-html\">\n      <header class=\"header\">\n        <nav class=\"navbar\">\n          <button class=\"button is-rounded today\">Today</button>\n          <button class=\"button is-rounded prev\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-left.png\"\n              srcset=\"./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x\"\n            />\n          </button>\n          <button class=\"button is-rounded next\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-right.png\"\n              srcset=\"\n                ./images/ic-arrow-line-right@2x.png 2x,\n                ./images/ic-arrow-line-right@3x.png 3x\n              \"\n            />\n          </button>\n          <span class=\"navbar--range\"></span>\n        </nav>\n      </header>\n      <main id=\"app\"></main>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js\"></script>\n    <script src=\"../dist/toastui-calendar.ie11.min.js\"></script>\n    <script src=\"./scripts/mock-data.js\"></script>\n    <script src=\"./scripts/utils.js\"></script>\n    <script type=\"text/javascript\" class=\"code-js\">\n      var Calendar = window.tui.Calendar;\n\n      var cal = new Calendar('#app', {\n        defaultView: 'month',\n        calendars: MOCK_CALENDARS,\n        month: {\n          visibleWeeksCount: 2,\n        },\n      });\n    </script>\n    <script type=\"text/javascript\">\n      var todayButton = $('.today');\n      var prevButton = $('.prev');\n      var nextButton = $('.next');\n      var range = $('.navbar--range');\n\n      function displayEvents() {\n        var events = generateRandomEvents(\n          cal.getViewName(),\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd()\n        );\n        cal.clear();\n        cal.createEvents(events);\n      }\n\n      function displayRenderRange() {\n        range.textContent = getNavbarRange(cal.getDateRangeStart(), cal.getDateRangeEnd(), 'week');\n      }\n\n      todayButton.addEventListener('click', function () {\n        cal.today();\n        displayEvents();\n        displayRenderRange();\n      });\n      prevButton.addEventListener('click', function () {\n        cal.prev();\n        displayEvents();\n        displayRenderRange();\n      });\n      nextButton.addEventListener('click', function () {\n        cal.next();\n        displayEvents();\n        displayRenderRange();\n      });\n\n      displayEvents();\n      displayRenderRange();\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/calendar/examples/03-monthly-view-3weeks.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\"\n    />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n    <title>TOAST UI Calendar Example - Monthly View Basic</title>\n    <link rel=\"stylesheet\" href=\"../dist/toastui-calendar.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/reset.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/app.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/icons.css\" />\n    <style>\n      .navbar {\n        padding: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"app-container code-html\">\n      <header class=\"header\">\n        <nav class=\"navbar\">\n          <button class=\"button is-rounded today\">Today</button>\n          <button class=\"button is-rounded prev\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-left.png\"\n              srcset=\"./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x\"\n            />\n          </button>\n          <button class=\"button is-rounded next\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-right.png\"\n              srcset=\"\n                ./images/ic-arrow-line-right@2x.png 2x,\n                ./images/ic-arrow-line-right@3x.png 3x\n              \"\n            />\n          </button>\n          <span class=\"navbar--range\"></span>\n        </nav>\n      </header>\n      <main id=\"app\"></main>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js\"></script>\n    <script src=\"../dist/toastui-calendar.ie11.min.js\"></script>\n    <script src=\"./scripts/mock-data.js\"></script>\n    <script src=\"./scripts/utils.js\"></script>\n    <script type=\"text/javascript\" class=\"code-js\">\n      var Calendar = window.tui.Calendar;\n\n      var cal = new Calendar('#app', {\n        defaultView: 'month',\n        calendars: MOCK_CALENDARS,\n        month: {\n          visibleWeeksCount: 3,\n        },\n      });\n    </script>\n    <script type=\"text/javascript\">\n      var todayButton = $('.today');\n      var prevButton = $('.prev');\n      var nextButton = $('.next');\n      var range = $('.navbar--range');\n\n      function displayEvents() {\n        var events = generateRandomEvents(\n          cal.getViewName(),\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd()\n        );\n        cal.clear();\n        cal.createEvents(events);\n      }\n\n      function displayRenderRange() {\n        range.textContent = getNavbarRange(cal.getDateRangeStart(), cal.getDateRangeEnd(), 'week');\n      }\n\n      todayButton.addEventListener('click', function () {\n        cal.today();\n        displayEvents();\n        displayRenderRange();\n      });\n      prevButton.addEventListener('click', function () {\n        cal.prev();\n        displayEvents();\n        displayRenderRange();\n      });\n      nextButton.addEventListener('click', function () {\n        cal.next();\n        displayEvents();\n        displayRenderRange();\n      });\n\n      displayEvents();\n      displayRenderRange();\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/calendar/examples/04-weekly-view.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\"\n    />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n    <title>TOAST UI Calendar Example - Weekly View</title>\n    <link rel=\"stylesheet\" href=\"../dist/toastui-calendar.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/reset.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/app.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/icons.css\" />\n    <style>\n      .navbar {\n        padding: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"app-container code-html\">\n      <header class=\"header\">\n        <nav class=\"navbar\">\n          <button class=\"button is-rounded today\">Today</button>\n          <button class=\"button is-rounded prev\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-left.png\"\n              srcset=\"./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x\"\n            />\n          </button>\n          <button class=\"button is-rounded next\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-right.png\"\n              srcset=\"\n                ./images/ic-arrow-line-right@2x.png 2x,\n                ./images/ic-arrow-line-right@3x.png 3x\n              \"\n            />\n          </button>\n          <span class=\"navbar--range\"></span>\n        </nav>\n      </header>\n      <main id=\"app\"></main>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js\"></script>\n    <script src=\"../dist/toastui-calendar.ie11.min.js\"></script>\n    <script src=\"./scripts/mock-data.js\"></script>\n    <script src=\"./scripts/utils.js\"></script>\n    <script type=\"text/javascript\" class=\"code-js\">\n      var Calendar = window.tui.Calendar;\n      var cal = new Calendar('#app', {\n        defaultView: 'week',\n        calendars: MOCK_CALENDARS,\n      });\n    </script>\n    <script type=\"text/javascript\">\n      var todayButton = $('.today');\n      var prevButton = $('.prev');\n      var nextButton = $('.next');\n      var range = $('.navbar--range');\n\n      function displayEvents() {\n        var events = generateRandomEvents(\n          cal.getViewName(),\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd()\n        );\n        cal.clear();\n        cal.createEvents(events);\n      }\n\n      function displayRenderRange() {\n        var viewName = cal.getViewName();\n\n        range.textContent = getNavbarRange(\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd(),\n          viewName\n        );\n      }\n\n      todayButton.addEventListener('click', function () {\n        cal.today();\n        displayEvents();\n        displayRenderRange();\n      });\n      prevButton.addEventListener('click', function () {\n        cal.prev();\n        displayEvents();\n        displayRenderRange();\n      });\n      nextButton.addEventListener('click', function () {\n        cal.next();\n        displayEvents();\n        displayRenderRange();\n      });\n\n      displayEvents();\n      displayRenderRange();\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/calendar/examples/05-weekly-view-no-event-view.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\"\n    />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n    <title>TOAST UI Calendar Example - Weekly View (No Event View)</title>\n    <link rel=\"stylesheet\" href=\"../dist/toastui-calendar.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/reset.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/app.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/icons.css\" />\n    <style>\n      .navbar {\n        padding: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"app-container code-html\">\n      <header class=\"header\">\n        <nav class=\"navbar\">\n          <button class=\"button is-rounded today\">Today</button>\n          <button class=\"button is-rounded prev\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-left.png\"\n              srcset=\"./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x\"\n            />\n          </button>\n          <button class=\"button is-rounded next\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-right.png\"\n              srcset=\"\n                ./images/ic-arrow-line-right@2x.png 2x,\n                ./images/ic-arrow-line-right@3x.png 3x\n              \"\n            />\n          </button>\n          <span class=\"navbar--range\"></span>\n        </nav>\n      </header>\n      <main id=\"app\"></main>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js\"></script>\n    <script src=\"../dist/toastui-calendar.ie11.min.js\"></script>\n    <script src=\"./scripts/mock-data.js\"></script>\n    <script src=\"./scripts/utils.js\"></script>\n    <script type=\"text/javascript\" class=\"code-js\">\n      var Calendar = window.tui.Calendar;\n      var cal = new Calendar('#app', {\n        defaultView: 'week',\n        calendars: MOCK_CALENDARS,\n        week: {\n          taskView: true,\n          eventView: false,\n        },\n      });\n    </script>\n    <script type=\"text/javascript\">\n      var todayButton = $('.today');\n      var prevButton = $('.prev');\n      var nextButton = $('.next');\n      var range = $('.navbar--range');\n      function displayEvents() {\n        var events = generateRandomEvents(\n          cal.getViewName(),\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd()\n        );\n        cal.clear();\n        cal.createEvents(events);\n      }\n\n      function displayRenderRange() {\n        var viewName = cal.getViewName();\n\n        range.textContent = getNavbarRange(\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd(),\n          viewName\n        );\n      }\n\n      todayButton.addEventListener('click', function () {\n        cal.today();\n        displayEvents();\n        displayRenderRange();\n      });\n      prevButton.addEventListener('click', function () {\n        cal.prev();\n        displayEvents();\n        displayRenderRange();\n      });\n      nextButton.addEventListener('click', function () {\n        cal.next();\n        displayEvents();\n        displayRenderRange();\n      });\n\n      displayEvents();\n      displayRenderRange();\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/calendar/examples/06-daily-view.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\"\n    />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n    <title>TOAST UI Calendar Example - Daily View</title>\n    <link rel=\"stylesheet\" href=\"../dist/toastui-calendar.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/reset.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/app.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/icons.css\" />\n    <style>\n      .navbar {\n        padding: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"app-container code-html\">\n      <header class=\"header\">\n        <nav class=\"navbar\">\n          <button class=\"button is-rounded today\">Today</button>\n          <button class=\"button is-rounded prev\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-left.png\"\n              srcset=\"./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x\"\n            />\n          </button>\n          <button class=\"button is-rounded next\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-right.png\"\n              srcset=\"\n                ./images/ic-arrow-line-right@2x.png 2x,\n                ./images/ic-arrow-line-right@3x.png 3x\n              \"\n            />\n          </button>\n          <span class=\"navbar--range\"></span>\n        </nav>\n      </header>\n      <main id=\"app\"></main>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js\"></script>\n    <script src=\"../dist/toastui-calendar.ie11.min.js\"></script>\n    <script src=\"./scripts/mock-data.js\"></script>\n    <script src=\"./scripts/utils.js\"></script>\n    <script type=\"text/javascript\" class=\"code-js\">\n      var Calendar = window.tui.Calendar;\n      var cal = new Calendar('#app', {\n        defaultView: 'day',\n        calendars: MOCK_CALENDARS,\n      });\n    </script>\n    <script type=\"text/javascript\">\n      var todayButton = $('.today');\n      var prevButton = $('.prev');\n      var nextButton = $('.next');\n      var range = $('.navbar--range');\n      function displayEvents() {\n        var events = generateRandomEvents(\n          cal.getViewName(),\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd()\n        );\n        cal.clear();\n        cal.createEvents(events);\n      }\n\n      function displayRenderRange() {\n        var viewName = cal.getViewName();\n\n        range.textContent = getNavbarRange(\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd(),\n          viewName\n        );\n      }\n\n      todayButton.addEventListener('click', function () {\n        cal.today();\n        displayEvents();\n        displayRenderRange();\n      });\n      prevButton.addEventListener('click', function () {\n        cal.prev();\n        displayEvents();\n        displayRenderRange();\n      });\n      nextButton.addEventListener('click', function () {\n        cal.next();\n        displayEvents();\n        displayRenderRange();\n      });\n\n      displayEvents();\n      displayRenderRange();\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/calendar/examples/07-narrow-weekends.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\"\n    />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n    <title>TOAST UI Calendar Example - Narrow Weekends</title>\n    <link rel=\"stylesheet\" href=\"../dist/toastui-calendar.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/reset.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/app.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/icons.css\" />\n    <style>\n      .navbar {\n        padding: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"app-container code-html\">\n      <header class=\"header\">\n        <nav class=\"navbar\">\n          <button class=\"button is-rounded switch-view\">Switch View (Monthly / Weekly)</button>\n          <button class=\"button is-rounded today\">Today</button>\n          <button class=\"button is-rounded prev\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-left.png\"\n              srcset=\"./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x\"\n            />\n          </button>\n          <button class=\"button is-rounded next\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-right.png\"\n              srcset=\"\n                ./images/ic-arrow-line-right@2x.png 2x,\n                ./images/ic-arrow-line-right@3x.png 3x\n              \"\n            />\n          </button>\n          <span class=\"navbar--range\"></span>\n        </nav>\n      </header>\n      <main id=\"app\"></main>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js\"></script>\n    <script src=\"../dist/toastui-calendar.ie11.min.js\"></script>\n    <script src=\"./scripts/mock-data.js\"></script>\n    <script src=\"./scripts/utils.js\"></script>\n    <script type=\"text/javascript\" class=\"code-js\">\n      var Calendar = window.tui.Calendar;\n\n      var cal = new Calendar('#app', {\n        defaultView: 'month',\n        calendars: MOCK_CALENDARS,\n        month: {\n          narrowWeekend: true,\n        },\n        week: {\n          narrowWeekend: true,\n        },\n      });\n    </script>\n    <script type=\"text/javascript\">\n      var switchButton = $('.switch-view');\n      var todayButton = $('.today');\n      var prevButton = $('.prev');\n      var nextButton = $('.next');\n      var range = $('.navbar--range');\n      function displayEvents() {\n        var events = generateRandomEvents(\n          cal.getViewName(),\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd()\n        );\n        cal.clear();\n        cal.createEvents(events);\n      }\n\n      function displayRenderRange() {\n        var viewName = cal.getViewName();\n\n        range.textContent = getNavbarRange(\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd(),\n          viewName\n        );\n      }\n\n      switchButton.addEventListener('click', function () {\n        if (cal.getViewName() === 'month') {\n          cal.changeView('week');\n        } else {\n          cal.changeView('month');\n        }\n        displayRenderRange();\n      });\n      todayButton.addEventListener('click', function () {\n        cal.today();\n        displayEvents();\n        displayRenderRange();\n      });\n      prevButton.addEventListener('click', function () {\n        cal.prev();\n        displayEvents();\n        displayRenderRange();\n      });\n      nextButton.addEventListener('click', function () {\n        cal.next();\n        displayEvents();\n        displayRenderRange();\n      });\n\n      displayEvents();\n      displayRenderRange();\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/calendar/examples/08-hidden-weekends.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\"\n    />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n    <title>TOAST UI Calendar Example - Hidden Weekends</title>\n    <link rel=\"stylesheet\" href=\"../dist/toastui-calendar.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/reset.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/app.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/icons.css\" />\n    <style>\n      .navbar {\n        padding: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"app-container code-html\">\n      <header class=\"header\">\n        <nav class=\"navbar\">\n          <button class=\"button is-rounded switch-view\">Switch View (Monthly / Weekly)</button>\n          <button class=\"button is-rounded today\">Today</button>\n          <button class=\"button is-rounded prev\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-left.png\"\n              srcset=\"./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x\"\n            />\n          </button>\n          <button class=\"button is-rounded next\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-right.png\"\n              srcset=\"\n                ./images/ic-arrow-line-right@2x.png 2x,\n                ./images/ic-arrow-line-right@3x.png 3x\n              \"\n            />\n          </button>\n          <span class=\"navbar--range\"></span>\n        </nav>\n      </header>\n      <main id=\"app\"></main>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js\"></script>\n    <script src=\"../dist/toastui-calendar.ie11.min.js\"></script>\n    <script src=\"./scripts/mock-data.js\"></script>\n    <script src=\"./scripts/utils.js\"></script>\n    <script type=\"text/javascript\" class=\"code-js\">\n      var Calendar = window.tui.Calendar;\n\n      var cal = new Calendar('#app', {\n        defaultView: 'month',\n        calendars: MOCK_CALENDARS,\n        month: {\n          workweek: true,\n        },\n        week: {\n          workweek: true,\n        },\n      });\n    </script>\n    <script type=\"text/javascript\">\n      var switchButton = $('.switch-view');\n      var todayButton = $('.today');\n      var prevButton = $('.prev');\n      var nextButton = $('.next');\n      var range = $('.navbar--range');\n      function displayEvents() {\n        var events = generateRandomEvents(\n          cal.getViewName(),\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd()\n        );\n        cal.clear();\n        cal.createEvents(events);\n      }\n\n      function displayRenderRange() {\n        var viewName = cal.getViewName();\n\n        range.textContent = getNavbarRange(\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd(),\n          viewName\n        );\n      }\n\n      switchButton.addEventListener('click', function () {\n        if (cal.getViewName() === 'month') {\n          cal.changeView('week');\n        } else {\n          cal.changeView('month');\n        }\n        displayRenderRange();\n      });\n      todayButton.addEventListener('click', function () {\n        cal.today();\n        displayEvents();\n        displayRenderRange();\n      });\n      prevButton.addEventListener('click', function () {\n        cal.prev();\n        displayEvents();\n        displayRenderRange();\n      });\n      nextButton.addEventListener('click', function () {\n        cal.next();\n        displayEvents();\n        displayRenderRange();\n      });\n\n      displayEvents();\n      displayRenderRange();\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/calendar/examples/09-timezone.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\"\n    />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n    <title>TOAST UI Calendar Example - Timezone (Weekly View)</title>\n    <link rel=\"stylesheet\" href=\"../dist/toastui-calendar.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/reset.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/app.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/icons.css\" />\n    <style>\n      .navbar {\n        padding: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"app-container code-html\">\n      <header class=\"header\">\n        <nav class=\"navbar\">\n          <button class=\"button is-rounded today\">Today</button>\n          <button class=\"button is-rounded prev\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-left.png\"\n              srcset=\"./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x\"\n            />\n          </button>\n          <button class=\"button is-rounded next\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-right.png\"\n              srcset=\"\n                ./images/ic-arrow-line-right@2x.png 2x,\n                ./images/ic-arrow-line-right@3x.png 3x\n              \"\n            />\n          </button>\n          <span class=\"navbar--range\"></span>\n        </nav>\n      </header>\n      <main id=\"app\"></main>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js\"></script>\n    <script src=\"../dist/toastui-calendar.ie11.min.js\"></script>\n    <script src=\"./scripts/mock-data.js\"></script>\n    <script src=\"./scripts/utils.js\"></script>\n    <script type=\"text/javascript\" class=\"code-js\">\n      var Calendar = window.tui.Calendar;\n\n      var cal = new Calendar('#app', {\n        defaultView: 'week',\n        calendars: MOCK_CALENDARS,\n        timezone: {\n          zones: [\n            {\n              timezoneName: 'Asia/Seoul',\n              displayLabel: 'UTC+09',\n              tooltip: 'Seoul',\n            },\n            {\n              timezoneName: 'Europe/London',\n              displayLabel: 'UTC+00',\n              tooltip: 'London',\n            },\n          ],\n        },\n        week: {\n          showTimezoneCollapseButton: true,\n        },\n        theme: {\n          week: {\n            dayGridLeft: {\n              width: '8rem',\n            },\n            timeGridLeft: {\n              width: '8rem',\n            },\n          },\n        },\n      });\n\n      cal.on('clickTimezonesCollapseBtn', function (prevCollapsedState) {\n        cal.setOptions({\n          week: {\n            timezonesCollapsed: !prevCollapsedState,\n          },\n        });\n      });\n    </script>\n    <script type=\"text/javascript\">\n      var todayButton = $('.today');\n      var prevButton = $('.prev');\n      var nextButton = $('.next');\n      var range = $('.navbar--range');\n      function displayEvents() {\n        var events = generateRandomEvents(\n          cal.getViewName(),\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd()\n        );\n        cal.clear();\n        cal.createEvents(events);\n      }\n\n      function displayRenderRange() {\n        var viewName = cal.getViewName();\n\n        range.textContent = getNavbarRange(\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd(),\n          viewName\n        );\n      }\n\n      todayButton.addEventListener('click', function () {\n        cal.today();\n        displayEvents();\n        displayRenderRange();\n      });\n      prevButton.addEventListener('click', function () {\n        cal.prev();\n        displayEvents();\n        displayRenderRange();\n      });\n      nextButton.addEventListener('click', function () {\n        cal.next();\n        displayEvents();\n        displayRenderRange();\n      });\n\n      displayEvents();\n      displayRenderRange();\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/calendar/examples/10-theme-common.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\"\n    />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n    <title>TOAST UI Calendar Example - Common Theme</title>\n    <link rel=\"stylesheet\" href=\"../dist/toastui-calendar.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/reset.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/app.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/icons.css\" />\n    <style>\n      .navbar {\n        padding: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"app-container code-html\">\n      <header class=\"header\">\n        <nav class=\"navbar\">\n          <button class=\"button is-rounded today\">Today</button>\n          <button class=\"button is-rounded prev\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-left.png\"\n              srcset=\"./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x\"\n            />\n          </button>\n          <button class=\"button is-rounded next\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-right.png\"\n              srcset=\"\n                ./images/ic-arrow-line-right@2x.png 2x,\n                ./images/ic-arrow-line-right@3x.png 3x\n              \"\n            />\n          </button>\n          <span class=\"navbar--range\"></span>\n        </nav>\n      </header>\n      <main id=\"app\"></main>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js\"></script>\n    <script src=\"../dist/toastui-calendar.ie11.min.js\"></script>\n    <script src=\"./scripts/mock-data.js\"></script>\n    <script src=\"./scripts/utils.js\"></script>\n    <script type=\"text/javascript\" class=\"code-js\">\n      var Calendar = window.tui.Calendar;\n\n      var cal = new Calendar('#app', {\n        defaultView: 'month',\n        calendars: MOCK_CALENDARS,\n        theme: {\n          // NOTE: Not every theme is not included. For more info, check the theme docs.\n          common: {\n            backgroundColor: '#8de0cd',\n            border: '1px solid #818545',\n            gridSelection: {\n              backgroundColor: 'rgba(81, 230, 92, 0.05)',\n              border: '1px dotted #515ce6',\n            },\n            saturday: {\n              color: 'rgba(64, 64, 255, 0.5)',\n            },\n            today: {\n              color: '#ff4194',\n            },\n          },\n        },\n      });\n    </script>\n    <script type=\"text/javascript\">\n      var todayButton = $('.today');\n      var prevButton = $('.prev');\n      var nextButton = $('.next');\n      var range = $('.navbar--range');\n      function displayEvents() {\n        var events = generateRandomEvents(\n          cal.getViewName(),\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd()\n        );\n        cal.clear();\n        cal.createEvents(events);\n      }\n\n      function displayRenderRange() {\n        var viewName = cal.getViewName();\n\n        range.textContent = getNavbarRange(\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd(),\n          viewName\n        );\n      }\n\n      todayButton.addEventListener('click', function () {\n        cal.today();\n        displayEvents();\n        displayRenderRange();\n      });\n      prevButton.addEventListener('click', function () {\n        cal.prev();\n        displayEvents();\n        displayRenderRange();\n      });\n      nextButton.addEventListener('click', function () {\n        cal.next();\n        displayEvents();\n        displayRenderRange();\n      });\n\n      displayEvents();\n      displayRenderRange();\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/calendar/examples/11-theme-monthly.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\"\n    />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n    <title>TOAST UI Calendar Example - Theme for Weekly View</title>\n    <link rel=\"stylesheet\" href=\"../dist/toastui-calendar.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/reset.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/app.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/icons.css\" />\n    <style>\n      .navbar {\n        padding: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"app-container code-html\">\n      <header class=\"header\">\n        <nav class=\"navbar\">\n          <button class=\"button is-rounded today\">Today</button>\n          <button class=\"button is-rounded prev\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-left.png\"\n              srcset=\"./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x\"\n            />\n          </button>\n          <button class=\"button is-rounded next\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-right.png\"\n              srcset=\"\n                ./images/ic-arrow-line-right@2x.png 2x,\n                ./images/ic-arrow-line-right@3x.png 3x\n              \"\n            />\n          </button>\n          <span class=\"navbar--range\"></span>\n        </nav>\n      </header>\n      <main id=\"app\"></main>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js\"></script>\n    <script src=\"../dist/toastui-calendar.ie11.min.js\"></script>\n    <script src=\"./scripts/mock-data.js\"></script>\n    <script src=\"./scripts/utils.js\"></script>\n    <script type=\"text/javascript\" class=\"code-js\">\n      var Calendar = window.tui.Calendar;\n\n      var cal = new Calendar('#app', {\n        defaultView: 'month',\n        calendars: MOCK_CALENDARS,\n        // NOTE: Not every theme is not included. For more info, check the theme docs.\n        theme: {\n          month: {\n            dayName: {\n              borderLeft: 'none',\n              backgroundColor: 'rgba(51, 51, 51, 0.4)',\n            },\n            moreView: {\n              border: '1px solid grey',\n              boxShadow: '0 2px 6px 0 grey',\n              backgroundColor: 'white',\n              width: 320,\n              height: 200,\n            },\n            weekend: {\n              backgroundColor: 'rgba(255, 64, 64, 0.4)',\n            },\n            gridCell: {\n              footerHeight: 31,\n            },\n          },\n        },\n      });\n    </script>\n    <script type=\"text/javascript\">\n      var todayButton = $('.today');\n      var prevButton = $('.prev');\n      var nextButton = $('.next');\n      var range = $('.navbar--range');\n      function displayEvents() {\n        var events = generateRandomEvents(\n          cal.getViewName(),\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd()\n        );\n        cal.clear();\n        cal.createEvents(events);\n      }\n\n      function displayRenderRange() {\n        var viewName = cal.getViewName();\n\n        range.textContent = getNavbarRange(\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd(),\n          viewName\n        );\n      }\n\n      todayButton.addEventListener('click', function () {\n        cal.today();\n        displayEvents();\n        displayRenderRange();\n      });\n      prevButton.addEventListener('click', function () {\n        cal.prev();\n        displayEvents();\n        displayRenderRange();\n      });\n      nextButton.addEventListener('click', function () {\n        cal.next();\n        displayEvents();\n        displayRenderRange();\n      });\n\n      displayEvents();\n      displayRenderRange();\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/calendar/examples/12-theme-weekly.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\"\n    />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n    <title>TOAST UI Calendar Example - Theme for Weekly View</title>\n    <link rel=\"stylesheet\" href=\"../dist/toastui-calendar.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/reset.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/app.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/icons.css\" />\n    <style>\n      .navbar {\n        padding: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"app-container code-html\">\n      <header class=\"header\">\n        <nav class=\"navbar\">\n          <button class=\"button is-rounded today\">Today</button>\n          <button class=\"button is-rounded prev\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-left.png\"\n              srcset=\"./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x\"\n            />\n          </button>\n          <button class=\"button is-rounded next\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-right.png\"\n              srcset=\"\n                ./images/ic-arrow-line-right@2x.png 2x,\n                ./images/ic-arrow-line-right@3x.png 3x\n              \"\n            />\n          </button>\n          <span class=\"navbar--range\"></span>\n        </nav>\n      </header>\n      <main id=\"app\"></main>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js\"></script>\n    <script src=\"../dist/toastui-calendar.ie11.min.js\"></script>\n    <script src=\"./scripts/mock-data.js\"></script>\n    <script src=\"./scripts/utils.js\"></script>\n    <script type=\"text/javascript\" class=\"code-js\">\n      var Calendar = window.tui.Calendar;\n\n      var cal = new Calendar('#app', {\n        defaultView: 'week',\n        calendars: MOCK_CALENDARS,\n        timezone: {\n          zones: [\n            {\n              timezoneName: 'Asia/Seoul',\n              displayLabel: 'UTC+09',\n              tooltip: 'Seoul',\n            },\n            {\n              timezoneName: 'Europe/London',\n              displayLabel: 'UTC+00',\n              tooltip: 'London',\n            },\n          ],\n        },\n        // NOTE: Not every theme is not included. For more info, check the theme docs.\n        theme: {\n          week: {\n            dayName: {\n              borderLeft: 'none',\n              borderTop: '1px dotted red',\n              borderBottom: '1px dotted red',\n              backgroundColor: 'rgba(81, 92, 230, 0.05)',\n            },\n            dayGrid: {\n              backgroundColor: 'rgba(81, 92, 230, 0.05)',\n            },\n            dayGridLeft: {\n              borderRight: 'none',\n              backgroundColor: 'rgba(81, 92, 230, 0.05)',\n              width: '144px',\n            },\n            timeGridLeft: {\n              borderRight: 'none',\n              backgroundColor: 'rgba(81, 92, 230, 0.05)',\n              width: '144px',\n            },\n            timeGridLeftAdditionalTimezone: {\n              backgroundColor: '#e5e5e5',\n            },\n            timeGridHalfHourLine: {\n              borderBottom: '1px dotted #e5e5e5',\n            },\n            nowIndicatorPast: {\n              border: '1px dashed red',\n            },\n            futureTime: {\n              color: 'red',\n            },\n          },\n        },\n      });\n    </script>\n    <script type=\"text/javascript\">\n      var todayButton = $('.today');\n      var prevButton = $('.prev');\n      var nextButton = $('.next');\n      var range = $('.navbar--range');\n\n      function displayEvents() {\n        var events = generateRandomEvents(\n          cal.getViewName(),\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd()\n        );\n        cal.clear();\n        cal.createEvents(events);\n      }\n\n      function displayRenderRange() {\n        var viewName = cal.getViewName();\n\n        range.textContent = getNavbarRange(\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd(),\n          viewName\n        );\n      }\n\n      todayButton.addEventListener('click', function () {\n        cal.today();\n        displayEvents();\n        displayRenderRange();\n      });\n      prevButton.addEventListener('click', function () {\n        cal.prev();\n        displayEvents();\n        displayRenderRange();\n      });\n      nextButton.addEventListener('click', function () {\n        cal.next();\n        displayEvents();\n        displayRenderRange();\n      });\n\n      displayEvents();\n      displayRenderRange();\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/calendar/examples/13-template-monthly.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\"\n    />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n    <title>TOAST UI Calendar Example - Templates for Monthly View</title>\n    <link rel=\"stylesheet\" href=\"../dist/toastui-calendar.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/reset.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/app.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/icons.css\" />\n    <style>\n      .navbar {\n        padding: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"app-container code-html\">\n      <header class=\"header\">\n        <nav class=\"navbar\">\n          <button class=\"button is-rounded today\">Today</button>\n          <button class=\"button is-rounded prev\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-left.png\"\n              srcset=\"./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x\"\n            />\n          </button>\n          <button class=\"button is-rounded next\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-right.png\"\n              srcset=\"\n                ./images/ic-arrow-line-right@2x.png 2x,\n                ./images/ic-arrow-line-right@3x.png 3x\n              \"\n            />\n          </button>\n          <span class=\"navbar--range\"></span>\n        </nav>\n      </header>\n      <main id=\"app\"></main>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js\"></script>\n    <script src=\"../dist/toastui-calendar.ie11.min.js\"></script>\n    <script src=\"./scripts/mock-data.js\"></script>\n    <script src=\"./scripts/utils.js\"></script>\n    <script type=\"text/javascript\" class=\"code-js\">\n      var Calendar = window.tui.Calendar;\n\n      var cal = new Calendar('#app', {\n        defaultView: 'month',\n        calendars: MOCK_CALENDARS,\n        template: {\n          monthGridHeader: function (data) {\n            var date = parseInt(data.date.split('-')[2], 10);\n\n            return (\n              '<span class=\"calendar-month-header\" style=\"margin-left: 4px;\">' +\n              (data.month + 1) +\n              '/' +\n              date +\n              '</span>'\n            );\n          },\n          monthGridHeaderExceed: function (hiddenEvents) {\n            return (\n              '<span class=\"calendar-month-header-exceed\" style=\"font-size: 0.8rem\">' +\n              '+' +\n              hiddenEvents +\n              '</span>'\n            );\n          },\n          monthDayName: function (data) {\n            var label = data.label;\n\n            if (data.day === 5) {\n              label = '🎉 TGIF';\n            }\n\n            return '<span class=\"calendar-month-day-name\">' + label + '</span>';\n          },\n        },\n      });\n    </script>\n    <script type=\"text/javascript\">\n      var todayButton = $('.today');\n      var prevButton = $('.prev');\n      var nextButton = $('.next');\n      var range = $('.navbar--range');\n\n      function displayEvents() {\n        var events = generateRandomEvents(\n          cal.getViewName(),\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd()\n        );\n        cal.clear();\n        cal.createEvents(events);\n      }\n\n      function displayRenderRange() {\n        var viewName = cal.getViewName();\n\n        range.textContent = getNavbarRange(\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd(),\n          viewName\n        );\n      }\n\n      todayButton.addEventListener('click', function () {\n        cal.today();\n        displayEvents();\n        displayRenderRange();\n      });\n      prevButton.addEventListener('click', function () {\n        cal.prev();\n        displayEvents();\n        displayRenderRange();\n      });\n      nextButton.addEventListener('click', function () {\n        cal.next();\n        displayEvents();\n        displayRenderRange();\n      });\n\n      displayEvents();\n      displayRenderRange();\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/calendar/examples/14-template-weekly.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\"\n    />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n    <title>TOAST UI Calendar Example - Template for Weekly View</title>\n    <link rel=\"stylesheet\" href=\"../dist/toastui-calendar.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/reset.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/app.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/icons.css\" />\n    <style>\n      .navbar {\n        padding: 0;\n      }\n    </style>\n  </head>\n  <body k>\n    <div class=\"app-container code-html\">\n      <header class=\"header\">\n        <nav class=\"navbar\">\n          <button class=\"button is-rounded today\">Today</button>\n          <button class=\"button is-rounded prev\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-left.png\"\n              srcset=\"./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x\"\n            />\n          </button>\n          <button class=\"button is-rounded next\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-right.png\"\n              srcset=\"\n                ./images/ic-arrow-line-right@2x.png 2x,\n                ./images/ic-arrow-line-right@3x.png 3x\n              \"\n            />\n          </button>\n          <span class=\"navbar--range\"></span>\n        </nav>\n      </header>\n      <main id=\"app\"></main>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js\"></script>\n    <script src=\"../dist/toastui-calendar.ie11.min.js\"></script>\n    <script src=\"./scripts/mock-data.js\"></script>\n    <script src=\"./scripts/utils.js\"></script>\n    <script type=\"text/javascript\" class=\"code-js\">\n      var Calendar = window.tui.Calendar;\n\n      var cal = new Calendar('#app', {\n        defaultView: 'week',\n        calendars: MOCK_CALENDARS,\n        timezone: {\n          zones: [\n            {\n              timezoneName: 'Asia/Seoul',\n              displayLabel: 'UTC+09',\n              tooltip: 'Seoul',\n            },\n            {\n              timezoneName: 'Europe/London',\n              tooltip: 'London',\n            },\n          ],\n        },\n        theme: {\n          week: {\n            dayGridLeft: {\n              width: '8rem',\n            },\n            timeGridLeft: {\n              width: '8rem',\n            },\n          },\n        },\n        template: {\n          milestone: function (event) {\n            return '<span class=\"calendar-event-milestone\">' + '⛳ ' + event.title + '</span>';\n          },\n          milestoneTitle: function () {\n            return '<strong>Milestones</strong>';\n          },\n          task: function (event) {\n            return '<span class=\"calendar-event-task\">' + '✅ ' + event.title + '</span>';\n          },\n          taskTitle: function () {\n            return '<strong>Tasks</strong>';\n          },\n          allday: function (event) {\n            return '<span class=\"calendar-event-allday\">' + event.title + '</span>';\n          },\n          alldayTitle: function () {\n            return '<strong>All day events</strong>';\n          },\n          time: function (event) {\n            return (\n              '<span class=\"calendar-event-time\" style=\"color: #222;\">' + event.title + '</span>'\n            );\n          },\n          goingDuration: function (event) {\n            return (\n              '<span class=\"calendar-event-going-duration\" style=\"color: #222;\">' +\n              event.goingDuration +\n              '</span>'\n            );\n          },\n          comingDuration: function (event) {\n            return (\n              '<span class=\"calendar-event-coming-duration\" style=\"color: #222;\">' +\n              event.comingDuration +\n              '</span>'\n            );\n          },\n          weekDayName: function (data) {\n            var date = data.dateInstance.toDate();\n\n            return (\n              '<span class=\"calendar-event-weekday-name\">' +\n              moment(date).format('MM-DD') +\n              '</span>'\n            );\n          },\n          weekGridFooterExceed: function (hiddenEvents) {\n            return (\n              '<span class=\"calendar-event-weekgrid-footer-exceed\">+' + hiddenEvents + '</span>'\n            );\n          },\n          collapseBtnTitle: function () {\n            return '⬆️';\n          },\n          timezoneDisplayLabel: function (data) {\n            if (data.displayLabel) {\n              return data.displayLabel;\n            }\n\n            return String(\n              data.timezoneOffset > 0 ? '+' + data.timezoneOffset : data.timezoneOffset\n            );\n          },\n          timegridNowIndicatorLabel: function (data) {\n            return 'Now: ' + moment(data.time.toDate()).format('hh:mm');\n          },\n        },\n      });\n    </script>\n    <script type=\"text/javascript\">\n      var todayButton = $('.today');\n      var prevButton = $('.prev');\n      var nextButton = $('.next');\n      var range = $('.navbar--range');\n\n      function displayEvents() {\n        var events = generateRandomEvents(\n          cal.getViewName(),\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd()\n        );\n        cal.clear();\n        cal.createEvents(events);\n      }\n\n      function displayRenderRange() {\n        var viewName = cal.getViewName();\n\n        range.textContent = getNavbarRange(\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd(),\n          viewName\n        );\n      }\n\n      todayButton.addEventListener('click', function () {\n        cal.today();\n        displayEvents();\n        displayRenderRange();\n      });\n      prevButton.addEventListener('click', function () {\n        cal.prev();\n        displayEvents();\n        displayRenderRange();\n      });\n      nextButton.addEventListener('click', function () {\n        cal.next();\n        displayEvents();\n        displayRenderRange();\n      });\n\n      displayEvents();\n      displayRenderRange();\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/calendar/examples/15-template-popup.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\"\n    />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n    <title>TOAST UI Calendar Example - Theme for Popup</title>\n    <link\n      rel=\"stylesheet\"\n      href=\"https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.min.css\"\n    />\n    <link\n      rel=\"stylesheet\"\n      href=\"https://uicdn.toast.com/tui.date-picker/latest/tui-date-picker.min.css\"\n    />\n    <link rel=\"stylesheet\" href=\"../dist/toastui-calendar.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/reset.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/app.css\" />\n    <link rel=\"stylesheet\" href=\"./styles/icons.css\" />\n    <style>\n      .navbar {\n        padding: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"app-container code-html\">\n      <header class=\"header\">\n        <nav class=\"navbar\">\n          <button class=\"button is-rounded today\">Today</button>\n          <button class=\"button is-rounded prev\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-left.png\"\n              srcset=\"./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x\"\n            />\n          </button>\n          <button class=\"button is-rounded next\">\n            <img\n              alt=\"prev\"\n              src=\"./images/ic-arrow-line-right.png\"\n              srcset=\"\n                ./images/ic-arrow-line-right@2x.png 2x,\n                ./images/ic-arrow-line-right@3x.png 3x\n              \"\n            />\n          </button>\n          <span class=\"navbar--range\"></span>\n        </nav>\n      </header>\n      <main id=\"app\"></main>\n    </div>\n\n    <script src=\"https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.min.js\"></script>\n    <script src=\"https://uicdn.toast.com/tui.date-picker/latest/tui-date-picker.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js\"></script>\n    <script src=\"../dist/toastui-calendar.ie11.min.js\"></script>\n    <script src=\"./scripts/mock-data.js\"></script>\n    <script src=\"./scripts/utils.js\"></script>\n    <script type=\"text/javascript\" class=\"code-js\">\n      var Calendar = window.tui.Calendar;\n\n      var cal = new Calendar('#app', {\n        defaultView: 'month',\n        calendars: MOCK_CALENDARS,\n        useFormPopup: true,\n        useDetailPopup: true,\n        template: {\n          popupIsAllday: function () {\n            return 'All day?';\n          },\n          popupStateFree: function () {\n            return '🏝️ Free';\n          },\n          popupStateBusy: function () {\n            return '🔥 Busy';\n          },\n          titlePlaceholder: function () {\n            return 'Enter title';\n          },\n          locationPlaceholder: function () {\n            return 'Enter location';\n          },\n          startDatePlaceholder: function () {\n            return 'Start date';\n          },\n          endDatePlaceholder: function () {\n            return 'End date';\n          },\n          popupSave: function () {\n            return 'Add Event';\n          },\n          popupUpdate: function () {\n            return 'Update Event';\n          },\n          popupEdit: function () {\n            return 'Modify';\n          },\n          popupDelete: function () {\n            return 'Remove';\n          },\n          popupDetailTitle: function (data) {\n            return 'Detail of ' + data.title;\n          },\n        },\n      });\n    </script>\n    <script type=\"text/javascript\">\n      var todayButton = $('.today');\n      var prevButton = $('.prev');\n      var nextButton = $('.next');\n      var range = $('.navbar--range');\n      function displayEvents() {\n        var events = generateRandomEvents(\n          cal.getViewName(),\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd()\n        );\n        cal.clear();\n        cal.createEvents(events);\n      }\n\n      function displayRenderRange() {\n        var viewName = cal.getViewName();\n\n        range.textContent = getNavbarRange(\n          cal.getDateRangeStart(),\n          cal.getDateRangeEnd(),\n          viewName\n        );\n      }\n\n      todayButton.addEventListener('click', function () {\n        cal.today();\n        displayEvents();\n        displayRenderRange();\n      });\n      prevButton.addEventListener('click', function () {\n        cal.prev();\n        displayEvents();\n        displayRenderRange();\n      });\n      nextButton.addEventListener('click', function () {\n        cal.next();\n        displayEvents();\n        displayRenderRange();\n      });\n\n      displayEvents();\n      displayRenderRange();\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/calendar/examples/scripts/app.js",
    "content": "/* eslint-disable no-var,prefer-destructuring,prefer-template,no-undef,object-shorthand,no-console */\n// for testing IE11 compatibility, this file doesn't use ES6 syntax.\n(function (Calendar) {\n  var cal;\n  // Constants\n  var CALENDAR_CSS_PREFIX = 'toastui-calendar-';\n  var cls = function (className) {\n    return CALENDAR_CSS_PREFIX + className;\n  };\n\n  // Elements\n  var navbarRange = $('.navbar--range');\n  var prevButton = $('.prev');\n  var nextButton = $('.next');\n  var todayButton = $('.today');\n  var dropdown = $('.dropdown');\n  var dropdownTrigger = $('.dropdown-trigger');\n  var dropdownTriggerIcon = $('.dropdown-icon');\n  var dropdownContent = $('.dropdown-content');\n  var checkboxCollapse = $('.checkbox-collapse');\n  var sidebar = $('.sidebar');\n\n  // App State\n  var appState = {\n    activeCalendarIds: MOCK_CALENDARS.map(function (calendar) {\n      return calendar.id;\n    }),\n    isDropdownActive: false,\n  };\n\n  // functions to handle calendar behaviors\n  function reloadEvents() {\n    var randomEvents;\n\n    cal.clear();\n    randomEvents = generateRandomEvents(\n      cal.getViewName(),\n      cal.getDateRangeStart(),\n      cal.getDateRangeEnd()\n    );\n    cal.createEvents(randomEvents);\n  }\n\n  function getReadableViewName(viewType) {\n    switch (viewType) {\n      case 'month':\n        return 'Monthly';\n      case 'week':\n        return 'Weekly';\n      case 'day':\n        return 'Daily';\n      default:\n        throw new Error('no view type');\n    }\n  }\n\n  function displayRenderRange() {\n    var rangeStart = cal.getDateRangeStart();\n    var rangeEnd = cal.getDateRangeEnd();\n\n    navbarRange.textContent = getNavbarRange(rangeStart, rangeEnd, cal.getViewName());\n  }\n\n  function setDropdownTriggerText() {\n    var viewName = cal.getViewName();\n    var buttonText = $('.dropdown .button-text');\n    buttonText.textContent = getReadableViewName(viewName);\n  }\n\n  function toggleDropdownState() {\n    appState.isDropdownActive = !appState.isDropdownActive;\n    dropdown.classList.toggle('is-active', appState.isDropdownActive);\n    dropdownTriggerIcon.classList.toggle(cls('open'), appState.isDropdownActive);\n  }\n\n  function setAllCheckboxes(checked) {\n    var checkboxes = $$('.sidebar-item > input[type=\"checkbox\"]');\n\n    checkboxes.forEach(function (checkbox) {\n      checkbox.checked = checked;\n      setCheckboxBackgroundColor(checkbox);\n    });\n  }\n\n  function setCheckboxBackgroundColor(checkbox) {\n    var calendarId = checkbox.value;\n    var label = checkbox.nextElementSibling;\n    var calendarInfo = MOCK_CALENDARS.find(function (calendar) {\n      return calendar.id === calendarId;\n    });\n\n    if (!calendarInfo) {\n      calendarInfo = {\n        backgroundColor: '#2a4fa7',\n      };\n    }\n\n    label.style.setProperty(\n      '--checkbox-' + calendarId,\n      checkbox.checked ? calendarInfo.backgroundColor : '#fff'\n    );\n  }\n\n  function update() {\n    setDropdownTriggerText();\n    displayRenderRange();\n    reloadEvents();\n  }\n\n  function bindAppEvents() {\n    dropdownTrigger.addEventListener('click', toggleDropdownState);\n\n    prevButton.addEventListener('click', function () {\n      cal.prev();\n      update();\n    });\n\n    nextButton.addEventListener('click', function () {\n      cal.next();\n      update();\n    });\n\n    todayButton.addEventListener('click', function () {\n      cal.today();\n      update();\n    });\n\n    dropdownContent.addEventListener('click', function (e) {\n      var targetViewName;\n\n      if ('viewName' in e.target.dataset) {\n        targetViewName = e.target.dataset.viewName;\n        cal.changeView(targetViewName);\n        checkboxCollapse.disabled = targetViewName === 'month';\n        toggleDropdownState();\n        update();\n      }\n    });\n\n    checkboxCollapse.addEventListener('change', function (e) {\n      if ('checked' in e.target) {\n        cal.setOptions({\n          week: {\n            collapseDuplicateEvents: !!e.target.checked,\n          },\n          useDetailPopup: !e.target.checked,\n        });\n      }\n    });\n\n    sidebar.addEventListener('click', function (e) {\n      if ('value' in e.target) {\n        if (e.target.value === 'all') {\n          if (appState.activeCalendarIds.length > 0) {\n            cal.setCalendarVisibility(appState.activeCalendarIds, false);\n            appState.activeCalendarIds = [];\n            setAllCheckboxes(false);\n          } else {\n            appState.activeCalendarIds = MOCK_CALENDARS.map(function (calendar) {\n              return calendar.id;\n            });\n            cal.setCalendarVisibility(appState.activeCalendarIds, true);\n            setAllCheckboxes(true);\n          }\n        } else if (appState.activeCalendarIds.indexOf(e.target.value) > -1) {\n          appState.activeCalendarIds.splice(appState.activeCalendarIds.indexOf(e.target.value), 1);\n          cal.setCalendarVisibility(e.target.value, false);\n          setCheckboxBackgroundColor(e.target);\n        } else {\n          appState.activeCalendarIds.push(e.target.value);\n          cal.setCalendarVisibility(e.target.value, true);\n          setCheckboxBackgroundColor(e.target);\n        }\n      }\n    });\n  }\n\n  function bindInstanceEvents() {\n    cal.on({\n      clickMoreEventsBtn: function (btnInfo) {\n        console.log('clickMoreEventsBtn', btnInfo);\n      },\n      clickEvent: function (eventInfo) {\n        console.log('clickEvent', eventInfo);\n      },\n      clickDayName: function (dayNameInfo) {\n        console.log('clickDayName', dayNameInfo);\n      },\n      selectDateTime: function (dateTimeInfo) {\n        console.log('selectDateTime', dateTimeInfo);\n      },\n      beforeCreateEvent: function (event) {\n        console.log('beforeCreateEvent', event);\n        event.id = chance.guid();\n\n        cal.createEvents([event]);\n        cal.clearGridSelections();\n      },\n      beforeUpdateEvent: function (eventInfo) {\n        var event, changes;\n\n        console.log('beforeUpdateEvent', eventInfo);\n\n        event = eventInfo.event;\n        changes = eventInfo.changes;\n\n        cal.updateEvent(event.id, event.calendarId, changes);\n      },\n      beforeDeleteEvent: function (eventInfo) {\n        console.log('beforeDeleteEvent', eventInfo);\n\n        cal.deleteEvent(eventInfo.id, eventInfo.calendarId);\n      },\n    });\n  }\n\n  function initCheckbox() {\n    var checkboxes = $$('input[type=\"checkbox\"]');\n\n    checkboxes.forEach(function (checkbox) {\n      setCheckboxBackgroundColor(checkbox);\n    });\n  }\n\n  function getEventTemplate(event, isAllday) {\n    var html = [];\n    var start = moment(event.start.toDate().toUTCString());\n    if (!isAllday) {\n      html.push('<strong>' + start.format('HH:mm') + '</strong> ');\n    }\n\n    if (event.isPrivate) {\n      html.push('<span class=\"calendar-font-icon ic-lock-b\"></span>');\n      html.push(' Private');\n    } else {\n      if (event.recurrenceRule) {\n        html.push('<span class=\"calendar-font-icon ic-repeat-b\"></span>');\n      } else if (event.attendees.length > 0) {\n        html.push('<span class=\"calendar-font-icon ic-user-b\"></span>');\n      } else if (event.location) {\n        html.push('<span class=\"calendar-font-icon ic-location-b\"></span>');\n      }\n      html.push(' ' + event.title);\n    }\n\n    return html.join('');\n  }\n\n  // Calendar instance with options\n  // eslint-disable-next-line no-undef\n  cal = new Calendar('#app', {\n    calendars: MOCK_CALENDARS,\n    useFormPopup: true,\n    useDetailPopup: true,\n    eventFilter: function (event) {\n      var currentView = cal.getViewName();\n      if (currentView === 'month') {\n        return ['allday', 'time'].includes(event.category) && event.isVisible;\n      }\n\n      return event.isVisible;\n    },\n    template: {\n      allday: function (event) {\n        return getEventTemplate(event, true);\n      },\n      time: function (event) {\n        return getEventTemplate(event, false);\n      },\n    },\n  });\n\n  // Init\n  bindInstanceEvents();\n  bindAppEvents();\n  initCheckbox();\n  update();\n})(tui.Calendar);\n"
  },
  {
    "path": "apps/calendar/examples/scripts/mock-data.js",
    "content": "/* eslint-disable */\nvar MOCK_CALENDARS = [\n  {\n    id: '1',\n    name: 'My Calendar',\n    color: '#ffffff',\n    borderColor: '#9e5fff',\n    backgroundColor: '#9e5fff',\n    dragBackgroundColor: '#9e5fff',\n  },\n  {\n    id: '2',\n    name: 'Work',\n    color: '#ffffff',\n    borderColor: '#00a9ff',\n    backgroundColor: '#00a9ff',\n    dragBackgroundColor: '#00a9ff',\n  },\n  {\n    id: '3',\n    name: 'Family',\n    color: '#ffffff',\n    borderColor: '#DB473F',\n    backgroundColor: '#DB473F',\n    dragBackgroundColor: '#DB473F',\n  },\n  {\n    id: '4',\n    name: 'Friends',\n    color: '#ffffff',\n    borderColor: '#03bd9e',\n    backgroundColor: '#03bd9e',\n    dragBackgroundColor: '#03bd9e',\n  },\n  {\n    id: '5',\n    name: 'Travel',\n    color: '#ffffff',\n    borderColor: '#bbdc00',\n    backgroundColor: '#bbdc00',\n    dragBackgroundColor: '#bbdc00',\n  },\n];\n\nvar EVENT_CATEGORIES = ['milestone', 'task'];\n\nfunction generateRandomEvent(calendar, renderStart, renderEnd) {\n  function generateTime(event, renderStart, renderEnd) {\n    var startDate = moment(renderStart.getTime());\n    var endDate = moment(renderEnd.getTime());\n    var diffDate = endDate.diff(startDate, 'days');\n\n    event.isAllday = chance.bool({ likelihood: 30 });\n    if (event.isAllday) {\n      event.category = 'allday';\n    } else if (chance.bool({ likelihood: 30 })) {\n      event.category = EVENT_CATEGORIES[chance.integer({ min: 0, max: 1 })];\n      if (event.category === EVENT_CATEGORIES[1]) {\n        event.dueDateClass = 'morning';\n      }\n    } else {\n      event.category = 'time';\n    }\n\n    startDate.add(chance.integer({ min: 0, max: diffDate }), 'days');\n    startDate.hours(chance.integer({ min: 0, max: 23 }));\n    startDate.minutes(chance.bool() ? 0 : 30);\n    event.start = startDate.toDate();\n\n    endDate = moment(startDate);\n    if (event.isAllday) {\n      endDate.add(chance.integer({ min: 0, max: 3 }), 'days');\n    }\n\n    event.end = endDate.add(chance.integer({ min: 1, max: 4 }), 'hour').toDate();\n\n    if (!event.isAllday && chance.bool({ likelihood: 20 })) {\n      event.goingDuration = chance.integer({ min: 30, max: 120 });\n      event.comingDuration = chance.integer({ min: 30, max: 120 });\n\n      if (chance.bool({ likelihood: 50 })) {\n        event.end = event.start;\n      }\n    }\n  }\n\n  function generateNames() {\n    var names = [];\n    var i = 0;\n    var length = chance.integer({ min: 1, max: 10 });\n\n    for (; i < length; i += 1) {\n      names.push(chance.name());\n    }\n\n    return names;\n  }\n\n  var id = chance.guid();\n  var calendarId = calendar.id;\n  var title = chance.sentence({ words: 3 });\n  var body = chance.bool({ likelihood: 20 }) ? chance.sentence({ words: 10 }) : '';\n  var isReadOnly = chance.bool({ likelihood: 20 });\n  var isPrivate = chance.bool({ likelihood: 20 });\n  var location = chance.address();\n  var attendees = chance.bool({ likelihood: 70 }) ? generateNames() : [];\n  var recurrenceRule = '';\n  var state = chance.bool({ likelihood: 50 }) ? 'Busy' : 'Free';\n  var goingDuration = chance.bool({likelihood: 20}) ? chance.integer({ min: 30, max: 120 }) : 0;\n  var comingDuration = chance.bool({likelihood: 20}) ? chance.integer({ min: 30, max: 120 }) : 0;\n  var raw = {\n    memo: chance.sentence(),\n    creator: {\n      name: chance.name(),\n      avatar: chance.avatar(),\n      email: chance.email(),\n      phone: chance.phone(),\n    },\n  };\n\n  var event = {\n    id: id,\n    calendarId: calendarId,\n    title: title,\n    body: body,\n    isReadOnly: isReadOnly,\n    isPrivate: isPrivate,\n    location: location,\n    attendees: attendees,\n    recurrenceRule: recurrenceRule,\n    state: state,\n    goingDuration: goingDuration,\n    comingDuration: comingDuration,\n    raw: raw,\n  }\n\n  generateTime(event, renderStart, renderEnd);\n\n  if (event.category === 'milestone') {\n    event.color = '#000'\n    event.backgroundColor = 'transparent';\n    event.borderColor = 'transparent';\n    event.dragBackgroundColor = 'transparent';\n  }\n\n  return event;\n}\n\nfunction generateRandomEvents(viewName, renderStart, renderEnd) {\n  var i, j;\n  var event, duplicateEvent;\n  var events = [];\n\n  MOCK_CALENDARS.forEach(function(calendar) {\n    for (i = 0; i < chance.integer({ min: 20, max: 50 }); i += 1) {\n      event = generateRandomEvent(calendar, renderStart, renderEnd);\n      events.push(event);\n\n      if (i % 5 === 0) {\n        for (j = 0; j < chance.integer({min: 0, max: 2}); j+= 1) {\n          duplicateEvent = JSON.parse(JSON.stringify(event));\n          duplicateEvent.id += `-${j}`;\n          duplicateEvent.calendarId = chance.integer({min: 1, max: 5}).toString();\n          duplicateEvent.goingDuration = 30 * chance.integer({min: 0, max: 4});\n          duplicateEvent.comingDuration = 30 * chance.integer({min: 0, max: 4});\n          events.push(duplicateEvent);\n        }\n      }\n    }\n  });\n\n  return events;\n}\n"
  },
  {
    "path": "apps/calendar/examples/scripts/utils.js",
    "content": "/* eslint-disable no-var,prefer-template,no-undef */\nvar $ = function (selector) {\n  return document.querySelector(selector);\n};\n\nvar $$ = function (selector) {\n  return Array.prototype.slice.call(document.querySelectorAll(selector));\n};\n\nfunction getNavbarRange(tzStart, tzEnd, viewType) {\n  var start = tzStart.toDate();\n  var end = tzEnd.toDate();\n  var middle;\n  if (viewType === 'month') {\n    middle = new Date(start.getTime() + (end.getTime() - start.getTime()) / 2);\n\n    return moment(middle).format('YYYY-MM');\n  }\n  if (viewType === 'day') {\n    return moment(start).format('YYYY-MM-DD');\n  }\n  if (viewType === 'week') {\n    return moment(start).format('YYYY-MM-DD') + ' ~ ' + moment(end).format('YYYY-MM-DD');\n  }\n  throw new Error('no view type');\n}\n"
  },
  {
    "path": "apps/calendar/examples/styles/app.css",
    "content": "@import \"https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css\";\n\n.header {\n    padding: 1rem;\n    border-bottom: 1px solid #bbb;\n}\n\n.app-container {\n    display: flex;\n    flex-direction: column;\n    align-items: stretch;\n    height: 100%;\n}\n\n.content {\n    display: flex;\n    height: 100%;\n}\n\n.sidebar {\n    display: flex;\n    flex: 0 1 12rem;\n    flex-direction: column;\n    padding: 1.25rem;\n    background-color: #fafafa;\n    border-right: 1px solid #d5d5d5;\n}\n\n.sidebar hr {\n    margin: 1rem 0;\n}\n\n.sidebar-item + .sidebar-item {\n    margin-top: 0.75rem;\n}\n\n.sidebar .app-footer {\n    margin-top: auto;\n    font-size: 0.75rem;\n}\n\n.app-column {\n    display: flex;\n    flex-direction: column;\n    flex: 1 0 auto;\n}\n\n.app-column nav {\n    flex: 0 1 4rem;\n    border-bottom: 1px solid #e5e5e5;\n}\n\n#app {\n    flex: 1 0 auto;\n}\n\n.navbar {\n    display: flex;\n    align-items: center;\n    padding: 1rem;\n}\n\n.navbar .dropdown {\n    margin-right: 1rem;\n}\n\n.navbar .dropdown .toastui-calendar-icon {\n    margin-left: 0.5rem;\n}\n\n.button.prev, .button.next {\n    padding: 0.8rem;\n}\n\n.navbar .button + .button {\n    margin-left: 0.25rem;\n}\n\n.navbar .navbar--range {\n    margin-left: 1rem;\n    font-size: 1.25rem;\n}\n\n.navbar .nav-checkbox {\n    margin-left: auto;\n}\n\ninput:disabled + label {\n    color: #ccc;\n    cursor: not-allowed;\n}\n\n.toastui-calendar-template-time strong {\n    color: inherit;\n}\n\n.sidebar-item input[type=\"checkbox\"]:not(.checkbox-all) {\n    visibility: hidden;\n}\n\n.checkbox {\n    position: relative;\n}\n\n.checkbox-calendar::before {\n    content: \"\";\n    position: absolute;\n    left: -1.5rem;\n    width: 1.25rem;\n    height: 1.25rem;\n    border-radius: 50%;\n    border: 1px solid #ddd;\n}\n\n.checkbox.checkbox-1::before {\n    background-color: var(--checkbox-1);\n}\n\n.checkbox.checkbox-2::before {\n    background-color: var(--checkbox-2);\n}\n\n.checkbox.checkbox-3::before {\n    background-color: var(--checkbox-3);\n}\n\n.checkbox.checkbox-4::before {\n    background-color: var(--checkbox-4);\n}\n\n.checkbox.checkbox-5::before {\n    background-color: var(--checkbox-5);\n}\n"
  },
  {
    "path": "apps/calendar/examples/styles/icons.css",
    "content": "/* font icons */\n@font-face {\n    font-family: 'tui-calendar-font-icon';\n    src: url('../fonts/icon.eot') format('embedded-opentype'),\n         url('../fonts/icon.ttf') format('truetype'),\n         url('../fonts/icon.woff') format('woff'),\n         url('../fonts/icon.svg') format('svg');\n}\n\n.calendar-icon {\n  width: 14px;\n  height: 14px;\n  display: inline-block;\n  vertical-align: middle;\n}\n\n.calendar-font-icon {\n  font-family: 'tui-calendar-font-icon', sans-serif;\n  font-size: 10px;\n  font-weight: normal;\n}\n\n.img-bi {\n  background: url('../images/img-bi.png') no-repeat;\n  width: 215px;\n  height: 16px;\n}\n\n.ic_view_month {\n  background: url('../images/ic-view-month.png') no-repeat;\n}\n\n.ic_view_week {\n  background: url('../images/ic-view-week.png') no-repeat;\n}\n\n.ic_view_day {\n  background: url('../images/ic-view-day.png') no-repeat;\n}\n\n.ic-arrow-line-left {\n  background: url('../images/ic-arrow-line-left.png') no-repeat;\n}\n\n.ic-arrow-line-right {\n  background: url('../images/ic-arrow-line-right.png') no-repeat;\n}\n\n.ic-travel-time {\n  background: url('../images/ic-traveltime-w.png') no-repeat;\n}\n\n/* font icons */\n.ic-location-b:before {\n  content: '\\e900';\n}\n\n.ic-lock-b:before {\n  content: '\\e901';\n}\n\n.ic-milestone-b:before {\n  content: '\\e902';\n}\n\n.ic-readonly-b:before {\n  content: '\\e903';\n}\n\n.ic-repeat-b:before {\n  content: '\\e904';\n}\n\n.ic-state-b:before {\n  content: '\\e905';\n}\n\n.ic-user-b:before {\n  content: '\\e906';\n}\n"
  },
  {
    "path": "apps/calendar/examples/styles/reset.css",
    "content": "@import url(https://fonts.googleapis.com/css?family=Noto+Sans);\n\n*, *::before, *::after {\n    box-sizing: border-box;\n}\n\n* {\n    margin: 0;\n}\n\nhtml, body {\n    height: 100%;\n}\n\nbody {\n    line-height: 1.5;\n    -webkit-font-smoothing: antialiased;\n    font-family: 'Noto Sans', sans-serif;\n}\n\nimg, picture, video, canvas, svg {\n    display: block;\n    max-width: 100%;\n}\n\ninput, button, textarea, select {\n    font: inherit;\n}\n\np, h1, h2, h3, h4, h5, h6 {\n    overflow-wrap: break-word;\n}\n"
  },
  {
    "path": "apps/calendar/jest.config.js",
    "content": "module.exports = {\n  testEnvironment: 'jsdom',\n  clearMocks: true,\n  preset: 'ts-jest',\n  moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node', 'd.ts'],\n  moduleNameMapper: {\n    '\\\\.(css)$': '<rootDir>/src/test/cssFileMock.ts',\n    '^@src/(.*)$': '<rootDir>/src/$1',\n    '^@stories/(.*)$': '<rootDir>/stories/$1',\n  },\n  globals: { 'ts-jest': { tsconfig: '<rootDir>/tsconfig.test.json' } },\n  watchPathIgnorePatterns: ['<rootDir>/.storybook', '<rootDir>/.stories', '/node_modules/'],\n  testPathIgnorePatterns: ['/node_modules/', '/dist/', '/playwright/'],\n  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts', '<rootDir>/src/test/matchers.ts'],\n};\n"
  },
  {
    "path": "apps/calendar/jsdoc.conf.json",
    "content": "{\n  \"source\": {\n    \"include\": [\n      \"src/js/factory/calendar.js\",\n      \"src/js/theme/themeConfig.js\",\n      \"src/js/common/timezone.js\",\n      \"README.md\"\n    ],\n    \"exclude\": [],\n    \"includePattern\": \".+\\\\.js(doc)?$\",\n    \"excludePattern\": \"(^|\\\\/|\\\\\\\\)_\"\n  },\n  \"plugins\": [\n    \"plugins/markdown\"\n  ],\n  \"templates\": {\n    \"name\": \"Calendar\",\n    \"logo\": {\n      \"url\": \"https://cloud.githubusercontent.com/assets/12269563/20029815/01133928-a39a-11e6-80f3-12500a91c755.png\",\n      \"width\": \"150px\",\n      \"height\": \"13px\",\n      \"link\": \"https://github.com/nhn/tui.jsdoc-template\"\n    }\n  },\n  \"opts\": {\n    \"private\": false,\n    \"recurse\": true,\n    \"destination\": \"doc\",\n    \"tutorials\": \"examples\",\n    \"template\": \"../../node_modules/tui-jsdoc-template\",\n    \"package\": \"package.json\"\n  }\n}\n"
  },
  {
    "path": "apps/calendar/package.json",
    "content": "{\n  \"name\": \"@toast-ui/calendar\",\n  \"author\": \"NHN Cloud FE Development Lab <dl_javascript@nhn.com>\",\n  \"version\": \"2.1.3\",\n  \"main\": \"./dist/toastui-calendar.js\",\n  \"types\": \"./types/index.d.ts\",\n  \"sideEffects\": [\n    \"*.css\"\n  ],\n  \"module\": \"./dist/toastui-calendar.mjs\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/toastui-calendar.mjs\",\n      \"require\": \"./dist/toastui-calendar.js\"\n    },\n    \"./ie11\": \"./dist/toastui-calendar.ie11.js\",\n    \"./esm\": \"./dist/toastui-calendar.mjs\",\n    \"./toastui-calendar.css\": \"./dist/toastui-calendar.css\",\n    \"./toastui-calendar.min.css\": \"./dist/toastui-calendar.min.css\",\n    \"./dist/*\": \"./dist/*\"\n  },\n  \"typesVersions\": {\n    \"*\": {\n      \"*\": [\n        \"./types/index.d.ts\"\n      ]\n    }\n  },\n  \"license\": \"MIT\",\n  \"description\": \"TOAST UI Calendar\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/nhn/tui.calendar.git\"\n  },\n  \"keywords\": [\n    \"nhn\",\n    \"toast\",\n    \"toastui\",\n    \"toast-ui\",\n    \"calendar\",\n    \"fullcalendar\",\n    \"daily\",\n    \"weekly\",\n    \"monthly\",\n    \"business week\",\n    \"milestone\",\n    \"task\",\n    \"allday\"\n  ],\n  \"files\": [\n    \"dist\",\n    \"types/index.d.ts\",\n    \"types/factory\",\n    \"types/time/date.d.ts\",\n    \"types/types/@(events|options|template|theme|eventBus).d.ts\"\n  ],\n  \"dependencies\": {\n    \"immer\": \"^9.0.15\",\n    \"isomorphic-dompurify\": \"^0.20.0\",\n    \"preact\": \"^10.10.0\",\n    \"preact-render-to-string\": \"^5.2.1\",\n    \"tui-date-picker\": \"^4.0.1\",\n    \"tui-time-picker\": \"^2.0.1\"\n  },\n  \"devDependencies\": {\n    \"@storybook/addons\": \"^6.5.9\",\n    \"@storybook/builder-webpack5\": \"^6.5.9\",\n    \"@storybook/core\": \"^6.5.9\",\n    \"@storybook/manager-webpack5\": \"^6.5.9\",\n    \"@storybook/preact\": \"^6.5.9\",\n    \"@storybook/theming\": \"^6.5.9\",\n    \"@types/chance\": \"^1.1.3\",\n    \"chance\": \"^1.1.8\",\n    \"css-loader\": \"^6.7.1\",\n    \"css-minimizer-webpack-plugin\": \"^3.4.1\",\n    \"eslint-webpack-plugin\": \"^3.2.0\",\n    \"postcss\": \"^8.4.14\",\n    \"postcss-loader\": \"^6.2.1\",\n    \"postcss-prefixer\": \"^2.1.3\",\n    \"storybook\": \"^6.5.9\",\n    \"style-loader\": \"^3.3.1\",\n    \"stylelint\": \"^14.9.1\",\n    \"stylelint-config-recommended\": \"^8.0.0\",\n    \"stylelint-webpack-plugin\": \"^3.3.0\",\n    \"terser-webpack-plugin\": \"^5.3.3\",\n    \"webpack-bundle-analyzer\": \"^4.5.0\",\n    \"webpack-inject-plugin\": \"^1.5.5\"\n  },\n  \"scripts\": {\n    \"check-types\": \"tsc -p ./tsconfig.json --noEmit\",\n    \"lint\": \"npm run check-types && eslint .\",\n    \"release-note\": \"tuie\",\n    \"build\": \"rimraf dist/ && concurrently 'npm:build:*'\",\n    \"build:modern\": \"webpack --config webpack.config.js && webpack --config webpack.config.js --env minify\",\n    \"build:ie11\": \"webpack --config webpack.config.js --env ie11 && webpack --config webpack.config.js --env minify ie11\",\n    \"build:esm\": \"vite build\",\n    \"build:types\": \"rimraf types/ && tsc -p ./tsconfig.declaration.json\",\n    \"analyze\": \"webpack --config webpack.config.js --env --profile --json > stats.json && webpack-bundle-analyzer stats.json ./dist\",\n    \"develop\": \"npm run storybook\",\n    \"storybook\": \"start-storybook -p 6006\",\n    \"storybook:build\": \"build-storybook\",\n    \"docs:prebuild\": \"npm run build && tsc --outDir tmpdoc --sourceMap false\",\n    \"docs:dev\": \"rimraf tmpdoc/ && npm run docs:prebuild && source ~/.nvm/nvm.sh && nvm use 10 && tuidoc --serv\",\n    \"docs:build\": \"rimraf tmpdoc/ && npm run docs:prebuild && source ~/.nvm/nvm.sh && nvm use 10 && tuidoc\",\n    \"publish:cdn\": \"node scripts/publishToCDN.js\",\n    \"update:wrapper\": \"node scripts/updateWrapper.js\"\n  }\n}\n"
  },
  {
    "path": "apps/calendar/playwright/assertions.ts",
    "content": "import type { Locator, Page } from '@playwright/test';\nimport { expect } from '@playwright/test';\n\nimport type { FormattedTimeString } from '@t/time/datetime';\n\nimport type { BoundingBox } from './types';\nimport { getBoundingBox } from './utils';\n\nexport async function assertDayGridSelectionMatching(\n  page: Page,\n  startIdx: number,\n  endIdx: number,\n  cellClassName: string,\n  selectionClassName: string\n) {\n  const startCellLocator = page.locator(cellClassName).nth(startIdx);\n  const endCellLocator = page.locator(cellClassName).nth(endIdx);\n\n  const selectionLocator = page.locator(selectionClassName);\n  const selectionStartLocator = selectionLocator.first();\n  const selectionEndLocator = selectionLocator.last();\n\n  const [\n    startCellBoundingBox,\n    endCellBoundingBox,\n    selectionStartBoundingBox,\n    selectionEndBoundingBox,\n  ] = await Promise.all([\n    getBoundingBox(startCellLocator),\n    getBoundingBox(endCellLocator),\n    getBoundingBox(selectionStartLocator),\n    getBoundingBox(selectionEndLocator),\n  ]);\n\n  expect(selectionStartBoundingBox.x).toBeCloseTo(startCellBoundingBox.x, -1);\n  expect(selectionStartBoundingBox.y).toBeCloseTo(startCellBoundingBox.y, -1);\n  expect(selectionEndBoundingBox.x + selectionEndBoundingBox.width).toBeCloseTo(\n    endCellBoundingBox.x + endCellBoundingBox.width,\n    -1\n  );\n  expect(selectionEndBoundingBox.y).toBeCloseTo(endCellBoundingBox.y, -1);\n\n  const totalCellCount = endIdx - startIdx + 1;\n  const totalSelectionWidth = await selectionLocator.evaluateAll((selections) =>\n    (selections as HTMLElement[]).reduce(\n      (total, selectionRow) => selectionRow.getBoundingClientRect().width + total,\n      0\n    )\n  );\n  expect(Math.floor(totalSelectionWidth / totalCellCount)).toBeCloseTo(\n    startCellBoundingBox.width,\n    -1\n  );\n}\n\nexport async function assertAccumulatedDayGridSelectionMatching(\n  page: Page,\n  startIdx: number,\n  endIdx: number,\n  nthSelection: number,\n  isAcrossWeeks: boolean\n) {\n  const cellClassName = '.toastui-calendar-daygrid-cell';\n  const selectionClassName =\n    '.toastui-calendar-accumulated-grid-selection .toastui-calendar-grid-selection';\n\n  const startCellLocator = page.locator(cellClassName).nth(startIdx);\n  const endCellLocator = page.locator(cellClassName).nth(endIdx);\n\n  const selectionLocator = page.locator(selectionClassName);\n  const selectionStartLocator = selectionLocator.nth(nthSelection);\n  const selectionEndLocator = selectionLocator.nth(isAcrossWeeks ? nthSelection + 1 : nthSelection);\n\n  const [\n    startCellBoundingBox,\n    endCellBoundingBox,\n    selectionStartBoundingBox,\n    selectionEndBoundingBox,\n  ] = await Promise.all([\n    getBoundingBox(startCellLocator),\n    getBoundingBox(endCellLocator),\n    getBoundingBox(selectionStartLocator),\n    getBoundingBox(selectionEndLocator),\n  ]);\n\n  expect(selectionStartBoundingBox.x).toBeCloseTo(startCellBoundingBox.x, -1);\n  expect(selectionStartBoundingBox.y).toBeCloseTo(startCellBoundingBox.y, -1);\n  expect(selectionEndBoundingBox.x + selectionEndBoundingBox.width).toBeCloseTo(\n    endCellBoundingBox.x + endCellBoundingBox.width,\n    -1\n  );\n  expect(selectionEndBoundingBox.y).toBeCloseTo(endCellBoundingBox.y, -1);\n\n  const totalCellCount = endIdx - startIdx + 1;\n  const startSelectionWidth = await selectionStartLocator.evaluateAll((selections) =>\n    (selections as HTMLElement[]).reduce(\n      (total, selectionRow) => selectionRow.getBoundingClientRect().width + total,\n      0\n    )\n  );\n  const endSelectionWidth = await selectionEndLocator.evaluateAll((selections) =>\n    (selections as HTMLElement[]).reduce(\n      (total, selectionRow) => selectionRow.getBoundingClientRect().width + total,\n      0\n    )\n  );\n  const totalSelectionWidth = isAcrossWeeks\n    ? startSelectionWidth + endSelectionWidth\n    : (startSelectionWidth + endSelectionWidth) / 2;\n\n  expect(Math.floor(totalSelectionWidth / totalCellCount)).toBeCloseTo(\n    startCellBoundingBox.width,\n    -1\n  );\n}\n\nexport function assertBoundingBoxIncluded(targetBox: BoundingBox, wrappingBox: BoundingBox) {\n  expect(targetBox.x).toBeGreaterThanOrEqual(wrappingBox.x);\n  expect(targetBox.y).toBeGreaterThanOrEqual(wrappingBox.y);\n  expect(targetBox.x + targetBox.width).toBeLessThanOrEqual(wrappingBox.x + wrappingBox.width);\n  expect(targetBox.y + targetBox.height).toBeLessThanOrEqual(wrappingBox.y + wrappingBox.height);\n}\n\nexport async function assertTimeGridSelection(\n  selectionLocator: Locator,\n  expected: {\n    startTop: number;\n    endBottom: number;\n    totalElements?: number; // not used in day view tests\n    formattedTimes: FormattedTimeString[];\n  }\n) {\n  const timeGridSelectionElements = (await selectionLocator.evaluateAll(\n    (selection) => selection\n  )) as HTMLElement[];\n  const expectedFormattedTime = expected.formattedTimes.join(' - ');\n\n  if (expected.totalElements) {\n    expect(timeGridSelectionElements).toHaveLength(expected.totalElements);\n  }\n\n  await expect(selectionLocator.first()).toHaveText(expectedFormattedTime);\n\n  const firstElementBoundingBox = await getBoundingBox(selectionLocator.first());\n  expect(firstElementBoundingBox.y).toBeCloseTo(expected.startTop, 0);\n\n  const lastElementBoundingBox = await getBoundingBox(selectionLocator.last());\n  expect(lastElementBoundingBox.y + lastElementBoundingBox.height).toBeCloseTo(\n    expected.endBottom,\n    0\n  );\n}\n"
  },
  {
    "path": "apps/calendar/playwright/configs.ts",
    "content": "const PORT = process.env.CI ? 8080 : 6006;\n\nconst generatePageUrl = (viewId: string) =>\n  `http://localhost:${PORT}/iframe.html?id=${viewId}&args=&viewMode=story`;\n\nexport const DAY_VIEW_PAGE_URL = generatePageUrl('e2e-day-view--fixed-events');\n\nexport const WEEK_VIEW_PAGE_URL = generatePageUrl('e2e-week-view--fixed-events');\n\nexport const WEEK_VIEW_TIMEZONE_PAGE_URL = generatePageUrl(\n  'e2e-week-view--different-primary-timezone'\n);\n\nexport const WEEK_VIEW_DUPLICATE_EVENTS_PAGE_URL = generatePageUrl(\n  'e2e-week-view--duplicate-events'\n);\n\nexport const WEEK_VIEW_HOUR_START_OPTION_PAGE_URL = generatePageUrl(\n  'e2e-week-view--hour-start-option'\n);\n\nexport const MONTH_VIEW_EMPTY_PAGE_URL = generatePageUrl('e2e-month-view--empty');\n\nexport const MONTH_VIEW_PAGE_URL = generatePageUrl('e2e-month-view--fixed-events');\n"
  },
  {
    "path": "apps/calendar/playwright/constants.ts",
    "content": "export enum ClickDelay {\n  Immediate = 1,\n  Short = 100,\n  Long = 300,\n}\n"
  },
  {
    "path": "apps/calendar/playwright/day/timeGridEventMoving.e2e.ts",
    "content": "import { expect, test } from '@playwright/test';\nimport type { Matchers } from '@playwright/test/types/expect-types';\n\nimport type TZDate from '../../src/time/date';\nimport { addHours, isSameDate, setTimeStrToDate } from '../../src/time/datetime';\nimport type { FormattedTimeString } from '../../src/types/time/datetime';\nimport { mockDayViewEvents } from '../../stories/mocks/mockDayViewEvents';\nimport { DAY_VIEW_PAGE_URL } from '../configs';\nimport {\n  dragAndDrop,\n  getBoundingBox,\n  getGuideTimeEventSelector,\n  getTimeEventSelector,\n  getTimeGridLineSelector,\n  getTimeStrFromDate,\n  waitForSingleElement,\n} from '../utils';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(DAY_VIEW_PAGE_URL);\n});\n\nconst MOVE_EVENT_SELECTOR = '[class*=\"dragging--move-event\"]';\n\n// Every time grid events in mockDayViewEvents should include DRAG_START_TIME.\nconst DRAG_START_TIME = '04:00';\n\nconst cases: {\n  title: string;\n  step: number;\n  matcherToCompare: Extract<keyof Matchers<number>, 'toBeGreaterThan' | 'toBeLessThan'>;\n}[] = [\n  {\n    title: 'to the top',\n    step: -3, // move to 3 hours back\n    matcherToCompare: 'toBeLessThan',\n  },\n  {\n    title: 'to the bottom',\n    step: 5, // move to 5 hours later\n    matcherToCompare: 'toBeGreaterThan',\n  },\n];\n\nconst timeEvents = mockDayViewEvents.filter(({ isAllday }) => !isAllday);\nconst [, SHORT_TIME_EVENT] = timeEvents;\n\ntimeEvents.forEach(({ title: eventTitle, start, end }) => {\n  test.describe(`Move the ${eventTitle} event in the time grid`, () => {\n    cases.forEach(({ title, step }) => {\n      test(`${title}`, async ({ page }) => {\n        // Given\n        const targetEventSelector = `[data-testid*=\"time-event-${eventTitle}\"]`;\n        const eventLocator = page.locator(targetEventSelector);\n        const eventBoundingBoxBeforeMove = await getBoundingBox(eventLocator);\n\n        const dragStartRowLocator = page.locator(getTimeGridLineSelector(DRAG_START_TIME));\n        const dragStartRowBoundingBox = await getBoundingBox(dragStartRowLocator);\n\n        const targetTime = getTimeStrFromDate(\n          addHours(setTimeStrToDate(end, DRAG_START_TIME), step)\n        ) as FormattedTimeString;\n        const targetRowLocator = page.locator(getTimeGridLineSelector(targetTime));\n        const expectedStartTimeAfterMove = getTimeStrFromDate(addHours(start, step));\n\n        // When\n        await dragAndDrop({\n          page,\n          sourceLocator: eventLocator,\n          targetLocator: targetRowLocator,\n          options: {\n            sourcePosition: {\n              x: 1,\n              y: dragStartRowBoundingBox.y - eventBoundingBoxBeforeMove.y + 1,\n            },\n            targetPosition: {\n              y: 1,\n              x: 1,\n            },\n          },\n        });\n        await waitForSingleElement(eventLocator);\n\n        // Then\n        await expect\n          .poll(() => eventLocator.textContent())\n          .toMatch(new RegExp(expectedStartTimeAfterMove));\n      });\n    });\n  });\n});\n\ntest('When pressing down the ESC key, the moving event resets to the initial position.', async ({\n  page,\n}) => {\n  // Given\n  const eventLocator = page.locator(getTimeEventSelector(SHORT_TIME_EVENT.title));\n  const eventBoundingBoxBeforeMove = await getBoundingBox(eventLocator);\n\n  const targetStartTime = getTimeStrFromDate(\n    addHours(SHORT_TIME_EVENT.end as TZDate, 1)\n  ) as FormattedTimeString;\n  const targetRowLocator = page.locator(getTimeGridLineSelector(targetStartTime));\n\n  // When\n  await dragAndDrop({\n    page,\n    sourceLocator: eventLocator,\n    targetLocator: targetRowLocator,\n    hold: true,\n  });\n  await page.keyboard.down('Escape');\n\n  // Then\n  const eventBoundingBoxAfterMove = await getBoundingBox(eventLocator);\n  expect(eventBoundingBoxAfterMove).toEqual(eventBoundingBoxBeforeMove);\n});\n\ntest.describe('CSS class for a move event', () => {\n  test('should be applied depending on a dragging state.', async ({ page }) => {\n    // Given\n    const eventLocator = page.locator(getTimeEventSelector(SHORT_TIME_EVENT.title));\n    const eventBoundingBox = await getBoundingBox(eventLocator);\n    const moveEventClassLocator = page.locator(MOVE_EVENT_SELECTOR);\n\n    // When (a drag has not started yet)\n    await page.mouse.move(eventBoundingBox.x + 10, eventBoundingBox.y + 10);\n    await page.mouse.down();\n\n    // Then\n    expect(await moveEventClassLocator.count()).toBe(0);\n\n    // When (a drag is working)\n    await page.mouse.move(eventBoundingBox.x + 10, eventBoundingBox.y + 50);\n\n    // Then\n    expect(await moveEventClassLocator.count()).toBe(1);\n\n    // When (a drag is finished)\n    await page.mouse.up();\n\n    // Then\n    expect(await moveEventClassLocator.count()).toBe(0);\n  });\n\n  test('should not be applied when a drag is canceled.', async ({ page }) => {\n    // Given\n    const eventLocator = page.locator(getTimeEventSelector(SHORT_TIME_EVENT.title));\n    const moveEventClassLocator = page.locator(MOVE_EVENT_SELECTOR);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: eventLocator,\n      targetLocator: eventLocator,\n      options: {\n        targetPosition: { x: 10, y: 30 },\n      },\n      hold: true,\n    });\n    await page.keyboard.down('Escape');\n\n    // Then\n    expect(await moveEventClassLocator.count()).toBe(0);\n  });\n});\n\nconst [LONG_TIME_EVENT] = mockDayViewEvents.filter(({ title }) => title === 'long time');\n\ntest.describe(`Calibrate event's height while dragging`, () => {\n  cases.forEach(({ title, step, matcherToCompare }) => {\n    test(`${title}`, async ({ page }) => {\n      // Given\n      const targetEventSelector = `[data-testid*=\"time-event-${LONG_TIME_EVENT.title}\"]`;\n      const eventLocator = page.locator(targetEventSelector);\n      const eventBoundingBoxBeforeMove = await getBoundingBox(eventLocator);\n\n      const targetTime = getTimeStrFromDate(\n        addHours(setTimeStrToDate(LONG_TIME_EVENT.end, DRAG_START_TIME), step)\n      ) as FormattedTimeString;\n      const targetRowLocator = page.locator(getTimeGridLineSelector(targetTime));\n\n      // When\n      await dragAndDrop({\n        page,\n        sourceLocator: eventLocator,\n        targetLocator: targetRowLocator,\n        hold: true,\n      });\n\n      // Then\n      await expect\n        .poll(async () => {\n          const guideBoundingBox = await getBoundingBox(eventLocator.first());\n          return guideBoundingBox.height;\n        })\n        [matcherToCompare](eventBoundingBoxBeforeMove.height);\n    });\n  });\n});\n\nconst ONE_DAY_TIME_EVENTS = mockDayViewEvents.filter(\n  ({ isAllday, start, end }) => !isAllday && isSameDate(start, end)\n);\n\nONE_DAY_TIME_EVENTS.forEach(({ title }) => {\n  test(`The height of guide element should be same as the event element. - ${title}`, async ({\n    page,\n  }) => {\n    // Given\n    const eventLocator = page.locator(getTimeEventSelector(title));\n    const eventBoundingBox = await getBoundingBox(eventLocator);\n\n    const targetRowLocator = page.locator(getTimeGridLineSelector('02:00'));\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: eventLocator,\n      targetLocator: targetRowLocator,\n      options: {\n        sourcePosition: {\n          x: 5,\n          y: 5,\n        },\n        targetPosition: {\n          y: 5,\n          x: 5,\n        },\n      },\n      hold: true,\n    });\n\n    // Then\n    const guideLocator = page.locator(getGuideTimeEventSelector());\n    const guideBoundingBox = await getBoundingBox(guideLocator);\n    expect(guideBoundingBox.height).toBeCloseTo(eventBoundingBox.height, 0);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/playwright/day/timeGridEventResizing.e2e.ts",
    "content": "import type { Page } from '@playwright/test';\nimport { expect, test } from '@playwright/test';\nimport type { Matchers } from '@playwright/test/types/expect-types';\n\nimport type TZDate from '../../src/time/date';\nimport { addHours, addMinutes } from '../../src/time/datetime';\nimport type { FormattedTimeString } from '../../src/types/time/datetime';\nimport { mockDayViewEvents } from '../../stories/mocks/mockDayViewEvents';\nimport { DAY_VIEW_PAGE_URL } from '../configs';\nimport {\n  dragAndDrop,\n  getBoundingBox,\n  getTimeEventSelector,\n  getTimeGridLineSelector,\n  getTimeStrFromDate,\n  waitForSingleElement,\n} from '../utils';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(DAY_VIEW_PAGE_URL);\n});\n\nconst RESIZE_HANDLER_SELECTOR = '[class*=\"resize-handler\"]';\nconst RESIZE_EVENT_SELECTOR = '[class*=\"dragging--resize-vertical-event\"]';\n\nconst cases: {\n  title: string;\n  step: number;\n  matcherToCompare: Extract<keyof Matchers<number>, 'toBeGreaterThan' | 'toBeLessThan'>;\n}[] = [\n  {\n    title: 'to the top',\n    step: -1, // move the end time to 1 hour back\n    matcherToCompare: 'toBeLessThan',\n  },\n  {\n    title: 'to the bottom',\n    step: 2, // move the end time to 2 hours later\n    matcherToCompare: 'toBeGreaterThan',\n  },\n];\n\nasync function setup({\n  page,\n  targetEventTitle,\n  targetEndTime,\n}: {\n  page: Page;\n  targetEventTitle: string;\n  targetEndTime: FormattedTimeString;\n}) {\n  // Given\n  const targetEventSelector = `[data-testid*=\"time-event-${targetEventTitle}\"]`;\n  const eventLocator = page.locator(targetEventSelector);\n  const eventBoundingBoxBeforeResize = await getBoundingBox(eventLocator);\n\n  const resizeHandlerLocator = eventLocator.locator(RESIZE_HANDLER_SELECTOR);\n\n  const targetRowLocator = page.locator(getTimeGridLineSelector(targetEndTime));\n  const targetRowBoundingBox = await getBoundingBox(targetRowLocator);\n\n  // When\n  await dragAndDrop({\n    page,\n    sourceLocator: resizeHandlerLocator,\n    targetLocator: targetRowLocator,\n    options: {\n      sourcePosition: {\n        x: 1,\n        y: 1,\n      },\n      targetPosition: {\n        x: 1,\n        y: targetRowBoundingBox.height / 2,\n      },\n    },\n  });\n\n  await waitForSingleElement(eventLocator);\n  const eventBoundingBoxAfterResize = await getBoundingBox(eventLocator);\n\n  return {\n    eventLocator,\n    eventBoundingBoxBeforeResize,\n    eventBoundingBoxAfterResize,\n    targetRowBoundingBox,\n  };\n}\n\nconst timeEvents = mockDayViewEvents.filter(\n  ({ isAllday, goingDuration, comingDuration }) => !isAllday && !goingDuration && !comingDuration\n);\nconst [, SHORT_TIME_EVENT] = timeEvents;\n\ntimeEvents.forEach(({ title: eventTitle, start, end }) => {\n  test.describe(`Resize the ${eventTitle} event in the time grid`, () => {\n    cases.forEach(({ title, step, matcherToCompare: compareAssertion }) => {\n      test(`${title}`, async ({ page }) => {\n        const targetEndTime = getTimeStrFromDate(\n          addMinutes(end, (step * 2 - 1) * 30)\n        ) as FormattedTimeString;\n        const {\n          eventLocator,\n          eventBoundingBoxBeforeResize,\n          eventBoundingBoxAfterResize,\n          targetRowBoundingBox,\n        } = await setup({\n          page,\n          targetEventTitle: eventTitle,\n          targetEndTime,\n        });\n\n        // Then\n        expect(eventBoundingBoxAfterResize.height)[compareAssertion](\n          eventBoundingBoxBeforeResize.height\n        );\n\n        await expect.poll(() => eventLocator.textContent()).toContain(getTimeStrFromDate(start));\n\n        expect(\n          eventBoundingBoxAfterResize.height - eventBoundingBoxBeforeResize.height\n        ).toBeCloseTo(targetRowBoundingBox.height * step * 2, -1);\n      });\n    });\n\n    test(`then it should have a minimum height(=1 row) even if the event is resized to before the start time`, async ({\n      page,\n    }) => {\n      const { eventBoundingBoxAfterResize, targetRowBoundingBox } = await setup({\n        page,\n        targetEventTitle: eventTitle,\n        targetEndTime: '00:00',\n      });\n\n      // Then\n      expect(eventBoundingBoxAfterResize.height).toBeCloseTo(targetRowBoundingBox.height, -1);\n    });\n  });\n});\n\ntest('When pressing down the ESC key, the resizing event resets to the initial size.', async ({\n  page,\n}) => {\n  // Given\n  const eventLocator = page.locator(getTimeEventSelector(SHORT_TIME_EVENT.title));\n  const eventBoundingBoxBeforeResize = await getBoundingBox(eventLocator);\n\n  const resizeHandlerLocator = eventLocator.locator(RESIZE_HANDLER_SELECTOR);\n\n  const targetStartTime = getTimeStrFromDate(\n    addHours(SHORT_TIME_EVENT.end as TZDate, 1)\n  ) as FormattedTimeString;\n  const targetRowLocator = page.locator(getTimeGridLineSelector(targetStartTime));\n\n  // When\n  await dragAndDrop({\n    page,\n    sourceLocator: resizeHandlerLocator,\n    targetLocator: targetRowLocator,\n    hold: true,\n  });\n  await page.keyboard.down('Escape');\n\n  // Then\n  const eventBoundingBoxAfterResize = await getBoundingBox(eventLocator);\n  expect(eventBoundingBoxAfterResize).toEqual(eventBoundingBoxBeforeResize);\n});\n\ntest.describe('CSS class for a resize event', () => {\n  test('should be applied depending on a dragging state.', async ({ page }) => {\n    // Given\n    const eventLocator = page.locator(getTimeEventSelector(SHORT_TIME_EVENT.title));\n    const resizeHandlerLocator = eventLocator.locator(RESIZE_HANDLER_SELECTOR);\n    const resizeHandlerBoundingBox = await getBoundingBox(resizeHandlerLocator);\n\n    const resizeEventClassLocator = page.locator(RESIZE_EVENT_SELECTOR);\n\n    // When (a drag has not started yet)\n    await page.mouse.move(resizeHandlerBoundingBox.x + 10, resizeHandlerBoundingBox.y + 3);\n    await page.mouse.down();\n\n    // Then\n    expect(await resizeEventClassLocator.count()).toBe(0);\n\n    // When (a drag is working)\n    await page.mouse.move(resizeHandlerBoundingBox.x + 10, resizeHandlerBoundingBox.y + 50);\n\n    // Then\n    expect(await resizeEventClassLocator.count()).toBe(1);\n\n    // When (a drag is finished)\n    await page.mouse.up();\n\n    // Then\n    expect(await resizeEventClassLocator.count()).toBe(0);\n  });\n\n  test('should not be applied when a drag is canceled.', async ({ page }) => {\n    // Given\n    const eventLocator = page.locator(getTimeEventSelector(SHORT_TIME_EVENT.title));\n    const resizeHandlerLocator = eventLocator.locator(RESIZE_HANDLER_SELECTOR);\n\n    const resizeEventClassLocator = page.locator(RESIZE_EVENT_SELECTOR);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: resizeHandlerLocator,\n      targetLocator: resizeHandlerLocator,\n      options: {\n        targetPosition: { x: 10, y: 30 },\n      },\n      hold: true,\n    });\n    await page.keyboard.down('Escape');\n\n    // Then\n    expect(await resizeEventClassLocator.count()).toBe(0);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/playwright/day/timeGridScrollSync.e2e.ts",
    "content": "import type { Page } from '@playwright/test';\nimport { expect, test } from '@playwright/test';\n\nimport { mockDayViewEvents } from '../../stories/mocks/mockDayViewEvents';\nimport { DAY_VIEW_PAGE_URL } from '../configs';\nimport { getBoundingBox, getPrefixedClassName } from '../utils';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(DAY_VIEW_PAGE_URL);\n});\n\n// NOTE: Syncing scroll only happens when the mousemove event is fired\n// and cannot use `dragAndDrop` because it's better to be manually controlled.\n\nfunction getScrollTop(el: HTMLElement) {\n  return el.scrollTop;\n}\n\ntest.describe('Scroll syncing in time grid when selecting grid', () => {\n  /**\n   * Top right of the column should be empty\n   */\n  async function setup(page: Page) {\n    const timeGridContainerLocator = page.locator(\n      `${getPrefixedClassName('panel')}${getPrefixedClassName('time')}`\n    );\n    const targetColumnLocator = page.locator('[data-testid*=timegrid-column]');\n\n    const containerBoundingBox = await getBoundingBox(timeGridContainerLocator);\n    const columnBoundingBox = await getBoundingBox(targetColumnLocator);\n\n    return {\n      targetColumnLocator,\n      timeGridContainerLocator,\n      containerBoundingBox,\n      columnBoundingBox,\n    };\n  }\n\n  test('it should sync scroll while dragging down to the bottom', async ({ page }) => {\n    // Given\n    const {\n      targetColumnLocator,\n      columnBoundingBox,\n      timeGridContainerLocator,\n      containerBoundingBox,\n    } = await setup(page);\n    const scrollTopBeforeSync = await timeGridContainerLocator.evaluate(getScrollTop);\n\n    // When\n    await targetColumnLocator.hover({\n      position: {\n        x: columnBoundingBox.width - 10,\n        y: 10,\n      },\n      force: true,\n    });\n    await page.mouse.down();\n    await page.mouse.move(\n      columnBoundingBox.x + columnBoundingBox.width / 2,\n      containerBoundingBox.y + containerBoundingBox.height - 10\n    );\n\n    // Then\n    await expect\n      .poll(async () => {\n        const scrollTopAfterSync = await timeGridContainerLocator.evaluate(getScrollTop);\n        return scrollTopAfterSync;\n      })\n      .toBeGreaterThan(scrollTopBeforeSync);\n  });\n\n  test('it should sync scroll while dragging up to the top', async ({ page }) => {\n    // Given\n    const {\n      targetColumnLocator,\n      columnBoundingBox,\n      timeGridContainerLocator,\n      containerBoundingBox,\n    } = await setup(page);\n\n    // Middle of the column\n    const xPosition = columnBoundingBox.x + columnBoundingBox.width / 2;\n\n    // Scroll down to the bottom of the column\n    await targetColumnLocator.hover();\n    await page.mouse.wheel(0, containerBoundingBox.height);\n    let scrollTopBeforeSync = await timeGridContainerLocator.evaluate(getScrollTop);\n    await expect\n      .poll(async () => {\n        scrollTopBeforeSync = await timeGridContainerLocator.evaluate(getScrollTop);\n\n        return scrollTopBeforeSync;\n      })\n      .toBeCloseTo(containerBoundingBox.height, -2);\n\n    // When\n    // drag up to the top of the column\n    await page.mouse.move(xPosition, containerBoundingBox.y + containerBoundingBox.height - 10);\n    await page.mouse.down();\n\n    await expect\n      .poll(async () => {\n        await page.mouse.move(xPosition, containerBoundingBox.y);\n\n        // Then\n        const scrollTopAfterSync = await timeGridContainerLocator.evaluate(getScrollTop);\n        return scrollTopAfterSync;\n      })\n      .toBeLessThan(scrollTopBeforeSync);\n  });\n});\n\nmockDayViewEvents\n  .filter(({ isAllday }) => !isAllday)\n  .forEach(({ title: eventTitle }) => {\n    test.describe(`Scroll syncing in time grid when moving the ${eventTitle} event`, () => {\n      async function setup(page: Page) {\n        const timeGridContainerLocator = page.locator(\n          `${getPrefixedClassName('panel')}${getPrefixedClassName('time')}`\n        );\n        const targetEventLocator = page.locator(`[data-testid*=\"time-event-${eventTitle}\"]`);\n        const containerBoundingBox = await getBoundingBox(timeGridContainerLocator);\n        const eventBoundingBox = await getBoundingBox(targetEventLocator);\n\n        return {\n          timeGridContainerLocator,\n          targetEventLocator,\n          containerBoundingBox,\n          eventBoundingBox,\n        };\n      }\n\n      test('it should sync scroll while moving event to the edge of the bottom', async ({\n        page,\n      }) => {\n        // Given\n        const {\n          timeGridContainerLocator,\n          targetEventLocator,\n          containerBoundingBox,\n          eventBoundingBox,\n        } = await setup(page);\n        const scrollTopBeforeSync = await timeGridContainerLocator.evaluate(getScrollTop);\n\n        // When\n        await targetEventLocator.hover({\n          position: {\n            x: eventBoundingBox.width / 2,\n            y: 3,\n          },\n          force: true,\n        });\n        await page.mouse.down();\n        await page.mouse.move(\n          eventBoundingBox.x + eventBoundingBox.width / 2,\n          containerBoundingBox.y + containerBoundingBox.height - 10\n        );\n        await page.mouse.up();\n\n        // Then\n        await expect\n          .poll(async () => {\n            const scrollTopAfterSync = await timeGridContainerLocator.evaluate(getScrollTop);\n            return scrollTopAfterSync;\n          })\n          .toBeGreaterThan(scrollTopBeforeSync);\n      });\n\n      test('it should sync scroll while moving event to the edge of the top', async ({ page }) => {\n        // Given\n        const {\n          timeGridContainerLocator,\n          targetEventLocator,\n          containerBoundingBox,\n          eventBoundingBox,\n        } = await setup(page);\n\n        // Let's move the event to the bottom first.\n        const middleXOfEvent = eventBoundingBox.x + eventBoundingBox.width / 2;\n        await targetEventLocator.hover({\n          position: {\n            x: eventBoundingBox.width / 2,\n            y: 3,\n          },\n          force: true,\n        });\n        await page.mouse.down();\n        await page.mouse.move(\n          middleXOfEvent,\n          containerBoundingBox.y + containerBoundingBox.height - 10\n        );\n        await page.mouse.up();\n\n        // Then scroll down a little.\n        await page.mouse.wheel(0, containerBoundingBox.height / 2);\n        let scrollTopBeforeSync = await timeGridContainerLocator.evaluate(getScrollTop);\n        await expect\n          .poll(async () => {\n            scrollTopBeforeSync = await timeGridContainerLocator.evaluate(getScrollTop);\n\n            return scrollTopBeforeSync;\n          })\n          .toBeGreaterThan(containerBoundingBox.height / 2);\n\n        // When\n        await targetEventLocator.hover({\n          position: {\n            x: eventBoundingBox.width / 2,\n            y: 3,\n          },\n          force: true,\n        });\n        await page.mouse.down();\n\n        await expect\n          .poll(async () => {\n            await page.mouse.move(middleXOfEvent, containerBoundingBox.y);\n\n            // Then\n            const scrollTopAfterSync = await timeGridContainerLocator.evaluate(getScrollTop);\n            return scrollTopAfterSync;\n          })\n          .toBeLessThan(scrollTopBeforeSync);\n      });\n    });\n  });\n"
  },
  {
    "path": "apps/calendar/playwright/day/timeGridSelection.e2e.ts",
    "content": "import { expect, test } from '@playwright/test';\n\nimport { assertTimeGridSelection } from '../assertions';\nimport { DAY_VIEW_PAGE_URL } from '../configs';\nimport { ClickDelay } from '../constants';\nimport {\n  dragAndDrop,\n  getBoundingBox,\n  getTimeGridLineSelector,\n  waitForSingleElement,\n} from '../utils';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(DAY_VIEW_PAGE_URL);\n});\n\nconst GRID_SELECTION_SELECTOR = '[data-testid*=\"time-grid-selection\"]';\n\n// NOTE: Only firefox automatically scrolls into view at some random tests, so narrowing the range of movement.\n// Maybe `scrollIntoViewIfNeeded` is not supported in the firefox?\n// reference: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoViewIfNeeded\ntest.describe('TimeGrid Selection', () => {\n  const SELECT_START_TIME = '03:00';\n\n  test('should be able to select a time slot with clicking', async ({ page }) => {\n    // Given\n    const startGridLineLocator = page.locator(getTimeGridLineSelector(SELECT_START_TIME));\n    const timeGridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);\n    const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);\n\n    // When\n    await startGridLineLocator.click({ force: true, delay: ClickDelay.Long });\n    await waitForSingleElement(timeGridSelectionLocator); // Test for debounced click handler.\n\n    // Then\n    await assertTimeGridSelection(timeGridSelectionLocator, {\n      startTop: startGridLineBoundingBox.y,\n      endBottom: startGridLineBoundingBox.y + startGridLineBoundingBox.height,\n      formattedTimes: ['03:00', '03:30'],\n    });\n  });\n\n  test.fixme('should be able to select a time slot with double clicking', async ({ page }) => {\n    // Given\n    const startGridLineLocator = page.locator(getTimeGridLineSelector(SELECT_START_TIME));\n    const timeGridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);\n    const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);\n\n    // When\n    await startGridLineLocator.dblclick({ force: true, delay: ClickDelay.Immediate });\n\n    // Then\n    await assertTimeGridSelection(timeGridSelectionLocator, {\n      startTop: startGridLineBoundingBox.y,\n      endBottom: startGridLineBoundingBox.y + startGridLineBoundingBox.height,\n      formattedTimes: ['03:00', '03:30'],\n    });\n  });\n\n  test('should be able to select a range of time from top to bottom', async ({ page }) => {\n    // Given\n    const startGridLineLocator = page.locator(getTimeGridLineSelector(SELECT_START_TIME));\n    const targetGridLineLocator = page.locator(getTimeGridLineSelector('05:00'));\n    const timeGridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);\n\n    const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);\n    const targetGridLineBoundingBox = await getBoundingBox(targetGridLineLocator);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: startGridLineLocator,\n      targetLocator: targetGridLineLocator,\n    });\n\n    // Then\n    await assertTimeGridSelection(timeGridSelectionLocator, {\n      formattedTimes: ['03:00', '05:30'],\n      startTop: startGridLineBoundingBox.y,\n      endBottom: targetGridLineBoundingBox.y + targetGridLineBoundingBox.height,\n    });\n  });\n\n  test('should be able to select a range of time from bottom to top', async ({ page }) => {\n    // Given\n    const startGridLineLocator = page.locator(getTimeGridLineSelector(SELECT_START_TIME));\n    const targetGridLineLocator = page.locator(getTimeGridLineSelector('01:00'));\n    const timeGridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);\n\n    const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);\n    const targetGridLineBoundingBox = await getBoundingBox(targetGridLineLocator);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: startGridLineLocator,\n      targetLocator: targetGridLineLocator,\n    });\n\n    // Then\n    await assertTimeGridSelection(timeGridSelectionLocator, {\n      formattedTimes: ['01:00', '03:30'],\n      startTop: targetGridLineBoundingBox.y,\n      endBottom: startGridLineBoundingBox.y + startGridLineBoundingBox.height,\n    });\n  });\n});\n\ntest('When pressing down the ESC key, the grid selection is canceled.', async ({ page }) => {\n  // Given\n  const startGridLineLocator = page.locator(getTimeGridLineSelector('03:00'));\n  const targetGridLineLocator = page.locator(getTimeGridLineSelector('05:00'));\n\n  // When\n  await dragAndDrop({\n    page,\n    sourceLocator: startGridLineLocator,\n    targetLocator: targetGridLineLocator,\n    hold: true,\n  });\n  await page.keyboard.down('Escape');\n\n  // Then\n  const gridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);\n  expect(await gridSelectionLocator.count()).toBe(0);\n});\n"
  },
  {
    "path": "apps/calendar/playwright/month/accumulatedGridSelection.e2e.ts",
    "content": "import { test } from '@playwright/test';\n\nimport { assertAccumulatedDayGridSelectionMatching } from '../assertions';\nimport { MONTH_VIEW_EMPTY_PAGE_URL } from '../configs';\nimport { selectMonthGridCells } from '../utils';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(MONTH_VIEW_EMPTY_PAGE_URL);\n});\n\ntest('select 2 cells in each week', async ({ page }) => {\n  await selectMonthGridCells(page, 21, 23);\n  await selectMonthGridCells(page, 28, 30);\n\n  await assertAccumulatedDayGridSelectionMatching(page, 21, 23, 0, false);\n});\n\ntest('select 2 cells across 2 weeks', async ({ page }) => {\n  await selectMonthGridCells(page, 13, 14);\n  await selectMonthGridCells(page, 20, 21);\n\n  await assertAccumulatedDayGridSelectionMatching(page, 13, 14, 0, true);\n});\n\ntest('select cell across 2 weeks and select cell in 1 week', async ({ page }) => {\n  await selectMonthGridCells(page, 13, 14);\n  await selectMonthGridCells(page, 24, 25);\n\n  await assertAccumulatedDayGridSelectionMatching(page, 13, 14, 0, true);\n});\n"
  },
  {
    "path": "apps/calendar/playwright/month/eventMoving.e2e.ts",
    "content": "import type { Page } from '@playwright/test';\nimport { expect, test } from '@playwright/test';\n\nimport type { EventObject } from '../../src/types/events';\nimport { mockMonthViewEventsFixed } from '../../stories/mocks/mockMonthViewEvents';\nimport { MONTH_VIEW_PAGE_URL } from '../configs';\nimport { Direction } from '../types';\nimport {\n  dragAndDrop,\n  getBoundingBox,\n  getCellSelector,\n  getHorizontalEventSelector,\n  waitForSingleElement,\n} from '../utils';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(MONTH_VIEW_PAGE_URL);\n});\n\nconst MOVE_EVENT_SELECTOR = '[class*=\"dragging--move-event\"]';\n\nconst [TARGET_EVENT1, TARGET_EVENT2, TARGET_EVENT3] = mockMonthViewEventsFixed;\nconst testCases: {\n  event: EventObject;\n  startCellIndex: number;\n  endCellIndex: number;\n  directions: Direction[];\n}[] = [\n  {\n    event: TARGET_EVENT1,\n    startCellIndex: 7,\n    endCellIndex: 16,\n    directions: [Direction.Up, Direction.Right, Direction.Down],\n  },\n  {\n    event: TARGET_EVENT2,\n    startCellIndex: 16,\n    endCellIndex: 18,\n    directions: [Direction.Up, Direction.Right, Direction.Down, Direction.Left],\n  },\n  {\n    event: TARGET_EVENT3,\n    startCellIndex: 25,\n    endCellIndex: 27,\n    directions: [Direction.Up, Direction.Right, Direction.Down, Direction.Left],\n  },\n];\nconst rightDirectionTestCases = testCases.filter((testCase) =>\n  testCase.directions.includes(Direction.Right)\n);\nconst leftDirectionTestCases = testCases.filter((testCase) =>\n  testCase.directions.includes(Direction.Left)\n);\nconst lowerDirectionTestCases = testCases.filter((testCase) =>\n  testCase.directions.includes(Direction.Down)\n);\nconst upperDirectionTestCases = testCases.filter((testCase) =>\n  testCase.directions.includes(Direction.Up)\n);\n\nasync function setup(page: Page, event: EventObject, targetCellIndex: number) {\n  const targetCellLocator = page.locator(getCellSelector(targetCellIndex));\n  const targetCellBoundingBox = await getBoundingBox(targetCellLocator);\n  const eventLocator = page.locator(getHorizontalEventSelector(event)).first();\n  const boundingBoxBeforeMoving = await getBoundingBox(eventLocator);\n\n  await dragAndDrop({ page, sourceLocator: eventLocator, targetLocator: targetCellLocator });\n  await waitForSingleElement(eventLocator);\n\n  let boundingBoxAfterMoving = await getBoundingBox(eventLocator);\n  await expect\n    .poll(async () => {\n      boundingBoxAfterMoving = await getBoundingBox(eventLocator);\n\n      return boundingBoxAfterMoving;\n    })\n    .not.toEqual(boundingBoxBeforeMoving);\n\n  return {\n    targetCellBoundingBox,\n    boundingBoxBeforeMoving,\n    boundingBoxAfterMoving,\n  };\n}\n\ntest.describe('event moving', () => {\n  /**\n   * Suppose we have the following cells in the month view.\n   * Each number represents the index of the cell.\n   *\n   * [\n   *   [ 0,  1,  2,  3,  4,  5,  6],\n   *   [ 7,  8,  9, 10, 11, 12, 13],\n   *   [14, 15, 16, 17, 18, 19 ,20],\n   *   [21, 22, 23, 24, 25, 26, 27],\n   *   [28, 29, 30, 31, 32, 33, 34],\n   * ]\n   */\n\n  rightDirectionTestCases.forEach(({ event, startCellIndex }) => {\n    const getRightCellIndex = (cellIndex: number) => cellIndex + 1;\n\n    test(`moving month event ${event.title} for direction right`, async ({ page }) => {\n      // Given\n      const rightCellIndex = getRightCellIndex(startCellIndex);\n\n      // When\n      const { targetCellBoundingBox, boundingBoxBeforeMoving, boundingBoxAfterMoving } =\n        await setup(page, event, rightCellIndex);\n\n      // Then\n      expect(boundingBoxAfterMoving.x).toBeGreaterThan(boundingBoxBeforeMoving.x);\n      expect(boundingBoxAfterMoving.x).toBeCloseTo(targetCellBoundingBox.x, 1);\n      expect(boundingBoxAfterMoving.x).toBeLessThan(\n        targetCellBoundingBox.x + targetCellBoundingBox.width\n      );\n    });\n  });\n\n  leftDirectionTestCases.forEach(({ event, startCellIndex }) => {\n    const getLeftCellIndex = (cellIndex: number) => cellIndex - 1;\n\n    test(`moving month event ${event.title} for direction left`, async ({ page }) => {\n      // Given\n      const leftCellIndex = getLeftCellIndex(startCellIndex);\n\n      // When\n      const { targetCellBoundingBox, boundingBoxBeforeMoving, boundingBoxAfterMoving } =\n        await setup(page, event, leftCellIndex);\n\n      // Then\n      expect(boundingBoxAfterMoving.x).toBeLessThan(boundingBoxBeforeMoving.x);\n      expect(boundingBoxAfterMoving.x).toBeCloseTo(targetCellBoundingBox.x, 1);\n      expect(boundingBoxAfterMoving.x).toBeLessThan(\n        targetCellBoundingBox.x + targetCellBoundingBox.width\n      );\n    });\n  });\n\n  lowerDirectionTestCases.forEach(({ event, startCellIndex }) => {\n    const getDownCellIndex = (cellIndex: number) => cellIndex + 7;\n\n    test(`moving month event ${event.title} for direction down`, async ({ page }) => {\n      // Given\n      const downCellIndex = getDownCellIndex(startCellIndex);\n\n      // When\n      const { targetCellBoundingBox, boundingBoxBeforeMoving, boundingBoxAfterMoving } =\n        await setup(page, event, downCellIndex);\n\n      // Then\n      expect(boundingBoxAfterMoving.y).toBeGreaterThan(boundingBoxBeforeMoving.y);\n      expect(boundingBoxAfterMoving.width).toBeCloseTo(boundingBoxBeforeMoving.width);\n      expect(boundingBoxAfterMoving.y).toBeGreaterThan(targetCellBoundingBox.y);\n      expect(boundingBoxAfterMoving.y).toBeLessThan(\n        targetCellBoundingBox.y + targetCellBoundingBox.height\n      );\n    });\n  });\n\n  upperDirectionTestCases.forEach(({ event, startCellIndex }) => {\n    const getUpCellIndex = (cellIndex: number) => cellIndex - 7;\n\n    test(`moving month event ${event.title} for direction up`, async ({ page }) => {\n      // Given\n      const upCellIndex = getUpCellIndex(startCellIndex);\n\n      // When\n      const { targetCellBoundingBox, boundingBoxBeforeMoving, boundingBoxAfterMoving } =\n        await setup(page, event, upCellIndex);\n\n      // Then\n      expect(boundingBoxAfterMoving.y).toBeLessThan(boundingBoxBeforeMoving.y);\n      expect(boundingBoxAfterMoving.width).toBeCloseTo(boundingBoxBeforeMoving.width);\n      expect(boundingBoxAfterMoving.y).toBeGreaterThan(targetCellBoundingBox.y);\n      expect(boundingBoxAfterMoving.y).toBeLessThan(\n        targetCellBoundingBox.y + targetCellBoundingBox.height\n      );\n    });\n  });\n\n  test('moving month grid event to end of week', async ({ page }) => {\n    // Given\n    const eventLocator = page.locator(getHorizontalEventSelector(TARGET_EVENT2));\n    const endOfWeekCellLocator = page.locator(getCellSelector(20));\n    const endOfWeekCellBoundingBox = await getBoundingBox(endOfWeekCellLocator);\n    const secondOfWeekCellLocator = page.locator(getCellSelector(22));\n    const secondOfWeekCellBoundingBox = await getBoundingBox(secondOfWeekCellLocator);\n\n    // When\n    await dragAndDrop({ page, sourceLocator: eventLocator, targetLocator: endOfWeekCellLocator });\n\n    // Then\n    await expect.poll(() => eventLocator.evaluateAll((events) => events.length)).toBe(2);\n\n    const targetEventLength = await eventLocator.evaluateAll((events) =>\n      (events as HTMLElement[]).reduce(\n        (total, eventRow) => eventRow.getBoundingClientRect().width + total,\n        0\n      )\n    );\n    const firstEventLocator = eventLocator.first();\n    const lastEventLocator = eventLocator.last();\n    const firstEventBoundingBox = await getBoundingBox(firstEventLocator);\n    const lastEventBoundingBox = await getBoundingBox(lastEventLocator);\n\n    expect(firstEventBoundingBox.x).toBeCloseTo(endOfWeekCellBoundingBox.x, 3);\n    expect(lastEventBoundingBox.x).toBeLessThan(\n      secondOfWeekCellBoundingBox.x + secondOfWeekCellBoundingBox.width\n    );\n    expect(firstEventBoundingBox.y).toBeLessThan(secondOfWeekCellBoundingBox.y);\n    expect(lastEventBoundingBox.y).toBeGreaterThan(secondOfWeekCellBoundingBox.y);\n    expect(targetEventLength).toBeCloseTo(endOfWeekCellBoundingBox.width * 3, 1);\n  });\n});\n\ntest('When pressing down the ESC key, the moving event resets to the initial position.', async ({\n  page,\n}) => {\n  // Given\n  const eventLocator = page.locator(getHorizontalEventSelector(TARGET_EVENT2)).first();\n  const eventBoundingBoxBeforeMove = await getBoundingBox(eventLocator);\n\n  const targetCellLocator = page.locator(getCellSelector(20));\n\n  // When\n  await dragAndDrop({\n    page,\n    sourceLocator: eventLocator,\n    targetLocator: targetCellLocator,\n    hold: true,\n  });\n  await page.keyboard.down('Escape');\n\n  // Then\n  const eventBoundingBoxAfterMove = await getBoundingBox(eventLocator);\n  expect(eventBoundingBoxAfterMove).toEqual(eventBoundingBoxBeforeMove);\n});\n\ntest.describe('CSS class for a move event', () => {\n  test('should be applied depending on a dragging state.', async ({ page }) => {\n    // Given\n    const eventLocator = page.locator(getHorizontalEventSelector(TARGET_EVENT2)).first();\n    const eventBoundingBox = await getBoundingBox(eventLocator);\n    const moveEventClassLocator = page.locator(MOVE_EVENT_SELECTOR);\n\n    // When (a drag has not started yet)\n    await page.mouse.move(eventBoundingBox.x + 10, eventBoundingBox.y + 10);\n    await page.mouse.down();\n\n    // Then\n    expect(await moveEventClassLocator.count()).toBe(0);\n\n    // When (a drag is working)\n    await page.mouse.move(eventBoundingBox.x + 10, eventBoundingBox.y + 50);\n\n    // Then\n    expect(await moveEventClassLocator.count()).toBe(1);\n\n    // When (a drag is finished)\n    await page.mouse.up();\n\n    // Then\n    expect(await moveEventClassLocator.count()).toBe(0);\n  });\n\n  test('should not be applied when a drag is canceled.', async ({ page }) => {\n    // Given\n    const eventLocator = page.locator(getHorizontalEventSelector(TARGET_EVENT2)).first();\n    const moveEventClassLocator = page.locator(MOVE_EVENT_SELECTOR);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: eventLocator,\n      targetLocator: eventLocator,\n      options: {\n        targetPosition: { x: 10, y: 30 },\n      },\n      hold: true,\n    });\n    await page.keyboard.down('Escape');\n\n    // Then\n    expect(await moveEventClassLocator.count()).toBe(0);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/playwright/month/eventResizing.e2e.ts",
    "content": "import type { Locator } from '@playwright/test';\nimport { expect, test } from '@playwright/test';\n\nimport { mockMonthViewEventsFixed } from '../../stories/mocks/mockMonthViewEvents';\nimport { assertBoundingBoxIncluded } from '../assertions';\nimport { MONTH_VIEW_PAGE_URL } from '../configs';\nimport {\n  dragAndDrop,\n  getBoundingBox,\n  getCellSelector,\n  getHorizontalEventSelector,\n  waitForSingleElement,\n} from '../utils';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(MONTH_VIEW_PAGE_URL);\n});\n\nconst RESIZE_EVENT_SELECTOR = '[class*=\"dragging--resize-horizontal-event\"]';\n\nconst [TARGET_EVENT1, TARGET_EVENT2] = mockMonthViewEventsFixed;\n\nfunction getResizeIconLocatorOfEvent(eventLocator: Locator) {\n  return eventLocator.last().locator('data-testid=horizontal-event-resize-icon');\n}\n\ntest.describe('event resizing', () => {\n  /**\n   * Suppose we have the following cells in the month view.\n   * Each number represents the index of the cell.\n   *\n   * [\n   *   [ 0,  1,  2,  3,  4,  5,  6],\n   *   [ 7,  8,  9, 10, 11, 12, 13],\n   *   [14, 15, 16, 17, 18, 19 ,20],\n   *   [21, 22, 23, 24, 25, 26, 27],\n   *   [28, 29, 30, 31, 32, 33, 34],\n   * ]\n   */\n\n  // target event is rendered from #7 to #16\n  const RESIZE_TARGET_SELECTOR = getHorizontalEventSelector(TARGET_EVENT1);\n\n  test('resize event to the right in the same row', async ({ page }) => {\n    // Given\n    const eventsLocator = page.locator(RESIZE_TARGET_SELECTOR);\n    const resizeIconLocator = getResizeIconLocatorOfEvent(eventsLocator);\n    const targetCellLocator = page.locator(getCellSelector(19));\n    const eventBoundingBoxBeforeResizing = await getBoundingBox(eventsLocator.last());\n\n    // When\n    await dragAndDrop({ page, sourceLocator: resizeIconLocator, targetLocator: targetCellLocator });\n\n    // Then\n    await expect.poll(() => eventsLocator.count()).toBe(2);\n\n    await expect\n      .poll(async () => {\n        const eventBoundingBoxAfterResizing = await getBoundingBox(eventsLocator.last());\n        return eventBoundingBoxAfterResizing.width;\n      })\n      .toBeGreaterThan(eventBoundingBoxBeforeResizing.width);\n\n    const targetCellBoundingBox = await getBoundingBox(targetCellLocator);\n    const resizeIconBoundingBoxAfterResizing = await getBoundingBox(resizeIconLocator);\n    assertBoundingBoxIncluded(resizeIconBoundingBoxAfterResizing, targetCellBoundingBox);\n  });\n\n  test('resize event to the left in the same row', async ({ page }) => {\n    // Given\n    const eventsLocator = page.locator(RESIZE_TARGET_SELECTOR);\n    const resizeIconLocator = getResizeIconLocatorOfEvent(eventsLocator);\n    const targetCellLocator = page.locator(getCellSelector(14));\n    const eventBoundingBoxBeforeResizing = await getBoundingBox(eventsLocator.last());\n\n    // When\n    await dragAndDrop({ page, sourceLocator: resizeIconLocator, targetLocator: targetCellLocator });\n\n    // Then\n    await expect.poll(() => eventsLocator.count()).toBe(2);\n\n    await expect\n      .poll(async () => {\n        const eventBoundingBoxAfterResizing = await getBoundingBox(eventsLocator.last());\n        return eventBoundingBoxAfterResizing.width;\n      })\n      .toBeLessThan(eventBoundingBoxBeforeResizing.width);\n\n    const targetCellBoundingBox = await getBoundingBox(targetCellLocator);\n    const resizeIconBoundingBoxAfterResizing = await getBoundingBox(resizeIconLocator);\n    assertBoundingBoxIncluded(resizeIconBoundingBoxAfterResizing, targetCellBoundingBox);\n  });\n\n  test('resize event to the right in the next two rows', async ({ page }) => {\n    // Given\n    const eventsLocator = page.locator(RESIZE_TARGET_SELECTOR);\n    const resizeIconLocator = getResizeIconLocatorOfEvent(eventsLocator);\n    const targetCellLocator = page.locator(getCellSelector(31));\n\n    // When\n    await dragAndDrop({ page, sourceLocator: resizeIconLocator, targetLocator: targetCellLocator });\n\n    // Then\n    await expect.poll(() => eventsLocator.count()).toBe(4);\n\n    const resizeIconBoundingBoxAfterResizing = await getBoundingBox(resizeIconLocator);\n    const targetCellBoundingBox = await getBoundingBox(targetCellLocator);\n    assertBoundingBoxIncluded(resizeIconBoundingBoxAfterResizing, targetCellBoundingBox);\n  });\n\n  test('resize event to the left in the next two rows', async ({ page }) => {\n    // Given\n    const eventsLocator = page.locator(RESIZE_TARGET_SELECTOR);\n    const resizeIconLocator = getResizeIconLocatorOfEvent(eventsLocator);\n    const targetCellLocator = page.locator(getCellSelector(28));\n\n    // When\n    await dragAndDrop({ page, sourceLocator: resizeIconLocator, targetLocator: targetCellLocator });\n\n    // Then\n    await expect.poll(() => eventsLocator.count()).toBe(4);\n\n    const resizeIconBoundingBoxAfterResizing = await getBoundingBox(resizeIconLocator);\n    const targetCellBoundingBox = await getBoundingBox(targetCellLocator);\n    assertBoundingBoxIncluded(resizeIconBoundingBoxAfterResizing, targetCellBoundingBox);\n  });\n\n  test('shrink event - to the end of the first row of rendered events', async ({ page }) => {\n    // Given\n    const eventsLocator = page.locator(RESIZE_TARGET_SELECTOR);\n    const resizeIconLocator = getResizeIconLocatorOfEvent(eventsLocator);\n    const targetCellLocator = page.locator(getCellSelector(13));\n\n    // When\n    await dragAndDrop({ page, sourceLocator: resizeIconLocator, targetLocator: targetCellLocator });\n    await waitForSingleElement(eventsLocator);\n\n    // Then\n    const resizeIconBoundingBoxAfterResizing = await getBoundingBox(resizeIconLocator);\n    const targetCellBoundingBox = await getBoundingBox(targetCellLocator);\n    assertBoundingBoxIncluded(resizeIconBoundingBoxAfterResizing, targetCellBoundingBox);\n  });\n\n  test('shrink event - to take place of just one cell', async ({ page }) => {\n    // Given\n    const eventsLocator = page.locator(RESIZE_TARGET_SELECTOR);\n    const resizeIconLocator = getResizeIconLocatorOfEvent(eventsLocator);\n    const targetCellLocator = page.locator(getCellSelector(7));\n\n    // When\n    await dragAndDrop({ page, sourceLocator: resizeIconLocator, targetLocator: targetCellLocator });\n    await waitForSingleElement(eventsLocator);\n\n    // Then\n    const resizeIconBoundingBoxAfterResizing = await getBoundingBox(resizeIconLocator);\n    const targetCellBoundingBox = await getBoundingBox(targetCellLocator);\n    assertBoundingBoxIncluded(resizeIconBoundingBoxAfterResizing, targetCellBoundingBox);\n  });\n\n  test('prevent resizing when dragging to above the first row of the event or left of the first cell of the event', async ({\n    page,\n  }) => {\n    // Given\n    const eventsLocator = page.locator(RESIZE_TARGET_SELECTOR);\n    const resizeIconLocator = getResizeIconLocatorOfEvent(eventsLocator);\n    const targetCellLocator = page.locator(getCellSelector(0));\n    const expectedCellLocator = page.locator(getCellSelector(16));\n\n    // When\n    await dragAndDrop({ page, sourceLocator: resizeIconLocator, targetLocator: targetCellLocator });\n\n    // Then\n    await expect.poll(() => eventsLocator.count()).toBe(2);\n\n    const resizeIconBoundingBoxAfterResizing = await getBoundingBox(resizeIconLocator);\n    const expectedCellBoundingBox = await getBoundingBox(expectedCellLocator);\n    assertBoundingBoxIncluded(resizeIconBoundingBoxAfterResizing, expectedCellBoundingBox);\n  });\n});\n\ntest('When pressing down the ESC key, the resizing event resets to the initial size.', async ({\n  page,\n}) => {\n  // Given\n  const eventLocator = page.locator(getHorizontalEventSelector(TARGET_EVENT2));\n  const eventBoundingBoxBeforeResize = await getBoundingBox(eventLocator.last());\n\n  const resizeHandlerLocator = getResizeIconLocatorOfEvent(eventLocator);\n\n  const targetCellLocator = page.locator(getCellSelector(20));\n\n  // When\n  await dragAndDrop({\n    page,\n    sourceLocator: resizeHandlerLocator,\n    targetLocator: targetCellLocator,\n    hold: true,\n  });\n  await page.keyboard.down('Escape');\n\n  // Then\n  const eventBoundingBoxAfterResize = await getBoundingBox(eventLocator);\n  expect(eventBoundingBoxAfterResize).toEqual(eventBoundingBoxBeforeResize);\n});\n\ntest.describe('CSS class for a resize event', () => {\n  test('should be applied depending on a dragging state.', async ({ page }) => {\n    // Given\n    const eventLocator = page.locator(getHorizontalEventSelector(TARGET_EVENT2));\n    const resizeHandlerLocator = getResizeIconLocatorOfEvent(eventLocator);\n    const resizeHandlerBoundingBox = await getBoundingBox(resizeHandlerLocator);\n\n    const resizeEventClassLocator = page.locator(RESIZE_EVENT_SELECTOR);\n\n    // When (a drag has not started yet)\n    await page.mouse.move(resizeHandlerBoundingBox.x + 1, resizeHandlerBoundingBox.y + 3);\n    await page.mouse.down();\n\n    // Then\n    expect(await resizeEventClassLocator.count()).toBe(0);\n\n    // When (a drag is working)\n    await page.mouse.move(resizeHandlerBoundingBox.x + 10, resizeHandlerBoundingBox.y + 50);\n\n    // Then\n    expect(await resizeEventClassLocator.count()).toBe(1);\n\n    // When (a drag is finished)\n    await page.mouse.up();\n\n    // Then\n    expect(await resizeEventClassLocator.count()).toBe(0);\n  });\n\n  test('should not be applied when a drag is canceled.', async ({ page }) => {\n    // Given\n    const eventLocator = page.locator(getHorizontalEventSelector(TARGET_EVENT2));\n    const resizeHandlerLocator = getResizeIconLocatorOfEvent(eventLocator);\n\n    const resizeEventClassLocator = page.locator(RESIZE_EVENT_SELECTOR);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: resizeHandlerLocator,\n      targetLocator: resizeHandlerLocator,\n      options: {\n        targetPosition: { x: 10, y: 30 },\n      },\n      hold: true,\n    });\n    await page.keyboard.down('Escape');\n\n    // Then\n    expect(await resizeEventClassLocator.count()).toBe(0);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/playwright/month/gridSelection.e2e.ts",
    "content": "import type { Page } from '@playwright/test';\nimport { expect, test } from '@playwright/test';\n\nimport { assertDayGridSelectionMatching } from '../assertions';\nimport { MONTH_VIEW_PAGE_URL } from '../configs';\nimport { ClickDelay } from '../constants';\nimport { dragAndDrop, getPrefixedClassName, selectMonthGridCells } from '../utils';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(MONTH_VIEW_PAGE_URL);\n});\n\nconst MONTH_GRID_CELL_SELECTOR = getPrefixedClassName('daygrid-cell');\nconst GRID_SELECTION_SELECTOR = `${getPrefixedClassName('weekday')} > ${getPrefixedClassName(\n  'grid-selection'\n)}`;\n\n/**\n * Suppose we have the following cells in the month view.\n * Each number represents the index of the cell.\n *\n * [\n *   [ 0,  1,  2,  3,  4,  5,  6],\n *   [ 7,  8,  9, 10, 11, 12, 13],\n *   [14, 15, 16, 17, 18, 19 ,20],\n *   [21, 22, 23, 24, 25, 26, 27],\n *   [28, 29, 30, 31, 32, 33, 34],\n * ]\n */\n\nfunction assertMonthGridSelectionMatching(page: Page, startIndex: number, endIndex: number) {\n  return assertDayGridSelectionMatching(\n    page,\n    startIndex,\n    endIndex,\n    MONTH_GRID_CELL_SELECTOR,\n    GRID_SELECTION_SELECTOR\n  );\n}\n\ntest('select a cell by clicking.', async ({ page }) => {\n  // Given\n  const monthGridCellLocator = page.locator(MONTH_GRID_CELL_SELECTOR).nth(31);\n\n  // When\n  await monthGridCellLocator.click({ delay: ClickDelay.Short });\n\n  // Then\n  await assertMonthGridSelectionMatching(page, 31, 31);\n});\n\n// It looks like triple click happens.\n// Affected by auto clearing grid selection when form popup closed.\ntest.fixme('select a cell by double clicking.', async ({ page }) => {\n  // Given\n  const monthGridCellLocator = page.locator(MONTH_GRID_CELL_SELECTOR).nth(31);\n\n  // When\n  await monthGridCellLocator.dblclick({ delay: ClickDelay.Immediate });\n\n  // Then\n  await assertMonthGridSelectionMatching(page, 31, 31);\n});\n\ntest('select a cell by drag and drop.', async ({ page }) => {\n  await selectMonthGridCells(page, 31, 31);\n\n  await assertMonthGridSelectionMatching(page, 31, 31);\n});\n\ntest('select 2 cells from left to right', async ({ page }) => {\n  await selectMonthGridCells(page, 31, 32);\n\n  await assertMonthGridSelectionMatching(page, 31, 32);\n});\n\ntest('select 2 cells from right to left(reverse)', async ({ page }) => {\n  await selectMonthGridCells(page, 32, 31);\n\n  await assertMonthGridSelectionMatching(page, 31, 32);\n});\n\ntest('select 2 rows from top to bottom', async ({ page }) => {\n  await selectMonthGridCells(page, 25, 32);\n\n  await assertMonthGridSelectionMatching(page, 25, 32);\n});\n\ntest('select 2 rows from bottom to top(reverse)', async ({ page }) => {\n  await selectMonthGridCells(page, 32, 25);\n\n  await assertMonthGridSelectionMatching(page, 25, 32);\n});\n\ntest('select entire row', async ({ page }) => {\n  await selectMonthGridCells(page, 28, 34);\n\n  await assertMonthGridSelectionMatching(page, 28, 34);\n});\n\ntest('event form popup with grid selection', async ({ page }) => {\n  await selectMonthGridCells(page, 28, 34);\n\n  const floatingLayer = page.locator('css=[role=dialog]');\n\n  expect(floatingLayer).not.toBeNull();\n});\n\ntest('When pressing down the ESC key, the grid selection is canceled.', async ({ page }) => {\n  // Given\n  const startCellLocator = page.locator(MONTH_GRID_CELL_SELECTOR).nth(31);\n  const targetCellLocator = page.locator(MONTH_GRID_CELL_SELECTOR).nth(32);\n\n  // When\n  await dragAndDrop({\n    page,\n    sourceLocator: startCellLocator,\n    targetLocator: targetCellLocator,\n    hold: true,\n  });\n  await page.keyboard.down('Escape');\n\n  // Then\n  const gridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);\n  expect(await gridSelectionLocator.count()).toBe(0);\n});\n"
  },
  {
    "path": "apps/calendar/playwright/month/seeMoreEventsPopup.e2e.ts",
    "content": "import { expect, test } from '@playwright/test';\n\nimport { MONTH_VIEW_PAGE_URL } from '../configs';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(MONTH_VIEW_PAGE_URL);\n});\n\ntest.describe('more events popup', () => {\n  test('when clicking on \"more events\" button, popup should be visible', async ({ page }) => {\n    await page.click('text=/\\\\d+ more/i >> nth=0');\n\n    const popupLocator = page.locator('css=[role=dialog]');\n    expect(await popupLocator.isVisible()).toBe(true);\n\n    const listLocator = popupLocator.locator('css=[class*=list]');\n    const listItemCount = await listLocator.evaluate((list) => list.children.length);\n\n    expect(listItemCount).toBeGreaterThan(0);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/playwright/month/visibleEventCount.e2e.ts",
    "content": "import { expect, test } from '@playwright/test';\n\nimport { MONTH_VIEW_PAGE_URL } from '../configs';\nimport { getCellSelector, getPrefixedClassName } from '../utils';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(MONTH_VIEW_PAGE_URL);\n});\n\nconst eventsRowClassName = getPrefixedClassName('weekday-events');\n\ntest.describe('visibleEventCount option', () => {\n  // In the default viewport, 3 event blocks are visible.\n  // 16th cell has 12 events.\n\n  test('when visibleEventCount is set to 0, no events should be visible', async ({ page }) => {\n    // Given\n    const targetCell = page.locator(getCellSelector(16));\n    const events = page.locator(eventsRowClassName).nth(2);\n\n    // When\n    await page.evaluate(() => {\n      window.$cal.setOptions({\n        month: {\n          visibleEventCount: 0,\n        },\n      });\n    });\n\n    // Then\n    expect(await events.evaluate((_events) => _events.children.length)).toBe(0);\n\n    const moreButton = targetCell.locator('button');\n    await expect(moreButton).toHaveText('12 more');\n  });\n\n  test('when visibleEventCount is bigger than the content area, it only shows events within the content area', async ({\n    page,\n  }) => {\n    // Given\n    const targetCell = page.locator(getCellSelector(16));\n    const events = page.locator(eventsRowClassName).nth(2);\n\n    // When\n    await page.evaluate(() => {\n      window.$cal.setOptions({\n        month: {\n          visibleEventCount: 10,\n        },\n      });\n    });\n\n    // Then\n    expect(await events.evaluate((_events) => _events.children.length)).toBe(3);\n\n    const moreButton = targetCell.locator('button');\n    await expect(moreButton).toHaveText('9 more');\n  });\n});\n"
  },
  {
    "path": "apps/calendar/playwright/playwright-env.d.ts",
    "content": "import type Calendar from '../src/factory/calendar';\n\ndeclare global {\n  interface Window {\n    $cal: Calendar;\n  }\n}\n"
  },
  {
    "path": "apps/calendar/playwright/types.ts",
    "content": "export type BoundingBox = {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n};\n\nexport enum Direction {\n  Up = 0,\n  UpperRight = 1,\n  Right = 2,\n  LowerRight = 3,\n  Down = 4,\n  LowerLeft = 5,\n  Left = 6,\n  UpperLeft = 7,\n}\n"
  },
  {
    "path": "apps/calendar/playwright/utils.ts",
    "content": "import type { Locator, Page } from '@playwright/test';\nimport { expect } from '@playwright/test';\n\nimport type TZDate from '../src/time/date';\nimport type { EventObject } from '../src/types/events';\nimport type { FormattedTimeString } from '../src/types/time/datetime';\nimport type { BoundingBox } from './types';\n\nexport function getPrefixedClassName(className: string) {\n  return `.toastui-calendar-${className}`;\n}\n\nexport async function dragAndDrop({\n  page,\n  sourceLocator,\n  targetLocator,\n  options = {},\n  hold = false,\n}: {\n  page: Page;\n  sourceLocator: Locator;\n  targetLocator: Locator;\n  options?: Parameters<Locator['dragTo']>[1];\n  hold?: boolean;\n}) {\n  const sourceBoundingBox = await getBoundingBox(sourceLocator);\n  const targetBoundingBox = await getBoundingBox(targetLocator);\n\n  const sourceX = sourceBoundingBox.x + (options?.sourcePosition?.x ?? sourceBoundingBox.width / 2);\n  const sourceY =\n    sourceBoundingBox.y + (options?.sourcePosition?.y ?? sourceBoundingBox.height / 2);\n  const targetX = targetBoundingBox.x + (options?.targetPosition?.x ?? targetBoundingBox.width / 2);\n  const targetY =\n    targetBoundingBox.y + (options?.targetPosition?.y ?? targetBoundingBox.height / 2);\n\n  await page.mouse.move(sourceX, sourceY);\n  await page.mouse.down();\n  await page.mouse.move(targetX, targetY, { steps: 4 });\n  if (!hold) {\n    await page.mouse.up();\n  }\n}\n\nexport async function selectGridCells(\n  page: Page,\n  startCellIdx: number,\n  endCellIdx: number,\n  className: string\n) {\n  const startCellLocator = page.locator(className).nth(startCellIdx);\n  const endCellLocator = page.locator(className).nth(endCellIdx);\n\n  await dragAndDrop({ page, sourceLocator: startCellLocator, targetLocator: endCellLocator });\n}\n\nexport function selectMonthGridCells(page: Page, startCellIndex: number, endCellIndex: number) {\n  return selectGridCells(page, startCellIndex, endCellIndex, '.toastui-calendar-daygrid-cell');\n}\n\nexport async function getBoundingBox(locator: Locator): Promise<BoundingBox> {\n  const boundingBox = await locator.boundingBox();\n\n  if (!boundingBox) {\n    throw new Error(`BoundingBox of ${locator} is not found`);\n  }\n\n  return boundingBox;\n}\n\nexport function getTimeEventSelector(title: string): string {\n  return `[data-testid^=\"time-event-${title}-\"]`;\n}\n\nexport function getGuideTimeEventSelector(): string {\n  return `[data-testid^=\"guide-time-event\"]`;\n}\n\nexport function getHorizontalEventSelector(event: EventObject): string {\n  return `data-testid=${event.calendarId}-${event.id}-${event.title}`;\n}\n\nexport function getTimeGridLineSelector(start: FormattedTimeString): string {\n  return `[data-testid*=\"gridline-${start}\"]`;\n}\n\nexport function getCellSelector(cellIndex: number): string {\n  return `.toastui-calendar-daygrid-cell >> nth=${cellIndex}`;\n}\n\nexport function getTimeStrFromDate(d: TZDate) {\n  const fixToTwoDigits = (num: number) => num.toString().padStart(2, '0');\n\n  const hour = d.getHours();\n  const minute = d.getMinutes();\n\n  return `${fixToTwoDigits(hour)}:${fixToTwoDigits(minute)}`;\n}\n\nexport function waitForSingleElement(locator: Locator) {\n  return expect.poll(() => locator.count()).toBe(1);\n}\n\n/**\n * Get locator matches testId.\n */\nexport function queryLocatorByTestId(page: Page, testId: string) {\n  return page.locator(`[data-testid*=\"${testId}\"]`);\n}\n"
  },
  {
    "path": "apps/calendar/playwright/week/alldayGridEventMoving.e2e.ts",
    "content": "import type { Locator } from '@playwright/test';\nimport { expect, test } from '@playwright/test';\n\nimport { mockWeekViewEvents } from '../../stories/mocks/mockWeekViewEvents';\nimport { WEEK_VIEW_PAGE_URL } from '../configs';\nimport {\n  dragAndDrop,\n  getBoundingBox,\n  getHorizontalEventSelector,\n  getPrefixedClassName,\n} from '../utils';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(WEEK_VIEW_PAGE_URL);\n});\n\nconst ALL_DAY_GRID_CELL_SELECTOR = `${getPrefixedClassName(\n  'panel'\n)}:has-text(\"All Day\") ${getPrefixedClassName('panel-grid')}`;\nconst MOVE_EVENT_SELECTOR = '[class*=\"dragging--move-event\"]';\n\nconst [TARGET_EVENT] = mockWeekViewEvents.filter(({ isAllday }) => isAllday);\nconst TARGET_EVENT_SELECTOR = getHorizontalEventSelector(TARGET_EVENT);\n\nasync function getX(locator: Locator) {\n  const boundingBox = await getBoundingBox(locator);\n\n  return boundingBox.x;\n}\n\nasync function getWidth(locator: Locator) {\n  const boundingBox = await getBoundingBox(locator);\n\n  return boundingBox.width;\n}\n\n/**\n * Suppose we have the following cells in the week view.\n * Each number represents the index of the cell.\n *\n * [ 0,  1,  2,  3,  4,  5,  6]\n */\ntest('moving allday grid row event from left to right', async ({ page }) => {\n  // Given\n  const targetEventLocator = page.locator(TARGET_EVENT_SELECTOR);\n  const boundingBoxBeforeMoving = await getBoundingBox(targetEventLocator);\n  const fifthOfWeekCellLocator = page.locator(ALL_DAY_GRID_CELL_SELECTOR).nth(4);\n\n  // When\n  await dragAndDrop({\n    page,\n    sourceLocator: targetEventLocator,\n    targetLocator: fifthOfWeekCellLocator,\n  });\n\n  // Then\n  await expect.poll(() => getX(targetEventLocator)).toBeGreaterThan(boundingBoxBeforeMoving.x);\n  await expect\n    .poll(() => getWidth(targetEventLocator))\n    .toBeCloseTo(boundingBoxBeforeMoving.width, 3);\n});\n\ntest.describe('moving allday grid row event when moving by holding the middle or end', () => {\n  test('holding middle of event', async ({ page }) => {\n    // Given\n    const targetEventLocator = page.locator(TARGET_EVENT_SELECTOR);\n    const boundingBoxBeforeMoving = await getBoundingBox(targetEventLocator);\n    const fourthOfWeekCellLocator = page.locator(ALL_DAY_GRID_CELL_SELECTOR).nth(3);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: targetEventLocator,\n      targetLocator: fourthOfWeekCellLocator,\n    });\n\n    // Then\n    await expect\n      .poll(() => getX(targetEventLocator))\n      .toBeCloseTo(boundingBoxBeforeMoving.x + (boundingBoxBeforeMoving.width * 2) / 3, 1);\n    await expect\n      .poll(() => getX(targetEventLocator))\n      .toBeLessThan(boundingBoxBeforeMoving.x + boundingBoxBeforeMoving.width);\n  });\n\n  test('holding end of event', async ({ page }) => {\n    // Given\n    const targetEventLocator = page.locator(TARGET_EVENT_SELECTOR);\n    const boundingBoxBeforeMoving = await getBoundingBox(targetEventLocator);\n    const fourthOfWeekCellLocator = page.locator(ALL_DAY_GRID_CELL_SELECTOR).nth(3);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: targetEventLocator,\n      targetLocator: fourthOfWeekCellLocator,\n      options: {\n        sourcePosition: {\n          x: (boundingBoxBeforeMoving.width * 5) / 6,\n          y: boundingBoxBeforeMoving.height / 2,\n        },\n      },\n    });\n\n    // Then\n    await expect\n      .poll(() => getX(targetEventLocator))\n      .toBeCloseTo(boundingBoxBeforeMoving.x + boundingBoxBeforeMoving.width / 3, 1);\n    await expect\n      .poll(() => getX(targetEventLocator))\n      .toBeLessThan(boundingBoxBeforeMoving.x + boundingBoxBeforeMoving.width);\n  });\n});\n\ntest('When pressing down the ESC key, the moving event resets to the initial position.', async ({\n  page,\n}) => {\n  // Given\n  const eventLocator = page.locator(TARGET_EVENT_SELECTOR);\n  const eventBoundingBoxBeforeMove = await getBoundingBox(eventLocator);\n\n  const targetCellLocator = page.locator(ALL_DAY_GRID_CELL_SELECTOR).nth(4);\n\n  // When\n  await dragAndDrop({\n    page,\n    sourceLocator: eventLocator,\n    targetLocator: targetCellLocator,\n    hold: true,\n  });\n  await page.keyboard.down('Escape');\n\n  // Then\n  const eventBoundingBoxAfterMove = await getBoundingBox(eventLocator);\n  expect(eventBoundingBoxAfterMove).toEqual(eventBoundingBoxBeforeMove);\n});\n\ntest.describe('CSS class for a move event', () => {\n  test('should be applied depending on a dragging state.', async ({ page }) => {\n    // Given\n    const eventLocator = page.locator(TARGET_EVENT_SELECTOR);\n    const eventBoundingBox = await getBoundingBox(eventLocator);\n    const moveEventClassLocator = page.locator(MOVE_EVENT_SELECTOR);\n\n    // When (a drag has not started yet)\n    await page.mouse.move(eventBoundingBox.x + 10, eventBoundingBox.y + 10);\n    await page.mouse.down();\n\n    // Then\n    expect(await moveEventClassLocator.count()).toBe(0);\n\n    // When (a drag is working)\n    await page.mouse.move(eventBoundingBox.x + 10, eventBoundingBox.y + 50);\n\n    // Then\n    expect(await moveEventClassLocator.count()).toBe(1);\n\n    // When (a drag is finished)\n    await page.mouse.up();\n\n    // Then\n    expect(await moveEventClassLocator.count()).toBe(0);\n  });\n\n  test('should not be applied when a drag is canceled.', async ({ page }) => {\n    // Given\n    const eventLocator = page.locator(TARGET_EVENT_SELECTOR);\n    const moveEventClassLocator = page.locator(MOVE_EVENT_SELECTOR);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: eventLocator,\n      targetLocator: eventLocator,\n      options: {\n        targetPosition: { x: 10, y: 30 },\n      },\n    });\n    await page.keyboard.down('Escape');\n\n    // Then\n    expect(await moveEventClassLocator.count()).toBe(0);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/playwright/week/alldayGridEventResizing.e2e.ts",
    "content": "import type { Locator } from '@playwright/test';\nimport { expect, test } from '@playwright/test';\n\nimport { mockWeekViewEvents } from '../../stories/mocks/mockWeekViewEvents';\nimport { WEEK_VIEW_PAGE_URL } from '../configs';\nimport {\n  dragAndDrop,\n  getBoundingBox,\n  getHorizontalEventSelector,\n  getPrefixedClassName,\n} from '../utils';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(WEEK_VIEW_PAGE_URL);\n});\n\nconst ALL_DAY_GRID_CELL_SELECTOR = `${getPrefixedClassName(\n  'panel'\n)}:has-text(\"All Day\") ${getPrefixedClassName('panel-grid')}`;\nconst RESIZE_HANDLER_SELECTOR = getPrefixedClassName('handle-y');\nconst RESIZE_EVENT_SELECTOR = '[class*=\"dragging--resize-horizontal-event\"]';\n\nconst [TARGET_EVENT] = mockWeekViewEvents.filter(({ isAllday }) => isAllday);\nconst TARGET_EVENT_SELECTOR = getHorizontalEventSelector(TARGET_EVENT);\n\nasync function getWidth(locator: Locator) {\n  const boundingBox = await getBoundingBox(locator);\n\n  return boundingBox.width;\n}\n\n/**\n * Suppose we have the following cells in the week view.\n * Each number represents the index of the cell.\n *\n * [ 0,  1,  2,  3,  4,  5,  6]\n */\ntest('resizing allday grid row event from left to right', async ({ page }) => {\n  // Given\n  const targetEventLocator = page.locator(TARGET_EVENT_SELECTOR);\n  const boundingBoxBeforeResizing = await getBoundingBox(targetEventLocator);\n  const resizerLocator = targetEventLocator.locator(RESIZE_HANDLER_SELECTOR);\n  const endOfWeekCellLocator = page.locator(ALL_DAY_GRID_CELL_SELECTOR).last();\n\n  // When\n  await dragAndDrop({ page, sourceLocator: resizerLocator, targetLocator: endOfWeekCellLocator });\n\n  // Then\n  await expect\n    .poll(() => getWidth(targetEventLocator))\n    .toBeGreaterThan(boundingBoxBeforeResizing.width);\n});\n\ntest.describe('When pressing down the ESC key', () => {\n  test('the resizing event resets to the initial size.', async ({ page }) => {\n    // Given\n    const eventLocator = page.locator(TARGET_EVENT_SELECTOR);\n    const eventBoundingBoxBeforeResize = await getBoundingBox(eventLocator);\n\n    const resizeHandlerLocator = eventLocator.locator(RESIZE_HANDLER_SELECTOR);\n\n    const targetCellLocator = page.locator(ALL_DAY_GRID_CELL_SELECTOR).nth(4);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: resizeHandlerLocator,\n      targetLocator: targetCellLocator,\n      hold: true,\n    });\n    await page.keyboard.down('Escape');\n\n    // Then\n    const eventBoundingBoxAfterResize = await getBoundingBox(eventLocator);\n    expect(eventBoundingBoxAfterResize).toEqual(eventBoundingBoxBeforeResize);\n  });\n});\n\ntest.describe('CSS class for a resize event', () => {\n  test('should be applied depending on a dragging state.', async ({ page }) => {\n    // Given\n    const eventLocator = page.locator(TARGET_EVENT_SELECTOR);\n    const resizeHandlerLocator = eventLocator.locator(RESIZE_HANDLER_SELECTOR);\n    const resizeHandlerBoundingBox = await getBoundingBox(resizeHandlerLocator);\n\n    const resizeEventClassLocator = page.locator(RESIZE_EVENT_SELECTOR);\n\n    // When (a drag has not started yet)\n    await page.mouse.move(resizeHandlerBoundingBox.x + 1, resizeHandlerBoundingBox.y + 3);\n    await page.mouse.down();\n\n    // Then\n    expect(await resizeEventClassLocator.count()).toBe(0);\n\n    // When (a drag is working)\n    await page.mouse.move(resizeHandlerBoundingBox.x + 10, resizeHandlerBoundingBox.y + 50);\n\n    // Then\n    expect(await resizeEventClassLocator.count()).toBe(1);\n\n    // When (a drag is finished)\n    await page.mouse.up();\n\n    // Then\n    expect(await resizeEventClassLocator.count()).toBe(0);\n  });\n\n  test('should not be applied when a drag is canceled.', async ({ page }) => {\n    // Given\n    const eventLocator = page.locator(TARGET_EVENT_SELECTOR);\n    const resizeHandlerLocator = eventLocator.locator(RESIZE_HANDLER_SELECTOR);\n\n    const resizeEventClassLocator = page.locator(RESIZE_EVENT_SELECTOR);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: resizeHandlerLocator,\n      targetLocator: resizeHandlerLocator,\n      options: {\n        targetPosition: { x: 10, y: 30 },\n      },\n      hold: true,\n    });\n    await page.keyboard.down('Escape');\n\n    // Then\n    expect(await resizeEventClassLocator.count()).toBe(0);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/playwright/week/dayGridSelection.e2e.ts",
    "content": "import type { Page } from '@playwright/test';\nimport { expect, test } from '@playwright/test';\n\nimport { assertDayGridSelectionMatching } from '../assertions';\nimport { WEEK_VIEW_PAGE_URL } from '../configs';\nimport { ClickDelay } from '../constants';\nimport { dragAndDrop, getPrefixedClassName, selectGridCells } from '../utils';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(WEEK_VIEW_PAGE_URL);\n});\n\nconst WEEK_GRID_CELL_SELECTOR = getPrefixedClassName('panel-grid');\nconst DAY_GRID_SELECTION_SELECTOR = getPrefixedClassName('grid-selection');\n\nfunction selectWeekGridCells(page: Page, startCellIndex: number, endCellIndex: number) {\n  return selectGridCells(page, startCellIndex, endCellIndex, WEEK_GRID_CELL_SELECTOR);\n}\n\nfunction assertWeekGridSelectionMatching(page: Page, startIndex: number, endIndex: number) {\n  return assertDayGridSelectionMatching(\n    page,\n    startIndex,\n    endIndex,\n    WEEK_GRID_CELL_SELECTOR,\n    DAY_GRID_SELECTION_SELECTOR\n  );\n}\n\ntest('select a cell by clicking.', async ({ page }) => {\n  // Given\n  const weekGridCellLocator = page.locator(WEEK_GRID_CELL_SELECTOR).nth(14);\n\n  // When\n  await weekGridCellLocator.click({ delay: ClickDelay.Short });\n\n  // Then\n  await assertWeekGridSelectionMatching(page, 14, 14);\n});\n\ntest('select a cell by double clicking.', async ({ page }) => {\n  // Given\n  const weekGridCellLocator = page.locator(WEEK_GRID_CELL_SELECTOR).nth(14);\n\n  // When\n  await weekGridCellLocator.dblclick({ delay: ClickDelay.Immediate });\n\n  // Then\n  await assertWeekGridSelectionMatching(page, 14, 14);\n});\n\ntest('select 2 cells from left to right', async ({ page }) => {\n  await selectWeekGridCells(page, 14, 15);\n\n  await assertWeekGridSelectionMatching(page, 14, 15);\n});\n\ntest('select 2 cells from right to left(reverse)', async ({ page }) => {\n  await selectWeekGridCells(page, 15, 14);\n\n  await assertWeekGridSelectionMatching(page, 14, 15);\n});\n\ntest('event form popup with grid selection', async ({ page }) => {\n  await selectWeekGridCells(page, 14, 15);\n\n  const floatingLayer = page.locator('css=[role=dialog]');\n\n  expect(floatingLayer).not.toBeNull();\n});\n\ntest('When pressing down the ESC key, the grid selection is canceled.', async ({ page }) => {\n  // Given\n  const startCellLocator = page.locator(WEEK_GRID_CELL_SELECTOR).nth(14);\n  const targetCellLocator = page.locator(WEEK_GRID_CELL_SELECTOR).nth(15);\n\n  // When\n  await dragAndDrop({\n    page,\n    sourceLocator: startCellLocator,\n    targetLocator: targetCellLocator,\n    hold: true,\n  });\n  await page.keyboard.down('Escape');\n\n  // Then\n  const gridSelectionLocator = page.locator(DAY_GRID_SELECTION_SELECTOR);\n  expect(await gridSelectionLocator.count()).toBe(0);\n});\n"
  },
  {
    "path": "apps/calendar/playwright/week/hourStartOption.e2e.ts",
    "content": "import { expect, test } from '@playwright/test';\n\nimport { WEEK_VIEW_PAGE_URL } from '../configs';\nimport { dragAndDrop, getBoundingBox, getTimeGridLineSelector } from '../utils';\n\n// Regression test for #1228\ntest.describe('Moving events with week.hourStart option', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto(WEEK_VIEW_PAGE_URL);\n  });\n\n  test('it should be able to move an event when the week.hourStart option is set', async ({\n    page,\n  }) => {\n    // Given\n    await page.evaluate(() => {\n      window.$cal.setOptions({\n        week: {\n          hourStart: 4,\n        },\n      });\n    });\n    const targetHourTextToMove = '08:00';\n    const targetEventLocator = page.locator('text=/short time event/');\n    const targetRowLocator = page.locator(getTimeGridLineSelector(targetHourTextToMove));\n    const { y: rowY } = await getBoundingBox(targetRowLocator);\n    const { x: eventX } = await getBoundingBox(targetEventLocator);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: targetEventLocator,\n      targetLocator: targetRowLocator,\n      options: {\n        targetPosition: {\n          x: eventX + 10,\n          y: 10,\n        },\n      },\n    });\n\n    // Then\n    const eventBoundingBoxAfterMoving = await getBoundingBox(targetEventLocator);\n    expect(eventBoundingBoxAfterMoving.y).toBeCloseTo(rowY, -1);\n\n    const parentText = await targetEventLocator.evaluate((el) => el?.parentElement?.textContent);\n    expect(parentText).toContain(targetHourTextToMove);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/playwright/week/primaryTimezone.e2e.ts",
    "content": "import { expect, test } from '@playwright/test';\n\nimport { mockWeekViewEvents } from '../../stories/mocks/mockWeekViewEvents';\nimport { WEEK_VIEW_TIMEZONE_PAGE_URL } from '../configs';\nimport { queryLocatorByTestId } from '../utils';\n\nconst [mockEvent] = mockWeekViewEvents.filter(({ title }) => title.includes('short'));\n\n// From local timezone (+09:00) to target timezone (+05:00)\nconst TARGET_TIMEZONE_HOUR_DIFFERENCE = 4;\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(WEEK_VIEW_TIMEZONE_PAGE_URL);\n});\n\ntest.describe('Primary Timezone', () => {\n  test(`${mockEvent.title} should be rendered at the top of the time grid in Pakistan Standard Time`, async ({\n    page,\n  }) => {\n    // Given\n    const targetEventLocator = queryLocatorByTestId(page, `time-event-${mockEvent.title}`);\n    const originalStartTime = mockEvent.start.toDate();\n    const originalStartHour = originalStartTime.getHours();\n    const originalStartMinute = originalStartTime.getMinutes();\n\n    const expectedEventStartTimeStr = `${String(\n      originalStartHour - TARGET_TIMEZONE_HOUR_DIFFERENCE\n    ).padStart(2, '0')}:${String(originalStartMinute).padStart(2, '0')}`;\n\n    // When\n    // Rendered\n\n    // Then\n    await expect(targetEventLocator).toHaveText(new RegExp(expectedEventStartTimeStr));\n  });\n});\n"
  },
  {
    "path": "apps/calendar/playwright/week/timeGridEventClick.e2e.ts",
    "content": "import { expect, test } from '@playwright/test';\nimport type { BoundingBox } from 'playwright/types';\n\nimport { mockWeekViewEvents } from '../../stories/mocks/mockWeekViewEvents';\nimport { WEEK_VIEW_DUPLICATE_EVENTS_PAGE_URL, WEEK_VIEW_PAGE_URL } from '../configs';\nimport { getBoundingBox, getTimeEventSelector } from '../utils';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(WEEK_VIEW_PAGE_URL);\n});\n\nconst targetEvents = mockWeekViewEvents.filter(({ isAllday }) => !isAllday);\ntargetEvents.forEach(({ title }) => {\n  test(`Click event: show popup when ${title} is clicked`, async ({ page }) => {\n    // Given\n    const targetEventSelector = getTimeEventSelector(title);\n    const targetEventLocator = page.locator(targetEventSelector).last();\n    const targetEventBoundingBox = await getBoundingBox(targetEventLocator);\n\n    // When\n    await page.mouse.move(targetEventBoundingBox.x + 2, targetEventBoundingBox.y + 2);\n    await page.mouse.down();\n    await page.mouse.up();\n\n    // Then\n    const detailPopup = page.locator('css=[role=dialog]');\n    await expect(detailPopup).toBeVisible();\n  });\n});\n\ntest.describe('Collapse duplicate events', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto(WEEK_VIEW_DUPLICATE_EVENTS_PAGE_URL);\n  });\n\n  const collapsedEvents = mockWeekViewEvents.filter(({ title }) =>\n    title.match(/duplicate event(\\s\\d)?$/)\n  );\n  const [collapsedEvent] = collapsedEvents;\n\n  test('The duplicate events are sorted according to the result of getDuplicateEvents option.', async ({\n    page,\n  }) => {\n    // Given\n    // getDuplicateEvents: sort by calendarId in descending order\n    const sortedDuplicateEvents = mockWeekViewEvents\n      .filter(({ title }) => title.startsWith('duplicate event 2'))\n      .sort((a, b) => (b.calendarId > a.calendarId ? 1 : -1));\n\n    // When\n    // Nothing\n\n    // Then\n    const promiseBoundingBoxes: Promise<BoundingBox>[] = [];\n    sortedDuplicateEvents.forEach((event) => {\n      const eventLocator = page.locator(getTimeEventSelector(event.title));\n      promiseBoundingBoxes.push(getBoundingBox(eventLocator));\n    });\n\n    await Promise.all(promiseBoundingBoxes).then((eventBoundingBoxes) => {\n      let prevX = -1;\n      eventBoundingBoxes.forEach(({ x }) => {\n        expect(prevX).toBeLessThan(x);\n        prevX = x;\n      });\n    });\n  });\n\n  collapsedEvents.forEach((event) => {\n    test(`When clicking the collapsed duplicate event, it should be expanded. - ${event.title}`, async ({\n      page,\n    }) => {\n      // Given\n      const collapsedEventLocator = page.locator(getTimeEventSelector(event.title));\n      const { x, y, width: widthBeforeClick } = await getBoundingBox(collapsedEventLocator);\n      const mainEventLocator = page.locator(getTimeEventSelector(`${event.title} - main`));\n      const { width: mainEventWidth } = await getBoundingBox(mainEventLocator);\n\n      // When\n      await page.mouse.move(x + 2, y + 2);\n      await page.mouse.down();\n      await page.mouse.up();\n\n      // Then\n      const { width: widthAfterClick } = await getBoundingBox(collapsedEventLocator);\n      expect(widthAfterClick).toBeGreaterThan(widthBeforeClick);\n      expect(widthAfterClick).toBeCloseTo(mainEventWidth, -1);\n    });\n  });\n\n  const otherEvents = mockWeekViewEvents.filter(({ title }) => {\n    return (\n      title === 'duplicate event with durations' || // duplicate event in the same duplicate event group\n      title === 'duplicate event 2' || // duplicate event but not in the same duplicate event group\n      title === 'short time event' // normal event\n    );\n  });\n  otherEvents.forEach((otherEvent) => {\n    test(`When clicking the other event (title: ${otherEvent.title}), the previous expanded event should be collapsed.`, async ({\n      page,\n    }) => {\n      // Given\n      const collapsedEventLocator = page.locator(getTimeEventSelector(collapsedEvent.title));\n      const { x, y, width: widthBeforeClick } = await getBoundingBox(collapsedEventLocator);\n      await page.mouse.move(x + 2, y + 2);\n      await page.mouse.down();\n      await page.mouse.up();\n\n      // When\n      const otherEventLocator = page.locator(getTimeEventSelector(otherEvent.title));\n      const {\n        x: otherX,\n        y: otherY,\n        width: otherWidthBeforeClick,\n      } = await getBoundingBox(otherEventLocator);\n      await page.mouse.move(otherX + 2, otherY + 2);\n      await page.mouse.down();\n      await page.mouse.up();\n\n      // Then\n      const { width: widthAfterClick } = await getBoundingBox(collapsedEventLocator);\n      const { width: otherWidthAfterClick } = await getBoundingBox(otherEventLocator);\n      expect(widthAfterClick).toBeCloseTo(widthBeforeClick, -1);\n\n      if (otherEvent.title.includes('duplicate')) {\n        // if the next clicked event is duplicate, it should be expanded.\n        expect(otherWidthAfterClick).toBeGreaterThan(otherWidthBeforeClick);\n      }\n    });\n  });\n\n  test('When clicking one day of a two-day duplicate event, the other day also should be expanded.', async ({\n    page,\n  }) => {\n    // Given\n    const longCollapsedEventTitle = 'duplicate long event';\n    const longCollapsedEventLocator = page.locator(getTimeEventSelector(longCollapsedEventTitle));\n    const firstDayLocator = longCollapsedEventLocator.first();\n    const lastDayLocator = longCollapsedEventLocator.last();\n    const { x, y, width: widthBeforeClick } = await getBoundingBox(firstDayLocator);\n\n    // When\n    await page.mouse.move(x + 2, y + 2);\n    await page.mouse.down();\n    await page.mouse.up();\n\n    // Then\n    const { width: widthAfterClick } = await getBoundingBox(firstDayLocator);\n    const { width: widthAfterClickOnLastDay } = await getBoundingBox(lastDayLocator);\n    expect(widthAfterClick).toBeGreaterThan(widthBeforeClick);\n    expect(widthAfterClickOnLastDay).toBeCloseTo(widthAfterClick);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/playwright/week/timeGridEventMoving.e2e.ts",
    "content": "import type { Locator } from '@playwright/test';\nimport { expect, test } from '@playwright/test';\n\nimport type TZDate from '../../src/time/date';\nimport { addHours, isSameDate } from '../../src/time/datetime';\nimport type { FormattedTimeString } from '../../src/types/time/datetime';\nimport { mockWeekViewEvents } from '../../stories/mocks/mockWeekViewEvents';\nimport { WEEK_VIEW_PAGE_URL } from '../configs';\nimport { Direction } from '../types';\nimport {\n  dragAndDrop,\n  getBoundingBox,\n  getGuideTimeEventSelector,\n  getTimeEventSelector,\n  getTimeGridLineSelector,\n  getTimeStrFromDate,\n} from '../utils';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(WEEK_VIEW_PAGE_URL);\n});\n\nconst TIME_EVENTS = mockWeekViewEvents.filter(({ isAllday }) => !isAllday);\nconst [TWO_VIEW_EVENT, SHORT_TIME_EVENT, LONG_TIME_EVENT] = TIME_EVENTS;\n\nconst MOVE_EVENT_SELECTOR = '[class*=\"dragging--move-event\"]';\n\n/**\n * 8 Directions for testing\n *\n * 7  0  1\n * 6     2\n * 5  4  3\n */\nconst cases: {\n  title: string;\n  eventColumnIndex: number;\n  directionInfo: {\n    direction: Direction;\n    startTimeAfterMoving: FormattedTimeString;\n    timeToDrop: FormattedTimeString;\n  }[];\n}[] = [\n  {\n    title: TWO_VIEW_EVENT.title,\n    eventColumnIndex: 0,\n    directionInfo: [\n      {\n        direction: Direction.Right,\n        startTimeAfterMoving: '10:00',\n        timeToDrop: '00:00',\n      },\n      {\n        direction: Direction.LowerRight,\n        startTimeAfterMoving: '12:00',\n        timeToDrop: '02:00',\n      },\n      {\n        direction: Direction.Down,\n        startTimeAfterMoving: '12:00',\n        timeToDrop: '02:00',\n      },\n    ],\n  },\n  {\n    title: SHORT_TIME_EVENT.title,\n    eventColumnIndex: 3,\n    directionInfo: [\n      {\n        direction: Direction.Up,\n        startTimeAfterMoving: '02:00',\n        timeToDrop: '02:00',\n      },\n      {\n        direction: Direction.UpperRight,\n        startTimeAfterMoving: '02:00',\n        timeToDrop: '02:00',\n      },\n      {\n        direction: Direction.Right,\n        startTimeAfterMoving: '04:00',\n        timeToDrop: '04:00',\n      },\n      {\n        direction: Direction.LowerRight,\n        startTimeAfterMoving: '06:00',\n        timeToDrop: '06:00',\n      },\n      {\n        direction: Direction.Down,\n        startTimeAfterMoving: '06:00',\n        timeToDrop: '06:00',\n      },\n      {\n        direction: Direction.LowerLeft,\n        startTimeAfterMoving: '06:00',\n        timeToDrop: '06:00',\n      },\n      {\n        direction: Direction.Left,\n        startTimeAfterMoving: '04:00',\n        timeToDrop: '04:00',\n      },\n      {\n        direction: Direction.UpperLeft,\n        startTimeAfterMoving: '02:00',\n        timeToDrop: '02:00',\n      },\n    ],\n  },\n  {\n    title: LONG_TIME_EVENT.title,\n    eventColumnIndex: 6,\n    directionInfo: [\n      {\n        direction: Direction.Down,\n        startTimeAfterMoving: '12:00',\n        timeToDrop: '02:00',\n      },\n      {\n        direction: Direction.LowerLeft,\n        startTimeAfterMoving: '12:00',\n        timeToDrop: '02:00',\n      },\n      {\n        direction: Direction.Left,\n        startTimeAfterMoving: '10:00',\n        timeToDrop: '00:00',\n      },\n    ],\n  },\n];\n\ncases.forEach(({ title, eventColumnIndex, directionInfo }) => {\n  directionInfo.forEach(({ direction, startTimeAfterMoving, timeToDrop }) => {\n    test(`Moving event: ${title} for ${direction}`, async ({ page }) => {\n      // Given\n      const eventLocator = page.locator(getTimeEventSelector(title)).last();\n      const targetRowLocator = page.locator(getTimeGridLineSelector(timeToDrop));\n      const targetColumnLocator = page.locator(`data-testid=timegrid-column-${eventColumnIndex}`);\n\n      const targetColumnBoundingBox = await getBoundingBox(targetColumnLocator);\n\n      // When\n      await dragAndDrop({\n        page,\n        sourceLocator: eventLocator,\n        targetLocator: targetRowLocator,\n        options: {\n          sourcePosition: {\n            x: 5,\n            y: 5,\n          },\n          targetPosition: {\n            y: 5,\n            x: targetColumnBoundingBox.x - 5,\n          },\n        },\n      });\n\n      // Then\n      const eventBoundingBoxAfterMove = await getBoundingBox(eventLocator);\n      expect(eventBoundingBoxAfterMove.x).toBeCloseTo(targetColumnBoundingBox.x, -1);\n      await expect.poll(() => eventLocator.textContent()).toMatch(new RegExp(startTimeAfterMoving));\n    });\n  });\n});\n\ntest('When pressing down the ESC key, the moving event resets to the initial position.', async ({\n  page,\n}) => {\n  // Given\n  const eventLocator = page.locator(getTimeEventSelector(SHORT_TIME_EVENT.title));\n  const eventBoundingBoxBeforeMove = await getBoundingBox(eventLocator);\n\n  const targetStartTime = getTimeStrFromDate(\n    addHours(SHORT_TIME_EVENT.end as TZDate, 1)\n  ) as FormattedTimeString;\n  const targetRowLocator = page.locator(getTimeGridLineSelector(targetStartTime));\n\n  // When\n  await dragAndDrop({\n    page,\n    sourceLocator: eventLocator,\n    targetLocator: targetRowLocator,\n    hold: true,\n  });\n  await page.keyboard.down('Escape');\n\n  // Then\n  const eventBoundingBoxAfterMove = await getBoundingBox(eventLocator);\n  expect(eventBoundingBoxAfterMove).toEqual(eventBoundingBoxBeforeMove);\n});\n\ntest.describe('CSS class for a move event', () => {\n  test('should be applied depending on a dragging state.', async ({ page }) => {\n    // Given\n    const eventLocator = page.locator(getTimeEventSelector(SHORT_TIME_EVENT.title));\n    const eventBoundingBox = await getBoundingBox(eventLocator);\n    const moveEventClassLocator = page.locator(MOVE_EVENT_SELECTOR);\n\n    // When (a drag has not started yet)\n    await page.mouse.move(eventBoundingBox.x + 10, eventBoundingBox.y + 10);\n    await page.mouse.down();\n\n    // Then\n    expect(await moveEventClassLocator.count()).toBe(0);\n\n    // When (a drag is working)\n    await page.mouse.move(eventBoundingBox.x + 10, eventBoundingBox.y + 50);\n\n    // Then\n    expect(await moveEventClassLocator.count()).toBe(1);\n\n    // When (a drag is finished)\n    await page.mouse.up();\n\n    // Then\n    expect(await moveEventClassLocator.count()).toBe(0);\n  });\n\n  test('should not be applied when a drag is canceled.', async ({ page }) => {\n    // Given\n    const eventLocator = page.locator(getTimeEventSelector(SHORT_TIME_EVENT.title));\n    const moveEventClassLocator = page.locator(MOVE_EVENT_SELECTOR);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: eventLocator,\n      targetLocator: eventLocator,\n      options: {\n        targetPosition: { x: 10, y: 30 },\n      },\n      hold: true,\n    });\n    await page.keyboard.down('Escape');\n\n    // Then\n    expect(await moveEventClassLocator.count()).toBe(0);\n  });\n});\n\ntest.describe(`Calibrate event's height while dragging`, () => {\n  let lowerLongTimeEventLocator: Locator;\n  let upperLongTimeEventLocator: Locator;\n  let guideLocator: Locator;\n\n  test.beforeEach(({ page }) => {\n    const targetEventSelector = getTimeEventSelector(LONG_TIME_EVENT.title);\n\n    lowerLongTimeEventLocator = page.locator(targetEventSelector).first();\n    upperLongTimeEventLocator = page.locator(targetEventSelector).last();\n    guideLocator = page.locator(getGuideTimeEventSelector());\n  });\n\n  test('lower long time event become longer while drag to upper side', async ({ page }) => {\n    // Given\n    const eventBoundingBox = await getBoundingBox(lowerLongTimeEventLocator);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: lowerLongTimeEventLocator,\n      targetLocator: lowerLongTimeEventLocator,\n      options: {\n        sourcePosition: { x: 10, y: 10 },\n        targetPosition: { x: 10, y: -10 },\n      },\n      hold: true,\n    });\n\n    // Then\n    const guideBoundingBox = await getBoundingBox(guideLocator);\n    expect(guideBoundingBox.y).toBeLessThan(eventBoundingBox.y);\n    expect(guideBoundingBox.height).toBeGreaterThan(eventBoundingBox.height);\n  });\n\n  test('lower long time event become shorter while drag to lower side', async ({ page }) => {\n    // Given\n    const eventBoundingBox = await getBoundingBox(lowerLongTimeEventLocator);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: lowerLongTimeEventLocator,\n      targetLocator: lowerLongTimeEventLocator,\n      options: {\n        sourcePosition: { x: 10, y: 10 },\n        targetPosition: { x: 10, y: 30 },\n      },\n      hold: true,\n    });\n\n    // Then\n    const guideBoundingBox = await getBoundingBox(guideLocator);\n    // NOTE: the guide event's height is greater than event's height, but it looks like it isn't.\n    //       height is truncated because of stacking context.\n    expect(guideBoundingBox.y).toBeGreaterThan(eventBoundingBox.y);\n  });\n\n  test('upper long time event become longer while drag to lower side', async ({ page }) => {\n    // Given\n    const eventBoundingBox = await getBoundingBox(upperLongTimeEventLocator);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: upperLongTimeEventLocator,\n      targetLocator: upperLongTimeEventLocator,\n      options: {\n        sourcePosition: { x: 10, y: 10 },\n        targetPosition: { x: 10, y: 30 },\n      },\n      hold: true,\n    });\n\n    // Then\n    const guideBoundingBox = await getBoundingBox(guideLocator);\n    expect(guideBoundingBox.height).toBeGreaterThan(eventBoundingBox.height);\n  });\n\n  test('upper long time event become shorter while drag to upper side', async ({ page }) => {\n    // Given\n    const eventBoundingBox = await getBoundingBox(upperLongTimeEventLocator);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: upperLongTimeEventLocator,\n      targetLocator: upperLongTimeEventLocator,\n      options: {\n        sourcePosition: { x: 10, y: 100 },\n        targetPosition: { x: 10, y: 50 },\n      },\n      hold: true,\n    });\n\n    // Then\n    const guideBoundingBox = await getBoundingBox(guideLocator);\n    expect(guideBoundingBox.height).toBeLessThan(eventBoundingBox.height);\n  });\n});\n\nconst ONE_DAY_TIME_EVENTS = mockWeekViewEvents.filter(\n  ({ isAllday, start, end }) => !isAllday && isSameDate(start, end)\n);\n\nONE_DAY_TIME_EVENTS.forEach(({ title }) => {\n  test(`The height of guide element should be same as the event element. - ${title}`, async ({\n    page,\n  }) => {\n    // Given\n    const eventLocator = page.locator(getTimeEventSelector(title));\n    const eventBoundingBox = await getBoundingBox(eventLocator);\n\n    const targetRowLocator = page.locator(getTimeGridLineSelector('02:00'));\n    const targetColumnLocator = page.locator('data-testid=timegrid-column-2');\n    const targetColumnBoundingBox = await getBoundingBox(targetColumnLocator);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: eventLocator,\n      targetLocator: targetRowLocator,\n      options: {\n        sourcePosition: {\n          x: 5,\n          y: 5,\n        },\n        targetPosition: {\n          y: 5,\n          x: targetColumnBoundingBox.x - 5,\n        },\n      },\n      hold: true,\n    });\n\n    // Then\n    const guideLocator = page.locator(getGuideTimeEventSelector());\n    const guideBoundingBox = await getBoundingBox(guideLocator);\n    expect(guideBoundingBox.height).toBeCloseTo(eventBoundingBox.height, 0);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/playwright/week/timeGridEventResizing.e2e.ts",
    "content": "import type { Page } from '@playwright/test';\nimport { expect, test } from '@playwright/test';\nimport type { Matchers } from '@playwright/test/types/expect-types';\n\nimport type TZDate from '../../src/time/date';\nimport { addHours } from '../../src/time/datetime';\nimport type { FormattedTimeString } from '../../src/types/time/datetime';\nimport { mockWeekViewEvents } from '../../stories/mocks/mockWeekViewEvents';\nimport { WEEK_VIEW_PAGE_URL } from '../configs';\nimport {\n  dragAndDrop,\n  getBoundingBox,\n  getGuideTimeEventSelector,\n  getTimeEventSelector,\n  getTimeGridLineSelector,\n  getTimeStrFromDate,\n  queryLocatorByTestId,\n  waitForSingleElement,\n} from '../utils';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(WEEK_VIEW_PAGE_URL);\n});\n\nconst RESIZE_HANDLER_SELECTOR = '[class*=\"resize-handler\"]';\nconst RESIZE_EVENT_SELECTOR = '[class*=\"dragging--resize-vertical-event\"]';\n\nfunction getHourDifference(minuend: FormattedTimeString, subtrahend: FormattedTimeString) {\n  return Number(minuend.split(':')[0]) - Number(subtrahend.split(':')[0]);\n}\n\ninterface EventInfo {\n  title: string;\n  startTime: FormattedTimeString;\n  endTime: FormattedTimeString;\n  endDateColumnIndex: number;\n}\n\nconst [TWO_VIEW_EVENT, SHORT_TIME_EVENT, LONG_TIME_EVENT] = mockWeekViewEvents.filter(\n  ({ isAllday }) => !isAllday\n);\n\nconst targetEvents: EventInfo[] = [\n  {\n    title: TWO_VIEW_EVENT.title,\n    startTime: '10:00',\n    endTime: '06:00',\n    endDateColumnIndex: 0,\n  },\n  {\n    title: SHORT_TIME_EVENT.title,\n    startTime: '04:00',\n    endTime: '06:00',\n    endDateColumnIndex: 3,\n  },\n  {\n    title: LONG_TIME_EVENT.title,\n    startTime: '10:00',\n    endTime: '06:00',\n    endDateColumnIndex: 6,\n  },\n];\n\nconst cases: {\n  title: string;\n  targetEndTime: FormattedTimeString;\n  targetColumnIndex?: number;\n  matcherToCompare: Extract<keyof Matchers<number>, 'toBeGreaterThan' | 'toBeLessThan'>;\n}[] = [\n  {\n    title: 'to the bottom',\n    targetEndTime: '08:00',\n    matcherToCompare: 'toBeGreaterThan',\n  },\n  {\n    title: 'to the top',\n    targetEndTime: '04:00',\n    matcherToCompare: 'toBeLessThan',\n  },\n  {\n    title: 'up to the y of the cursor when the target column is to the right of the current column',\n    targetEndTime: '08:00',\n    targetColumnIndex: 5,\n    matcherToCompare: 'toBeGreaterThan',\n  },\n  {\n    title: 'up to the y of the cursor when the target column is to the left of the current column',\n    targetEndTime: '04:00',\n    targetColumnIndex: 0,\n    matcherToCompare: 'toBeLessThan',\n  },\n];\n\nasync function setup({\n  page,\n  targetEventTitle,\n  targetEndTime,\n  targetColumnIndex,\n}: {\n  page: Page;\n  targetEventTitle: string;\n  targetEndTime: FormattedTimeString;\n  targetColumnIndex: number;\n}) {\n  // Given\n  const eventLocator = queryLocatorByTestId(page, `time-event-${targetEventTitle}`).last();\n  const resizeHandlerLocator = eventLocator.locator(RESIZE_HANDLER_SELECTOR);\n  const targetRowLocator = page.locator(getTimeGridLineSelector(targetEndTime));\n  const targetColumnLocator = queryLocatorByTestId(page, `timegrid-column-${targetColumnIndex}`);\n  const eventBoundingBoxBeforeResize = await getBoundingBox(eventLocator);\n\n  const targetRowBoundingBox = await getBoundingBox(targetRowLocator);\n  const targetColumnBoundingBox = await getBoundingBox(targetColumnLocator);\n\n  // When\n  await dragAndDrop({\n    page,\n    sourceLocator: resizeHandlerLocator,\n    targetLocator: targetRowLocator,\n    options: {\n      sourcePosition: {\n        x: 1,\n        y: 1,\n      },\n      targetPosition: {\n        x: targetColumnBoundingBox.x + 1,\n        y: targetRowBoundingBox.height / 2,\n      },\n    },\n  });\n  await waitForSingleElement(eventLocator);\n\n  let eventBoundingBoxAfterResize = await getBoundingBox(eventLocator);\n  await expect\n    .poll(async () => {\n      eventBoundingBoxAfterResize = await getBoundingBox(eventLocator);\n\n      return eventBoundingBoxAfterResize;\n    })\n    .not.toEqual(eventBoundingBoxBeforeResize);\n\n  return {\n    eventLocator,\n    eventBoundingBoxBeforeResize,\n    eventBoundingBoxAfterResize,\n    targetRowBoundingBox,\n  };\n}\n\ntargetEvents.forEach(({ title: eventTitle, startTime, endTime, endDateColumnIndex }) => {\n  test.describe(`Resize a ${eventTitle} in the time grid`, () => {\n    cases.forEach(\n      ({ title, targetEndTime, targetColumnIndex, matcherToCompare: compareAssertion }) => {\n        test(`${title}`, async ({ page }) => {\n          // Given\n\n          // When\n          const {\n            eventLocator,\n            eventBoundingBoxBeforeResize,\n            eventBoundingBoxAfterResize,\n            targetRowBoundingBox,\n          } = await setup({\n            page,\n            targetEventTitle: eventTitle,\n            targetEndTime,\n            targetColumnIndex: targetColumnIndex || endDateColumnIndex,\n          });\n\n          // Then\n          expect(eventBoundingBoxAfterResize.height)[compareAssertion](\n            eventBoundingBoxBeforeResize.height\n          );\n\n          await expect(eventLocator).toContainText(startTime);\n\n          const rowCount = getHourDifference(targetEndTime, endTime) * 2 + 1;\n          expect(\n            eventBoundingBoxAfterResize.height - eventBoundingBoxBeforeResize.height\n          ).toBeCloseTo(targetRowBoundingBox.height * rowCount, -1);\n        });\n      }\n    );\n\n    test(`then it should have a minimum height(=1 row) even if the event is resized to before the start time`, async ({\n      page,\n    }) => {\n      // Given\n\n      // When\n      const { eventBoundingBoxAfterResize, targetRowBoundingBox } = await setup({\n        page,\n        targetEventTitle: eventTitle,\n        targetEndTime: '00:00',\n        targetColumnIndex: endDateColumnIndex,\n      });\n\n      // Then\n      expect(eventBoundingBoxAfterResize.height).toBeCloseTo(targetRowBoundingBox.height, -1);\n    });\n  });\n});\n\ntest('When pressing down the ESC key, the resizing event resets to the initial size.', async ({\n  page,\n}) => {\n  // Given\n  const eventLocator = page.locator(getTimeEventSelector(SHORT_TIME_EVENT.title));\n  const eventBoundingBoxBeforeResize = await getBoundingBox(eventLocator);\n\n  const resizeHandlerLocator = eventLocator.locator(RESIZE_HANDLER_SELECTOR);\n\n  const targetStartTime = getTimeStrFromDate(\n    addHours(SHORT_TIME_EVENT.end as TZDate, 1)\n  ) as FormattedTimeString;\n  const targetRowLocator = page.locator(getTimeGridLineSelector(targetStartTime));\n\n  // When\n  await dragAndDrop({\n    page,\n    sourceLocator: resizeHandlerLocator,\n    targetLocator: targetRowLocator,\n    hold: true,\n  });\n  await page.keyboard.down('Escape');\n\n  // Then\n  const eventBoundingBoxAfterResize = await getBoundingBox(eventLocator);\n  expect(eventBoundingBoxAfterResize).toEqual(eventBoundingBoxBeforeResize);\n});\n\ntest.describe('CSS class for a resize event', () => {\n  test('should be applied depending on a dragging state.', async ({ page }) => {\n    // Given\n    const eventLocator = page.locator(getTimeEventSelector(SHORT_TIME_EVENT.title));\n    const resizeHandlerLocator = eventLocator.locator(RESIZE_HANDLER_SELECTOR);\n    const resizeHandlerBoundingBox = await getBoundingBox(resizeHandlerLocator);\n\n    const resizeEventClassLocator = page.locator(RESIZE_EVENT_SELECTOR);\n\n    // When (a drag has not started yet)\n    await page.mouse.move(resizeHandlerBoundingBox.x + 10, resizeHandlerBoundingBox.y + 3);\n    await page.mouse.down();\n\n    // Then\n    expect(await resizeEventClassLocator.count()).toBe(0);\n\n    // When (a drag is working)\n    await page.mouse.move(resizeHandlerBoundingBox.x + 10, resizeHandlerBoundingBox.y + 50);\n\n    // Then\n    expect(await resizeEventClassLocator.count()).toBe(1);\n\n    // When (a drag is finished)\n    await page.mouse.up();\n\n    // Then\n    expect(await resizeEventClassLocator.count()).toBe(0);\n  });\n\n  test('should not be applied when a drag is canceled.', async ({ page }) => {\n    // Given\n    const eventLocator = page.locator(getTimeEventSelector(SHORT_TIME_EVENT.title));\n    const resizeHandlerLocator = eventLocator.locator(RESIZE_HANDLER_SELECTOR);\n\n    const resizeEventClassLocator = page.locator(RESIZE_EVENT_SELECTOR);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: resizeHandlerLocator,\n      targetLocator: resizeHandlerLocator,\n      options: {\n        targetPosition: { x: 10, y: 50 },\n      },\n    });\n    await page.keyboard.down('Escape');\n\n    // Then\n    expect(await resizeEventClassLocator.count()).toBe(0);\n  });\n});\n\ntest.describe('Resizing the event which has a going or coming duration', () => {\n  const [event] = mockWeekViewEvents.filter(\n    ({ isAllday, goingDuration, comingDuration }) => !isAllday && (goingDuration || comingDuration)\n  );\n\n  async function setupResizingGoingOrComingDuration(\n    page: Page,\n    targetEndTime: FormattedTimeString,\n    hold?: boolean\n  ) {\n    const eventLocator = queryLocatorByTestId(page, `time-event-${event.title}`).last();\n\n    const travelTimeLocator = eventLocator.locator('[class*=\"travel-time\"]');\n    const goingDurationLocator = travelTimeLocator.first();\n    const goingDurationBoundingBoxBeforeResize = await getBoundingBox(goingDurationLocator);\n    const comingDurationLocator = travelTimeLocator.last();\n    const comingDurationBoundingBoxBeforeResize = await getBoundingBox(comingDurationLocator);\n\n    const eventTimeLocator = eventLocator.locator('[class*=\"event-time\"]');\n    const modelDurationBoundingBoxBeforeResize = await getBoundingBox(eventTimeLocator);\n\n    const resizeHandlerLocator = eventLocator.locator(RESIZE_HANDLER_SELECTOR);\n    const targetRowLocator = page.locator(getTimeGridLineSelector(targetEndTime));\n    const targetRowBoundingBox = await getBoundingBox(targetRowLocator);\n\n    await dragAndDrop({\n      page,\n      sourceLocator: resizeHandlerLocator,\n      targetLocator: targetRowLocator,\n      options: {\n        sourcePosition: {\n          x: 1,\n          y: 1,\n        },\n        targetPosition: {\n          x: targetRowBoundingBox.width / 2,\n          y: targetRowBoundingBox.height / 2,\n        },\n      },\n      hold,\n    });\n\n    let draggingGoingDurationBoundingBox = null;\n    let draggingComingDurationBoundingBox = null;\n    let draggingModelDurationBoundingBox = null;\n    if (hold) {\n      const draggingEventLocator = page.locator(getGuideTimeEventSelector());\n      const draggingTravelTimeLocator = draggingEventLocator.locator('[class*=\"travel-time\"]');\n      const draggingGoingDurationLocator = draggingTravelTimeLocator.first();\n      draggingGoingDurationBoundingBox = await getBoundingBox(draggingGoingDurationLocator);\n      const draggingComingDurationLocator = draggingTravelTimeLocator.last();\n      draggingComingDurationBoundingBox = await getBoundingBox(draggingComingDurationLocator);\n\n      const draggingEventTimeLocator = draggingEventLocator.locator('[class*=\"event-time\"]');\n      draggingModelDurationBoundingBox = await getBoundingBox(draggingEventTimeLocator);\n    }\n\n    const goingDurationBoundingBoxAfterResize = await getBoundingBox(goingDurationLocator);\n    const comingDurationBoundingBoxAfterResize = await getBoundingBox(comingDurationLocator);\n    const modelDurationBoundingBoxAfterResize = await getBoundingBox(eventTimeLocator);\n\n    return {\n      goingDurationBoundingBoxBeforeResize,\n      goingDurationBoundingBoxAfterResize,\n      comingDurationBoundingBoxBeforeResize,\n      comingDurationBoundingBoxAfterResize,\n      modelDurationBoundingBoxBeforeResize,\n      modelDurationBoundingBoxAfterResize,\n      draggingGoingDurationBoundingBox,\n      draggingComingDurationBoundingBox,\n      draggingModelDurationBoundingBox,\n      rowHeight: targetRowBoundingBox.height,\n    };\n  }\n\n  test('should change only the end time.', async ({ page }) => {\n    // Given\n    const targetEndTime = '10:00';\n\n    // When\n    const {\n      goingDurationBoundingBoxBeforeResize,\n      goingDurationBoundingBoxAfterResize,\n      comingDurationBoundingBoxBeforeResize,\n      comingDurationBoundingBoxAfterResize,\n      modelDurationBoundingBoxBeforeResize,\n      modelDurationBoundingBoxAfterResize,\n    } = await setupResizingGoingOrComingDuration(page, targetEndTime);\n\n    // Then\n    expect(goingDurationBoundingBoxAfterResize.height).toBeCloseTo(\n      goingDurationBoundingBoxBeforeResize.height,\n      -1\n    );\n    expect(comingDurationBoundingBoxAfterResize.height).toBeCloseTo(\n      comingDurationBoundingBoxBeforeResize.height,\n      -1\n    );\n    expect(modelDurationBoundingBoxAfterResize.height).toBeGreaterThan(\n      modelDurationBoundingBoxBeforeResize.height\n    );\n  });\n\n  test('the dragging element should have a minimum height(=1 row) without going and coming durations.', async ({\n    page,\n  }) => {\n    // Given\n    const targetEndTime = '04:00';\n\n    // When\n    const {\n      goingDurationBoundingBoxBeforeResize,\n      comingDurationBoundingBoxBeforeResize,\n      draggingGoingDurationBoundingBox,\n      draggingComingDurationBoundingBox,\n      draggingModelDurationBoundingBox,\n      rowHeight,\n    } = await setupResizingGoingOrComingDuration(page, targetEndTime, true);\n\n    // Then\n    expect(draggingGoingDurationBoundingBox?.height).toBeCloseTo(\n      goingDurationBoundingBoxBeforeResize.height,\n      -1\n    );\n    expect(draggingComingDurationBoundingBox?.height).toBeCloseTo(\n      comingDurationBoundingBoxBeforeResize.height,\n      -1\n    );\n    expect(draggingModelDurationBoundingBox?.height).toBeCloseTo(rowHeight, -1);\n  });\n\n  test('the result of resizing should have a minimum height(=1 row) without going and coming durations.', async ({\n    page,\n  }) => {\n    // Given\n    const targetEndTime = '04:00';\n\n    // When\n    const {\n      goingDurationBoundingBoxBeforeResize,\n      goingDurationBoundingBoxAfterResize,\n      comingDurationBoundingBoxBeforeResize,\n      comingDurationBoundingBoxAfterResize,\n      modelDurationBoundingBoxAfterResize,\n      rowHeight,\n    } = await setupResizingGoingOrComingDuration(page, targetEndTime);\n\n    // Then\n    expect(goingDurationBoundingBoxAfterResize.height).toBeCloseTo(\n      goingDurationBoundingBoxBeforeResize.height,\n      -1\n    );\n    expect(comingDurationBoundingBoxAfterResize.height).toBeCloseTo(\n      comingDurationBoundingBoxBeforeResize.height,\n      -1\n    );\n    expect(modelDurationBoundingBoxAfterResize.height).toBeCloseTo(rowHeight, -1);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/playwright/week/timeGridScrollSync.e2e.ts",
    "content": "import type { Page } from '@playwright/test';\nimport { expect, test } from '@playwright/test';\n\nimport { WEEK_VIEW_PAGE_URL } from '../configs';\nimport { getBoundingBox, getPrefixedClassName } from '../utils';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(WEEK_VIEW_PAGE_URL);\n});\n\n// NOTE: Syncing scroll only happens when the mousemove event is fired\n// and cannot use `dragAndDrop` because it's better to be manually controlled.\n\nfunction getScrollTop(el: HTMLElement) {\n  return el.scrollTop;\n}\n\ntest.describe('Scroll syncing in time grid when selecting grid', () => {\n  /**\n   * The third column should be empty\n   */\n  async function setup(page: Page) {\n    const timeGridContainerLocator = page.locator(\n      `${getPrefixedClassName('panel')}${getPrefixedClassName('time')}`\n    );\n    const targetColumnLocator = page.locator('data-testid=timegrid-column-2');\n\n    const containerBoundingBox = await getBoundingBox(timeGridContainerLocator);\n    const columnBoundingBox = await getBoundingBox(targetColumnLocator);\n\n    return {\n      targetColumnLocator,\n      timeGridContainerLocator,\n      containerBoundingBox,\n      columnBoundingBox,\n    };\n  }\n\n  test('it should sync scroll while dragging down to the bottom', async ({ page }) => {\n    // Given\n    const {\n      targetColumnLocator,\n      columnBoundingBox,\n      timeGridContainerLocator,\n      containerBoundingBox,\n    } = await setup(page);\n    const scrollTopBeforeSync = await timeGridContainerLocator.evaluate(getScrollTop);\n\n    // When\n    await targetColumnLocator.hover({\n      position: {\n        x: columnBoundingBox.width / 2,\n        y: 10,\n      },\n      force: true,\n    });\n    await page.mouse.down();\n    await page.mouse.move(\n      columnBoundingBox.x + columnBoundingBox.width / 2,\n      containerBoundingBox.y + containerBoundingBox.height - 10\n    );\n\n    // Then\n    const scrollTopAfterSync = await timeGridContainerLocator.evaluate(getScrollTop);\n    expect(scrollTopAfterSync).toBeGreaterThan(scrollTopBeforeSync);\n  });\n\n  test('it should sync scroll while dragging up to the top', async ({ page }) => {\n    // Given\n    const {\n      targetColumnLocator,\n      columnBoundingBox,\n      timeGridContainerLocator,\n      containerBoundingBox,\n    } = await setup(page);\n\n    // Middle of the column\n    const xPosition = columnBoundingBox.x + columnBoundingBox.width / 2;\n\n    // Scroll down to the bottom of the column\n    await targetColumnLocator.hover();\n    await page.mouse.wheel(0, containerBoundingBox.height);\n    let scrollTopBeforeSync = await timeGridContainerLocator.evaluate(getScrollTop);\n    await expect\n      .poll(async () => {\n        scrollTopBeforeSync = await timeGridContainerLocator.evaluate(getScrollTop);\n\n        return scrollTopBeforeSync;\n      })\n      .toBeCloseTo(containerBoundingBox.height, -2);\n\n    // When\n    // drag up to the top of the column\n    await page.mouse.move(xPosition, containerBoundingBox.y + containerBoundingBox.height - 10);\n    await page.mouse.down();\n\n    await expect\n      .poll(async () => {\n        await page.mouse.move(xPosition, containerBoundingBox.y);\n\n        // Then\n        const scrollTopAfterSync = await timeGridContainerLocator.evaluate(getScrollTop);\n        return scrollTopAfterSync;\n      })\n      .toBeLessThan(scrollTopBeforeSync);\n  });\n});\n\ntest.describe('Scroll syncing in time grid when moving event', () => {\n  async function setup(page: Page) {\n    const timeGridContainerLocator = page.locator(\n      `${getPrefixedClassName('panel')}${getPrefixedClassName('time')}`\n    );\n    const targetEventLocator = page.locator(\n      '[data-testid*=\"time-event\"]:has-text(\"short time event\")'\n    );\n    const containerBoundingBox = await getBoundingBox(timeGridContainerLocator);\n    const eventBoundingBox = await getBoundingBox(targetEventLocator);\n\n    return {\n      timeGridContainerLocator,\n      targetEventLocator,\n      containerBoundingBox,\n      eventBoundingBox,\n    };\n  }\n\n  test('it should sync scroll while moving event to the edge of the bottom', async ({ page }) => {\n    // Given\n    const { timeGridContainerLocator, targetEventLocator, containerBoundingBox, eventBoundingBox } =\n      await setup(page);\n    const scrollTopBeforeSync = await timeGridContainerLocator.evaluate(getScrollTop);\n\n    // When\n    await targetEventLocator.hover({\n      position: {\n        x: eventBoundingBox.width / 2,\n        y: 3,\n      },\n      force: true,\n    });\n    await page.mouse.down();\n    await page.mouse.move(\n      eventBoundingBox.x + eventBoundingBox.width / 2,\n      containerBoundingBox.y + containerBoundingBox.height - 10\n    );\n    await page.mouse.up();\n\n    // Then\n    const scrollTopAfterSync = await timeGridContainerLocator.evaluate(getScrollTop);\n    expect(scrollTopAfterSync).toBeGreaterThan(scrollTopBeforeSync);\n  });\n\n  test('it should sync scroll while moving event to the edge of the top', async ({ page }) => {\n    // Given\n    const { timeGridContainerLocator, targetEventLocator, containerBoundingBox, eventBoundingBox } =\n      await setup(page);\n\n    // Let's move the event to the bottom first.\n    const middleXOfEvent = eventBoundingBox.x + eventBoundingBox.width / 2;\n    await targetEventLocator.hover({\n      position: {\n        x: eventBoundingBox.width / 2,\n        y: 3,\n      },\n      force: true,\n    });\n    await page.mouse.down();\n    await page.mouse.move(\n      middleXOfEvent,\n      containerBoundingBox.y + containerBoundingBox.height - 10\n    );\n    await page.mouse.up();\n\n    // Then scroll down a little.\n    await page.mouse.wheel(0, containerBoundingBox.height / 2);\n    let scrollTopBeforeSync = await timeGridContainerLocator.evaluate(getScrollTop);\n    await expect\n      .poll(async () => {\n        scrollTopBeforeSync = await timeGridContainerLocator.evaluate(getScrollTop);\n\n        return scrollTopBeforeSync;\n      })\n      .toBeCloseTo(containerBoundingBox.height / 2, -2);\n\n    // When\n    await targetEventLocator.hover({\n      position: {\n        x: eventBoundingBox.width / 2,\n        y: 3,\n      },\n      force: true,\n    });\n    await page.mouse.down();\n\n    await expect\n      .poll(async () => {\n        await page.mouse.move(middleXOfEvent, containerBoundingBox.y - 10);\n\n        // Then\n        const scrollTopAfterSync = await timeGridContainerLocator.evaluate(getScrollTop);\n        return scrollTopAfterSync;\n      })\n      .toBeLessThan(scrollTopBeforeSync);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/playwright/week/timeGridSelection.e2e.ts",
    "content": "import { expect, test } from '@playwright/test';\n\nimport { assertTimeGridSelection } from '../assertions';\nimport { WEEK_VIEW_HOUR_START_OPTION_PAGE_URL, WEEK_VIEW_PAGE_URL } from '../configs';\nimport { ClickDelay } from '../constants';\nimport { dragAndDrop, getBoundingBox, getTimeGridLineSelector } from '../utils';\n\nconst GRID_SELECTION_SELECTOR = '[data-testid*=\"time-grid-selection\"]';\n\ntest.describe('Time Grid Selection - Default Options', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto(WEEK_VIEW_PAGE_URL);\n  });\n\n  // NOTE: Only firefox automatically scrolls into view at some random tests, so narrowing the range of movement.\n  // Maybe `scrollIntoViewIfNeeded` is not supported in the firefox?\n  // reference: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoViewIfNeeded\n  const BASE_GRIDLINE_SELECTOR = 'data-testid=gridline-03:00-03:30';\n\n  test('should be able to select a time slot with clicking', async ({ page }) => {\n    // Given\n    const startGridLineLocator = page.locator(BASE_GRIDLINE_SELECTOR);\n    const timeGridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);\n    const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);\n\n    // When\n    await startGridLineLocator.click({ force: true, delay: ClickDelay.Short });\n    await timeGridSelectionLocator.waitFor(); // Test for debounced click handler.\n\n    // Then\n    await assertTimeGridSelection(timeGridSelectionLocator, {\n      totalElements: 1,\n      formattedTimes: ['03:00', '03:30'],\n      startTop: startGridLineBoundingBox.y,\n      endBottom: startGridLineBoundingBox.y + startGridLineBoundingBox.height,\n    });\n  });\n\n  // FIXME: it fails on safari & firefox\n  test.fixme('should be able to select a time slot with double clicking', async ({ page }) => {\n    // Given\n    const startGridLineLocator = page.locator(BASE_GRIDLINE_SELECTOR);\n    const timeGridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);\n    const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);\n\n    // When\n    await startGridLineLocator.dblclick({ force: true, delay: ClickDelay.Immediate });\n\n    // Then\n    await assertTimeGridSelection(timeGridSelectionLocator, {\n      totalElements: 1,\n      formattedTimes: ['03:00', '03:30'],\n      startTop: startGridLineBoundingBox.y,\n      endBottom: startGridLineBoundingBox.y + startGridLineBoundingBox.height,\n    });\n  });\n\n  test('should be able to select a range of time from bottom to top', async ({ page }) => {\n    // Given\n    const startGridLineLocator = page.locator(BASE_GRIDLINE_SELECTOR);\n    const targetGridLineLocator = page.locator(getTimeGridLineSelector('01:00'));\n    const timeGridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);\n\n    const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);\n    const targetGridLineBoundingBox = await getBoundingBox(targetGridLineLocator);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: startGridLineLocator,\n      targetLocator: targetGridLineLocator,\n    });\n\n    // Then\n    await assertTimeGridSelection(timeGridSelectionLocator, {\n      totalElements: 1,\n      formattedTimes: ['01:00', '03:30'],\n      startTop: targetGridLineBoundingBox.y,\n      endBottom: startGridLineBoundingBox.y + startGridLineBoundingBox.height,\n    });\n  });\n\n  test('should be able to select a range of time to upper right', async ({ page }) => {\n    // Given\n    const startGridLineLocator = page.locator(BASE_GRIDLINE_SELECTOR);\n    const targetGridLineLocator = page.locator(getTimeGridLineSelector('01:00'));\n    const timeGridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);\n\n    const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);\n    const targetGridLineBoundingBox = await getBoundingBox(targetGridLineLocator);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: startGridLineLocator,\n      targetLocator: targetGridLineLocator,\n      options: {\n        targetPosition: {\n          x: targetGridLineBoundingBox.width,\n          y: startGridLineBoundingBox.height,\n        },\n      },\n    });\n\n    // Then\n    await assertTimeGridSelection(timeGridSelectionLocator, {\n      totalElements: 4,\n      formattedTimes: ['03:00'],\n      startTop: startGridLineBoundingBox.y,\n      endBottom: targetGridLineBoundingBox.y + targetGridLineBoundingBox.height,\n    });\n  });\n\n  test('should be able to select a range of time to right', async ({ page }) => {\n    // Given\n    const startGridLineLocator = page.locator(BASE_GRIDLINE_SELECTOR);\n    const timeGridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);\n\n    const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: startGridLineLocator,\n      targetLocator: startGridLineLocator,\n      options: {\n        targetPosition: {\n          x: startGridLineBoundingBox.width,\n          y: 1,\n        },\n      },\n    });\n\n    // Then\n    await assertTimeGridSelection(timeGridSelectionLocator, {\n      totalElements: 4,\n      formattedTimes: ['03:00'],\n      startTop: startGridLineBoundingBox.y,\n      endBottom: startGridLineBoundingBox.y + startGridLineBoundingBox.height,\n    });\n  });\n\n  test('should be able to select a range of time to lower right', async ({ page }) => {\n    // Given\n    const startGridLineLocator = page.locator(BASE_GRIDLINE_SELECTOR);\n    const targetGridLineLocator = page.locator(getTimeGridLineSelector('05:00'));\n    const timeGridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);\n\n    const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);\n    const targetGridLineBoundingBox = await getBoundingBox(targetGridLineLocator);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: startGridLineLocator,\n      targetLocator: targetGridLineLocator,\n      options: {\n        targetPosition: {\n          x: targetGridLineBoundingBox.width,\n          y: targetGridLineBoundingBox.height,\n        },\n      },\n    });\n\n    // Then\n    await assertTimeGridSelection(timeGridSelectionLocator, {\n      totalElements: 4,\n      formattedTimes: ['03:00'],\n      startTop: startGridLineBoundingBox.y,\n      endBottom: targetGridLineBoundingBox.y + targetGridLineBoundingBox.height,\n    });\n  });\n\n  test('should be able to select a range of time from top to bottom', async ({ page }) => {\n    // Given\n    const startGridLineLocator = page.locator(BASE_GRIDLINE_SELECTOR);\n    const targetGridLineLocator = page.locator(getTimeGridLineSelector('05:00'));\n    const timeGridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);\n\n    const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);\n    const targetGridLineBoundingBox = await getBoundingBox(targetGridLineLocator);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: startGridLineLocator,\n      targetLocator: targetGridLineLocator,\n    });\n\n    // Then\n    await assertTimeGridSelection(timeGridSelectionLocator, {\n      totalElements: 1,\n      formattedTimes: ['03:00', '05:30'],\n      startTop: startGridLineBoundingBox.y,\n      endBottom: targetGridLineBoundingBox.y + targetGridLineBoundingBox.height,\n    });\n  });\n\n  test('should be able to select a range of time to lower left', async ({ page }) => {\n    // Given\n    const startGridLineLocator = page.locator(BASE_GRIDLINE_SELECTOR);\n    const targetGridLineLocator = page.locator(getTimeGridLineSelector('05:00'));\n    const timeGridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);\n\n    const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);\n    const targetGridLineBoundingBox = await getBoundingBox(targetGridLineLocator);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: startGridLineLocator,\n      targetLocator: targetGridLineLocator,\n      options: {\n        targetPosition: {\n          x: 0,\n          y: targetGridLineBoundingBox.height,\n        },\n      },\n    });\n\n    // Then\n    await assertTimeGridSelection(timeGridSelectionLocator, {\n      totalElements: 4,\n      formattedTimes: ['05:00'],\n      startTop: targetGridLineBoundingBox.y,\n      endBottom: startGridLineBoundingBox.y + startGridLineBoundingBox.height,\n    });\n  });\n\n  test('should be able to select a range of time to left', async ({ page }) => {\n    // Given\n    const startGridLineLocator = page.locator(BASE_GRIDLINE_SELECTOR);\n    const timeGridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);\n\n    const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: startGridLineLocator,\n      targetLocator: startGridLineLocator,\n      options: {\n        targetPosition: {\n          x: 1,\n          y: 1,\n        },\n      },\n    });\n\n    // Then\n    await assertTimeGridSelection(timeGridSelectionLocator, {\n      totalElements: 4,\n      formattedTimes: ['03:00'],\n      startTop: startGridLineBoundingBox.y,\n      endBottom: startGridLineBoundingBox.y + startGridLineBoundingBox.height,\n    });\n  });\n\n  test('should be able to select a range of time to upper left', async ({ page }) => {\n    // Given\n    const startGridLineLocator = page.locator(BASE_GRIDLINE_SELECTOR);\n    const targetGridLineLocator = page.locator(getTimeGridLineSelector('01:00'));\n    const timeGridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);\n\n    const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);\n    const targetGridLineBoundingBox = await getBoundingBox(targetGridLineLocator);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: startGridLineLocator,\n      targetLocator: targetGridLineLocator,\n      options: {\n        targetPosition: {\n          x: 1,\n          y: 1,\n        },\n      },\n    });\n\n    // Then\n    await assertTimeGridSelection(timeGridSelectionLocator, {\n      totalElements: 4,\n      formattedTimes: ['01:00'],\n      startTop: targetGridLineBoundingBox.y,\n      endBottom: startGridLineBoundingBox.y + startGridLineBoundingBox.height,\n    });\n  });\n\n  test('When pressing down the ESC key, the grid selection is canceled.', async ({ page }) => {\n    // Given\n    const startGridLineLocator = page.locator(getTimeGridLineSelector('03:00'));\n    const targetGridLineLocator = page.locator(getTimeGridLineSelector('05:00'));\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: startGridLineLocator,\n      targetLocator: targetGridLineLocator,\n      hold: true,\n    });\n    await page.keyboard.down('Escape');\n\n    // Then\n    const gridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);\n    expect(await gridSelectionLocator.count()).toBe(0);\n  });\n});\n\n// Regression test for #1238\n// Since the most of the test duplicated with the normal one, check for a few cases including horizontal selections only.\ntest.describe('Time Grid Selection - With different hourStart and hourEnd', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto(WEEK_VIEW_HOUR_START_OPTION_PAGE_URL);\n  });\n\n  const getColumnSelector = (columnIndex: number) => `data-testid=timegrid-column-${columnIndex}`;\n\n  test('should be able to select a range of time to lower right', async ({ page }) => {\n    // Given\n    const startGridLineLocator = page.locator(getTimeGridLineSelector('06:00'));\n    const targetGridLineLocator = page.locator(getTimeGridLineSelector('08:00'));\n    const startColumnLocator = page.locator(getColumnSelector(4));\n    const targetColumnLocator = page.locator(getColumnSelector(5));\n\n    const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);\n    const targetGridLineBoundingBox = await getBoundingBox(targetGridLineLocator);\n    const startColumnBoundingBox = await getBoundingBox(startColumnLocator);\n    const endColumnBoundingBox = await getBoundingBox(targetColumnLocator);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: startGridLineLocator,\n      targetLocator: targetGridLineLocator,\n      options: {\n        sourcePosition: {\n          x: startColumnBoundingBox.x,\n          y: 5,\n        },\n        targetPosition: {\n          x: endColumnBoundingBox.x,\n          y: 5,\n        },\n      },\n    });\n\n    // Then\n    await assertTimeGridSelection(page.locator(GRID_SELECTION_SELECTOR), {\n      totalElements: 2,\n      formattedTimes: ['06:00'],\n      startTop: startGridLineBoundingBox.y,\n      endBottom: targetGridLineBoundingBox.y + targetGridLineBoundingBox.height,\n    });\n  });\n\n  test('should be able to select a range of time to upper left', async ({ page }) => {\n    // Given\n    const startGridLineLocator = page.locator(getTimeGridLineSelector('08:00'));\n    const targetGridLineLocator = page.locator(getTimeGridLineSelector('06:00'));\n    const startColumnLocator = page.locator(getColumnSelector(5));\n    const targetColumnLocator = page.locator(getColumnSelector(4));\n\n    const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);\n    const targetGridLineBoundingBox = await getBoundingBox(targetGridLineLocator);\n    const startColumnBoundingBox = await getBoundingBox(startColumnLocator);\n    const endColumnBoundingBox = await getBoundingBox(targetColumnLocator);\n\n    // When\n    await dragAndDrop({\n      page,\n      sourceLocator: startGridLineLocator,\n      targetLocator: targetGridLineLocator,\n      options: {\n        sourcePosition: {\n          x: startColumnBoundingBox.x,\n          y: 5,\n        },\n        targetPosition: {\n          x: endColumnBoundingBox.x,\n          y: 5,\n        },\n      },\n    });\n\n    // Then\n    await assertTimeGridSelection(page.locator(GRID_SELECTION_SELECTOR), {\n      totalElements: 2,\n      formattedTimes: ['06:00'],\n      startTop: targetGridLineBoundingBox.y,\n      endBottom: startGridLineBoundingBox.y + startGridLineBoundingBox.height,\n    });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/postcss.config.js",
    "content": "module.exports = {\n  plugins: [\n    // eslint-disable-next-line @typescript-eslint/no-var-requires\n    require('postcss-prefixer')({\n      prefix: 'toastui-calendar-',\n    }),\n  ],\n};\n"
  },
  {
    "path": "apps/calendar/scripts/publishToCDN.js",
    "content": "/* eslint-disable */\nconst path = require('path');\nconst fs = require('fs');\nconst fetch = require('node-fetch');\nconst pkg = require('../package.json');\n\nconst LOCAL_DIST_PATH = path.join(__dirname, '../dist');\nconst STORAGE_API_URL = 'https://api-storage.cloud.toast.com/v1';\nconst IDENTITY_API_URL = 'https://api-identity.infrastructure.cloud.toast.com/v2.0';\n\nconst TOAST_CLOUD_TENANTID = process.env.TOAST_CLOUD_TENANTID;\nconst TOAST_CLOUD_STORAGEID = process.env.TOAST_CLOUD_STORAGEID;\nconst TOAST_CLOUD_USERNAME = process.env.TOAST_CLOUD_USERNAME;\nconst TOAST_CLOUD_PASSWORD = process.env.TOAST_CLOUD_PASSWORD;\n\nasync function getTOASTCloudContainer(token) {\n  const response = await fetch(`${STORAGE_API_URL}/${TOAST_CLOUD_STORAGEID}`, {\n    method: 'GET',\n    headers: {\n      'Content-Type': 'application/json',\n      'X-Auth-Token': token,\n    },\n  });\n  const container = await response.text();\n\n  return `${container.trim()}/calendar`;\n}\n\nasync function getTOASTCloudToken() {\n  const data = {\n    auth: {\n      tenantId: TOAST_CLOUD_TENANTID,\n      passwordCredentials: {\n        username: TOAST_CLOUD_USERNAME,\n        password: TOAST_CLOUD_PASSWORD,\n      },\n    },\n  };\n\n  const response = await fetch(`${IDENTITY_API_URL}/tokens`, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify(data),\n  });\n  const result = await response.json();\n\n  return result.access.token.id;\n}\n\nfunction publishToCdn(token, localPath, cdnPath) {\n  const files = fs.readdirSync(localPath);\n\n  files.forEach((fileName) => {\n    const objectPath = `${cdnPath}/${fileName}`;\n\n    if (fileName.match(/.(js|css|svg)$/)) {\n      const readStream = fs.createReadStream(`${localPath}/${fileName}`);\n      const contentType = /css$/.test(fileName)\n        ? 'text/css'\n        : /js$/.test(fileName)\n        ? 'text/javascript'\n        : 'image/svg+xml';\n\n      fetch(`${STORAGE_API_URL}/${objectPath}`, {\n        method: 'PUT',\n        headers: {\n          'Content-Type': contentType,\n          'X-Auth-Token': token,\n        },\n        body: readStream,\n      });\n    } else {\n      publishToCdn(token, `${localPath}/${fileName}`, objectPath);\n    }\n  });\n}\n\nasync function publish() {\n  const token = await getTOASTCloudToken();\n  const container = await getTOASTCloudContainer(token);\n  const cdnPath = `${TOAST_CLOUD_STORAGEID}/${container}`;\n\n  [`v${pkg.version}`, 'latest'].forEach((dir) => {\n    publishToCdn(token, LOCAL_DIST_PATH, `${cdnPath}/${dir}`);\n  });\n}\n\npublish();\n"
  },
  {
    "path": "apps/calendar/scripts/updateWrapper.js",
    "content": "/* eslint-disable */\nconst path = require('path');\nconst fs = require('fs');\n\nconst CORE_PACKAGE_JSON_PATH = path.join(__dirname, '../package.json');\nconst REACT_PACKAGE_JSON_PATH = path.join(__dirname, '../../react-calendar/package.json');\nconst VUE_PACKAGE_JSON_PATH = path.join(__dirname, '../../vue-calendar/package.json');\n\nconst corePackage = require(CORE_PACKAGE_JSON_PATH);\nconst reactPackage = require(REACT_PACKAGE_JSON_PATH);\nconst vuePackage = require(VUE_PACKAGE_JSON_PATH);\n\nconst version = corePackage.version;\n\nreactPackage.version = version;\nreactPackage.dependencies['@toast-ui/calendar'] = `^${version}`;\n\nfs.writeFileSync(REACT_PACKAGE_JSON_PATH, `${JSON.stringify(reactPackage, null, 2)}\\n`);\n\nvuePackage.version = version;\nvuePackage.dependencies['@toast-ui/calendar'] = `^${version}`;\n\nfs.writeFileSync(VUE_PACKAGE_JSON_PATH, `${JSON.stringify(vuePackage, null, 2)}\\n`);\n"
  },
  {
    "path": "apps/calendar/src/calendarContainer.tsx",
    "content": "import { h } from 'preact';\n\nimport { StoreProvider } from '@src/contexts/calendarStore';\nimport { EventBusProvider } from '@src/contexts/eventBus';\nimport { FloatingLayerProvider } from '@src/contexts/floatingLayer';\nimport { ThemeProvider } from '@src/contexts/themeStore';\nimport type { EventBus } from '@src/utils/eventBus';\n\nimport type { PropsWithChildren } from '@t/components/common';\nimport type { ExternalEventTypes } from '@t/eventBus';\nimport type { CalendarStore, InternalStoreAPI } from '@t/store';\nimport type { ThemeStore } from '@t/theme';\n\ninterface Props {\n  theme: InternalStoreAPI<ThemeStore>;\n  store: InternalStoreAPI<CalendarStore>;\n  eventBus: EventBus<ExternalEventTypes>;\n}\n\nexport function CalendarContainer({ theme, store, eventBus, children }: PropsWithChildren<Props>) {\n  return (\n    <EventBusProvider value={eventBus}>\n      <ThemeProvider store={theme}>\n        <StoreProvider store={store}>\n          <FloatingLayerProvider>{children}</FloatingLayerProvider>\n        </StoreProvider>\n      </ThemeProvider>\n    </EventBusProvider>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridCommon/dayName.tsx",
    "content": "import { h } from 'preact';\n\nimport type { DayNameThemes } from '@src/components/dayGridCommon/gridHeader';\nimport { Template } from '@src/components/template';\nimport { useEventBus } from '@src/contexts/eventBus';\nimport { cls } from '@src/helpers/css';\nimport { getDayName } from '@src/helpers/dayName';\nimport { usePrimaryTimezone } from '@src/hooks/timezone/usePrimaryTimezone';\nimport type { TemplateName } from '@src/template/default';\nimport type TZDate from '@src/time/date';\nimport { isSameDate, isSaturday, isSunday, isWeekend, toFormat } from '@src/time/datetime';\n\nimport type { CalendarViewType, StyleProp } from '@t/components/common';\nimport type { TemplateMonthDayName, TemplateWeekDayName } from '@t/template';\n\ninterface Props {\n  type: CalendarViewType;\n  dayName: TemplateWeekDayName | TemplateMonthDayName;\n  style: StyleProp;\n  theme: DayNameThemes;\n}\n\nfunction isWeekDayName(\n  type: 'week' | 'month',\n  dayName: Props['dayName']\n): dayName is TemplateWeekDayName {\n  return type === 'week';\n}\n\nfunction getWeekDayNameColor({\n  dayName,\n  theme,\n  today,\n}: {\n  dayName: TemplateWeekDayName;\n  theme: Props['theme'];\n  today: TZDate;\n}) {\n  const { day, dateInstance } = dayName;\n  const isToday = isSameDate(today, dateInstance);\n  const isPastDay = !isToday && dateInstance < today;\n\n  if (isSunday(day)) {\n    return theme.common.holiday.color;\n  }\n  if (isPastDay) {\n    return theme.week?.pastDay.color;\n  }\n  if (isSaturday(day)) {\n    return theme.common.saturday.color;\n  }\n  if (isToday) {\n    return theme.week?.today.color;\n  }\n\n  return theme.common.dayName.color;\n}\n\nfunction getMonthDayNameColor({\n  dayName,\n  theme,\n}: {\n  dayName: TemplateMonthDayName;\n  theme: Props['theme'];\n}) {\n  const { day } = dayName;\n\n  if (isSunday(day)) {\n    return theme.common.holiday.color;\n  }\n  if (isSaturday(day)) {\n    return theme.common.saturday.color;\n  }\n\n  return theme.common.dayName.color;\n}\n\nexport function DayName({ dayName, style, type, theme }: Props) {\n  const eventBus = useEventBus();\n  const [, getNow] = usePrimaryTimezone();\n  const today = getNow();\n  const { day } = dayName;\n  const color =\n    type === 'week'\n      ? getWeekDayNameColor({ dayName: dayName as TemplateWeekDayName, theme, today })\n      : getMonthDayNameColor({ dayName: dayName as TemplateMonthDayName, theme });\n\n  const templateType = `${type}DayName` as TemplateName;\n\n  const handleClick = () => {\n    if (isWeekDayName(type, dayName)) {\n      eventBus.fire('clickDayName', { date: toFormat(dayName.dateInstance, 'YYYY-MM-DD') });\n    }\n  };\n\n  return (\n    <div className={cls('day-name-item', type)} style={style}>\n      <span\n        className={cls({ [`holiday-${getDayName(day)}`]: isWeekend(day) })}\n        style={{ color }}\n        onClick={handleClick}\n        data-testid={`dayName-${type}-${getDayName(day)}`}\n      >\n        <Template template={templateType} param={dayName} />\n      </span>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridCommon/gridHeader.tsx",
    "content": "import { h } from 'preact';\n\nimport { DayName } from '@src/components/dayGridCommon/dayName';\nimport { DEFAULT_DAY_NAME_MARGIN_LEFT } from '@src/constants/style';\nimport { useTheme } from '@src/contexts/themeStore';\nimport { cls, toPercent } from '@src/helpers/css';\n\nimport type { CalendarViewType } from '@t/components/common';\nimport type { CalendarMonthOptions, CalendarWeekOptions } from '@t/store';\nimport type { TemplateMonthDayName, TemplateWeekDayName } from '@t/template';\nimport type { CommonTheme, MonthTheme, ThemeState, WeekTheme } from '@t/theme';\nimport type { CellStyle } from '@t/time/datetime';\n\ntype TemplateDayNames = (TemplateWeekDayName | TemplateMonthDayName)[];\n\nexport type DayNameThemes = {\n  common: {\n    saturday: CommonTheme['saturday'];\n    holiday: CommonTheme['holiday'];\n    today: CommonTheme['today'];\n    dayName: CommonTheme['dayName'];\n  };\n  week?: {\n    pastDay: WeekTheme['pastDay'];\n    today: WeekTheme['today'];\n    dayName: WeekTheme['dayName'];\n  };\n  month?: {\n    dayName: MonthTheme['dayName'];\n  };\n};\n\ninterface Props {\n  type: CalendarViewType;\n  dayNames: TemplateDayNames;\n  options?: CalendarMonthOptions | CalendarWeekOptions;\n  marginLeft?: string;\n  rowStyleInfo: CellStyle[];\n}\n\nfunction weekDayNameSelector(theme: ThemeState): DayNameThemes {\n  return {\n    common: {\n      saturday: theme.common.saturday,\n      holiday: theme.common.holiday,\n      today: theme.common.today,\n      dayName: theme.common.dayName,\n    },\n    week: {\n      pastDay: theme.week.pastDay,\n      today: theme.week.today,\n      dayName: theme.week.dayName,\n    },\n  };\n}\n\nfunction monthDayNameSelector(theme: ThemeState): DayNameThemes {\n  return {\n    common: {\n      saturday: theme.common.saturday,\n      holiday: theme.common.holiday,\n      today: theme.common.today,\n      dayName: theme.common.dayName,\n    },\n    month: {\n      dayName: theme.month.dayName,\n    },\n  };\n}\n\nexport function GridHeader({\n  dayNames,\n  marginLeft = DEFAULT_DAY_NAME_MARGIN_LEFT,\n  rowStyleInfo,\n  type = 'month',\n}: Props) {\n  const theme = useTheme(type === 'month' ? monthDayNameSelector : weekDayNameSelector);\n  const { backgroundColor = 'white', borderLeft = null, ...rest } = theme[type]?.dayName ?? {};\n  const { borderTop = null, borderBottom = null } = rest as WeekTheme['dayName'];\n\n  return (\n    <div\n      data-testid={`grid-header-${type}`}\n      className={cls('day-names', type)}\n      style={{\n        backgroundColor,\n        borderTop,\n        borderBottom,\n      }}\n    >\n      <div className={cls('day-name-container')} style={{ marginLeft }}>\n        {(dayNames as TemplateDayNames).map((dayName, index) => (\n          <DayName\n            type={type}\n            key={`dayNames-${dayName.day}`}\n            dayName={dayName}\n            style={{\n              width: toPercent(rowStyleInfo[index].width),\n              left: toPercent(rowStyleInfo[index].left),\n              borderLeft,\n            }}\n            theme={theme}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridCommon/gridSelection.tsx",
    "content": "import { h } from 'preact';\n\nimport { useTheme } from '@src/contexts/themeStore';\nimport { cls, toPercent } from '@src/helpers/css';\nimport { getLeftAndWidth } from '@src/helpers/grid';\nimport type TZDate from '@src/time/date';\n\nimport type { GridSelectionDataByRow } from '@t/components/gridSelection';\nimport type { ThemeState } from '@t/theme';\n\ninterface Props {\n  type: 'allday' | 'month' | 'accumulated';\n  gridSelectionData: GridSelectionDataByRow;\n  weekDates: TZDate[];\n  narrowWeekend: boolean;\n}\n\nfunction commonGridSelectionSelector(theme: ThemeState) {\n  return theme.common.gridSelection;\n}\n\nexport function GridSelection({ type, gridSelectionData, weekDates, narrowWeekend }: Props) {\n  const { backgroundColor, border } = useTheme(commonGridSelectionSelector);\n  const { startCellIndex, endCellIndex } = gridSelectionData;\n\n  const { left, width } = getLeftAndWidth(\n    Math.min(startCellIndex, endCellIndex),\n    Math.max(startCellIndex, endCellIndex),\n    weekDates,\n    narrowWeekend\n  );\n\n  const style = {\n    left: toPercent(left),\n    width: toPercent(width),\n    height: toPercent(100),\n    backgroundColor,\n    border,\n  };\n\n  return width > 0 ? <div className={cls(type, 'grid-selection')} style={style} /> : null;\n}\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridMonth/accumulatedGridSelection.tsx",
    "content": "import { h } from 'preact';\nimport { useCallback } from 'preact/hooks';\n\nimport { GridSelection } from '@src/components/dayGridCommon/gridSelection';\nimport { useStore } from '@src/contexts/calendarStore';\nimport { cls } from '@src/helpers/css';\nimport { dayGridMonthSelectionHelper } from '@src/helpers/gridSelection';\nimport type TZDate from '@src/time/date';\n\ninterface Props {\n  rowIndex: number;\n  weekDates: TZDate[];\n  narrowWeekend: boolean;\n}\n\nexport function AccumulatedGridSelection({ rowIndex, weekDates, narrowWeekend }: Props) {\n  const gridSelectionDataByRow = useStore(\n    useCallback(\n      (state) =>\n        state.gridSelection.accumulated.dayGridMonth.map((gridSelection) =>\n          dayGridMonthSelectionHelper.calculateSelection(gridSelection, rowIndex, weekDates.length)\n        ),\n      [rowIndex, weekDates]\n    )\n  );\n\n  return (\n    <div className={cls('accumulated-grid-selection')}>\n      {gridSelectionDataByRow.map((gridSelectionData) =>\n        gridSelectionData ? (\n          <GridSelection\n            type=\"accumulated\"\n            gridSelectionData={gridSelectionData}\n            weekDates={weekDates}\n            narrowWeekend={narrowWeekend}\n          />\n        ) : null\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridMonth/cellHeader.tsx",
    "content": "import { h } from 'preact';\nimport { useMemo } from 'preact/hooks';\n\nimport { MoreEventsButton } from '@src/components/dayGridMonth/moreEventsButton';\nimport { Template } from '@src/components/template';\nimport { CellBarType } from '@src/constants/grid';\nimport { useStore } from '@src/contexts/calendarStore';\nimport { useCommonTheme, useMonthTheme } from '@src/contexts/themeStore';\nimport { cls } from '@src/helpers/css';\nimport { usePrimaryTimezone } from '@src/hooks/timezone/usePrimaryTimezone';\nimport { viewSelector } from '@src/selectors';\nimport type { TemplateName } from '@src/template/default';\nimport type TZDate from '@src/time/date';\nimport { isSaturday, isSunday, toFormat } from '@src/time/datetime';\nimport { capitalize } from '@src/utils/string';\nimport { isNil } from '@src/utils/type';\n\nimport type { CommonTheme, MonthTheme } from '@t/theme';\n\ninterface Props {\n  type?: CellBarType;\n  exceedCount?: number;\n  date: TZDate;\n  onClickExceedCount: () => void;\n}\n\nfunction getDateColor({\n  date,\n  theme,\n  renderDate,\n  isToday,\n}: {\n  date: TZDate;\n  theme: { common: CommonTheme; month: MonthTheme };\n  renderDate: TZDate;\n  isToday: boolean;\n}) {\n  const dayIndex = date.getDay();\n  const thisMonth = renderDate.getMonth();\n  const isSameMonth = thisMonth === date.getMonth();\n\n  const {\n    common: { holiday, saturday, today, dayName },\n    month: { dayExceptThisMonth, holidayExceptThisMonth },\n  } = theme;\n\n  if (isToday) {\n    return today.color;\n  }\n\n  if (isSunday(dayIndex)) {\n    return isSameMonth ? holiday.color : holidayExceptThisMonth.color;\n  }\n\n  if (isSaturday(dayIndex)) {\n    return isSameMonth ? saturday.color : dayExceptThisMonth.color;\n  }\n\n  if (!isSameMonth) {\n    return dayExceptThisMonth.color;\n  }\n\n  return dayName.color;\n}\n\nfunction useCellHeaderTheme() {\n  const common = useCommonTheme();\n  const month = useMonthTheme();\n\n  return useMemo(() => ({ common, month }), [common, month]);\n}\n\nexport function CellHeader({\n  type = CellBarType.header,\n  exceedCount = 0,\n  date,\n  onClickExceedCount,\n}: Props) {\n  const { renderDate } = useStore(viewSelector);\n\n  const [, getNow] = usePrimaryTimezone();\n  const theme = useCellHeaderTheme();\n  const height = theme.month.gridCell[`${type}Height`];\n\n  const ymd = toFormat(date, 'YYYYMMDD');\n  const todayYmd = toFormat(getNow(), 'YYYYMMDD');\n  const isToday = ymd === todayYmd;\n  const templateParam = {\n    date: toFormat(date, 'YYYY-MM-DD'),\n    day: date.getDay(),\n    hiddenEventCount: exceedCount,\n    isOtherMonth: date.getMonth() !== renderDate.getMonth(),\n    isToday: ymd === todayYmd,\n    month: date.getMonth(),\n    ymd,\n  };\n  const gridCellDateStyle = { color: getDateColor({ date, theme, isToday, renderDate }) };\n  const monthGridTemplate = `monthGrid${capitalize(type)}` as TemplateName;\n\n  if (isNil(height)) {\n    return null;\n  }\n\n  return (\n    <div className={cls(`grid-cell-${type}`)} style={{ height }}>\n      <span className={cls('grid-cell-date')} style={gridCellDateStyle}>\n        <Template template={monthGridTemplate} param={templateParam} />\n      </span>\n      {exceedCount ? (\n        <MoreEventsButton\n          type={type}\n          number={exceedCount}\n          onClickButton={onClickExceedCount}\n          className={cls('grid-cell-more-events')}\n        />\n      ) : null}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridMonth/dayGridMonth.tsx",
    "content": "import { h } from 'preact';\nimport { useEffect, useMemo, useRef, useState } from 'preact/hooks';\n\nimport { AccumulatedGridSelection } from '@src/components/dayGridMonth/accumulatedGridSelection';\nimport { GridRow } from '@src/components/dayGridMonth/gridRow';\nimport { GridSelectionByRow } from '@src/components/dayGridMonth/gridSelectionByRow';\nimport { MonthEvents } from '@src/components/dayGridMonth/monthEvents';\nimport { MovingEventShadow } from '@src/components/dayGridMonth/movingEventShadow';\nimport { ResizingGuideByRow } from '@src/components/dayGridMonth/resizingGuideByRow';\nimport {\n  MONTH_CELL_BAR_HEIGHT,\n  MONTH_CELL_PADDING_TOP,\n  MONTH_EVENT_HEIGHT,\n  MONTH_EVENT_MARGIN_TOP,\n} from '@src/constants/style';\nimport { useStore } from '@src/contexts/calendarStore';\nimport { useTheme } from '@src/contexts/themeStore';\nimport { cls, toPercent } from '@src/helpers/css';\nimport { createGridPositionFinder, getRenderedEventUIModels } from '@src/helpers/grid';\nimport { dayGridMonthSelectionHelper } from '@src/helpers/gridSelection';\nimport { useCalendarData } from '@src/hooks/calendar/useCalendarData';\nimport { useDOMNode } from '@src/hooks/common/useDOMNode';\nimport { useGridSelection } from '@src/hooks/gridSelection/useGridSelection';\nimport { calendarSelector, optionsSelector } from '@src/selectors';\nimport { monthVisibleEventCountSelector } from '@src/selectors/options';\nimport { monthGridCellSelector } from '@src/selectors/theme';\nimport type TZDate from '@src/time/date';\nimport { getSize } from '@src/utils/dom';\nimport { passConditionalProp } from '@src/utils/preact';\n\nimport type { CalendarMonthOptions } from '@t/store';\nimport type { CellInfo } from '@t/time/datetime';\n\nconst TOTAL_PERCENT_HEIGHT = 100;\n\ninterface Props {\n  dateMatrix: TZDate[][];\n  rowInfo: CellInfo[];\n  cellWidthMap: string[][];\n}\n\nfunction useCellContentAreaHeight(eventHeight: number) {\n  const visibleEventCount = useStore(monthVisibleEventCountSelector);\n  const { headerHeight: themeHeaderHeight, footerHeight: themeFooterHeight } =\n    useTheme(monthGridCellSelector);\n\n  const ref = useRef<HTMLDivElement>(null);\n  const [cellContentAreaHeight, setCellContentAreaHeight] = useState(0);\n\n  useEffect(() => {\n    if (ref.current) {\n      const rowHeight = getSize(ref.current).height;\n      const headerHeight = MONTH_CELL_PADDING_TOP + (themeHeaderHeight ?? MONTH_CELL_BAR_HEIGHT);\n      const footerHeight = themeFooterHeight ?? 0;\n\n      const baseContentAreaHeight = rowHeight - headerHeight - footerHeight;\n      const visibleEventCountHeight = visibleEventCount * (eventHeight + MONTH_EVENT_MARGIN_TOP);\n\n      setCellContentAreaHeight(Math.min(baseContentAreaHeight, visibleEventCountHeight));\n    }\n  }, [themeFooterHeight, themeHeaderHeight, eventHeight, visibleEventCount]);\n\n  return { ref, cellContentAreaHeight };\n}\n\nexport function DayGridMonth({ dateMatrix = [], rowInfo = [], cellWidthMap = [] }: Props) {\n  const [gridContainer, setGridContainerRef] = useDOMNode<HTMLDivElement>();\n  const calendar = useStore(calendarSelector);\n  // TODO: event height need to be dynamic\n  const { ref, cellContentAreaHeight } = useCellContentAreaHeight(MONTH_EVENT_HEIGHT);\n\n  const { eventFilter, month: monthOptions, isReadOnly } = useStore(optionsSelector);\n  const { narrowWeekend, startDayOfWeek } = monthOptions as CalendarMonthOptions;\n  const rowHeight = TOTAL_PERCENT_HEIGHT / dateMatrix.length;\n\n  const gridPositionFinder = useMemo(\n    () =>\n      createGridPositionFinder({\n        container: gridContainer,\n        rowsCount: dateMatrix.length,\n        columnsCount: dateMatrix[0].length,\n        narrowWeekend,\n        startDayOfWeek,\n      }),\n    [dateMatrix, gridContainer, narrowWeekend, startDayOfWeek]\n  );\n\n  const calendarData = useCalendarData(calendar, eventFilter);\n  const renderedEventUIModels = useMemo(\n    () => dateMatrix.map((week) => getRenderedEventUIModels(week, calendarData, narrowWeekend)),\n    [calendarData, dateMatrix, narrowWeekend]\n  );\n\n  const onMouseDown = useGridSelection({\n    type: 'dayGridMonth',\n    gridPositionFinder,\n    dateCollection: dateMatrix,\n    dateGetter: dayGridMonthSelectionHelper.getDateFromCollection,\n    selectionSorter: dayGridMonthSelectionHelper.sortSelection,\n  });\n\n  return (\n    <div\n      ref={setGridContainerRef}\n      onMouseDown={passConditionalProp(!isReadOnly, onMouseDown)}\n      className={cls('month-daygrid')}\n    >\n      {dateMatrix.map((week, rowIndex) => {\n        const { uiModels, gridDateEventModelMap } = renderedEventUIModels[rowIndex];\n\n        return (\n          <div\n            key={`dayGrid-events-${rowIndex}`}\n            className={cls('month-week-item')}\n            style={{ height: toPercent(rowHeight) }}\n            ref={ref}\n          >\n            <div className={cls('weekday')}>\n              <GridRow\n                gridDateEventModelMap={gridDateEventModelMap}\n                week={week}\n                rowInfo={rowInfo}\n                contentAreaHeight={cellContentAreaHeight}\n              />\n              <MonthEvents\n                name=\"month\"\n                events={uiModels}\n                contentAreaHeight={cellContentAreaHeight}\n                eventHeight={MONTH_EVENT_HEIGHT}\n                className={cls('weekday-events')}\n              />\n              <GridSelectionByRow\n                weekDates={week}\n                narrowWeekend={narrowWeekend}\n                rowIndex={rowIndex}\n              />\n              <AccumulatedGridSelection\n                rowIndex={rowIndex}\n                weekDates={week}\n                narrowWeekend={narrowWeekend}\n              />\n            </div>\n            <ResizingGuideByRow\n              dateMatrix={dateMatrix}\n              gridPositionFinder={gridPositionFinder}\n              rowIndex={rowIndex}\n              cellWidthMap={cellWidthMap}\n              renderedUIModels={renderedEventUIModels}\n            />\n            <MovingEventShadow\n              dateMatrix={dateMatrix}\n              gridPositionFinder={gridPositionFinder}\n              rowIndex={rowIndex}\n              rowInfo={rowInfo}\n            />\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridMonth/gridCell.tsx",
    "content": "import { h } from 'preact';\nimport { useCallback, useEffect, useState } from 'preact/hooks';\n\nimport { CellHeader } from '@src/components/dayGridMonth/cellHeader';\nimport { CellBarType } from '@src/constants/grid';\nimport {\n  MONTH_EVENT_HEIGHT,\n  MONTH_EVENT_MARGIN_TOP,\n  MONTH_MORE_VIEW_HEADER_HEIGHT,\n  MONTH_MORE_VIEW_HEADER_MARGIN_BOTTOM,\n  MONTH_MORE_VIEW_MIN_WIDTH,\n  MONTH_MORE_VIEW_PADDING,\n} from '@src/constants/style';\nimport { useDispatch } from '@src/contexts/calendarStore';\nimport { useLayoutContainer } from '@src/contexts/layoutContainer';\nimport { useTheme } from '@src/contexts/themeStore';\nimport { cls } from '@src/helpers/css';\nimport { getExceedCount } from '@src/helpers/grid';\nimport { useDOMNode } from '@src/hooks/common/useDOMNode';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport { monthMoreViewSelector } from '@src/selectors/theme';\nimport type TZDate from '@src/time/date';\nimport { isWeekend } from '@src/time/datetime';\nimport { getSize } from '@src/utils/dom';\n\nimport type { StyleProp } from '@t/components/common';\nimport type { PopupPosition, Rect } from '@t/store';\nimport type { ThemeState } from '@t/theme';\n\ninterface RectSize {\n  width: number;\n  height: number;\n}\n\ntype SeeMoreRectParam = {\n  cell: HTMLDivElement;\n  layoutContainer: HTMLDivElement;\n  popupSize: { width: number; height: number };\n};\n\nfunction getSeeMorePopupSize({\n  grid,\n  offsetWidth,\n  eventLength,\n  layerSize,\n}: {\n  grid: HTMLDivElement;\n  offsetWidth: number;\n  eventLength: number;\n  layerSize: {\n    width: number | null;\n    height: number | null;\n  };\n}): RectSize {\n  const minHeight = getSize(grid).height + MONTH_MORE_VIEW_PADDING * 2;\n  let width = offsetWidth + MONTH_MORE_VIEW_PADDING * 2;\n\n  const { width: moreViewWidth, height: moreViewHeight } = layerSize;\n\n  const MAX_DISPLAY_EVENT_COUNT = 10;\n\n  width = Math.max(width, MONTH_MORE_VIEW_MIN_WIDTH);\n  let height =\n    MONTH_MORE_VIEW_HEADER_HEIGHT + MONTH_MORE_VIEW_HEADER_MARGIN_BOTTOM + MONTH_MORE_VIEW_PADDING;\n  const eventHeight = MONTH_EVENT_HEIGHT + MONTH_EVENT_MARGIN_TOP;\n\n  if (eventLength <= MAX_DISPLAY_EVENT_COUNT) {\n    height += eventHeight * eventLength;\n  } else {\n    height += eventHeight * MAX_DISPLAY_EVENT_COUNT;\n  }\n\n  if (moreViewWidth) {\n    width = moreViewWidth;\n  }\n\n  if (moreViewHeight) {\n    height = moreViewHeight;\n  }\n\n  if (isNaN(height) || height < minHeight) {\n    height = minHeight;\n  }\n\n  return { width, height };\n}\n\nfunction getSeeMorePopupPosition(popupSize: RectSize, appContainerSize: Rect, cellRect: Rect) {\n  const {\n    width: containerWidth,\n    height: containerHeight,\n    left: containerLeft,\n    top: containerTop,\n  } = appContainerSize;\n  const { width: popupWidth, height: popupHeight } = popupSize;\n\n  const containerRight = containerLeft + containerWidth;\n  const containerBottom = containerTop + containerHeight;\n\n  let left = cellRect.left + cellRect.width / 2 - popupWidth / 2;\n  let { top } = cellRect;\n\n  const isLeftOutOfContainer = left < containerLeft;\n  const isRightOutOfContainer = left + popupWidth > containerRight;\n  const isUpperOutOfContainer = top < containerTop;\n  const isLowerOutOfContainer = top + popupHeight > containerBottom;\n\n  if (isLeftOutOfContainer) {\n    left = containerLeft;\n  }\n\n  if (isRightOutOfContainer) {\n    left = containerRight - popupWidth;\n  }\n\n  if (isUpperOutOfContainer) {\n    top = containerTop;\n  }\n\n  if (isLowerOutOfContainer) {\n    top = containerBottom - popupHeight;\n  }\n\n  return { top: top + window.scrollY, left: left + window.scrollX };\n}\n\nfunction getSeeMorePopupRect({\n  layoutContainer,\n  cell,\n  popupSize,\n}: SeeMoreRectParam): PopupPosition {\n  const containerRect = layoutContainer.getBoundingClientRect();\n  const cellRect = cell.getBoundingClientRect();\n\n  const popupPosition = getSeeMorePopupPosition(popupSize, containerRect, cellRect);\n\n  return { ...popupSize, ...popupPosition };\n}\n\nfunction usePopupPosition(\n  eventLength: number,\n  parentContainer?: HTMLDivElement | null,\n  layoutContainer?: HTMLDivElement | null\n) {\n  const { width: moreViewWidth, height: moreViewHeight } = useTheme(monthMoreViewSelector);\n\n  const [container, containerRefCallback] = useDOMNode<HTMLDivElement>();\n  const [popupPosition, setPopupPosition] = useState<PopupPosition | null>(null);\n\n  useEffect(() => {\n    if (layoutContainer && parentContainer && container) {\n      const popupSize = getSeeMorePopupSize({\n        grid: parentContainer,\n        offsetWidth: container.offsetWidth,\n        eventLength,\n        layerSize: {\n          width: moreViewWidth,\n          height: moreViewHeight,\n        },\n      });\n\n      const rect = getSeeMorePopupRect({\n        cell: container,\n        layoutContainer,\n        popupSize,\n      });\n\n      setPopupPosition(rect);\n    }\n  }, [layoutContainer, container, eventLength, parentContainer, moreViewWidth, moreViewHeight]);\n\n  return { popupPosition, containerRefCallback };\n}\n\nfunction weekendBackgroundColorSelector(theme: ThemeState) {\n  return theme.month.weekend.backgroundColor;\n}\n\ninterface Props {\n  date: TZDate;\n  style?: StyleProp;\n  parentContainer?: HTMLDivElement | null;\n  events?: EventUIModel[];\n  contentAreaHeight: number;\n}\n\nexport function GridCell({ date, events = [], style, parentContainer, contentAreaHeight }: Props) {\n  const layoutContainer = useLayoutContainer();\n  const { showSeeMorePopup } = useDispatch('popup');\n  const backgroundColor = useTheme(weekendBackgroundColorSelector);\n\n  const { popupPosition, containerRefCallback } = usePopupPosition(\n    events.length,\n    parentContainer,\n    layoutContainer\n  );\n\n  const onOpenSeeMorePopup = useCallback(() => {\n    if (popupPosition) {\n      showSeeMorePopup({\n        date,\n        popupPosition,\n        events,\n      });\n    }\n  }, [date, events, popupPosition, showSeeMorePopup]);\n\n  const exceedCount = getExceedCount(\n    events,\n    contentAreaHeight,\n    MONTH_EVENT_HEIGHT + MONTH_EVENT_MARGIN_TOP\n  );\n\n  return (\n    <div\n      className={cls('daygrid-cell')}\n      style={{ ...style, backgroundColor: isWeekend(date.getDay()) ? backgroundColor : 'inherit' }}\n      ref={containerRefCallback}\n    >\n      <CellHeader\n        type={CellBarType.header}\n        exceedCount={exceedCount}\n        date={date}\n        onClickExceedCount={onOpenSeeMorePopup}\n      />\n      <CellHeader\n        type={CellBarType.footer}\n        exceedCount={exceedCount}\n        date={date}\n        onClickExceedCount={onOpenSeeMorePopup}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridMonth/gridRow.tsx",
    "content": "import { h } from 'preact';\nimport { memo } from 'preact/compat';\nimport { useCallback } from 'preact/hooks';\n\nimport { GridCell } from '@src/components/dayGridMonth/gridCell';\nimport { useTheme } from '@src/contexts/themeStore';\nimport { cls, toPercent } from '@src/helpers/css';\nimport { useDOMNode } from '@src/hooks/common/useDOMNode';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport type TZDate from '@src/time/date';\nimport { toFormat, toStartOfDay } from '@src/time/datetime';\n\nimport type { CellStyle } from '@t/time/datetime';\n\ninterface Props {\n  gridDateEventModelMap?: Record<string, EventUIModel[]>;\n  week: TZDate[];\n  rowInfo: CellStyle[];\n  contentAreaHeight: number;\n}\n\nexport const GridRow = memo(function GridRow({\n  week,\n  rowInfo,\n  gridDateEventModelMap = {},\n  contentAreaHeight,\n}: Props) {\n  const [container, containerRefCallback] = useDOMNode<HTMLDivElement>();\n  const border = useTheme(useCallback((theme) => theme.common.border, []));\n\n  return (\n    <div className={cls('weekday-grid')} style={{ borderTop: border }} ref={containerRefCallback}>\n      {week.map((date, columnIndex) => {\n        const dayIndex = date.getDay();\n        const { width, left } = rowInfo[columnIndex];\n        const ymd = toFormat(toStartOfDay(date), 'YYYYMMDD');\n\n        return (\n          <GridCell\n            key={`daygrid-cell-${dayIndex}`}\n            date={date}\n            style={{\n              width: toPercent(width),\n              left: toPercent(left),\n            }}\n            parentContainer={container}\n            events={gridDateEventModelMap[ymd]}\n            contentAreaHeight={contentAreaHeight}\n          />\n        );\n      })}\n    </div>\n  );\n});\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridMonth/gridSelectionByRow.tsx",
    "content": "import { h } from 'preact';\nimport { useCallback } from 'preact/hooks';\n\nimport { GridSelection } from '@src/components/dayGridCommon/gridSelection';\nimport { useStore } from '@src/contexts/calendarStore';\nimport { dayGridMonthSelectionHelper } from '@src/helpers/gridSelection';\nimport type TZDate from '@src/time/date';\nimport { isNil } from '@src/utils/type';\n\nimport type { CalendarState } from '@t/store';\n\ninterface Props {\n  weekDates: TZDate[];\n  narrowWeekend: boolean;\n  rowIndex: number;\n}\n\nexport function GridSelectionByRow({ weekDates, narrowWeekend, rowIndex }: Props) {\n  const gridSelectionDataByRow = useStore(\n    useCallback(\n      (state: CalendarState) =>\n        dayGridMonthSelectionHelper.calculateSelection(\n          state.gridSelection.dayGridMonth,\n          rowIndex,\n          weekDates.length\n        ),\n      [rowIndex, weekDates.length]\n    )\n  );\n\n  if (isNil(gridSelectionDataByRow)) {\n    return null;\n  }\n\n  return (\n    <GridSelection\n      type=\"month\"\n      gridSelectionData={gridSelectionDataByRow}\n      weekDates={weekDates}\n      narrowWeekend={narrowWeekend}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridMonth/monthEvents.tsx",
    "content": "import { h } from 'preact';\nimport { memo } from 'preact/compat';\n\nimport { HorizontalEvent } from '@src/components/events/horizontalEvent';\nimport { MONTH_CELL_BAR_HEIGHT, MONTH_EVENT_MARGIN_TOP } from '@src/constants/style';\nimport { useTheme } from '@src/contexts/themeStore';\nimport { EVENT_HEIGHT, isWithinHeight } from '@src/helpers/grid';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport { monthGridCellSelector } from '@src/selectors/theme';\n\ninterface Props {\n  name: string;\n  contentAreaHeight: number;\n  eventHeight?: number;\n  events: EventUIModel[];\n  className: string;\n}\n\nexport const MonthEvents = memo(function MonthEvents({\n  contentAreaHeight,\n  eventHeight = EVENT_HEIGHT,\n  events,\n  name,\n  className,\n}: Props) {\n  const { headerHeight } = useTheme(monthGridCellSelector);\n\n  const dayEvents = events\n    .filter(isWithinHeight(contentAreaHeight, eventHeight + MONTH_EVENT_MARGIN_TOP))\n    .map((uiModel) => (\n      <HorizontalEvent\n        key={`${name}-DayEvent-${uiModel.cid()}`}\n        uiModel={uiModel}\n        eventHeight={eventHeight}\n        headerHeight={headerHeight ?? MONTH_CELL_BAR_HEIGHT}\n      />\n    ));\n\n  return <div className={className}>{dayEvents}</div>;\n});\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridMonth/moreEventsButton.tsx",
    "content": "import { h } from 'preact';\n\nimport { Template } from '@src/components/template';\nimport { CellBarType } from '@src/constants/grid';\nimport { useDispatch } from '@src/contexts/calendarStore';\nimport type { TemplateName } from '@src/template/default';\n\ninterface Props {\n  type?: CellBarType;\n  number: number;\n  onClickButton: () => void;\n  className: string;\n}\n\nexport function MoreEventsButton({ type, number, onClickButton, className }: Props) {\n  const { reset } = useDispatch('dnd');\n\n  // prevent unexpected grid selection when clicking on the button\n  const handleMouseDown = (e: MouseEvent) => {\n    e.stopPropagation();\n  };\n\n  const handleClick = () => {\n    reset();\n    onClickButton();\n  };\n\n  const exceedButtonTemplate = `monthGrid${\n    type === CellBarType.header ? 'Header' : 'Footer'\n  }Exceed` as TemplateName;\n\n  return (\n    <button type=\"button\" onMouseDown={handleMouseDown} onClick={handleClick} className={className}>\n      <Template template={exceedButtonTemplate} param={number} />\n    </button>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridMonth/movingEventShadow.tsx",
    "content": "import type { ComponentProps } from 'preact';\nimport { h } from 'preact';\n\nimport type { DayGridMonth } from '@src/components/dayGridMonth/dayGridMonth';\nimport { HorizontalEvent } from '@src/components/events/horizontalEvent';\nimport { MONTH_CELL_BAR_HEIGHT, MONTH_CELL_PADDING_TOP } from '@src/constants/style';\nimport { EVENT_HEIGHT } from '@src/helpers/grid';\nimport { useDayGridMonthEventMove } from '@src/hooks/dayGridMonth/useDayGridMonthEventMove';\nimport { isNil } from '@src/utils/type';\n\nimport type { GridPositionFinder } from '@t/grid';\n\ntype Props = Pick<ComponentProps<typeof DayGridMonth>, 'dateMatrix' | 'rowInfo'> & {\n  rowIndex: number;\n  gridPositionFinder: GridPositionFinder;\n};\n\nexport function MovingEventShadow({ dateMatrix, gridPositionFinder, rowInfo, rowIndex }: Props) {\n  const movingEvent = useDayGridMonthEventMove({\n    dateMatrix,\n    rowInfo,\n    gridPositionFinder,\n    rowIndex,\n  });\n\n  if (isNil(movingEvent)) {\n    return null;\n  }\n\n  return (\n    <HorizontalEvent\n      uiModel={movingEvent}\n      movingLeft={movingEvent.left}\n      eventHeight={EVENT_HEIGHT}\n      headerHeight={MONTH_CELL_PADDING_TOP + MONTH_CELL_BAR_HEIGHT}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridMonth/resizingGuideByRow.tsx",
    "content": "import type { ComponentProps } from 'preact';\nimport { h } from 'preact';\n\nimport type { DayGridMonth } from '@src/components/dayGridMonth/dayGridMonth';\nimport { HorizontalEvent } from '@src/components/events/horizontalEvent';\nimport {\n  MONTH_CELL_BAR_HEIGHT,\n  MONTH_CELL_PADDING_TOP,\n  MONTH_EVENT_HEIGHT,\n} from '@src/constants/style';\nimport { cls } from '@src/helpers/css';\nimport type { getRenderedEventUIModels } from '@src/helpers/grid';\nimport { useDayGridMonthEventResize } from '@src/hooks/dayGridMonth/useDayGridMonthEventResize';\nimport { isNil } from '@src/utils/type';\n\nimport type { GridPositionFinder } from '@t/grid';\n\ntype Props = Pick<ComponentProps<typeof DayGridMonth>, 'dateMatrix' | 'cellWidthMap'> & {\n  gridPositionFinder: GridPositionFinder;\n  renderedUIModels: ReturnType<typeof getRenderedEventUIModels>[];\n  rowIndex: number;\n};\n\nexport function ResizingGuideByRow({\n  dateMatrix,\n  cellWidthMap,\n  gridPositionFinder,\n  renderedUIModels,\n  rowIndex,\n}: Props) {\n  const resizingGuideProps = useDayGridMonthEventResize({\n    dateMatrix,\n    gridPositionFinder,\n    cellWidthMap,\n    renderedUIModels,\n    rowIndex,\n  });\n\n  if (isNil(resizingGuideProps)) {\n    return null;\n  }\n\n  const [uiModel, resizingWidth] = resizingGuideProps;\n\n  return (\n    <div className={cls('weekday-events')}>\n      <HorizontalEvent\n        key={`resizing-event-${uiModel.cid()}`}\n        uiModel={uiModel}\n        eventHeight={MONTH_EVENT_HEIGHT}\n        headerHeight={MONTH_CELL_PADDING_TOP + MONTH_CELL_BAR_HEIGHT}\n        resizingWidth={resizingWidth}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridWeek/alldayGridRow.tsx",
    "content": "import { Fragment, h } from 'preact';\nimport { useMemo } from 'preact/hooks';\n\nimport { AlldayGridSelection } from '@src/components/dayGridWeek/alldayGridSelection';\nimport { GridCells } from '@src/components/dayGridWeek/gridCells';\nimport { MovingEventShadow } from '@src/components/dayGridWeek/movingEventShadow';\nimport { ResizingEventShadow } from '@src/components/dayGridWeek/resizingEventShadow';\nimport { HorizontalEvent } from '@src/components/events/horizontalEvent';\nimport { Template } from '@src/components/template';\nimport { DEFAULT_PANEL_HEIGHT, WEEK_EVENT_MARGIN_TOP } from '@src/constants/style';\nimport { useStore } from '@src/contexts/calendarStore';\nimport { useTheme } from '@src/contexts/themeStore';\nimport { cls } from '@src/helpers/css';\nimport { createGridPositionFinder, EVENT_HEIGHT, isWithinHeight } from '@src/helpers/grid';\nimport { alldayGridRowSelectionHelper } from '@src/helpers/gridSelection';\nimport { useDOMNode } from '@src/hooks/common/useDOMNode';\nimport { useGridRowHeightController } from '@src/hooks/dayGridWeek/useGridRowHeightController';\nimport { useGridSelection } from '@src/hooks/gridSelection/useGridSelection';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport { optionsSelector } from '@src/selectors';\nimport { weekDayGridLeftSelector } from '@src/selectors/theme';\nimport type TZDate from '@src/time/date';\nimport { Day } from '@src/time/datetime';\n\nimport type { WeekOptions } from '@t/options';\nimport type { CellStyle } from '@t/time/datetime';\n\ninterface Props {\n  events: EventUIModel[];\n  weekDates: TZDate[];\n  timesWidth?: number;\n  timezonesCount?: number;\n  height?: number;\n  options?: WeekOptions;\n  shouldRenderDefaultPopup?: boolean;\n  rowStyleInfo: CellStyle[];\n  gridColWidthMap: string[][];\n}\n\nconst rowTitleTemplate = `alldayTitle` as const;\n\nexport function AlldayGridRow({\n  events,\n  weekDates,\n  height = DEFAULT_PANEL_HEIGHT,\n  options = {},\n  rowStyleInfo,\n  gridColWidthMap,\n}: Props) {\n  const { isReadOnly } = useStore(optionsSelector);\n  const dayGridLeftTheme = useTheme(weekDayGridLeftSelector);\n  const [panelContainer, setPanelContainerRef] = useDOMNode<HTMLDivElement>();\n\n  const { narrowWeekend = false, startDayOfWeek = Day.SUN } = options;\n\n  const maxTop = useMemo(() => Math.max(0, ...events.map(({ top }) => top)), [events]);\n  const gridPositionFinder = useMemo(\n    () =>\n      createGridPositionFinder({\n        container: panelContainer,\n        rowsCount: 1,\n        columnsCount: weekDates.length,\n        narrowWeekend,\n        startDayOfWeek,\n      }),\n    [panelContainer, weekDates.length, narrowWeekend, startDayOfWeek]\n  );\n\n  const { clickedIndex, isClickedCount, onClickExceedCount, onClickCollapseButton } =\n    useGridRowHeightController(maxTop, 'allday');\n\n  const horizontalEvents = useMemo(\n    () =>\n      events\n        .filter(isWithinHeight(height, EVENT_HEIGHT + WEEK_EVENT_MARGIN_TOP))\n        .map((uiModel) => (\n          <HorizontalEvent\n            key={`allday-DayEvent-${uiModel.cid()}`}\n            uiModel={uiModel}\n            eventHeight={EVENT_HEIGHT}\n            headerHeight={0}\n          />\n        )),\n    [events, height]\n  );\n\n  const startGridSelection = useGridSelection({\n    type: 'dayGridWeek',\n    gridPositionFinder,\n    dateCollection: weekDates,\n    selectionSorter: alldayGridRowSelectionHelper.sortSelection,\n    dateGetter: alldayGridRowSelectionHelper.getDateFromCollection,\n  });\n\n  const onMouseDown = (e: MouseEvent) => {\n    const target = e.target as HTMLElement;\n\n    if (isReadOnly || !target.classList.contains(cls('panel-grid'))) {\n      return;\n    }\n\n    startGridSelection(e);\n  };\n\n  return (\n    <Fragment>\n      <div className={cls('panel-title')} style={dayGridLeftTheme}>\n        <Template template={rowTitleTemplate} param=\"alldayTitle\" />\n      </div>\n      <div className={cls('allday-panel')} ref={setPanelContainerRef} onMouseDown={onMouseDown}>\n        <div className={cls('panel-grid-wrapper')}>\n          <GridCells\n            uiModels={events}\n            weekDates={weekDates}\n            narrowWeekend={narrowWeekend}\n            height={height}\n            clickedIndex={clickedIndex}\n            isClickedCount={isClickedCount}\n            onClickExceedCount={onClickExceedCount}\n            onClickCollapseButton={onClickCollapseButton}\n          />\n        </div>\n        <div className={cls(`panel-allday-events`)}>{horizontalEvents}</div>\n        <ResizingEventShadow\n          weekDates={weekDates}\n          gridPositionFinder={gridPositionFinder}\n          gridColWidthMap={gridColWidthMap}\n        />\n        <MovingEventShadow rowStyleInfo={rowStyleInfo} gridPositionFinder={gridPositionFinder} />\n        <AlldayGridSelection weekDates={weekDates} narrowWeekend={narrowWeekend} />\n      </div>\n    </Fragment>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridWeek/alldayGridSelection.tsx",
    "content": "import type { ComponentProps } from 'preact';\nimport { h } from 'preact';\n\nimport { GridSelection } from '@src/components/dayGridCommon/gridSelection';\nimport type { AlldayGridRow } from '@src/components/dayGridWeek/alldayGridRow';\nimport { useStore } from '@src/contexts/calendarStore';\nimport { alldayGridRowSelectionHelper } from '@src/helpers/gridSelection';\nimport { isNil } from '@src/utils/type';\n\nimport type { CalendarState } from '@t/store';\n\nfunction dayGridWeekSelectionSelector(state: CalendarState) {\n  return alldayGridRowSelectionHelper.calculateSelection(state.gridSelection.dayGridWeek);\n}\n\ntype Props = Pick<ComponentProps<typeof AlldayGridRow>, 'weekDates'> & {\n  narrowWeekend: boolean;\n};\n\nexport function AlldayGridSelection({ weekDates, narrowWeekend }: Props) {\n  const calculatedGridSelection = useStore(dayGridWeekSelectionSelector);\n\n  if (isNil(calculatedGridSelection)) {\n    return null;\n  }\n\n  return (\n    <GridSelection\n      type=\"allday\"\n      gridSelectionData={calculatedGridSelection}\n      weekDates={weekDates}\n      narrowWeekend={narrowWeekend}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridWeek/gridCell.tsx",
    "content": "import { h } from 'preact';\nimport { useCallback } from 'preact/hooks';\n\nimport { Template } from '@src/components/template';\nimport { useTheme } from '@src/contexts/themeStore';\nimport { cls } from '@src/helpers/css';\n\ntype Props = {\n  isLastCell: boolean;\n  width: string;\n  left: string;\n} & ExceedCountProps &\n  CollapseButtonProps;\n\ninterface ExceedCountProps {\n  index: number;\n  exceedCount: number;\n  isClicked: boolean;\n  onClickExceedCount: (exceedCount: number) => void;\n}\n\ninterface CollapseButtonProps {\n  isClicked: boolean;\n  isClickedIndex: boolean;\n  onClickCollapseButton: () => void;\n}\n\nfunction ExceedCount({ index, exceedCount, isClicked, onClickExceedCount }: ExceedCountProps) {\n  const clickExceedCount = () => onClickExceedCount(index);\n  const style = { display: isClicked ? 'none' : '' };\n\n  return exceedCount && !isClicked ? (\n    <span className={cls('weekday-exceed-in-week')} onClick={clickExceedCount} style={style}>\n      <Template template=\"weekGridFooterExceed\" param={exceedCount} />\n    </span>\n  ) : null;\n}\n\nfunction CollapseButton({ isClicked, isClickedIndex, onClickCollapseButton }: CollapseButtonProps) {\n  return isClicked && isClickedIndex ? (\n    <span className={cls('weekday-exceed-in-week')} onClick={onClickCollapseButton}>\n      <Template template=\"collapseBtnTitle\" />\n    </span>\n  ) : null;\n}\n\nexport function GridCell({\n  width,\n  left,\n  index,\n  exceedCount,\n  isClicked,\n  onClickExceedCount,\n  isClickedIndex,\n  onClickCollapseButton,\n  isLastCell,\n}: Props) {\n  const { borderRight, backgroundColor } = useTheme(useCallback((theme) => theme.week.dayGrid, []));\n  const style = {\n    width,\n    left,\n    borderRight: isLastCell ? 'none' : borderRight,\n    backgroundColor,\n  };\n\n  return (\n    <div className={cls('panel-grid')} style={style}>\n      <ExceedCount\n        index={index}\n        exceedCount={exceedCount}\n        isClicked={isClicked}\n        onClickExceedCount={onClickExceedCount}\n      />\n      <CollapseButton\n        isClickedIndex={isClickedIndex}\n        isClicked={isClicked}\n        onClickCollapseButton={onClickCollapseButton}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridWeek/gridCells.tsx",
    "content": "import { Fragment, h } from 'preact';\nimport { memo } from 'preact/compat';\n\nimport { GridCell } from '@src/components/dayGridWeek/gridCell';\nimport { toPercent } from '@src/helpers/css';\nimport {\n  EVENT_HEIGHT,\n  getExceedCount,\n  getGridWidthAndLeftPercentValues,\n  isInGrid,\n  TOTAL_WIDTH,\n} from '@src/helpers/grid';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport type TZDate from '@src/time/date';\n\ninterface Props {\n  uiModels: EventUIModel[];\n  weekDates: TZDate[];\n  narrowWeekend: boolean;\n  height: number;\n  clickedIndex: number;\n  isClickedCount: boolean;\n  onClickExceedCount: (index: number) => void;\n  onClickCollapseButton: () => void;\n}\n\nexport const GridCells = memo(function GridCells({\n  uiModels,\n  weekDates,\n  narrowWeekend,\n  height,\n  clickedIndex,\n  isClickedCount,\n  onClickExceedCount,\n  onClickCollapseButton,\n}: Props) {\n  // @TODO: get margin value dynamically\n  const eventTopMargin = 2;\n  const { widthList, leftList } = getGridWidthAndLeftPercentValues(\n    weekDates,\n    narrowWeekend,\n    TOTAL_WIDTH\n  );\n  const lastCellIndex = weekDates.length - 1;\n\n  return (\n    <Fragment>\n      {weekDates.map((cell, index) => {\n        const width = toPercent(widthList[index]);\n        const left = toPercent(leftList[index]);\n\n        const uiModelsInCell = uiModels.filter(isInGrid(cell));\n        const exceedCount = getExceedCount(uiModelsInCell, height, EVENT_HEIGHT + eventTopMargin);\n        const isClickedIndex = index === clickedIndex;\n        const isLastCell = index === lastCellIndex;\n\n        return (\n          <GridCell\n            key={`panel-grid-${cell.getDate()}`}\n            width={width}\n            left={left}\n            index={index}\n            exceedCount={exceedCount}\n            isClicked={isClickedCount}\n            onClickExceedCount={onClickExceedCount}\n            isClickedIndex={isClickedIndex}\n            onClickCollapseButton={onClickCollapseButton}\n            isLastCell={isLastCell}\n          />\n        );\n      })}\n    </Fragment>\n  );\n});\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridWeek/movingEventShadow.tsx",
    "content": "import type { ComponentProps } from 'preact';\nimport { h } from 'preact';\n\nimport type { AlldayGridRow } from '@src/components/dayGridWeek/alldayGridRow';\nimport { HorizontalEvent } from '@src/components/events/horizontalEvent';\nimport { EVENT_HEIGHT } from '@src/helpers/grid';\nimport { useAlldayGridRowEventMove } from '@src/hooks/dayGridWeek/useAlldayGridRowEventMove';\nimport { isNil } from '@src/utils/type';\n\nimport type { GridPositionFinder } from '@t/grid';\n\ntype Props = Pick<ComponentProps<typeof AlldayGridRow>, 'rowStyleInfo'> & {\n  gridPositionFinder: GridPositionFinder;\n};\n\nexport function MovingEventShadow({\n  rowStyleInfo,\n  gridPositionFinder,\n}: Pick<Props, 'rowStyleInfo'> & {\n  gridPositionFinder: GridPositionFinder;\n}) {\n  const { movingEvent, movingLeft } = useAlldayGridRowEventMove({\n    rowStyleInfo,\n    gridPositionFinder,\n  });\n\n  if (isNil(movingEvent)) {\n    return null;\n  }\n\n  return (\n    <HorizontalEvent\n      uiModel={movingEvent}\n      eventHeight={EVENT_HEIGHT}\n      headerHeight={0}\n      movingLeft={movingLeft}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridWeek/otherGridRow.tsx",
    "content": "import { Fragment, h } from 'preact';\nimport { useMemo } from 'preact/hooks';\n\nimport { GridCells } from '@src/components/dayGridWeek/gridCells';\nimport { HorizontalEvent } from '@src/components/events/horizontalEvent';\nimport { Template } from '@src/components/template';\nimport { DEFAULT_PANEL_HEIGHT, WEEK_EVENT_MARGIN_TOP } from '@src/constants/style';\nimport { useTheme } from '@src/contexts/themeStore';\nimport { cls } from '@src/helpers/css';\nimport { EVENT_HEIGHT, isWithinHeight } from '@src/helpers/grid';\nimport { useGridRowHeightController } from '@src/hooks/dayGridWeek/useGridRowHeightController';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport { weekDayGridLeftSelector } from '@src/selectors/theme';\nimport type TZDate from '@src/time/date';\n\nimport type { WeekOptions } from '@t/options';\nimport type { AlldayEventCategory } from '@t/panel';\n\ntype GridRowTitleTemplate = `${AlldayEventCategory}Title`;\n\ninterface Props {\n  category: Exclude<AlldayEventCategory, 'allday'>;\n  events: EventUIModel[];\n  weekDates: TZDate[];\n  timesWidth?: number;\n  timezonesCount?: number;\n  height?: number;\n  options?: WeekOptions;\n  gridColWidthMap: string[][];\n}\n\nexport function OtherGridRow({\n  events,\n  weekDates,\n  category,\n  height = DEFAULT_PANEL_HEIGHT,\n  options = {},\n}: Props) {\n  const dayGridLeftTheme = useTheme(weekDayGridLeftSelector);\n\n  const maxTop = useMemo(() => Math.max(0, ...events.map(({ top }) => top)), [events]);\n  const { narrowWeekend = false } = options;\n  const rowTitleTemplate: GridRowTitleTemplate = `${category}Title`;\n\n  const { clickedIndex, isClickedCount, onClickExceedCount, onClickCollapseButton } =\n    useGridRowHeightController(maxTop, category);\n\n  const horizontalEvents = useMemo(\n    () =>\n      events\n        .filter(isWithinHeight(height, EVENT_HEIGHT + WEEK_EVENT_MARGIN_TOP))\n        .map((uiModel) => (\n          <HorizontalEvent\n            key={`${category}-DayEvent-${uiModel.cid()}`}\n            uiModel={uiModel}\n            eventHeight={EVENT_HEIGHT}\n            headerHeight={0}\n          />\n        )),\n    [category, events, height]\n  );\n\n  return (\n    <Fragment>\n      <div className={cls('panel-title')} style={dayGridLeftTheme}>\n        <Template template={rowTitleTemplate} param={category} />\n      </div>\n      <div className={cls('allday-panel')}>\n        <div className={cls('panel-grid-wrapper')}>\n          <GridCells\n            uiModels={events}\n            weekDates={weekDates}\n            narrowWeekend={narrowWeekend}\n            height={height}\n            clickedIndex={clickedIndex}\n            isClickedCount={isClickedCount}\n            onClickExceedCount={onClickExceedCount}\n            onClickCollapseButton={onClickCollapseButton}\n          />\n        </div>\n        <div className={cls(`panel-${category}-events`)}>{horizontalEvents}</div>\n      </div>\n    </Fragment>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/dayGridWeek/resizingEventShadow.tsx",
    "content": "import type { ComponentProps } from 'preact';\nimport { h } from 'preact';\n\nimport type { AlldayGridRow } from '@src/components/dayGridWeek/alldayGridRow';\nimport { HorizontalEvent } from '@src/components/events/horizontalEvent';\nimport { EVENT_HEIGHT } from '@src/helpers/grid';\nimport { useAlldayGridRowEventResize } from '@src/hooks/dayGridWeek/useAlldayGridRowEventResize';\nimport { isNil } from '@src/utils/type';\n\nimport type { GridPositionFinder } from '@t/grid';\n\ntype Props = Pick<ComponentProps<typeof AlldayGridRow>, 'weekDates' | 'gridColWidthMap'> & {\n  gridPositionFinder: GridPositionFinder;\n};\n\nexport function ResizingEventShadow({ weekDates, gridColWidthMap, gridPositionFinder }: Props) {\n  const { resizingEvent, resizingWidth } = useAlldayGridRowEventResize({\n    weekDates,\n    gridColWidthMap,\n    gridPositionFinder,\n  });\n\n  if (isNil(resizingEvent)) {\n    return null;\n  }\n\n  return (\n    <HorizontalEvent\n      uiModel={resizingEvent}\n      eventHeight={EVENT_HEIGHT}\n      headerHeight={0}\n      resizingWidth={resizingWidth}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/events/backgroundEvent.tsx",
    "content": "import { h } from 'preact';\n\nimport { cls } from '@src/helpers/css';\nimport type EventUIModel from '@src/model/eventUIModel';\n\nconst classNames = {\n  background: cls('event-background'),\n};\n\ninterface Props {\n  uiModel: EventUIModel;\n  width?: string;\n  height?: string;\n  top?: string;\n  right?: string;\n  bottom?: string;\n  left?: string;\n}\n\nexport function BackgroundEvent({\n  uiModel,\n  width = '100%',\n  height = '100px',\n  top = '',\n  right = '',\n  bottom = '',\n  left = '',\n}: Props) {\n  const style = {\n    backgroundColor: uiModel.model.backgroundColor,\n    width,\n    height,\n    top,\n    right,\n    bottom,\n    left,\n  };\n\n  return <span className={classNames.background} style={style} />;\n}\n"
  },
  {
    "path": "apps/calendar/src/components/events/horizontalEvent.spec.tsx",
    "content": "import { h } from 'preact';\n\nimport { HorizontalEvent } from '@src/components/events/horizontalEvent';\nimport { initCalendarStore } from '@src/contexts/calendarStore';\nimport EventModel from '@src/model/eventModel';\nimport EventUIModel from '@src/model/eventUIModel';\nimport { dragAndDrop, fireEvent, render, screen } from '@src/test/utils';\nimport { EventBusImpl } from '@src/utils/eventBus';\n\nimport type { ExternalEventTypes } from '@t/eventBus';\n\ndescribe(`Firing 'clickEvent'`, () => {\n  const eventTitle = 'click-event';\n  function setup() {\n    const eventBus = new EventBusImpl<ExternalEventTypes>();\n    const handler = jest.fn();\n    eventBus.on('clickEvent', handler);\n    const uiModel = new EventUIModel(\n      new EventModel({\n        id: '1',\n        title: eventTitle,\n        start: new Date(2020, 0, 1, 10, 0),\n        end: new Date(2020, 0, 1, 12, 0),\n      })\n    );\n    const props = {\n      uiModel,\n      eventHeight: 30,\n      headerHeight: 0,\n    };\n\n    return {\n      props,\n      eventBus,\n      handler,\n    };\n  }\n\n  it('should fire event when clicked', () => {\n    // Given\n    const { props, eventBus, handler } = setup();\n    render(<HorizontalEvent {...props} />, { eventBus });\n\n    // When\n    const event = screen.getByText(eventTitle);\n    fireEvent.mouseDown(event);\n    fireEvent.mouseUp(event);\n\n    // Then\n    expect(handler).toBeCalledWith(\n      expect.objectContaining({\n        event: props.uiModel.model.toEventObject(),\n      })\n    );\n  });\n\n  it('should not fire when dragged', () => {\n    // Given\n    const { props, eventBus, handler } = setup();\n    render(<HorizontalEvent {...props} />, { eventBus });\n\n    // When\n    const event = screen.getByText(eventTitle);\n    dragAndDrop({\n      element: event,\n      targetPosition: {\n        clientX: 100,\n        clientY: 100,\n      },\n    });\n\n    // Then\n    expect(handler).not.toBeCalled();\n  });\n});\n\ndescribe(`Firing 'afterRenderEvent'`, () => {\n  function setup() {\n    const eventBus = new EventBusImpl<ExternalEventTypes>();\n    const handler = jest.fn();\n    eventBus.on('afterRenderEvent', handler);\n    const uiModel = new EventUIModel(\n      new EventModel({\n        id: '1',\n        start: new Date(2020, 0, 1, 10, 0),\n        end: new Date(2020, 0, 1, 12, 0),\n      })\n    );\n    const props = {\n      uiModel,\n      eventHeight: 30,\n      headerHeight: 0,\n    };\n\n    return {\n      props,\n      eventBus,\n      handler,\n    };\n  }\n\n  it(`should fire 'afterRenderEvent' when only the component is mounted`, () => {\n    // Given\n    const { props, eventBus, handler } = setup();\n\n    // When\n    const { rerender } = render(<HorizontalEvent {...props} />, { eventBus });\n\n    // Then\n    expect(handler).toBeCalledWith(props.uiModel.model.toEventObject());\n\n    // When rerender\n    handler.mockReset();\n    rerender(<HorizontalEvent {...props} />);\n\n    // Then\n    expect(handler).not.toBeCalled();\n  });\n\n  it(`should not fire 'afterRenderEvent' when the component is working as a drag & drop interaction guide element`, () => {\n    // Given\n    const { props, eventBus, handler } = setup();\n    const propsWithResizingWidth = {\n      ...props,\n      resizingWidth: '100px',\n    };\n    const propsWithMovingLeft = {\n      ...props,\n      movingLeft: 100,\n    };\n\n    // When (resizingWidth)\n    const { rerender } = render(<HorizontalEvent {...propsWithResizingWidth} />, { eventBus });\n\n    // Then\n    expect(handler).not.toBeCalled();\n\n    // When (movingLeft)\n    rerender(<HorizontalEvent {...propsWithMovingLeft} />);\n\n    // Then\n    expect(handler).not.toBeCalled();\n  });\n\n  it(`should fire 'afterRenderEvent' when it is in readonly mode`, () => {\n    // Given\n    const { eventBus, handler } = setup();\n    const uiModel = new EventUIModel(\n      new EventModel({\n        id: '1',\n        start: new Date(2020, 0, 1, 10, 0),\n        end: new Date(2020, 0, 1, 12, 0),\n        isReadOnly: true,\n      })\n    );\n\n    const props = {\n      uiModel,\n      eventHeight: 30,\n      headerHeight: 0,\n    };\n\n    // When\n    render(<HorizontalEvent {...props} />, { eventBus });\n\n    // Then\n    expect(handler).toHaveBeenCalledTimes(1);\n  });\n});\n\ndescribe('Apply customStyle', () => {\n  it('should apply customStyle when the EventModel has the customStyle object', () => {\n    // Given\n    const customStyle = {\n      textDecoration: 'line-through',\n    };\n    const uiModel = new EventUIModel(\n      new EventModel({\n        id: '1',\n        title: 'style-test',\n        start: new Date(2020, 0, 1, 10, 0),\n        end: new Date(2020, 0, 1, 12, 0),\n        customStyle,\n      })\n    );\n    const props = {\n      uiModel,\n      eventHeight: 30,\n      headerHeight: 0,\n    };\n    const testId = '1-style-test';\n\n    // When\n    render(<HorizontalEvent {...props} />);\n\n    // Then\n    const container = screen.getByTestId(testId);\n    expect(container).toHaveStyle(customStyle);\n  });\n});\n\ndescribe('Color values', () => {\n  const calendarId = 'cal1';\n  const title = 'style-test';\n  const calendarColors = {\n    color: '#ff0000',\n    borderColor: '#ff0000',\n    backgroundColor: '#ff0000',\n    dragBackgroundColor: '#ff0000',\n  };\n  const store = initCalendarStore({\n    calendars: [\n      {\n        id: calendarId,\n        name: calendarId,\n        ...calendarColors,\n      },\n    ],\n  });\n\n  it('should apply calendar color values to the event', () => {\n    // Given\n    const uiModel = new EventUIModel(\n      new EventModel({\n        id: '1',\n        calendarId,\n        title,\n        start: new Date(2020, 0, 1, 10, 0),\n        end: new Date(2020, 0, 1, 12, 0),\n      })\n    );\n    const props = {\n      uiModel,\n      eventHeight: 30,\n      headerHeight: 0,\n    };\n\n    // When\n    render(<HorizontalEvent {...props} />, { store });\n\n    // Then\n    const container = screen.getByTestId(new RegExp(title));\n    expect(container.children[0]).toHaveStyle({\n      color: calendarColors.color,\n      backgroundColor: calendarColors.backgroundColor,\n      borderLeft: `3px solid ${calendarColors.borderColor}`,\n    });\n  });\n\n  it('should apply color properties of the event model overriding calendar color values', () => {\n    // Given\n    const eventColors = {\n      color: '#6f53d4',\n      backgroundColor: '#6f53d4',\n      borderColor: '#6f53d4',\n      dragBackgroundColor: '#6f53d4',\n    };\n\n    const uiModel = new EventUIModel(\n      new EventModel({\n        id: '1',\n        calendarId,\n        title,\n        start: new Date(2020, 0, 1, 10, 0),\n        end: new Date(2020, 0, 1, 12, 0),\n        ...eventColors,\n      })\n    );\n    const props = {\n      uiModel,\n      eventHeight: 30,\n      headerHeight: 0,\n    };\n\n    // When\n    render(<HorizontalEvent {...props} />, { store });\n\n    // Then\n    const container = screen.getByTestId(new RegExp(title));\n    expect(container.children[0]).toHaveStyle({\n      color: eventColors.color,\n      backgroundColor: eventColors.backgroundColor,\n      borderLeft: `3px solid ${eventColors.borderColor}`,\n    });\n  });\n\n  it('should apply the dragBackgroundColor value to the dragging event', () => {\n    // Given\n    const eventColors = {\n      color: '#6f53d4',\n      backgroundColor: '#6f53d4',\n      borderColor: '#6f53d4',\n      dragBackgroundColor: '#fb96f6',\n    };\n\n    const uiModel = new EventUIModel(\n      new EventModel({\n        id: '1',\n        calendarId,\n        title,\n        start: new Date(2020, 0, 1, 10, 0),\n        end: new Date(2020, 0, 1, 12, 0),\n        ...eventColors,\n      })\n    );\n    const props = {\n      uiModel,\n      eventHeight: 30,\n      headerHeight: 0,\n    };\n    const getEventItemElement = () =>\n      screen.getByTestId(new RegExp(title))?.children?.[0] as HTMLElement;\n\n    // When\n    render(<HorizontalEvent {...props} />, { store });\n    dragAndDrop({\n      element: getEventItemElement(),\n      targetPosition: {\n        clientX: 100,\n        clientY: 100,\n      },\n      hold: true,\n    });\n\n    // Then\n    expect(getEventItemElement()).toHaveStyle({\n      color: eventColors.color,\n      backgroundColor: eventColors.dragBackgroundColor,\n      borderLeft: `3px solid ${eventColors.borderColor}`,\n    });\n  });\n});\n\ndescribe('isReadOnly', () => {\n  function setup() {\n    const eventName = 'readonly-event';\n    const uiModel = new EventUIModel(\n      new EventModel({\n        id: '1',\n        title: eventName,\n        start: new Date(2020, 0, 1, 10, 0),\n        end: new Date(2020, 0, 1, 12, 0),\n        isReadOnly: true,\n      })\n    );\n\n    const props = {\n      uiModel,\n      eventHeight: 30,\n      headerHeight: 0,\n    };\n\n    return {\n      props,\n    };\n  }\n\n  it(\"should be able to show detail popup even if the model's `isReadOnly` property is `true`\", () => {\n    // Given\n    const store = initCalendarStore({\n      useDetailPopup: true,\n    });\n\n    const eventName = 'readonly-event';\n    const uiModel = new EventUIModel(\n      new EventModel({\n        id: '1',\n        title: eventName,\n        start: new Date(2020, 0, 1, 10, 0),\n        end: new Date(2020, 0, 1, 12, 0),\n        isReadOnly: true,\n      })\n    );\n\n    const props = {\n      uiModel,\n      eventHeight: 30,\n      headerHeight: 0,\n    };\n    const showDetailPopupSpy = jest.fn();\n    store.getState().dispatch.popup.showDetailPopup = showDetailPopupSpy;\n\n    // When\n    render(<HorizontalEvent {...props} />, { store });\n\n    // Then\n    const event = screen.getByText(eventName);\n    // NOTE: userEvent.click is not working as expected\n    fireEvent.mouseDown(event);\n    fireEvent.mouseUp(event);\n\n    expect(showDetailPopupSpy).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/components/events/horizontalEvent.tsx",
    "content": "import { h } from 'preact';\nimport { useEffect, useRef, useState } from 'preact/hooks';\n\nimport { HorizontalEventResizeIcon } from '@src/components/events/horizontalEventResizeIcon';\nimport { Template } from '@src/components/template';\nimport { useDispatch, useStore } from '@src/contexts/calendarStore';\nimport { useEventBus } from '@src/contexts/eventBus';\nimport { useLayoutContainer } from '@src/contexts/layoutContainer';\nimport { cls, getEventColors, toPercent, toPx } from '@src/helpers/css';\nimport { DRAGGING_TYPE_CREATORS } from '@src/helpers/drag';\nimport { useCalendarColor } from '@src/hooks/calendar/useCalendarColor';\nimport { useDrag } from '@src/hooks/common/useDrag';\nimport { useTransientUpdate } from '@src/hooks/common/useTransientUpdate';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport { dndSelector, optionsSelector, viewSelector } from '@src/selectors';\nimport { DraggingState } from '@src/slices/dnd';\nimport { isSameDate } from '@src/time/datetime';\nimport { passConditionalProp } from '@src/utils/preact';\nimport { isPresent } from '@src/utils/type';\n\nimport type { CalendarColor } from '@t/options';\n\ninterface Props {\n  uiModel: EventUIModel;\n  eventHeight: number;\n  headerHeight: number;\n  resizingWidth?: string | null;\n  flat?: boolean;\n  movingLeft?: number | null;\n}\n\nfunction getMargins(flat: boolean) {\n  return {\n    vertical: flat ? 5 : 2,\n    horizontal: 8,\n  };\n}\n\nfunction getBorderRadius(exceedLeft: boolean, exceedRight: boolean): string {\n  const leftBorderRadius = exceedLeft ? 0 : '2px';\n  const rightBorderRadius = exceedRight ? 0 : '2px';\n\n  return `${leftBorderRadius} ${rightBorderRadius} ${rightBorderRadius} ${leftBorderRadius}`;\n}\n\nfunction getEventItemStyle({\n  uiModel,\n  flat,\n  eventHeight,\n  isDraggingTarget,\n  calendarColor,\n}: Required<Pick<Props, 'uiModel' | 'flat' | 'eventHeight'>> & {\n  isDraggingTarget: boolean;\n  calendarColor: CalendarColor;\n}) {\n  const { exceedLeft, exceedRight } = uiModel;\n  const { color, backgroundColor, dragBackgroundColor, borderColor } = getEventColors(\n    uiModel,\n    calendarColor\n  );\n\n  const defaultItemStyle = {\n    color,\n    backgroundColor: isDraggingTarget ? dragBackgroundColor : backgroundColor,\n    borderLeft: exceedLeft ? 'none' : `3px solid ${borderColor}`,\n    borderRadius: getBorderRadius(exceedLeft, exceedRight),\n    overflow: 'hidden',\n    height: eventHeight,\n    lineHeight: toPx(eventHeight),\n    opacity: isDraggingTarget ? 0.5 : 1,\n  };\n  const margins = getMargins(flat);\n\n  return flat\n    ? {\n        marginTop: margins.vertical,\n        ...defaultItemStyle,\n      }\n    : {\n        marginLeft: exceedLeft ? 0 : margins.horizontal,\n        marginRight: exceedRight ? 0 : margins.horizontal,\n        ...defaultItemStyle,\n      };\n}\n\nfunction getContainerStyle({\n  flat,\n  uiModel,\n  resizingWidth,\n  movingLeft,\n  eventHeight,\n  headerHeight,\n}: Required<Props>) {\n  const { top, left, width, model } = uiModel;\n  const margins = getMargins(flat);\n\n  const baseStyle = flat\n    ? {}\n    : {\n        width: resizingWidth || toPercent(width),\n        left: toPercent(movingLeft ?? left),\n        top: (top - 1) * (eventHeight + margins.vertical) + headerHeight,\n        position: 'absolute',\n      };\n\n  return Object.assign(baseStyle, model.customStyle);\n}\n\nfunction getTestId({ model }: EventUIModel) {\n  const calendarId = model.calendarId ? `${model.calendarId}-` : '';\n  const id = model.id ? `${model.id}-` : '';\n\n  return `${calendarId}${id}${model.title}`;\n}\n\nconst classNames = {\n  eventBody: cls('weekday-event'),\n  eventTitle: cls('weekday-event-title'),\n  eventDot: cls('weekday-event-dot'),\n  moveEvent: cls('dragging--move-event'),\n  resizeEvent: cls('dragging--resize-horizontal-event'),\n};\n\n// eslint-disable-next-line complexity\nexport function HorizontalEvent({\n  flat = false,\n  uiModel,\n  eventHeight,\n  headerHeight,\n  resizingWidth = null,\n  movingLeft = null,\n}: Props) {\n  const { currentView } = useStore(viewSelector);\n  const { useDetailPopup, isReadOnly: isReadOnlyCalendar } = useStore(optionsSelector);\n\n  const { setDraggingEventUIModel } = useDispatch('dnd');\n  const { showDetailPopup } = useDispatch('popup');\n\n  const layoutContainer = useLayoutContainer();\n  const eventBus = useEventBus();\n  const calendarColor = useCalendarColor(uiModel.model);\n\n  const [isDraggingTarget, setIsDraggingTarget] = useState<boolean>(false);\n  const eventContainerRef = useRef<HTMLDivElement>(null);\n\n  const { isReadOnly, id, calendarId } = uiModel.model;\n\n  const isDraggingGuideEvent = isPresent(resizingWidth) || isPresent(movingLeft);\n  const isDraggableEvent = !isReadOnlyCalendar && !isReadOnly && !isDraggingGuideEvent;\n\n  const startDragEvent = (className: string) => {\n    setDraggingEventUIModel(uiModel);\n    layoutContainer?.classList.add(className);\n  };\n  const endDragEvent = (className: string) => {\n    setIsDraggingTarget(false);\n    layoutContainer?.classList.remove(className);\n  };\n\n  useTransientUpdate(dndSelector, ({ draggingEventUIModel, draggingState }) => {\n    if (\n      draggingState === DraggingState.DRAGGING &&\n      draggingEventUIModel?.cid() === uiModel.cid() &&\n      !isDraggingGuideEvent\n    ) {\n      setIsDraggingTarget(true);\n    } else {\n      setIsDraggingTarget(false);\n    }\n  });\n\n  useEffect(() => {\n    if (!isDraggingGuideEvent) {\n      eventBus.fire('afterRenderEvent', uiModel.model.toEventObject());\n    }\n    // This effect is only for the first render.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  const onResizeStart = useDrag(DRAGGING_TYPE_CREATORS.resizeEvent('dayGrid', `${uiModel.cid()}`), {\n    onDragStart: () => startDragEvent(classNames.resizeEvent),\n    onMouseUp: () => endDragEvent(classNames.resizeEvent),\n    onPressESCKey: () => endDragEvent(classNames.resizeEvent),\n  });\n  const onMoveStart = useDrag(DRAGGING_TYPE_CREATORS.moveEvent('dayGrid', `${uiModel.cid()}`), {\n    onDragStart: () => {\n      if (isDraggableEvent) {\n        startDragEvent(classNames.moveEvent);\n      }\n    },\n    onMouseUp: (e, { draggingState }) => {\n      endDragEvent(classNames.moveEvent);\n\n      const isClick = draggingState <= DraggingState.INIT;\n      if (isClick && useDetailPopup && eventContainerRef.current) {\n        showDetailPopup(\n          {\n            event: uiModel.model,\n            eventRect: eventContainerRef.current.getBoundingClientRect(),\n          },\n          flat\n        );\n      }\n\n      if (isClick) {\n        eventBus.fire('clickEvent', { event: uiModel.model.toEventObject(), nativeEvent: e });\n      }\n    },\n    onPressESCKey: () => endDragEvent(classNames.moveEvent),\n  });\n\n  const handleResizeStart = (e: MouseEvent) => {\n    e.stopPropagation();\n\n    if (isDraggableEvent) {\n      onResizeStart(e);\n    }\n  };\n\n  const handleMoveStart = (e: MouseEvent) => {\n    e.stopPropagation();\n    onMoveStart(e);\n  };\n\n  const isDotEvent =\n    !isDraggingTarget &&\n    currentView === 'month' &&\n    uiModel.model.category === 'time' &&\n    isSameDate(uiModel.model.start, uiModel.model.end);\n  const shouldHideResizeHandler =\n    !isDraggableEvent || flat || isDraggingTarget || uiModel.exceedRight;\n  const containerStyle = getContainerStyle({\n    uiModel,\n    eventHeight,\n    headerHeight,\n    flat,\n    movingLeft,\n    resizingWidth,\n  });\n  const eventItemStyle = getEventItemStyle({\n    uiModel,\n    flat,\n    eventHeight,\n    isDraggingTarget,\n    calendarColor,\n  });\n\n  return (\n    <div\n      className={cls('weekday-event-block', {\n        'weekday-exceed-left': uiModel.exceedLeft,\n        'weekday-exceed-right': uiModel.exceedRight,\n      })}\n      style={containerStyle}\n      data-testid={passConditionalProp(isDraggableEvent, getTestId(uiModel))}\n      data-calendar-id={calendarId}\n      data-event-id={id}\n      ref={eventContainerRef}\n    >\n      <div\n        className={classNames.eventBody}\n        style={{\n          ...eventItemStyle,\n          backgroundColor: isDotEvent ? null : eventItemStyle.backgroundColor,\n          borderLeft: isDotEvent ? null : eventItemStyle.borderLeft,\n        }}\n        onMouseDown={handleMoveStart}\n      >\n        {isDotEvent ? (\n          <span\n            className={classNames.eventDot}\n            style={{ backgroundColor: eventItemStyle.backgroundColor }}\n          />\n        ) : null}\n        <span className={classNames.eventTitle}>\n          <Template template={uiModel.model.category} param={uiModel.model} />\n        </span>\n        {!shouldHideResizeHandler ? (\n          <HorizontalEventResizeIcon onMouseDown={handleResizeStart} />\n        ) : null}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/events/horizontalEventResizeIcon.tsx",
    "content": "import { h } from 'preact';\n\nimport { cls } from '@src/helpers/css';\n\nimport type { MouseEventListener } from '@t/util';\n\ninterface Props {\n  onMouseDown?: MouseEventListener;\n}\n\nexport function HorizontalEventResizeIcon({ onMouseDown }: Props) {\n  return (\n    <span\n      className={`${cls('weekday-resize-handle')} ${cls('handle-y')}`}\n      onMouseDown={onMouseDown}\n      data-testid=\"horizontal-event-resize-icon\"\n    >\n      <i className={`${cls('icon')} ${cls('ic-handle-y')}`} />\n    </span>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/events/timeEvent.spec.tsx",
    "content": "import { h } from 'preact';\n\nimport { TimeEvent } from '@src/components/events/timeEvent';\nimport { initCalendarStore } from '@src/contexts/calendarStore';\nimport EventModel from '@src/model/eventModel';\nimport EventUIModel from '@src/model/eventUIModel';\nimport { dragAndDrop, fireEvent, render, screen } from '@src/test/utils';\nimport TZDate from '@src/time/date';\nimport { EventBusImpl } from '@src/utils/eventBus';\n\nimport type { ExternalEventTypes } from '@t/eventBus';\n\ndescribe(`Firing 'clickEvent'`, () => {\n  const eventTitle = 'click-event';\n\n  function setup() {\n    const eventBus = new EventBusImpl<ExternalEventTypes>();\n    const handler = jest.fn();\n    eventBus.on('clickEvent', handler);\n    const uiModel = new EventUIModel(\n      new EventModel({\n        id: '1',\n        title: eventTitle,\n        start: new Date('2022-05-19T09:00:00'),\n        end: new Date('2022-05-19T10:00:00'),\n      })\n    );\n    const props = {\n      uiModel,\n      nextStartTime: new TZDate('2022-05-19T11:00:00'),\n    };\n\n    return {\n      props,\n      eventBus,\n      handler,\n    };\n  }\n\n  it('should fire event when clicked', () => {\n    // Given\n    const { eventBus, handler, props } = setup();\n    render(<TimeEvent {...props} />, { eventBus });\n\n    // When\n    const event = screen.getByText(eventTitle);\n    fireEvent.mouseDown(event);\n    fireEvent.mouseUp(event);\n\n    // Then\n    expect(handler).toBeCalledWith(\n      expect.objectContaining({\n        event: props.uiModel.model.toEventObject(),\n      })\n    );\n  });\n\n  it('should not fire when dragged', () => {\n    // Given\n    const { eventBus, handler, props } = setup();\n    render(<TimeEvent {...props} />, { eventBus });\n\n    // When\n    const event = screen.getByText(eventTitle);\n    dragAndDrop({\n      element: event,\n      targetPosition: {\n        clientX: 100,\n        clientY: 100,\n      },\n    });\n\n    // Then\n    expect(handler).not.toBeCalled();\n  });\n});\n\ndescribe(`Firing 'afterRenderEvent'`, () => {\n  function setup() {\n    const eventBus = new EventBusImpl<ExternalEventTypes>();\n    const handler = jest.fn();\n    eventBus.on('afterRenderEvent', handler);\n    const uiModel = new EventUIModel(\n      new EventModel({\n        id: '1',\n        start: new Date('2022-05-19T09:00:00'),\n        end: new Date('2022-05-19T10:00:00'),\n      })\n    );\n    const props = {\n      uiModel,\n      nextStartTime: new TZDate('2022-05-19T11:00:00'),\n    };\n\n    return {\n      props,\n      eventBus,\n      handler,\n    };\n  }\n\n  it(`should fire 'afterRenderEvent' when only the component is mounted`, () => {\n    // Given\n    const { props, eventBus, handler } = setup();\n\n    // When\n    const { rerender } = render(<TimeEvent {...props} />, { eventBus });\n\n    // Then\n    expect(handler).toBeCalledWith(props.uiModel.model.toEventObject());\n\n    // When (re-render)\n    handler.mockReset();\n    rerender(<TimeEvent {...props} />);\n\n    // Then\n    expect(handler).not.toBeCalled();\n  });\n\n  it(`should not fire 'afterRenderEvent' when the component is working as a drag & drop interaction guide element`, () => {\n    // Given\n    const { props, eventBus, handler } = setup();\n\n    // When\n    render(<TimeEvent {...props} isResizingGuide={true} />, { eventBus });\n\n    // Then\n    expect(handler).not.toBeCalled();\n  });\n});\n\ndescribe('Apply customStyle', () => {\n  it('should apply customStyle when the EventModel has the customStyle object', () => {\n    // Given\n    const customStyle = {\n      textDecoration: 'line-through',\n    };\n    const uiModel = new EventUIModel(\n      new EventModel({\n        id: '1',\n        title: 'style-test',\n        start: new Date('2022-06-05T09:00:00'),\n        end: new Date('2022-06-05T1100:00'),\n        customStyle,\n      })\n    );\n\n    // When\n    render(<TimeEvent uiModel={uiModel} />);\n\n    // Then\n    const container = screen.getByTestId(/time-event/);\n    expect(container).toHaveStyle(customStyle);\n  });\n});\n\ndescribe('Color values', () => {\n  const calendarId = 'cal1';\n  const title = 'style-test';\n  const calendarColors = {\n    color: '#ff0000',\n    borderColor: '#ff0000',\n    backgroundColor: '#ff0000',\n    dragBackgroundColor: '#ff0000',\n  };\n  const store = initCalendarStore({\n    calendars: [\n      {\n        id: calendarId,\n        name: calendarId,\n        ...calendarColors,\n      },\n    ],\n  });\n  it('should apply calendar color values to the event', () => {\n    // Given\n    const uiModel = new EventUIModel(\n      new EventModel({\n        id: '1',\n        calendarId,\n        title,\n        start: new Date('2022-06-05T09:00:00'),\n        end: new Date('2022-06-05T1100:00'),\n      })\n    );\n\n    // When\n    render(<TimeEvent uiModel={uiModel} />, { store });\n\n    // Then\n    const container = screen.getByTestId(/time-event/);\n    expect(container).toHaveStyle({\n      color: calendarColors.color,\n      borderColor: `3px solid ${calendarColors.borderColor}`,\n      backgroundColor: calendarColors.backgroundColor,\n    });\n  });\n\n  it('should apply color properties of the event model overriding calendar colors', () => {\n    // Given\n    const eventColors = {\n      color: '#723a5c',\n      borderColor: '#723a5c',\n      backgroundColor: '#723a5c',\n    };\n    const uiModel = new EventUIModel(\n      new EventModel({\n        id: '1',\n        calendarId,\n        title,\n        start: new Date('2022-06-05T09:00:00'),\n        end: new Date('2022-06-05T1100:00'),\n        ...eventColors,\n      })\n    );\n\n    // When\n    render(<TimeEvent uiModel={uiModel} />, { store });\n\n    // Then\n    const container = screen.getByTestId(/time-event/);\n    expect(container).toHaveStyle({\n      color: eventColors.color,\n      borderColor: `3px solid ${eventColors.borderColor}`,\n      backgroundColor: eventColors.backgroundColor,\n    });\n  });\n\n  it('should apply the dragBackgroundColor value to the dragging event', () => {\n    // Given\n    const uiModel = new EventUIModel(\n      new EventModel({\n        id: '1',\n        calendarId,\n        title,\n        start: new Date('2022-06-05T09:00:00'),\n        end: new Date('2022-06-05T1100:00'),\n      })\n    );\n    const getEventElement = () => screen.getByTestId(/time-event/);\n\n    // When\n    render(<TimeEvent uiModel={uiModel} />, { store });\n    dragAndDrop({\n      element: getEventElement(),\n      targetPosition: {\n        clientX: 100,\n        clientY: 100,\n      },\n      hold: true,\n    });\n\n    // Then\n    expect(getEventElement()).toHaveStyle({\n      backgroundColor: calendarColors.dragBackgroundColor,\n    });\n  });\n});\n\ndescribe('isReadOnly', () => {\n  it(\"should be able to show detail popup even if the model's `isReadOnly` property is `true`\", () => {\n    // Given\n    const store = initCalendarStore({\n      useDetailPopup: true,\n    });\n    const eventName = 'readonly-event';\n    const uiModel = new EventUIModel(\n      new EventModel({\n        id: '1',\n        title: eventName,\n        start: new Date('2022-06-05T09:00:00'),\n        end: new Date('2022-06-05T1100:00'),\n        isReadOnly: true,\n      })\n    );\n    const showDetailPopupSpy = jest.fn();\n    store.getState().dispatch.popup.showDetailPopup = showDetailPopupSpy;\n\n    // When\n    render(<TimeEvent uiModel={uiModel} />, { store });\n\n    // Then\n    const event = screen.getByText(eventName);\n    // NOTE: userEvent.click is not working as expected\n    fireEvent.mouseDown(event);\n    fireEvent.mouseUp(event);\n\n    expect(showDetailPopupSpy).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/components/events/timeEvent.tsx",
    "content": "import { h } from 'preact';\nimport { useEffect, useRef, useState } from 'preact/hooks';\n\nimport { Template } from '@src/components/template';\nimport { DEFAULT_DUPLICATE_EVENT_CID } from '@src/constants/layout';\nimport { TIME_EVENT_CONTAINER_MARGIN_LEFT } from '@src/constants/style';\nimport { useDispatch, useStore } from '@src/contexts/calendarStore';\nimport { useEventBus } from '@src/contexts/eventBus';\nimport { useLayoutContainer } from '@src/contexts/layoutContainer';\nimport { cls, extractPercentPx, getEventColors, toPercent } from '@src/helpers/css';\nimport { DRAGGING_TYPE_CREATORS } from '@src/helpers/drag';\nimport { useCalendarColor } from '@src/hooks/calendar/useCalendarColor';\nimport { useDrag } from '@src/hooks/common/useDrag';\nimport { useTransientUpdate } from '@src/hooks/common/useTransientUpdate';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport { dndSelector, optionsSelector } from '@src/selectors';\nimport { DraggingState } from '@src/slices/dnd';\nimport type TZDate from '@src/time/date';\nimport { isPresent, isString } from '@src/utils/type';\n\nimport type { StyleProp } from '@t/components/common';\nimport type { CalendarColor } from '@t/options';\n\nconst classNames = {\n  time: cls('event-time'),\n  content: cls('event-time-content'),\n  travelTime: cls('travel-time'),\n  resizeHandleX: cls('resize-handler-x'),\n  moveEvent: cls('dragging--move-event'),\n  resizeEvent: cls('dragging--resize-vertical-event'),\n};\n\ninterface Props {\n  uiModel: EventUIModel;\n  isResizingGuide?: boolean;\n  nextStartTime?: TZDate | null;\n  minHeight?: number;\n}\n\nfunction getMarginLeft(left: number | string) {\n  const { percent, px } = extractPercentPx(`${left}`);\n\n  return left > 0 || percent > 0 || px > 0 ? TIME_EVENT_CONTAINER_MARGIN_LEFT : 0;\n}\n\nfunction getContainerWidth(width: number | string, marginLeft: number) {\n  if (isString(width)) {\n    return width;\n  }\n  if (width >= 0) {\n    return `calc(${toPercent(width)} - ${marginLeft}px)`;\n  }\n\n  return '';\n}\n\nfunction getStyles({\n  uiModel,\n  isDraggingTarget,\n  hasNextStartTime,\n  calendarColor,\n  minHeight,\n}: {\n  uiModel: EventUIModel;\n  isDraggingTarget: boolean;\n  hasNextStartTime: boolean;\n  calendarColor: CalendarColor;\n  minHeight: number;\n}) {\n  const {\n    top,\n    left,\n    height,\n    width,\n    duplicateLeft,\n    duplicateWidth,\n    goingDurationHeight,\n    modelDurationHeight,\n    comingDurationHeight,\n    croppedStart,\n    croppedEnd,\n  } = uiModel;\n  // TODO: check and get theme values\n  const travelBorderColor = 'white';\n  const borderRadius = 2;\n  const defaultMarginBottom = 2;\n  const marginLeft = getMarginLeft(left);\n\n  const { color, backgroundColor, borderColor, dragBackgroundColor } = getEventColors(\n    uiModel,\n    calendarColor\n  );\n  const containerStyle: StyleProp = {\n    width: getContainerWidth(duplicateWidth || width, marginLeft),\n    height: `calc(${toPercent(Math.max(height, minHeight))} - ${defaultMarginBottom}px)`,\n    top: toPercent(top),\n    left: duplicateLeft || toPercent(left),\n    borderRadius,\n    borderLeft: `3px solid ${borderColor}`,\n    marginLeft,\n    color,\n    backgroundColor: isDraggingTarget ? dragBackgroundColor : backgroundColor,\n    opacity: isDraggingTarget ? 0.5 : 1,\n    zIndex: hasNextStartTime ? 1 : 0,\n  };\n\n  const goingDurationStyle = {\n    height: toPercent(goingDurationHeight),\n    borderBottom: `1px dashed ${travelBorderColor}`,\n  };\n  const modelDurationStyle = {\n    height: toPercent(modelDurationHeight),\n  };\n  const comingDurationStyle = {\n    height: toPercent(comingDurationHeight),\n    borderTop: `1px dashed ${travelBorderColor}`,\n  };\n\n  if (croppedStart) {\n    containerStyle.borderTopLeftRadius = 0;\n    containerStyle.borderTopRightRadius = 0;\n  }\n\n  if (croppedEnd) {\n    containerStyle.borderBottomLeftRadius = 0;\n    containerStyle.borderBottomRightRadius = 0;\n  }\n\n  return {\n    containerStyle,\n    goingDurationStyle,\n    modelDurationStyle,\n    comingDurationStyle,\n  };\n}\n\nfunction isDraggableEvent({\n  uiModel,\n  isReadOnlyCalendar,\n  isDraggingTarget,\n  hasNextStartTime,\n}: {\n  uiModel: EventUIModel;\n  isReadOnlyCalendar: boolean;\n  isDraggingTarget: boolean;\n  hasNextStartTime: boolean;\n}) {\n  const { model } = uiModel;\n  return !isReadOnlyCalendar && !model.isReadOnly && !isDraggingTarget && !hasNextStartTime;\n}\n\n// eslint-disable-next-line complexity\nexport function TimeEvent({\n  uiModel,\n  nextStartTime,\n  isResizingGuide = false,\n  minHeight = 0,\n}: Props) {\n  const {\n    useDetailPopup,\n    isReadOnly: isReadOnlyCalendar,\n    week: weekOptions,\n  } = useStore(optionsSelector);\n  const calendarColor = useCalendarColor(uiModel.model);\n  const { collapseDuplicateEvents } = weekOptions;\n\n  const layoutContainer = useLayoutContainer();\n  const { showDetailPopup } = useDispatch('popup');\n  const { setDraggingEventUIModel } = useDispatch('dnd');\n  const { setSelectedDuplicateEventCid } = useDispatch('weekViewLayout');\n\n  const eventBus = useEventBus();\n\n  const eventContainerRef = useRef<HTMLDivElement>(null);\n\n  const [isDraggingTarget, setIsDraggingTarget] = useState<boolean>(false);\n\n  const { model, goingDurationHeight, modelDurationHeight, comingDurationHeight, croppedEnd } =\n    uiModel;\n  const { id, calendarId, customStyle } = model;\n  const hasNextStartTime = isPresent(nextStartTime);\n  const { containerStyle, goingDurationStyle, modelDurationStyle, comingDurationStyle } = getStyles(\n    { uiModel, isDraggingTarget, hasNextStartTime, calendarColor, minHeight }\n  );\n  const isGuide = hasNextStartTime || isResizingGuide;\n\n  useTransientUpdate(dndSelector, ({ draggingEventUIModel, draggingState }) => {\n    if (\n      draggingState === DraggingState.DRAGGING &&\n      draggingEventUIModel?.cid() === uiModel.cid() &&\n      !hasNextStartTime &&\n      !isResizingGuide\n    ) {\n      setIsDraggingTarget(true);\n    } else {\n      setIsDraggingTarget(false);\n    }\n  });\n\n  useEffect(() => {\n    if (!isResizingGuide) {\n      eventBus.fire('afterRenderEvent', uiModel.model.toEventObject());\n    }\n    // This effect is only for the first render.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  const startDragEvent = (className: string) => {\n    setDraggingEventUIModel(uiModel);\n    layoutContainer?.classList.add(className);\n  };\n  const endDragEvent = (className: string) => {\n    setIsDraggingTarget(false);\n    layoutContainer?.classList.remove(className);\n  };\n\n  const onMoveStart = useDrag(DRAGGING_TYPE_CREATORS.moveEvent('timeGrid', `${uiModel.cid()}`), {\n    onDragStart: () => {\n      if (isDraggable) {\n        startDragEvent(classNames.moveEvent);\n      }\n    },\n    onMouseUp: (e, { draggingState }) => {\n      endDragEvent(classNames.moveEvent);\n\n      const isClick = draggingState <= DraggingState.INIT;\n      if (isClick && collapseDuplicateEvents) {\n        const selectedDuplicateEventCid =\n          uiModel.duplicateEvents.length > 0 ? uiModel.cid() : DEFAULT_DUPLICATE_EVENT_CID;\n        setSelectedDuplicateEventCid(selectedDuplicateEventCid);\n      }\n\n      if (isClick && useDetailPopup && eventContainerRef.current) {\n        showDetailPopup(\n          {\n            event: uiModel.model,\n            eventRect: eventContainerRef.current.getBoundingClientRect(),\n          },\n          false\n        );\n      }\n\n      if (isClick) {\n        eventBus.fire('clickEvent', { event: uiModel.model.toEventObject(), nativeEvent: e });\n      }\n    },\n    onPressESCKey: () => endDragEvent(classNames.moveEvent),\n  });\n  const handleMoveStart = (e: MouseEvent) => {\n    e.stopPropagation();\n    onMoveStart(e);\n  };\n\n  const onResizeStart = useDrag(\n    DRAGGING_TYPE_CREATORS.resizeEvent('timeGrid', `${uiModel.cid()}`),\n    {\n      onDragStart: () => startDragEvent(classNames.resizeEvent),\n      onMouseUp: () => endDragEvent(classNames.resizeEvent),\n      onPressESCKey: () => endDragEvent(classNames.resizeEvent),\n    }\n  );\n  const handleResizeStart = (e: MouseEvent) => {\n    e.stopPropagation();\n    onResizeStart(e);\n  };\n\n  const isDraggable = isDraggableEvent({\n    uiModel,\n    isReadOnlyCalendar,\n    isDraggingTarget,\n    hasNextStartTime,\n  });\n  const shouldShowResizeHandle = isDraggable && !croppedEnd;\n\n  return (\n    <div\n      data-testid={`${isGuide ? 'guide-' : ''}time-event-${model.title}-${uiModel.cid()}`}\n      data-calendar-id={calendarId}\n      data-event-id={id}\n      className={classNames.time}\n      style={{ ...containerStyle, ...customStyle }}\n      onMouseDown={handleMoveStart}\n      ref={eventContainerRef}\n    >\n      {goingDurationHeight ? (\n        <div className={classNames.travelTime} style={goingDurationStyle}>\n          <Template template=\"goingDuration\" param={model} />\n        </div>\n      ) : null}\n      {modelDurationHeight ? (\n        <div className={classNames.content} style={modelDurationStyle}>\n          <Template\n            template=\"time\"\n            param={{\n              ...model.toEventObject(),\n              start: hasNextStartTime ? nextStartTime : model.start,\n            }}\n          />\n        </div>\n      ) : null}\n      {comingDurationHeight ? (\n        <div className={classNames.travelTime} style={comingDurationStyle}>\n          <Template template=\"comingDuration\" param={model} />\n        </div>\n      ) : null}\n      {shouldShowResizeHandle ? (\n        <div className={classNames.resizeHandleX} onMouseDown={handleResizeStart} />\n      ) : null}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/layout.tsx",
    "content": "import type { ComponentChildren, ComponentProps } from 'preact';\nimport { h, toChildArray } from 'preact';\nimport { useLayoutEffect, useMemo } from 'preact/hooks';\n\nimport type { Panel } from '@src/components/panel';\nimport { EventDetailPopup } from '@src/components/popup/eventDetailPopup';\nimport { EventFormPopup } from '@src/components/popup/eventFormPopup';\nimport { PopupOverlay } from '@src/components/popup/popupOverlay';\nimport { SeeMoreEventsPopup } from '@src/components/popup/seeMoreEventsPopup';\nimport { useDispatch } from '@src/contexts/calendarStore';\nimport { LayoutContainerProvider } from '@src/contexts/layoutContainer';\nimport { useTheme } from '@src/contexts/themeStore';\nimport { cls, toPercent } from '@src/helpers/css';\nimport { useDOMNode } from '@src/hooks/common/useDOMNode';\nimport { commonThemeSelector } from '@src/selectors/theme';\nimport { noop } from '@src/utils/noop';\nimport { isNil, isNumber, isString } from '@src/utils/type';\n\nimport type { PropsWithChildren, StyleProp } from '@t/components/common';\n\ninterface Props {\n  height?: number;\n  width?: number;\n  className?: string;\n  autoAdjustPanels?: boolean;\n  children: ComponentChildren;\n}\n\nfunction getLayoutStylesFromInfo(width?: number, height?: number) {\n  const styles: StyleProp = { height: toPercent(100) };\n\n  if (width) {\n    styles.width = width;\n  }\n  if (height) {\n    styles.height = height;\n  }\n\n  return styles;\n}\n\n// TODO: consider `direction` and `resizeMode`\nexport function Layout({\n  children,\n  width,\n  height,\n  className = '',\n  autoAdjustPanels = false,\n}: PropsWithChildren<Props>) {\n  const { backgroundColor } = useTheme(commonThemeSelector);\n\n  const [container, containerRefCallback] = useDOMNode<HTMLDivElement>();\n  const { setLastPanelType, updateLayoutHeight } = useDispatch('weekViewLayout');\n\n  const layoutClassName = useMemo(() => `${cls('layout')} ${className}`, [className]);\n\n  useLayoutEffect(() => {\n    if (container) {\n      const onResizeWindow = () => updateLayoutHeight(container.offsetHeight);\n\n      onResizeWindow();\n      window.addEventListener('resize', onResizeWindow);\n\n      return () => window.removeEventListener('resize', onResizeWindow);\n    }\n\n    return noop;\n  }, [container, updateLayoutHeight]);\n\n  useLayoutEffect(() => {\n    if (container && autoAdjustPanels) {\n      const childArray = toChildArray(children);\n      const lastChild = childArray[childArray.length - 1];\n\n      if (!isString(lastChild) && !isNumber(lastChild) && !isNil(lastChild)) {\n        setLastPanelType((lastChild.props as unknown as ComponentProps<typeof Panel>).name);\n      }\n    }\n  }, [children, setLastPanelType, autoAdjustPanels, container]);\n\n  return (\n    <LayoutContainerProvider value={container}>\n      <div\n        ref={containerRefCallback}\n        className={layoutClassName}\n        style={{ ...getLayoutStylesFromInfo(width, height), backgroundColor }}\n      >\n        {container ? children : null}\n      </div>\n      <EventFormPopup />\n      <EventDetailPopup />\n      <SeeMoreEventsPopup />\n      <PopupOverlay />\n    </LayoutContainerProvider>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/panel.tsx",
    "content": "import { Fragment, h } from 'preact';\nimport { forwardRef } from 'preact/compat';\nimport { useCallback, useLayoutEffect, useMemo } from 'preact/hooks';\n\nimport { PanelResizer } from '@src/components/panelResizer';\nimport { DEFAULT_RESIZER_LENGTH } from '@src/constants/layout';\nimport { DEFAULT_PANEL_HEIGHT } from '@src/constants/style';\nimport { useDispatch, useStore } from '@src/contexts/calendarStore';\nimport { cls } from '@src/helpers/css';\nimport { isBoolean, isNil } from '@src/utils/type';\n\nimport type { PropsWithChildren, StyleProp } from '@t/components/common';\nimport type { AlldayEventCategory } from '@t/panel';\n\ninterface Props {\n  name: string;\n  overflowY?: boolean;\n  overflowX?: boolean;\n  show?: boolean;\n\n  autoSize?: number;\n  initialHeight?: number;\n  initialWidth?: number;\n  minHeight?: number;\n  minWidth?: number;\n  maxHeight?: number;\n  maxWidth?: number;\n\n  expandable?: boolean;\n  maxExpandableHeight?: number;\n  maxExpandableWidth?: number;\n\n  resizable?: boolean | string[];\n  resizerHeight?: number;\n  resizerWidth?: number;\n}\n\nfunction getPanelSide(side: number, maxExpandableSide?: number) {\n  return maxExpandableSide ? Math.min(maxExpandableSide, side) : side;\n}\n\nfunction getPanelStyle({\n  initialHeight,\n  initialWidth,\n  overflowX,\n  overflowY,\n  maxExpandableWidth,\n  maxExpandableHeight,\n  minHeight,\n  maxHeight,\n  minWidth,\n  maxWidth,\n}: Partial<Props>) {\n  const style: StyleProp = {};\n\n  if (initialWidth) {\n    style.width = getPanelSide(initialWidth, maxExpandableWidth);\n    style.height = '100%';\n  }\n  if (initialHeight) {\n    style.width = '100%';\n    style.height = getPanelSide(initialHeight, maxExpandableHeight);\n  }\n\n  if (overflowX) {\n    style.overflowX = 'auto';\n  }\n  if (overflowY) {\n    style.overflowY = 'auto';\n  }\n\n  return { ...style, minHeight, maxHeight, minWidth, maxWidth };\n}\n\nexport const Panel = forwardRef<HTMLDivElement, PropsWithChildren<Props>>(function Panel(\n  {\n    name,\n    initialWidth = DEFAULT_PANEL_HEIGHT,\n    initialHeight = DEFAULT_PANEL_HEIGHT,\n    overflowX,\n    overflowY,\n    maxExpandableWidth,\n    maxExpandableHeight,\n    minHeight,\n    maxHeight,\n    minWidth,\n    maxWidth,\n    resizerWidth = DEFAULT_RESIZER_LENGTH,\n    resizerHeight = DEFAULT_RESIZER_LENGTH,\n    resizable,\n    children,\n  },\n  ref\n) {\n  const { updateDayGridRowHeight } = useDispatch('weekViewLayout');\n  const { height: dayGridRowHeight } = useStore(\n    useCallback((state) => state.weekViewLayout.dayGridRows[name] ?? {}, [name])\n  );\n  const height = dayGridRowHeight ?? initialHeight;\n\n  useLayoutEffect(() => {\n    updateDayGridRowHeight({ rowName: name, height: initialHeight });\n  }, [initialHeight, name, updateDayGridRowHeight]);\n\n  const styles = getPanelStyle({\n    initialWidth,\n    initialHeight: height,\n    overflowX,\n    overflowY,\n    maxExpandableWidth,\n    maxExpandableHeight,\n    minHeight,\n    maxHeight,\n    minWidth,\n    maxWidth,\n  });\n\n  const isResizable = useMemo(() => {\n    if (isNil(resizable) || isBoolean(resizable)) {\n      return !!resizable;\n    }\n\n    return resizable.includes(name);\n  }, [resizable, name]);\n\n  return (\n    <Fragment>\n      <div className={cls('panel', name)} style={styles} ref={ref}>\n        {children}\n      </div>\n      {isResizable ? (\n        <PanelResizer\n          name={name as AlldayEventCategory}\n          width={resizerWidth}\n          height={resizerHeight}\n        />\n      ) : null}\n    </Fragment>\n  );\n});\n"
  },
  {
    "path": "apps/calendar/src/components/panelResizer.tsx",
    "content": "import { h } from 'preact';\nimport { useCallback, useRef, useState } from 'preact/hooks';\n\nimport { useDispatch } from '@src/contexts/calendarStore';\nimport { useTheme } from '@src/contexts/themeStore';\nimport { cls } from '@src/helpers/css';\nimport { DRAGGING_TYPE_CONSTANTS } from '@src/helpers/drag';\nimport { useDrag } from '@src/hooks/common/useDrag';\n\nimport type { StyleProp } from '@t/components/common';\nimport type { AlldayEventCategory } from '@t/panel';\n\ninterface Props {\n  name: AlldayEventCategory;\n  width: number;\n  height: number;\n}\n\nfunction getDefaultStyle(height: number, border: string) {\n  return {\n    height,\n    width: '100%',\n    cursor: 'row-resize',\n    borderTop: border,\n    borderBottom: border,\n  };\n}\n\nexport function PanelResizer({ name, height }: Props) {\n  const border = useTheme(useCallback((theme) => theme.week.panelResizer.border, []));\n  const style = getDefaultStyle(height, border);\n  const defaultGuideStyle = {\n    ...style,\n    display: 'none',\n    border: 'none',\n    backgroundColor: '#999',\n  };\n\n  const [guideStyle, setGuideStyle] = useState<StyleProp>(defaultGuideStyle);\n  const startPos = useRef<{ left: number; top: number } | null>(null);\n  const { updateDayGridRowHeightByDiff } = useDispatch('weekViewLayout');\n\n  const onMouseDown = useDrag(DRAGGING_TYPE_CONSTANTS.panelResizer, {\n    onDragStart: (e) => {\n      startPos.current = { left: e.pageX, top: e.pageY };\n    },\n    onDrag: (e) => {\n      if (startPos.current) {\n        const top = e.pageY - startPos.current.top;\n\n        setGuideStyle((prev) => ({ ...prev, top, display: null }));\n      }\n    },\n    onMouseUp: (e) => {\n      if (startPos.current) {\n        const diff = e.pageY - startPos.current.top;\n\n        startPos.current = null;\n\n        setGuideStyle(defaultGuideStyle);\n        updateDayGridRowHeightByDiff({ rowName: name, diff });\n      }\n    },\n  });\n\n  return (\n    <div style={{ position: 'relative' }}>\n      <div className={cls('panel-resizer')} style={style} onMouseDown={onMouseDown} />\n      <div className={cls('panel-resizer-guide')} style={guideStyle} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/popup/calendarDropdownMenu.tsx",
    "content": "import { h } from 'preact';\n\nimport { cls } from '@src/helpers/css';\n\nimport type { CalendarInfo } from '@t/options';\n\ninterface Props {\n  open?: boolean;\n  calendars: CalendarInfo[];\n  setOpened: (isOpened: boolean) => void;\n  onChangeIndex: (index: number) => void;\n}\n\ninterface DropdownMenuItemProps {\n  index: number;\n  name: string;\n  backgroundColor: string;\n  onClick: (e: MouseEvent, index: number) => void;\n}\n\nconst classNames = {\n  dropdownMenu: cls('dropdown-menu'),\n  dropdownMenuItem: cls('dropdown-menu-item'),\n  dotIcon: cls('icon', 'dot'),\n  content: cls('content'),\n};\n\nfunction DropdownMenuItem({ index, name, backgroundColor, onClick }: DropdownMenuItemProps) {\n  return (\n    <li className={classNames.dropdownMenuItem} onClick={(e) => onClick(e, index)}>\n      <span className={classNames.dotIcon} style={{ backgroundColor }} />\n      <span className={classNames.content}>{name}</span>\n    </li>\n  );\n}\n\nexport function CalendarDropdownMenu({ calendars, setOpened, onChangeIndex }: Props) {\n  const handleDropdownMenuItemClick = (e: MouseEvent, index: number) => {\n    e.stopPropagation();\n    setOpened(false);\n    onChangeIndex(index);\n  };\n\n  return (\n    <ul className={classNames.dropdownMenu}>\n      {calendars.map(({ name, backgroundColor = '000' }, index) => (\n        <DropdownMenuItem\n          key={`dropdown-${name}-${index}`}\n          index={index}\n          name={name}\n          backgroundColor={backgroundColor}\n          onClick={handleDropdownMenuItemClick}\n        />\n      ))}\n    </ul>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/popup/calendarSelector.tsx",
    "content": "import { h } from 'preact';\n\nimport { CalendarDropdownMenu } from '@src/components/popup/calendarDropdownMenu';\nimport { PopupSection } from '@src/components/popup/popupSection';\nimport { cls } from '@src/helpers/css';\nimport { useDropdownState } from '@src/hooks/common/useDropdownState';\nimport type { FormStateDispatcher } from '@src/hooks/popup/useFormState';\nimport { FormStateActionType } from '@src/hooks/popup/useFormState';\n\nimport type { CalendarInfo } from '@t/options';\n\ninterface Props {\n  calendars: CalendarInfo[];\n  selectedCalendarId?: string;\n  formStateDispatch: FormStateDispatcher;\n}\n\nconst classNames = {\n  popupSection: ['dropdown-section', 'calendar-section'],\n  popupSectionItem: cls('popup-section-item', 'popup-button'),\n  dotIcon: cls('icon', 'dot'),\n  content: cls('content', 'event-calendar'),\n};\n\nexport function CalendarSelector({ calendars, selectedCalendarId, formStateDispatch }: Props) {\n  const { isOpened, setOpened, toggleDropdown } = useDropdownState();\n\n  const selectedCalendar = calendars.find((calendar) => calendar.id === selectedCalendarId);\n  const { backgroundColor = '', name = '' } = selectedCalendar ?? {};\n\n  const changeIndex = (index: number) =>\n    formStateDispatch({ type: FormStateActionType.setCalendarId, calendarId: calendars[index].id });\n\n  return (\n    <PopupSection onClick={toggleDropdown} classNames={classNames.popupSection}>\n      <button type=\"button\" className={classNames.popupSectionItem}>\n        <span className={classNames.dotIcon} style={{ backgroundColor }} />\n        <span className={classNames.content}>{name}</span>\n        <span className={cls('icon', 'ic-dropdown-arrow', { open: isOpened })} />\n      </button>\n      {isOpened && (\n        <CalendarDropdownMenu\n          calendars={calendars}\n          setOpened={setOpened}\n          onChangeIndex={changeIndex}\n        />\n      )}\n    </PopupSection>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/popup/closePopupButton.tsx",
    "content": "import { h } from 'preact';\n\nimport { Template } from '@src/components/template';\nimport { useDispatch } from '@src/contexts/calendarStore';\nimport { cls } from '@src/helpers/css';\nimport { isFunction } from '@src/utils/type';\n\ninterface Props {\n  type: 'moreEvents' | 'form';\n  close?: () => void;\n}\n\nconst classNames = {\n  closeButton: cls('popup-button', 'popup-close'),\n  closeIcon: cls('icon', 'ic-close'),\n};\n\nexport function ClosePopupButton({ type, close }: Props) {\n  const { hideAllPopup } = useDispatch('popup');\n\n  const onClickHandler = () => {\n    hideAllPopup();\n\n    if (isFunction(close)) {\n      close();\n    }\n  };\n\n  return (\n    <button type=\"button\" className={classNames.closeButton} onClick={onClickHandler}>\n      {type === 'moreEvents' ? (\n        <Template template=\"monthMoreClose\" />\n      ) : (\n        <i className={classNames.closeIcon} />\n      )}\n    </button>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/popup/confirmPopupButton.tsx",
    "content": "import { h } from 'preact';\n\nimport { cls } from '@src/helpers/css';\n\nimport type { PropsWithChildren } from '@t/components/common';\n\nconst classNames = {\n  confirmButton: cls('popup-button', 'popup-confirm'),\n};\n\nexport function ConfirmPopupButton({ children }: PropsWithChildren) {\n  return (\n    <button type=\"submit\" className={classNames.confirmButton}>\n      <span>{children}</span>\n    </button>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/popup/dateSelector.tsx",
    "content": "import type { RefObject } from 'preact';\nimport { h } from 'preact';\nimport { forwardRef } from 'preact/compat';\nimport { useEffect, useRef } from 'preact/hooks';\n\nimport type { DateRangePicker } from 'tui-date-picker';\nimport DatePicker from 'tui-date-picker';\n\nimport { PopupSection } from '@src/components/popup/popupSection';\nimport { Template } from '@src/components/template';\nimport { useStore } from '@src/contexts/calendarStore';\nimport { cls } from '@src/helpers/css';\nimport type { FormStateDispatcher } from '@src/hooks/popup/useFormState';\nimport { FormStateActionType } from '@src/hooks/popup/useFormState';\nimport { useStringOnlyTemplate } from '@src/hooks/template/useStringOnlyTemplate';\nimport { optionsSelector } from '@src/selectors';\nimport TZDate from '@src/time/date';\n\ninterface Props {\n  start: TZDate;\n  end: TZDate;\n  isAllday?: boolean;\n  formStateDispatch: FormStateDispatcher;\n}\n\nconst classNames = {\n  datePickerContainer: cls('datepicker-container'),\n  datePicker: cls('popup-section-item', 'popup-date-picker'),\n  allday: cls('popup-section-item', 'popup-section-allday'),\n  dateIcon: cls('icon', 'ic-date'),\n  dateDash: cls('popup-date-dash'),\n  content: cls('content'),\n};\n\nexport const DateSelector = forwardRef<DateRangePicker, Props>(function DateSelector(\n  { start, end, isAllday = false, formStateDispatch },\n  ref\n) {\n  const { usageStatistics } = useStore(optionsSelector);\n  const startPickerContainerRef = useRef<HTMLDivElement>(null);\n  const startPickerInputRef = useRef<HTMLInputElement>(null);\n  const endPickerContainerRef = useRef<HTMLDivElement>(null);\n  const endPickerInputRef = useRef<HTMLInputElement>(null);\n\n  const startDatePlaceholder = useStringOnlyTemplate({\n    template: 'startDatePlaceholder',\n    defaultValue: 'Start Date',\n  });\n  const endDatePlaceholder = useStringOnlyTemplate({\n    template: 'endDatePlaceholder',\n    defaultValue: 'End Date',\n  });\n  const toggleAllday = () =>\n    formStateDispatch({ type: FormStateActionType.setAllday, isAllday: !isAllday });\n\n  useEffect(() => {\n    if (\n      startPickerContainerRef.current &&\n      startPickerInputRef.current &&\n      endPickerContainerRef.current &&\n      endPickerInputRef.current\n    ) {\n      const startDate = new TZDate(start);\n      const endDate = new TZDate(end);\n      // NOTE: Setting default start/end time when editing allday event first time.\n      // This logic refers to Apple calendar's behavior.\n      if (isAllday) {\n        startDate.setHours(12, 0, 0);\n        endDate.setHours(13, 0, 0);\n      }\n\n      (ref as RefObject<DateRangePicker>).current = DatePicker.createRangePicker({\n        startpicker: {\n          date: startDate.toDate(),\n          input: startPickerInputRef.current,\n          container: startPickerContainerRef.current,\n        },\n        endpicker: {\n          date: endDate.toDate(),\n          input: endPickerInputRef.current,\n          container: endPickerContainerRef.current,\n        },\n        format: isAllday ? 'yyyy-MM-dd' : 'yyyy-MM-dd HH:mm',\n        timePicker: isAllday\n          ? false\n          : {\n              showMeridiem: false,\n              usageStatistics,\n            },\n        usageStatistics,\n      });\n    }\n  }, [start, end, isAllday, usageStatistics, ref]);\n\n  return (\n    <PopupSection>\n      <div className={classNames.datePicker}>\n        <span className={classNames.dateIcon} />\n        <input\n          name=\"start\"\n          className={classNames.content}\n          placeholder={startDatePlaceholder}\n          ref={startPickerInputRef}\n        />\n        <div className={classNames.datePickerContainer} ref={startPickerContainerRef} />\n      </div>\n      <span className={classNames.dateDash}>-</span>\n      <div className={classNames.datePicker}>\n        <span className={classNames.dateIcon} />\n        <input\n          name=\"end\"\n          className={classNames.content}\n          placeholder={endDatePlaceholder}\n          ref={endPickerInputRef}\n        />\n        <div className={classNames.datePickerContainer} ref={endPickerContainerRef} />\n      </div>\n      <div className={classNames.allday} onClick={toggleAllday}>\n        <span\n          className={cls('icon', {\n            'ic-checkbox-normal': !isAllday,\n            'ic-checkbox-checked': isAllday,\n          })}\n        />\n        <span className={classNames.content}>\n          <Template template=\"popupIsAllday\" />\n        </span>\n        <input\n          name=\"isAllday\"\n          type=\"checkbox\"\n          className={cls('hidden-input')}\n          value={isAllday ? 'true' : 'false'}\n          checked={isAllday}\n        />\n      </div>\n    </PopupSection>\n  );\n});\n"
  },
  {
    "path": "apps/calendar/src/components/popup/eventDetailPopup.spec.tsx",
    "content": "import { h } from 'preact';\n\nimport { EventDetailPopup } from '@src/components/popup/eventDetailPopup';\nimport { initCalendarStore, StoreProvider, useDispatch } from '@src/contexts/calendarStore';\nimport { EventBusProvider } from '@src/contexts/eventBus';\nimport { FloatingLayerProvider } from '@src/contexts/floatingLayer';\nimport EventModel from '@src/model/eventModel';\nimport { render, screen, userEvent } from '@src/test/utils';\nimport TZDate from '@src/time/date';\nimport { EventBusImpl } from '@src/utils/eventBus';\n\nimport type { PropsWithChildren } from '@t/components/common';\nimport type { Options } from '@t/options';\n\ndescribe('event detail popup', () => {\n  const mockCalendarId = 'calendarId';\n  const mockCalendarName = 'mock calendar';\n\n  const event = new EventModel({\n    id: 'id',\n    calendarId: mockCalendarId,\n    title: 'title',\n    body: 'body',\n    start: new TZDate(),\n    end: new TZDate(),\n    isAllday: false,\n    location: 'location',\n    attendees: ['attendee1', 'attendee2'],\n    recurrenceRule: 'recurrence rule',\n    isReadOnly: false,\n    backgroundColor: '#03bd9e',\n    state: 'Busy',\n  });\n  const Wrapper = ({ children }: PropsWithChildren) => {\n    const { showDetailPopup } = useDispatch('popup');\n    showDetailPopup(\n      {\n        event,\n        eventRect: {\n          width: 10,\n          height: 10,\n          left: 0,\n          top: 0,\n        },\n      },\n      false\n    );\n\n    return <FloatingLayerProvider>{children}</FloatingLayerProvider>;\n  };\n\n  function setup(options: Options = {}) {\n    const eventBus = new EventBusImpl();\n    const store = initCalendarStore({\n      calendars: [\n        {\n          id: mockCalendarId,\n          name: mockCalendarName,\n        },\n      ],\n      ...options,\n    });\n\n    // Spy should be set before rendering\n    const showFormPopupSpy = jest.fn();\n    store.getState().dispatch.popup.showFormPopup = showFormPopupSpy;\n\n    const renderResult = render(\n      <EventBusProvider value={eventBus}>\n        <StoreProvider store={store}>\n          <Wrapper>\n            <EventDetailPopup />\n          </Wrapper>\n        </StoreProvider>\n      </EventBusProvider>\n    );\n\n    return {\n      eventBus,\n      store,\n      renderResult,\n      showFormPopupSpy,\n    };\n  }\n\n  it('should display location when `event.location` is exists', () => {\n    setup();\n    const { location } = event;\n    const locationText = screen.getByText(location).textContent;\n\n    expect(locationText).toBe(location);\n  });\n\n  it('should display recurrence rule when `event.recurrenceRule` is exists', () => {\n    setup();\n    const { recurrenceRule } = event;\n    const recurrenceRuleText = screen.getByText(recurrenceRule).textContent;\n\n    expect(recurrenceRuleText).toBe(recurrenceRule);\n  });\n\n  it('should display attendees when `event.attendees` is exists', () => {\n    setup();\n    const { attendees } = event;\n    const text = attendees.join(', ');\n    const attendeesText = screen.getByText(text).textContent;\n\n    expect(attendeesText).toBe(text);\n  });\n\n  it('should display state when `event.state` is exists', () => {\n    setup();\n    const { state } = event;\n    const stateText = screen.getByText(state).textContent;\n\n    expect(stateText).toBe(state);\n  });\n\n  it('should display calendar name when `event.calendarId` and corresponding calendar is exists', () => {\n    setup();\n    const calendarName = screen.getByText(mockCalendarName);\n\n    expect(calendarName).toBeInTheDocument();\n  });\n\n  it('should display body when `event.body` is exists', () => {\n    setup();\n    const { body } = event;\n    const bodyText = screen.getByText(body).textContent;\n\n    expect(bodyText).toBe(body);\n  });\n\n  it('should display edit and delete buttons when event is not read only', () => {\n    setup();\n    const editButton = screen.getByText('Edit');\n    const deleteButton = screen.getByText('Delete');\n\n    expect(editButton).not.toBeNull();\n    expect(deleteButton).not.toBeNull();\n  });\n\n  it('should open the form popup when edit button is clicked, while the `useFormPopup` option is enabled', async () => {\n    // Given\n    const { showFormPopupSpy } = setup({ useFormPopup: true });\n    const user = userEvent.setup();\n    const editButton = screen.getByText('Edit');\n\n    // When\n    await user.click(editButton);\n\n    // Then\n    expect(showFormPopupSpy).toHaveBeenCalledWith(expect.objectContaining({ event }));\n  });\n\n  it('should only fire the `beforeUpdateEvent` event when edit button is clicked, while the `useFormPopup` option is disabled', async () => {\n    // Given\n    const { eventBus } = setup({ useFormPopup: false });\n    const mockEventHandler = jest.fn();\n    eventBus.on('beforeUpdateEvent', mockEventHandler);\n\n    const user = userEvent.setup();\n    const editButton = screen.getByText('Edit');\n\n    // When\n    await user.click(editButton);\n\n    // Then\n    expect(mockEventHandler).toHaveBeenCalledWith(\n      expect.objectContaining({ event: event.toEventObject() })\n    );\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/components/popup/eventDetailPopup.tsx",
    "content": "import { h } from 'preact';\nimport { createPortal } from 'preact/compat';\nimport { useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks';\n\nimport { EventDetailSectionDetail } from '@src/components/popup/eventDetailSectionDetail';\nimport { EventDetailSectionHeader } from '@src/components/popup/eventDetailSectionHeader';\nimport { Template } from '@src/components/template';\nimport { DetailPopupArrowDirection, HALF_OF_POPUP_ARROW_HEIGHT } from '@src/constants/popup';\nimport { useDispatch, useStore } from '@src/contexts/calendarStore';\nimport { useEventBus } from '@src/contexts/eventBus';\nimport { useFloatingLayer } from '@src/contexts/floatingLayer';\nimport { useLayoutContainer } from '@src/contexts/layoutContainer';\nimport { cls } from '@src/helpers/css';\nimport { isLeftOutOfLayout, isTopOutOfLayout } from '@src/helpers/popup';\nimport { useCalendarColor } from '@src/hooks/calendar/useCalendarColor';\nimport { optionsSelector } from '@src/selectors';\nimport { eventDetailPopupParamSelector } from '@src/selectors/popup';\nimport TZDate from '@src/time/date';\nimport { isNil } from '@src/utils/type';\n\nimport type { StyleProp } from '@t/components/common';\nimport type { Rect } from '@t/store';\n\nconst classNames = {\n  popupContainer: cls('popup-container'),\n  detailContainer: cls('detail-container'),\n  topLine: cls('popup-top-line'),\n  border: cls('popup-arrow-border'),\n  fill: cls('popup-arrow-fill'),\n  sectionButton: cls('popup-section', 'section-button'),\n  content: cls('content'),\n  editIcon: cls('icon', 'ic-edit'),\n  deleteIcon: cls('icon', 'ic-delete'),\n  editButton: cls('edit-button'),\n  deleteButton: cls('delete-button'),\n  verticalLine: cls('vertical-line'),\n};\n\nfunction calculatePopupPosition(eventRect: Rect, layoutRect: Rect, popupRect: Rect) {\n  let top = eventRect.top + eventRect.height / 2 - popupRect.height / 2;\n  let left = eventRect.left + eventRect.width;\n\n  if (isTopOutOfLayout(top, layoutRect, popupRect)) {\n    top = layoutRect.top + layoutRect.height - popupRect.height;\n  }\n\n  if (isLeftOutOfLayout(left, layoutRect, popupRect)) {\n    left = eventRect.left - popupRect.width;\n  }\n\n  return [\n    Math.max(top, layoutRect.top) + window.scrollY,\n    Math.max(left, layoutRect.left) + window.scrollX,\n  ];\n}\n\nfunction calculatePopupArrowPosition(eventRect: Rect, layoutRect: Rect, popupRect: Rect) {\n  const top = eventRect.top + eventRect.height / 2 + window.scrollY;\n  const popupLeft = eventRect.left + eventRect.width;\n\n  const isOutOfLayout = popupLeft + popupRect.width > layoutRect.left + layoutRect.width;\n  const direction = isOutOfLayout\n    ? DetailPopupArrowDirection.right\n    : DetailPopupArrowDirection.left;\n\n  return { top, direction };\n}\n\nexport function EventDetailPopup() {\n  const { useFormPopup } = useStore(optionsSelector);\n  const popupParams = useStore(eventDetailPopupParamSelector);\n  const { event, eventRect } = popupParams ?? {};\n\n  const { showFormPopup, hideDetailPopup } = useDispatch('popup');\n\n  const calendarColor = useCalendarColor(event);\n  const layoutContainer = useLayoutContainer();\n  const detailPopupSlot = useFloatingLayer('detailPopupSlot');\n  const eventBus = useEventBus();\n  const popupContainerRef = useRef<HTMLDivElement>(null);\n\n  const [style, setStyle] = useState<StyleProp>({});\n  const [arrowTop, setArrowTop] = useState<number>(0);\n  const [arrowDirection, setArrowDirection] = useState<DetailPopupArrowDirection>(\n    DetailPopupArrowDirection.left\n  );\n\n  const popupArrowClassName = useMemo(() => {\n    const right = arrowDirection === DetailPopupArrowDirection.right;\n    const left = arrowDirection === DetailPopupArrowDirection.left;\n\n    return cls('popup-arrow', { right, left });\n  }, [arrowDirection]);\n\n  useLayoutEffect(() => {\n    if (popupContainerRef.current && eventRect && layoutContainer) {\n      const layoutRect = layoutContainer.getBoundingClientRect();\n      const popupRect = popupContainerRef.current.getBoundingClientRect();\n\n      const [top, left] = calculatePopupPosition(eventRect, layoutRect, popupRect);\n      const { top: arrowTopPosition, direction } = calculatePopupArrowPosition(\n        eventRect,\n        layoutRect,\n        popupRect\n      );\n\n      setStyle({ top, left });\n      setArrowTop(arrowTopPosition - top - HALF_OF_POPUP_ARROW_HEIGHT);\n      setArrowDirection(direction);\n    }\n  }, [eventRect, layoutContainer]);\n\n  if (isNil(event) || isNil(eventRect) || isNil(detailPopupSlot)) {\n    return null;\n  }\n\n  const {\n    title = '',\n    isAllday = false,\n    start = new TZDate(),\n    end = new TZDate(),\n    location,\n    state,\n    isReadOnly,\n    isPrivate,\n  } = event;\n\n  const popupArrowPointPosition = {\n    top: eventRect.top + eventRect.height / 2,\n    left: eventRect.left + eventRect.width / 2,\n  };\n\n  const onClickEditButton = () => {\n    if (useFormPopup) {\n      showFormPopup({\n        isCreationPopup: false,\n        event,\n        title,\n        location,\n        start,\n        end,\n        isAllday,\n        isPrivate,\n        eventState: state,\n        popupArrowPointPosition,\n      });\n    } else {\n      eventBus.fire('beforeUpdateEvent', { event: event.toEventObject(), changes: {} });\n    }\n  };\n\n  const onClickDeleteButton = () => {\n    eventBus.fire('beforeDeleteEvent', event.toEventObject());\n    hideDetailPopup();\n  };\n\n  return createPortal(\n    <div role=\"dialog\" className={classNames.popupContainer} ref={popupContainerRef} style={style}>\n      <div className={classNames.detailContainer}>\n        <EventDetailSectionHeader event={event} />\n        <EventDetailSectionDetail event={event} />\n        {!isReadOnly && (\n          <div className={classNames.sectionButton}>\n            <button type=\"button\" className={classNames.editButton} onClick={onClickEditButton}>\n              <span className={classNames.editIcon} />\n              <span className={classNames.content}>\n                <Template template=\"popupEdit\" as=\"span\" />\n              </span>\n            </button>\n            <div className={classNames.verticalLine} />\n            <button type=\"button\" className={classNames.deleteButton} onClick={onClickDeleteButton}>\n              <span className={classNames.deleteIcon} />\n              <span className={classNames.content}>\n                <Template template=\"popupDelete\" as=\"span\" />\n              </span>\n            </button>\n          </div>\n        )}\n      </div>\n      <div\n        className={classNames.topLine}\n        style={{ backgroundColor: calendarColor.backgroundColor }}\n      />\n      <div className={popupArrowClassName}>\n        <div className={classNames.border} style={{ top: arrowTop }}>\n          <div className={classNames.fill} />\n        </div>\n      </div>\n    </div>,\n    detailPopupSlot\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/popup/eventDetailSectionDetail.tsx",
    "content": "import { h } from 'preact';\n\nimport { Template } from '@src/components/template';\nimport { cls } from '@src/helpers/css';\nimport { useCalendarById } from '@src/hooks/calendar/useCalendarById';\nimport type EventModel from '@src/model/eventModel';\n\ninterface Props {\n  event: EventModel;\n}\n\nconst classNames = {\n  detailItem: cls('detail-item'),\n  detailItemIndent: cls('detail-item', 'detail-item-indent'),\n  detailItemSeparate: cls('detail-item', 'detail-item-separate'),\n  sectionDetail: cls('popup-section', 'section-detail'),\n  content: cls('content'),\n  locationIcon: cls('icon', 'ic-location-b'),\n  repeatIcon: cls('icon', 'ic-repeat-b'),\n  userIcon: cls('icon', 'ic-user-b'),\n  stateIcon: cls('icon', 'ic-state-b'),\n  calendarDotIcon: cls('icon', 'calendar-dot'),\n};\n\n// eslint-disable-next-line complexity\nexport function EventDetailSectionDetail({ event }: Props) {\n  const { location, recurrenceRule, attendees, state, calendarId, body } = event;\n  const calendar = useCalendarById(calendarId);\n\n  return (\n    <div className={classNames.sectionDetail}>\n      {location && (\n        <div className={classNames.detailItem}>\n          <span className={classNames.locationIcon} />\n          <span className={classNames.content}>\n            <Template template=\"popupDetailLocation\" param={event} as=\"span\" />\n          </span>\n        </div>\n      )}\n      {recurrenceRule && (\n        <div className={classNames.detailItem}>\n          <span className={classNames.repeatIcon} />\n          <span className={classNames.content}>\n            <Template template=\"popupDetailRecurrenceRule\" param={event} as=\"span\" />\n          </span>\n        </div>\n      )}\n      {attendees && (\n        <div className={classNames.detailItemIndent}>\n          <span className={classNames.userIcon} />\n          <span className={classNames.content}>\n            <Template template=\"popupDetailAttendees\" param={event} as=\"span\" />\n          </span>\n        </div>\n      )}\n      {state && (\n        <div className={classNames.detailItem}>\n          <span className={classNames.stateIcon} />\n          <span className={classNames.content}>\n            <Template template=\"popupDetailState\" param={event} as=\"span\" />\n          </span>\n        </div>\n      )}\n      {calendar && (\n        <div className={classNames.detailItem}>\n          <span\n            className={classNames.calendarDotIcon}\n            style={{\n              backgroundColor: calendar?.backgroundColor ?? '',\n            }}\n          />\n          <span className={classNames.content}>{calendar?.name ?? ''}</span>\n        </div>\n      )}\n      {body && (\n        <div className={classNames.detailItemSeparate}>\n          <span className={classNames.content}>\n            <Template template=\"popupDetailBody\" param={event} as=\"span\" />\n          </span>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/popup/eventDetailSectionHeader.tsx",
    "content": "import { h } from 'preact';\n\nimport { Template } from '@src/components/template';\nimport { cls } from '@src/helpers/css';\nimport type EventModel from '@src/model/eventModel';\n\ninterface Props {\n  event: EventModel;\n}\n\nconst classNames = {\n  sectionHeader: cls('popup-section', 'section-header'),\n  content: cls('content'),\n  eventTitle: cls('event-title'),\n};\n\nexport function EventDetailSectionHeader({ event }: Props) {\n  return (\n    <div className={classNames.sectionHeader}>\n      <div className={classNames.eventTitle}>\n        <Template template=\"popupDetailTitle\" param={event} as=\"span\" />\n      </div>\n      <div className={classNames.content}>\n        <Template template=\"popupDetailDate\" param={event} as=\"span\" />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/popup/eventFormPopup.spec.tsx",
    "content": "import { h } from 'preact';\nimport { useCallback, useEffect } from 'preact/hooks';\n\nimport { EventFormPopup } from '@src/components/popup/eventFormPopup';\nimport { initCalendarStore, useDispatch } from '@src/contexts/calendarStore';\nimport { useEventBus } from '@src/contexts/eventBus';\nimport { cls } from '@src/helpers/css';\nimport { fireEvent, render, screen, userEvent } from '@src/test/utils';\nimport TZDate from '@src/time/date';\n\nimport type { Options } from '@t/options';\n\nconst selectors = {\n  calendarSection: `.${cls('calendar-section')}`,\n  privateButton: `.${cls('popup-section-private')}.${cls('popup-button')}`,\n  privateIcon: `.${cls('ic-private')}`,\n  hiddenDatePicker: `.${cls('datepicker-container')} .tui-datepicker.tui-hidden`,\n  timePicker: '.tui-datepicker-footer .tui-timepicker',\n};\n\ndescribe('event form popup', () => {\n  const start = new TZDate();\n  const end = new TZDate();\n  const isAllday = false;\n  const isPrivate = false;\n  const state = 'Busy';\n  const mockFn = jest.fn();\n\n  const Component = () => {\n    const eventBus = useEventBus();\n    const { showFormPopup } = useDispatch('popup');\n    const mockHandler = useCallback(mockFn, [mockFn]);\n\n    useEffect(() => {\n      eventBus.on('beforeCreateEvent', mockHandler);\n      showFormPopup({\n        isCreationPopup: true,\n        title: '',\n        location: '',\n        start,\n        end,\n        isAllday,\n        isPrivate,\n        eventState: state,\n        popupArrowPointPosition: {\n          top: 0,\n          left: 0,\n        },\n      });\n    }, [eventBus, mockHandler, showFormPopup]);\n\n    return <EventFormPopup />;\n  };\n\n  const setup = (calendars?: Options['calendars']) => {\n    const store = initCalendarStore({ calendars });\n\n    return render(<Component />, { store });\n  };\n\n  it('should display CalendarSelector when `calendars` is exists', () => {\n    const calendars = [{ id: '1', name: 'calendar name' }];\n\n    setup(calendars);\n\n    const calendarSelectorContent = screen.getByRole('button', { name: calendars[0].name });\n\n    expect(calendarSelectorContent).not.toBeNull();\n  });\n\n  it('should be able to select calendar', () => {\n    const calendars = [\n      {\n        id: '1',\n        name: 'Personal',\n      },\n      {\n        id: '2',\n        name: 'Work',\n      },\n    ];\n\n    setup(calendars);\n\n    let calendarSelectorButton = screen.getByRole('button', { name: calendars[0].name });\n    fireEvent.click(calendarSelectorButton);\n    fireEvent.click(screen.getByText('Work'));\n\n    calendarSelectorButton = screen.getByRole('button', { name: calendars[1].name });\n    expect(calendarSelectorButton).toBeInTheDocument();\n\n    fireEvent.click(calendarSelectorButton);\n    fireEvent.click(screen.getByText('Personal'));\n\n    calendarSelectorButton = screen.getByRole('button', { name: calendars[0].name });\n    expect(calendarSelectorButton).toBeInTheDocument();\n  });\n\n  it('should not display CalendarSelector when `calendars` is not exists', () => {\n    const { container } = setup();\n\n    const calendarSelector = container.querySelector(selectors.calendarSection);\n\n    expect(calendarSelector).toBeNull();\n  });\n\n  it('should be changed private icon when private button is clicked', () => {\n    const { container } = setup();\n\n    const privateButton = container.querySelector(selectors.privateButton) ?? container;\n    let privateIcon = container.querySelector(selectors.privateIcon);\n\n    expect(privateButton).not.toBeNull();\n    expect(privateIcon).toBeNull();\n\n    fireEvent.click(privateButton);\n\n    privateIcon = container.querySelector(selectors.privateIcon);\n\n    expect(privateIcon).not.toBeNull();\n  });\n\n  it('should render range-picker but range-picker is hidden', () => {\n    const { container } = setup();\n\n    const datePicker = container.querySelectorAll(selectors.hiddenDatePicker);\n\n    expect(datePicker).toHaveLength(2);\n  });\n\n  ['Start date', 'End date'].forEach((placeholder) => {\n    it(`should render range picker when ${placeholder} input is clicked`, () => {\n      const { container } = setup();\n\n      fireEvent.click(screen.getByPlaceholderText(placeholder));\n      const datePicker = container.querySelectorAll(selectors.hiddenDatePicker);\n\n      expect(datePicker).toHaveLength(1);\n    });\n  });\n\n  it('should not render time-picker in range-picker when allday button is clicked', () => {\n    const { container } = setup();\n\n    fireEvent.click(screen.getByText('All day'));\n    const timePicker = container.querySelector(selectors.timePicker);\n\n    expect(timePicker).toBeNull();\n  });\n\n  it('should fire `beforeCreateEvent` custom event when save button is clicked', () => {\n    setup();\n\n    fireEvent.click(screen.getByRole('button', { name: /Save/i }));\n\n    expect(mockFn).toBeCalled();\n    expect(mockFn).toHaveBeenCalledWith({\n      start,\n      end,\n      isAllday,\n      isPrivate,\n      state,\n      title: '',\n      location: '',\n    });\n  });\n\n  it('should fire `beforeCreateEvent` custom event with changed values when save button is clicked', () => {\n    const changedTitle = 'changed title';\n    const changedLocation = 'changed location';\n\n    setup();\n\n    fireEvent.change(screen.getByPlaceholderText('Subject'), { target: { value: changedTitle } });\n    fireEvent.change(screen.getByPlaceholderText('Location'), {\n      target: { value: changedLocation },\n    });\n    fireEvent.click(screen.getByRole('button', { name: /Save/i }));\n\n    expect(mockFn).toHaveBeenCalledWith({\n      start,\n      end,\n      isAllday,\n      isPrivate,\n      state,\n      title: changedTitle,\n      location: changedLocation,\n    });\n  });\n\n  // Regression tests for #1233\n  it('should preserve input values when \"All day\" checkbox is toggled', async () => {\n    // Given\n    setup();\n    const user = userEvent.setup();\n    const getTitleInput = (): HTMLInputElement => screen.getByPlaceholderText('Subject');\n    const getLocationInput = (): HTMLInputElement => screen.getByPlaceholderText('Location');\n    const allDayCheckbox = screen.getByText('All day');\n\n    const givenTitle = 'title';\n    const givenLocation = 'location';\n\n    await user.type(getTitleInput(), givenTitle);\n    await user.type(getLocationInput(), givenLocation);\n\n    // When\n    await user.click(allDayCheckbox);\n\n    // Then\n    expect(getTitleInput().value).toBe(givenTitle);\n    expect(getLocationInput().value).toBe(givenLocation);\n\n    // When change input and toggle again\n    const concatStr = ' changed';\n    await user.type(getTitleInput(), concatStr);\n    await user.type(getLocationInput(), concatStr);\n    await user.click(allDayCheckbox);\n\n    // Then\n    expect(getTitleInput().value).toBe(`${givenTitle}${concatStr}`);\n    expect(getLocationInput().value).toBe(`${givenLocation}${concatStr}`);\n  });\n\n  it('should preserve input values when selecting calendar', async () => {\n    // Given\n    const calendars = [\n      {\n        id: '1',\n        name: 'Personal',\n      },\n      {\n        id: '2',\n        name: 'Work',\n      },\n    ];\n    setup(calendars);\n\n    const user = userEvent.setup();\n    const getTitleInput = (): HTMLInputElement => screen.getByPlaceholderText('Subject');\n    const getLocationInput = (): HTMLInputElement => screen.getByPlaceholderText('Location');\n\n    const givenTitle = 'title';\n    const givenLocation = 'location';\n\n    await user.type(getTitleInput(), givenTitle);\n    await user.type(getLocationInput(), givenLocation);\n\n    // When\n    await user.click(screen.getByRole('button', { name: calendars[0].name }));\n    await user.click(screen.getByText(calendars[1].name));\n\n    // Then\n    expect(getTitleInput().value).toBe(givenTitle);\n    expect(getLocationInput().value).toBe(givenLocation);\n\n    // When change input and toggle again\n    const concatStr = ' changed';\n    await user.type(getTitleInput(), concatStr);\n    await user.type(getLocationInput(), concatStr);\n    await user.click(screen.getByRole('button', { name: calendars[1].name }));\n    await user.click(screen.getByText(calendars[0].name));\n\n    // Then\n    expect(getTitleInput().value).toBe(`${givenTitle}${concatStr}`);\n    expect(getLocationInput().value).toBe(`${givenLocation}${concatStr}`);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/components/popup/eventFormPopup.tsx",
    "content": "import { h } from 'preact';\nimport { createPortal } from 'preact/compat';\nimport { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks';\n\nimport type { DateRangePicker } from 'tui-date-picker';\n\nimport { CalendarSelector } from '@src/components/popup/calendarSelector';\nimport { ClosePopupButton } from '@src/components/popup/closePopupButton';\nimport { ConfirmPopupButton } from '@src/components/popup/confirmPopupButton';\nimport { DateSelector } from '@src/components/popup/dateSelector';\nimport { EventStateSelector } from '@src/components/popup/eventStateSelector';\nimport { LocationInputBox } from '@src/components/popup/locationInputBox';\nimport { PopupSection } from '@src/components/popup/popupSection';\nimport { TitleInputBox } from '@src/components/popup/titleInputBox';\nimport { Template } from '@src/components/template';\nimport {\n  BOOLEAN_KEYS_OF_EVENT_MODEL_DATA,\n  FormPopupArrowDirection,\n  HALF_OF_POPUP_ARROW_HEIGHT,\n} from '@src/constants/popup';\nimport { useDispatch, useStore } from '@src/contexts/calendarStore';\nimport { useEventBus } from '@src/contexts/eventBus';\nimport { useFloatingLayer } from '@src/contexts/floatingLayer';\nimport { useLayoutContainer } from '@src/contexts/layoutContainer';\nimport { cls } from '@src/helpers/css';\nimport { isLeftOutOfLayout, isTopOutOfLayout } from '@src/helpers/popup';\nimport { FormStateActionType, useFormState } from '@src/hooks/popup/useFormState';\nimport type EventModel from '@src/model/eventModel';\nimport { calendarSelector } from '@src/selectors';\nimport { eventFormPopupParamSelector } from '@src/selectors/popup';\nimport TZDate from '@src/time/date';\nimport { compare } from '@src/time/datetime';\nimport { isNil, isPresent } from '@src/utils/type';\n\nimport type { FormEvent, StyleProp } from '@t/components/common';\nimport type { BooleanKeyOfEventObject, EventObject } from '@t/events';\nimport type { PopupArrowPointPosition, Rect } from '@t/store';\n\nconst classNames = {\n  popupContainer: cls('popup-container'),\n  formContainer: cls('form-container'),\n  popupArrowBorder: cls('popup-arrow-border'),\n  popupArrowFill: cls('popup-arrow-fill'),\n};\n\nfunction calculatePopupPosition(\n  popupArrowPointPosition: PopupArrowPointPosition,\n  layoutRect: Rect,\n  popupRect: Rect\n) {\n  let top = popupArrowPointPosition.top - popupRect.height - HALF_OF_POPUP_ARROW_HEIGHT;\n  let left = popupArrowPointPosition.left - popupRect.width / 2;\n  let direction = FormPopupArrowDirection.bottom;\n\n  if (top < layoutRect.top) {\n    direction = FormPopupArrowDirection.top;\n    top = popupArrowPointPosition.top + HALF_OF_POPUP_ARROW_HEIGHT;\n  }\n\n  if (isTopOutOfLayout(top, layoutRect, popupRect)) {\n    top = layoutRect.top + layoutRect.height - popupRect.height;\n  }\n\n  if (isLeftOutOfLayout(left, layoutRect, popupRect)) {\n    left = layoutRect.left + layoutRect.width - popupRect.width;\n  }\n\n  return {\n    top: top + window.scrollY,\n    left: Math.max(left, layoutRect.left) + window.scrollX,\n    direction,\n  };\n}\n\nfunction isBooleanKey(key: string): key is BooleanKeyOfEventObject {\n  return BOOLEAN_KEYS_OF_EVENT_MODEL_DATA.indexOf(key as BooleanKeyOfEventObject) !== -1;\n}\n\nfunction getChanges(event: EventModel, eventObject: EventObject) {\n  return Object.entries(eventObject).reduce((changes, [key, value]) => {\n    const eventObjectKey = key as keyof EventObject;\n\n    if (event[eventObjectKey] instanceof TZDate) {\n      // NOTE: handle TZDate\n      if (compare(event[eventObjectKey], value) !== 0) {\n        changes[eventObjectKey] = value;\n      }\n    } else if (event[eventObjectKey] !== value) {\n      changes[eventObjectKey] = value;\n    }\n\n    return changes;\n  }, {} as EventObject);\n}\n\nexport function EventFormPopup() {\n  const { calendars } = useStore(calendarSelector);\n  const { hideAllPopup } = useDispatch('popup');\n  const popupParams = useStore(eventFormPopupParamSelector);\n  const { start, end, popupArrowPointPosition, close, isCreationPopup, event } = popupParams ?? {};\n  const eventBus = useEventBus();\n  const formPopupSlot = useFloatingLayer('formPopupSlot');\n  const [formState, formStateDispatch] = useFormState(calendars[0]?.id);\n\n  const datePickerRef = useRef<DateRangePicker>(null);\n  const popupContainerRef = useRef<HTMLDivElement>(null);\n  const [style, setStyle] = useState<StyleProp>({});\n  const [arrowLeft, setArrowLeft] = useState<number>(0);\n  const [arrowDirection, setArrowDirection] = useState<FormPopupArrowDirection>(\n    FormPopupArrowDirection.bottom\n  );\n\n  const layoutContainer = useLayoutContainer();\n\n  const popupArrowClassName = useMemo(() => {\n    const top = arrowDirection === FormPopupArrowDirection.top;\n    const bottom = arrowDirection === FormPopupArrowDirection.bottom;\n\n    return cls('popup-arrow', { top, bottom });\n  }, [arrowDirection]);\n\n  useLayoutEffect(() => {\n    if (popupContainerRef.current && popupArrowPointPosition && layoutContainer) {\n      const layoutRect = layoutContainer.getBoundingClientRect();\n      const popupRect = popupContainerRef.current.getBoundingClientRect();\n\n      const { top, left, direction } = calculatePopupPosition(\n        popupArrowPointPosition,\n        layoutRect,\n        popupRect\n      );\n      const arrowLeftPosition = popupArrowPointPosition.left - left;\n\n      setStyle({ left, top });\n      setArrowLeft(arrowLeftPosition);\n      setArrowDirection(direction);\n    }\n  }, [layoutContainer, popupArrowPointPosition]);\n\n  // Sync store's popupParams with formState when editing event\n  useEffect(() => {\n    if (isPresent(popupParams) && isPresent(event)) {\n      formStateDispatch({\n        type: FormStateActionType.init,\n        event: {\n          title: popupParams.title,\n          location: popupParams.location,\n          isAllday: popupParams.isAllday,\n          isPrivate: popupParams.isPrivate,\n          calendarId: event.calendarId,\n          state: popupParams.eventState,\n        },\n      });\n    }\n  }, [calendars, event, formStateDispatch, popupParams]);\n\n  // Reset form states when closing the popup\n  useEffect(() => {\n    if (isNil(popupParams)) {\n      formStateDispatch({ type: FormStateActionType.reset });\n    }\n  }, [formStateDispatch, popupParams]);\n\n  if (isNil(start) || isNil(end) || isNil(formPopupSlot)) {\n    return null;\n  }\n\n  const onSubmit = (e: FormEvent) => {\n    e.preventDefault();\n\n    const formData = new FormData(e.target as HTMLFormElement);\n    const eventData: EventObject = { ...formState };\n\n    formData.forEach((data, key) => {\n      eventData[key as keyof EventObject] = isBooleanKey(key) ? data === 'true' : data;\n    });\n\n    eventData.start = new TZDate(datePickerRef.current?.getStartDate());\n    eventData.end = new TZDate(datePickerRef.current?.getEndDate());\n\n    if (isCreationPopup) {\n      eventBus.fire('beforeCreateEvent', eventData);\n    } else if (event) {\n      const changes = getChanges(event, eventData);\n\n      eventBus.fire('beforeUpdateEvent', { event: event.toEventObject(), changes });\n    }\n    hideAllPopup();\n  };\n\n  return createPortal(\n    <div role=\"dialog\" className={classNames.popupContainer} ref={popupContainerRef} style={style}>\n      <form onSubmit={onSubmit}>\n        <div className={classNames.formContainer}>\n          {calendars?.length ? (\n            <CalendarSelector\n              selectedCalendarId={formState.calendarId}\n              calendars={calendars}\n              formStateDispatch={formStateDispatch}\n            />\n          ) : (\n            <PopupSection />\n          )}\n          <TitleInputBox\n            title={formState.title}\n            isPrivate={formState.isPrivate}\n            formStateDispatch={formStateDispatch}\n          />\n          <LocationInputBox location={formState.location} formStateDispatch={formStateDispatch} />\n          <DateSelector\n            start={start}\n            end={end}\n            isAllday={formState.isAllday}\n            formStateDispatch={formStateDispatch}\n            ref={datePickerRef}\n          />\n          <EventStateSelector eventState={formState.state} formStateDispatch={formStateDispatch} />\n          <ClosePopupButton type=\"form\" close={close} />\n          <PopupSection>\n            <ConfirmPopupButton>\n              {isCreationPopup ? (\n                <Template template=\"popupSave\" />\n              ) : (\n                <Template template=\"popupUpdate\" />\n              )}\n            </ConfirmPopupButton>\n          </PopupSection>\n        </div>\n        <div className={popupArrowClassName}>\n          <div className={classNames.popupArrowBorder} style={{ left: arrowLeft }}>\n            <div className={classNames.popupArrowFill} />\n          </div>\n        </div>\n      </form>\n    </div>,\n    formPopupSlot\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/popup/eventStateSelector.tsx",
    "content": "import { h } from 'preact';\n\nimport { PopupSection } from '@src/components/popup/popupSection';\nimport { StateDropdownMenu } from '@src/components/popup/stateDropdownMenu';\nimport { Template } from '@src/components/template';\nimport { cls } from '@src/helpers/css';\nimport { useDropdownState } from '@src/hooks/common/useDropdownState';\nimport type { FormStateDispatcher } from '@src/hooks/popup/useFormState';\nimport { FormStateActionType } from '@src/hooks/popup/useFormState';\n\nimport type { EventState } from '@t/events';\n\ninterface Props {\n  eventState?: EventState;\n  formStateDispatch: FormStateDispatcher;\n}\n\nconst classNames = {\n  popupSection: ['dropdown-section', 'state-section'],\n  popupSectionItem: cls('popup-section-item', 'popup-button'),\n  stateIcon: cls('icon', 'ic-state'),\n  arrowIcon: cls('icon', 'ic-dropdown-arrow'),\n  content: cls('content', 'event-state'),\n};\n\nexport function EventStateSelector({ eventState = 'Busy', formStateDispatch }: Props) {\n  const { isOpened, setOpened, toggleDropdown } = useDropdownState();\n\n  const handleChangeEventState = (state: EventState) =>\n    formStateDispatch({ type: FormStateActionType.setState, state });\n\n  return (\n    <PopupSection onClick={toggleDropdown} classNames={classNames.popupSection}>\n      <button type=\"button\" className={classNames.popupSectionItem}>\n        <span className={classNames.stateIcon} />\n        <span className={classNames.content}>\n          {eventState === 'Busy' ? (\n            <Template template=\"popupStateBusy\" />\n          ) : (\n            <Template template=\"popupStateFree\" />\n          )}\n        </span>\n        <span className={classNames.arrowIcon} />\n      </button>\n      {isOpened && (\n        <StateDropdownMenu setOpened={setOpened} setEventState={handleChangeEventState} />\n      )}\n    </PopupSection>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/popup/locationInputBox.tsx",
    "content": "import { h } from 'preact';\nimport type { ChangeEventHandler } from 'preact/compat';\n\nimport { PopupSection } from '@src/components/popup/popupSection';\nimport { cls } from '@src/helpers/css';\nimport type { FormStateDispatcher } from '@src/hooks/popup/useFormState';\nimport { FormStateActionType } from '@src/hooks/popup/useFormState';\nimport { useStringOnlyTemplate } from '@src/hooks/template/useStringOnlyTemplate';\n\ninterface Props {\n  location?: string;\n  formStateDispatch: FormStateDispatcher;\n}\n\nconst classNames = {\n  popupSectionItem: cls('popup-section-item', 'popup-section-location'),\n  locationIcon: cls('icon', 'ic-location'),\n  content: cls('content'),\n};\n\nexport function LocationInputBox({ location, formStateDispatch }: Props) {\n  const locationPlaceholder = useStringOnlyTemplate({\n    template: 'locationPlaceholder',\n    defaultValue: 'Location',\n  });\n\n  const handleLocationChange: ChangeEventHandler<HTMLInputElement> = (e) => {\n    formStateDispatch({ type: FormStateActionType.setLocation, location: e.currentTarget.value });\n  };\n\n  return (\n    <PopupSection>\n      <div className={classNames.popupSectionItem}>\n        <span className={classNames.locationIcon} />\n        <input\n          name=\"location\"\n          className={classNames.content}\n          placeholder={locationPlaceholder}\n          value={location}\n          onChange={handleLocationChange}\n        />\n      </div>\n    </PopupSection>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/popup/popupOverlay.tsx",
    "content": "import { h } from 'preact';\n\nimport { useDispatch, useStore } from '@src/contexts/calendarStore';\nimport { cls } from '@src/helpers/css';\nimport type { CalendarState } from '@src/types/store';\nimport { isPresent } from '@src/utils/type';\n\nfunction shownPopupParamSelector(state: CalendarState) {\n  return Object.values(state.popup).find((popup) => isPresent(popup));\n}\n\nexport function PopupOverlay() {\n  const shownPopupParam = useStore(shownPopupParamSelector);\n  const { hideAllPopup } = useDispatch('popup');\n\n  const isPopupShown = isPresent(shownPopupParam);\n\n  const onClick = (ev: MouseEvent) => {\n    ev.stopPropagation();\n\n    shownPopupParam?.close?.();\n    hideAllPopup();\n  };\n\n  return (\n    <div\n      className={cls('popup-overlay')}\n      style={{ display: isPopupShown ? 'block' : 'none' }}\n      onClick={onClick}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/popup/popupSection.tsx",
    "content": "import { h } from 'preact';\n\nimport { cls } from '@src/helpers/css';\nimport { noop } from '@src/utils/noop';\n\nimport type { PropsWithChildren } from '@t/components/common';\n\ninterface Props {\n  classNames?: string[];\n  onClick?: () => void;\n}\n\nexport function PopupSection({\n  children,\n  classNames = [],\n  onClick = noop,\n}: PropsWithChildren<Props>) {\n  return (\n    <div className={cls('popup-section', ...classNames)} onClick={onClick}>\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/popup/seeMoreEventsPopup.tsx",
    "content": "import { h } from 'preact';\nimport { createPortal } from 'preact/compat';\nimport { useEffect, useRef } from 'preact/hooks';\n\nimport { HorizontalEvent } from '@src/components/events/horizontalEvent';\nimport { ClosePopupButton } from '@src/components/popup/closePopupButton';\nimport { Template } from '@src/components/template';\nimport {\n  MONTH_EVENT_HEIGHT,\n  MONTH_MORE_VIEW_HEADER_HEIGHT,\n  MONTH_MORE_VIEW_HEADER_MARGIN_BOTTOM,\n  MONTH_MORE_VIEW_HEADER_PADDING,\n  MONTH_MORE_VIEW_HEADER_PADDING_TOP,\n} from '@src/constants/style';\nimport { useStore } from '@src/contexts/calendarStore';\nimport { useEventBus } from '@src/contexts/eventBus';\nimport { useFloatingLayer } from '@src/contexts/floatingLayer';\nimport { useMonthTheme } from '@src/contexts/themeStore';\nimport { cls } from '@src/helpers/css';\nimport { seeMorePopupParamSelector } from '@src/selectors/popup';\nimport { toFormat } from '@src/time/datetime';\nimport { isNil } from '@src/utils/type';\n\nconst classNames = {\n  container: cls('see-more-container'),\n  seeMore: cls('see-more'),\n  header: cls('see-more-header'),\n  list: cls('month-more-list'),\n};\n\nexport function SeeMoreEventsPopup() {\n  const popupParams = useStore(seeMorePopupParamSelector);\n  const { date, events = [], popupPosition } = popupParams ?? {};\n  const { moreView, moreViewTitle } = useMonthTheme();\n  const seeMorePopupSlot = useFloatingLayer('seeMorePopupSlot');\n  const eventBus = useEventBus();\n  const moreEventsPopupContainerRef = useRef(null);\n  const isHidden = isNil(date) || isNil(popupPosition) || isNil(seeMorePopupSlot);\n\n  useEffect(() => {\n    if (!isHidden && moreEventsPopupContainerRef.current) {\n      eventBus.fire('clickMoreEventsBtn', {\n        date: date.toDate(),\n        target: moreEventsPopupContainerRef.current,\n      });\n    }\n  }, [date, eventBus, isHidden]);\n\n  if (isHidden) {\n    return null;\n  }\n\n  const style = {\n    height: MONTH_MORE_VIEW_HEADER_HEIGHT,\n    marginBottom: MONTH_MORE_VIEW_HEADER_MARGIN_BOTTOM,\n    padding: MONTH_MORE_VIEW_HEADER_PADDING,\n    backgroundColor: moreViewTitle.backgroundColor,\n  };\n\n  const moreTitle = {\n    ymd: toFormat(date, 'YYYY-MM-DD'),\n    day: date.getDay(),\n    date: date.getDate().toString().padStart(2, '0'),\n  };\n\n  const moreViewListStyle = {\n    height: `calc(100% - ${\n      MONTH_MORE_VIEW_HEADER_HEIGHT +\n      MONTH_MORE_VIEW_HEADER_MARGIN_BOTTOM +\n      MONTH_MORE_VIEW_HEADER_PADDING_TOP\n    }px)`,\n  };\n\n  return createPortal(\n    <div\n      role=\"dialog\"\n      className={classNames.container}\n      style={popupPosition}\n      ref={moreEventsPopupContainerRef}\n    >\n      <div className={classNames.seeMore} style={moreView}>\n        <div className={classNames.header} style={style}>\n          <Template template=\"monthMoreTitleDate\" param={moreTitle} />\n          <ClosePopupButton type=\"moreEvents\" />\n        </div>\n        <div className={classNames.list} style={moreViewListStyle}>\n          {events.map((uiModel) => (\n            <HorizontalEvent\n              key={`see-more-event-item-${uiModel.cid()}`}\n              uiModel={uiModel}\n              eventHeight={MONTH_EVENT_HEIGHT}\n              headerHeight={MONTH_MORE_VIEW_HEADER_HEIGHT}\n              flat={true}\n            />\n          ))}\n        </div>\n      </div>\n    </div>,\n    seeMorePopupSlot\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/popup/stateDropdownMenu.tsx",
    "content": "import { h } from 'preact';\n\nimport { Template } from '@src/components/template';\nimport { cls } from '@src/helpers/css';\n\nimport type { EventState } from '@t/events';\n\ninterface Props {\n  setOpened: (isOpened: boolean) => void;\n  setEventState: (eventState: EventState) => void;\n}\n\nconst EVENT_STATES: EventState[] = ['Busy', 'Free'];\nconst classNames = {\n  popupSectionItem: cls('popup-section-item', 'dropdown-menu-item'),\n  dropdownMenu: cls('dropdown-menu'),\n  icon: cls('icon'),\n  content: cls('content'),\n};\n\nexport function StateDropdownMenu({ setOpened, setEventState }: Props) {\n  const onClickDropdown = (e: MouseEvent, state: EventState) => {\n    e.stopPropagation();\n    setOpened(false);\n    setEventState(state);\n  };\n\n  return (\n    <ul className={classNames.dropdownMenu}>\n      {EVENT_STATES.map((state) => (\n        <li\n          key={state}\n          className={classNames.popupSectionItem}\n          onClick={(e) => onClickDropdown(e, state)}\n        >\n          <span className={classNames.icon} />\n          <span className={classNames.content}>\n            {state === 'Busy' ? (\n              <Template template=\"popupStateBusy\" />\n            ) : (\n              <Template template=\"popupStateFree\" />\n            )}\n          </span>\n        </li>\n      ))}\n    </ul>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/popup/titleInputBox.tsx",
    "content": "import { h } from 'preact';\nimport type { ChangeEventHandler } from 'preact/compat';\n\nimport { PopupSection } from '@src/components/popup/popupSection';\nimport { cls } from '@src/helpers/css';\nimport type { FormStateDispatcher } from '@src/hooks/popup/useFormState';\nimport { FormStateActionType } from '@src/hooks/popup/useFormState';\nimport { useStringOnlyTemplate } from '@src/hooks/template/useStringOnlyTemplate';\n\ninterface Props {\n  title?: string;\n  isPrivate?: boolean;\n  formStateDispatch: FormStateDispatcher;\n}\n\nconst classNames = {\n  popupSectionItem: cls('popup-section-item', 'popup-section-title'),\n  privateButton: cls('popup-section-item', 'popup-section-private', 'popup-button'),\n  titleIcon: cls('icon', 'ic-title'),\n  content: cls('content'),\n};\n\nexport function TitleInputBox({ title, isPrivate = false, formStateDispatch }: Props) {\n  const titlePlaceholder = useStringOnlyTemplate({\n    template: 'titlePlaceholder',\n    defaultValue: 'Subject',\n  });\n\n  const togglePrivate = () =>\n    formStateDispatch({ type: FormStateActionType.setPrivate, isPrivate: !isPrivate });\n\n  const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => {\n    formStateDispatch({ type: FormStateActionType.setTitle, title: e.currentTarget.value });\n  };\n\n  return (\n    <PopupSection>\n      <div className={classNames.popupSectionItem}>\n        <span className={classNames.titleIcon} />\n        <input\n          name=\"title\"\n          className={classNames.content}\n          placeholder={titlePlaceholder}\n          value={title}\n          onChange={handleInputChange}\n          required\n        />\n      </div>\n      <button type=\"button\" className={classNames.privateButton} onClick={togglePrivate}>\n        <span className={cls('icon', { 'ic-private': isPrivate, 'ic-public': !isPrivate })} />\n        <input\n          name=\"isPrivate\"\n          type=\"checkbox\"\n          className={cls('hidden-input')}\n          value={isPrivate ? 'true' : 'false'}\n          checked={isPrivate}\n        />\n      </button>\n    </PopupSection>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/template.tsx",
    "content": "import { cloneElement, createElement } from 'preact';\n\nimport { useStore } from '@src/contexts/calendarStore';\nimport { cls } from '@src/helpers/css';\nimport { templateSelector } from '@src/selectors';\nimport type { TemplateName } from '@src/template/default';\nimport { sanitize } from '@src/utils/sanitizer';\nimport { isNil, isString } from '@src/utils/type';\n\nimport type { TemplateReturnType } from '@t/template';\n\ninterface Props {\n  template: TemplateName;\n  param?: any;\n  as?: keyof HTMLElementTagNameMap;\n}\n\nexport function Template({ template, param, as: tagName = 'div' }: Props) {\n  const templates = useStore(templateSelector);\n  const templateFunc: Function = templates[template];\n\n  if (isNil(templateFunc)) {\n    return null;\n  }\n\n  const htmlOrVnode: TemplateReturnType = templateFunc(param);\n\n  return isString(htmlOrVnode)\n    ? createElement(tagName, {\n        className: cls(`template-${template}`),\n        dangerouslySetInnerHTML: {\n          __html: sanitize(htmlOrVnode),\n        },\n      })\n    : cloneElement(htmlOrVnode, {\n        className: `${htmlOrVnode.props.className ?? ''} ${cls(`template-${template}`)}`,\n      });\n}\n"
  },
  {
    "path": "apps/calendar/src/components/timeGrid/column.tsx",
    "content": "import { h } from 'preact';\nimport { memo } from 'preact/compat';\nimport { useCallback } from 'preact/hooks';\n\nimport { TimeEvent } from '@src/components/events/timeEvent';\nimport { GridSelectionByColumn } from '@src/components/timeGrid/gridSelectionByColumn';\nimport { useTheme } from '@src/contexts/themeStore';\nimport { cls } from '@src/helpers/css';\nimport { usePrimaryTimezone } from '@src/hooks/timezone/usePrimaryTimezone';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport type TZDate from '@src/time/date';\nimport { isSameDate, isWeekend } from '@src/time/datetime';\n\nimport type { GridPositionFinder, TimeGridData } from '@t/grid';\nimport type { ThemeState } from '@t/theme';\n\nimport { ResizingGuideByColumn } from './resizingGuideByColumn';\n\nconst classNames = {\n  column: cls('column'),\n  backgrounds: cls('background-events'),\n  events: cls('events'),\n};\n\n// TODO: implement BackgroundEvents\n// function BackgroundEvents({\n//   eventUIModels,\n//   startTime,\n//   endTime,\n// }: {\n//   eventUIModels: EventUIModel[];\n//   startTime: TZDate;\n//   endTime: TZDate;\n// }) {\n//   const backgroundEvents = eventUIModels.filter(isBackgroundEvent);\n\n//   return (\n//     <div className={classNames.backgrounds}>\n//       {backgroundEvents.map((eventUIModel, index) => {\n//         const { top, height } = getTopHeightByTime(\n//           eventUIModel.model.start,\n//           eventUIModel.model.end,\n//           startTime,\n//           endTime\n//         );\n\n//         return (\n//           <BackgroundEvent\n//             uiModel={eventUIModel}\n//             top={toPercent(top)}\n//             height={toPercent(height)}\n//             key={`backgroundEvent-${index}`}\n//           />\n//         );\n//       })}\n//     </div>\n//   );\n// }\n\nfunction VerticalEvents({\n  eventUIModels,\n  minEventHeight,\n}: {\n  eventUIModels: EventUIModel[];\n  minEventHeight: number;\n}) {\n  // @TODO: use dynamic value\n  const style = { marginRight: 8 };\n\n  return (\n    <div className={classNames.events} style={style}>\n      {eventUIModels.map((eventUIModel) => (\n        <TimeEvent\n          key={`${eventUIModel.valueOf()}-${eventUIModel.cid()}`}\n          uiModel={eventUIModel}\n          minHeight={minEventHeight}\n        />\n      ))}\n    </div>\n  );\n}\n\nfunction backgroundColorSelector(theme: ThemeState) {\n  return {\n    defaultBackgroundColor: theme.week.dayGrid.backgroundColor,\n    todayBackgroundColor: theme.week.today.backgroundColor,\n    weekendBackgroundColor: theme.week.weekend.backgroundColor,\n  };\n}\n\nfunction getBackgroundColor({\n  today,\n  columnDate,\n  defaultBackgroundColor,\n  todayBackgroundColor,\n  weekendBackgroundColor,\n}: {\n  today: TZDate;\n  columnDate: TZDate;\n  defaultBackgroundColor: string;\n  todayBackgroundColor: string;\n  weekendBackgroundColor: string;\n}) {\n  const isTodayColumn = isSameDate(today, columnDate);\n  const isWeekendColumn = isWeekend(columnDate.getDay());\n\n  if (isTodayColumn) {\n    return todayBackgroundColor;\n  }\n\n  if (isWeekendColumn) {\n    return weekendBackgroundColor;\n  }\n\n  return defaultBackgroundColor;\n}\n\ninterface Props {\n  timeGridData: TimeGridData;\n  columnDate: TZDate;\n  columnWidth: string;\n  columnIndex: number;\n  totalUIModels: EventUIModel[][];\n  gridPositionFinder: GridPositionFinder;\n  isLastColumn: boolean;\n  readOnly?: boolean;\n}\n\nexport const Column = memo(function Column({\n  columnDate,\n  columnWidth,\n  columnIndex,\n  totalUIModels,\n  gridPositionFinder,\n  timeGridData,\n  isLastColumn,\n}: Props) {\n  const { rows: timeGridRows } = timeGridData;\n  const borderRight = useTheme(useCallback((theme) => theme.week.timeGrid.borderRight, []));\n  const backgroundColorTheme = useTheme(backgroundColorSelector);\n  const [, getNow] = usePrimaryTimezone();\n  const today = getNow();\n\n  // const [startTime, endTime] = useMemo(() => {\n  //   const { startTime: startTimeStr } = first(timeGridRows);\n  //   const { endTime: endTimeStr } = last(timeGridRows);\n\n  //   const start = setTimeStrToDate(columnDate, startTimeStr);\n  //   const end = setTimeStrToDate(columnDate, endTimeStr);\n\n  //   return [start, end];\n  // }, [columnDate, timeGridRows]);\n\n  const backgroundColor = getBackgroundColor({ today, columnDate, ...backgroundColorTheme });\n\n  const style = {\n    width: columnWidth,\n    backgroundColor,\n    borderRight: isLastColumn ? 'none' : borderRight,\n  };\n\n  const uiModelsByColumn = totalUIModels[columnIndex];\n\n  const minEventHeight = timeGridRows[0].height;\n\n  return (\n    <div\n      className={classNames.column}\n      style={style}\n      data-testid={`timegrid-column-${columnDate.getDay()}`}\n    >\n      {/* <BackgroundEvents eventUIModels={uiModelsByColumn} startTime={startTime} endTime={endTime} /> */}\n      <VerticalEvents eventUIModels={uiModelsByColumn} minEventHeight={minEventHeight} />\n      <ResizingGuideByColumn\n        gridPositionFinder={gridPositionFinder}\n        totalUIModels={totalUIModels}\n        columnIndex={columnIndex}\n        timeGridData={timeGridData}\n      />\n      <GridSelectionByColumn columnIndex={columnIndex} timeGridRows={timeGridRows} />\n    </div>\n  );\n});\n"
  },
  {
    "path": "apps/calendar/src/components/timeGrid/gridLines.tsx",
    "content": "import { h } from 'preact';\nimport { memo } from 'preact/compat';\n\nimport { useTheme } from '@src/contexts/themeStore';\nimport { cls, toPercent } from '@src/helpers/css';\n\nimport type { TimeGridRow } from '@t/grid';\nimport type { ThemeState } from '@t/theme';\n\nfunction gridLineBorderSelector(theme: ThemeState) {\n  return {\n    halfHourLineBorder: theme.week.timeGridHalfHourLine.borderBottom,\n    hourLineBorder: theme.week.timeGridHourLine.borderBottom,\n  };\n}\n\nexport const GridLines = memo(function GridLines({\n  timeGridRows,\n}: {\n  timeGridRows: TimeGridRow[];\n}) {\n  const { halfHourLineBorder, hourLineBorder } = useTheme(gridLineBorderSelector);\n\n  return (\n    <div className={cls('gridlines')}>\n      {timeGridRows.map((time, index) => {\n        const isUpperLine = index % 2 === 0;\n\n        return (\n          <div\n            key={`gridline-${time.startTime}-${time.endTime}`}\n            className={cls('gridline-half')}\n            style={{\n              top: toPercent(time.top),\n              height: toPercent(time.height),\n              borderBottom: isUpperLine ? halfHourLineBorder : hourLineBorder,\n            }}\n            data-testid={`gridline-${time.startTime}-${time.endTime}`}\n          />\n        );\n      })}\n    </div>\n  );\n});\n"
  },
  {
    "path": "apps/calendar/src/components/timeGrid/gridSelectionByColumn.tsx",
    "content": "import { h } from 'preact';\nimport { useCallback, useMemo } from 'preact/hooks';\n\nimport { useStore } from '@src/contexts/calendarStore';\nimport { useTheme } from '@src/contexts/themeStore';\nimport { cls, toPercent } from '@src/helpers/css';\nimport { timeGridSelectionHelper } from '@src/helpers/gridSelection';\nimport { isNil } from '@src/utils/type';\n\nimport type { TimeGridRow } from '@t/grid';\nimport type { CalendarState } from '@t/store';\n\nfunction GridSelection({ top, height, text }: { top: number; height: number; text: string }) {\n  const { backgroundColor, border } = useTheme(\n    useCallback((theme) => theme.common.gridSelection, [])\n  );\n  const color = useTheme(useCallback((theme) => theme.week.gridSelection.color, []));\n\n  const style = {\n    top: toPercent(top),\n    height: toPercent(height),\n    backgroundColor,\n    border,\n  };\n\n  return (\n    <div\n      className={cls('time', 'grid-selection')}\n      style={style}\n      data-testid={`time-grid-selection-${top}-${height}`}\n    >\n      {text.length > 0 ? (\n        <span className={cls('grid-selection-label')} style={{ color }}>\n          {text}\n        </span>\n      ) : null}\n    </div>\n  );\n}\n\ninterface Props {\n  columnIndex: number;\n  timeGridRows: TimeGridRow[];\n}\n\nexport function GridSelectionByColumn({ columnIndex, timeGridRows }: Props) {\n  const gridSelectionData = useStore(\n    useCallback(\n      (state: CalendarState) =>\n        timeGridSelectionHelper.calculateSelection(\n          state.gridSelection.timeGrid,\n          columnIndex,\n          timeGridRows.length - 1\n        ),\n      [columnIndex, timeGridRows]\n    )\n  );\n\n  const gridSelectionProps = useMemo(() => {\n    if (!gridSelectionData) {\n      return null;\n    }\n\n    const { startRowIndex, endRowIndex, isStartingColumn, isSelectingMultipleColumns } =\n      gridSelectionData;\n\n    const { top: startRowTop, startTime: startRowStartTime } = timeGridRows[startRowIndex];\n    const {\n      top: endRowTop,\n      height: endRowHeight,\n      endTime: endRowEndTime,\n    } = timeGridRows[endRowIndex];\n\n    const gridSelectionHeight = endRowTop + endRowHeight - startRowTop;\n\n    let text = `${startRowStartTime} - ${endRowEndTime}`;\n    if (isSelectingMultipleColumns) {\n      text = isStartingColumn ? startRowStartTime : '';\n    }\n\n    return {\n      top: startRowTop,\n      height: gridSelectionHeight,\n      text,\n    };\n  }, [gridSelectionData, timeGridRows]);\n\n  if (isNil(gridSelectionProps)) {\n    return null;\n  }\n\n  return <GridSelection {...gridSelectionProps} />;\n}\n"
  },
  {
    "path": "apps/calendar/src/components/timeGrid/index.ts",
    "content": "import type { TimeUnit } from '@t/events';\n\nexport const className = 'timegrid';\nexport const addTimeGridPrefix = (selector: string) => `${className}-${selector}`;\n\nexport const timeFormats: Record<TimeUnit, string> = {\n  second: 'HH:mm:ss',\n  minute: 'HH:mm',\n  hour: 'HH:mm',\n  date: 'HH:mm',\n  month: 'MM.DD',\n  year: 'YYYY.MM.DD',\n};\n"
  },
  {
    "path": "apps/calendar/src/components/timeGrid/movingEventShadow.tsx",
    "content": "import { h } from 'preact';\n\nimport { TimeEvent } from '@src/components/events/timeEvent';\nimport { useTimeGridEventMove } from '@src/hooks/timeGrid/useTimeGridEventMove';\nimport { isNil } from '@src/utils/type';\n\nimport type { GridPositionFinder, TimeGridData } from '@t/grid';\n\nexport function MovingEventShadow({\n  gridPositionFinder,\n  timeGridData,\n}: {\n  gridPositionFinder: GridPositionFinder;\n  timeGridData: TimeGridData;\n}) {\n  const { movingEvent, nextStartTime } = useTimeGridEventMove({\n    gridPositionFinder,\n    timeGridData,\n  });\n\n  if (isNil(movingEvent)) {\n    return null;\n  }\n\n  return <TimeEvent uiModel={movingEvent} nextStartTime={nextStartTime} />;\n}\n"
  },
  {
    "path": "apps/calendar/src/components/timeGrid/nowIndicator.tsx",
    "content": "import { h } from 'preact';\nimport { useEffect, useRef } from 'preact/hooks';\n\nimport { addTimeGridPrefix } from '@src/components/timeGrid';\nimport { useEventBus } from '@src/contexts/eventBus';\nimport { useLayoutContainer } from '@src/contexts/layoutContainer';\nimport { useTheme } from '@src/contexts/themeStore';\nimport { cls, toPercent } from '@src/helpers/css';\nimport { TEST_IDS } from '@src/test/testIds';\n\nimport type { ScrollBehaviorOptions } from '@t/eventBus';\nimport type { ThemeState } from '@t/theme';\n\nconst classNames = {\n  line: cls(addTimeGridPrefix('now-indicator')),\n  left: cls(addTimeGridPrefix('now-indicator-left')),\n  marker: cls(addTimeGridPrefix('now-indicator-marker')),\n  today: cls(addTimeGridPrefix('now-indicator-today')),\n  right: cls(addTimeGridPrefix('now-indicator-right')),\n};\n\ninterface Props {\n  top: number;\n  columnWidth: number;\n  columnCount: number;\n  columnIndex: number;\n}\n\nfunction nowIndicatorTheme(theme: ThemeState) {\n  return {\n    pastBorder: theme.week.nowIndicatorPast.border,\n    todayBorder: theme.week.nowIndicatorToday.border,\n    futureBorder: theme.week.nowIndicatorFuture.border,\n    bulletBackgroundColor: theme.week.nowIndicatorBullet.backgroundColor,\n  };\n}\n\nexport function NowIndicator({ top, columnWidth, columnCount, columnIndex }: Props) {\n  const { pastBorder, todayBorder, futureBorder, bulletBackgroundColor } =\n    useTheme(nowIndicatorTheme);\n\n  const layoutContainer = useLayoutContainer();\n  const eventBus = useEventBus();\n  const indicatorRef = useRef<HTMLDivElement | null>(null);\n\n  const leftLine = {\n    left: toPercent(columnWidth * columnIndex),\n    width: toPercent(columnWidth * columnIndex),\n  };\n  const rightLine = {\n    left: toPercent(columnWidth * (columnIndex + 1)),\n    width: toPercent(columnWidth * (columnCount - columnIndex + 1)),\n  };\n\n  useEffect(() => {\n    const scrollToNow = (behavior: ScrollBehaviorOptions) => {\n      const scrollArea = layoutContainer?.querySelector(`.${cls('panel')}.${cls('time')}`) ?? null;\n\n      if (scrollArea && indicatorRef.current) {\n        const { offsetHeight: scrollAreaOffsetHeight } = scrollArea as HTMLDivElement;\n        const { offsetTop: targetOffsetTop } = indicatorRef.current;\n        const newScrollTop = targetOffsetTop - scrollAreaOffsetHeight / 2;\n\n        // NOTE: IE11 doesn't support `scrollTo`\n        if (scrollArea.scrollTo) {\n          scrollArea.scrollTo({ top: newScrollTop, behavior });\n        } else {\n          scrollArea.scrollTop = newScrollTop;\n        }\n      }\n    };\n    eventBus.on('scrollToNow', scrollToNow);\n\n    return () => eventBus.off('scrollToNow', scrollToNow);\n  }, [eventBus, layoutContainer]);\n\n  useEffect(() => {\n    eventBus.fire('scrollToNow', 'smooth');\n  }, [eventBus]);\n\n  return (\n    <div\n      ref={indicatorRef}\n      className={classNames.line}\n      style={{ top: toPercent(top) }}\n      data-testid={TEST_IDS.NOW_INDICATOR}\n    >\n      <div className={classNames.left} style={{ width: leftLine.width, borderTop: pastBorder }} />\n      <div\n        className={classNames.marker}\n        style={{ left: leftLine.left, backgroundColor: bulletBackgroundColor }}\n      />\n      <div\n        className={classNames.today}\n        style={{\n          left: leftLine.left,\n          width: toPercent(columnWidth),\n          borderTop: todayBorder,\n        }}\n      />\n      <div\n        className={classNames.right}\n        style={{\n          left: rightLine.left,\n          borderTop: futureBorder,\n        }}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/timeGrid/nowIndicatorLabel.tsx",
    "content": "import { h } from 'preact';\nimport { useCallback, useMemo } from 'preact/hooks';\n\nimport { Template } from '@src/components/template';\nimport { addTimeGridPrefix, timeFormats } from '@src/components/timeGrid';\nimport { useTheme } from '@src/contexts/themeStore';\nimport { cls, toPercent } from '@src/helpers/css';\nimport { TEST_IDS } from '@src/test/testIds';\nimport type TZDate from '@src/time/date';\nimport { getDateDifference } from '@src/time/datetime';\n\nimport type { TimeUnit } from '@t/events';\n\nconst classNames = {\n  now: addTimeGridPrefix('current-time'),\n  dayDifference: addTimeGridPrefix('day-difference'),\n};\n\ninterface Props {\n  unit: TimeUnit;\n  top: number;\n  now: TZDate;\n  zonedNow: TZDate;\n}\n\nexport function NowIndicatorLabel({ unit, top, now, zonedNow }: Props) {\n  const color = useTheme(useCallback((theme) => theme.week.nowIndicatorLabel.color, []));\n\n  const dateDifference = useMemo(() => {\n    return getDateDifference(zonedNow, now);\n  }, [zonedNow, now]);\n\n  const model = {\n    unit,\n    time: zonedNow,\n    format: timeFormats[unit],\n  };\n\n  return (\n    <div\n      className={cls(classNames.now)}\n      style={{ top: toPercent(top), color }}\n      data-testid={TEST_IDS.NOW_INDICATOR_LABEL}\n    >\n      {dateDifference !== 0 && (\n        <span className={cls(classNames.dayDifference)}>{`[${\n          dateDifference > 0 ? '+' : '-'\n        }${Math.abs(dateDifference)}]`}</span>\n      )}\n      <Template template=\"timegridNowIndicatorLabel\" param={model} as=\"span\" />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/timeGrid/resizingGuideByColumn.tsx",
    "content": "import { h } from 'preact';\n\nimport { TimeEvent } from '@src/components/events/timeEvent';\nimport { useTimeGridEventResize } from '@src/hooks/timeGrid/useTimeGridEventResize';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport { isNil } from '@src/utils/type';\n\nimport type { GridPositionFinder, TimeGridData } from '@t/grid';\n\nexport function ResizingGuideByColumn({\n  gridPositionFinder,\n  totalUIModels,\n  columnIndex,\n  timeGridData,\n}: {\n  gridPositionFinder: GridPositionFinder;\n  totalUIModels: EventUIModel[][];\n  columnIndex: number;\n  timeGridData: TimeGridData;\n}) {\n  const guideUIModel = useTimeGridEventResize({\n    gridPositionFinder,\n    totalUIModels,\n    columnIndex,\n    timeGridData,\n  });\n\n  if (isNil(guideUIModel)) {\n    return null;\n  }\n\n  return <TimeEvent uiModel={guideUIModel} isResizingGuide={true} />;\n}\n"
  },
  {
    "path": "apps/calendar/src/components/timeGrid/timeColumn.tsx",
    "content": "import { h } from 'preact';\nimport { memo } from 'preact/compat';\nimport { useCallback, useMemo } from 'preact/hooks';\n\nimport { Template } from '@src/components/template';\nimport { addTimeGridPrefix } from '@src/components/timeGrid';\nimport { NowIndicatorLabel } from '@src/components/timeGrid/nowIndicatorLabel';\nimport { useStore } from '@src/contexts/calendarStore';\nimport { useTheme } from '@src/contexts/themeStore';\nimport { cls, toPercent } from '@src/helpers/css';\nimport { useTZConverter } from '@src/hooks/timezone/useTZConverter';\nimport {\n  showNowIndicatorOptionSelector,\n  timezonesCollapsedOptionSelector,\n} from '@src/selectors/options';\nimport { weekTimeGridLeftSelector } from '@src/selectors/theme';\nimport { timezonesSelector } from '@src/selectors/timezone';\nimport TZDate from '@src/time/date';\nimport { addMinutes, setTimeStrToDate } from '@src/time/datetime';\nimport { isNil, isPresent } from '@src/utils/type';\n\nimport type { TimeGridRow } from '@t/grid';\nimport type { ThemeState } from '@t/theme';\n\nconst classNames = {\n  timeColumn: addTimeGridPrefix('time-column'),\n  hourRows: addTimeGridPrefix('hour-rows'),\n  time: addTimeGridPrefix('time'),\n  timeLabel: addTimeGridPrefix('time-label'),\n  first: addTimeGridPrefix('time-first'),\n  last: addTimeGridPrefix('time-last'),\n  hidden: addTimeGridPrefix('time-hidden'),\n};\n\ninterface HourRowsProps {\n  rowsInfo: {\n    date: TZDate;\n    top: number;\n    className: string;\n    diffFromPrimaryTimezone?: number;\n  }[];\n  isPrimary: boolean;\n  borderRight?: string;\n  width: number;\n  nowIndicatorState: {\n    top: number;\n    now: TZDate;\n  } | null;\n}\n\nfunction backgroundColorSelector(theme: ThemeState) {\n  return {\n    primaryTimezoneBackgroundColor: theme.week.timeGridLeft.backgroundColor,\n    subTimezoneBackgroundColor: theme.week.timeGridLeftAdditionalTimezone.backgroundColor,\n  };\n}\n\nfunction timeColorSelector(theme: ThemeState) {\n  return {\n    pastTimeColor: theme.week.pastTime.color,\n    futureTimeColor: theme.week.futureTime.color,\n  };\n}\n\nfunction HourRows({ rowsInfo, isPrimary, borderRight, width, nowIndicatorState }: HourRowsProps) {\n  const showNowIndicator = useStore(showNowIndicatorOptionSelector);\n  const { primaryTimezoneBackgroundColor, subTimezoneBackgroundColor } =\n    useTheme(backgroundColorSelector);\n  const { pastTimeColor, futureTimeColor } = useTheme(timeColorSelector);\n  const zonedNow = isPresent(nowIndicatorState)\n    ? addMinutes(nowIndicatorState.now, rowsInfo[0].diffFromPrimaryTimezone ?? 0)\n    : null;\n\n  const backgroundColor = isPrimary ? primaryTimezoneBackgroundColor : subTimezoneBackgroundColor;\n\n  return (\n    <div\n      role=\"rowgroup\"\n      className={cls(classNames.hourRows)}\n      style={{ width: toPercent(width), borderRight, backgroundColor }}\n    >\n      {rowsInfo.map(({ date, top, className }) => {\n        const isPast = isPresent(zonedNow) && date < zonedNow;\n        const color = isPast ? pastTimeColor : futureTimeColor;\n\n        return (\n          <div\n            key={date.getTime()}\n            className={className}\n            style={{\n              top: toPercent(top),\n              color,\n            }}\n            role=\"row\"\n          >\n            <Template\n              template={`timegridDisplay${isPrimary ? 'Primary' : ''}Time`}\n              param={{ time: date }}\n              as=\"span\"\n            />\n          </div>\n        );\n      })}\n      {showNowIndicator && isPresent(nowIndicatorState) && isPresent(zonedNow) && (\n        <NowIndicatorLabel\n          unit=\"hour\"\n          top={nowIndicatorState.top}\n          now={nowIndicatorState.now}\n          zonedNow={zonedNow}\n        />\n      )}\n    </div>\n  );\n}\n\ninterface Props {\n  timeGridRows: TimeGridRow[];\n  nowIndicatorState: { top: number; now: TZDate } | null;\n}\n\nexport const TimeColumn = memo(function TimeColumn({ timeGridRows, nowIndicatorState }: Props) {\n  const showNowIndicator = useStore(showNowIndicatorOptionSelector);\n  const timezones = useStore(timezonesSelector);\n  const timezonesCollapsed = useStore(timezonesCollapsedOptionSelector);\n\n  const tzConverter = useTZConverter();\n  const { width, borderRight } = useTheme(weekTimeGridLeftSelector);\n\n  const rowsByHour = useMemo(\n    () => timeGridRows.filter((_, index) => index % 2 === 0 || index === timeGridRows.length - 1),\n    [timeGridRows]\n  );\n  const hourRowsPropsMapper = useCallback(\n    (row: TimeGridRow, index: number, diffFromPrimaryTimezone?: number) => {\n      const shouldHideRow = ({ top: rowTop, height: rowHeight }: TimeGridRow) => {\n        if (!showNowIndicator || isNil(nowIndicatorState)) {\n          return false;\n        }\n\n        const indicatorTop = nowIndicatorState.top;\n\n        return rowTop - rowHeight <= indicatorTop && indicatorTop <= rowTop + rowHeight;\n      };\n\n      const isFirst = index === 0;\n      const isLast = index === rowsByHour.length - 1;\n      const className = cls(classNames.time, {\n        [classNames.first]: isFirst,\n        [classNames.last]: isLast,\n        [classNames.hidden]: shouldHideRow(row),\n      });\n      let date = setTimeStrToDate(new TZDate(), isLast ? row.endTime : row.startTime);\n      if (isPresent(diffFromPrimaryTimezone)) {\n        date = addMinutes(date, diffFromPrimaryTimezone);\n      }\n\n      return {\n        date,\n        top: row.top,\n        className,\n        diffFromPrimaryTimezone,\n      };\n    },\n    [rowsByHour, nowIndicatorState, showNowIndicator]\n  );\n\n  const [primaryTimezone, ...otherTimezones] = timezones;\n  const hourRowsWidth = otherTimezones.length > 0 ? 100 / (otherTimezones.length + 1) : 100;\n  const primaryTimezoneHourRowsProps = rowsByHour.map((row, index) =>\n    hourRowsPropsMapper(row, index)\n  );\n  const otherTimezoneHourRowsProps = useMemo(() => {\n    if (otherTimezones.length === 0) {\n      return [];\n    }\n\n    return otherTimezones.reverse().map((timezone) => {\n      const { timezoneName } = timezone;\n      const primaryTimezoneOffset = tzConverter(primaryTimezone.timezoneName).getTimezoneOffset();\n      const currentTimezoneOffset = tzConverter(timezoneName).getTimezoneOffset();\n      const diffFromPrimaryTimezone = currentTimezoneOffset - primaryTimezoneOffset;\n\n      return rowsByHour.map((row, index) =>\n        hourRowsPropsMapper(row, index, diffFromPrimaryTimezone)\n      );\n    });\n  }, [hourRowsPropsMapper, otherTimezones, primaryTimezone, rowsByHour, tzConverter]);\n\n  return (\n    <div\n      className={cls(classNames.timeColumn)}\n      style={{ width }}\n      data-testid=\"timegrid-time-column\"\n    >\n      {!timezonesCollapsed &&\n        otherTimezoneHourRowsProps.map((rowsInfo) => (\n          <HourRows\n            key={rowsInfo[0].diffFromPrimaryTimezone}\n            rowsInfo={rowsInfo}\n            isPrimary={false}\n            borderRight={borderRight}\n            width={hourRowsWidth}\n            nowIndicatorState={nowIndicatorState}\n          />\n        ))}\n      <HourRows\n        rowsInfo={primaryTimezoneHourRowsProps}\n        isPrimary={true}\n        borderRight={borderRight}\n        width={timezonesCollapsed ? 100 : hourRowsWidth}\n        nowIndicatorState={nowIndicatorState}\n      />\n    </div>\n  );\n});\n"
  },
  {
    "path": "apps/calendar/src/components/timeGrid/timeGrid.spec.tsx",
    "content": "import { h } from 'preact';\n\nimport { TimeGrid } from '@src/components/timeGrid/timeGrid';\nimport { toPercent } from '@src/helpers/css';\nimport { createTimeGridData, getWeekDates } from '@src/helpers/grid';\nimport { createDate } from '@src/test/helpers';\nimport { TEST_IDS } from '@src/test/testIds';\nimport { act, cleanup, render, screen, within } from '@src/test/utils';\nimport { MS_PER_HOUR, subtractDate } from '@src/time/datetime';\n\ndescribe('Showing Current Time Indicator', () => {\n  const originalTimezone = process.env.TZ;\n  const baseDate = createDate(2022, 3, 22); // Tuesday\n  baseDate.setHours(12); // 12:00\n  const timeColumnTestId = 'timegrid-time-column';\n\n  beforeEach(() => {\n    process.env.TZ = 'UTC';\n    jest.useFakeTimers().setSystemTime(baseDate.getTime());\n  });\n\n  afterEach(() => {\n    cleanup();\n    jest.useRealTimers();\n    process.env.TZ = originalTimezone;\n  });\n\n  it('should render indicator component when rendering TimeGrid including current date', () => {\n    // Given\n    const weekDatesIncludingBaseDate = getWeekDates(baseDate, {\n      startDayOfWeek: 0,\n      workweek: false,\n    });\n    const timeGridData = createTimeGridData(weekDatesIncludingBaseDate, {\n      hourStart: 0,\n      hourEnd: 24,\n    });\n\n    // When\n    render(<TimeGrid timeGridData={timeGridData} events={[]} />);\n\n    // Then\n    const indicator = screen.getByTestId(TEST_IDS.NOW_INDICATOR);\n    expect(indicator).toBeInTheDocument();\n  });\n\n  it('should not render indicator component when rendering TimeGrid not including current date', () => {\n    // Given\n    const weekDatesBeforeBaseDate = getWeekDates(subtractDate(baseDate, 7), {\n      startDayOfWeek: 0,\n      workweek: false,\n    });\n    const timeGridData = createTimeGridData(weekDatesBeforeBaseDate, { hourStart: 0, hourEnd: 24 });\n\n    // When\n    render(<TimeGrid timeGridData={timeGridData} events={[]} />);\n\n    // Then\n    const indicator = screen.queryByTestId(TEST_IDS.NOW_INDICATOR);\n    expect(indicator).not.toBeInTheDocument();\n  });\n\n  it('should set initial position when the TimeGrid component is mounted', () => {\n    // Given\n    const weekDatesIncludingBaseDate = getWeekDates(baseDate, {\n      startDayOfWeek: 0,\n      workweek: false,\n    });\n    const timeGridData = createTimeGridData(weekDatesIncludingBaseDate, {\n      hourStart: 0,\n      hourEnd: 24,\n    });\n\n    // When\n    render(<TimeGrid timeGridData={timeGridData} events={[]} />);\n\n    // Then\n    const indicator = screen.getByTestId(TEST_IDS.NOW_INDICATOR);\n    expect(indicator).toHaveStyle({ top: '50%' });\n  });\n\n  it('should overlap(hide) display time on the same position in the time column', () => {\n    // Given\n    const weekDatesIncludingBaseDate = getWeekDates(baseDate, {\n      startDayOfWeek: 0,\n      workweek: false,\n    });\n    const timeGridData = createTimeGridData(weekDatesIncludingBaseDate, {\n      hourStart: 0,\n      hourEnd: 24,\n    });\n\n    // When\n    render(<TimeGrid timeGridData={timeGridData} events={[]} />);\n\n    // Then\n    const timeColumn = screen.getByTestId(timeColumnTestId);\n    const targetTimeColumnText = within(timeColumn).getByText('12 pm');\n\n    expect(\n      Array.from(targetTimeColumnText?.parentElement?.classList ?? []).some((className) =>\n        className.includes('hidden')\n      )\n    ).toBe(true);\n  });\n\n  it('should move position lower when the time passed', () => {\n    // Given\n    const weekDatesIncludingBaseDate = getWeekDates(baseDate, {\n      startDayOfWeek: 0,\n      workweek: false,\n    });\n    const timeGridData = createTimeGridData(weekDatesIncludingBaseDate, {\n      hourStart: 0,\n      hourEnd: 24,\n    });\n    const oneHourHeightPercent = timeGridData.rows[0].height * 2;\n    const initialTop = 50;\n    render(<TimeGrid timeGridData={timeGridData} events={[]} />);\n\n    const indicator = screen.getByTestId(TEST_IDS.NOW_INDICATOR);\n    expect(indicator).toHaveStyle({ top: toPercent(initialTop) });\n\n    // When\n    act(() => {\n      jest.advanceTimersByTime(MS_PER_HOUR);\n    });\n\n    // Then\n    expect(indicator).toHaveStyle({ top: toPercent(initialTop + oneHourHeightPercent) });\n\n    // move lower\n    // When\n    act(() => {\n      jest.advanceTimersByTime(MS_PER_HOUR);\n    });\n\n    // Then\n    expect(indicator).toHaveStyle({ top: toPercent(initialTop + oneHourHeightPercent * 2) });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/components/timeGrid/timeGrid.tsx",
    "content": "import { h } from 'preact';\nimport { useCallback, useLayoutEffect, useMemo, useState } from 'preact/hooks';\n\nimport { addTimeGridPrefix, className as timegridClassName } from '@src/components/timeGrid';\nimport { Column } from '@src/components/timeGrid/column';\nimport { GridLines } from '@src/components/timeGrid/gridLines';\nimport { MovingEventShadow } from '@src/components/timeGrid/movingEventShadow';\nimport { NowIndicator } from '@src/components/timeGrid/nowIndicator';\nimport { TimeColumn } from '@src/components/timeGrid/timeColumn';\nimport { useStore } from '@src/contexts/calendarStore';\nimport { useTheme } from '@src/contexts/themeStore';\nimport { isBetween, setRenderInfoOfUIModels } from '@src/controller/column';\nimport { getTopPercentByTime } from '@src/controller/times';\nimport { cls, toPercent } from '@src/helpers/css';\nimport { createGridPositionFinder } from '@src/helpers/grid';\nimport { timeGridSelectionHelper } from '@src/helpers/gridSelection';\nimport { useDOMNode } from '@src/hooks/common/useDOMNode';\nimport { useInterval } from '@src/hooks/common/useInterval';\nimport { useIsMounted } from '@src/hooks/common/useIsMounted';\nimport { useGridSelection } from '@src/hooks/gridSelection/useGridSelection';\nimport { usePrimaryTimezone } from '@src/hooks/timezone/usePrimaryTimezone';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport { optionsSelector } from '@src/selectors';\nimport { showNowIndicatorOptionSelector } from '@src/selectors/options';\nimport { weekTimeGridLeftSelector } from '@src/selectors/theme';\nimport type TZDate from '@src/time/date';\nimport {\n  isSameDate,\n  MS_PER_MINUTES,\n  setTimeStrToDate,\n  toEndOfDay,\n  toStartOfDay,\n} from '@src/time/datetime';\nimport type { CollapseDuplicateEventsOptions } from '@src/types/options';\nimport { first, last } from '@src/utils/array';\nimport { passConditionalProp } from '@src/utils/preact';\nimport { isPresent } from '@src/utils/type';\n\nimport type { TimeGridData } from '@t/grid';\n\nconst classNames = {\n  timegrid: cls(timegridClassName),\n  scrollArea: cls(addTimeGridPrefix('scroll-area')),\n};\n\ninterface Props {\n  events: EventUIModel[];\n  timeGridData: TimeGridData;\n}\n\nexport function TimeGrid({ timeGridData, events }: Props) {\n  const {\n    isReadOnly,\n    week: { narrowWeekend, startDayOfWeek, collapseDuplicateEvents },\n  } = useStore(optionsSelector);\n  const showNowIndicator = useStore(showNowIndicatorOptionSelector);\n  const selectedDuplicateEventCid = useStore(\n    (state) => state.weekViewLayout.selectedDuplicateEventCid\n  );\n  const [, getNow] = usePrimaryTimezone();\n\n  const isMounted = useIsMounted();\n  const { width: timeGridLeftWidth } = useTheme(weekTimeGridLeftSelector);\n\n  const [nowIndicatorState, setNowIndicatorState] = useState<{\n    top: number;\n    now: TZDate;\n  } | null>(null);\n\n  const { columns, rows } = timeGridData;\n  const lastColumnIndex = columns.length - 1;\n\n  const totalUIModels = useMemo(\n    () =>\n      columns\n        .map(({ date }) =>\n          events\n            .filter(isBetween(toStartOfDay(date), toEndOfDay(date)))\n            // NOTE: prevent shared reference between columns\n            .map((uiModel) => uiModel.clone())\n        )\n        .map((uiModelsByColumn, columnIndex) =>\n          setRenderInfoOfUIModels(\n            uiModelsByColumn,\n            setTimeStrToDate(columns[columnIndex].date, first(rows).startTime),\n            setTimeStrToDate(columns[columnIndex].date, last(rows).endTime),\n            selectedDuplicateEventCid,\n            collapseDuplicateEvents as CollapseDuplicateEventsOptions\n          )\n        ),\n    [columns, rows, events, selectedDuplicateEventCid, collapseDuplicateEvents]\n  );\n\n  const currentDateData = useMemo(() => {\n    const now = getNow();\n    const currentDateIndexInColumns = columns.findIndex((column) => isSameDate(column.date, now));\n    if (currentDateIndexInColumns < 0) {\n      return null;\n    }\n    const startTime = setTimeStrToDate(\n      columns[currentDateIndexInColumns].date,\n      timeGridData.rows[0].startTime\n    );\n    const endTime = setTimeStrToDate(\n      columns[currentDateIndexInColumns].date,\n      last(timeGridData.rows).endTime\n    );\n\n    return {\n      startTime,\n      endTime,\n      currentDateIndex: currentDateIndexInColumns,\n    };\n  }, [columns, getNow, timeGridData.rows]);\n\n  const [columnsContainer, setColumnsContainer] = useDOMNode();\n  const gridPositionFinder = useMemo(\n    () =>\n      createGridPositionFinder({\n        rowsCount: rows.length,\n        columnsCount: columns.length,\n        container: columnsContainer,\n        narrowWeekend,\n        startDayOfWeek,\n      }),\n    [columns.length, columnsContainer, narrowWeekend, rows.length, startDayOfWeek]\n  );\n\n  const onMouseDown = useGridSelection({\n    type: 'timeGrid',\n    gridPositionFinder,\n    selectionSorter: timeGridSelectionHelper.sortSelection,\n    dateGetter: timeGridSelectionHelper.getDateFromCollection,\n    dateCollection: timeGridData,\n  });\n\n  const updateTimeGridIndicator = useCallback(() => {\n    if (isPresent(currentDateData)) {\n      const { startTime, endTime } = currentDateData;\n      const now = getNow();\n\n      if (startTime <= now && now <= endTime) {\n        setNowIndicatorState({\n          top: getTopPercentByTime(now, startTime, endTime),\n          now,\n        });\n      }\n    }\n  }, [currentDateData, getNow]);\n\n  // Calculate initial setTimeIndicatorTop\n  useLayoutEffect(() => {\n    if (isMounted()) {\n      if ((currentDateData?.currentDateIndex ?? -1) >= 0) {\n        updateTimeGridIndicator();\n      } else {\n        setNowIndicatorState(null);\n      }\n    }\n  }, [currentDateData, isMounted, updateTimeGridIndicator]);\n\n  // Set interval to update timeIndicatorTop\n  useInterval(updateTimeGridIndicator, isPresent(currentDateData) ? MS_PER_MINUTES : null);\n\n  return (\n    <div className={classNames.timegrid}>\n      <div className={classNames.scrollArea}>\n        <TimeColumn timeGridRows={rows} nowIndicatorState={nowIndicatorState} />\n        <div\n          className={cls('columns')}\n          style={{ left: timeGridLeftWidth }}\n          ref={setColumnsContainer}\n          onMouseDown={passConditionalProp(!isReadOnly, onMouseDown)}\n        >\n          <GridLines timeGridRows={rows} />\n          <MovingEventShadow gridPositionFinder={gridPositionFinder} timeGridData={timeGridData} />\n          {columns.map((column, index) => (\n            <Column\n              key={column.date.toString()}\n              timeGridData={timeGridData}\n              columnDate={column.date}\n              columnWidth={toPercent(column.width)}\n              columnIndex={index}\n              totalUIModels={totalUIModels}\n              gridPositionFinder={gridPositionFinder}\n              isLastColumn={index === lastColumnIndex}\n            />\n          ))}\n          {showNowIndicator && isPresent(currentDateData) && isPresent(nowIndicatorState) ? (\n            <NowIndicator\n              top={nowIndicatorState.top}\n              columnWidth={columns[0].width}\n              columnCount={columns.length}\n              columnIndex={currentDateData.currentDateIndex}\n            />\n          ) : null}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/timeGrid/timezoneCollapseButton.tsx",
    "content": "import { h } from 'preact';\n\nimport { addTimeGridPrefix } from '@src/components/timeGrid';\nimport { useEventBus } from '@src/contexts/eventBus';\nimport { cls } from '@src/helpers/css';\n\nexport function TimezoneCollapseButton({ isCollapsed }: { isCollapsed: boolean }) {\n  const eventBus = useEventBus();\n\n  const iconClassName = cls('icon', {\n    'ic-arrow-right': isCollapsed,\n    'ic-arrow-left': !isCollapsed,\n  });\n\n  return (\n    <button\n      className={cls(addTimeGridPrefix('timezone-collapse-button'))}\n      aria-expanded={!isCollapsed}\n      onClick={() => eventBus.fire('clickTimezonesCollapseBtn', isCollapsed)}\n    >\n      <span className={iconClassName} role=\"img\" />\n    </button>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/timeGrid/timezoneCollpaseButton.spec.tsx",
    "content": "import { h } from 'preact';\n\nimport { TimezoneCollapseButton } from '@src/components/timeGrid/timezoneCollapseButton';\nimport { fireEvent, render } from '@src/test/utils';\nimport { EventBusImpl } from '@src/utils/eventBus';\n\nimport type { ExternalEventTypes } from '@t/eventBus';\n\ndescribe(`Firing 'clickTimezonesCollapseBtn' event`, () => {\n  it(`should fire 'clickTimezonesCollapseBtn when clicked`, () => {\n    // Given\n    const eventBus = new EventBusImpl<ExternalEventTypes>();\n    const handler = jest.fn();\n    eventBus.on('clickTimezonesCollapseBtn', handler);\n\n    // When (is collapsed)\n    const { container, rerender } = render(<TimezoneCollapseButton isCollapsed={false} />, {\n      eventBus,\n    });\n    const button = container.firstChild as HTMLButtonElement;\n    fireEvent.click(button);\n\n    // Then\n    expect(handler).toBeCalledWith(false);\n\n    // When (is not collapsed)\n    rerender(<TimezoneCollapseButton isCollapsed={true} />);\n    fireEvent.click(button);\n\n    // Then\n    expect(handler).toBeCalledWith(true);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/components/timeGrid/timezoneLabels.tsx",
    "content": "import { h } from 'preact';\nimport { useMemo } from 'preact/hooks';\n\nimport { Template } from '@src/components/template';\nimport { addTimeGridPrefix } from '@src/components/timeGrid';\nimport { TimezoneCollapseButton } from '@src/components/timeGrid/timezoneCollapseButton';\nimport { useStore } from '@src/contexts/calendarStore';\nimport { useTheme } from '@src/contexts/themeStore';\nimport { cls, toPercent } from '@src/helpers/css';\nimport { useTZConverter } from '@src/hooks/timezone/useTZConverter';\nimport {\n  showTimezoneCollapseButtonOptionSelector,\n  timezonesCollapsedOptionSelector,\n} from '@src/selectors/options';\nimport { weekTimeGridLeftSelector } from '@src/selectors/theme';\nimport { timezonesSelector } from '@src/selectors/timezone';\nimport { isUndefined } from '@src/utils/type';\n\ninterface TimezoneLabelProps {\n  label: string | null;\n  offset: number | null;\n  tooltip: string;\n  width: number;\n  left: number;\n}\n\nfunction TimezoneLabel({ label, offset, tooltip, width = 100, left }: TimezoneLabelProps) {\n  return (\n    <div\n      title={tooltip}\n      className={cls(addTimeGridPrefix('timezone-label'))}\n      style={{\n        width: toPercent(width),\n        height: toPercent(100),\n        left: toPercent(left),\n      }}\n      role=\"gridcell\"\n    >\n      <Template\n        template=\"timezoneDisplayLabel\"\n        param={{ displayLabel: label, timezoneOffset: offset }}\n        as=\"span\"\n      />\n    </div>\n  );\n}\n\nfunction useTimezoneCollapseOptions() {\n  const showTimezoneCollapseButton = useStore(showTimezoneCollapseButtonOptionSelector);\n  const timezonesCollapsed = useStore(timezonesCollapsedOptionSelector);\n\n  return useMemo(() => {\n    return {\n      showTimezoneCollapseButton,\n      timezonesCollapsed,\n    };\n  }, [showTimezoneCollapseButton, timezonesCollapsed]);\n}\n\nexport function TimezoneLabels({ top }: { top: number | null }) {\n  const timezones = useStore(timezonesSelector);\n  const { width } = useTheme(weekTimeGridLeftSelector);\n\n  const tzConverter = useTZConverter();\n  const { showTimezoneCollapseButton, timezonesCollapsed } = useTimezoneCollapseOptions();\n\n  if (timezones.length <= 1) {\n    return null;\n  }\n\n  const timezoneLabelProps = timezones.map(({ displayLabel, timezoneName, tooltip }) => {\n    return !isUndefined(displayLabel)\n      ? { label: displayLabel, offset: null, tooltip: tooltip ?? timezoneName }\n      : {\n          label: null,\n          offset: tzConverter(timezoneName).getTimezoneOffset(),\n          tooltip: tooltip ?? timezoneName,\n        };\n  });\n\n  const [primaryTimezone, ...restTimezones] = timezoneLabelProps;\n  const subTimezones = restTimezones.reverse();\n\n  const timezonesCount = timezonesCollapsed ? 1 : timezones.length;\n  const timezoneLabelWidth = 100 / timezonesCount;\n\n  return (\n    <div\n      style={{\n        top,\n        width,\n      }}\n      role=\"columnheader\"\n      className={cls('timezone-labels-slot')}\n    >\n      {!timezonesCollapsed &&\n        subTimezones.map((subTimezone, index) => (\n          <TimezoneLabel\n            key={`subTimezone-${subTimezone.label ?? subTimezone.offset}`}\n            width={timezoneLabelWidth}\n            left={timezoneLabelWidth * index}\n            {...subTimezone}\n          />\n        ))}\n      {showTimezoneCollapseButton && <TimezoneCollapseButton isCollapsed={timezonesCollapsed} />}\n      <TimezoneLabel\n        width={timezoneLabelWidth}\n        left={timezoneLabelWidth * subTimezones.length}\n        {...primaryTimezone}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/view/day.spec.tsx",
    "content": "import { h } from 'preact';\n\nimport { Day } from '@src/components/view/day';\nimport { DEFAULT_EVENT_PANEL, DEFAULT_TASK_PANEL } from '@src/constants/view';\nimport { initCalendarStore } from '@src/contexts/calendarStore';\nimport { cls } from '@src/helpers/css';\nimport { getActivePanels } from '@src/helpers/view';\nimport EventModel from '@src/model/eventModel';\nimport { render, screen } from '@src/test/utils';\nimport TZDate from '@src/time/date';\n\nimport type { EventObject } from '@t/events';\nimport type { Options, WeekOptions } from '@t/options';\n\ndescribe('day', () => {\n  function setup(options: Options, events?: EventModel[]) {\n    const store = initCalendarStore(options);\n    if (events) {\n      store.getState().dispatch.calendar.createEvents(events);\n    }\n\n    return render(<Day />, { store });\n  }\n\n  describe('eventView and taskView options', () => {\n    const cases: WeekOptions[] = [\n      { eventView: true, taskView: true },\n      { eventView: false, taskView: false },\n      { eventView: ['allday'], taskView: ['milestone'] },\n      { eventView: ['allday', 'time'], taskView: false },\n    ];\n\n    const panels = [...DEFAULT_TASK_PANEL, ...DEFAULT_EVENT_PANEL];\n\n    it.each(cases)(\n      'should show/hide the panels in the daily view: { eventView: $eventView, taskView: $eventView }',\n      (weekOptions) => {\n        // Given\n        const { container } = setup({ week: weekOptions });\n\n        // When\n        // Nothing\n\n        // Then\n        const activePanels = getActivePanels(\n          weekOptions.taskView ?? false,\n          weekOptions.eventView ?? false\n        );\n        panels.forEach((panel) => {\n          const panelEl = container.querySelector(`.${cls(panel)}`);\n          if (activePanels.includes(panel)) {\n            expect(panelEl).not.toBeNull();\n          } else {\n            expect(panelEl).toBeNull();\n          }\n        });\n      }\n    );\n  });\n\n  describe('eventFilter option', () => {\n    const events: EventModel[] = [];\n    const baseDateStr = '2022-05-22T00:00:00+09:00';\n    for (let i = 0; i < 2; i += 1) {\n      events.push(\n        new EventModel({\n          id: `${i}`,\n          title: `Event ${i}`,\n          category: 'time',\n          start: new TZDate(baseDateStr).addMinutes(60 * i),\n          end: new TZDate(baseDateStr).addMinutes(60 * i + 30),\n          isVisible: !!(i % 2),\n        })\n      );\n    }\n\n    beforeAll(() => {\n      jest.useFakeTimers();\n      jest.setSystemTime(new Date(baseDateStr));\n    });\n\n    afterAll(() => {\n      jest.useRealTimers();\n    });\n\n    it('should show only the events which of isVisible is true when the eventFilter option is not specified.', () => {\n      // Given\n      setup({}, events);\n\n      // When\n      // Nothing\n\n      // Then\n      const visibleEvent = events.find((event) => event.isVisible) as EventModel;\n      const invisibleEvent = events.find((event) => !event.isVisible) as EventModel;\n      expect(screen.queryByText(visibleEvent.title)).toBeInTheDocument();\n      expect(screen.queryByText(invisibleEvent.title)).not.toBeInTheDocument();\n    });\n\n    it('should show only the events that pass the eventFilter function.', () => {\n      // Given\n      const eventFilter = (event: EventObject) => !!(Number(event.id) % 2);\n      setup({ eventFilter }, events);\n\n      // When\n      // Nothing\n\n      // Then\n      const visibleEvent = events.find(eventFilter) as EventModel;\n      const invisibleEvent = events.find((event) => !eventFilter(event)) as EventModel;\n      expect(screen.queryByText(visibleEvent.title)).toBeInTheDocument();\n      expect(screen.queryByText(invisibleEvent.title)).not.toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/components/view/day.tsx",
    "content": "import { h } from 'preact';\nimport { useCallback, useMemo } from 'preact/hooks';\n\nimport { GridHeader } from '@src/components/dayGridCommon/gridHeader';\nimport { AlldayGridRow } from '@src/components/dayGridWeek/alldayGridRow';\nimport { OtherGridRow } from '@src/components/dayGridWeek/otherGridRow';\nimport { Layout } from '@src/components/layout';\nimport { Panel } from '@src/components/panel';\nimport { TimeGrid } from '@src/components/timeGrid/timeGrid';\nimport { TimezoneLabels } from '@src/components/timeGrid/timezoneLabels';\nimport { WEEK_DAY_NAME_BORDER, WEEK_DAY_NAME_HEIGHT } from '@src/constants/style';\nimport { useStore } from '@src/contexts/calendarStore';\nimport { useTheme } from '@src/contexts/themeStore';\nimport { cls } from '@src/helpers/css';\nimport { getDayNames } from '@src/helpers/dayName';\nimport { createTimeGridData, getWeekViewEvents } from '@src/helpers/grid';\nimport { getActivePanels } from '@src/helpers/view';\nimport { useCalendarData } from '@src/hooks/calendar/useCalendarData';\nimport { useDOMNode } from '@src/hooks/common/useDOMNode';\nimport { useTimeGridScrollSync } from '@src/hooks/timeGrid/useTimeGridScrollSync';\nimport { useTimezoneLabelsTop } from '@src/hooks/timeGrid/useTimezoneLabelsTop';\nimport {\n  calendarSelector,\n  optionsSelector,\n  viewSelector,\n  weekViewLayoutSelector,\n} from '@src/selectors';\nimport { primaryTimezoneSelector } from '@src/selectors/timezone';\nimport { addDate, getRowStyleInfo, toEndOfDay, toStartOfDay } from '@src/time/datetime';\n\nimport type { WeekOptions } from '@t/options';\nimport type { AlldayEventCategory } from '@t/panel';\n\nfunction useDayViewState() {\n  const calendar = useStore(calendarSelector);\n  const options = useStore(optionsSelector);\n  const { dayGridRows: gridRowLayout, lastPanelType } = useStore(weekViewLayoutSelector);\n  const { renderDate } = useStore(viewSelector);\n\n  return useMemo(\n    () => ({\n      calendar,\n      options,\n      gridRowLayout,\n      lastPanelType,\n      renderDate,\n    }),\n    [calendar, options, gridRowLayout, lastPanelType, renderDate]\n  );\n}\n\nexport function Day() {\n  const { calendar, options, gridRowLayout, lastPanelType, renderDate } = useDayViewState();\n\n  const primaryTimezoneName = useStore(primaryTimezoneSelector);\n\n  const gridHeaderMarginLeft = useTheme(useCallback((theme) => theme.week.dayGridLeft.width, []));\n  const [timePanel, setTimePanelRef] = useDOMNode<HTMLDivElement>();\n\n  const weekOptions = options.week as Required<WeekOptions>;\n  const { narrowWeekend, startDayOfWeek, workweek, hourStart, hourEnd, eventView, taskView } =\n    weekOptions;\n  const days = useMemo(() => [renderDate], [renderDate]);\n  const dayNames = getDayNames(days, options.week?.dayNames ?? []);\n  const { rowStyleInfo, cellWidthMap } = getRowStyleInfo(\n    days.length,\n    narrowWeekend,\n    startDayOfWeek,\n    workweek\n  );\n  const calendarData = useCalendarData(calendar, options.eventFilter);\n  const dayGridEvents = useMemo(() => {\n    const getFilterRange = () => {\n      if (primaryTimezoneName === 'Local') {\n        return [toStartOfDay(days[0]), toEndOfDay(days[0])];\n      }\n\n      // NOTE: Extend filter range because of timezone offset differences\n      return [toStartOfDay(addDate(days[0], -1)), toEndOfDay(addDate(days[0], 1))];\n    };\n\n    const [weekStartDate, weekEndDate] = getFilterRange();\n\n    return getWeekViewEvents(days, calendarData, {\n      narrowWeekend,\n      hourStart,\n      hourEnd,\n      weekStartDate,\n      weekEndDate,\n    });\n  }, [calendarData, days, hourEnd, hourStart, narrowWeekend, primaryTimezoneName]);\n  const timeGridData = useMemo(\n    () =>\n      createTimeGridData(days, {\n        hourStart,\n        hourEnd,\n        narrowWeekend,\n      }),\n    [days, hourEnd, hourStart, narrowWeekend]\n  );\n  const activePanels = getActivePanels(taskView, eventView);\n  const gridRows = activePanels.map((key) => {\n    if (key === 'time') {\n      return null;\n    }\n\n    const rowType = key as AlldayEventCategory;\n\n    return (\n      <Panel key={rowType} name={rowType} resizable={rowType !== lastPanelType}>\n        {rowType === 'allday' ? (\n          <AlldayGridRow\n            events={dayGridEvents[rowType]}\n            rowStyleInfo={rowStyleInfo}\n            gridColWidthMap={cellWidthMap}\n            weekDates={days}\n            height={gridRowLayout[rowType]?.height}\n            options={weekOptions}\n          />\n        ) : (\n          <OtherGridRow\n            category={rowType}\n            events={dayGridEvents[rowType]}\n            weekDates={days}\n            height={gridRowLayout[rowType]?.height}\n            options={weekOptions}\n            gridColWidthMap={cellWidthMap}\n          />\n        )}\n      </Panel>\n    );\n  });\n\n  useTimeGridScrollSync(timePanel, timeGridData.rows.length);\n\n  const stickyTop = useTimezoneLabelsTop(timePanel);\n\n  return (\n    <Layout className={cls('day-view')} autoAdjustPanels={true}>\n      <Panel name=\"day-view-day-names\" initialHeight={WEEK_DAY_NAME_HEIGHT + WEEK_DAY_NAME_BORDER}>\n        <GridHeader\n          type=\"week\"\n          dayNames={dayNames}\n          marginLeft={gridHeaderMarginLeft}\n          rowStyleInfo={rowStyleInfo}\n        />\n      </Panel>\n      {gridRows}\n      {activePanels.includes('time') ? (\n        <Panel name=\"time\" autoSize={1} ref={setTimePanelRef}>\n          <TimeGrid events={dayGridEvents.time} timeGridData={timeGridData} />\n          <TimezoneLabels top={stickyTop} />\n        </Panel>\n      ) : null}\n    </Layout>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/view/main.tsx",
    "content": "import type { FunctionComponent } from 'preact';\nimport { h } from 'preact';\nimport { useMemo } from 'preact/hooks';\n\nimport { Day } from '@src/components/view/day';\nimport { Month } from '@src/components/view/month';\nimport { Week } from '@src/components/view/week';\nimport { useStore } from '@src/contexts/calendarStore';\nimport { viewSelector } from '@src/selectors';\n\nimport type { ViewType } from '@t/options';\n\nconst views: {\n  [k in ViewType]: FunctionComponent;\n} = {\n  month: Month,\n  week: Week,\n  day: Day,\n};\n\nexport function Main() {\n  const { currentView } = useStore(viewSelector);\n  const CurrentViewComponent = useMemo(() => views[currentView] || (() => null), [currentView]);\n\n  return <CurrentViewComponent />;\n}\n"
  },
  {
    "path": "apps/calendar/src/components/view/month.spec.tsx",
    "content": "import { h } from 'preact';\n\nimport { Month } from '@src/components/view/month';\nimport { initCalendarStore } from '@src/contexts/calendarStore';\nimport { initThemeStore } from '@src/contexts/themeStore';\nimport EventModel from '@src/model/eventModel';\nimport { render, screen } from '@src/test/utils';\nimport TZDate from '@src/time/date';\nimport { noop } from '@src/utils/noop';\n\nimport type { EventObject } from '@t/events';\nimport type { MonthOptions, Options } from '@t/options';\n\nfunction setup(options: Options, events?: EventModel[]) {\n  const store = initCalendarStore(options);\n  const theme = initThemeStore(options.theme);\n\n  if (events) {\n    store.getState().dispatch.calendar.createEvents(events);\n  }\n\n  return render(<Month />, { store, theme });\n}\n\ndescribe('eventFilter option', () => {\n  beforeAll(() => {\n    jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({\n      x: 0,\n      y: 0,\n      top: 0,\n      bottom: 0,\n      left: 0,\n      right: 0,\n      width: 100,\n      height: 100,\n      toJSON: noop,\n    });\n  });\n\n  afterAll(() => {\n    jest.restoreAllMocks();\n  });\n\n  const events: EventModel[] = [];\n  for (let i = 0; i < 2; i += 1) {\n    events.push(\n      new EventModel({\n        id: `${i}`,\n        title: `Event ${i}`,\n        start: new TZDate().addMinutes(60 * i),\n        end: new TZDate().addMinutes(60 * i + 30),\n        isVisible: !!(i % 2),\n      })\n    );\n  }\n\n  it('should show only the events which of isVisible is true when the eventFilter option is not specified.', () => {\n    // Given\n    setup({}, events);\n\n    // When\n    // Nothing\n\n    // Then\n    const visibleEvent = events.find((event) => event.isVisible) as EventModel;\n    const invisibleEvent = events.find((event) => !event.isVisible) as EventModel;\n    expect(screen.queryByText(visibleEvent.title)).toBeInTheDocument();\n    expect(screen.queryByText(invisibleEvent.title)).not.toBeInTheDocument();\n  });\n\n  it('should show only the events that pass the eventFilter function.', () => {\n    // Given\n    const eventFilter = (event: EventObject) => !!(Number(event.id) % 2);\n    setup({ eventFilter }, events);\n\n    // When\n    // Nothing\n\n    // Then\n    const visibleEvent = events.find(eventFilter) as EventModel;\n    const invisibleEvent = events.find((event) => !eventFilter(event)) as EventModel;\n    expect(screen.queryByText(visibleEvent.title)).toBeInTheDocument();\n    expect(screen.queryByText(invisibleEvent.title)).not.toBeInTheDocument();\n  });\n});\n\ndescribe('dayNames Option', () => {\n  it('should show the default day names if the day names option is not specified.', () => {\n    // Given\n    const dayNames: MonthOptions['dayNames'] = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];\n\n    // When\n    setup({});\n\n    // Then\n    const header = screen.getByTestId('grid-header-month');\n\n    expect(header).toHaveTextContent(dayNames.join(''));\n  });\n\n  it('should show the day names specified by the day names option.', () => {\n    // Given\n    const dayNames: MonthOptions['dayNames'] = ['日', '月', '火', '水', '木', '金', '土'];\n\n    // When\n    setup({\n      month: {\n        dayNames,\n      },\n    });\n\n    // Then\n    const header = screen.getByTestId('grid-header-month');\n    expect(header).toHaveTextContent(dayNames.join(''));\n  });\n\n  it('should change day names following startDayOfWeek option.', () => {\n    // Given\n    const dayNames: MonthOptions['dayNames'] = ['日', '月', '火', '水', '木', '金', '土'];\n    const startDayOfWeek = 1; // Monday\n\n    // When\n    setup({\n      month: {\n        dayNames,\n        startDayOfWeek,\n      },\n    });\n\n    // Then\n    const header = screen.getByTestId('grid-header-month');\n    expect(header).toHaveTextContent(dayNames.slice(startDayOfWeek).concat('日').join(''));\n  });\n});\n\ndescribe('startDayOfWeek Option', () => {\n  it('should change the start day of week with default day names', () => {\n    // Given\n    const startDayOfWeek = 1; // Monday\n    const normalDayNameColor = 'white';\n    const sundayDayNameColor = 'red';\n\n    // When\n    setup({\n      month: {\n        startDayOfWeek,\n      },\n      theme: {\n        common: {\n          dayName: {\n            color: normalDayNameColor,\n          },\n          holiday: {\n            color: sundayDayNameColor,\n          },\n        },\n      },\n    });\n\n    // Then\n    const { 0: monday, length, [length - 1]: sunday } = screen.getAllByTestId(/dayName-month-/);\n\n    expect(monday).toHaveStyle({ color: normalDayNameColor });\n    expect(sunday).toHaveStyle({ color: sundayDayNameColor });\n  });\n\n  it('should change start day of week with custom day names', () => {\n    // Given\n    const startDayOfWeek = 2; // Tuesday\n    const givenDayNames: MonthOptions['dayNames'] = ['日', '月', '火', '水', '木', '金', '土'];\n    const normalDayNameColor = 'white';\n    const sundayDayNameColor = 'red';\n\n    // When\n    setup({\n      month: {\n        startDayOfWeek,\n        dayNames: givenDayNames,\n      },\n      theme: {\n        common: {\n          dayName: {\n            color: normalDayNameColor,\n          },\n          holiday: {\n            color: sundayDayNameColor,\n          },\n        },\n      },\n    });\n\n    // Then\n    const header = screen.getByTestId('grid-header-month');\n    expect(header).toHaveTextContent(\n      givenDayNames.slice(startDayOfWeek).concat(givenDayNames.slice(0, startDayOfWeek)).join('')\n    );\n    const { 0: tuesday, length, [length - 2]: sunday } = screen.getAllByTestId(/dayName-month-/);\n\n    expect(tuesday).toHaveStyle({ color: normalDayNameColor });\n    expect(sunday).toHaveStyle({ color: sundayDayNameColor });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/components/view/month.tsx",
    "content": "import { h } from 'preact';\nimport { useMemo } from 'preact/hooks';\n\nimport { GridHeader } from '@src/components/dayGridCommon/gridHeader';\nimport { DayGridMonth } from '@src/components/dayGridMonth/dayGridMonth';\nimport { Layout } from '@src/components/layout';\nimport { useStore } from '@src/contexts/calendarStore';\nimport { cls } from '@src/helpers/css';\nimport { createDateMatrixOfMonth } from '@src/helpers/grid';\nimport { optionsSelector, viewSelector } from '@src/selectors';\nimport { getRowStyleInfo, isWeekend } from '@src/time/datetime';\nimport { capitalize } from '@src/utils/string';\n\nimport type { MonthOptions } from '@t/options';\nimport type { CalendarStore } from '@t/store';\nimport type { CellInfo } from '@t/time/datetime';\n\nfunction getMonthDayNames(options: CalendarStore['options']) {\n  const { dayNames, startDayOfWeek, workweek } = options.month as Required<MonthOptions>;\n  const dayIndices = [...Array(7)].map((_, i) => (startDayOfWeek + i) % 7);\n  const monthDayNames = dayIndices.map((i) => ({\n    day: i,\n    label: capitalize(dayNames[i]),\n  }));\n\n  return monthDayNames.filter((dayNameInfo) => (workweek ? !isWeekend(dayNameInfo.day) : true));\n}\n\nexport function Month() {\n  const options = useStore(optionsSelector);\n  const { renderDate } = useStore(viewSelector);\n\n  const dayNames = getMonthDayNames(options);\n  const monthOptions = options.month as Required<MonthOptions>;\n  const { narrowWeekend, startDayOfWeek, workweek } = monthOptions;\n\n  const dateMatrix = useMemo(\n    () => createDateMatrixOfMonth(renderDate, monthOptions),\n    [monthOptions, renderDate]\n  );\n  const { rowStyleInfo, cellWidthMap } = useMemo(\n    () => getRowStyleInfo(dayNames.length, narrowWeekend, startDayOfWeek, workweek),\n    [dayNames.length, narrowWeekend, startDayOfWeek, workweek]\n  );\n  const rowInfo: CellInfo[] = rowStyleInfo.map((cellStyleInfo, index) => ({\n    ...cellStyleInfo,\n    date: dateMatrix[0][index],\n  }));\n\n  return (\n    <Layout className={cls('month')}>\n      <GridHeader\n        type=\"month\"\n        dayNames={dayNames}\n        options={monthOptions}\n        rowStyleInfo={rowStyleInfo}\n      />\n      <DayGridMonth dateMatrix={dateMatrix} rowInfo={rowInfo} cellWidthMap={cellWidthMap} />\n    </Layout>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/components/view/week.spec.tsx",
    "content": "import { h } from 'preact';\n\nimport { Week } from '@src/components/view/week';\nimport { DEFAULT_EVENT_PANEL, DEFAULT_TASK_PANEL } from '@src/constants/view';\nimport { initCalendarStore } from '@src/contexts/calendarStore';\nimport { cls } from '@src/helpers/css';\nimport { DEFAULT_DAY_NAMES } from '@src/helpers/dayName';\nimport { getActivePanels } from '@src/helpers/view';\nimport EventModel from '@src/model/eventModel';\nimport { render, screen } from '@src/test/utils';\nimport TZDate from '@src/time/date';\nimport { capitalize } from '@src/utils/string';\n\nimport type { EventObject } from '@t/events';\nimport type { Options, WeekOptions } from '@t/options';\n\nfunction setup(options: Options, events?: EventModel[]) {\n  const store = initCalendarStore(options);\n  if (events) {\n    store.getState().dispatch.calendar.createEvents(events);\n  }\n\n  return render(<Week />, { store });\n}\n\ndescribe('eventView and taskView options', () => {\n  const cases: WeekOptions[] = [\n    { eventView: true, taskView: true },\n    { eventView: false, taskView: false },\n    { eventView: ['allday'], taskView: ['milestone'] },\n    { eventView: ['allday', 'time'], taskView: false },\n  ];\n\n  const panels = [...DEFAULT_TASK_PANEL, ...DEFAULT_EVENT_PANEL];\n\n  it.each(cases)(\n    'should show/hide the panels in the daily view: { eventView: $eventView, taskView: $eventView }',\n    (weekOptions) => {\n      // Given\n      const { container } = setup({ week: weekOptions });\n\n      // When\n      // Nothing\n\n      // Then\n      const activePanels = getActivePanels(\n        weekOptions.taskView ?? false,\n        weekOptions.eventView ?? false\n      );\n      panels.forEach((panel) => {\n        const panelEl = container.querySelector(`.${cls(panel)}`);\n        if (activePanels.includes(panel)) {\n          expect(panelEl).not.toBeNull();\n        } else {\n          expect(panelEl).toBeNull();\n        }\n      });\n    }\n  );\n});\n\ndescribe('eventFilter option', () => {\n  const events: EventModel[] = [];\n  const baseDateStr = '2022-05-22T00:00:00+09:00';\n  for (let i = 0; i < 2; i += 1) {\n    events.push(\n      new EventModel({\n        id: `${i}`,\n        title: `Event ${i}`,\n        start: new TZDate(baseDateStr).addMinutes(60 * i),\n        end: new TZDate(baseDateStr).addMinutes(60 * i + 30),\n        isVisible: !!(i % 2),\n      })\n    );\n  }\n\n  beforeAll(() => {\n    jest.useFakeTimers();\n    jest.setSystemTime(new Date(baseDateStr));\n  });\n\n  afterAll(() => {\n    jest.useRealTimers();\n  });\n\n  it('should show only the events which of isVisible is true when the eventFilter option is not specified.', () => {\n    // Given\n    setup({}, events);\n\n    // When\n    // Nothing\n\n    // Then\n    const visibleEvent = events.find((event) => event.isVisible) as EventModel;\n    const invisibleEvent = events.find((event) => !event.isVisible) as EventModel;\n    expect(screen.queryByText(visibleEvent.title)).toBeInTheDocument();\n    expect(screen.queryByText(invisibleEvent.title)).not.toBeInTheDocument();\n  });\n\n  it('should show only the events that pass the eventFilter function.', () => {\n    // Given\n    const eventFilter = (event: EventObject) => !!(Number(event.id) % 2);\n    setup({ eventFilter }, events);\n\n    // When\n    // Nothing\n\n    // Then\n    const visibleEvent = events.find(eventFilter) as EventModel;\n    const invisibleEvent = events.find((event) => !eventFilter(event)) as EventModel;\n    expect(screen.queryByText(visibleEvent.title)).toBeInTheDocument();\n    expect(screen.queryByText(invisibleEvent.title)).not.toBeInTheDocument();\n  });\n});\n\ndescribe('dayNames option', () => {\n  // Week view has including date with day name, so let exclude them.\n  const extractDayNamesOnly = (text: string | null) => text?.replace(/\\d+\\s+/g, '');\n\n  it('should show the default day names if the day names option is not specified', () => {\n    // Given\n    const expectedTextContent = DEFAULT_DAY_NAMES.map((day) => capitalize(day)).join('');\n\n    // When\n    setup({});\n\n    // Then\n    const header = screen.getByTestId('grid-header-week');\n    expect(extractDayNamesOnly(header.textContent)).toBe(expectedTextContent);\n  });\n\n  it('should show the day names specified by the dayNames option', () => {\n    // Given\n    const dayNames: WeekOptions['dayNames'] = ['日', '月', '火', '水', '木', '金', '土'];\n    const expectedTextContent = dayNames.map((day) => capitalize(day)).join('');\n\n    // When\n    setup({\n      week: {\n        dayNames,\n      },\n    });\n\n    // Then\n    const header = screen.getByTestId('grid-header-week');\n    expect(extractDayNamesOnly(header.textContent)).toBe(expectedTextContent);\n  });\n\n  it('should change the order of day names following startDayOfWeek option', () => {\n    // Given\n    const dayNames: WeekOptions['dayNames'] = ['日', '月', '火', '水', '木', '金', '土'];\n    const startDayOfWeek = 1; // Monday\n    const expectedTextContent = [\n      ...dayNames.slice(startDayOfWeek),\n      ...dayNames.slice(0, startDayOfWeek),\n    ]\n      .map((day) => capitalize(day))\n      .join('');\n\n    // When\n    setup({\n      week: {\n        dayNames,\n        startDayOfWeek: 1,\n      },\n    });\n\n    // Then\n    const header = screen.getByTestId('grid-header-week');\n    expect(extractDayNamesOnly(header.textContent)).toBe(expectedTextContent);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/components/view/week.tsx",
    "content": "import { h } from 'preact';\nimport { useCallback, useMemo } from 'preact/hooks';\n\nimport { GridHeader } from '@src/components/dayGridCommon/gridHeader';\nimport { AlldayGridRow } from '@src/components/dayGridWeek/alldayGridRow';\nimport { OtherGridRow } from '@src/components/dayGridWeek/otherGridRow';\nimport { Layout } from '@src/components/layout';\nimport { Panel } from '@src/components/panel';\nimport { TimeGrid } from '@src/components/timeGrid/timeGrid';\nimport { TimezoneLabels } from '@src/components/timeGrid/timezoneLabels';\nimport { WEEK_DAY_NAME_BORDER, WEEK_DAY_NAME_HEIGHT } from '@src/constants/style';\nimport { useStore } from '@src/contexts/calendarStore';\nimport { useTheme } from '@src/contexts/themeStore';\nimport { cls } from '@src/helpers/css';\nimport { getDayNames } from '@src/helpers/dayName';\nimport { createTimeGridData, getWeekDates, getWeekViewEvents } from '@src/helpers/grid';\nimport { getActivePanels } from '@src/helpers/view';\nimport { useCalendarData } from '@src/hooks/calendar/useCalendarData';\nimport { useDOMNode } from '@src/hooks/common/useDOMNode';\nimport { useTimeGridScrollSync } from '@src/hooks/timeGrid/useTimeGridScrollSync';\nimport { useTimezoneLabelsTop } from '@src/hooks/timeGrid/useTimezoneLabelsTop';\nimport {\n  calendarSelector,\n  optionsSelector,\n  viewSelector,\n  weekViewLayoutSelector,\n} from '@src/selectors';\nimport { primaryTimezoneSelector } from '@src/selectors/timezone';\nimport { addDate, getRowStyleInfo, toEndOfDay, toStartOfDay } from '@src/time/datetime';\nimport { first, last } from '@src/utils/array';\n\nimport type { WeekOptions } from '@t/options';\nimport type { AlldayEventCategory } from '@t/panel';\n\nfunction useWeekViewState() {\n  const options = useStore(optionsSelector);\n  const calendar = useStore(calendarSelector);\n  const { dayGridRows: gridRowLayout, lastPanelType } = useStore(weekViewLayoutSelector);\n  const { renderDate } = useStore(viewSelector);\n\n  return useMemo(\n    () => ({\n      options,\n      calendar,\n      gridRowLayout,\n      lastPanelType,\n      renderDate,\n    }),\n    [calendar, gridRowLayout, lastPanelType, options, renderDate]\n  );\n}\n\nexport function Week() {\n  const { options, calendar, gridRowLayout, lastPanelType, renderDate } = useWeekViewState();\n  const gridHeaderMarginLeft = useTheme(useCallback((theme) => theme.week.dayGridLeft.width, []));\n\n  const primaryTimezoneName = useStore(primaryTimezoneSelector);\n\n  const [timePanel, setTimePanelRef] = useDOMNode<HTMLDivElement>();\n\n  const weekOptions = options.week as Required<WeekOptions>;\n  const { narrowWeekend, startDayOfWeek, workweek, hourStart, hourEnd, eventView, taskView } =\n    weekOptions;\n  const weekDates = useMemo(() => getWeekDates(renderDate, weekOptions), [renderDate, weekOptions]);\n  const dayNames = getDayNames(weekDates, options.week?.dayNames ?? []);\n  const { rowStyleInfo, cellWidthMap } = getRowStyleInfo(\n    weekDates.length,\n    narrowWeekend,\n    startDayOfWeek,\n    workweek\n  );\n  const calendarData = useCalendarData(calendar, options.eventFilter);\n  const eventByPanel = useMemo(() => {\n    const getFilterRange = () => {\n      if (primaryTimezoneName === 'Local') {\n        return [toStartOfDay(first(weekDates)), toEndOfDay(last(weekDates))];\n      }\n\n      // NOTE: Extend filter range because of timezone offset differences\n      return [toStartOfDay(addDate(first(weekDates), -1)), toEndOfDay(addDate(last(weekDates), 1))];\n    };\n\n    const [weekStartDate, weekEndDate] = getFilterRange();\n\n    return getWeekViewEvents(weekDates, calendarData, {\n      narrowWeekend,\n      hourStart,\n      hourEnd,\n      weekStartDate,\n      weekEndDate,\n    });\n  }, [calendarData, hourEnd, hourStart, narrowWeekend, primaryTimezoneName, weekDates]);\n  const timeGridData = useMemo(\n    () =>\n      createTimeGridData(weekDates, {\n        hourStart,\n        hourEnd,\n        narrowWeekend,\n      }),\n    [hourEnd, hourStart, narrowWeekend, weekDates]\n  );\n\n  const activePanels = getActivePanels(taskView, eventView);\n  const dayGridRows = activePanels.map((key) => {\n    if (key === 'time') {\n      return null;\n    }\n\n    const rowType = key as AlldayEventCategory;\n\n    return (\n      <Panel name={rowType} key={rowType} resizable={rowType !== lastPanelType}>\n        {rowType === 'allday' ? (\n          <AlldayGridRow\n            events={eventByPanel[rowType]}\n            rowStyleInfo={rowStyleInfo}\n            gridColWidthMap={cellWidthMap}\n            weekDates={weekDates}\n            height={gridRowLayout[rowType]?.height}\n            options={weekOptions}\n          />\n        ) : (\n          <OtherGridRow\n            category={rowType}\n            events={eventByPanel[rowType]}\n            weekDates={weekDates}\n            height={gridRowLayout[rowType]?.height}\n            options={weekOptions}\n            gridColWidthMap={cellWidthMap}\n          />\n        )}\n      </Panel>\n    );\n  });\n  const hasTimePanel = useMemo(() => activePanels.includes('time'), [activePanels]);\n\n  useTimeGridScrollSync(timePanel, timeGridData.rows.length);\n\n  const stickyTop = useTimezoneLabelsTop(timePanel);\n\n  return (\n    <Layout className={cls('week-view')} autoAdjustPanels={true}>\n      <Panel\n        name=\"week-view-day-names\"\n        initialHeight={WEEK_DAY_NAME_HEIGHT + WEEK_DAY_NAME_BORDER * 2}\n      >\n        <GridHeader\n          type=\"week\"\n          dayNames={dayNames}\n          marginLeft={gridHeaderMarginLeft}\n          options={weekOptions}\n          rowStyleInfo={rowStyleInfo}\n        />\n      </Panel>\n      {dayGridRows}\n      {hasTimePanel ? (\n        <Panel name=\"time\" autoSize={1} ref={setTimePanelRef}>\n          <TimeGrid events={eventByPanel.time} timeGridData={timeGridData} />\n          <TimezoneLabels top={stickyTop} />\n        </Panel>\n      ) : null}\n    </Layout>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/constants/error.ts",
    "content": "export const INVALID_DATETIME_FORMAT = 'Invalid DateTime Format';\nexport const INVALID_TIMEZONE_NAME = 'Invalid IANA Timezone Name';\nexport const INVALID_VIEW_TYPE = 'Invalid View Type';\n"
  },
  {
    "path": "apps/calendar/src/constants/grid.ts",
    "content": "export const DEFAULT_VISIBLE_WEEKS = 6;\n\nexport enum CellBarType {\n  header = 'header',\n  footer = 'footer',\n}\n"
  },
  {
    "path": "apps/calendar/src/constants/keyboard.ts",
    "content": "export enum KEY {\n  ESCAPE = 'Escape',\n}\n\nexport const KEYCODE: Record<KEY, number> = {\n  [KEY.ESCAPE]: 27,\n};\n"
  },
  {
    "path": "apps/calendar/src/constants/layout.ts",
    "content": "export const DEFAULT_RESIZER_LENGTH = 3;\n\nexport const DEFAULT_DUPLICATE_EVENT_CID = -1;\n"
  },
  {
    "path": "apps/calendar/src/constants/message.ts",
    "content": "export const MESSAGE_PREFIX = '@toast-ui/calendar: ';\n"
  },
  {
    "path": "apps/calendar/src/constants/mouse.ts",
    "content": "export const MINIMUM_DRAG_MOUSE_DISTANCE = 3;\n"
  },
  {
    "path": "apps/calendar/src/constants/popup.ts",
    "content": "import { cls } from '@src/helpers/css';\n\nimport type { BooleanKeyOfEventObject } from '@t/events';\n\nexport const SEE_MORE_POPUP_SLOT_CLASS_NAME = cls('see-more-popup-slot');\nexport const EVENT_FORM_POPUP_SLOT_CLASS_NAME = cls('event-form-popup-slot');\nexport const EVENT_DETAIL_POPUP_SLOT_CLASS_NAME = cls('event-detail-popup-slot');\n\nexport const HALF_OF_POPUP_ARROW_HEIGHT = 8;\n\nexport const BOOLEAN_KEYS_OF_EVENT_MODEL_DATA: BooleanKeyOfEventObject[] = [\n  'isPrivate',\n  'isAllday',\n  'isPending',\n  'isFocused',\n  'isVisible',\n  'isReadOnly',\n];\n\nexport enum DetailPopupArrowDirection {\n  right = 'right',\n  left = 'left',\n}\n\nexport enum FormPopupArrowDirection {\n  top = 'top',\n  bottom = 'bottom',\n}\n"
  },
  {
    "path": "apps/calendar/src/constants/statistics.ts",
    "content": "export const GA_TRACKING_ID = 'UA-129951699-1';\n"
  },
  {
    "path": "apps/calendar/src/constants/style.ts",
    "content": "// common day name\nexport const DEFAULT_DAY_NAME_MARGIN_LEFT = '0';\n\n// month day name\nexport const MONTH_DAY_NAME_HEIGHT = 31;\n\n// month event\nexport const MONTH_EVENT_BORDER_RADIUS = 2;\nexport const MONTH_EVENT_HEIGHT = 24;\nexport const MONTH_EVENT_MARGIN_TOP = 2;\nexport const MONTH_EVENT_MARGIN_LEFT = 8;\nexport const MONTH_EVENT_MARGIN_RIGHT = 8;\n\n// month cell\nexport const MONTH_CELL_PADDING_TOP = 3;\nexport const MONTH_CELL_BAR_HEIGHT = 27;\n\n// month more view\nexport const MONTH_MORE_VIEW_PADDING = 5;\nexport const MONTH_MORE_VIEW_MIN_WIDTH = 280;\nexport const MONTH_MORE_VIEW_HEADER_HEIGHT = 44;\nexport const MONTH_MORE_VIEW_HEADER_MARGIN_BOTTOM = 12;\nexport const MONTH_MORE_VIEW_HEADER_PADDING_TOP = 12;\nexport const MONTH_MORE_VIEW_HEADER_PADDING = '12px 17px 0';\n\n// week day name\nexport const WEEK_DAY_NAME_HEIGHT = 42;\nexport const WEEK_DAY_NAME_BORDER = 1;\n\n// week panel resizer\nexport const WEEK_PANEL_RESIZER_HEIGHT = 3;\n\n// week event\nexport const WEEK_EVENT_BORDER_RADIUS = 2;\nexport const WEEK_EVENT_HEIGHT = 24;\nexport const WEEK_EVENT_MARGIN_TOP = 2;\nexport const WEEK_EVENT_MARGIN_LEFT = 8;\nexport const WEEK_EVENT_MARGIN_RIGHT = 8;\n\nexport const DEFAULT_PANEL_HEIGHT = 72;\n\n// default color values for events\nexport const DEFAULT_EVENT_COLORS = {\n  color: '#000',\n  backgroundColor: '#a1b56c',\n  dragBackgroundColor: '#a1b56c',\n  borderColor: '#000',\n};\n\nexport const TIME_EVENT_CONTAINER_MARGIN_LEFT = 2;\n\nexport const COLLAPSED_DUPLICATE_EVENT_WIDTH_PX = 9;\n"
  },
  {
    "path": "apps/calendar/src/constants/theme.ts",
    "content": "import type { DeepRequired } from 'ts-essentials';\n\nimport type { CommonTheme, MonthTheme, WeekTheme } from '@t/theme';\n\nexport const DEFAULT_COMMON_THEME: DeepRequired<CommonTheme> = {\n  border: '1px solid #e5e5e5',\n  backgroundColor: 'white',\n  holiday: {\n    color: '#ff4040',\n  },\n  saturday: {\n    color: '#333',\n  },\n  dayName: {\n    color: '#333',\n  },\n  today: {\n    color: '#fff',\n  },\n  gridSelection: {\n    backgroundColor: 'rgba(81, 92, 230, 0.05)',\n    border: '1px solid #515ce6',\n  },\n};\n\nexport const DEFAULT_WEEK_THEME: DeepRequired<WeekTheme> = {\n  dayName: {\n    borderLeft: 'none',\n    borderTop: '1px solid #e5e5e5',\n    borderBottom: '1px solid #e5e5e5',\n    backgroundColor: 'inherit',\n  },\n  weekend: {\n    backgroundColor: 'inherit',\n  },\n  today: {\n    color: 'inherit',\n    backgroundColor: 'rgba(81, 92, 230, 0.05)',\n  },\n  pastDay: {\n    color: '#bbb',\n  },\n  panelResizer: {\n    border: '1px solid #e5e5e5',\n  },\n  dayGrid: {\n    borderRight: '1px solid #e5e5e5',\n    backgroundColor: 'inherit',\n  },\n  dayGridLeft: {\n    borderRight: '1px solid #e5e5e5',\n    backgroundColor: 'inherit',\n    width: '72px',\n  },\n  timeGrid: {\n    borderRight: '1px solid #e5e5e5',\n  },\n  timeGridLeft: {\n    backgroundColor: 'inherit',\n    borderRight: '1px solid #e5e5e5',\n    width: '72px',\n  },\n  timeGridLeftAdditionalTimezone: {\n    backgroundColor: 'white',\n  },\n  timeGridHalfHourLine: {\n    borderBottom: 'none',\n  },\n  timeGridHourLine: {\n    borderBottom: '1px solid #e5e5e5',\n  },\n  nowIndicatorLabel: {\n    color: '#515ce6',\n  },\n  nowIndicatorPast: {\n    border: '1px dashed #515ce6',\n  },\n  nowIndicatorBullet: {\n    backgroundColor: '#515ce6',\n  },\n  nowIndicatorToday: {\n    border: '1px solid #515ce6',\n  },\n  nowIndicatorFuture: {\n    border: 'none',\n  },\n  pastTime: {\n    color: '#bbb',\n  },\n  futureTime: {\n    color: '#333',\n  },\n  gridSelection: {\n    color: '#515ce6',\n  },\n};\n\nexport const DEFAULT_MONTH_THEME: DeepRequired<MonthTheme> = {\n  dayName: {\n    borderLeft: 'none',\n    backgroundColor: 'inherit',\n  },\n  holidayExceptThisMonth: {\n    color: 'rgba(255, 64, 64, 0.4)',\n  },\n  dayExceptThisMonth: {\n    color: 'rgba(51, 51, 51, 0.4)',\n  },\n  weekend: {\n    backgroundColor: 'inherit',\n  },\n  moreView: {\n    border: '1px solid #d5d5d5',\n    boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.1)',\n    backgroundColor: 'white',\n    width: null,\n    height: null,\n  },\n  gridCell: {\n    headerHeight: 31,\n    footerHeight: null,\n  },\n  moreViewTitle: {\n    backgroundColor: 'inherit',\n  },\n};\n"
  },
  {
    "path": "apps/calendar/src/constants/view.ts",
    "content": "import type { EventView, TaskView, ViewType } from '@t/options';\n\nexport const VIEW_TYPE: {\n  [key: string]: ViewType;\n} = {\n  MONTH: 'month',\n  WEEK: 'week',\n  DAY: 'day',\n};\n\nexport const DEFAULT_TASK_PANEL: TaskView[] = ['milestone', 'task'];\n\nexport const DEFAULT_EVENT_PANEL: EventView[] = ['allday', 'time'];\n"
  },
  {
    "path": "apps/calendar/src/contexts/calendarStore.ts",
    "content": "import { useCallback } from 'preact/hooks';\n\nimport { createCalendarDispatchers, createCalendarSlice } from '@src/slices/calendar';\nimport { createDndDispatchers, createDndSlice } from '@src/slices/dnd';\nimport {\n  createGridSelectionDispatchers,\n  createGridSelectionSlice,\n} from '@src/slices/gridSelection';\nimport { createWeekViewLayoutDispatchers, createWeekViewLayoutSlice } from '@src/slices/layout';\nimport { createOptionsDispatchers, createOptionsSlice } from '@src/slices/options';\nimport { createPopupDispatchers, createPopupSlice } from '@src/slices/popup';\nimport { createTemplateDispatchers, createTemplateSlice } from '@src/slices/template';\nimport { createViewDispatchers, createViewSlice } from '@src/slices/view';\nimport { createStoreContext } from '@src/store';\nimport { createStore } from '@src/store/internal';\n\nimport type { Options } from '@t/options';\nimport type { CalendarStore, Dispatchers, SetState } from '@t/store';\n\nconst storeCreator = (options: Options) => (set: SetState<CalendarStore>) => {\n  return {\n    ...createOptionsSlice(options),\n    ...createTemplateSlice(options.template),\n    ...createPopupSlice(),\n    ...createWeekViewLayoutSlice(),\n    ...createCalendarSlice(options.calendars),\n    ...createViewSlice(options.defaultView),\n    ...createDndSlice(),\n    ...createGridSelectionSlice(),\n    dispatch: {\n      options: createOptionsDispatchers(set),\n      popup: createPopupDispatchers(set),\n      weekViewLayout: createWeekViewLayoutDispatchers(set),\n      calendar: createCalendarDispatchers(set),\n      view: createViewDispatchers(set),\n      dnd: createDndDispatchers(set),\n      gridSelection: createGridSelectionDispatchers(set),\n      template: createTemplateDispatchers(set),\n    },\n  };\n};\n\nexport const initCalendarStore = (options: Options = {}) =>\n  createStore<CalendarStore>(storeCreator(options));\n\nconst { StoreProvider, useStore, useInternalStore } = createStoreContext<CalendarStore>();\nexport { StoreProvider, useInternalStore, useStore };\n\nexport function useDispatch(): Dispatchers;\nexport function useDispatch<Group extends keyof Dispatchers>(group: Group): Dispatchers[Group];\nexport function useDispatch<Group extends keyof Dispatchers>(group?: Group) {\n  return useStore(\n    useCallback(\n      (state) => {\n        if (!group) {\n          return state.dispatch;\n        }\n\n        return state.dispatch[group];\n      },\n      [group]\n    )\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/contexts/eventBus.spec.tsx",
    "content": "import { h } from 'preact';\nimport { useLayoutEffect } from 'preact/hooks';\n\nimport { EventBusProvider, useEventBus } from '@src/contexts/eventBus';\nimport { fireEvent, render, screen } from '@src/test/utils';\nimport type { EventBus } from '@src/utils/eventBus';\nimport { EventBusImpl } from '@src/utils/eventBus';\n\nimport type { PropsWithChildren } from '@t/components/common';\n\ndescribe('Event Bus Context', () => {\n  let eventBus: EventBus<any>;\n  let mockHandler: jest.Mock;\n\n  const wrapper = ({ children }: PropsWithChildren) => (\n    <EventBusProvider value={eventBus}>{children}</EventBusProvider>\n  );\n  const Component = ({\n    handler,\n    fireOnce = false,\n  }: {\n    handler: (...args: any[]) => any;\n    fireOnce?: boolean;\n  }) => {\n    const eb = useEventBus();\n\n    useLayoutEffect(() => {\n      eb[fireOnce ? 'once' : 'on']('test', handler);\n    }, [eb, fireOnce, handler]);\n\n    return (\n      <div>\n        <button onClick={() => eb.fire('test')}>Fire Event</button>\n        <button onClick={() => eb.off('test')}>Remove Event</button>\n      </div>\n    );\n  };\n\n  beforeEach(() => {\n    eventBus = new EventBusImpl<any>();\n    mockHandler = jest.fn();\n  });\n\n  afterEach(() => {\n    jest.clearAllMocks();\n  });\n\n  it('should accessible via \"useEventBus\" hook', () => {\n    // Given\n    render(<Component handler={mockHandler} />, { wrapper });\n\n    // When\n    const fireButton = screen.getByText(/fire event/i);\n    fireEvent.click(fireButton);\n    fireEvent.click(fireButton);\n\n    // Then\n    expect(mockHandler).toHaveBeenCalledTimes(2);\n  });\n\n  it('should fire event once when needed', () => {\n    // Given\n    render(<Component handler={mockHandler} fireOnce={true} />, { wrapper });\n\n    // When\n    const fireButton = screen.getByText(/fire event/i);\n    fireEvent.click(fireButton);\n    fireEvent.click(fireButton);\n\n    // Then\n    expect(mockHandler).toHaveBeenCalledTimes(1);\n  });\n\n  it('should be able to remove registered event', () => {\n    // Given\n    render(<Component handler={mockHandler} />, { wrapper });\n\n    // When\n    const fireButton = screen.getByText(/fire event/i);\n    const removeButton = screen.getByText(/remove event/i);\n    fireEvent.click(fireButton);\n    fireEvent.click(removeButton);\n    fireEvent.click(fireButton);\n\n    // Then\n    expect(mockHandler).toHaveBeenCalledTimes(1);\n  });\n\n  it('should be able to remove a specific handler among registered events', () => {\n    // Given\n    const handler1 = jest.fn();\n    const handler2 = jest.fn();\n    // Note: jest.fn() is not an instance of `Function`, so we need to wrap it\n    // so that EventBus recognize and turn off handlers.\n    function wrapMockHandler(mockFn: jest.Mock) {\n      return (...args: any[]) => mockFn(...args);\n    }\n    const wrappedHandler1 = wrapMockHandler(handler1);\n    const wrappedHandler2 = wrapMockHandler(handler2);\n\n    function MultipleHandlerComponent() {\n      const eb = useEventBus();\n\n      useLayoutEffect(() => {\n        eb.on('test', wrappedHandler1);\n        eb.on('test', wrappedHandler2);\n      }, [eb]);\n\n      return (\n        <div>\n          <button onClick={() => eb.fire('test')}>Fire Event</button>\n          <button onClick={() => eb.off('test', wrappedHandler2)}>Remove handler2 Only</button>\n        </div>\n      );\n    }\n    render(<MultipleHandlerComponent />, { wrapper });\n\n    // When\n    const fireButton = screen.getByText(/fire/i);\n    const removeButton = screen.getByText(/remove/i);\n    fireEvent.click(fireButton);\n    fireEvent.click(removeButton);\n    fireEvent.click(fireButton);\n\n    // Then\n    expect(handler1).toHaveBeenCalledTimes(2);\n    expect(handler2).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/contexts/eventBus.tsx",
    "content": "import { createContext } from 'preact';\nimport { useContext } from 'preact/hooks';\n\nimport type { EventBus } from '@src/utils/eventBus';\n\nimport type { ExternalEventTypes, InternalEventTypes } from '@t/eventBus';\n\nconst EventBusContext = createContext<EventBus<ExternalEventTypes & InternalEventTypes> | null>(\n  null\n);\n\nexport const EventBusProvider = EventBusContext.Provider;\n\nexport const useEventBus = () => {\n  const eventBus = useContext(EventBusContext);\n\n  if (!eventBus) {\n    throw new Error('useEventBus must be used within a EventBusProvider');\n  }\n\n  return eventBus;\n};\n"
  },
  {
    "path": "apps/calendar/src/contexts/floatingLayer.tsx",
    "content": "import { createContext, h } from 'preact';\nimport { useContext } from 'preact/hooks';\n\nimport {\n  EVENT_DETAIL_POPUP_SLOT_CLASS_NAME,\n  EVENT_FORM_POPUP_SLOT_CLASS_NAME,\n  SEE_MORE_POPUP_SLOT_CLASS_NAME,\n} from '@src/constants/popup';\nimport { cls } from '@src/helpers/css';\nimport { useDOMNode } from '@src/hooks/common/useDOMNode';\nimport { isUndefined } from '@src/utils/type';\n\nimport type { PropsWithChildren } from '@t/components/common';\n\ninterface FloatingLayers {\n  container: HTMLDivElement | null;\n  seeMorePopupSlot: HTMLDivElement | null;\n  formPopupSlot: HTMLDivElement | null;\n  detailPopupSlot: HTMLDivElement | null;\n}\n\ntype FloatingLayerType = keyof FloatingLayers;\n\nconst FloatingLayerContext = createContext<FloatingLayers | null>(null);\n\nexport function FloatingLayerProvider({ children }: PropsWithChildren) {\n  const [containerRef, containerRefCallback] = useDOMNode<HTMLDivElement>();\n  const [seeMorePopupSlotRef, seeMorePopupSlotRefCallback] = useDOMNode<HTMLDivElement>();\n  const [formPopupSlotRef, formPopupSlotRefCallback] = useDOMNode<HTMLDivElement>();\n  const [detailPopupSlotRef, detailPopupSlotRefCallback] = useDOMNode<HTMLDivElement>();\n\n  const floatingLayer = {\n    container: containerRef,\n    seeMorePopupSlot: seeMorePopupSlotRef,\n    formPopupSlot: formPopupSlotRef,\n    detailPopupSlot: detailPopupSlotRef,\n  };\n\n  return (\n    <FloatingLayerContext.Provider value={floatingLayer}>\n      {children}\n      <div ref={containerRefCallback} className={cls('floating-layer')}>\n        <div ref={seeMorePopupSlotRefCallback} className={SEE_MORE_POPUP_SLOT_CLASS_NAME} />\n        <div ref={formPopupSlotRefCallback} className={EVENT_FORM_POPUP_SLOT_CLASS_NAME} />\n        <div ref={detailPopupSlotRefCallback} className={EVENT_DETAIL_POPUP_SLOT_CLASS_NAME} />\n      </div>\n    </FloatingLayerContext.Provider>\n  );\n}\n\nexport const useFloatingLayer = (floatingLayerType: FloatingLayerType) => {\n  const floatingLayers = useContext(FloatingLayerContext);\n\n  if (isUndefined(floatingLayers)) {\n    throw new Error('FloatingLayerProvider is not found');\n  }\n\n  return floatingLayers?.[floatingLayerType] ?? null;\n};\n"
  },
  {
    "path": "apps/calendar/src/contexts/layoutContainer.tsx",
    "content": "import { createContext } from 'preact';\nimport { useContext } from 'preact/hooks';\n\nimport { isUndefined } from '@src/utils/type';\n\nconst LayoutContainerContext = createContext<HTMLDivElement | null>(null);\n\nexport const LayoutContainerProvider = LayoutContainerContext.Provider;\n\nexport const useLayoutContainer = () => {\n  const ref = useContext(LayoutContainerContext);\n\n  if (isUndefined(ref)) {\n    throw new Error('LayoutContainerProvider is not found');\n  }\n\n  return ref;\n};\n"
  },
  {
    "path": "apps/calendar/src/contexts/themeStore.tsx",
    "content": "import { useCallback } from 'preact/hooks';\n\nimport { commonThemeSelector, monthThemeSelector, weekThemeSelector } from '@src/selectors/theme';\nimport { createStoreContext } from '@src/store';\nimport { createStore } from '@src/store/internal';\nimport { createCommonTheme } from '@src/theme/common';\nimport { createThemeDispatch } from '@src/theme/dispatch';\nimport { createMonthTheme } from '@src/theme/month';\nimport { createWeekTheme } from '@src/theme/week';\n\nimport type { Options } from '@t/options';\nimport type { SetState } from '@t/store';\nimport type {\n  CommonTheme,\n  MonthTheme,\n  ThemeDispatchers,\n  ThemeState,\n  ThemeStore,\n  WeekTheme,\n} from '@t/theme';\n\nconst themeStoreCreator =\n  (themeOptions: Options['theme'] = {}) =>\n  (set: SetState<ThemeStore>) => {\n    return {\n      ...createCommonTheme(themeOptions?.common),\n      ...createWeekTheme(themeOptions?.week),\n      ...createMonthTheme(themeOptions?.month),\n      dispatch: {\n        ...createThemeDispatch(set),\n      },\n    };\n  };\n\nexport const initThemeStore = (themeOptions: Options['theme'] = {}) =>\n  createStore<ThemeStore>(themeStoreCreator(themeOptions));\n\nconst {\n  StoreProvider: ThemeProvider,\n  useInternalStore: useInternalThemeStore,\n  useStore: useTheme,\n} = createStoreContext<ThemeStore>();\nexport { ThemeProvider, useInternalThemeStore, useTheme };\n\nexport function useThemeDispatch(): ThemeDispatchers {\n  return useTheme(useCallback((state) => state.dispatch, []));\n}\n\nexport function useCommonTheme(): CommonTheme {\n  return useTheme(commonThemeSelector);\n}\n\nexport function useWeekTheme(): WeekTheme {\n  return useTheme(weekThemeSelector);\n}\n\nexport function useMonthTheme(): MonthTheme {\n  return useTheme(monthThemeSelector);\n}\n\nexport function useAllTheme(): ThemeState {\n  return useTheme(\n    useCallback(\n      ({ common, week, month }) => ({\n        common,\n        week,\n        month,\n      }),\n      []\n    )\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/controller/base.spec.ts",
    "content": "import {\n  createEvent,\n  createEventCollection,\n  deleteEvent,\n  findByDateRange,\n  getDateRange,\n  updateEvent,\n} from '@src/controller/base';\nimport EventModel from '@src/model/eventModel';\nimport TZDate from '@src/time/date';\n\nimport type { CalendarData, EventObject } from '@t/events';\n\ndescribe('controller/base', () => {\n  let calendarData: CalendarData;\n  let eventDataList: EventObject[];\n\n  beforeEach(() => {\n    calendarData = {\n      calendars: [],\n      events: createEventCollection(),\n      idsOfDay: {},\n    };\n    eventDataList = [\n      {\n        title: 'hunting',\n        isAllday: true,\n        start: '2015/05/01',\n        end: '2015/05/02',\n      },\n      {\n        title: 'meeting',\n        isAllday: false,\n        start: '2015/05/03 12:30:00',\n        end: '2015/05/03 16:00:00',\n      },\n      {\n        title: 'physical training',\n        isAllday: false,\n        start: '2015/05/03 18:30:00',\n        end: '2015/05/03 19:30:00',\n      },\n      {\n        title: 'A',\n        isAllday: false,\n        start: '2015/05/02 12:30:00',\n        end: '2015/05/03 09:20:00',\n      },\n    ];\n  });\n\n  describe('getDateRange()', () => {\n    let event: EventModel;\n\n    it('calculate contain dates for specific events.', () => {\n      const expected = [\n        new TZDate('2015/05/01'),\n        new TZDate('2015/05/02'),\n        new TZDate('2015/05/03'),\n      ];\n\n      event = new EventModel({\n        title: 'A',\n        isAllday: true,\n        start: '2015/05/01',\n        end: '2015/05/03',\n      });\n\n      expect(getDateRange(event.getStarts(), event.getEnds())).toEqual(expected);\n    });\n\n    it('can calculate non all day event.', () => {\n      const expected = [\n        new TZDate('2015/05/01'),\n        new TZDate('2015/05/02'),\n        new TZDate('2015/05/03'),\n      ];\n\n      event = new EventModel({\n        title: 'A',\n        isAllday: false,\n        start: '2015/05/01 12:30:00',\n        end: '2015/05/03 09:20:00',\n      });\n\n      expect(getDateRange(event.getStarts(), event.getEnds())).toEqual(expected);\n    });\n  });\n\n  describe('createEvent()', () => {\n    it('return itself for chaining pattern.', () => {\n      const event = new EventModel(eventDataList[0]);\n\n      expect(event.equals(createEvent(calendarData, eventDataList[0]))).toBe(true);\n    });\n\n    it('create event instance by raw event data.', () => {\n      const id = createEvent(calendarData, eventDataList[0]).cid();\n      const id2 = createEvent(calendarData, eventDataList[1]).cid();\n      const id3 = createEvent(calendarData, eventDataList[3]).cid();\n\n      expect(calendarData.events.size).toBe(3);\n      expect(calendarData.idsOfDay).toEqual({\n        '20150501': [id],\n        '20150502': [id, id3],\n        '20150503': [id2, id3],\n      });\n    });\n  });\n\n  describe('findByDateRange()', () => {\n    let eventList: EventModel[];\n    let idList: number[];\n\n    beforeEach(() => {\n      eventList = [];\n      idList = [];\n\n      eventDataList.forEach((data) => {\n        const item = createEvent(calendarData, data);\n        eventList.push(item);\n        idList.push(item.cid());\n      });\n\n      /*\n       * matrix: {\n       * '20150501': [id1],\n       * '20150502': [id1, id4],\n       * '20150503': [id2, id3, id4]\n       * }\n       */\n    });\n\n    it('by YMD', () => {\n      const expected = {\n        '20150430': [],\n        '20150501': ['hunting'],\n        '20150502': ['hunting', 'A'],\n      };\n\n      const start = new TZDate('2015/04/30');\n      const end = new TZDate('2015/05/02');\n      const result = findByDateRange(calendarData, { start, end });\n\n      expect(result).toEqualUIModelByTitle(expected);\n    });\n\n    it('return ui models in dates properly.', () => {\n      const expected = {\n        '20150502': ['hunting', 'A'],\n        '20150503': ['A', 'meeting', 'physical training'],\n      };\n\n      const start = new TZDate('2015/05/02');\n      const end = new TZDate('2015/05/03');\n\n      const result = findByDateRange(calendarData, { start, end });\n\n      expect(result).toEqualUIModelByTitle(expected);\n    });\n  });\n\n  describe('updateEvent()', () => {\n    it('update owned event and date matrix.', () => {\n      const model = createEvent(calendarData, {\n        title: 'Go to work',\n        isAllday: false,\n        start: '2015/05/01 09:30:00',\n        end: '2015/05/01 18:30:00',\n      });\n      const id = model.cid();\n\n      updateEvent(calendarData, model.id, model.calendarId, {\n        title: 'Go to work',\n        isAllday: false,\n        start: '2015/05/02',\n        end: '2015/05/02',\n      });\n\n      const event = calendarData.events.getFirstItem();\n\n      expect(event).not.toBeNull();\n\n      type CompatableEvent = Record<string, any>;\n\n      expect(event).toEqual(\n        expect.objectContaining<CompatableEvent>({\n          title: 'Go to work',\n          isAllday: false,\n          start: new TZDate('2015/05/02'),\n          end: new TZDate('2015/05/02'),\n        })\n      );\n\n      expect(calendarData.idsOfDay).toEqual({\n        '20150501': [],\n        '20150502': [id],\n      });\n    });\n  });\n\n  describe('deleteEvent()', () => {\n    let event: EventModel;\n\n    beforeEach(() => {\n      event = createEvent(calendarData, {\n        title: 'Go to work',\n        isAllday: false,\n        start: '2015/05/01 09:30:00',\n        end: '2015/05/01 18:30:00',\n      });\n    });\n\n    it('delete an event by model.', () => {\n      expect(deleteEvent(calendarData, event)).toEqual(event);\n      expect(calendarData.events.size).toBe(0);\n      expect(calendarData.idsOfDay).toEqual({\n        '20150501': [],\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/controller/base.ts",
    "content": "import { isSameEvent } from '@src/helpers/events';\nimport EventModel from '@src/model/eventModel';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport type TZDate from '@src/time/date';\nimport { makeDateRange, MS_PER_DAY, toEndOfDay, toFormat, toStartOfDay } from '@src/time/datetime';\nimport Collection from '@src/utils/collection';\n\nimport type { CalendarData, EventObject, IDS_OF_DAY } from '@t/events';\nimport type { CalendarInfo } from '@t/options';\n\n/**\n * Make a event collection\n * @returns {Collection<EventModel>} instance\n */\nexport function createEventCollection<T extends EventModel | EventUIModel>(...initItems: T[]) {\n  const collection = new Collection<T>((event) => event.cid());\n\n  if (initItems.length) {\n    collection.add(...initItems);\n  }\n\n  return collection;\n}\n/**\n * Calculate contain dates in event.\n * @param {TZDate} start - start date of range\n * @param {TZDate} end - end date of range\n * @returns {array} contain dates.\n */\nexport function getDateRange(start: TZDate, end: TZDate) {\n  return makeDateRange(toStartOfDay(start), toEndOfDay(end), MS_PER_DAY);\n}\n\nexport function isAllday(event: EventModel) {\n  return (\n    event.isAllday ||\n    (event.category === 'time' && Number(event.end) - Number(event.start) > MS_PER_DAY)\n  );\n}\n\n/**\n * function for group each event models.\n * @type {function}\n * @param {EventUIModel} uiModel - ui model instance\n * @returns {string} group key\n */\nexport function filterByCategory(uiModel: EventUIModel) {\n  const { model } = uiModel;\n\n  if (isAllday(model)) {\n    return 'allday';\n  }\n\n  return model.category;\n}\n\n/****************\n * Events CRUD\n ****************/\n\n/**\n * Set date matrix to supplied event model instance.\n * @param {IDS_OF_DAY} idsOfDay - ids of day\n * @param {EventModel} event - instance of event model.\n */\nexport function addToMatrix(idsOfDay: IDS_OF_DAY, event: EventModel) {\n  const containDates = getDateRange(event.getStarts(), event.getEnds());\n\n  containDates.forEach((date) => {\n    const ymd = toFormat(date, 'YYYYMMDD');\n    const matrix = (idsOfDay[ymd] = idsOfDay[ymd] || []);\n\n    matrix.push(event.cid());\n  });\n}\n\n/**\n * Remove event's id from matrix.\n * @param {IDS_OF_DAY} idsOfDay - ids of day\n * @param {EventModel} event - instance of event model\n */\nexport function removeFromMatrix(idsOfDay: IDS_OF_DAY, event: EventModel) {\n  const modelID = event.cid();\n\n  Object.values(idsOfDay).forEach((ids: number[]) => {\n    const index = ids.indexOf(modelID);\n\n    if (~index) {\n      ids.splice(index, 1);\n    }\n  });\n}\n\nexport function addEvent(calendarData: CalendarData, event: EventModel) {\n  calendarData.events.add(event);\n  addToMatrix(calendarData.idsOfDay, event);\n\n  return event;\n}\n\nexport function createEvent(calendarData: CalendarData, eventData: EventObject) {\n  const event = new EventModel(eventData);\n\n  return addEvent(calendarData, event);\n}\n\nexport function createEvents(calendarData: CalendarData, events: EventObject[] = []) {\n  return events.map((eventData) => createEvent(calendarData, eventData));\n}\n\n/**\n * Update an event.\n * @param {CalendarData} calendarData - data of calendar\n * @param {string} eventId - event id\n * @param {string} calendarId - calendar id\n * @param {EventObject} eventData - event data\n * @returns {boolean} success or failure\n */\nexport function updateEvent(\n  calendarData: CalendarData,\n  eventId: string,\n  calendarId: string,\n  eventData: EventObject\n) {\n  const { idsOfDay } = calendarData;\n  const event = calendarData.events.find((item) => isSameEvent(item, eventId, calendarId));\n\n  if (!event) {\n    return false;\n  }\n\n  event.init({ ...event, ...eventData });\n\n  removeFromMatrix(idsOfDay, event);\n  addToMatrix(idsOfDay, event);\n\n  return true;\n}\n\n/**\n * Delete event instance from controller.\n * @param {CalendarData} calendarData - data of calendar\n * @param {EventModel} event - event model instance to delete\n * @returns {EventModel} deleted model instance.\n */\nexport function deleteEvent(calendarData: CalendarData, event: EventModel) {\n  removeFromMatrix(calendarData.idsOfDay, event);\n  calendarData.events.remove(event);\n\n  return event;\n}\n\nexport function clearEvents(calendarData: CalendarData) {\n  calendarData.idsOfDay = {};\n  calendarData.events.clear();\n}\n\n/**\n * Set calendar list\n * @param {CalendarData} calendarData - data of calendar\n * @param {Array.<Calendar>} calendars - calendar list\n */\nexport function setCalendars(calendarData: CalendarData, calendars: CalendarInfo[]) {\n  calendarData.calendars = calendars;\n}\n\n/**\n * Return events in supplied date range.\n *\n * available only YMD.\n * @param {CalendarData} calendarData - data of calendar\n * @param {{start: TZDate, end: TZDate}} condition - condition of find range\n * @returns {object.<string, Collection>} event collection grouped by dates.\n */\nexport function findByDateRange(\n  calendarData: CalendarData,\n  condition: { start: TZDate; end: TZDate }\n): Record<string, EventModel[]> {\n  const { start, end } = condition;\n  const { events, idsOfDay } = calendarData;\n  const range = getDateRange(start, end);\n  const result: Record<string, EventModel[]> = {};\n  let ids;\n  let ymd;\n  let uiModels: EventModel[];\n\n  range.forEach((date) => {\n    ymd = toFormat(date, 'YYYYMMDD');\n    ids = idsOfDay[ymd];\n    uiModels = result[ymd] = [];\n\n    if (ids && ids.length) {\n      uiModels.push(...ids.map((id) => events.get(id) as EventModel));\n    }\n  });\n\n  return result;\n}\n"
  },
  {
    "path": "apps/calendar/src/controller/column.spec.ts",
    "content": "import { DEFAULT_DUPLICATE_EVENT_CID } from '@src/constants/layout';\nimport {\n  COLLAPSED_DUPLICATE_EVENT_WIDTH_PX,\n  TIME_EVENT_CONTAINER_MARGIN_LEFT,\n} from '@src/constants/style';\nimport { setRenderInfoOfUIModels } from '@src/controller/column';\nimport EventModel from '@src/model/eventModel';\nimport EventUIModel from '@src/model/eventUIModel';\nimport TZDate from '@src/time/date';\nimport type { EventObject, EventObjectWithDefaultValues } from '@src/types/events';\nimport type { CollapseDuplicateEventsOptions } from '@src/types/options';\n\nfunction createEventUIModels(data: EventObject[]): EventUIModel[] {\n  return data.map((datum) => new EventUIModel(new EventModel(datum)));\n}\n\nfunction createCurrentDateWithTime(h: number, m: number) {\n  const today = new TZDate();\n  today.setHours(h, m, 0, 0);\n\n  return today;\n}\n\ndescribe('collapseDuplicateEvents option', () => {\n  let eventUIModels: EventUIModel[];\n\n  beforeEach(() => {\n    eventUIModels = createEventUIModels([\n      {\n        id: '1',\n        calendarId: 'cal1',\n        title: 'duplicate event',\n        start: createCurrentDateWithTime(3, 0),\n        end: createCurrentDateWithTime(4, 0),\n      },\n      {\n        id: '2',\n        calendarId: 'cal2',\n        title: 'duplicate event',\n        start: createCurrentDateWithTime(3, 0),\n        end: createCurrentDateWithTime(4, 0),\n        goingDuration: 60,\n      },\n      {\n        id: '3',\n        calendarId: 'cal3',\n        title: 'duplicate event',\n        start: createCurrentDateWithTime(3, 0),\n        end: createCurrentDateWithTime(4, 0),\n        goingDuration: 30,\n        comingDuration: 60,\n      },\n    ]);\n  });\n\n  const startColumnTime = createCurrentDateWithTime(0, 0);\n  const endColumnTime = createCurrentDateWithTime(24, 0);\n\n  it('when it is false, duplicate events have the same widths.', () => {\n    // Given\n    // nothing\n\n    // When\n    const result = setRenderInfoOfUIModels(\n      eventUIModels,\n      startColumnTime,\n      endColumnTime,\n      DEFAULT_DUPLICATE_EVENT_CID\n    );\n\n    // Then\n    result.forEach((uiModel) => {\n      expect(uiModel.width).toBeCloseTo(100 / result.length, 0);\n    });\n  });\n\n  it('when it sets, the main event is expanded and the others are collapsed.', () => {\n    // Given\n    const mainEventId = eventUIModels[0].model.id;\n    const collapseDuplicateEventsOptions: CollapseDuplicateEventsOptions = {\n      getDuplicateEvents(targetEvent, events) {\n        return events\n          .filter((event) => event.title === targetEvent.title)\n          .sort((a, b) => (a.id > b.id ? 1 : -1));\n      },\n      getMainEvent(events) {\n        return events.find((event) => event.id === mainEventId) as EventObjectWithDefaultValues;\n      },\n    };\n\n    // When\n    const result = setRenderInfoOfUIModels(\n      eventUIModels,\n      startColumnTime,\n      endColumnTime,\n      DEFAULT_DUPLICATE_EVENT_CID,\n      collapseDuplicateEventsOptions\n    );\n\n    // Then\n    result.forEach((uiModel) => {\n      const expected =\n        uiModel.model.id === mainEventId\n          ? `calc(100% - ${\n              (COLLAPSED_DUPLICATE_EVENT_WIDTH_PX + TIME_EVENT_CONTAINER_MARGIN_LEFT) *\n                (result.length - 1) +\n              TIME_EVENT_CONTAINER_MARGIN_LEFT\n            }px)`\n          : `${COLLAPSED_DUPLICATE_EVENT_WIDTH_PX}px`;\n\n      expect(uiModel.duplicateWidth).toBe(expected);\n    });\n  });\n\n  it('when it sets and one of the duplicate events is selected, the selected event is expanded and the others are collapsed.', () => {\n    // Given\n    const selectedDuplicateEventCid = eventUIModels[1].cid();\n    const mainEventId = eventUIModels[0].model.id;\n    const collapseDuplicateEventsOptions: CollapseDuplicateEventsOptions = {\n      getDuplicateEvents(targetEvent, events) {\n        return events\n          .filter((event) => event.title === targetEvent.title)\n          .sort((a, b) => (a.id > b.id ? 1 : -1));\n      },\n      getMainEvent(events) {\n        return events.find((event) => event.id === mainEventId) as EventObjectWithDefaultValues;\n      },\n    };\n\n    // When\n    const result = setRenderInfoOfUIModels(\n      eventUIModels,\n      startColumnTime,\n      endColumnTime,\n      selectedDuplicateEventCid,\n      collapseDuplicateEventsOptions\n    );\n\n    // Then\n    result.forEach((uiModel) => {\n      const expected =\n        uiModel.cid() === selectedDuplicateEventCid\n          ? `calc(100% - ${\n              (COLLAPSED_DUPLICATE_EVENT_WIDTH_PX + TIME_EVENT_CONTAINER_MARGIN_LEFT) *\n                (result.length - 1) +\n              TIME_EVENT_CONTAINER_MARGIN_LEFT\n            }px)`\n          : `${COLLAPSED_DUPLICATE_EVENT_WIDTH_PX}px`;\n\n      expect(uiModel.duplicateWidth).toBe(expected);\n    });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/controller/column.ts",
    "content": "import { DEFAULT_DUPLICATE_EVENT_CID } from '@src/constants/layout';\nimport {\n  COLLAPSED_DUPLICATE_EVENT_WIDTH_PX,\n  TIME_EVENT_CONTAINER_MARGIN_LEFT,\n} from '@src/constants/style';\nimport { createEventCollection } from '@src/controller/base';\nimport { getCollisionGroup, getMatrices } from '@src/controller/core';\nimport { getTopHeightByTime } from '@src/controller/times';\nimport { extractPercentPx, toPercent, toPx } from '@src/helpers/css';\nimport { isTimeEvent } from '@src/model/eventModel';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport type TZDate from '@src/time/date';\nimport { addMinutes, max, min } from '@src/time/datetime';\nimport type { CollapseDuplicateEventsOptions } from '@src/types/options';\nimport array from '@src/utils/array';\n\nconst MIN_HEIGHT_PERCENT = 1;\n\ninterface RenderInfoOptions {\n  baseWidth: number;\n  columnIndex: number;\n  renderStart: TZDate;\n  renderEnd: TZDate;\n  modelStart: TZDate;\n  modelEnd: TZDate;\n  goingStart: TZDate;\n  comingEnd: TZDate;\n  startColumnTime: TZDate;\n  endColumnTime: TZDate;\n}\n\n/**\n * Filter that get events in supplied date ranges.\n * @param {TZDate} startColumnTime - start date\n * @param {TZDate} endColumnTime - end date\n * @returns {function} event filter function\n */\nexport function isBetween(startColumnTime: TZDate, endColumnTime: TZDate) {\n  return (uiModel: EventUIModel) => {\n    const { goingDuration = 0, comingDuration = 0 } = uiModel.model;\n    const ownStarts = addMinutes(uiModel.getStarts(), -goingDuration);\n    const ownEnds = addMinutes(uiModel.getEnds(), comingDuration);\n\n    return !(ownEnds <= startColumnTime || ownStarts >= endColumnTime);\n  };\n}\n\nfunction setInnerHeights(uiModel: EventUIModel, options: RenderInfoOptions) {\n  const { renderStart, renderEnd, modelStart, modelEnd } = options;\n  const { goingDuration = 0, comingDuration = 0 } = uiModel.model;\n\n  let modelDurationHeight = 100;\n\n  if (goingDuration > 0) {\n    const { height: goingDurationHeight } = getTopHeightByTime(\n      renderStart,\n      modelStart,\n      renderStart,\n      renderEnd\n    );\n    uiModel.goingDurationHeight = goingDurationHeight;\n    modelDurationHeight -= goingDurationHeight;\n  }\n\n  if (comingDuration > 0) {\n    const { height: comingDurationHeight } = getTopHeightByTime(\n      modelEnd,\n      renderEnd,\n      renderStart,\n      renderEnd\n    );\n    uiModel.comingDurationHeight = comingDurationHeight;\n    modelDurationHeight -= comingDurationHeight;\n  }\n\n  uiModel.modelDurationHeight = modelDurationHeight;\n}\n\nfunction setCroppedEdges(uiModel: EventUIModel, options: RenderInfoOptions) {\n  const { goingStart, comingEnd, startColumnTime, endColumnTime } = options;\n\n  if (goingStart < startColumnTime) {\n    uiModel.croppedStart = true;\n  }\n  if (comingEnd > endColumnTime) {\n    uiModel.croppedEnd = true;\n  }\n}\n\nfunction getDuplicateLeft(uiModel: EventUIModel, baseLeft: number) {\n  const { duplicateEvents, duplicateEventIndex } = uiModel;\n\n  const prevEvent = duplicateEvents[duplicateEventIndex - 1];\n  let left: number | string = baseLeft;\n  if (prevEvent) {\n    // duplicateLeft = prevEvent.duplicateLeft + prevEvent.duplicateWidth + marginLeft\n    const { percent: leftPercent, px: leftPx } = extractPercentPx(`${prevEvent.duplicateLeft}`);\n    const { percent: widthPercent, px: widthPx } = extractPercentPx(`${prevEvent.duplicateWidth}`);\n    const percent = leftPercent + widthPercent;\n    const px = leftPx + widthPx + TIME_EVENT_CONTAINER_MARGIN_LEFT;\n\n    if (percent !== 0) {\n      left = `calc(${toPercent(percent)} ${px > 0 ? '+' : '-'} ${toPx(Math.abs(px))})`;\n    } else {\n      left = toPx(px);\n    }\n  } else {\n    left = toPercent(left);\n  }\n\n  return left;\n}\n\nfunction getDuplicateWidth(uiModel: EventUIModel, baseWidth: number) {\n  const { collapse } = uiModel;\n\n  // if it is collapsed, (COLLAPSED_DUPLICATE_EVENT_WIDTH_PX)px\n  // if it is expanded, (baseWidth)% - (other duplicate events' width + marginLeft)px - (its marginLeft)px\n  return collapse\n    ? `${COLLAPSED_DUPLICATE_EVENT_WIDTH_PX}px`\n    : `calc(${toPercent(baseWidth)} - ${toPx(\n        (COLLAPSED_DUPLICATE_EVENT_WIDTH_PX + TIME_EVENT_CONTAINER_MARGIN_LEFT) *\n          (uiModel.duplicateEvents.length - 1) +\n          TIME_EVENT_CONTAINER_MARGIN_LEFT\n      )})`;\n}\n\nfunction setDimension(uiModel: EventUIModel, options: RenderInfoOptions) {\n  const { startColumnTime, endColumnTime, baseWidth, columnIndex, renderStart, renderEnd } =\n    options;\n  const { duplicateEvents } = uiModel;\n  const { top, height } = getTopHeightByTime(\n    renderStart,\n    renderEnd,\n    startColumnTime,\n    endColumnTime\n  );\n  const dimension = {\n    top,\n    left: baseWidth * columnIndex,\n    width: baseWidth,\n    height: Math.max(MIN_HEIGHT_PERCENT, height),\n    duplicateLeft: '',\n    duplicateWidth: '',\n  };\n\n  if (duplicateEvents.length > 0) {\n    dimension.duplicateLeft = getDuplicateLeft(uiModel, dimension.left);\n    dimension.duplicateWidth = getDuplicateWidth(uiModel, dimension.width);\n  }\n\n  uiModel.setUIProps(dimension);\n}\n\nfunction getRenderInfoOptions(\n  uiModel: EventUIModel,\n  columnIndex: number,\n  baseWidth: number,\n  startColumnTime: TZDate,\n  endColumnTime: TZDate\n) {\n  const { goingDuration = 0, comingDuration = 0 } = uiModel.model;\n  const modelStart = uiModel.getStarts();\n  const modelEnd = uiModel.getEnds();\n  const goingStart = addMinutes(modelStart, -goingDuration);\n  const comingEnd = addMinutes(modelEnd, comingDuration);\n  const renderStart = max(goingStart, startColumnTime);\n  const renderEnd = min(comingEnd, endColumnTime);\n\n  return {\n    baseWidth,\n    columnIndex,\n    modelStart,\n    modelEnd,\n    renderStart,\n    renderEnd,\n    goingStart,\n    comingEnd,\n    startColumnTime,\n    endColumnTime,\n    duplicateEvents: uiModel.duplicateEvents,\n  };\n}\n\nfunction setRenderInfo({\n  uiModel,\n  columnIndex,\n  baseWidth,\n  startColumnTime,\n  endColumnTime,\n  isDuplicateEvent = false,\n}: {\n  uiModel: EventUIModel;\n  columnIndex: number;\n  baseWidth: number;\n  startColumnTime: TZDate;\n  endColumnTime: TZDate;\n  isDuplicateEvent?: boolean;\n}) {\n  if (!isDuplicateEvent && uiModel.duplicateEvents.length > 0) {\n    uiModel.duplicateEvents.forEach((event) => {\n      setRenderInfo({\n        uiModel: event,\n        columnIndex,\n        baseWidth,\n        startColumnTime,\n        endColumnTime,\n        isDuplicateEvent: true,\n      });\n    });\n\n    return;\n  }\n\n  const renderInfoOptions = getRenderInfoOptions(\n    uiModel,\n    columnIndex,\n    baseWidth,\n    startColumnTime,\n    endColumnTime\n  );\n\n  setDimension(uiModel, renderInfoOptions);\n  setInnerHeights(uiModel, renderInfoOptions);\n  setCroppedEdges(uiModel, renderInfoOptions);\n}\n\nfunction setDuplicateEvents(\n  uiModels: EventUIModel[],\n  options: CollapseDuplicateEventsOptions,\n  selectedDuplicateEventCid: number\n) {\n  const { getDuplicateEvents, getMainEvent } = options;\n\n  const eventObjects = uiModels.map((uiModel) => uiModel.model.toEventObject());\n\n  uiModels.forEach((targetUIModel) => {\n    if (targetUIModel.collapse || targetUIModel.duplicateEvents.length > 0) {\n      return;\n    }\n\n    const duplicateEvents = getDuplicateEvents(targetUIModel.model.toEventObject(), eventObjects);\n\n    if (duplicateEvents.length <= 1) {\n      return;\n    }\n\n    const mainEvent = getMainEvent(duplicateEvents);\n\n    const duplicateEventUIModels = duplicateEvents.map(\n      (event) => uiModels.find((uiModel) => uiModel.cid() === event.__cid) as EventUIModel\n    );\n    const isSelectedGroup = !!(\n      selectedDuplicateEventCid > DEFAULT_DUPLICATE_EVENT_CID &&\n      duplicateEvents.find((event) => event.__cid === selectedDuplicateEventCid)\n    );\n    const duplicateStarts = duplicateEvents.reduce((acc, { start, goingDuration }) => {\n      const renderStart = addMinutes(start, -goingDuration);\n      return min(acc, renderStart);\n    }, duplicateEvents[0].start);\n    const duplicateEnds = duplicateEvents.reduce((acc, { end, comingDuration }) => {\n      const renderEnd = addMinutes(end, comingDuration);\n      return max(acc, renderEnd);\n    }, duplicateEvents[0].end);\n\n    duplicateEventUIModels.forEach((event, index) => {\n      const isMain = event.cid() === mainEvent.__cid;\n      const collapse = !(\n        (isSelectedGroup && event.cid() === selectedDuplicateEventCid) ||\n        (!isSelectedGroup && isMain)\n      );\n\n      event.setUIProps({\n        duplicateEvents: duplicateEventUIModels,\n        duplicateEventIndex: index,\n        collapse,\n        isMain,\n        duplicateStarts,\n        duplicateEnds,\n      });\n    });\n  });\n\n  return uiModels;\n}\n\n/**\n * Convert to EventUIModel and make rendering information of events\n * @param {EventUIModel[]} events - event list\n * @param {TZDate} startColumnTime - start date\n * @param {TZDate} endColumnTime - end date\n */\nexport function setRenderInfoOfUIModels(\n  events: EventUIModel[],\n  startColumnTime: TZDate,\n  endColumnTime: TZDate,\n  selectedDuplicateEventCid: number,\n  collapseDuplicateEventsOptions?: CollapseDuplicateEventsOptions\n) {\n  const uiModels: EventUIModel[] = events\n    .filter(isTimeEvent)\n    .filter(isBetween(startColumnTime, endColumnTime))\n    .sort(array.compare.event.asc);\n\n  if (collapseDuplicateEventsOptions) {\n    setDuplicateEvents(uiModels, collapseDuplicateEventsOptions, selectedDuplicateEventCid);\n  }\n  const expandedEvents = uiModels.filter((uiModel) => !uiModel.collapse);\n\n  const uiModelColl = createEventCollection(...expandedEvents);\n  const usingTravelTime = true;\n  const collisionGroups = getCollisionGroup(expandedEvents, usingTravelTime);\n  const matrices = getMatrices(uiModelColl, collisionGroups, usingTravelTime);\n\n  matrices.forEach((matrix) => {\n    const maxRowLength = Math.max(...matrix.map((row) => row.length));\n    const baseWidth = Math.round(100 / maxRowLength);\n\n    matrix.forEach((row) => {\n      row.forEach((uiModel, columnIndex) => {\n        setRenderInfo({ uiModel, columnIndex, baseWidth, startColumnTime, endColumnTime });\n      });\n    });\n  });\n\n  return uiModels;\n}\n"
  },
  {
    "path": "apps/calendar/src/controller/core.spec.ts",
    "content": "import {\n  getCollisionGroup,\n  getEventInDateRangeFilter,\n  getLastRowInColumn,\n  getMatrices,\n  limitRenderRange,\n} from '@src/controller/core';\nimport EventModel from '@src/model/eventModel';\nimport EventUIModel from '@src/model/eventUIModel';\nimport TZDate from '@src/time/date';\nimport array from '@src/utils/array';\nimport Collection from '@src/utils/collection';\n\nimport type { CollisionGroup, EventObject } from '@t/events';\n\ndescribe('Base.Core', () => {\n  let mockData: EventObject[];\n  let eventList: EventModel[];\n  let expected;\n  let actual;\n\n  beforeEach(() => {\n    mockData = [\n      {\n        title: 'A',\n        isAllday: false,\n        start: '2015-05-01T10:20:00',\n        end: '2015-05-01T10:40:00',\n        category: 'time',\n      },\n      {\n        title: 'B',\n        isAllday: false,\n        start: '2015-05-01T10:30:00',\n        end: '2015-05-01T11:30:00',\n        category: 'time',\n      },\n      {\n        title: 'C',\n        isAllday: false,\n        start: '2015-05-01T11:20:00',\n        end: '2015-05-01T12:00:00',\n        category: 'time',\n      },\n      {\n        title: 'D',\n        isAllday: false,\n        start: '2015-05-01T10:50:00',\n        end: '2015-05-01T11:10:00',\n        category: 'time',\n      },\n      {\n        title: 'E',\n        isAllday: false,\n        start: '2015-05-01T13:20:00',\n        end: '2015-05-01T13:40:00',\n        category: 'time',\n      },\n      {\n        title: 'F',\n        isAllday: false,\n        start: '2015-05-01T14:00:00',\n        end: '2015-05-01T14:20:00',\n        category: 'time',\n      },\n      {\n        title: 'G',\n        isAllday: false,\n        start: '2015-05-01T14:10:00',\n        end: '2015-05-01T14:20:00',\n        category: 'time',\n      },\n      {\n        title: 'H',\n        isAllday: false,\n        start: '2015-05-01T16:00:00',\n        end: '2015-05-01T18:00:00',\n        category: 'time',\n      },\n      {\n        title: 'I',\n        isAllday: false,\n        start: '2015-05-01T17:00:00',\n        end: '2015-05-01T20:00:00',\n        category: 'time',\n      },\n      {\n        title: 'J',\n        isAllday: false,\n        start: '2015-05-01T19:00:00',\n        end: '2015-05-01T21:00:00',\n        category: 'time',\n      },\n      {\n        title: '물고기 밥주기',\n        isAllday: false,\n        start: '2015-05-01T22:00:00',\n        end: '2015-05-01T22:10:00',\n        category: 'time',\n      },\n    ];\n    eventList = mockData.map((data) => new EventModel(data)).sort(array.compare.event.asc);\n  });\n\n  describe('getCollisionGroup()', () => {\n    it('Get collision group properly.', () => {\n      actual = getCollisionGroup(eventList);\n      expected = [\n        [eventList[0].cid(), eventList[1].cid(), eventList[2].cid(), eventList[3].cid()],\n        [eventList[4].cid()],\n        [eventList[5].cid(), eventList[6].cid()],\n        [eventList[7].cid(), eventList[8].cid(), eventList[9].cid()],\n        [eventList[10].cid()],\n      ];\n\n      expect(actual).toEqual(expected);\n    });\n\n    describe('When calculating the collision, it is affected by the travel time.', () => {\n      let collisionEventList: EventModel[];\n\n      beforeEach(() => {\n        const events: EventObject[] = [\n          {\n            title: 'A',\n            isAllday: false,\n            start: '2015-05-01T10:20:00',\n            end: '2015-05-01T10:40:00',\n            category: 'time',\n            goingDuration: 0,\n            comingDuration: 20,\n          },\n          {\n            title: 'B',\n            isAllday: false,\n            start: '2015-05-01T10:40:00',\n            end: '2015-05-01T11:50:00',\n            category: 'time',\n            goingDuration: 10,\n            comingDuration: 10,\n          },\n          {\n            title: 'C',\n            isAllday: false,\n            start: '2015-05-01T11:00:00',\n            end: '2015-05-01T12:00:00',\n            category: 'time',\n            goingDuration: 30,\n            comingDuration: 30,\n          },\n          {\n            title: 'D',\n            isAllday: false,\n            start: '2015-05-01T12:00:00',\n            end: '2015-05-01T13:00:00',\n            category: 'time',\n            goingDuration: 10,\n            comingDuration: 10,\n          },\n        ];\n\n        collisionEventList = events\n          .map((data) => new EventModel(data))\n          .sort(array.compare.event.asc);\n      });\n\n      it('should get collision group properly with travel time.', () => {\n        expect(getCollisionGroup(collisionEventList, true)).toEqual([\n          [\n            collisionEventList[0].cid(),\n            collisionEventList[1].cid(),\n            collisionEventList[2].cid(),\n            collisionEventList[3].cid(),\n          ],\n        ]);\n      });\n\n      it('should get collision group properly without travel time.', () => {\n        expect(getCollisionGroup(collisionEventList, false)).toEqual([\n          [collisionEventList[0].cid()],\n          [collisionEventList[1].cid(), collisionEventList[2].cid()],\n          [collisionEventList[3].cid()],\n        ]);\n      });\n    });\n  });\n\n  describe('getLastRowInColumn()', () => {\n    let test: Array<Array<number | undefined>>;\n\n    beforeEach(() => {\n      test = [\n        [1, 1, 1],\n        // eslint-disable-next-line no-undefined\n        [1, undefined, 3],\n        // eslint-disable-next-line no-undefined\n        [4, undefined, undefined],\n      ];\n    });\n\n    it('return -1 when column not exist.', () => {\n      const result = getLastRowInColumn(test, 4);\n\n      expect(result).toBe(-1);\n    });\n\n    it('can calculate last row in column in 2d array.', () => {\n      const result = getLastRowInColumn(test, 0);\n\n      expect(result).toBe(2);\n    });\n  });\n\n  describe('getMatrices()', () => {\n    let collection: Collection<EventModel>;\n    let collisionGroup: CollisionGroup;\n\n    beforeEach(() => {\n      collection = new Collection<EventModel>((model) => {\n        return model.cid();\n      });\n    });\n\n    it('can calculate matrices accurately.', () => {\n      collection.add(...eventList);\n      collisionGroup = getCollisionGroup(eventList);\n\n      expected = [\n        [[eventList[0], eventList[1]], [eventList[2]], [eventList[3]]],\n        [[eventList[4]]],\n        [[eventList[5], eventList[6]]],\n        [[eventList[7], eventList[8]], [eventList[9]]],\n        [[eventList[10]]],\n      ];\n      actual = getMatrices(collection, collisionGroup);\n\n      expect(actual).toEqual(expected);\n    });\n\n    describe('When calculating matrices, it is affected by the travel time.', () => {\n      let matrixEventList: EventModel[];\n\n      beforeEach(() => {\n        const events: EventObject[] = [\n          {\n            title: 'A',\n            isAllday: false,\n            start: '2015-05-01T10:20:00',\n            end: '2015-05-01T10:40:00',\n            category: 'time',\n            goingDuration: 0,\n            comingDuration: 20,\n          },\n          {\n            title: 'B',\n            isAllday: false,\n            start: '2015-05-01T10:40:00',\n            end: '2015-05-01T11:50:00',\n            category: 'time',\n            goingDuration: 10,\n            comingDuration: 10,\n          },\n          {\n            title: 'C',\n            isAllday: false,\n            start: '2015-05-01T11:00:00',\n            end: '2015-05-01T12:00:00',\n            category: 'time',\n            goingDuration: 30,\n            comingDuration: 30,\n          },\n          {\n            title: 'D',\n            isAllday: false,\n            start: '2015-05-01T12:00:00',\n            end: '2015-05-01T13:00:00',\n            category: 'time',\n            goingDuration: 10,\n            comingDuration: 10,\n          },\n        ];\n\n        matrixEventList = events.map((data) => new EventModel(data)).sort(array.compare.event.asc);\n        collection.add(...matrixEventList);\n      });\n\n      afterEach(() => {\n        collection.clear();\n      });\n\n      it('can calculate matrices accurately with travel time', () => {\n        const usingTravelTime = true;\n        collisionGroup = getCollisionGroup(matrixEventList, usingTravelTime);\n\n        expect(getMatrices(collection, collisionGroup, usingTravelTime)).toEqual([\n          [[matrixEventList[0], matrixEventList[1], matrixEventList[2]], [matrixEventList[3]]],\n        ]);\n      });\n\n      it('can calculate matrices accurately without travel time', () => {\n        const usingTravelTime = false;\n        collisionGroup = getCollisionGroup(matrixEventList, usingTravelTime);\n\n        expect(getMatrices(collection, collisionGroup, usingTravelTime)).toEqual([\n          [[matrixEventList[0]]],\n          [[matrixEventList[1], matrixEventList[2]]],\n          [[matrixEventList[3]]],\n        ]);\n      });\n    });\n  });\n\n  describe('limitRenderRange', () => {\n    let uiModelCollection: Collection<EventUIModel>;\n\n    beforeEach(() => {\n      uiModelCollection = new Collection((uiModel) => {\n        return uiModel.cid();\n      });\n    });\n\n    it('fill renderStarts, renderEnds to each ui model in collection.', () => {\n      // 5/1 10:20 ~ 5/1 10:40\n      uiModelCollection.add(new EventUIModel(eventList[0]));\n\n      const limit1 = new TZDate('2015-05-01T10:30:00');\n      const limit2 = new TZDate('2015-05-01T10:40:00');\n\n      limitRenderRange(limit1, limit2, uiModelCollection);\n\n      const uiModel = uiModelCollection.getFirstItem();\n\n      expect(uiModel).not.toBeNull();\n\n      if (uiModel) {\n        expect(uiModel.renderStarts).toEqual(limit1);\n        expect(uiModel.renderEnds).toBeUndefined();\n      }\n    });\n  });\n\n  describe('getEventInDateRangeFilter', () => {\n    let uiModelCollection: Collection<EventUIModel>;\n\n    beforeEach(() => {\n      uiModelCollection = new Collection((uiModel) => {\n        return uiModel.cid();\n      });\n    });\n\n    it('filter events properly.', () => {\n      let filterFn;\n      let d1;\n      let d2;\n\n      //                     start ------------- end\n      // A ownStart - ownEnd\n      // B ownStart -------- ownEnd\n      // C ownStart ------------------ ownEnd\n      // D                   ownSta -- ownEnd\n      // E                           ownS - ownE\n      // F                            ownSta --- ownEn\n      // G                             ownStart ------------------- ownEnd\n      // H                                        ownS ------------ ownEnd\n      // I                                               ownStart - ownEnd\n      // L ownStart ----------------------------------------------- ownEnd\n\n      // 10:20 ~ 10:40\n      uiModelCollection.add(new EventUIModel(eventList[0]));\n\n      // A: 09:30 ~ 10:10\n      d1 = new TZDate('2015-05-01T09:30:00');\n      d2 = new TZDate('2015-05-01T10:10:00');\n      filterFn = getEventInDateRangeFilter(d1, d2);\n\n      expect(uiModelCollection.filter(filterFn).size).toBe(0);\n\n      // B: 09:30 ~ 10:20\n      d1 = new TZDate('2015-05-01T09:30:00');\n      d2 = new TZDate('2015-05-01T10:20:00');\n      filterFn = getEventInDateRangeFilter(d1, d2);\n\n      expect(uiModelCollection.filter(filterFn).size).toBe(1);\n\n      // C: 09:30 ~ 10:30\n      d1 = new TZDate('2015-05-01T09:30:00');\n      d2 = new TZDate('2015-05-01T10:30:00');\n      filterFn = getEventInDateRangeFilter(d1, d2);\n\n      expect(uiModelCollection.filter(filterFn).size).toBe(1);\n\n      // D: 10:20 ~ 10:30\n      d1 = new TZDate('2015-05-01T10:20:00');\n      d2 = new TZDate('2015-05-01T10:30:00');\n      filterFn = getEventInDateRangeFilter(d1, d2);\n\n      expect(uiModelCollection.filter(filterFn).size).toBe(1);\n\n      // E: 10:25 ~ 10:35\n      d1 = new TZDate('2015-05-01T10:25:00');\n      d2 = new TZDate('2015-05-01T10:35:00');\n      filterFn = getEventInDateRangeFilter(d1, d2);\n\n      expect(uiModelCollection.filter(filterFn).size).toBe(1);\n\n      // F: 10:30 ~ 10:40\n      d1 = new TZDate('2015-05-01T10:30:00');\n      d2 = new TZDate('2015-05-01T10:40:00');\n      filterFn = getEventInDateRangeFilter(d1, d2);\n\n      expect(uiModelCollection.filter(filterFn).size).toBe(1);\n\n      // G: 10:30 ~ 10:50\n      d1 = new TZDate('2015-05-01T10:30:00');\n      d2 = new TZDate('2015-05-01T10:50:00');\n      filterFn = getEventInDateRangeFilter(d1, d2);\n\n      expect(uiModelCollection.filter(filterFn).size).toBe(1);\n\n      // H: 10:40 ~ 10:50\n      d1 = new TZDate('2015-05-01T10:40:00');\n      d2 = new TZDate('2015-05-01T10:50:00');\n      filterFn = getEventInDateRangeFilter(d1, d2);\n\n      expect(uiModelCollection.filter(filterFn).size).toBe(1);\n\n      // I: 10:50 ~ 10:55\n      d1 = new TZDate('2015-05-01T10:50:00');\n      d2 = new TZDate('2015-05-01T10:55:00');\n      filterFn = getEventInDateRangeFilter(d1, d2);\n\n      expect(uiModelCollection.filter(filterFn).size).toBe(0);\n\n      // L: 10:10 ~ 10:50\n      d1 = new TZDate('2015-05-01T10:10:00');\n      d2 = new TZDate('2015-05-01T10:50:00');\n      filterFn = getEventInDateRangeFilter(d1, d2);\n\n      expect(uiModelCollection.filter(filterFn).size).toBe(1);\n    });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/controller/core.ts",
    "content": "import type EventModel from '@src/model/eventModel';\nimport EventUIModel from '@src/model/eventUIModel';\nimport TZDate from '@src/time/date';\nimport { makeDateRange, MS_PER_DAY, toEndOfDay, toFormat, toStartOfDay } from '@src/time/datetime';\nimport type { Filter } from '@src/utils/collection';\nimport Collection from '@src/utils/collection';\nimport { isUndefined } from '@src/utils/type';\n\nimport type { CollisionGroup, Matrix, Matrix3d } from '@t/events';\n\n/**\n * Calculate collision group.\n * @param {Array<EventModel|EventUIModel>} events list of ui models.\n * @param {boolean} [usingTravelTime = true]\n * @returns {Array<number[]>} Collision Group.\n */\nexport function getCollisionGroup<Events extends EventModel | EventUIModel>(\n  events: Events[],\n  usingTravelTime = true\n) {\n  const collisionGroups: CollisionGroup = [];\n  let previousEventList: Array<Events>;\n\n  if (!events.length) {\n    return collisionGroups;\n  }\n\n  collisionGroups[0] = [events[0].cid()];\n  events.slice(1).forEach((event: Events, index: number) => {\n    previousEventList = events.slice(0, index + 1).reverse();\n\n    // If overlapping previous events, find a Collision Group of overlapping events and add this events\n    const found = previousEventList.find((previous: Events) =>\n      event.collidesWith(previous, usingTravelTime)\n    );\n\n    if (!found) {\n      // This event is a event that does not overlap with the previous event, so a new Collision Group is constructed.\n      collisionGroups.push([event.cid()]);\n    } else {\n      collisionGroups\n        .slice()\n        .reverse()\n        .some((group) => {\n          if (~group.indexOf(found.cid())) {\n            // If you find a previous event that overlaps, include it in the Collision Group to which it belongs.\n            group.push(event.cid());\n\n            return true; // returning true can stop this loop\n          }\n\n          return false;\n        });\n    }\n  });\n\n  return collisionGroups;\n}\n\n/**\n * Get row length by column index in 2d matrix.\n * @param {array[]} matrix Matrix\n * @param {number} col Column index.\n * @returns {number} Last row number in column or -1\n */\nexport function getLastRowInColumn(matrix: Array<any[]>, col: number) {\n  let { length: row } = matrix;\n\n  while (row > 0) {\n    row -= 1;\n    if (!isUndefined(matrix[row][col])) {\n      return row;\n    }\n  }\n\n  return -1;\n}\n\n/**\n * Calculate matrix for appointment block element placing.\n * @param {Collection} collection model collection.\n * @param {Array<number[]>} collisionGroups Collision groups for event set.\n * @param {boolean} [usingTravelTime = true]\n * @returns {array} matrices\n */\nexport function getMatrices<T extends EventModel | EventUIModel>(\n  collection: Collection<T>,\n  collisionGroups: CollisionGroup,\n  usingTravelTime = true\n): Matrix3d<T> {\n  const result: Matrix3d<T> = [];\n\n  collisionGroups.forEach((group) => {\n    const matrix: Matrix<T> = [[]];\n\n    group.forEach((eventID) => {\n      const event = collection.get(eventID) as T;\n      let col = 0;\n      let found = false;\n      let nextRow;\n      let lastRowInColumn;\n\n      while (!found) {\n        lastRowInColumn = getLastRowInColumn(matrix, col);\n\n        if (lastRowInColumn === -1) {\n          matrix[0].push(event);\n          found = true;\n        } else if (!event.collidesWith(matrix[lastRowInColumn][col], usingTravelTime)) {\n          nextRow = lastRowInColumn + 1;\n          if (isUndefined(matrix[nextRow])) {\n            matrix[nextRow] = [];\n          }\n          matrix[nextRow][col] = event;\n          found = true;\n        }\n\n        col += 1;\n      }\n    });\n\n    result.push(matrix);\n  });\n\n  return result;\n}\n\n/**\n * Filter that get event model in supplied date ranges.\n * @param {TZDate} start - start date\n * @param {TZDate} end - end date\n * @returns {function} event filter function\n */\nexport function getEventInDateRangeFilter(\n  start: TZDate,\n  end: TZDate\n): Filter<EventModel | EventUIModel> {\n  return (model) => {\n    const ownStarts = model.getStarts();\n    const ownEnds = model.getEnds();\n\n    // shorthand condition of\n    //\n    // (ownStarts >= start && ownEnds <= end) ||\n    // (ownStarts < start && ownEnds >= start) ||\n    // (ownEnds > end && ownStarts <= end)\n    return !(ownEnds < start || ownStarts > end);\n  };\n}\n\n/**\n * Position each ui model for placing into container\n * @param {TZDate} start - start date to render\n * @param {TZDate} end - end date to render\n * @param {Matrix3d} matrices - matrices from controller\n * @param {function} [iteratee] - iteratee function invoke each ui models\n */\nexport function positionUIModels(\n  start: TZDate,\n  end: TZDate,\n  matrices: Matrix3d<EventUIModel>,\n  iteratee?: (uiModel: EventUIModel) => void\n) {\n  const ymdListToRender = makeDateRange(start, end, MS_PER_DAY).map((date) =>\n    toFormat(date, 'YYYYMMDD')\n  );\n\n  matrices.forEach((matrix) => {\n    matrix.forEach((column) => {\n      column.forEach((uiModel, index) => {\n        if (!uiModel) {\n          return;\n        }\n\n        const ymd = toFormat(uiModel.getStarts(), 'YYYYMMDD');\n        const dateLength = makeDateRange(\n          toStartOfDay(uiModel.getStarts()),\n          toEndOfDay(uiModel.getEnds()),\n          MS_PER_DAY\n        ).length;\n\n        uiModel.top = index;\n        uiModel.left = ymdListToRender.indexOf(ymd);\n        uiModel.width = dateLength;\n\n        iteratee?.(uiModel);\n      });\n    });\n  });\n}\n\n/**\n * Limit render range for ui models\n * @param {TZDate} start\n * @param {TZDate} end\n * @param {EventUIModel} uiModel - ui model instance\n * @returns {EventUIModel} ui model that limited render range\n */\nfunction limit(start: TZDate, end: TZDate, uiModel: EventUIModel) {\n  if (uiModel.getStarts() < start) {\n    uiModel.exceedLeft = true;\n    uiModel.renderStarts = new TZDate(start);\n  }\n\n  if (uiModel.getEnds() > end) {\n    uiModel.exceedRight = true;\n    uiModel.renderEnds = new TZDate(end);\n  }\n\n  return uiModel;\n}\n\n/**\n * Limit start, end date each ui model for render properly\n * @param {TZDate} start - start date to render\n * @param {TZDate} end - end date to render\n * @param {Collection<EventUIModel>|EventUIModel} uiModelColl - collection of EventUIModel or EventUIModel\n * @returns {?EventUIModel} return ui model when third parameter is\n *  ui model\n */\nexport function limitRenderRange(\n  start: TZDate,\n  end: TZDate,\n  uiModelColl: Collection<EventUIModel> | EventUIModel\n) {\n  if (uiModelColl instanceof Collection) {\n    uiModelColl.each((uiModel) => {\n      limit(start, end, uiModel);\n\n      return true;\n    });\n\n    return null;\n  }\n\n  return limit(start, end, uiModelColl);\n}\n\n/**\n * Convert event model collection to ui model collection.\n * @param {Collection} eventCollection - collection of event model\n * @returns {Collection} collection of event ui model\n */\nexport function convertToUIModel(eventCollection: Collection<EventModel>) {\n  const uiModelColl = new Collection<EventUIModel>((uiModel) => {\n    return uiModel.cid();\n  });\n\n  eventCollection.each(function (event) {\n    uiModelColl.add(new EventUIModel(event));\n  });\n\n  return uiModelColl;\n}\n"
  },
  {
    "path": "apps/calendar/src/controller/month.spec.ts",
    "content": "import { addEvent, createEventCollection } from '@src/controller/base';\nimport { findByDateRange } from '@src/controller/month';\nimport EventModel from '@src/model/eventModel';\nimport TZDate from '@src/time/date';\nimport array from '@src/utils/array';\n\nimport type { CalendarData, EventObject } from '@t/events';\n\ndescribe('Base.Month', () => {\n  // eslint-disable-next-line no-undefined\n  const undef = undefined;\n  let calendarData: CalendarData;\n  let mockData: EventObject[];\n  let eventList: EventModel[];\n  let actual;\n\n  beforeEach(() => {\n    calendarData = {\n      calendars: [],\n      events: createEventCollection(),\n      idsOfDay: {},\n    };\n\n    mockData = [\n      {\n        title: '[김동우] 휴가',\n        isAllday: true,\n        start: '2015-11-15T00:00:00',\n        end: '2015-11-21T23:59:59',\n      },\n      {\n        title: '김태용[일본출장]',\n        isAllday: true,\n        start: '2015-11-16T00:00:00',\n        end: '2015-11-18T23:59:59',\n      },\n      {\n        title: '[류진경] 오전반차',\n        isAllday: true,\n        start: '2015-11-16T00:00:00',\n        end: '2015-11-16T23:59:59',\n      },\n      {\n        title: '[일본 출장] 김지해, 장세영',\n        isAllday: true,\n        start: '2015-11-17T00:00:00',\n        end: '2015-11-20T23:59:59',\n      },\n      {\n        title: 'FE 스크럼 - 1',\n        isAllday: false,\n        start: '2015-11-16T09:40:00',\n        end: '2015-11-16T10:00:00',\n      },\n      {\n        title: '[김성호] 연차',\n        isAllday: true,\n        start: '2015-11-17T00:00:00',\n        end: '2015-11-17T23:59:59',\n      },\n      {\n        title: 'FE 스크럼 - 2',\n        isAllday: false,\n        start: '2015-11-17T09:40:00',\n        end: '2015-11-17T10:00:00',\n      },\n      {\n        title: '팀 스터디 - A',\n        isAllday: false,\n        start: '2015-11-17T12:30:00',\n        end: '2015-11-17T13:00:00',\n      },\n      {\n        title: 'FE 스크럼 - 3',\n        isAllday: false,\n        start: '2015-11-18T09:40:00',\n        end: '2015-11-18T10:00:00',\n      },\n      {\n        title: '[코드리뷰] 콤보차트 리펙토링',\n        isAllday: false,\n        start: '2015-11-18T10:00:00',\n        end: '2015-11-18T11:00:00',\n      },\n      {\n        title: 'FE 스크럼 - 4',\n        isAllday: false,\n        start: '2015-11-19T09:40:00',\n        end: '2015-11-19T10:00:00',\n      },\n      {\n        title: '팀 스터디 - B',\n        isAllday: false,\n        start: '2015-11-19T12:00:00',\n        end: '2015-11-19T13:00:00',\n      },\n      {\n        title: '캘린더이야기',\n        isAllday: false,\n        start: '2015-11-19T12:30:00',\n        end: '2015-11-19T13:00:00',\n      },\n      {\n        title: 'FE 스크럼 - 5',\n        isAllday: false,\n        start: '2015-11-20T09:40:00',\n        end: '2015-11-20T10:00:00',\n      },\n      {\n        title: '주간보고 작성',\n        isAllday: false,\n        start: '2015-11-20T14:00:00',\n        end: '2015-11-20T15:00:00',\n      },\n    ];\n    // mock schedule list\n    eventList = mockData.map((data) => new EventModel(data)).sort(array.compare.event.asc);\n  });\n\n  describe('findByDateRange()', () => {\n    beforeEach(() => {\n      // add schedule instance to controller\n      eventList.forEach((event) => {\n        addEvent(calendarData, event);\n      });\n    });\n\n    it('get events in month', () => {\n      const start = new TZDate(2015, 10, 1);\n      const end = new TZDate(2015, 10, 30);\n\n      /**\n       * |15        |16        |17        |18        |19        |20        |21        |\n       * |<<<<[김동우] 휴가>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>|\n       * |          |<<<<김태용[오전반차]>>>>>>>>>>>>|          |          |          |\n       * |          |<<[류진경]>>|<<<<[일본 출장] 김지해, 장세영>>>>>>>>>>>>>|          |\n       * |          |<FE스크-1>|<<[김성호]>>|<FE스크-3>|<FE스크-4>|<FE스크-5>|          |\n       * |          |          |<FE스크-2>|<[코드리]>|<팀스터디>|<주간보고>|          |\n       * |          |          |<팀스터디>|          |<캘린더이>|          |          |\n       */\n      const expectedMatrix = [\n        ['[김동우] 휴가', '김태용[일본출장]', '[류진경] 오전반차', '[김성호] 연차'],\n        [undef, 'FE 스크럼 - 4', '[일본 출장] 김지해, 장세영'],\n        [undef, '팀 스터디 - B', 'FE 스크럼 - 1'],\n        [undef, 'FE 스크럼 - 5', 'FE 스크럼 - 2'],\n        [undef, '주간보고 작성', '팀 스터디 - A'],\n        [undef, undef, 'FE 스크럼 - 3'],\n        [undef, undef, '[코드리뷰] 콤보차트 리펙토링'],\n        [undef, undef, '캘린더이야기'],\n      ];\n\n      const expectedTop = [\n        [1, 2, 3, 4],\n        [undef, 4, 3],\n        [undef, 5, 4],\n        [undef, 4, 5],\n        [undef, 5, 6],\n        [undef, undef, 4],\n        [undef, undef, 5],\n        [undef, undef, 6],\n      ];\n\n      actual = findByDateRange(calendarData, { start, end, andFilters: [], alldayFirstMode: true });\n\n      expect(actual[0]).toEqualMatricesTitle(expectedMatrix);\n      expect(actual[0]).toEqualMatricesTop(expectedTop);\n    });\n  });\n\n  describe('findByDateRange() by stacking time and all-day event', () => {\n    beforeEach(() => {\n      // add schedule instance to controller\n      eventList.forEach((event) => {\n        addEvent(calendarData, event);\n      });\n    });\n\n    it('get events in month for all-day event', () => {\n      const start = new TZDate(2015, 10, 1);\n      const end = new TZDate(2015, 10, 30);\n\n      /**\n       * |15        |16        |17        |18        |19        |20        |21        |\n       * |<<<<[김동우] 휴가>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>|\n       * |          |<<<<김태용[일본출장]    >>>>>>>>>>>>| <FE스크-4>| <FE스크-5>|          |\n       * |          |<<[류진경]>>|<<<<[일본 출장] 김지해, 장세영>>>>>>>>>>>>>>>>>>|          |\n       * |          | <FE스크-1>|<<[김성호]>>| <FE스크-3>|  <팀스터디>|  <주간보고>|          |\n       * |          |          |  <FE스크-2>| <[코드리]>|  <캘린더이>|          |          |\n       * |          |          |   <팀스터디>|         |          |          |          |\n       */\n      const expectedMatrix = [\n        ['[김동우] 휴가', '김태용[일본출장]', '[류진경] 오전반차', '[김성호] 연차'],\n        [undef, 'FE 스크럼 - 4', '[일본 출장] 김지해, 장세영'],\n        [undef, '팀 스터디 - B', 'FE 스크럼 - 1'],\n        [undef, 'FE 스크럼 - 5', 'FE 스크럼 - 2'],\n        [undef, '주간보고 작성', '팀 스터디 - A'],\n        [undef, undef, 'FE 스크럼 - 3'],\n        [undef, undef, '[코드리뷰] 콤보차트 리펙토링'],\n        [undef, undef, '캘린더이야기'],\n      ];\n\n      const expectedTop = [\n        [1, 2, 3, 4],\n        [undef, 2, 3],\n        [undef, 4, 4],\n        [undef, 2, 5],\n        [undef, 4, 6],\n        [undef, undef, 4],\n        [undef, undef, 5],\n        [undef, undef, 5],\n      ];\n\n      actual = findByDateRange(calendarData, { start, end });\n\n      expect(actual[0]).toEqualMatricesTitle(expectedMatrix);\n      expect(actual[0]).toEqualMatricesTop(expectedTop);\n    });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/controller/month.ts",
    "content": "import {\n  convertToUIModel,\n  getCollisionGroup,\n  getEventInDateRangeFilter,\n  getMatrices,\n  limitRenderRange,\n  positionUIModels,\n} from '@src/controller/core';\nimport type EventModel from '@src/model/eventModel';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport type TZDate from '@src/time/date';\nimport { isSameDate, toEndOfDay, toFormat, toStartOfDay } from '@src/time/datetime';\nimport array from '@src/utils/array';\nimport type { Filter } from '@src/utils/collection';\nimport Collection from '@src/utils/collection';\nimport { isUndefined } from '@src/utils/type';\n\nimport type { CalendarData, IDS_OF_DAY } from '@t/events';\n\n/**\n * Filter function for find allday event\n * @param {EventUIModel} uiModel - ui model\n * @returns {boolean} whether model is allday event?\n */\nfunction _isAllday({ model }: EventUIModel) {\n  return model.isAllday || model.hasMultiDates;\n}\n\n/**\n * Filter function for find time event\n * @param {EventUIModel} uiModel - ui model\n * @returns {boolean} whether model is time event?\n */\nfunction _isNotAllday(uiModel: EventUIModel) {\n  return !_isAllday(uiModel);\n}\n\n/**\n * Weight top value +1 for month view render\n * @param {EventUIModel} uiModel - ui model\n */\nfunction _weightTopValue(uiModel: EventUIModel) {\n  uiModel.top = uiModel.top || 0;\n  uiModel.top += 1;\n}\n\n/**\n * Adjust render range to render properly.\n *\n * Limit start, end for each allday events and expand start, end for\n * each time events\n * @param {TZDate} start - render start date\n * @param {TZDate} end - render end date\n * @param {Collection} uiModelColl - collection of ui model.\n */\nfunction _adjustRenderRange(start: TZDate, end: TZDate, uiModelColl: Collection<EventUIModel>) {\n  uiModelColl.each((uiModel) => {\n    if (uiModel.model.isAllday || uiModel.model.hasMultiDates) {\n      limitRenderRange(toStartOfDay(start), toEndOfDay(end), uiModel);\n    }\n  });\n}\n\n/**\n * Get max top index value for allday events in specific date (YMD)\n * @param idsOfDay\n * @param {string} ymd - yyyymmdd formatted value\n * @param {Collection} uiModelAlldayColl - collection of allday events\n * @returns {number} max top index value in date\n */\nfunction _getAlldayMaxTopIndexAtYMD(\n  idsOfDay: IDS_OF_DAY,\n  ymd: string,\n  uiModelAlldayColl: Collection<EventUIModel>\n) {\n  const topIndexesInDate: number[] = [];\n\n  idsOfDay[ymd].forEach((cid) => {\n    uiModelAlldayColl.doWhenHas(cid, (uiModel) => {\n      topIndexesInDate.push(uiModel.top);\n    });\n  });\n\n  if (topIndexesInDate.length > 0) {\n    return Math.max(...topIndexesInDate);\n  }\n\n  return 0;\n}\n\n/**\n * Adjust time ui model's top index value\n * @param idsOfDay\n * @param {Collection} uiModelColl - collection of ui ui model\n */\nfunction _adjustTimeTopIndex(idsOfDay: IDS_OF_DAY, uiModelColl: Collection<EventUIModel>) {\n  const vAlldayColl = uiModelColl.filter(_isAllday);\n  const sortedTimeEvents = uiModelColl.filter(_isNotAllday).sort(array.compare.event.asc);\n  const maxIndexInYMD: Record<string, number> = {};\n\n  sortedTimeEvents.forEach((timeUIModel) => {\n    const eventYMD = toFormat(timeUIModel.getStarts(), 'YYYYMMDD');\n    let alldayMaxTopInYMD = maxIndexInYMD[eventYMD];\n\n    if (isUndefined(alldayMaxTopInYMD)) {\n      alldayMaxTopInYMD = maxIndexInYMD[eventYMD] = _getAlldayMaxTopIndexAtYMD(\n        idsOfDay,\n        eventYMD,\n        vAlldayColl\n      );\n    }\n    maxIndexInYMD[eventYMD] = timeUIModel.top = alldayMaxTopInYMD + 1;\n  });\n}\n\n/**\n * Adjust time ui model's top index value\n * @param {IDS_OF_DAY} idsOfDay - ids of days\n * @param {Collection} uiModelColl - collection of ui ui model\n */\nfunction _stackTimeFromTop(idsOfDay: IDS_OF_DAY, uiModelColl: Collection<EventUIModel>) {\n  const uiModelAlldayColl = uiModelColl.filter(_isAllday);\n  const sortedTimeEvents = uiModelColl.filter(_isNotAllday).sort(array.compare.event.asc);\n  const indiceInYMD: Record<string, number[]> = {};\n\n  sortedTimeEvents.forEach((timeUIModel) => {\n    const eventYMD = toFormat(timeUIModel.getStarts(), 'YYYYMMDD');\n    let topArrayInYMD = indiceInYMD[eventYMD];\n\n    if (isUndefined(topArrayInYMD)) {\n      topArrayInYMD = indiceInYMD[eventYMD] = [];\n      idsOfDay[eventYMD].forEach((cid) => {\n        uiModelAlldayColl.doWhenHas(cid, (uiModel) => {\n          topArrayInYMD.push(uiModel.top);\n        });\n      });\n    }\n\n    if (topArrayInYMD.indexOf(timeUIModel.top) >= 0) {\n      const maxTopInYMD = Math.max(...topArrayInYMD) + 1;\n      for (let i = 1; i <= maxTopInYMD; i += 1) {\n        timeUIModel.top = i;\n        if (topArrayInYMD.indexOf(timeUIModel.top) < 0) {\n          break;\n        }\n      }\n    }\n    topArrayInYMD.push(timeUIModel.top);\n  });\n}\n\n/**\n * Convert multi-date time event to all-day event\n * @param {Collection} uiModelColl - collection of ui models.\n * property.\n */\nfunction _addMultiDatesInfo(uiModelColl: Collection<EventUIModel>) {\n  uiModelColl.each((uiModel) => {\n    const { model } = uiModel;\n    const start = model.getStarts();\n    const end = model.getEnds();\n\n    model.hasMultiDates = !isSameDate(start, end);\n\n    if (!model.isAllday && model.hasMultiDates) {\n      uiModel.renderStarts = toStartOfDay(start);\n      uiModel.renderEnds = toEndOfDay(end);\n    }\n  });\n}\n\n/**\n * Find event and get ui model for specific month\n * @returns {object} ui model data\n * @param calendarData\n * @param condition\n */\nexport function findByDateRange(\n  calendarData: CalendarData,\n  condition: {\n    start: TZDate;\n    end: TZDate;\n    andFilters?: Filter<EventModel | EventUIModel>[];\n    alldayFirstMode?: boolean;\n  }\n) {\n  const { start, end, andFilters = [], alldayFirstMode = false } = condition;\n  const { events, idsOfDay } = calendarData;\n  const filterFn = Collection.and(...[getEventInDateRangeFilter(start, end)].concat(andFilters));\n\n  const coll = events.filter(filterFn);\n  const uiModelColl = convertToUIModel(coll);\n  _addMultiDatesInfo(uiModelColl);\n  _adjustRenderRange(start, end, uiModelColl);\n  const vList = uiModelColl.sort(array.compare.event.asc);\n  const usingTravelTime = false;\n  const collisionGroup = getCollisionGroup(vList, usingTravelTime);\n  const matrices = getMatrices(uiModelColl, collisionGroup, usingTravelTime);\n  positionUIModels(start, end, matrices, _weightTopValue);\n\n  if (alldayFirstMode) {\n    _adjustTimeTopIndex(idsOfDay, uiModelColl);\n  } else {\n    _stackTimeFromTop(idsOfDay, uiModelColl);\n  }\n\n  return matrices;\n}\n"
  },
  {
    "path": "apps/calendar/src/controller/times.spec.ts",
    "content": "import { getNextGridTime, getPrevGridTime, getTopPercentByTime } from '@src/controller/times';\nimport TZDate from '@src/time/date';\n\ndescribe('times controller', () => {\n  describe('getTopPercentByTimeUnit() calculate top pixel value between start date, end date', () => {\n    it('Using hours in a day', () => {\n      // 12:00:00 is middle time of one days. return 50%\n      expect(\n        getTopPercentByTime(\n          new TZDate('2020-05-05T12:00:00'),\n          new TZDate('2020-05-05T00:00:00'),\n          new TZDate('2020-05-06T00:00:00')\n        )\n      ).toBe(50);\n\n      expect(\n        getTopPercentByTime(\n          new TZDate('2020-05-05T00:00:00'),\n          new TZDate('2020-05-05T00:00:00'),\n          new TZDate('2020-05-06T00:00:00')\n        )\n      ).toBe(0);\n\n      expect(\n        getTopPercentByTime(\n          new TZDate('2020-05-05T22:30:00'),\n          new TZDate('2020-05-05T21:00:00'),\n          new TZDate('2020-05-06T00:00:00')\n        )\n      ).toBe(50);\n\n      expect(\n        getTopPercentByTime(\n          new TZDate('2020-05-05T11:00:00'),\n          new TZDate('2020-05-05T09:00:00'),\n          new TZDate('2020-05-05T14:00:00')\n        )\n      ).toBe(40);\n    });\n\n    it('Using dates in a week', () => {\n      // 2020-05-03T00:00:00 is middle time between 2020-05-01T00:00:00 and 2020-05-05T00:00:00. return 50%\n      expect(\n        getTopPercentByTime(\n          new TZDate('2020-05-03T00:00:00'),\n          new TZDate('2020-05-01T00:00:00'),\n          new TZDate('2020-05-05T00:00:00')\n        )\n      ).toBe(50);\n\n      expect(\n        getTopPercentByTime(\n          new TZDate('2020-05-02T00:00:00'),\n          new TZDate('2020-05-01T00:00:00'),\n          new TZDate('2020-05-05T00:00:00')\n        )\n      ).toBe(25);\n\n      expect(\n        getTopPercentByTime(\n          new TZDate('2020-05-04T00:00:00'),\n          new TZDate('2020-05-01T00:00:00'),\n          new TZDate('2020-05-05T00:00:00')\n        )\n      ).toBe(75);\n    });\n\n    it('Using dates in a month', () => {\n      // 2020-05-16T00:00:00 is middle time between 2020-05-01T00:00:00 and 2020-05-31T00:00:00. return 50%\n      expect(\n        getTopPercentByTime(\n          new TZDate('2020-05-16T00:00:00'),\n          new TZDate('2020-05-01T00:00:00'),\n          new TZDate('2020-05-31T00:00:00')\n        )\n      ).toBe(50);\n\n      expect(\n        getTopPercentByTime(\n          new TZDate('2020-05-08T12:00:00'),\n          new TZDate('2020-05-01T00:00:00'),\n          new TZDate('2020-05-31T00:00:00')\n        )\n      ).toBe(25);\n\n      expect(\n        getTopPercentByTime(\n          new TZDate('2020-05-23T12:00:00'),\n          new TZDate('2020-05-01T00:00:00'),\n          new TZDate('2020-05-31T00:00:00')\n        )\n      ).toBe(75);\n    });\n\n    it('Using month in a year', () => {\n      // 2020-05-16T00:00:00 is middle time between 2020-01-01T00:00:00 and 2021-01-01T00:00:00. return 50%\n      expect(\n        getTopPercentByTime(\n          new TZDate('2020-07-01T00:00:00'),\n          new TZDate('2020-01-01T00:00:00'),\n          new TZDate('2021-01-01T00:00:00')\n        )\n      ).toBeLessThanOrEqual(50);\n    });\n  });\n\n  it('getPrevGridTime', () => {\n    const time = new TZDate('2015-05-05T12:00:00');\n    const slot = 30;\n    const unit = 'minute';\n    const result = getPrevGridTime(time, slot, unit);\n    const prevGridTime = new TZDate('2015-05-05T12:00:00');\n\n    expect(result).toEqual(prevGridTime);\n  });\n\n  it('getPrevGridTime with slot 15 minutes', () => {\n    const time = new TZDate('2015-05-05T12:18:00');\n    const slot = 15;\n    const unit = 'minute';\n    const result = getPrevGridTime(time, slot, unit);\n    const prevGridTime = new TZDate('2015-05-05T12:15:00');\n\n    expect(result).toEqual(prevGridTime);\n  });\n\n  it(`getPrevGridTime with slot 1 hour`, () => {\n    const time = new TZDate('2015-05-05T12:10:00');\n    const slot = 1;\n    const unit = 'hour';\n    const result = getPrevGridTime(time, slot, unit);\n    const prevGridTime = new TZDate('2015-05-05T12:00:00');\n\n    expect(result).toEqual(prevGridTime);\n  });\n\n  it(`getPrevGridTime with slot 1 day`, () => {\n    const time = new TZDate('2015-05-05T12:10:00');\n    const slot = 1;\n    const unit = 'date';\n    const result = getPrevGridTime(time, slot, unit);\n    const prevGridTime = new TZDate('2015-05-05T00:00:00');\n\n    expect(result).toEqual(prevGridTime);\n  });\n\n  it(`getPrevGridTime with slot 1 month`, () => {\n    const time = new TZDate('2015-05-05T12:10:00');\n    const slot = 1;\n    const unit = 'month';\n    const result = getPrevGridTime(time, slot, unit);\n    const prevGridTime = new TZDate('2015-05-01T00:00:00');\n\n    expect(result).toEqual(prevGridTime);\n  });\n\n  it(`getPrevGridTime with slot 1 year`, () => {\n    const time = new TZDate('2015-05-05T12:10:00');\n    const slot = 1;\n    const unit = 'year';\n    const result = getPrevGridTime(time, slot, unit);\n    const prevGridTime = new TZDate('2015-01-01T00:00:00');\n\n    expect(result).toEqual(prevGridTime);\n  });\n\n  it('getNextGridTime', () => {\n    const time = new TZDate('2015-05-05T12:10:00');\n    const slot = 30;\n    const unit = 'minute';\n    const result = getNextGridTime(time, slot, unit);\n    const nextGridTime = new TZDate('2015-05-05T12:30:00');\n\n    expect(result).toEqual(nextGridTime);\n  });\n\n  it('getNextGridTime with slot 15 minutes', () => {\n    const time = new TZDate('2015-05-05T12:10:00');\n    const slot = 15;\n    const unit = 'minute';\n    const result = getNextGridTime(time, slot, unit);\n    const nextGridTime = new TZDate('2015-05-05T12:15:00');\n\n    expect(result).toEqual(nextGridTime);\n  });\n\n  it(`getNextGridTime with slot 1 hour`, () => {\n    const time = new TZDate('2015-05-05T12:10:00');\n    const slot = 1;\n    const unit = 'hour';\n    const result = getNextGridTime(time, slot, unit);\n    const nextGridTime = new TZDate('2015-05-05T13:00:00');\n\n    expect(result).toEqual(nextGridTime);\n  });\n\n  it(`getNextGridTime with slot 1 day`, () => {\n    const time = new TZDate('2015-05-05T12:10:00');\n    const slot = 1;\n    const unit = 'date';\n    const result = getNextGridTime(time, slot, unit);\n    const nextGridTime = new TZDate('2015-05-06T00:00:00');\n\n    expect(result).toEqual(nextGridTime);\n  });\n\n  it(`getNextGridTime with slot 1 month`, () => {\n    const time = new TZDate('2015-05-05T12:10:00');\n    const slot = 1;\n    const unit = 'month';\n    const result = getNextGridTime(time, slot, unit);\n    const nextGridTime = new TZDate('2015-06-01T00:00:00');\n\n    expect(result).toEqual(nextGridTime);\n  });\n\n  it(`getNextGridTime with slot 1 year`, () => {\n    const time = new TZDate('2015-05-05T12:10:00');\n    const slot = 1;\n    const unit = 'year';\n    const result = getNextGridTime(time, slot, unit);\n    const nextGridTime = new TZDate('2016-01-01T00:00:00');\n\n    expect(result).toEqual(nextGridTime);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/controller/times.ts",
    "content": "import type TZDate from '@src/time/date';\nimport { clone } from '@src/time/datetime';\nimport { limit, ratio } from '@src/utils/math';\n\nimport type { TimeUnit } from '@t/events';\n\n/**\n * @param date\n * @param {TZDate} [start] - start time\n * @param {TZDate} [end] - end time\n * @returns {number} The percent value represent current time between start and end\n */\nexport function getTopPercentByTime(date: TZDate, start: TZDate, end: TZDate) {\n  const startTime = start.getTime();\n  const endTime = end.getTime();\n  const time = limit(date.getTime(), [startTime], [endTime]) - startTime;\n  const max = endTime - startTime;\n\n  const topPercent = ratio(max, 100, time);\n\n  return limit(topPercent, [0], [100]);\n}\n\n/**\n * @typedef {Object} VerticalPositionsByTime\n * @property {number} top - top percent\n * @property {number} height - height percent\n */\n/**\n *\n * @param {TZDate} start target time which is converted to percent value\n * @param {TZDate} end target time which is converted to percent value\n * @param {TZDate} minTime start time\n * @param {TZDate} maxTime end time\n * @returns {VerticalPositionsByTime} verticalPositions\n */\nexport function getTopHeightByTime(start: TZDate, end: TZDate, minTime: TZDate, maxTime: TZDate) {\n  const top = getTopPercentByTime(start, minTime, maxTime);\n  const bottom = getTopPercentByTime(end, minTime, maxTime);\n  const height = bottom - top;\n\n  return {\n    top,\n    height,\n  };\n}\n\nfunction setValueByUnit(time: TZDate, value: number, unit: TimeUnit) {\n  if (unit === 'minute') {\n    time.setMinutes(value, 0, 0);\n  } else if (unit === 'hour') {\n    time.setHours(value, 0, 0, 0);\n  } else if (unit === 'date') {\n    time.setHours(0, 0, 0, 0);\n    time.setDate(value + 1);\n  } else if (unit === 'month') {\n    time.setHours(0, 0, 0, 0);\n    time.setMonth(value, 1);\n  } else if (unit === 'year') {\n    time.setHours(0, 0, 0, 0);\n    time.setFullYear(value, 0, 1);\n  }\n\n  return time;\n}\n\n/**\n * Get a previous grid time before the time\n * @param {TZDate} time - target time\n * @param slot\n * @param unit\n * @returns {TZDate} - next grid time\n */\nexport function getPrevGridTime(time: TZDate, slot: number, unit: TimeUnit) {\n  let index = 0;\n  let prevGridTime = setValueByUnit(clone(time), slot * index, unit);\n  let nextGridTime;\n\n  index += 1;\n  do {\n    nextGridTime = setValueByUnit(clone(time), slot * index, unit);\n    index += 1;\n\n    if (nextGridTime < time) {\n      prevGridTime = clone(nextGridTime);\n    }\n  } while (nextGridTime <= time);\n\n  return prevGridTime;\n}\n\n/**\n * Get a next grid time after the time\n * @param {TZDate} time - target time\n * @param slot\n * @param unit\n * @returns {TZDate} - next grid time\n */\nexport function getNextGridTime(time: TZDate, slot: number, unit: TimeUnit) {\n  let index = 0;\n  let nextGridTime;\n\n  do {\n    nextGridTime = setValueByUnit(clone(time), slot * index, unit);\n    index += 1;\n  } while (nextGridTime < time);\n\n  return nextGridTime;\n}\n"
  },
  {
    "path": "apps/calendar/src/controller/week.spec.ts",
    "content": "import { addToMatrix, createEvent, createEventCollection } from '@src/controller/base';\nimport { _makeHourRangeFilter, findByDateRange, splitEventByDateRange } from '@src/controller/week';\nimport EventModel from '@src/model/eventModel';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport TZDate from '@src/time/date';\nimport Collection from '@src/utils/collection';\n\nimport type { CalendarData, EventObject, Matrix3d, TimeGridEventMatrix } from '@t/events';\nimport type { Panel } from '@t/panel';\n\ndescribe('Base.Week', () => {\n  let calendarData: CalendarData;\n  let mockData: EventObject[];\n\n  beforeEach(() => {\n    calendarData = {\n      calendars: [],\n      events: createEventCollection(),\n      idsOfDay: {},\n    };\n    mockData = [\n      {\n        title: 'A',\n        isAllday: false,\n        start: '2015-05-01T10:20:00',\n        end: '2015-05-01T10:40:00',\n        category: 'time',\n      },\n      {\n        title: 'B',\n        isAllday: false,\n        start: '2015-05-01T10:30:00',\n        end: '2015-05-01T11:30:00',\n        category: 'time',\n      },\n      {\n        title: 'C',\n        isAllday: false,\n        start: '2015-05-01T11:20:00',\n        end: '2015-05-01T12:00:00',\n        category: 'time',\n      },\n      {\n        title: 'D',\n        isAllday: false,\n        start: '2015-05-01T10:50:00',\n        end: '2015-05-01T11:10:00',\n        category: 'time',\n      },\n      {\n        title: 'E',\n        isAllday: false,\n        start: '2015-05-01T13:20:00',\n        end: '2015-05-01T13:40:00',\n        category: 'time',\n      },\n      {\n        title: 'F',\n        isAllday: false,\n        start: '2015-05-01T14:00:00',\n        end: '2015-05-01T14:20:00',\n        category: 'time',\n      },\n      {\n        title: 'G',\n        isAllday: false,\n        start: '2015-05-01T14:10:00',\n        end: '2015-05-01T14:20:00',\n        category: 'time',\n      },\n      {\n        title: 'H',\n        isAllday: false,\n        start: '2015-05-01T16:00:00',\n        end: '2015-05-01T18:00:00',\n        category: 'time',\n      },\n      {\n        title: 'I',\n        isAllday: false,\n        start: '2015-05-01T17:00:00',\n        end: '2015-05-01T20:00:00',\n        category: 'time',\n      },\n      {\n        title: 'J',\n        isAllday: false,\n        start: '2015-05-01T19:00:00',\n        end: '2015-05-01T21:00:00',\n        category: 'time',\n      },\n      {\n        title: '물고기 밥주기',\n        isAllday: false,\n        start: '2015-05-01T22:00:00',\n        end: '2015-05-01T22:10:00',\n        category: 'time',\n      },\n    ];\n  });\n\n  describe('findByDateRange', () => {\n    let panels: Panel[];\n\n    beforeEach(() => {\n      panels = [\n        {\n          name: 'time',\n          type: 'timegrid',\n          handlers: ['click', 'creation', 'move', 'resize'],\n          show: true,\n        },\n      ];\n\n      mockData.forEach((data) => {\n        createEvent(calendarData, data);\n      });\n\n      /*\n       * It is different from the actual data structure.\n       * Please only refer to the schedule.\n       * matrix: {\n       * '20150501': [id1],\n       * '20150502': [id1, id4],\n       * '20150503': [id2, id3, id4]\n       * }\n       */\n    });\n\n    it('by YMD', () => {\n      const start = new TZDate('2015/04/30');\n      const end = new TZDate('2015/05/02');\n\n      const result = findByDateRange(calendarData, {\n        start,\n        end,\n        panels,\n        andFilters: [],\n        options: {\n          hourStart: 0,\n          hourEnd: 24,\n        },\n      });\n\n      // There are 5 collision blocks on 5/1.\n      expect((result.time as TimeGridEventMatrix)['20150501'].length).toBe(5);\n    });\n\n    it('Can add more AND clause filter function by third parameter', () => {\n      const start = new TZDate('2015/04/30');\n      const end = new TZDate('2015/05/02');\n\n      // Since there is only one event with title J\n      const result = findByDateRange(calendarData, {\n        start,\n        end,\n        panels,\n        andFilters: [(model: EventModel | EventUIModel) => (model as EventModel).title === 'J'],\n        options: { hourStart: 0, hourEnd: 24 },\n      }) as Record<string, Record<string, Matrix3d<EventUIModel>>>;\n\n      // One collision block in the timeline group\n      expect(result.time['20150501'].length).toBe(1);\n    });\n  });\n\n  describe('_getHourRangeFilter()', () => {\n    let hourRangeFilter: (event: EventModel) => boolean;\n    let event: EventModel;\n\n    beforeEach(() => {\n      // 8:00 ~ 20:00\n      hourRangeFilter = _makeHourRangeFilter(10, 12);\n      event = new EventModel();\n    });\n\n    it('filter event by start, end date visible', () => {\n      event.start = new TZDate('2018-05-02T09:30:00');\n      event.end = new TZDate('2018-05-02T13:30:00');\n\n      expect(hourRangeFilter(event)).toBe(true);\n\n      event.start = new TZDate('2018-05-02T00:00:00');\n      event.end = new TZDate('2018-05-02T10:30:00');\n\n      expect(hourRangeFilter(event)).toBe(true);\n\n      event.start = new TZDate('2018-05-02T10:30:00');\n      event.end = new TZDate('2018-05-02T11:30:00');\n\n      expect(hourRangeFilter(event)).toBe(true);\n\n      event.start = new TZDate('2018-05-02T11:30:00');\n      event.end = new TZDate('2018-05-02T15:00:00');\n\n      expect(hourRangeFilter(event)).toBe(true);\n\n      event.start = new TZDate('2018-05-02T00:00:00');\n      event.end = new TZDate('2018-05-02T10:00:00');\n\n      expect(hourRangeFilter(event)).toBe(false);\n\n      event.start = new TZDate('2018-05-02T10:00:00');\n      event.end = new TZDate('2018-05-02T12:00:00');\n\n      expect(hourRangeFilter(event)).toBe(true);\n\n      event.start = new TZDate('2018-05-02T12:00:00');\n      event.end = new TZDate('2018-05-02T15:00:00');\n\n      expect(hourRangeFilter(event)).toBe(false);\n\n      event.start = new TZDate('2018-05-02T09:00:00');\n      event.end = new TZDate('2018-05-02T15:00:00');\n\n      expect(hourRangeFilter(event)).toBe(true);\n\n      event.start = new TZDate('2018-05-02T09:00:00');\n      event.end = new TZDate('2018-05-02T15:00:00');\n\n      expect(hourRangeFilter(event)).toBe(true);\n\n      event.start = new TZDate('2018-05-02T09:00:00');\n      event.end = new TZDate('2018-05-03T09:00:00');\n\n      expect(hourRangeFilter(event)).toBe(true); // true, false??\n\n      event.start = new TZDate('2018-05-02T11:00:00');\n      event.end = new TZDate('2018-05-03T09:00:00');\n\n      expect(hourRangeFilter(event)).toBe(true);\n    });\n  });\n\n  describe('splitEventByDateRange()', () => {\n    let events: EventModel[];\n    let collection: Collection<EventModel>;\n\n    beforeEach(() => {\n      collection = new Collection((item) => {\n        return item.cid();\n      });\n\n      events = [\n        {\n          title: 'A',\n          isAllday: false,\n          start: '2015/05/01 09:30:00',\n          end: '2015/05/01 18:30:00',\n        },\n        {\n          title: 'B',\n          isAllday: false,\n          start: '2015/05/02 09:30:00',\n          end: '2015/05/02 18:30:00',\n        },\n        {\n          title: 'C',\n          isAllday: true,\n          start: '2015/05/01 09:00:00',\n          end: '2015/05/02 09:00:00',\n        },\n      ].map((eventData) => new EventModel(eventData));\n\n      collection.add(...events);\n\n      events.forEach((event) => {\n        calendarData.events.add(event);\n        addToMatrix(calendarData.idsOfDay, event);\n      });\n    });\n\n    it('split event by ymd.', () => {\n      const result = splitEventByDateRange(\n        calendarData.idsOfDay,\n        new TZDate('2015-05-01T00:00:00'),\n        new TZDate('2015-05-03T23:59:59'),\n        collection\n      );\n\n      const getter = (item: EventModel) => item.cid();\n      const expected = {\n        '20150501': new Collection(getter),\n        '20150502': new Collection(getter),\n        '20150503': new Collection(getter),\n      };\n\n      expected['20150501'].add(events[0]);\n      expected['20150501'].add(events[2]);\n      expected['20150502'].add(events[1]);\n      expected['20150502'].add(events[2]);\n\n      expect(result['20150501'].toArray()).toEqual(expected['20150501'].toArray());\n      expect(result['20150502'].toArray()).toEqual(expected['20150502'].toArray());\n      expect(result['20150503'].toArray()).toEqual(expected['20150503'].toArray());\n    });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/controller/week.ts",
    "content": "import { filterByCategory, getDateRange } from '@src/controller/base';\nimport {\n  convertToUIModel,\n  getCollisionGroup,\n  getEventInDateRangeFilter,\n  getMatrices,\n  limitRenderRange,\n  positionUIModels,\n} from '@src/controller/core';\nimport type EventModel from '@src/model/eventModel';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport TZDate from '@src/time/date';\nimport { toEndOfDay, toFormat, toStartOfDay } from '@src/time/datetime';\nimport array from '@src/utils/array';\nimport type { Filter } from '@src/utils/collection';\nimport Collection from '@src/utils/collection';\nimport { isNil } from '@src/utils/type';\n\nimport type {\n  CalendarData,\n  DayGridEventMatrix,\n  EventGroupMap,\n  IDS_OF_DAY,\n  Matrix3d,\n} from '@t/events';\nimport type { WeekOptions } from '@t/options';\nimport type { Panel } from '@t/panel';\n\n/**********\n * TIME GRID VIEW\n **********/\n\n/**\n * make a filter function that is not included range of start, end hour\n * @param {number} hStart - hour start\n * @param {number} hEnd - hour end\n * @returns {function} - filtering function\n */\nexport function _makeHourRangeFilter(hStart: number, hEnd: number) {\n  // eslint-disable-next-line complexity\n  return (uiModel: EventModel | EventUIModel) => {\n    const ownHourStart = uiModel.getStarts();\n    const ownHourEnd = uiModel.getEnds();\n    const ownHourStartTime = ownHourStart.getTime();\n    const ownHourEndTime = ownHourEnd.getTime();\n    const yyyy = ownHourStart.getFullYear();\n    const mm = ownHourStart.getMonth();\n    const dd = ownHourStart.getDate();\n\n    const hourStart = new TZDate(yyyy, mm, dd).setHours(hStart);\n    const hourEnd = new TZDate(yyyy, mm, dd).setHours(hEnd);\n\n    return (\n      (ownHourStartTime >= hourStart && ownHourStartTime < hourEnd) ||\n      (ownHourEndTime > hourStart && ownHourEndTime <= hourEnd) ||\n      (ownHourStartTime < hourStart && ownHourEndTime > hourStart) ||\n      (ownHourEndTime > hourEnd && ownHourStartTime < hourEnd)\n    );\n  };\n}\n\n/**\n * make ui model function depending on start and end hour\n * if time view options has start or end hour condition\n * it add filter\n * @param {number} hourStart - start hour to be shown\n * @param {number} hourEnd - end hour to be shown\n * @returns {function} function\n */\nexport function _makeGetUIModelFuncForTimeView(\n  hourStart: number,\n  hourEnd: number\n): (uiModelColl: Collection<EventUIModel>) => EventUIModel[] {\n  if (hourStart === 0 && hourEnd === 24) {\n    return (uiModelColl: Collection<EventUIModel>) => {\n      return uiModelColl.sort(array.compare.event.asc);\n    };\n  }\n\n  return (uiModelColl: Collection<EventUIModel>) => {\n    return uiModelColl\n      .filter(_makeHourRangeFilter(hourStart, hourEnd))\n      .sort(array.compare.event.asc);\n  };\n}\n\n/**\n * split event model by ymd.\n * @param {IDS_OF_DAY} idsOfDay - ids of days\n * @param {TZDate} start - start date\n * @param {TZDate} end - end date\n * @param {Collection<EventUIModel>} uiModelColl - collection of ui models.\n * @returns {object.<string, Collection>} splitted event model collections.\n */\nexport function splitEventByDateRange(\n  idsOfDay: IDS_OF_DAY,\n  start: TZDate,\n  end: TZDate,\n  uiModelColl: Collection<EventModel> | Collection<EventUIModel>\n) {\n  const result: Record<string, Collection<EventModel | EventUIModel>> = {};\n  const range = getDateRange(start, end);\n\n  range.forEach((date: TZDate) => {\n    const ymd = toFormat(date, 'YYYYMMDD');\n    const ids = idsOfDay[ymd];\n    const collection = (result[ymd] = new Collection<EventModel | EventUIModel>((event) => {\n      return event.cid();\n    }));\n\n    if (ids && ids.length) {\n      ids.forEach((id) => {\n        uiModelColl.doWhenHas(id, (event: EventModel | EventUIModel) => {\n          collection.add(event);\n        });\n      });\n    }\n  }, {});\n\n  return result;\n}\n\n/**\n * create ui model for time view part\n * @param {IDS_OF_DAY} idsOfDay - model controller\n * @param {object} condition - find options\n *  @param {TZDate} condition.start - start date.\n *  @param {TZDate} condition.end - end date.\n *  @param {Collection} condition.uiModelTimeColl - collection of ui models.\n *  @param {number} condition.hourStart - start hour to be shown\n *  @param {number} condition.hourEnd - end hour to be shown\n * @returns {object} ui model for time part.\n */\nexport function getUIModelForTimeView(\n  idsOfDay: IDS_OF_DAY,\n  condition: {\n    start: TZDate;\n    end: TZDate;\n    uiModelTimeColl: Collection<EventUIModel>;\n    hourStart: number;\n    hourEnd: number;\n  }\n) {\n  const { start, end, uiModelTimeColl, hourStart, hourEnd } = condition;\n  const ymdSplitted = splitEventByDateRange(idsOfDay, start, end, uiModelTimeColl);\n  const result: Record<string, Matrix3d<EventUIModel>> = {};\n\n  const _getUIModel = _makeGetUIModelFuncForTimeView(hourStart, hourEnd);\n  const usingTravelTime = true;\n\n  Object.entries(ymdSplitted).forEach(([ymd, uiModelColl]) => {\n    const uiModels = _getUIModel(uiModelColl as Collection<EventUIModel>);\n    const collisionGroups = getCollisionGroup(uiModels, usingTravelTime);\n    const matrices = getMatrices(uiModelColl, collisionGroups, usingTravelTime);\n\n    result[ymd] = matrices as Matrix3d<EventUIModel>;\n  });\n\n  return result;\n}\n\n/**********\n * ALLDAY VIEW\n **********/\n\n/**\n * Set hasMultiDates flag to true and set date ranges for rendering\n * @param {Collection} uiModelColl - collection of ui models.\n */\nexport function _addMultiDatesInfo(uiModelColl: Collection<EventUIModel>) {\n  uiModelColl.each((uiModel) => {\n    const { model } = uiModel;\n\n    model.hasMultiDates = true;\n    uiModel.renderStarts = toStartOfDay(model.getStarts());\n    uiModel.renderEnds = toEndOfDay(model.getEnds());\n  });\n}\n\n/**\n * create ui model for allday view part\n * @param {TZDate} start start date.\n * @param {TZDate} end end date.\n * @param {Collection} uiModelColl - ui models of allday event.\n * @returns {DayGridEventMatrix} matrix of allday event ui models.\n */\nexport function getUIModelForAlldayView(\n  start: TZDate,\n  end: TZDate,\n  uiModelColl: Collection<EventUIModel>\n): DayGridEventMatrix {\n  if (!uiModelColl || !uiModelColl.size) {\n    return [];\n  }\n\n  _addMultiDatesInfo(uiModelColl);\n  limitRenderRange(start, end, uiModelColl);\n\n  const uiModels = uiModelColl.sort(array.compare.event.asc);\n  const usingTravelTime = true;\n  const collisionGroups = getCollisionGroup(uiModels, usingTravelTime);\n  const matrices = getMatrices(uiModelColl, collisionGroups, usingTravelTime);\n\n  positionUIModels(start, end, matrices);\n\n  return matrices;\n}\n\n/**********\n * READ\n **********/\n\n/**\n * Populate events in date range.\n * @param {CalendarData} calendarData - data store\n * @param {object} condition - find options\n *  @param {IDS_OF_DAY} condition.idsOfDay - model controller\n *  @param {TZDate} condition.start start date.\n *  @param {TZDate} condition.end end date.\n *  @param {Array.<object>} condition.panels - event panels like 'milestone', 'task', 'allday', 'time'\n *  @param {function[]} condition.[andFilters] - optional filters to applying search query\n *  @param {Object} condition.options - week view options\n * @returns {object} events grouped by dates.\n */\nexport function findByDateRange(\n  calendarData: CalendarData,\n  condition: {\n    start: TZDate;\n    end: TZDate;\n    panels: Panel[];\n    andFilters: Filter<EventModel | EventUIModel>[];\n    options: WeekOptions;\n  }\n) {\n  const { start, end, panels, andFilters = [], options } = condition;\n  const { events, idsOfDay } = calendarData;\n  const hourStart = options?.hourStart ?? 0;\n  const hourEnd = options?.hourEnd ?? 24;\n  const filterFn = Collection.and(...[getEventInDateRangeFilter(start, end)].concat(andFilters));\n  const uiModelColl = convertToUIModel(events.filter(filterFn));\n\n  const group: Record<string, Collection<EventUIModel>> = uiModelColl.groupBy(filterByCategory);\n\n  return panels.reduce<EventGroupMap>(\n    (acc, cur) => {\n      const { name, type } = cur;\n\n      if (isNil(group[name])) {\n        return acc;\n      }\n\n      return {\n        ...acc,\n        [name]:\n          type === 'daygrid'\n            ? getUIModelForAlldayView(start, end, group[name])\n            : getUIModelForTimeView(idsOfDay, {\n                start,\n                end,\n                uiModelTimeColl: group[name],\n                hourStart,\n                hourEnd,\n              }),\n      };\n    },\n    {\n      milestone: [],\n      task: [],\n      allday: [],\n      time: {},\n    }\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/css/common.css",
    "content": ".holiday {\n  color: red;\n  font-size: 15px;\n}\n"
  },
  {
    "path": "apps/calendar/src/css/daygrid/dayGrid.css",
    "content": ".layout.month {\n  height: 100%;\n}\n\n.month .day-names {\n  /* from constant MONTH_DAY_NAME_HEIGHT */\n  height: 31px;\n}\n\n.month .month-daygrid {\n  position: relative;\n  /* modify this if you want to change height of day names */\n  height: calc(100% - 31px);\n}\n\n.month-week-item {\n  position: relative;\n}\n\n.weekday-grid {\n  position: absolute;\n  min-height: inherit;\n  width: 100%;\n  height: 100%;\n}\n\n.daygrid-cell {\n  position: absolute;\n  height: 100%;\n  min-height: inherit;\n  padding: 3px 0;\n}\n\n.daygrid-cell + .daygrid-cell {\n  border-left: 1px solid #e5e5e5;\n}\n\n.grid-cell-date {\n  display: inline-block;\n  width: 27px;\n  height: 27px;\n  line-height: 1.7;\n  text-align: center;\n}\n\n.grid-cell-footer {\n  position: absolute;\n  width: 100%;\n  bottom: 0;\n}\n\n.grid-cell-more-events {\n  float: right;\n  height: 27px;\n  line-height: 27px;\n  padding: 0 5px;\n  text-align: center;\n  font-size: 11px;\n  font-weight: bold;\n  color: #aaa;\n  border: none;\n  background-color: transparent;\n  cursor: pointer;\n}\n\n.weekday-events {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  font-size: 12px;\n}\n\n/* TODO: define this class in more common place */\n.weekday-event {\n  cursor: pointer;\n}\n\n.weekday {\n  height: 100%;\n}\n\n.weekday .grid-selection {\n  position: absolute;\n}\n"
  },
  {
    "path": "apps/calendar/src/css/daygrid/dayNames.css",
    "content": ".day-names {\n  position: relative;\n}\n\n.day-name-container {\n  position: relative;\n}\n\n.day-name-item {\n  position: absolute;\n  font-size: 12px;\n  font-weight: normal;\n  text-align: left;\n  padding: 0 10px;\n}\n\n.day-name-item.week {\n  line-height: 38px;\n  height: 42px;\n}\n\n.day-name-item.month {\n  line-height: 31px;\n  height: 31px;\n}\n\n.day-view-day-names,\n.week-view-day-names {\n  border-bottom: 1px solid #e5e5e5;\n}\n\n.day-names.week {\n  height: 42px;\n  padding-left: 0;\n  text-align: left;\n}\n\n.day-names.month {\n  height: 31px;\n  padding: 0 10px;\n  font-size: 12px;\n  font-weight: normal;\n  text-align: left;\n}\n\n.day-name__date {\n  font-size: 26px;\n}\n\n.day-name__name {\n  font-size: 12px;\n}\n"
  },
  {
    "path": "apps/calendar/src/css/daygrid/index.css",
    "content": "@import './dayNames.css';\n@import './dayGrid.css';\n"
  },
  {
    "path": "apps/calendar/src/css/events/background.css",
    "content": ".event-background { position: absolute; }\n"
  },
  {
    "path": "apps/calendar/src/css/events/grid.css",
    "content": ".weekday-event-title {\n  display: block;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  padding-left: 3px;\n  font-weight: bold;\n  font-size: 12px;\n}\n\n.weekday-event-dot {\n  position: relative;\n  top: 8px;\n  float: left;\n  display: inline-block;\n  width: 8px;\n  height: 8px;\n  border-radius: 50%;\n}\n\n.weekday-event-dot + .weekday-event-title {\n  color: #333;\n}\n\n.weekday-resize-handle {\n  position: absolute;\n  top: 0;\n  right: 5px;\n}\n\n.weekday-resize-handle.handle-y {\n  cursor: col-resize;\n}\n\n.grid-cell-date .weekday-grid-date.weekday-grid-date-decorator {\n  display: inline-block;\n  width: 26px;\n  height: 26px;\n  line-height: 26px;\n  text-align: center;\n  background-color: #135de6;\n  border-radius: 50%;\n  font-weight: bold;\n  margin-left: 2px;\n}\n"
  },
  {
    "path": "apps/calendar/src/css/events/index.css",
    "content": "@import './background.css';\n@import './time.css';\n@import './grid.css';\n"
  },
  {
    "path": "apps/calendar/src/css/events/time.css",
    "content": ".event-time {\n  position: absolute;\n  overflow: hidden;\n  cursor: pointer;\n}\n\n.event-time .travel-time,\n.event-time .event-time-content {\n  overflow: hidden;\n  padding: 1px 0 0 3px;\n  font-size: 12px;\n}\n\n.resize-handler-x {\n  position: absolute;\n  right: 0;\n  bottom: 1px;\n  left: 0;\n  height: 8px;\n  text-align: center;\n  color: #fff;\n  cursor: row-resize;\n  background: url('../image/handler-x.png') no-repeat center bottom;\n}\n"
  },
  {
    "path": "apps/calendar/src/css/icons.css",
    "content": ".icon {\n  width: 14px;\n  height: 14px;\n  display: inline-block;\n  vertical-align: middle;\n}\n.icon.ic-title {\n  background: url('./image/ic-subject.png') no-repeat;\n}\n\n.icon.ic-location {\n  background: url('./image/ic-location.png') no-repeat;\n}\n\n.icon.ic-date {\n  background: url('./image/ic-date.png') no-repeat;\n}\n\n.icon.ic-state {\n  background: url('./image/ic-state.png') no-repeat;\n}\n\n.icon.ic-private {\n  background: url('./image/ic-lock.png') no-repeat;\n}\n\n.icon.ic-public {\n  background: url('./image/ic-unlock.png') no-repeat;\n}\n\n.icon.ic-close {\n  background: url('./image/ic-close.png') no-repeat;\n}\n\n.icon.ic-user-b {\n  background: url('./image/ic-user-b.png') no-repeat;\n  top: -4px;\n}\n\n.icon.ic-edit {\n  background: url('./image/ic-edit.png') no-repeat;\n}\n\n.icon.ic-delete {\n  background: url('./image/ic-delete.png') no-repeat;\n}\n\n.icon.ic-arrow-solid-top {\n  background: url('./image/ic-arrow-solid-top.png') no-repeat;\n}\n\n.icon.ic-milestone {\n  background: url('./image/ic-milestone.png') no-repeat;\n}\n\n.icon.ic-arrow-left {\n  background: url('./image/ic-arrow-left.png') no-repeat;\n}\n\n.icon.ic-arrow-right {\n  background: url('./image/ic-arrow-right.png') no-repeat;\n}\n\n.icon.ic-handle-y {\n  background: url('./image/handler-y.png') center no-repeat;\n}\n\n.icon.ic-checkbox-normal {\n  background: url('./image/ic-checkbox-normal.png') no-repeat;\n}\n\n.icon.ic-checkbox-checked {\n  background: url('./image/ic-checkbox-checked.png') no-repeat;\n}\n\n.icon.ic-dropdown-arrow {\n  background: url('./image/ic-arrow-solid-bottom.png') no-repeat;\n}\n\n.icon.open.ic-dropdown-arrow {\n  background: url('./image/ic-arrow-solid-top.png') no-repeat;\n}\n\n.ic-location-b {\n  background: url('./image/ic-location-b.png') no-repeat;\n  top: -4px;\n}\n\n.ic-state-b {\n  background: url('./image/ic-state-b.png') no-repeat;\n  top: -4px;\n}\n\n.ic-repeat-b {\n  background: url('./image/ic-repeat-b.png') no-repeat;\n  top: -4px;\n}\n"
  },
  {
    "path": "apps/calendar/src/css/index.css",
    "content": "@import './common.css';\n@import './layout.css';\n@import './icons.css';\n@import './timegrid/index.css';\n@import './events/index.css';\n@import './panel/index.css';\n@import './popup/index.css';\n@import './daygrid/index.css';\n"
  },
  {
    "path": "apps/calendar/src/css/layout.css",
    "content": ".layout {\n  box-sizing: border-box;\n  position: relative;\n  white-space: nowrap;\n}\n\n.layout * {\n  box-sizing: border-box;\n}\n\n.layout.dragging--move-event * {\n  cursor: move;\n}\n\n.layout.dragging--resize-horizontal-event * {\n  cursor: col-resize;\n}\n\n.layout.dragging--resize-vertical-event * {\n  cursor: row-resize;\n}\n\n.layout .panel-resizer {\n  user-select: none;\n}\n\n.layout .panel-resizer:hover {\n  border-color: #999;\n}\n\n.layout .panel-resizer-guide {\n  position: absolute;\n}\n\n.layout.horizontal .panel,\n.layout.horizontal .panel-resizer {\n  display: inline-block;\n  vertical-align: middle;\n}\n"
  },
  {
    "path": "apps/calendar/src/css/panel/allday.css",
    "content": ".panel-title {\n  display: table;\n  float: left;\n  height: 100%;\n  padding-right: 5px;\n}\n\n.panel-title .left-content {\n  display: table-cell;\n  vertical-align: middle;\n  text-align: right;\n  font-size: 11px;\n}\n\n.panel-grid-wrapper {\n  position: relative;\n  overflow-y: hidden;\n}\n\n.panel .panel-title,\n.panel .panel-grid-wrapper {\n  height: 100%;\n}\n\n.allday-panel {\n  position: relative;\n  height: 100%;\n  overflow-y: hidden;\n}\n\n.allday-panel .grid-selection {\n  position: absolute;\n  right: 10px;\n  z-index: 1;\n  top: 0;\n}\n\n.panel-grid {\n  height: 100%;\n  position: absolute;\n}\n\n.panel-event-wrapper {\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  overflow-y: scroll;\n}\n\n.panel-event-wrapper .weekday-event-block {\n  position: absolute;\n}\n\n.panel-event-wrapper .weekday-event {\n  position: relative;\n  margin: 0 10px 0 1px;\n  cursor: pointer;\n  border-left-style: solid;\n  border-left-width: 3px;\n\n  height: 18px;\n  border-radius: 0;\n  color: #9a1313;\n  background-color: rgba(218, 27, 27, 0.2);\n  border-color: #da1b1b;\n}\n\n.panel-event-wrapper .weekday-exceed-right .weekday-event {\n  margin-right: 0;\n}\n\n.panel-event {\n  position: absolute;\n  border: 1px solid #333;\n}\n\n.weekday-exceed-in-week {\n  position: absolute;\n  right: 5px;\n  bottom: 5px;\n  z-index: 1;\n  margin-right: 5px;\n  font-size: 12px;\n  line-height: 14px;\n  cursor: pointer;\n  padding: 1px 5px;\n  background-color: #fff;\n  border: 1px solid #ddd;\n  color: #000;\n}\n\n.collapse-btn-icon {\n  display: inline-block;\n  vertical-align: middle;\n  margin: -1px -14px 0 -4px;\n  width: 0;\n  height: 0;\n  border-left: 4px solid transparent;\n  border-right: 4px solid transparent;\n  border-bottom: 5px solid #4f5959;\n}\n\n.day-view .panel:not(.time),\n.week-view .panel:not(.time) {\n  overflow-y: scroll;\n}\n"
  },
  {
    "path": "apps/calendar/src/css/panel/index.css",
    "content": "@import './allday.css';\n"
  },
  {
    "path": "apps/calendar/src/css/popup/common.css",
    "content": ".floating-layer {\n  z-index: 1;\n}\n\n.floating-layer * {\n  box-sizing: border-box;\n}\n\n.popup-overlay {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  top: 0;\n  left: 0;\n}\n\n.popup-container {\n  position: absolute;\n  font-weight: 2.5;\n  box-shadow: 0 2px 6px 0 rgb(0 0 0 / 10%);\n  clear: both;\n  z-index: 2;\n}\n\n.popup-section {\n  font-size: 0;\n  min-height: 40px;\n}\n\n.popup-button.popup-close {\n  position: absolute;\n  top: 10px;\n  right: 10px;\n  background-color: #fff;\n  padding: 0;\n  border: none;\n}\n\n.popup-button.popup-confirm {\n  float: right;\n  width: 96px;\n  height: 36px;\n  border-radius: 40px;\n  background-color: #ff6618;\n  font-size: 12px;\n  font-weight: bold;\n  color: #fff;\n  border: none;\n}\n\n.dropdown-menu {\n  position: absolute;\n  width: 100%;\n  top: 31px;\n  z-index: 1;\n  padding: 4px 0;\n  background-color: #fff;\n  border: 1px solid #d5d5d5;\n  border-top: none;\n  border-radius: 0 0 2px 2px;\n}\n\n.dropdown-menu.open {\n  display: block;\n}\n\n.dropdown-menu-item {\n  width: 100%;\n  height: 30px;\n  border: none;\n  padding: 0 9px 0 12px;\n  font-size: 0;\n  border-radius: 2px;\n  cursor: pointer;\n}\n\n.popup-arrow-border,\n.popup-arrow-fill {\n  position: absolute;\n}\n"
  },
  {
    "path": "apps/calendar/src/css/popup/detail.css",
    "content": ".detail-container {\n  width: 301px;\n  min-width: 301px;\n  box-shadow: 0 2px 6px 0 rgb(0 0 0 / 10%);\n  background-color: #fff;\n  border: solid 1px #d5d5d5;\n  padding: 17px 17px 0;\n  border-radius: 2px;\n}\n\n.detail-container .section-header {\n  margin-bottom: 6px;\n}\n\n.detail-container .section-detail {\n  margin-bottom: 16px;\n}\n\n.detail-container .section-button {\n  border-top: 1px solid #e5e5e5;\n  font-size: 0;\n}\n\n.detail-container .content {\n  height: 24px;\n  font-size: 12px;\n  line-height: 2;\n}\n\n.detail-container .icon {\n  width: 12px;\n  height: 12px;\n  background-size: 12px;\n  position: relative;\n  margin-right: 8px;\n}\n\n.detail-container .calendar-dot {\n  border-radius: 50%;\n  width: 10px;\n  height: 10px;\n  top: -4px;\n  margin-right: 10px;\n}\n\n.event-title {\n  font-size: 15px;\n  font-weight: bold;\n  line-height: 1.6;\n  word-break: break-all;\n}\n\n.detail-item-indent {\n  text-indent: -20px;\n  padding-left: 20px;\n}\n\n.edit-button,\n.delete-button {\n  display: inline-block;\n  padding: 7px 9px 11px 9px;\n  width: calc(50% - 1px);\n  outline: none;\n  background: none;\n  border: none;\n  cursor: pointer;\n}\n\n.vertical-line {\n  background: #e5e5e5;\n  width: 1px;\n  height: 14px;\n  vertical-align: middle;\n  display: inline-block;\n  margin-top: -7px;\n}\n\n.section-button .icon {\n  margin-right: 4px;\n  top: -3px;\n}\n\n.section-button .content {\n  position: relative;\n  top: 2px;\n}\n\n.popup-top-line {\n  position: absolute;\n  border-radius: 2px 2px 0 0;\n  width: 100%;\n  height: 4px;\n  border: none;\n  top: 0;\n}\n\n.popup-arrow.left .popup-arrow-border {\n  border-top: 8px solid transparent;\n  border-right: 8px solid #d5d5d5;\n  border-bottom: 8px solid transparent;\n  border-left: none;\n  left: -7px;\n}\n\n.popup-arrow.left .popup-arrow-fill {\n  border-top: 7px solid transparent;\n  border-right: 7px solid #fff;\n  border-bottom: 7px solid transparent;\n  border-left: none;\n  top: -7px;\n  left: 1px;\n}\n\n.popup-arrow.right .popup-arrow-border {\n  border-top: 8px solid transparent;\n  border-right: none;\n  border-bottom: 8px solid transparent;\n  border-left: 8px solid #d5d5d5;\n  right: -7px;\n}\n\n.popup-arrow.right .popup-arrow-fill {\n  border-top: 7px solid transparent;\n  border-right: none;\n  border-bottom: 7px solid transparent;\n  border-left: 7px solid #fff;\n  top: -7px;\n  right: 1px;\n}\n"
  },
  {
    "path": "apps/calendar/src/css/popup/form.css",
    "content": ".form-container {\n  min-width: 474px;\n  box-shadow: 0 2px 6px 0 rgb(0 0 0 / 10%);\n  background-color: #fff;\n  border: 1px solid #d5d5d5;\n  padding: 17px;\n  border-radius: 2px;\n}\n\n.form-container .hidden-input {\n  display: none;\n}\n\n.form-container .grid-selection {\n  font-size: 11px;\n  font-weight: bold;\n}\n\n.popup-section-item {\n  height: 32px;\n  padding: 0 9px 0 12px;\n  border: 1px solid #d5d5d5;\n  display: inline-block;\n  font-size: 0;\n  border-radius: 2px;\n}\n\n.popup-section-item input {\n  border: none;\n  height: 30px;\n  outline: none;\n  display: inline-block;\n}\n\n.popup-section-item .content {\n  text-align: left;\n  display: inline-block;\n  font-size: 12px;\n  vertical-align: middle;\n  position: relative;\n  padding-left: 8px;\n}\n\n.popup-date-picker .content {\n  max-width: 125px;\n}\n\n.dropdown-section {\n  position: relative;\n}\n\n.dropdown-section.calendar-section {\n  width: 176px;\n}\n\n.dropdown-section .content {\n  line-height: 30px;\n}\n\n.popup-section-title input {\n  width: 365px;\n}\n\n.dot {\n  border-radius: 8px;\n  width: 12px;\n  height: 12px;\n  margin: 1px;\n}\n\n.content.event-calendar {\n  width: 125px;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  overflow: hidden;\n  top: -1px;\n}\n\n.popup-section-location .content {\n  width: 400px;\n}\n\n.popup-section-allday {\n  border: none;\n  padding: 0 0 0 8px;\n  cursor: pointer;\n}\n\n.popup-section-allday .ic-checkbox-normal {\n  display: inline-block;\n  cursor: pointer;\n  line-height: 14px;\n  margin: 0;\n  width: 14px;\n  height: 14px;\n  vertical-align: middle;\n}\n\n.popup-section-allday .content {\n  padding-left: 4px;\n}\n\n.popup-date-picker {\n  width: 176px;\n}\n\n.datepicker-container > div {\n  z-index: 1;\n}\n\n.popup-date-dash {\n  font-size: 12px;\n  color: #d5d5d5;\n  height: 32px;\n  padding: 0 4px;\n  vertical-align: middle;\n}\n\n.popup-button {\n  background: #fff;\n  border: 1px solid #d5d5d5;\n  border-radius: 2px;\n  text-align: center;\n  outline: none;\n  font-size: 12px;\n  cursor: pointer;\n  color: #333;\n}\n\n.popup-button.popup-section-private {\n  height: 32px;\n  padding: 8px;\n  font-size: 0;\n  margin-left: 4px;\n}\n\n.popup-button .event-state {\n  width: 58px;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  overflow: hidden;\n}\n\n.dropdown-section.state-section {\n  width: 109px;\n}\n\n.dropdown-section.state-section .popup-button {\n  width: 100%;\n}\n\n.state-section .content {\n  width: 58px;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  overflow: hidden;\n}\n\n.popup-section-item.dropdown-menu-item {\n  display: block;\n  height: 30px;\n  border: none;\n  cursor: pointer;\n}\n\n.dropdown-menu-item .content {\n  display: inline-block;\n  position: relative;\n  padding-left: 8px;\n  text-align: left;\n  font-size: 12px;\n  vertical-align: middle;\n}\n\n.popup-section-item.popup-button {\n  height: 32px;\n  font-size: 0;\n  top: -1px;\n}\n\n.popup-arrow.top .popup-arrow-border {\n  border-top: none;\n  border-right: 8px solid transparent;\n  border-bottom: 8px solid #d5d5d5;\n  border-left: 8px solid transparent;\n  left: calc(50% - 8px);\n  top: -7px;\n}\n\n.popup-arrow.top .popup-arrow-fill {\n  border-top: none;\n  border-right: 7px solid transparent;\n  border-bottom: 7px solid #fff;\n  border-left: 7px solid transparent;\n  left: -7px;\n  top: 1px;\n}\n\n.popup-arrow.bottom .popup-arrow-border {\n  border-top: 8px solid #d5d5d5;\n  border-right: 8px solid transparent;\n  border-bottom: none;\n  border-left: 8px solid transparent;\n  bottom: -7px;\n}\n\n.popup-arrow.bottom .popup-arrow-fill {\n  border-top: 7px solid #fff;\n  border-right: 7px solid transparent;\n  border-bottom: none;\n  border-left: 7px solid transparent;\n  left: -7px;\n  bottom: 1px;\n}\n"
  },
  {
    "path": "apps/calendar/src/css/popup/index.css",
    "content": "@import './common.css';\n@import './seeMore.css';\n@import './form.css';\n@import './detail.css';\n"
  },
  {
    "path": "apps/calendar/src/css/popup/seeMore.css",
    "content": ".see-more-container {\n  display: block;\n  position: absolute;\n  z-index: 1;\n}\n\n.see-more {\n  height: inherit;\n  padding: 5px;\n}\n\n.more-title-date {\n  font-size: 23px;\n  color: #333;\n}\n\n.more-title-day {\n  font-size: 12px;\n  color: #333;\n}\n\n.month-more-list {\n  overflow: auto;\n  padding: 0 17px;\n}\n\n.see-more-header {\n  position: relative;\n  border-bottom: none;\n}\n"
  },
  {
    "path": "apps/calendar/src/css/timegrid/column.css",
    "content": ".column {\n  position: relative;\n}\n\n.column .gridline-half {\n  position: absolute;\n  width: 100%;\n}\n\n.column .grid-selection {\n  position: absolute;\n  right: 10px;\n  left: 1px;\n  padding: 3px;\n}\n\n.column .grid-selection .grid-selection-label {\n  font-size: 11px;\n  font-weight: bold;\n}\n\n.column .events {\n  position: absolute;\n  left: 0;\n  top: 0;\n  right: 0;\n  bottom: 0;\n}\n"
  },
  {
    "path": "apps/calendar/src/css/timegrid/index.css",
    "content": "@import './timeColumn.css';\n@import './column.css';\n@import './timegrid.css';\n"
  },
  {
    "path": "apps/calendar/src/css/timegrid/timeColumn.css",
    "content": ".timegrid-time-column {\n  font-size: 11px;\n  height: 100%;\n}\n\n.timegrid-time-column .timegrid-hour-rows {\n  display: inline-block;\n  position: relative;\n  height: 100%;\n}\n\n.timegrid-time-column .timegrid-time {\n  text-align: right;\n  position: absolute;\n  right: 5px;\n  color: #333;\n}\n\n.timegrid-time-column .timegrid-time.timegrid-time-past {\n  font-weight: normal;\n}\n\n.timegrid-time-column .timegrid-time.timegrid-time-first {\n  line-height: normal;\n  visibility: hidden;\n}\n\n.timegrid-time-column .timegrid-time.timegrid-time-last {\n  height: 0;\n  visibility: hidden;\n}\n\n.timegrid-time-column .timegrid-time .timegrid-time-label,\n.timegrid-time-column .timegrid-time span {\n  transform: translateY(-50%);\n  position: absolute;\n  right: 0;\n}\n\n.timegrid-time-column .timegrid-current-time .timegrid-day-difference {\n  position: absolute;\n  right: 0;\n  bottom: 100%;\n}\n\n.timegrid-time-column .timegrid-time-hidden {\n  visibility: hidden;\n}\n\n.timegrid-time-column .timegrid-current-time {\n  position: absolute;\n  text-align: right;\n  right: 5px;\n  font-size: 11px;\n  font-weight: normal;\n  transform: translateY(-50%);\n}\n\n.timezone-labels-slot {\n  display: table;\n  table-layout: fixed;\n  position: absolute;\n  height: 40px;\n  border-bottom: 1px solid #e9e9e9;\n  background-color: #fff;\n}\n\n.timezone-labels-slot .timegrid-timezone-label {\n  display: table-cell;\n  background-color: #fff;\n  font-size: 11px;\n  border-right: 1px solid #e5e5e5;\n  vertical-align: middle;\n  padding-right: 5px;\n  text-align: right;\n}\n\n.timezone-labels-slot .timegrid-timezone-collapse-button {\n  position: absolute;\n  top: 2px;\n  bottom: 2px;\n  width: 10px;\n  border: 1px solid #dddddd;\n  border-left: none;\n  background: transparent;\n  cursor: pointer;\n}\n\n.timezone-labels-slot .timegrid-timezone-collapse-button .icon {\n  width: 4px;\n  height: 7px;\n  transform: translateX(-50%);\n}\n"
  },
  {
    "path": "apps/calendar/src/css/timegrid/timegrid.css",
    "content": ".panel.time {\n  overflow-y: auto;\n}\n\n.timegrid {\n  user-select: none;\n  position: relative;\n  height: 200%;\n  min-height: 900px;\n}\n\n.timegrid .timegrid-scroll-area {\n  position: relative;\n  height: 100%;\n}\n\n.timegrid .columns {\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  overflow: hidden;\n}\n\n.timegrid .columns .gridline-half {\n  position: absolute;\n  width: 100%;\n}\n\n.timegrid .columns .column {\n  display: inline-block;\n  height: 100%;\n}\n\n.timegrid .timegrid-now-indicator {\n  position: absolute;\n  left: 0;\n  right: 0;\n}\n\n.timegrid .timegrid-now-indicator .timegrid-now-indicator-left {\n  position: absolute;\n}\n\n.timegrid .timegrid-now-indicator .timegrid-now-indicator-marker {\n  position: absolute;\n  width: 9px;\n  height: 9px;\n  border-radius: 50%;\n  margin: -4px 0 0 -5px;\n}\n\n.timegrid .timegrid-now-indicator .timegrid-now-indicator-today {\n  position: absolute;\n}\n\n.timegrid .timegrid-now-indicator .timegrid-now-indicator-right {\n  position: absolute;\n  right: 0;\n}\n"
  },
  {
    "path": "apps/calendar/src/factory/__snapshots__/calendarCore.spec.tsx.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`getOptions/setOptions should get options 1`] = `\nObject {\n  \"defaultView\": \"week\",\n  \"eventFilter\": [Function],\n  \"gridSelection\": Object {\n    \"enableClick\": true,\n    \"enableDblClick\": true,\n  },\n  \"isReadOnly\": false,\n  \"month\": Object {\n    \"dayNames\": Array [\n      \"sun\",\n      \"mon\",\n      \"tue\",\n      \"wed\",\n      \"thu\",\n      \"fri\",\n      \"sat\",\n    ],\n    \"isAlways6Weeks\": true,\n    \"narrowWeekend\": false,\n    \"startDayOfWeek\": 0,\n    \"visibleEventCount\": 6,\n    \"visibleWeeksCount\": 0,\n    \"workweek\": false,\n  },\n  \"template\": Object {\n    \"allday\": [Function],\n    \"alldayTitle\": [Function],\n    \"collapseBtnTitle\": [Function],\n    \"comingDuration\": [Function],\n    \"endDatePlaceholder\": [Function],\n    \"goingDuration\": [Function],\n    \"locationPlaceholder\": [Function],\n    \"milestone\": [Function],\n    \"milestoneTitle\": [Function],\n    \"monthDayName\": [Function],\n    \"monthGridFooter\": [Function],\n    \"monthGridFooterExceed\": [Function],\n    \"monthGridHeader\": [Function],\n    \"monthGridHeaderExceed\": [Function],\n    \"monthMoreClose\": [Function],\n    \"monthMoreTitleDate\": [Function],\n    \"popupDelete\": [Function],\n    \"popupDetailAttendees\": [Function],\n    \"popupDetailBody\": [Function],\n    \"popupDetailDate\": [Function],\n    \"popupDetailLocation\": [Function],\n    \"popupDetailRecurrenceRule\": [Function],\n    \"popupDetailState\": [Function],\n    \"popupDetailTitle\": [Function],\n    \"popupEdit\": [Function],\n    \"popupIsAllday\": [Function],\n    \"popupSave\": [Function],\n    \"popupStateBusy\": [Function],\n    \"popupStateFree\": [Function],\n    \"popupUpdate\": [Function],\n    \"startDatePlaceholder\": [Function],\n    \"task\": [Function],\n    \"taskTitle\": [Function],\n    \"time\": [Function],\n    \"timegridDisplayPrimaryTime\": [Function],\n    \"timegridDisplayTime\": [Function],\n    \"timegridNowIndicatorLabel\": [Function],\n    \"timezoneDisplayLabel\": [Function],\n    \"titlePlaceholder\": [Function],\n    \"weekDayName\": [Function],\n    \"weekGridFooterExceed\": [Function],\n  },\n  \"theme\": Object {\n    \"common\": Object {\n      \"backgroundColor\": \"white\",\n      \"border\": \"1px solid #e5e5e5\",\n      \"dayName\": Object {\n        \"color\": \"#333\",\n      },\n      \"gridSelection\": Object {\n        \"backgroundColor\": \"rgba(81, 92, 230, 0.05)\",\n        \"border\": \"1px solid #515ce6\",\n      },\n      \"holiday\": Object {\n        \"color\": \"#ff4040\",\n      },\n      \"saturday\": Object {\n        \"color\": \"#333\",\n      },\n      \"today\": Object {\n        \"color\": \"#fff\",\n      },\n    },\n    \"month\": Object {\n      \"dayExceptThisMonth\": Object {\n        \"color\": \"rgba(51, 51, 51, 0.4)\",\n      },\n      \"dayName\": Object {\n        \"backgroundColor\": \"inherit\",\n        \"borderLeft\": \"none\",\n      },\n      \"gridCell\": Object {\n        \"footerHeight\": null,\n        \"headerHeight\": 31,\n      },\n      \"holidayExceptThisMonth\": Object {\n        \"color\": \"rgba(255, 64, 64, 0.4)\",\n      },\n      \"moreView\": Object {\n        \"backgroundColor\": \"white\",\n        \"border\": \"1px solid #d5d5d5\",\n        \"boxShadow\": \"0 2px 6px 0 rgba(0, 0, 0, 0.1)\",\n        \"height\": null,\n        \"width\": null,\n      },\n      \"moreViewTitle\": Object {\n        \"backgroundColor\": \"inherit\",\n      },\n      \"weekend\": Object {\n        \"backgroundColor\": \"inherit\",\n      },\n    },\n    \"week\": Object {\n      \"dayGrid\": Object {\n        \"backgroundColor\": \"inherit\",\n        \"borderRight\": \"1px solid #e5e5e5\",\n      },\n      \"dayGridLeft\": Object {\n        \"backgroundColor\": \"inherit\",\n        \"borderRight\": \"1px solid #e5e5e5\",\n        \"width\": \"72px\",\n      },\n      \"dayName\": Object {\n        \"backgroundColor\": \"inherit\",\n        \"borderBottom\": \"1px solid #e5e5e5\",\n        \"borderLeft\": \"none\",\n        \"borderTop\": \"1px solid #e5e5e5\",\n      },\n      \"futureTime\": Object {\n        \"color\": \"#333\",\n      },\n      \"gridSelection\": Object {\n        \"color\": \"#515ce6\",\n      },\n      \"nowIndicatorBullet\": Object {\n        \"backgroundColor\": \"#515ce6\",\n      },\n      \"nowIndicatorFuture\": Object {\n        \"border\": \"none\",\n      },\n      \"nowIndicatorLabel\": Object {\n        \"color\": \"#515ce6\",\n      },\n      \"nowIndicatorPast\": Object {\n        \"border\": \"1px dashed #515ce6\",\n      },\n      \"nowIndicatorToday\": Object {\n        \"border\": \"1px solid #515ce6\",\n      },\n      \"panelResizer\": Object {\n        \"border\": \"1px solid #e5e5e5\",\n      },\n      \"pastDay\": Object {\n        \"color\": \"#bbb\",\n      },\n      \"pastTime\": Object {\n        \"color\": \"#bbb\",\n      },\n      \"timeGrid\": Object {\n        \"borderRight\": \"1px solid #e5e5e5\",\n      },\n      \"timeGridHalfHourLine\": Object {\n        \"borderBottom\": \"none\",\n      },\n      \"timeGridHourLine\": Object {\n        \"borderBottom\": \"1px solid #e5e5e5\",\n      },\n      \"timeGridLeft\": Object {\n        \"backgroundColor\": \"inherit\",\n        \"borderRight\": \"1px solid #e5e5e5\",\n        \"width\": \"72px\",\n      },\n      \"timeGridLeftAdditionalTimezone\": Object {\n        \"backgroundColor\": \"white\",\n      },\n      \"today\": Object {\n        \"backgroundColor\": \"rgba(81, 92, 230, 0.05)\",\n        \"color\": \"inherit\",\n      },\n      \"weekend\": Object {\n        \"backgroundColor\": \"inherit\",\n      },\n    },\n  },\n  \"timezone\": Object {\n    \"zones\": Array [],\n  },\n  \"usageStatistics\": true,\n  \"useDetailPopup\": false,\n  \"useFormPopup\": false,\n  \"week\": Object {\n    \"collapseDuplicateEvents\": false,\n    \"dayNames\": Array [],\n    \"eventView\": true,\n    \"hourEnd\": 24,\n    \"hourStart\": 0,\n    \"narrowWeekend\": false,\n    \"showNowIndicator\": true,\n    \"showTimezoneCollapseButton\": false,\n    \"startDayOfWeek\": 0,\n    \"taskView\": true,\n    \"timezonesCollapsed\": false,\n    \"workweek\": false,\n  },\n}\n`;\n"
  },
  {
    "path": "apps/calendar/src/factory/calendar.tsx",
    "content": "import { h } from 'preact';\n\nimport { Main } from '@src/components/view/main';\nimport { VIEW_TYPE } from '@src/constants/view';\nimport CalendarCore from '@src/factory/calendarCore';\nimport { InvalidViewTypeError } from '@src/utils/error';\n\nimport type { Options, ViewType } from '@t/options';\n\n// TODO: move this function to a separate file such as util\nfunction isValidViewType(viewType: string): viewType is ViewType {\n  return !!Object.values(VIEW_TYPE).find((type) => type === viewType);\n}\n\n/**\n * Calendar class\n *\n * @class Calendar\n * @extends CalendarCore\n * @param {object} options - Calendar options. Check out {@link CalendarCore} for more information.\n */\nexport default class Calendar extends CalendarCore {\n  constructor(container: Element | string, options: Options = {}) {\n    super(container, options);\n\n    const { defaultView = 'week' } = options;\n\n    if (!isValidViewType(defaultView)) {\n      throw new InvalidViewTypeError(defaultView);\n    }\n\n    this.render();\n  }\n\n  protected getComponent() {\n    return <Main />;\n  }\n}\n"
  },
  {
    "path": "apps/calendar/src/factory/calendarCore.spec.tsx",
    "content": "import { h } from 'preact';\nimport { useEffect } from 'preact/hooks';\n\nimport sendHostname from 'tui-code-snippet/request/sendHostname';\n\nimport { HorizontalEvent } from '@src/components/events/horizontalEvent';\nimport { Layout } from '@src/components/layout';\nimport { GA_TRACKING_ID } from '@src/constants/statistics';\nimport { useDispatch, useStore } from '@src/contexts/calendarStore';\nimport { useAllTheme, useTheme } from '@src/contexts/themeStore';\nimport CalendarCore from '@src/factory/calendarCore';\nimport Month from '@src/factory/month';\nimport { isVisibleEvent } from '@src/helpers/events';\nimport { createDateMatrixOfMonth, getWeekDates } from '@src/helpers/grid';\nimport EventModel from '@src/model/eventModel';\nimport EventUIModel from '@src/model/eventUIModel';\nimport type { GridSelectionType } from '@src/slices/gridSelection';\nimport { act, screen, waitFor } from '@src/test/utils';\nimport TZDate from '@src/time/date';\nimport { addDate, subtractDate } from '@src/time/datetime';\n\nimport type { CalendarInfo } from '@t/options';\n\njest.mock('tui-code-snippet/request/sendHostname');\n\nfunction cleanup() {\n  document.body.innerHTML = '';\n}\n\nfunction MockComponent() {\n  const events = useStore((state) => state.calendar.events.toArray());\n\n  return events.length > 0 ? (\n    <div>\n      {events.map((event) => (\n        <div key={event.id}>event</div>\n      ))}\n    </div>\n  ) : (\n    <div>There is no events</div>\n  );\n}\n\nclass MockCalendar extends CalendarCore {\n  protected getComponent() {\n    return <MockComponent />;\n  }\n}\nlet mockCalendar: MockCalendar;\n\ndescribe('changeView/getViewName', () => {\n  beforeEach(() => {\n    const container = document.createElement('div');\n    document.body.appendChild(container);\n    mockCalendar = new MockCalendar(container);\n    act(() => {\n      mockCalendar.render();\n    });\n  });\n\n  afterEach(() => {\n    cleanup();\n  });\n\n  it('should return current view name', () => {\n    // Given\n\n    // When\n\n    // Then\n    expect(mockCalendar.getViewName()).toBe('week'); // Initial view is 'week'\n  });\n\n  it('should change current view to week', () => {\n    // Given\n\n    // When\n    mockCalendar.changeView('month');\n\n    // Then\n    expect(mockCalendar.getViewName()).toBe('month');\n  });\n\n  it('should change current view to day', () => {\n    // Given\n\n    // When\n    mockCalendar.changeView('day');\n\n    // Then\n    expect(mockCalendar.getViewName()).toBe('day');\n  });\n});\n\n// @TODO: Add more test cases for multiple events\ndescribe('createEvents', () => {\n  beforeEach(() => {\n    const container = document.createElement('div');\n    document.body.appendChild(container);\n    mockCalendar = new MockCalendar(container);\n  });\n\n  afterEach(() => {\n    cleanup();\n  });\n\n  it('should render 1 event', () => {\n    // Given\n    act(() => {\n      mockCalendar.render();\n    });\n    act(() => {\n      mockCalendar.createEvents([\n        {\n          id: '1',\n          calendarId: '1',\n          title: 'my event',\n          category: 'time',\n          dueDateClass: '',\n          start: '2018-01-18T22:30:00+09:00',\n          end: '2018-01-19T02:30:00+09:00',\n        },\n      ]);\n    });\n\n    // When\n\n    // Then\n    expect(screen.queryByText('event')).toBeInTheDocument();\n  });\n});\n\ndescribe('clear', () => {\n  beforeEach(() => {\n    const container = document.createElement('div');\n    document.body.appendChild(container);\n    mockCalendar = new MockCalendar(container);\n  });\n\n  afterEach(() => {\n    cleanup();\n  });\n\n  it('should clear events', () => {\n    // Given\n    act(() => {\n      mockCalendar.render();\n    });\n    act(() => {\n      mockCalendar.createEvents([\n        {\n          id: '1',\n          calendarId: '1',\n          title: 'my event',\n          category: 'time',\n          dueDateClass: '',\n          start: '2018-01-18T22:30:00+09:00',\n          end: '2018-01-19T02:30:00+09:00',\n        },\n      ]);\n    });\n    expect(screen.queryByText('event')).toBeInTheDocument();\n\n    // When\n    act(() => {\n      mockCalendar.clear();\n    });\n\n    // Then\n    expect(screen.queryByText('There is no events')).toBeInTheDocument();\n  });\n});\n\ndescribe('destroy', () => {\n  let container: HTMLDivElement;\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n    mockCalendar = new MockCalendar(container);\n    act(() => {\n      mockCalendar.render();\n    });\n  });\n\n  afterEach(() => {\n    cleanup();\n  });\n\n  it('should remove all calendar properties', () => {\n    // Given\n    const properties = Object.keys(mockCalendar) as (keyof MockCalendar)[];\n\n    // When\n    act(() => {\n      mockCalendar.destroy();\n    });\n\n    // Then\n    expect(container.innerHTML).toMatchInlineSnapshot(`\"\"`);\n    properties.forEach((property) => {\n      expect(mockCalendar[property]).toBeUndefined();\n    });\n  });\n});\n\ndescribe('openFormPopup', () => {\n  class MockPopupCalendar extends CalendarCore {\n    protected getComponent() {\n      return <Layout>mock</Layout>; // popup component is rendered in Layout component\n    }\n  }\n\n  let mockPopupCalendar: MockPopupCalendar;\n\n  beforeEach(() => {\n    const container = document.createElement('div');\n    document.body.appendChild(container);\n    mockPopupCalendar = new MockPopupCalendar(container);\n    act(() => {\n      mockPopupCalendar.render();\n    });\n  });\n\n  afterEach(() => {\n    cleanup();\n  });\n\n  it('should open form popup', () => {\n    // Given\n    expect(screen.queryByRole('dialog')).not.toBeInTheDocument();\n\n    // When\n    act(() => {\n      mockPopupCalendar.openFormPopup({ title: 'my event' });\n    });\n\n    // Then\n    expect(screen.queryByRole('dialog')).toBeInTheDocument();\n  });\n});\n\ndescribe('getDate/setDate', () => {\n  beforeEach(() => {\n    const container = document.createElement('div');\n    document.body.appendChild(container);\n    mockCalendar = new MockCalendar(container);\n  });\n\n  afterEach(() => {\n    cleanup();\n  });\n\n  it('should get current renderDate', () => {\n    // Given\n    const today = new TZDate();\n\n    // When\n\n    // Then\n    expect(mockCalendar.getDate()).toBeSameDate(today);\n  });\n\n  it('should set renderDate when param is TZDate', () => {\n    // Given\n    const targetDate = addDate(new TZDate(), 1);\n\n    // When\n    act(() => {\n      mockCalendar.setDate(targetDate);\n    });\n\n    // Then\n    expect(mockCalendar.getDate()).toBeSameDate(targetDate);\n  });\n\n  it('should set renderDate when param is string', () => {\n    // Given\n    const targetDate = '2022-03-15';\n    const expected = new TZDate(targetDate);\n\n    // When\n    act(() => {\n      mockCalendar.setDate(targetDate);\n    });\n\n    // Then\n    expect(mockCalendar.getDate()).toBeSameDate(expected);\n  });\n\n  it('should set renderDate when param is number', () => {\n    // Given\n    const targetDate = 1647314395599; // 2022-03-15\n    const expected = new TZDate(targetDate);\n\n    // When\n    act(() => {\n      mockCalendar.setDate(targetDate);\n    });\n\n    // Then\n    expect(mockCalendar.getDate()).toBeSameDate(expected);\n  });\n\n  it('should set renderDate when param is Date', () => {\n    // Given\n    const targetDate = new Date('2022-03-15');\n    const expected = new TZDate(targetDate);\n\n    // When\n    act(() => {\n      mockCalendar.setDate(targetDate);\n    });\n\n    // Then\n    expect(mockCalendar.getDate()).toBeSameDate(expected);\n  });\n});\n\ndescribe('prev/next/today', () => {\n  let container: HTMLDivElement;\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n  });\n\n  afterEach(() => {\n    cleanup();\n  });\n\n  describe('month view', () => {\n    function MockMonthView() {\n      const { renderDate } = useStore((state) => state.view);\n      const month = renderDate.getMonth();\n\n      return <div>month: {month}</div>;\n    }\n    class MockCalendarMonth extends CalendarCore {\n      protected getComponent() {\n        return <MockMonthView />;\n      }\n    }\n\n    let mockCalendarMonth: MockCalendarMonth;\n    const MONTHS_IN_YEAR = 12;\n\n    beforeEach(() => {\n      mockCalendarMonth = new MockCalendarMonth(container);\n      act(() => {\n        mockCalendarMonth.changeView('month');\n        mockCalendarMonth.render();\n      });\n    });\n\n    it('should render this month by default', () => {\n      // Given\n      const today = new TZDate();\n      const thisMonth = today.getMonth();\n\n      // When\n\n      // Then\n      expect(screen.queryByText(`month: ${thisMonth}`)).toBeInTheDocument();\n    });\n\n    it('should render next month when next is called', () => {\n      // Given\n      const today = new TZDate();\n      const thisMonth = today.getMonth();\n      const nextMonth = (thisMonth + 1) % MONTHS_IN_YEAR; // 0 ~ 11\n      expect(screen.queryByText(`month: ${thisMonth}`)).toBeInTheDocument();\n\n      // When\n      act(() => {\n        mockCalendarMonth.next();\n      });\n\n      // Then\n      expect(screen.queryByText(`month: ${thisMonth}`)).not.toBeInTheDocument();\n      expect(screen.queryByText(`month: ${nextMonth}`)).toBeInTheDocument();\n    });\n\n    it('should render prev month when prev is called', () => {\n      // Given\n      const today = new TZDate();\n      const thisMonth = today.getMonth();\n      const prevMonth = (thisMonth - 1 + MONTHS_IN_YEAR) % MONTHS_IN_YEAR; // 0 ~ 11\n      expect(screen.queryByText(`month: ${thisMonth}`)).toBeInTheDocument();\n\n      // When\n      act(() => {\n        mockCalendarMonth.prev();\n      });\n\n      // Then\n      expect(screen.queryByText(`month: ${thisMonth}`)).not.toBeInTheDocument();\n      expect(screen.queryByText(`month: ${prevMonth}`)).toBeInTheDocument();\n    });\n\n    it(`should render today's month when today is called`, () => {\n      // Given\n      const today = new TZDate();\n      const thisMonth = today.getMonth();\n      act(() => {\n        mockCalendarMonth.next();\n      });\n      expect(screen.queryByText(`month: ${thisMonth}`)).not.toBeInTheDocument();\n\n      // When\n      act(() => {\n        mockCalendarMonth.today();\n      });\n\n      // Then\n      expect(screen.queryByText(`month: ${thisMonth}`)).toBeInTheDocument();\n    });\n  });\n\n  describe('week view', () => {\n    const defaultWeekOptions = { startDayOfWeek: 0, workweek: false };\n    function MockWeekView() {\n      const { renderDate } = useStore((state) => state.view);\n      const weekDates = getWeekDates(renderDate, defaultWeekOptions);\n\n      return (\n        <div>\n          {weekDates.map((weekDate) => (\n            <div key={weekDate.getDate()}>date: {weekDate.getDate()}</div>\n          ))}\n        </div>\n      );\n    }\n    class MockCalendarWeek extends CalendarCore {\n      protected getComponent() {\n        return <MockWeekView />;\n      }\n    }\n\n    let mockCalendarWeek: MockCalendarWeek;\n\n    beforeEach(() => {\n      mockCalendarWeek = new MockCalendarWeek(container);\n      act(() => {\n        mockCalendarWeek.render();\n      });\n    });\n\n    it('should render this week by default', () => {\n      // Given\n      const today = new TZDate();\n      const weekDates = getWeekDates(today, defaultWeekOptions);\n      const dates = weekDates.map((weekDate) => weekDate.getDate());\n\n      // When\n\n      // Then\n      dates.forEach((date) => {\n        expect(screen.queryByText(`date: ${date}`)).toBeInTheDocument();\n      });\n    });\n\n    it('should render next week when next is called', () => {\n      // Given\n      const today = new TZDate();\n      const thisWeekDates = getWeekDates(today, defaultWeekOptions);\n      const nextWeekDates = getWeekDates(addDate(today, 7), defaultWeekOptions);\n\n      const thisDates = thisWeekDates.map((weekDate) => weekDate.getDate());\n      const nextDates = nextWeekDates.map((weekDate) => weekDate.getDate());\n\n      thisDates.forEach((date) => {\n        expect(screen.queryByText(`date: ${date}`)).toBeInTheDocument();\n      });\n\n      // When\n      act(() => {\n        mockCalendarWeek.next();\n      });\n\n      // Then\n      thisDates.forEach(async (date) => {\n        await waitFor(() => expect(screen.queryByText(`date: ${date}`)).not.toBeInTheDocument());\n      });\n      nextDates.forEach(async (date) => {\n        await waitFor(() => expect(screen.queryByText(`date: ${date}`)).toBeInTheDocument());\n      });\n    });\n\n    it('should render prev week when prev is called', () => {\n      // Given\n      const today = new TZDate();\n      const thisWeekDates = getWeekDates(today, defaultWeekOptions);\n      const prevWeekDates = getWeekDates(subtractDate(today, 7), defaultWeekOptions);\n\n      const thisDates = thisWeekDates.map((weekDate) => weekDate.getDate());\n      const prevDates = prevWeekDates.map((weekDate) => weekDate.getDate());\n\n      thisDates.forEach((date) => {\n        expect(screen.queryByText(`date: ${date}`)).toBeInTheDocument();\n      });\n\n      // When\n      act(() => {\n        mockCalendarWeek.prev();\n      });\n\n      // Then\n      thisDates.forEach(async (date) => {\n        await waitFor(() => expect(screen.queryByText(`date: ${date}`)).not.toBeInTheDocument());\n      });\n      prevDates.forEach(async (date) => {\n        await waitFor(() => expect(screen.queryByText(`date: ${date}`)).toBeInTheDocument());\n      });\n    });\n\n    it('should render this week when today is called', () => {\n      // Given\n      const today = new TZDate();\n      const thisWeekDates = getWeekDates(today, defaultWeekOptions);\n      const thisDates = thisWeekDates.map((weekDate) => weekDate.getDate());\n\n      act(() => {\n        mockCalendarWeek.next();\n      });\n\n      thisDates.forEach(async (date) => {\n        await waitFor(() => expect(screen.queryByText(`date: ${date}`)).not.toBeInTheDocument());\n      });\n\n      // When\n      act(() => {\n        mockCalendarWeek.today();\n      });\n\n      // Then\n      thisDates.forEach((date) => {\n        expect(screen.queryByText(`date: ${date}`)).toBeInTheDocument();\n      });\n    });\n  });\n\n  describe('day view', () => {\n    function MockDayView() {\n      const { renderDate } = useStore((state) => state.view);\n\n      return (\n        <div>\n          <div>date: {renderDate.getDate()}</div>\n        </div>\n      );\n    }\n    class MockCalendarDay extends CalendarCore {\n      protected getComponent() {\n        return <MockDayView />;\n      }\n    }\n\n    let mockCalendarDay: MockCalendarDay;\n\n    beforeEach(() => {\n      mockCalendarDay = new MockCalendarDay(container);\n      act(() => {\n        mockCalendarDay.changeView('day');\n        mockCalendarDay.render();\n      });\n    });\n\n    it('should render today by default', () => {\n      // Given\n      const today = new TZDate();\n\n      // When\n\n      // Then\n      expect(screen.queryByText(`date: ${today.getDate()}`)).toBeInTheDocument();\n    });\n\n    it('should render tomorrow when next is called', () => {\n      // Given\n      const today = new TZDate();\n      const tomorrow = addDate(today, 1);\n\n      expect(screen.queryByText(`date: ${today.getDate()}`)).toBeInTheDocument();\n\n      // When\n      act(() => {\n        mockCalendarDay.next();\n      });\n\n      // Then\n      expect(screen.queryByText(`date: ${today.getDate()}`)).not.toBeInTheDocument();\n      expect(screen.queryByText(`date: ${tomorrow.getDate()}`)).toBeInTheDocument();\n    });\n\n    it('should render yesterday when prev is called', () => {\n      // Given\n      const today = new TZDate();\n      const yesterday = subtractDate(today, 1);\n\n      expect(screen.queryByText(`date: ${today.getDate()}`)).toBeInTheDocument();\n\n      // When\n      act(() => {\n        mockCalendarDay.prev();\n      });\n\n      // Then\n      expect(screen.queryByText(`date: ${today.getDate()}`)).not.toBeInTheDocument();\n      expect(screen.queryByText(`date: ${yesterday.getDate()}`)).toBeInTheDocument();\n    });\n\n    it('should render today when today is called', () => {\n      // Given\n      const today = new TZDate();\n\n      act(() => {\n        mockCalendarDay.next();\n      });\n      expect(screen.queryByText(`date: ${today.getDate()}`)).not.toBeInTheDocument();\n\n      // When\n      act(() => {\n        mockCalendarDay.today();\n      });\n\n      // Then\n      expect(screen.queryByText(`date: ${today.getDate()}`)).toBeInTheDocument();\n    });\n  });\n});\n\ndescribe('setTheme', () => {\n  function MockThemeView() {\n    const { common, week, month } = useAllTheme();\n    const {\n      gridSelection: { backgroundColor },\n    } = common;\n    const {\n      nowIndicatorLabel: { color },\n    } = week;\n    const {\n      moreView: { boxShadow },\n    } = month;\n\n    return (\n      <div>\n        <div>gridSelection: {backgroundColor}</div>\n        <div>nowIndicatorLabel: {color}</div>\n        <div>moreView: {boxShadow}</div>\n      </div>\n    );\n  }\n  class MockCalendarTheme extends CalendarCore {\n    protected getComponent() {\n      return <MockThemeView />;\n    }\n  }\n\n  let container: HTMLDivElement;\n  let mockCalendarTheme: MockCalendarTheme;\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n    mockCalendarTheme = new MockCalendarTheme(container);\n    act(() => {\n      mockCalendarTheme.render();\n    });\n  });\n\n  afterEach(() => {\n    cleanup();\n  });\n\n  it('should change theme and rerender components', () => {\n    // Given\n    const gridSelectionBackgroundColor = '#ff0000';\n    const nowIndicatorLabelColor = '#00ff00';\n    const moreViewBoxShadow = '0 0 10px #0000ff';\n\n    expect(\n      screen.queryByText(`gridSelection: ${gridSelectionBackgroundColor}`)\n    ).not.toBeInTheDocument();\n    expect(\n      screen.queryByText(`nowIndicatorLabel: ${nowIndicatorLabelColor}`)\n    ).not.toBeInTheDocument();\n    expect(screen.queryByText(`moreView: ${moreViewBoxShadow}`)).not.toBeInTheDocument();\n\n    // When\n    act(() => {\n      mockCalendarTheme.setTheme({\n        common: {\n          gridSelection: { backgroundColor: gridSelectionBackgroundColor },\n        },\n        week: {\n          nowIndicatorLabel: { color: nowIndicatorLabelColor },\n        },\n        month: {\n          moreView: { boxShadow: moreViewBoxShadow },\n        },\n      });\n    });\n\n    // Then\n    expect(\n      screen.queryByText(`gridSelection: ${gridSelectionBackgroundColor}`)\n    ).toBeInTheDocument();\n    expect(screen.queryByText(`nowIndicatorLabel: ${nowIndicatorLabelColor}`)).toBeInTheDocument();\n    expect(screen.queryByText(`moreView: ${moreViewBoxShadow}`)).toBeInTheDocument();\n  });\n});\n\ndescribe('getOptions/setOptions', () => {\n  function MockOptionsView() {\n    const options = useStore((state) => state.options);\n    const { taskTitle } = useStore((state) => state.template);\n    const { dispatch, ...themeValues } = useTheme((theme) => theme);\n    const { defaultView, useFormPopup } = options;\n\n    return (\n      <div>\n        <div data-testid=\"defaultView\">{defaultView}</div>\n        <div data-testid=\"useFormPopup\">{String(useFormPopup)}</div>\n        <div data-testid=\"theme\">{JSON.stringify(themeValues)}</div>\n        <div data-testid=\"template\">{taskTitle()}</div>\n      </div>\n    );\n  }\n  class MockCalendarOptions extends CalendarCore {\n    protected getComponent() {\n      return <MockOptionsView />;\n    }\n  }\n\n  let container: HTMLDivElement;\n  let mockCalendarOptions: MockCalendarOptions;\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n    mockCalendarOptions = new MockCalendarOptions(container);\n    act(() => {\n      mockCalendarOptions.render();\n    });\n  });\n\n  afterEach(() => {\n    cleanup();\n  });\n\n  it('should get options', () => {\n    // When\n    const options = mockCalendarOptions.getOptions();\n\n    // Then\n    expect(options).toMatchSnapshot();\n  });\n\n  it('should change options', () => {\n    // Given\n    const defaultView = 'month';\n    const useFormPopup = true;\n    const previousThemeValue = screen.getByTestId('theme').textContent;\n\n    expect(screen.getByTestId('defaultView')).toHaveTextContent('week');\n    expect(screen.getByTestId('useFormPopup')).toHaveTextContent('false');\n    expect(screen.getByTestId('template')).toHaveTextContent('Task');\n\n    // When\n    act(() => {\n      mockCalendarOptions.setOptions({\n        defaultView,\n        useFormPopup,\n        theme: {\n          common: {\n            backgroundColor: '#ff0000',\n          },\n        },\n        template: {\n          taskTitle: () => 'Super Task',\n        },\n      });\n    });\n\n    // Then\n    expect(screen.getByTestId('defaultView')).toHaveTextContent(defaultView);\n    expect(screen.getByTestId('useFormPopup')).toHaveTextContent(String(useFormPopup));\n    expect(screen.getByTestId('theme').textContent).not.toEqual(previousThemeValue);\n    expect(screen.getByTestId('template')).toHaveTextContent('Super Task');\n  });\n});\n\ndescribe('setCalendars', () => {\n  function MockCalendarsView() {\n    const { calendars } = useStore((state) => state.calendar);\n\n    return (\n      <div>\n        {calendars.map((calendar) => (\n          <div key={calendar.id}>\n            {calendar.id} {calendar.name}\n          </div>\n        ))}\n      </div>\n    );\n  }\n  class MockCalendarCalendars extends CalendarCore {\n    protected getComponent() {\n      return <MockCalendarsView />;\n    }\n  }\n\n  let container: HTMLDivElement;\n  let mockCalendarCalendars: MockCalendarCalendars;\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n    mockCalendarCalendars = new MockCalendarCalendars(container);\n    act(() => {\n      mockCalendarCalendars.render();\n    });\n  });\n\n  afterEach(() => {\n    cleanup();\n  });\n\n  it('should change calendar list', () => {\n    // Given\n    const calendars: CalendarInfo[] = [\n      {\n        id: '1',\n        name: 'calendar1',\n      },\n      {\n        id: '2',\n        name: 'calendar2',\n      },\n    ];\n    calendars.forEach((calendar) => {\n      expect(screen.queryByText(`${calendar.id} ${calendar.name}`)).not.toBeInTheDocument();\n    });\n\n    // When\n    act(() => {\n      mockCalendarCalendars.setCalendars(calendars);\n    });\n\n    // Then\n    calendars.forEach((calendar) => {\n      expect(screen.queryByText(`${calendar.id} ${calendar.name}`)).toBeInTheDocument();\n    });\n  });\n});\n\ndescribe('setCalendarColor', () => {\n  function MockCalendarColorView() {\n    const { calendars } = useStore((state) => state.calendar);\n\n    return (\n      <div>\n        {calendars.map(({ id, color, backgroundColor, borderColor, dragBackgroundColor }) => (\n          <div key={id}>\n            {id}-{color}-{backgroundColor}-{borderColor}-{dragBackgroundColor}\n          </div>\n        ))}\n      </div>\n    );\n  }\n  class MockCalendarColor extends CalendarCore {\n    protected getComponent() {\n      return <MockCalendarColorView />;\n    }\n  }\n\n  let container: HTMLDivElement;\n  let mockCalendarColor: MockCalendarColor;\n  const calendarIdToChange = '1';\n  const calendarIdToDoNotChange = '2';\n  const color = '#ff0';\n  const backgroundColor = '#ff0';\n  const borderColor = '#ff0';\n  const dragBackgroundColor = '#ff0';\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n    mockCalendarColor = new MockCalendarColor(container);\n    act(() => {\n      mockCalendarColor.setCalendars([\n        {\n          id: '1',\n          name: 'calendar1',\n          color: '#fff',\n          backgroundColor: '#fff',\n          borderColor: '#fff',\n          dragBackgroundColor: '#fff',\n        },\n        {\n          id: '2',\n          name: 'calendar2',\n          color: '#000',\n          backgroundColor: '#000',\n          borderColor: '#000',\n          dragBackgroundColor: '#000',\n        },\n      ]);\n      mockCalendarColor.render();\n    });\n  });\n\n  afterEach(() => {\n    cleanup();\n  });\n\n  it('should change the calendar color that matches the calendar id', () => {\n    // Given\n    expect(\n      screen.queryByText(\n        `${calendarIdToChange}-${color}-${backgroundColor}-${borderColor}-${dragBackgroundColor}`\n      )\n    ).not.toBeInTheDocument();\n\n    // When\n    act(() => {\n      mockCalendarColor.setCalendarColor(calendarIdToChange, {\n        color,\n        backgroundColor,\n        borderColor,\n        dragBackgroundColor,\n      });\n    });\n\n    // Then\n    expect(\n      screen.queryByText(\n        `${calendarIdToChange}-${color}-${backgroundColor}-${borderColor}-${dragBackgroundColor}`\n      )\n    ).toBeInTheDocument();\n  });\n\n  it(`should not change the calendar color that doesn't matches the calendar id`, () => {\n    // Given\n    const notChangedColor = '#000';\n    const notChangedBackgroundColor = '#000';\n    const notChangedBorderColor = '#000';\n    const notChangedDragBackgroundColor = '#000';\n    expect(\n      screen.queryByText(\n        `${calendarIdToDoNotChange}-${notChangedColor}-${notChangedBackgroundColor}-${notChangedBorderColor}-${notChangedDragBackgroundColor}`\n      )\n    ).toBeInTheDocument();\n\n    // When\n    act(() => {\n      mockCalendarColor.setCalendarColor(calendarIdToChange, {\n        color,\n        backgroundColor,\n        borderColor,\n        dragBackgroundColor,\n      });\n    });\n\n    // Then\n    expect(\n      screen.queryByText(\n        `${calendarIdToDoNotChange}-${notChangedColor}-${notChangedBackgroundColor}-${notChangedBorderColor}-${notChangedDragBackgroundColor}`\n      )\n    ).toBeInTheDocument();\n  });\n});\n\ndescribe('hideMoreView', () => {\n  function MockMonthMoreViewPopup() {\n    const { showSeeMorePopup } = useDispatch('popup');\n\n    useEffect(() => {\n      showSeeMorePopup({ date: new TZDate(), events: [], popupPosition: { top: 0, left: 0 } });\n    }, [showSeeMorePopup]);\n\n    return <Layout>mock</Layout>; // popup component is rendered in Layout component\n  }\n  class MockCalendarMonthMoreViewPopup extends Month {\n    protected getComponent() {\n      return <MockMonthMoreViewPopup />;\n    }\n  }\n\n  let mockMonthView: MockCalendarMonthMoreViewPopup;\n\n  beforeEach(() => {\n    const container = document.createElement('div');\n    document.body.appendChild(container);\n    mockMonthView = new MockCalendarMonthMoreViewPopup(container);\n    act(() => {\n      mockMonthView.render();\n    });\n  });\n\n  afterEach(() => {\n    cleanup();\n  });\n\n  it('should hide see more popup', () => {\n    // Given\n    expect(screen.queryByRole('dialog')).toBeInTheDocument();\n\n    // When\n    act(() => {\n      mockMonthView.hideMoreView();\n    });\n\n    // Then\n    expect(screen.queryByRole('dialog')).not.toBeInTheDocument();\n  });\n});\n\ndescribe('getElement', () => {\n  const eventModel = new EventModel({ calendarId: 'mockCalendarId', id: 'mockEventId' });\n  const mockEventUIModel = new EventUIModel(eventModel);\n  class MockCalenderEvent extends CalendarCore {\n    protected getComponent() {\n      return (\n        <div>\n          <HorizontalEvent eventHeight={20} headerHeight={0} uiModel={mockEventUIModel} />\n          mock event\n        </div>\n      );\n    }\n  }\n\n  let mockCalenderEvent: MockCalenderEvent;\n\n  beforeEach(() => {\n    const container = document.createElement('div');\n    document.body.appendChild(container);\n    mockCalenderEvent = new MockCalenderEvent(container);\n    act(() => {\n      mockCalenderEvent.createEvents([eventModel]);\n      mockCalenderEvent.render();\n    });\n  });\n\n  afterEach(() => {\n    cleanup();\n  });\n\n  it('should find event element', () => {\n    // Given\n    const eventElement = screen.getByText('mock event').firstChild;\n\n    // When\n    const expectedEventElement = mockCalenderEvent.getElement('mockEventId', 'mockCalendarId');\n\n    // Then\n    expect(eventElement).toEqual(expectedEventElement);\n  });\n});\n\ndescribe('setCalendarVisibility', () => {\n  const eventModel1 = new EventModel({\n    calendarId: 'mockCalendarId1',\n    id: 'mockEventId1',\n    title: 'mockEvent1',\n    category: 'allday',\n    start: new TZDate(2022, 3, 22),\n    end: new TZDate(2022, 3, 24),\n  });\n  const eventModel2 = new EventModel({\n    calendarId: 'mockCalendarId2',\n    id: 'mockEventId2',\n    title: 'mockEvent2',\n    category: 'allday',\n    start: new TZDate(2022, 3, 23),\n    end: new TZDate(2022, 3, 25),\n  });\n  function MockHorizontalEvents() {\n    const events = useStore((state) => state.calendar.events.toArray());\n    const filteredEvents = events.filter((event) => isVisibleEvent(event));\n\n    return (\n      <div>\n        {filteredEvents.map((event) => (\n          <HorizontalEvent\n            key={event.id}\n            eventHeight={20}\n            headerHeight={0}\n            uiModel={new EventUIModel(event)}\n          />\n        ))}\n      </div>\n    );\n  }\n  class MockCalenderEvent extends CalendarCore {\n    protected getComponent() {\n      return <MockHorizontalEvents />;\n    }\n  }\n\n  let mockCalenderEvent: MockCalenderEvent;\n\n  beforeEach(() => {\n    const container = document.createElement('div');\n    document.body.appendChild(container);\n    mockCalenderEvent = new MockCalenderEvent(container);\n    act(() => {\n      mockCalenderEvent.createEvents([eventModel1, eventModel2]);\n      mockCalenderEvent.render();\n    });\n  });\n\n  afterEach(() => {\n    cleanup();\n  });\n\n  it('should hide events', () => {\n    // Given\n    expect(screen.queryByText('mockEvent1')).toBeInTheDocument();\n    expect(screen.queryByText('mockEvent2')).toBeInTheDocument();\n\n    // When\n    act(() => {\n      mockCalenderEvent.setCalendarVisibility('mockCalendarId1', false);\n    });\n\n    // Then\n    expect(screen.queryByText('mockEvent1')).not.toBeInTheDocument();\n    expect(screen.queryByText('mockEvent2')).toBeInTheDocument();\n  });\n\n  it('should toggle events', () => {\n    // Given\n    act(() => {\n      mockCalenderEvent.setCalendarVisibility('mockCalendarId1', false);\n    });\n    expect(screen.queryByText('mockEvent1')).not.toBeInTheDocument();\n    expect(screen.queryByText('mockEvent2')).toBeInTheDocument();\n\n    // When\n    act(() => {\n      mockCalenderEvent.setCalendarVisibility('mockCalendarId1', true);\n    });\n\n    // Then\n    expect(screen.queryByText('mockEvent1')).toBeInTheDocument();\n    expect(screen.queryByText('mockEvent2')).toBeInTheDocument();\n  });\n});\n\ndescribe('usageStatistics option', () => {\n  let container: HTMLDivElement;\n\n  beforeAll(() => {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n  });\n\n  afterEach(() => {\n    mockCalendar.destroy();\n    jest.clearAllMocks();\n  });\n\n  afterAll(() => {\n    cleanup();\n  });\n\n  it('should send a hostname when it is not specified.', () => {\n    // Given\n    mockCalendar = new MockCalendar(container);\n\n    // When\n    // Nothing\n\n    // Then\n    expect(sendHostname).toHaveBeenCalledWith('calendar', GA_TRACKING_ID);\n  });\n\n  it('should send a hostname when it is true.', () => {\n    // Given\n    mockCalendar = new MockCalendar(container, { usageStatistics: true });\n\n    // When\n    // Nothing\n\n    // Then\n    expect(sendHostname).toHaveBeenCalledWith('calendar', GA_TRACKING_ID);\n  });\n\n  it('should not send a hostname when it is false.', () => {\n    // Given\n    mockCalendar = new MockCalendar(container, { usageStatistics: false });\n\n    // When\n    // Nothing\n\n    // Then\n    expect(sendHostname).not.toHaveBeenCalled();\n  });\n});\n\ndescribe('getDateRangeStart/getDateRangeEnd', () => {\n  beforeEach(() => {\n    const container = document.createElement('div');\n    document.body.appendChild(container);\n    mockCalendar = new MockCalendar(container);\n    act(() => {\n      mockCalendar.render();\n    });\n  });\n\n  afterEach(() => {\n    cleanup();\n  });\n\n  describe('when setDate is called', () => {\n    it('should set renderRange on day view', () => {\n      // Given\n      const targetDate = new Date('2022-06-15'); // Wed\n      const expectedStartDate = new TZDate(targetDate);\n      const expectedEndDate = new TZDate(targetDate);\n\n      // When\n      act(() => {\n        mockCalendar.changeView('day');\n        mockCalendar.setDate(targetDate);\n      });\n\n      // Then\n      expect(mockCalendar.getDateRangeStart()).toBeSameDate(expectedStartDate);\n      expect(mockCalendar.getDateRangeEnd()).toBeSameDate(expectedEndDate);\n    });\n\n    it('should set renderRange on week view', () => {\n      // Given\n      const targetDate = new Date('2022-06-15'); // Wed\n      const expectedStartDate = new TZDate('2022-06-12');\n      const expectedEndDate = new TZDate('2022-06-18');\n\n      // When\n      act(() => {\n        mockCalendar.changeView('week');\n        mockCalendar.setDate(targetDate);\n      });\n\n      // Then\n      expect(mockCalendar.getDateRangeStart()).toBeSameDate(expectedStartDate);\n      expect(mockCalendar.getDateRangeEnd()).toBeSameDate(expectedEndDate);\n    });\n\n    it('should set renderRange on month when isAlways6Weeks is true', () => {\n      // Given\n      const targetDate = new Date('2022-06-15'); // Wed\n      const expectedStartDate = new TZDate('2022-05-29');\n      const expectedEndDate = new TZDate('2022-07-09');\n\n      // When\n      act(() => {\n        mockCalendar.changeView('month');\n        mockCalendar.setDate(targetDate);\n      });\n\n      // Then\n      expect(mockCalendar.getDateRangeStart()).toBeSameDate(expectedStartDate);\n      expect(mockCalendar.getDateRangeEnd()).toBeSameDate(expectedEndDate);\n    });\n\n    it('should set renderRange on month when isAlways6Weeks is false)', () => {\n      // Given\n      const targetDate = new Date('2022-06-15'); // Wed\n      const expectedStartDate = new TZDate('2022-05-29');\n      const expectedEndDate = new TZDate('2022-07-02'); // 5 weeks\n\n      // When\n      act(() => {\n        mockCalendar.changeView('month');\n        mockCalendar.setOptions({ month: { isAlways6Weeks: false } });\n        mockCalendar.setDate(targetDate);\n      });\n\n      // Then\n      expect(mockCalendar.getDateRangeStart()).toBeSameDate(expectedStartDate);\n      expect(mockCalendar.getDateRangeEnd()).toBeSameDate(expectedEndDate);\n    });\n  });\n\n  describe('when today is called', () => {\n    function getMonthDateRange(renderDate: TZDate, isAlways6Weeks: boolean) {\n      const matrix = createDateMatrixOfMonth(renderDate, { isAlways6Weeks });\n      const [[startDate]] = matrix;\n      const endDate = matrix[matrix.length - 1][matrix[0].length - 1];\n\n      return [startDate, endDate];\n    }\n\n    beforeEach(() => {\n      jest.useFakeTimers();\n      jest.setSystemTime(new Date('2022-06-15T20:00:00'));\n    });\n\n    afterEach(() => {\n      jest.clearAllTimers();\n      jest.useRealTimers();\n    });\n\n    it('should set renderRange on day view', () => {\n      // Given\n      const today = new TZDate();\n\n      // When\n      act(() => {\n        mockCalendar.changeView('day');\n        mockCalendar.today();\n      });\n\n      // Then\n      expect(mockCalendar.getDateRangeStart()).toBeSameDate(today);\n      expect(mockCalendar.getDateRangeEnd()).toBeSameDate(today);\n    });\n\n    it('should set renderRange on week', () => {\n      // Given\n      const today = new TZDate();\n      const expectedStartDate = addDate(today, today.getDay() * -1);\n      const expectedEndDate = addDate(expectedStartDate, 6);\n\n      // When\n      act(() => {\n        mockCalendar.changeView('week');\n        mockCalendar.today();\n      });\n\n      // Then\n      expect(mockCalendar.getDateRangeStart()).toBeSameDate(expectedStartDate);\n      expect(mockCalendar.getDateRangeEnd()).toBeSameDate(expectedEndDate);\n    });\n\n    it('should set renderRange on month when isAlways6Weeks is true', () => {\n      // Given\n      const today = new TZDate();\n      const [expectedStartDate, expectedEndDate] = getMonthDateRange(today, true);\n\n      // When\n      act(() => {\n        mockCalendar.changeView('month');\n        mockCalendar.today();\n      });\n\n      // Then\n      expect(mockCalendar.getDateRangeStart()).toBeSameDate(expectedStartDate);\n      expect(mockCalendar.getDateRangeEnd()).toBeSameDate(expectedEndDate);\n    });\n\n    it('should set renderRange on month when isAlways6Weeks is false', () => {\n      // Given\n      const isAlways6Weeks = false;\n      const today = new TZDate();\n      const [expectedStartDate, expectedEndDate] = getMonthDateRange(today, isAlways6Weeks);\n\n      // When\n      act(() => {\n        mockCalendar.changeView('month');\n        mockCalendar.setOptions({ month: { isAlways6Weeks } });\n        mockCalendar.today();\n      });\n\n      // Then\n      expect(mockCalendar.getDateRangeStart()).toBeSameDate(expectedStartDate);\n      expect(mockCalendar.getDateRangeEnd()).toBeSameDate(expectedEndDate);\n    });\n  });\n});\n\ndescribe('clearGridSelections', () => {\n  const gridSelectionsTypes: GridSelectionType[] = ['dayGridMonth', 'dayGridWeek', 'timeGrid'];\n\n  describe.each(gridSelectionsTypes)('when %s grid selection', (gridSelectionType) => {\n    function MockGridSelectionComponent() {\n      const {\n        startColumnIndex = 0,\n        startRowIndex = 0,\n        endColumnIndex = 0,\n        endRowIndex = 0,\n      } = useStore((state) => state.gridSelection[gridSelectionType]) ?? {};\n      const { setGridSelection } = useDispatch('gridSelection');\n\n      useEffect(() => {\n        setGridSelection(gridSelectionType, {\n          startColumnIndex: 1,\n          startRowIndex: 2,\n          endColumnIndex: 3,\n          endRowIndex: 4,\n        });\n      }, [setGridSelection]);\n\n      return (\n        <div>\n          <span data-testid=\"startColumn\">{startColumnIndex}</span>\n          <span data-testid=\"startRow\">{startRowIndex}</span>\n          <span data-testid=\"endColumn\">{endColumnIndex}</span>\n          <span data-testid=\"endRow\">{endRowIndex}</span>\n        </div>\n      );\n    }\n\n    class MockGridSelectionCalendar extends CalendarCore {\n      protected getComponent() {\n        return <MockGridSelectionComponent />;\n      }\n    }\n    let mockGridSelectionCalendar: MockGridSelectionCalendar;\n\n    beforeEach(() => {\n      const container = document.createElement('div');\n      document.body.appendChild(container);\n      mockGridSelectionCalendar = new MockGridSelectionCalendar(container);\n      act(() => {\n        mockGridSelectionCalendar.render();\n      });\n    });\n\n    afterEach(() => {\n      cleanup();\n    });\n\n    it(`should clear ${gridSelectionType} grid selections`, () => {\n      // Given\n      const startColumn = screen.getByTestId('startColumn');\n      const startRow = screen.getByTestId('startRow');\n      const endColumn = screen.getByTestId('endColumn');\n      const endRow = screen.getByTestId('endRow');\n      expect(startColumn).toHaveTextContent('1');\n      expect(startRow).toHaveTextContent('2');\n      expect(endColumn).toHaveTextContent('3');\n      expect(endRow).toHaveTextContent('4');\n\n      // When\n      act(() => {\n        mockGridSelectionCalendar.clearGridSelections();\n      });\n\n      // Then\n      expect(startColumn).toHaveTextContent('0');\n      expect(startRow).toHaveTextContent('0');\n      expect(endColumn).toHaveTextContent('0');\n      expect(endRow).toHaveTextContent('0');\n    });\n  });\n\n  describe('when accumulated grid selections', () => {\n    function MockGridSelectionComponent() {\n      const gridSelections = useStore((state) => state.gridSelection.accumulated.dayGridMonth);\n      const { addGridSelection } = useDispatch('gridSelection');\n\n      useEffect(() => {\n        addGridSelection('dayGridMonth', {\n          startColumnIndex: 1,\n          startRowIndex: 2,\n          endColumnIndex: 3,\n          endRowIndex: 4,\n        });\n      }, [addGridSelection]);\n\n      const [gridSelection] = gridSelections;\n      const {\n        startColumnIndex = 0,\n        startRowIndex = 0,\n        endColumnIndex = 0,\n        endRowIndex = 0,\n      } = gridSelection ?? {};\n\n      return (\n        <div>\n          <span data-testid=\"startColumn\">{startColumnIndex}</span>\n          <span data-testid=\"startRow\">{startRowIndex}</span>\n          <span data-testid=\"endColumn\">{endColumnIndex}</span>\n          <span data-testid=\"endRow\">{endRowIndex}</span>\n        </div>\n      );\n    }\n\n    class MockGridSelectionCalendar extends CalendarCore {\n      protected getComponent() {\n        return <MockGridSelectionComponent />;\n      }\n    }\n    let mockGridSelectionCalendar: MockGridSelectionCalendar;\n\n    beforeEach(() => {\n      const container = document.createElement('div');\n      document.body.appendChild(container);\n      mockGridSelectionCalendar = new MockGridSelectionCalendar(container);\n      act(() => {\n        mockGridSelectionCalendar.render();\n      });\n    });\n\n    afterEach(() => {\n      cleanup();\n    });\n\n    it(`should clear accumulated grid selections`, () => {\n      // Given\n      const startColumn = screen.getByTestId('startColumn');\n      const startRow = screen.getByTestId('startRow');\n      const endColumn = screen.getByTestId('endColumn');\n      const endRow = screen.getByTestId('endRow');\n      expect(startColumn).toHaveTextContent('1');\n      expect(startRow).toHaveTextContent('2');\n      expect(endColumn).toHaveTextContent('3');\n      expect(endRow).toHaveTextContent('4');\n\n      // When\n      act(() => {\n        mockGridSelectionCalendar.clearGridSelections();\n      });\n\n      // Then\n      expect(startColumn).toHaveTextContent('0');\n      expect(startRow).toHaveTextContent('0');\n      expect(endColumn).toHaveTextContent('0');\n      expect(endRow).toHaveTextContent('0');\n    });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/factory/calendarCore.tsx",
    "content": "import type { ComponentChild } from 'preact';\nimport { h, render } from 'preact';\nimport { unmountComponentAtNode } from 'preact/compat';\nimport renderToString from 'preact-render-to-string';\n\nimport type { DeepPartial } from 'ts-essentials';\nimport sendHostname from 'tui-code-snippet/request/sendHostname';\n\nimport { CalendarContainer } from '@src/calendarContainer';\nimport { GA_TRACKING_ID } from '@src/constants/statistics';\nimport { initCalendarStore } from '@src/contexts/calendarStore';\nimport { initThemeStore } from '@src/contexts/themeStore';\nimport { createDateMatrixOfMonth, getWeekDates } from '@src/helpers/grid';\nimport EventModel from '@src/model/eventModel';\nimport TZDate from '@src/time/date';\nimport { addDate, addMonths, toEndOfDay, toStartOfDay } from '@src/time/datetime';\nimport { last } from '@src/utils/array';\nimport type { EventBus } from '@src/utils/eventBus';\nimport { EventBusImpl } from '@src/utils/eventBus';\nimport { addAttributeHooks, removeAttributeHooks } from '@src/utils/sanitizer';\nimport { isNil, isPresent, isString } from '@src/utils/type';\n\nimport type { ExternalEventTypes, InternalEventTypes, ScrollBehaviorOptions } from '@t/eventBus';\nimport type { DateType, EventObject } from '@t/events';\nimport type { CalendarColor, CalendarInfo, Options, ViewType } from '@t/options';\nimport type {\n  CalendarMonthOptions,\n  CalendarState,\n  CalendarStore,\n  CalendarWeekOptions,\n  Dispatchers,\n  InternalStoreAPI,\n} from '@t/store';\nimport type { ThemeState, ThemeStore } from '@t/theme';\n\n/**\n * {@link https://nhn.github.io/tui.code-snippet/latest/CustomEvents CustomEvents} document at {@link https://github.com/nhn/tui.code-snippet tui-code-snippet}\n * @typedef {CustomEvents} CustomEvents\n */\n\n/**\n * Define Calendars to group events.\n *\n * @typedef {object} CalendarInfo\n * @property {string} id - Calendar id.\n * @property {string} name - Calendar name.\n * @property {string} color - Text color of events.\n * @property {string} borderColor - Left border color of events.\n * @property {string} backgroundColor - Background color of events.\n * @property {string} dragBackgroundColor - Background color of events during dragging.\n */\n\n/**\n * Timezone options of the calendar instance.\n *\n * For more information, see {@link https://github.com/nhn/tui.calendar/blob/main/docs/en/apis/options.md#timezone|Timezone options} in guide.\n *\n * @typedef {object} TimezoneOptions\n * @example\n * const calendar = new Calendar('#container', {\n *   timezone: {\n *     // @property {string} zones[].timezoneName - Timezone name. it should be one of IANA timezone names.\n *     // @property {string} [zones[].displayLabel] - Display label of timezone.\n *     // @property {string} [zones[].tooltip] - Tooltip of the element of the display label.\n *     zones: [\n *       {\n *         timezoneName: 'Asia/Seoul',\n *         displayLabel: 'UTC+9:00',\n *         tooltip: 'Seoul'\n *       },\n *       {\n *         timezoneName: 'Europe/London',\n *         displayLabel: 'UTC+1:00',\n *         tooltip: 'BST'\n *       }\n *     ],\n *     // This function will be called for rendering components for each timezone.\n *     // You don't have to use it if you're able to `Intl.DateTimeFormat` API with `timeZone` option.\n *     // this function should return timezone offset from UTC.\n *     // for instance, using moment-timezone:\n *     customOffsetCalculator: (timezoneName, timestamp) => {\n *       return moment.tz(timezoneName).utcOffset(timestamp);\n *     }\n *   }\n * });\n * @property {Array.<object>} zones - Timezone data.\n * @property {string} zones[].timezoneName - Timezone name. it should be one of IANA timezone names.\n * @property {string} [zones[].displayLabel] - Display label of timezone.\n * @property {string} [zones[].tooltip] - Tooltip of the element of the display label.\n * @property {function} customOffsetCalculator - Custom offset calculator when you're not able to leverage `Intl.DateTimeFormat` API.\n */\n\n/**\n * Object to create/modify events.\n * @typedef {object} EventObject\n * @property {string} [id] - Event id.\n * @property {string} [calendarId] - Calendar id.\n * @property {string} [title] - Event title.\n * @property {string} [body] - Body content of the event.\n * @property {string} [isAllday] - Whether the event is all day or not.\n * @property {string|number|Date|TZDate} [start] - Start time of the event.\n * @property {string|number|Date|TZDate} [end] - End time of the event.\n * @property {number} [goingDuration] - Travel time which is taken to go in minutes.\n * @property {number} [comingDuration] - Travel time which is taken to come back in minutes.\n * @property {string} [location] - Location of the event.\n * @property {Array.<string>} [attendees] - Attendees of the event.\n * @property {string} [category] - Category of the event. Available categories are 'milestone', 'task', 'time' and 'allday'.\n * @property {string} [dueDateClass] - Classification of work events. (before work, before lunch, before work)\n * @property {string} [recurrenceRule] - Recurrence rule of the event.\n * @property {string} [state] - State of the event. Available states are 'Busy', 'Free'.\n * @property {boolean} [isVisible] - Whether the event is visible or not.\n * @property {boolean} [isPending] - Whether the event is pending or not.\n * @property {boolean} [isFocused] - Whether the event is focused or not.\n * @property {boolean} [isReadOnly] - Whether the event is read only or not.\n * @property {boolean} [isPrivate] - Whether the event is private or not.\n * @property {string} [color] - Text color of the event.\n * @property {string} [backgroundColor] - Background color of the event.\n * @property {string} [dragBackgroundColor] - Background color of the event during dragging.\n * @property {string} [borderColor] - Left border color of the event.\n * @property {object} [customStyle] - Custom style of the event. The key of CSS property should be camelCase (e.g. {'fontSize': '12px'})\n * @property {*} [raw] - Raw data of the event. it's an arbitrary property for anything.\n */\n\n/**\n * CalendarCore class\n *\n * @class CalendarCore\n * @mixes CustomEvents\n * @param {string|Element} container - container element or selector.\n * @param {object} options - calendar options. For more information, see {@link https://github.com/nhn/tui.calendar/blob/main/docs/en/apis/calendar.md|Calendar options} in guide.\n *   @param {string} [options.defaultView=\"week\"] - Initial view type. Available values are: 'day', 'week', 'month'.\n *   @param {boolean} [options.useFormPopup=false] - Whether to use the default form popup when creating/modifying events.\n *   @param {boolean} [options.useDetailPopup=false] - Whether to use the default detail popup when clicking events.\n *   @param {boolean} [options.isReadOnly=false] - Whether the calendar is read-only.\n *   @param {boolean} [options.usageStatistics=true] - Whether to allow collect hostname and send the information to google analytics.\n *                                              For more information, check out the {@link https://github.com/nhn/tui.calendar/blob/main/apps/calendar/README.md#collect-statistics-on-the-use-of-open-source|documentation}.\n *   @param {function} [options.eventFilter] - A function that returns true if the event should be displayed. The default filter checks if the event's `isVisible` property is true.\n *   @param {object} [options.week] - Week option of the calendar instance.\n *     @param {number} [options.week.startDayOfWeek=0] - Start day of the week. Available values are 0 (Sunday) to 6 (Saturday).\n *     @param {Array.<string>} [options.week.dayNames] - Names of days of the week. Should be 7 items starting from Sunday to Saturday. If not specified, the default names are used.\n *                                               Default values are ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].\n *     @param {boolean} [options.week.workweek=false] - Whether to exclude Saturday and Sunday.\n *     @param {boolean} [options.week.showTimezoneCollapseButton=true] - Whether to show the timezone collapse button.\n *     @param {boolean} [options.week.timezonesCollapsed=false] - Whether to collapse the timezones.\n *     @param {number} [options.week.hourStart=0] - Start hour of the day. Available values are 0 to 24.\n *     @param {number} [options.week.hourEnd=24] - End hour of the day. Available values are 0 to 24. Must be greater than `hourStart`.\n *     @param {boolean} [options.week.narrowWeekend=false] - Whether to narrow down width of weekends to half.\n *     @param {boolean|Array.<string>} [options.week.eventView=true] - Determine which view to display events. Available values are 'allday' and 'time'. set to `false` to disable event view.\n *     @param {boolean|Array.<string>} [options.week.taskView=true] - Determine which view to display tasks. Available values are 'milestone' and 'task'. set to `false` to disable task view.\n *     @param {boolean|object} [options.week.collapseDuplicateEvents=false] - Whether to collapse duplicate events. If you want to filter duplicate events and choose the main event based on your requirements, set `getDuplicateEvents` and `getMainEvent`. For more information, see {@link https://github.com/nhn/tui.calendar/blob/main/docs/en/apis/options.md#weekcollapseduplicateevents|Options} in guide.\n *   @param {object} options.month - Month option of the calendar instance.\n *     @param {number} [options.month.startDayOfWeek=0] - Start day of the week. Available values are 0 (Sunday) to 6 (Saturday).\n *     @param {Array.<string>} [options.month.dayNames] - Names of days of the week. Should be 7 items starting from Sunday to Saturday. If not specified, the default names are used.\n *                                                Default values are ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].\n *     @param {boolean} [options.month.workweek=false] - Whether to exclude Saturday and Sunday.\n *     @param {boolean} [options.month.narrowWeekend=false] - Whether to narrow down width of weekends to half.\n *     @param {number} [options.month.visibleWeeksCount=0] - Number of weeks to display. 0 means display all weeks.\n *   @param {Array.<CalendarInfo>} [options.calendars] - Calendars to group events.\n *   @param {boolean|object} [options.gridSelection=true] - Whether to enable grid selection. or it's option. it's enabled when the value is `true` and object and will be disabled when `isReadOnly` is true.\n *     @param {boolean} options.gridSelection.enableDbClick - Whether to enable double click to select area.\n *     @param {boolean} options.gridSelection.enableClick - Whether to enable click to select area.\n *   @param {TimezoneOptions} options.timezone - Timezone option of the calendar instance. For more information about timezone, check out the {@link https://github.com/nhn/tui.calendar/blob/main/docs/en/apis/options.md|Options} in guide.\n *   @param {Theme} options.theme - Theme option of the calendar instance. For more information, see {@link https://github.com/nhn/tui.calendar/blob/main/docs/en/apis/theme.md|Theme} in guide.\n *   @param {TemplateConfig} options.template - Template option of the calendar instance. For more information, see {@link https://github.com/nhn/tui.calendar/blob/main/docs/en/apis/template.md|Template} in guide.\n */\nexport default abstract class CalendarCore\n  implements EventBus<ExternalEventTypes & InternalEventTypes>\n{\n  protected container: Element | null;\n\n  /**\n   * start and end date of weekly, monthly\n   * @private\n   */\n  protected renderRange: {\n    start: TZDate;\n    end: TZDate;\n  };\n\n  protected eventBus: EventBus<ExternalEventTypes & InternalEventTypes>;\n\n  protected theme: InternalStoreAPI<ThemeStore>;\n\n  protected store: InternalStoreAPI<CalendarStore>;\n\n  constructor(container: string | Element, options: Options = {}) {\n    // NOTE: Handling server side rendering. When container is not specified,\n    this.container = isString(container) ? document?.querySelector(container) ?? null : container;\n\n    this.theme = initThemeStore(options.theme);\n    this.eventBus = new EventBusImpl<ExternalEventTypes & InternalEventTypes>();\n    this.store = initCalendarStore(options);\n\n    this.renderRange = this.calculateRenderRange(toStartOfDay());\n\n    addAttributeHooks();\n\n    // NOTE: To make sure the user really wants to do this. Ignore any invalid values.\n    if (this.getStoreState().options.usageStatistics === true) {\n      sendHostname('calendar', GA_TRACKING_ID);\n    }\n  }\n\n  protected abstract getComponent(): ComponentChild;\n\n  protected getStoreState(): CalendarState;\n\n  protected getStoreState<Group extends keyof CalendarState>(group: Group): CalendarState[Group];\n\n  protected getStoreState<Group extends keyof CalendarState>(group?: Group) {\n    const state = this.store.getState();\n\n    return group ? state[group] : state;\n  }\n\n  protected getStoreDispatchers(): Dispatchers;\n\n  protected getStoreDispatchers<Group extends keyof Dispatchers>(group: Group): Dispatchers[Group];\n\n  protected getStoreDispatchers<Group extends keyof Dispatchers>(group?: Group) {\n    const dispatchers = this.store.getState().dispatch;\n\n    return group ? dispatchers[group] : dispatchers;\n  }\n\n  /**\n   * Destroys the instance.\n   */\n  destroy() {\n    if (this.container) {\n      unmountComponentAtNode(this.container);\n    }\n\n    this.store.clearListeners();\n    this.theme.clearListeners();\n    this.eventBus.off();\n    removeAttributeHooks();\n\n    for (const key in this) {\n      if (this.hasOwnProperty(key)) {\n        delete this[key];\n      }\n    }\n  }\n\n  private calculateMonthRenderDate({\n    renderDate,\n    offset,\n    monthOptions,\n  }: {\n    renderDate: TZDate;\n    offset: number;\n    monthOptions: CalendarMonthOptions;\n  }) {\n    let newRenderDate = new TZDate(renderDate);\n    const { visibleWeeksCount } = monthOptions;\n\n    if (visibleWeeksCount > 0) {\n      newRenderDate = addDate(newRenderDate, offset * 7 * visibleWeeksCount);\n    } else {\n      newRenderDate = addMonths(newRenderDate, offset);\n    }\n    const dateMatrix = createDateMatrixOfMonth(newRenderDate, monthOptions);\n\n    const [[start]] = dateMatrix;\n    const end = last(last(dateMatrix));\n\n    return {\n      renderDate: newRenderDate,\n      renderRange: { start, end },\n    };\n  }\n\n  private calculateWeekRenderDate({\n    renderDate,\n    offset,\n    weekOptions,\n  }: {\n    renderDate: TZDate;\n    offset: number;\n    weekOptions: CalendarWeekOptions;\n  }) {\n    const newRenderDate = new TZDate(renderDate);\n    newRenderDate.addDate(offset * 7);\n    const weekDates = getWeekDates(newRenderDate, weekOptions);\n\n    const [start] = weekDates;\n    const end = last(weekDates);\n\n    return {\n      renderDate: newRenderDate,\n      renderRange: { start, end },\n    };\n  }\n\n  private calculateDayRenderDate({ renderDate, offset }: { renderDate: TZDate; offset: number }) {\n    const newRenderDate = new TZDate(renderDate);\n    newRenderDate.addDate(offset);\n\n    const start = toStartOfDay(newRenderDate);\n    const end = toEndOfDay(newRenderDate);\n\n    return {\n      renderDate: newRenderDate,\n      renderRange: { start, end },\n    };\n  }\n\n  /**\n   * Move the rendered date to the next/prev range.\n   *\n   * The range of movement differs depending on the current view, Basically:\n   *   - In month view, it moves to the next/prev month.\n   *   - In week view, it moves to the next/prev week.\n   *   - In day view, it moves to the next/prev day.\n   *\n   * Also, the range depends on the options like how many visible weeks/months should be rendered.\n   *\n   * @param {number} offset The offset to move by.\n   *\n   * @example\n   * // Move to the next month in month view.\n   * calendar.move(1);\n   *\n   * // Move to the next year in month view.\n   * calendar.move(12);\n   *\n   * // Move to yesterday in day view.\n   * calendar.move(-1);\n   */\n  move(offset: number) {\n    if (isNil(offset)) {\n      return;\n    }\n\n    const { currentView, renderDate } = this.getStoreState().view;\n    const { options } = this.getStoreState();\n    const { setRenderDate } = this.getStoreDispatchers().view;\n\n    const newRenderDate = new TZDate(renderDate);\n\n    let calculatedRenderDate = {\n      renderDate: newRenderDate,\n      renderRange: { start: new TZDate(newRenderDate), end: new TZDate(newRenderDate) },\n    };\n\n    if (currentView === 'month') {\n      calculatedRenderDate = this.calculateMonthRenderDate({\n        renderDate,\n        offset,\n        monthOptions: options.month as CalendarMonthOptions,\n      });\n    } else if (currentView === 'week') {\n      calculatedRenderDate = this.calculateWeekRenderDate({\n        renderDate,\n        offset,\n        weekOptions: options.week as CalendarWeekOptions,\n      });\n    } else if (currentView === 'day') {\n      calculatedRenderDate = this.calculateDayRenderDate({ renderDate, offset });\n    }\n\n    setRenderDate(calculatedRenderDate.renderDate);\n    this.renderRange = calculatedRenderDate.renderRange;\n  }\n\n  /**********\n   * CRUD Methods\n   **********/\n\n  /**\n   * Create events and render calendar.\n   * @param {Array.<EventObject>} events - list of {@link EventObject}\n   * @example\n   * calendar.createEvents([\n   *   {\n   *     id: '1',\n   *     calendarId: '1',\n   *     title: 'my event',\n   *     category: 'time',\n   *     dueDateClass: '',\n   *     start: '2018-01-18T22:30:00+09:00',\n   *     end: '2018-01-19T02:30:00+09:00',\n   *   },\n   *   {\n   *     id: '2',\n   *     calendarId: '1',\n   *     title: 'second event',\n   *     category: 'time',\n   *     dueDateClass: '',\n   *     start: '2018-01-18T17:30:00+09:00',\n   *     end: '2018-01-19T17:31:00+09:00',\n   *   },\n   * ]);\n   */\n  createEvents(events: EventObject[]) {\n    const { createEvents } = this.getStoreDispatchers('calendar');\n\n    createEvents(events);\n  }\n\n  protected getEventModel(eventId: string, calendarId: string) {\n    const { events } = this.getStoreState('calendar');\n\n    return events.find(\n      ({ id, calendarId: eventCalendarId }) => id === eventId && eventCalendarId === calendarId\n    );\n  }\n\n  /**\n   * Get an {@link EventObject} with event's id and calendar's id.\n   *\n   * @param {string} eventId - event's id\n   * @param {string} calendarId - calendar's id of the event\n   * @returns {EventObject|null} event. If the event can't be found, it returns null.\n   *\n   * @example\n   * const event = calendar.getEvent(eventId, calendarId);\n   *\n   * console.log(event.title);\n   */\n  getEvent(eventId: string, calendarId: string) {\n    return this.getEventModel(eventId, calendarId)?.toEventObject() ?? null;\n  }\n\n  /**\n   * Update an event.\n   *\n   * @param {string} eventId - ID of an event to update\n   * @param {string} calendarId - The calendarId of the event to update\n   * @param {EventObject} changes - The new {@link EventObject} data to apply to the event\n   *\n   * @example\n   * calendar.on('beforeUpdateEvent', function ({ event, changes }) {\n   *   const { id, calendarId } = event;\n   *\n   *   calendar.updateEvent(id, calendarId, changes);\n   * });\n   */\n  updateEvent(eventId: string, calendarId: string, changes: EventObject) {\n    const { updateEvent } = this.getStoreDispatchers('calendar');\n    const event = this.getEventModel(eventId, calendarId);\n\n    if (event) {\n      updateEvent({ event, eventData: changes });\n    }\n  }\n\n  /**\n   * Delete an event.\n   *\n   * @param {string} eventId - event's id to delete\n   * @param {string} calendarId - The CalendarId of the event to delete\n   */\n  deleteEvent(eventId: string, calendarId: string) {\n    const { deleteEvent } = this.getStoreDispatchers('calendar');\n    const event = this.getEventModel(eventId, calendarId);\n\n    if (event) {\n      deleteEvent(event);\n    }\n  }\n\n  /**********\n   * General Methods\n   **********/\n\n  /**\n   * Set events' visibility by calendar ID\n   *\n   * @param {string|Array.<string>} calendarId - The calendar id or ids to change visibility\n   * @param {boolean} isVisible - If set to true, show the events. If set to false, hide the events.\n   */\n  setCalendarVisibility(calendarId: string | string[], isVisible: boolean) {\n    const { setCalendarVisibility } = this.getStoreDispatchers('calendar');\n    const calendarIds = Array.isArray(calendarId) ? calendarId : [calendarId];\n\n    setCalendarVisibility(calendarIds, isVisible);\n  }\n\n  /**\n   * Render the calendar.\n   *\n   * @example\n   * calendar.render();\n   *\n   * @example\n   * // Re-render the calendar when resizing a window.\n   * window.addEventListener('resize', () => {\n   *   calendar.render();\n   * });\n   */\n  render() {\n    if (isPresent(this.container)) {\n      render(\n        <CalendarContainer theme={this.theme} store={this.store} eventBus={this.eventBus}>\n          {this.getComponent()}\n        </CalendarContainer>,\n        this.container\n      );\n    }\n\n    return this;\n  }\n\n  /**\n   * For SSR(Server Side Rendering), Return the HTML string of the whole calendar.\n   *\n   * @returns {string} HTML string\n   */\n  renderToString(): string {\n    return renderToString(\n      <CalendarContainer theme={this.theme} store={this.store} eventBus={this.eventBus}>\n        {this.getComponent()}\n      </CalendarContainer>\n    );\n  }\n\n  /**\n   * Delete all events and clear view\n   *\n   * @example\n   * calendar.clear();\n   */\n  clear() {\n    const { clearEvents } = this.getStoreDispatchers('calendar');\n\n    clearEvents();\n  }\n\n  /**\n   * Scroll to current time on today in case of daily, weekly view.\n   * Nothing happens in the monthly view.\n   *\n   * @example\n   * function onNewEvents(events) {\n   *   calendar.createEvents(events);\n   *   calendar.scrollToNow('smooth');\n   * }\n   */\n  scrollToNow(scrollBehavior: ScrollBehaviorOptions = 'auto') {\n    this.eventBus.fire('scrollToNow', scrollBehavior);\n  }\n\n  private calculateRenderRange(renderDate: TZDate) {\n    const { currentView } = this.getStoreState().view;\n    const { options } = this.getStoreState();\n\n    const newRenderDate = new TZDate(renderDate);\n\n    let newRenderRange = { start: new TZDate(newRenderDate), end: new TZDate(newRenderDate) };\n\n    if (currentView === 'month') {\n      newRenderRange = this.calculateMonthRenderDate({\n        renderDate,\n        offset: 0,\n        monthOptions: options.month as CalendarMonthOptions,\n      }).renderRange;\n    } else if (currentView === 'week') {\n      newRenderRange = this.calculateWeekRenderDate({\n        renderDate,\n        offset: 0,\n        weekOptions: options.week as CalendarWeekOptions,\n      }).renderRange;\n    } else if (currentView === 'day') {\n      newRenderRange = this.calculateDayRenderDate({ renderDate, offset: 0 }).renderRange;\n    }\n\n    return newRenderRange;\n  }\n\n  /**\n   * Move to today.\n   *\n   * @example\n   * function onClickTodayBtn() {\n   *   calendar.today();\n   * }\n   */\n  today() {\n    const { setRenderDate } = this.getStoreDispatchers().view;\n    const today = new TZDate();\n\n    setRenderDate(today);\n    this.renderRange = this.calculateRenderRange(today);\n  }\n\n  /**\n   * Move to specific date.\n   *\n   * @param {Date|string|number|TZDate} date - The date to move. it should be eligible parameter to create a `Date` instance if `date` is string or number.\n   * @example\n   * calendar.on('clickDayName', (event) => {\n   *   if (calendar.getViewName() === 'week') {\n   *     const dateToMove = new Date(event.date);\n   *\n   *     calendar.setDate(dateToMove);\n   *     calendar.changeView('day');\n   *   }\n   * });\n   */\n  setDate(date: DateType) {\n    const { setRenderDate } = this.getStoreDispatchers('view');\n    const dateToChange = new TZDate(date);\n\n    setRenderDate(dateToChange);\n    this.renderRange = this.calculateRenderRange(dateToChange);\n  }\n\n  /**\n   * Move the calendar forward to the next range.\n   *\n   * @example\n   * function moveToNextOrPrevRange(offset) {\n   *   if (offset === -1) {\n   *     calendar.prev();\n   *   } else if (offset === 1) {\n   *     calendar.next();\n   *   }\n   * }\n   */\n  next() {\n    this.move(1);\n  }\n\n  /**\n   * Move the calendar backward to the previous range.\n   *\n   * @example\n   * function moveToNextOrPrevRange(offset) {\n   *   if (offset === -1) {\n   *     calendar.prev();\n   *   } else if (offset === 1) {\n   *     calendar.next();\n   *   }\n   * }\n   */\n  prev() {\n    this.move(-1);\n  }\n\n  /**\n   * Change color values of events belong to a certain calendar.\n   *\n   * @param {string} calendarId - The calendar ID\n   * @param {object} colorOptions - The color values of the calendar\n   *   @param {string} colorOptions.color - The text color of the events\n   *   @param {string} colorOptions.borderColor - Left border color of events\n   *   @param {string} colorOptions.backgroundColor - Background color of events\n   *   @param {string} colorOptions.dragBackgroundColor - Background color of events during dragging\n   *\n   * @example\n   * calendar.setCalendarColor('1', {\n   *     color: '#e8e8e8',\n   *     backgroundColor: '#585858',\n   *     borderColor: '#a1b56c',\n   *     dragBackgroundColor: '#585858',\n   * });\n   * calendar.setCalendarColor('2', {\n   *     color: '#282828',\n   *     backgroundColor: '#dc9656',\n   *     borderColor: '#a1b56c',\n   *     dragBackgroundColor: '#dc9656',\n   * });\n   * calendar.setCalendarColor('3', {\n   *     color: '#a16946',\n   *     backgroundColor: '#ab4642',\n   *     borderColor: '#a1b56c',\n   *     dragBackgroundColor: '#ab4642',\n   * });\n   */\n  setCalendarColor(calendarId: string, colorOptions: CalendarColor) {\n    const { setCalendarColor } = this.getStoreDispatchers().calendar;\n\n    setCalendarColor(calendarId, colorOptions);\n  }\n\n  /**\n   * Change current view type.\n   *\n   * @param {string} viewName - The new view name to change to. Available values are 'month', 'week', 'day'.\n   *\n   * @example\n   * // change to daily view\n   * calendar.changeView('day');\n   *\n   * // change to weekly view\n   * calendar.changeView('week');\n   *\n   * // change to monthly view\n   * calendar.changeView('month');\n   */\n  changeView(viewName: ViewType) {\n    const { changeView } = this.getStoreDispatchers('view');\n\n    changeView(viewName);\n    this.renderRange = this.calculateRenderRange(this.getDate());\n  }\n\n  /**\n   * Get the DOM element of the event by event id and calendar id\n   *\n   * @param {string} eventId - ID of event\n   * @param {string} calendarId - calendarId of event\n   * @returns {HTMLElement} event element if found or null\n   *\n   * @example\n   * const element = calendar.getElement(eventId, calendarId);\n   *\n   * console.log(element);\n   */\n  getElement(eventId: string, calendarId: string) {\n    const event = this.getEvent(eventId, calendarId);\n\n    if (event && this.container) {\n      return this.container.querySelector(\n        `[data-event-id=\"${eventId}\"][data-calendar-id=\"${calendarId}\"]`\n      );\n    }\n\n    return null;\n  }\n\n  /**\n   * Set the theme of the calendar.\n   *\n   * @param {Theme} theme - The theme object to apply. For more information, see {@link https://github.com/nhn/tui.calendar/blob/main/docs/en/apis/theme.md|Theme} in guide.\n   *\n   * @example\n   * calendar.setTheme({\n   *   common: {\n   *     gridSelection: {\n   *       backgroundColor: '#333',\n   *     },\n   *   },\n   *   week: {\n   *     nowIndicatorLabel: {\n   *       color: '#00FF00',\n   *     },\n   *   },\n   *   month: {\n   *     dayName: {\n   *       borderLeft: '1px solid #e5e5e5',\n   *     },\n   *   },\n   * });\n   */\n  setTheme(theme: DeepPartial<ThemeState>) {\n    const { setTheme } = this.theme.getState().dispatch;\n\n    setTheme(theme);\n  }\n\n  /**\n   * Get current options.\n   *\n   * @returns {Options} - The current options of the instance\n   */\n  getOptions() {\n    const { options, template } = this.getStoreState();\n    const { dispatch, ...theme } = this.theme.getState();\n\n    return {\n      ...options,\n      template,\n      theme,\n    };\n  }\n\n  /**\n   * Set options of calendar. For more information, see {@link https://github.com/nhn/tui.calendar/blob/main/docs/en/apis/options.md|Options} in guide.\n   *\n   * @param {Options} options - The options to set\n   */\n  setOptions(options: Options) {\n    // destructure options here for tui.doc to generate docs correctly\n    const { theme, template, ...restOptions } = options;\n    const { setTheme } = this.theme.getState().dispatch;\n    const {\n      options: { setOptions },\n      template: { setTemplate },\n    } = this.getStoreDispatchers();\n\n    if (isPresent(theme)) {\n      setTheme(theme);\n    }\n\n    if (isPresent(template)) {\n      setTemplate(template);\n    }\n\n    setOptions(restOptions);\n  }\n\n  /**\n   * Get current rendered date. (see {@link TZDate} for further information)\n   *\n   * @returns {TZDate}\n   */\n  getDate(): TZDate {\n    const { renderDate } = this.getStoreState().view;\n\n    return renderDate;\n  }\n\n  /**\n   * Start time of rendered date range. (see {@link TZDate} for further information)\n   *\n   * @returns {TZDate}\n   */\n  getDateRangeStart() {\n    return this.renderRange.start;\n  }\n\n  /**\n   * End time of rendered date range. (see {@link TZDate} for further information)\n   *\n   * @returns {TZDate}\n   */\n  getDateRangeEnd() {\n    return this.renderRange.end;\n  }\n\n  /**\n   * Get current view name('day', 'week', 'month').\n   *\n   * @returns {string} current view name ('day', 'week', 'month')\n   */\n  getViewName(): ViewType {\n    const { currentView } = this.getStoreState('view');\n\n    return currentView;\n  }\n\n  /**\n   * Set calendar list.\n   *\n   * @param {CalendarInfo[]} calendars - list of calendars\n   */\n  setCalendars(calendars: CalendarInfo[]) {\n    const { setCalendars } = this.getStoreDispatchers().calendar;\n\n    setCalendars(calendars);\n  }\n\n  // TODO: specify position of popup\n  /**\n   * Open event form popup with predefined form values.\n   *\n   * @param {EventObject} event - The predefined {@link EventObject} data to show in form.\n   */\n  openFormPopup(event: EventObject) {\n    const { showFormPopup } = this.getStoreDispatchers().popup;\n\n    const eventModel = new EventModel(event);\n    const { title, location, start, end, isAllday, isPrivate, state: eventState } = eventModel;\n\n    showFormPopup({\n      isCreationPopup: true,\n      event: eventModel,\n      title,\n      location,\n      start,\n      end,\n      isAllday,\n      isPrivate,\n      eventState,\n    });\n  }\n\n  clearGridSelections() {\n    const { clearAll } = this.getStoreDispatchers().gridSelection;\n\n    clearAll();\n  }\n\n  fire<EventName extends keyof ExternalEventTypes>(\n    eventName: EventName,\n    ...args: Parameters<ExternalEventTypes[EventName]>\n  ): EventBus<ExternalEventTypes> {\n    this.eventBus.fire(eventName, ...args);\n\n    return this;\n  }\n\n  off<EventName extends keyof ExternalEventTypes>(\n    eventName?: EventName,\n    handler?: ExternalEventTypes[EventName]\n  ): EventBus<ExternalEventTypes> {\n    this.eventBus.off(eventName, handler);\n\n    return this;\n  }\n\n  on<EventName extends keyof ExternalEventTypes>(\n    eventName: EventName,\n    handler: ExternalEventTypes[EventName]\n  ): EventBus<ExternalEventTypes> {\n    this.eventBus.on(eventName, handler);\n\n    return this;\n  }\n\n  once<EventName extends keyof ExternalEventTypes>(\n    eventName: EventName,\n    handler: ExternalEventTypes[EventName]\n  ): EventBus<ExternalEventTypes> {\n    this.eventBus.once(eventName, handler);\n\n    return this;\n  }\n}\n"
  },
  {
    "path": "apps/calendar/src/factory/day.tsx",
    "content": "import { h } from 'preact';\n\nimport { Day as DayComponent } from '@src/components/view/day';\nimport CalendarCore from '@src/factory/calendarCore';\n\nimport type { Options } from '@t/options';\n\nexport default class Day extends CalendarCore {\n  constructor(container: Element, options: Options = {}) {\n    super(container, options);\n\n    this.render();\n  }\n\n  protected getComponent() {\n    return <DayComponent />;\n  }\n}\n"
  },
  {
    "path": "apps/calendar/src/factory/month.spec.tsx",
    "content": "import Month from '@src/factory/month';\nimport { act, hasDesiredStartTime, screen } from '@src/test/utils';\nimport { noop } from '@src/utils/noop';\n\nimport { mockMonthViewEvents } from '@stories/mocks/mockMonthViewEvents';\n\nimport type { EventObject } from '@t/events';\nimport type { Options } from '@t/options';\n\nafterEach(() => {\n  document.body.innerHTML = '';\n  jest.resetAllMocks();\n  jest.useRealTimers();\n});\n\ndescribe('Primary Timezone', () => {\n  function setup(options: Options = {}, events: EventObject[] = mockMonthViewEvents) {\n    const container = document.createElement('div');\n    jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({\n      x: 0,\n      y: 0,\n      top: 0,\n      right: 0,\n      bottom: 0,\n      left: 0,\n      width: 600,\n      height: 600,\n      toJSON: noop,\n    }));\n\n    document.body.appendChild(container);\n    const instance = new Month(container, options);\n    act(() => {\n      instance.createEvents(events);\n    });\n\n    return { container, instance };\n  }\n\n  const reTargetEventTitle = 'short time event';\n\n  it('should create a zoned event with a string different from the primary timezone', () => {\n    // Given\n    jest.useFakeTimers();\n    jest.setSystemTime(new Date('2022-05-04T00:00:00+09:00'));\n\n    setup(\n      {\n        timezone: {\n          zones: [{ timezoneName: 'Asia/Karachi' }], // UTC+05:00\n        },\n      },\n      [\n        {\n          id: '1',\n          calendarId: 'cal1',\n          title: reTargetEventTitle,\n          category: 'time',\n          start: '2022-05-04T10:00:00+09:00',\n          end: '2022-05-04T11:00:00+09:00',\n        },\n      ]\n    );\n\n    // When\n    const targetEvent = screen.getByText(reTargetEventTitle);\n\n    // Then\n    // From UTC+9 to UTC+5\n    expect(hasDesiredStartTime(targetEvent, '06:00')).toBe(true);\n  });\n\n  it('should change the start time of events when setting the new timezone option', () => {\n    // Given\n    jest.useFakeTimers();\n    jest.setSystemTime(new Date('2022-05-04T00:00:00+09:00'));\n    const { instance } = setup({}, [\n      {\n        id: '1',\n        calendarId: 'cal1',\n        title: reTargetEventTitle,\n        category: 'time',\n        start: '2022-05-04T10:00:00+09:00',\n        end: '2022-05-04T11:00:00+09:00',\n      },\n    ]);\n    const prevEvent = screen.getByText(reTargetEventTitle);\n    expect(hasDesiredStartTime(prevEvent, '10:00')).toBe(true);\n\n    // When\n    act(() => {\n      instance.setOptions({\n        timezone: {\n          zones: [{ timezoneName: 'Asia/Karachi' }], // UTC+05:00\n        },\n      });\n    });\n\n    // Then\n    const changedEvent = screen.getByText(reTargetEventTitle);\n    expect(hasDesiredStartTime(changedEvent, '06:00')).toBe(true);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/factory/month.tsx",
    "content": "import { h } from 'preact';\n\nimport { Month as MonthComponent } from '@src/components/view/month';\nimport CalendarCore from '@src/factory/calendarCore';\n\nimport type { Options } from '@t/options';\n\nexport default class Month extends CalendarCore {\n  constructor(container: Element, options: Options = {}) {\n    super(container, options);\n\n    this.render();\n  }\n\n  protected getComponent() {\n    return <MonthComponent />;\n  }\n\n  /**\n   * Hide the more view\n   */\n  hideMoreView() {\n    const { hideSeeMorePopup } = this.getStoreDispatchers().popup;\n\n    hideSeeMorePopup();\n  }\n}\n"
  },
  {
    "path": "apps/calendar/src/factory/week.spec.tsx",
    "content": "import range from 'tui-code-snippet/array/range';\n\nimport Week from '@src/factory/week';\nimport { TEST_IDS } from '@src/test/testIds';\nimport { act, hasDesiredStartTime, screen, within } from '@src/test/utils';\nimport TZDate from '@src/time/date';\nimport { toFormat } from '@src/time/datetime';\n\nimport { mockWeekViewEvents } from '@stories/mocks/mockWeekViewEvents';\n\nimport type { EventObject } from '@t/events';\nimport type { Options } from '@t/options';\n\nfunction setup(options: Options = {}, events: EventObject[] = mockWeekViewEvents) {\n  const container = document.createElement('div');\n  document.body.appendChild(container);\n  const instance = new Week(container, options);\n  act(() => {\n    instance.createEvents(events);\n  });\n\n  return { container, instance };\n}\n\nafterEach(() => {\n  jest.useRealTimers();\n  document.body.innerHTML = '';\n});\n\ndescribe('Basic', () => {\n  it('should render with mock events', () => {\n    // Given\n    const { container } = setup();\n\n    // Then\n    expect(container).not.toBeEmptyDOMElement();\n  });\n});\n\ndescribe('Primary Timezone', () => {\n  const reTargetEvent = /short time event/i;\n\n  it('should create a zoned event with a string different from the primary timezone', () => {\n    // Given\n    jest.useFakeTimers();\n    const baseDate = new Date('2022-05-04T00:00:00+09:00');\n    jest.setSystemTime(baseDate);\n\n    const { instance } = setup(\n      {\n        timezone: {\n          zones: [{ timezoneName: 'Asia/Karachi' }], // UTC+05:00\n        },\n      },\n      [\n        {\n          id: '1',\n          calendarId: 'cal1',\n          title: 'short time event',\n          category: 'time',\n          start: '2022-05-04T04:00:00+09:00',\n          end: '2022-05-04T06:00:00+09:00',\n        },\n      ]\n    );\n    act(() => {\n      instance.setDate(baseDate);\n    });\n\n    // When\n    const targetEvent = screen.getByText(reTargetEvent);\n\n    // Then\n    expect(hasDesiredStartTime(targetEvent, '00:00')).toBe(true);\n  });\n\n  it('should create zoned event with a string same as the primary timezone', () => {\n    // Given\n    jest.useFakeTimers();\n    const baseDate = new Date('2022-05-04T00:00:00+09:00');\n    jest.setSystemTime(baseDate);\n    const { instance } = setup(\n      {\n        timezone: {\n          zones: [{ timezoneName: 'Asia/Karachi' }], // UTC+05:00\n        },\n      },\n      [\n        {\n          id: '1',\n          calendarId: 'cal1',\n          title: 'short time event',\n          category: 'time',\n          start: '2022-05-04T04:00:00+05:00',\n          end: '2022-05-04T06:00:00+05:00',\n        },\n      ]\n    );\n    act(() => {\n      instance.setDate(baseDate);\n    });\n\n    // When\n    const targetEvent = screen.getByText(reTargetEvent);\n\n    // Then\n    expect(hasDesiredStartTime(targetEvent, '04:00')).toBe(true);\n  });\n\n  it('should apply timezone option to timed events', () => {\n    // Given\n    setup({\n      timezone: {\n        zones: [\n          {\n            timezoneName: 'Asia/Karachi', // UTC+5\n          },\n        ],\n      },\n    });\n\n    // When\n    const targetEvent = screen.getByText(reTargetEvent);\n\n    // Then\n    expect(hasDesiredStartTime(targetEvent, '00:00')).toBe(true);\n  });\n\n  it('should change start & end time of events when timezone option changes from the local timezone', () => {\n    // Given\n    const { instance } = setup();\n    let targetEvent = screen.getByText(reTargetEvent);\n    expect(hasDesiredStartTime(targetEvent, '04:00')).toBe(true);\n\n    // When\n    act(() => {\n      instance.setOptions({\n        timezone: {\n          zones: [\n            {\n              timezoneName: 'Asia/Karachi', // UTC+5\n            },\n          ],\n        },\n      });\n    });\n\n    // Then\n    targetEvent = screen.getByText(reTargetEvent);\n    expect(hasDesiredStartTime(targetEvent, '00:00')).toBe(true);\n  });\n\n  it('should change start & end time of events when timezone option changes from another timezone', () => {\n    // Given\n    // To avoid DST when changing timezone, mock the base date of mock events\n    jest.spyOn(Date, 'now').mockImplementationOnce(() => new Date('2022-04-01T00:00:00').getTime());\n    const { instance } = setup({\n      timezone: {\n        zones: [\n          {\n            timezoneName: 'Asia/Karachi', // UTC+5\n          },\n        ],\n      },\n    });\n    let targetEvent = screen.getByText(reTargetEvent);\n\n    expect(hasDesiredStartTime(targetEvent, '00:00')).toBe(true);\n\n    // When\n    act(() => {\n      instance.setOptions({\n        timezone: {\n          zones: [\n            {\n              timezoneName: 'Australia/Sydney', // UTC+10\n            },\n          ],\n        },\n      });\n    });\n\n    // Then\n    targetEvent = screen.getByText(reTargetEvent);\n\n    expect(hasDesiredStartTime(targetEvent, '05:00')).toBe(true);\n  });\n\n  it('should render zoned events between the end of week and the start of week properly (Small offset difference)', () => {\n    // Given\n    jest.useFakeTimers();\n    const baseDate = new Date('2022-07-20'); // Wednesday\n    jest.setSystemTime(baseDate);\n\n    const startOfWeekEventTitle = 'start';\n    const endOfWeekEventTitle = 'end';\n\n    // When\n    const { instance } = setup(\n      {\n        timezone: {\n          zones: [\n            {\n              timezoneName: 'Pacific/Auckland', // UTC+12\n            },\n          ],\n        },\n      },\n      [\n        {\n          id: startOfWeekEventTitle,\n          title: startOfWeekEventTitle,\n          start: '2022-07-16T12:00:00Z', // Expected 2022-07-17 00:00:00 in UTC+12\n          end: '2022-07-16T13:00:00Z', // Expected 2022-07-17 01:00:00 in UTC+12\n        },\n        {\n          id: endOfWeekEventTitle,\n          title: endOfWeekEventTitle,\n          start: '2022-07-16T11:00:00Z', // Expected 2022-07-16 23:00:00 in UTC+12\n          end: '2022-07-16T11:59:00Z', // Expected 2022-07-16 23:59:00 in UTC+12\n        },\n      ]\n    );\n\n    // Then\n    const startOfWeekEvent = screen.getByText(startOfWeekEventTitle);\n    expect(startOfWeekEvent).toBeInTheDocument();\n\n    // When\n    // Move to previous week\n    act(() => {\n      instance.prev();\n    });\n\n    // Then\n    const endOfWeekEvent = screen.getByText(endOfWeekEventTitle);\n    expect(endOfWeekEvent).toBeInTheDocument();\n  });\n\n  it('should render zoned events between the end of week and the start of week properly (Large offset difference)', () => {\n    // Given\n    jest.useFakeTimers();\n    const baseDate = new Date('2022-07-20'); // Wednesday\n    jest.setSystemTime(baseDate);\n\n    const startOfWeekEventTitle = 'start';\n    const endOfWeekEventTitle = 'end';\n\n    // When\n    const { instance } = setup(\n      {\n        timezone: {\n          zones: [\n            {\n              timezoneName: 'US/Pacific', // UTC-7 at this time\n            },\n          ],\n        },\n      },\n      [\n        {\n          id: startOfWeekEventTitle,\n          title: startOfWeekEventTitle,\n          start: '2022-07-17T07:00:00Z', // Expected 2022-07-17 00:00:00 in UTC-07\n          end: '2022-07-17T08:00:00Z', // Expected 2022-07-17 01:00:00 in UTC-07\n        },\n        {\n          id: endOfWeekEventTitle,\n          title: endOfWeekEventTitle,\n          start: '2022-07-17T06:00:00Z', // Expected 2022-07-16 23:00:00 in UTC-07\n          end: '2022-07-17T06:59:00Z', // Expected 2022-07-16 23:59:00 in UTC-07\n        },\n      ]\n    );\n\n    // Then\n    const startOfWeekEvent = screen.getByText(startOfWeekEventTitle);\n    expect(startOfWeekEvent).toBeInTheDocument();\n\n    // When\n    // Move to previous week\n    act(() => {\n      instance.prev();\n    });\n\n    // Then\n    const endOfWeekEvent = screen.getByText(endOfWeekEventTitle);\n    expect(endOfWeekEvent).toBeInTheDocument();\n  });\n});\n\ndescribe('Multiple Timezone', () => {\n  it('should not render timezone labels when only one timezone is given', () => {\n    // Given\n    const timezoneOptions: Options = {\n      timezone: {\n        zones: [\n          {\n            timezoneName: 'Asia/Seoul',\n          },\n        ],\n      },\n    };\n\n    // When\n    setup(timezoneOptions);\n\n    // Then\n    expect(screen.queryByRole('columnheader')).toBeNull();\n  });\n\n  it('should render one hours column when only one timezone is given', () => {\n    // Given\n    const timezoneOptions: Options = {\n      timezone: {\n        zones: [\n          {\n            timezoneName: 'Asia/Seoul',\n          },\n        ],\n      },\n    };\n    const expectedHoursColumn = range(0, 25).map((hour) =>\n      toFormat(new TZDate(2022, 6, 1, hour), 'hh tt')\n    );\n\n    // When\n    const { instance } = setup(timezoneOptions);\n    act(() => {\n      // Set render date to the past so that current time indicator doesn't show up\n      instance.setDate('2020-06-01');\n    });\n\n    // Then\n    const hoursColumn = screen.getAllByRole('rowgroup');\n    expect(hoursColumn.length).toBe(1);\n\n    const hourRows = Array.from(hoursColumn[0].children).map((hourRow) => hourRow.textContent);\n    expect(hourRows).toEqual(expectedHoursColumn);\n  });\n\n  it('should render default timezone labels', () => {\n    // Given\n    const timezoneOptions: Options = {\n      timezone: {\n        zones: [\n          {\n            timezoneName: 'Asia/Seoul', // GMT+09:00\n          },\n          {\n            timezoneName: 'Asia/Karachi', // GMT+05:00\n          },\n        ],\n      },\n    };\n    const expectedLabels = ['GMT+05:00', 'GMT+09:00'];\n\n    // When\n    setup(timezoneOptions);\n\n    // Then\n    const labelContainer = screen.getByRole('columnheader');\n    const labels = within(labelContainer).getAllByRole('gridcell');\n\n    expect(labels.map((label) => label.textContent)).toEqual(expectedLabels);\n  });\n\n  it('should render custom timezone labels', () => {\n    // Given\n    const timezoneOptions: Options = {\n      timezone: {\n        zones: [\n          {\n            timezoneName: 'Asia/Seoul', // GMT+09:00\n            displayLabel: '+09',\n          },\n          {\n            timezoneName: 'Asia/Karachi', // GMT+05:00\n            displayLabel: '+05',\n          },\n        ],\n      },\n    };\n    const expectedLabels = ['+05', '+09'];\n\n    // When\n    setup(timezoneOptions);\n\n    // Then\n    const labelContainer = screen.getByRole('columnheader');\n    const labels = within(labelContainer).getAllByRole('gridcell');\n\n    expect(labels.map((label) => label.textContent)).toEqual(expectedLabels);\n  });\n\n  it('should render multiple hours column when multiple timezone is given', () => {\n    // Given\n    const timezoneOptions: Options = {\n      timezone: {\n        zones: [\n          {\n            timezoneName: 'Asia/Seoul',\n          },\n          {\n            timezoneName: 'Asia/Karachi',\n          },\n        ],\n      },\n    };\n    const expectedPrimaryHoursColumn = range(0, 25).map((hour) =>\n      toFormat(new TZDate(2022, 6, 1, hour), 'hh tt')\n    );\n    // -4 hours each\n    const expectedSecondaryHoursColumn = range(0, 25).map((hour) =>\n      toFormat(new TZDate(2022, 6, 1, hour - 4), 'HH:mm')\n    );\n\n    // When\n    const { instance } = setup(timezoneOptions);\n    act(() => {\n      // Set render date to the past so that current time indicator doesn't show up\n      instance.setDate('2020-06-01');\n    });\n\n    // Then\n    const hourColumns = screen.getAllByRole('rowgroup');\n    expect(hourColumns.length).toBe(2);\n    hourColumns.forEach((col) => {\n      expect(col).toHaveStyle({ width: '50%' });\n    });\n\n    const hourRowsTextByColumn = hourColumns.map((hourCol) =>\n      Array.from(hourCol.children).map((hourRow) => hourRow.textContent)\n    );\n    expect(hourRowsTextByColumn).toEqual([\n      expectedSecondaryHoursColumn,\n      expectedPrimaryHoursColumn,\n    ]);\n  });\n\n  it('should render current time of each timezones including date differences (minus 1)', () => {\n    // Given\n    jest.useFakeTimers();\n    const baseDate = new Date('2022-05-16T04:00:00Z'); // Make sure it's on UTC\n    jest.setSystemTime(baseDate);\n    const timezoneOptions: Options = {\n      timezone: {\n        zones: [\n          {\n            timezoneName: 'Etc/UTC',\n          },\n          {\n            timezoneName: 'US/Pacific', // UTC -7\n          },\n        ],\n      },\n    };\n    const expectedNowIndicatorLabels = ['[-1]21:00', '04:00'];\n\n    // When\n    setup(timezoneOptions);\n\n    // Then\n    const nowIndicatorLabels = screen.getAllByTestId(TEST_IDS.NOW_INDICATOR_LABEL);\n    expect(nowIndicatorLabels.map((label) => label.textContent)).toEqual(\n      expectedNowIndicatorLabels\n    );\n  });\n\n  it('should render current time of each timezones including date differences (plus 1)', () => {\n    // Given\n    jest.useFakeTimers();\n    const baseDate = new Date('2022-05-16T20:00:00Z'); // Make sure it's on UTC\n    jest.setSystemTime(baseDate);\n    const timezoneOptions: Options = {\n      timezone: {\n        zones: [\n          {\n            timezoneName: 'Etc/UTC',\n          },\n          {\n            timezoneName: 'Asia/Seoul', // UTC +9\n          },\n        ],\n      },\n    };\n    const expectedNowIndicatorLabels = ['[+1]05:00', '20:00'];\n\n    // When\n    setup(timezoneOptions);\n\n    // Then\n    const nowIndicatorLabels = screen.getAllByTestId(TEST_IDS.NOW_INDICATOR_LABEL);\n    expect(nowIndicatorLabels.map((label) => label.textContent)).toEqual(\n      expectedNowIndicatorLabels\n    );\n  });\n\n  it('should show only primary timezone when the timezonesCollapsed option is enabled', () => {\n    // Given\n    const option: Options = {\n      week: {\n        timezonesCollapsed: true,\n      },\n      timezone: {\n        zones: [\n          {\n            timezoneName: 'Etc/UTC',\n          },\n          {\n            timezoneName: 'Asia/Seoul', // UTC +9\n          },\n        ],\n      },\n    };\n\n    // When\n    setup(option);\n\n    // Then\n    const labelContainer = screen.getByRole('columnheader');\n    const hourColumns = screen.getAllByRole('rowgroup');\n\n    expect(labelContainer.children).toHaveLength(1);\n    expect(hourColumns).toHaveLength(1);\n    expect(hourColumns[0]).toHaveStyle({ width: '100%' });\n  });\n});\n\ndescribe('Now Indicator', () => {\n  it('should show a now indicator when the showNowIndicator option is true', () => {\n    // Given\n    const options = { week: { showNowIndicator: true } };\n\n    // When\n    setup(options);\n\n    // Then\n    expect(screen.getByTestId(TEST_IDS.NOW_INDICATOR)).not.toBeNull();\n    expect(screen.getByTestId(TEST_IDS.NOW_INDICATOR_LABEL)).not.toBeNull();\n  });\n\n  it('should hide a now indicator when the showNowIndicator option is false', () => {\n    // Given\n    const options = { week: { showNowIndicator: false } };\n\n    // When\n    setup(options);\n\n    // Then\n    expect(screen.queryByTestId(TEST_IDS.NOW_INDICATOR)).toBeNull();\n    expect(screen.queryByTestId(TEST_IDS.NOW_INDICATOR_LABEL)).toBeNull();\n  });\n});\n\ndescribe('Timezone Collapse Button', () => {\n  it('should show timezone collapse button when the showTimezoneCollapseButton option is enabled', () => {\n    // Given\n    const option: Options = {\n      week: {\n        showTimezoneCollapseButton: true,\n      },\n      timezone: {\n        zones: [\n          {\n            timezoneName: 'Asia/Seoul',\n          },\n          {\n            timezoneName: 'Asia/Karachi',\n          },\n        ],\n      },\n    };\n\n    // When\n    setup(option);\n\n    // Then\n    const timeLabelsContainer = screen.getByRole('columnheader');\n    expect(within(timeLabelsContainer).getByRole('button')).toBeInTheDocument();\n  });\n\n  it('should have different arrow icon according to the timezonesCollapsed option', () => {\n    // Given\n    const option: Options = {\n      week: {\n        showTimezoneCollapseButton: true,\n        timezonesCollapsed: true, // default is false.\n      },\n      timezone: {\n        zones: [\n          {\n            timezoneName: 'Asia/Seoul',\n          },\n          {\n            timezoneName: 'Asia/Karachi',\n          },\n        ],\n      },\n    };\n\n    // When (collapsed)\n    const { instance } = setup(option);\n\n    // Then\n    let collapseButton = within(screen.getByRole('columnheader')).getByRole('button');\n    let collapseButtonIcon = within(collapseButton).getByRole('img');\n    expect(collapseButtonIcon.classList.toString()).toMatch(/right/);\n    expect(collapseButton).toHaveAttribute('aria-expanded', 'false');\n\n    // When (expanded)\n    act(() => {\n      instance.setOptions({\n        week: {\n          timezonesCollapsed: false,\n        },\n      });\n    });\n\n    // Then\n    collapseButton = within(screen.getByRole('columnheader')).getByRole('button');\n    collapseButtonIcon = within(collapseButton).getByRole('img');\n    expect(collapseButtonIcon.classList.toString()).toMatch(/left/);\n    expect(collapseButton).toHaveAttribute('aria-expanded', 'true');\n  });\n});\n\ndescribe('Daylight Saving Time Transition', () => {\n  let eventResult: EventObject = {};\n  const resultSpy = jest.fn((event) => {\n    eventResult = event;\n  });\n\n  beforeEach(() => {\n    jest.useFakeTimers();\n  });\n\n  afterEach(() => {\n    resultSpy.mockClear();\n  });\n\n  describe('Northern Hemisphere', () => {\n    const options = {\n      timezone: {\n        zones: [\n          {\n            timezoneName: 'US/Pacific',\n          },\n        ],\n      },\n    };\n\n    it('should render an event during forward transition (02:00 ~ 03:00)', () => {\n      // Given\n      const baseDate = new Date('2022-03-14T00:00:00Z');\n      jest.setSystemTime(baseDate);\n\n      const { instance } = setup(options, []);\n      instance.on('afterRenderEvent', resultSpy);\n\n      // When\n      act(() => {\n        instance.createEvents([\n          {\n            id: 'forward',\n            title: 'Forward Transition',\n            start: '2022-03-13T09:00:00Z', // 02:00 in UTC-7\n            end: '2022-03-13T10:00:00Z', // 03:00 in UTC-7\n            category: 'time',\n          },\n        ]);\n      });\n\n      // Then\n      expect(resultSpy).toHaveBeenCalled();\n      expect(toFormat(eventResult.start as TZDate, 'YYYYMMDD')).toBe('20220313');\n      expect(toFormat(eventResult.start as TZDate, 'HH:mm')).toBe('01:00');\n      expect(toFormat(eventResult.end as TZDate, 'YYYYMMDD')).toBe('20220313');\n      expect(toFormat(eventResult.end as TZDate, 'HH:mm')).toBe('03:00');\n    });\n\n    it('should render an event during forward transition (01:00 ~ 04:00)', () => {\n      // Given\n      const baseDate = new Date('2022-03-14T00:00:00Z');\n      jest.setSystemTime(baseDate);\n\n      const { instance } = setup(options, []);\n      instance.on('afterRenderEvent', resultSpy);\n\n      // When\n      act(() => {\n        instance.createEvents([\n          {\n            id: 'forward',\n            title: 'Forward Transition',\n            start: '2022-03-13T09:00:00Z', // 01:00 in UTC-8\n            end: '2022-03-13T11:00:00Z', // 04:00 in UTC-7\n            category: 'time',\n          },\n        ]);\n      });\n\n      // Then\n      expect(resultSpy).toHaveBeenCalled();\n      expect(toFormat(eventResult.start as TZDate, 'YYYYMMDD')).toBe('20220313');\n      expect(toFormat(eventResult.start as TZDate, 'HH:mm')).toBe('01:00');\n      expect(toFormat(eventResult.end as TZDate, 'YYYYMMDD')).toBe('20220313');\n      expect(toFormat(eventResult.end as TZDate, 'HH:mm')).toBe('04:00');\n    });\n\n    it('should render an event during fallback transition (01:00 ~ 02:00)', () => {\n      // Given\n      const baseDate = new Date('2022-11-07T00:00:00Z');\n      jest.setSystemTime(baseDate);\n\n      const { instance } = setup(options, []);\n      instance.on('afterRenderEvent', resultSpy);\n\n      // When\n      act(() => {\n        instance.createEvents([\n          {\n            id: 'fallback',\n            title: 'Fallback Transition',\n            start: '2022-11-06T08:00:00Z', // 01:00 in UTC-7\n            end: '2022-11-06T10:00:00Z', // 02:00 in UTC-7\n            category: 'time',\n          },\n        ]);\n      });\n\n      // Then\n      expect(resultSpy).toHaveBeenCalled();\n      expect(toFormat(eventResult.start as TZDate, 'YYYYMMDD')).toBe('20221106');\n      expect(toFormat(eventResult.start as TZDate, 'HH:mm')).toBe('01:00');\n      expect(toFormat(eventResult.end as TZDate, 'YYYYMMDD')).toBe('20221106');\n      expect(toFormat(eventResult.end as TZDate, 'HH:mm')).toBe('02:00');\n    });\n\n    it('should render an event during fallback transition (01:00 ~ 03:00)', () => {\n      // Given\n      const baseDate = new Date('2022-11-07T00:00:00Z');\n      jest.setSystemTime(baseDate);\n\n      const { instance } = setup(options, []);\n      instance.on('afterRenderEvent', resultSpy);\n\n      // When\n      act(() => {\n        instance.createEvents([\n          {\n            id: 'fallback',\n            title: 'Fallback Transition',\n            start: '2022-11-06T08:00:00Z', // 01:00 in UTC-7\n            end: '2022-11-06T11:00:00Z', // 03:00 in UTC-7\n            category: 'time',\n          },\n        ]);\n      });\n\n      // Then\n      expect(resultSpy).toHaveBeenCalled();\n      expect(toFormat(eventResult.start as TZDate, 'YYYYMMDD')).toBe('20221106');\n      expect(toFormat(eventResult.start as TZDate, 'HH:mm')).toBe('01:00');\n      expect(toFormat(eventResult.end as TZDate, 'YYYYMMDD')).toBe('20221106');\n      expect(toFormat(eventResult.end as TZDate, 'HH:mm')).toBe('03:00');\n    });\n  });\n\n  describe('Southern Hemisphere', () => {\n    const options: Options = {\n      timezone: {\n        zones: [\n          {\n            timezoneName: 'Pacific/Auckland',\n          },\n        ],\n      },\n    };\n\n    it('should render an event during forward transition (01:00 ~ 03:00)', () => {\n      // Given\n      const baseDate = new Date('2022-09-26T00:00:00Z');\n      jest.setSystemTime(baseDate);\n\n      const { instance } = setup(options, []);\n      instance.on('afterRenderEvent', resultSpy);\n\n      // When\n      act(() => {\n        instance.createEvents([\n          {\n            id: 'forward',\n            title: 'Forward Transition',\n            start: '2022-09-24T13:00:00Z', // 01:00 in UTC+12\n            end: '2022-09-24T14:00:00Z', // 03:00 in UTC+13 - Forward gap\n            category: 'time',\n          },\n        ]);\n      });\n\n      // Then\n      expect(resultSpy).toHaveBeenCalled();\n      expect(toFormat(eventResult.start as TZDate, 'YYYYMMDD')).toBe('20220925');\n      expect(toFormat(eventResult.start as TZDate, 'HH:mm')).toBe('01:00');\n      expect(toFormat(eventResult.end as TZDate, 'YYYYMMDD')).toBe('20220925');\n      expect(toFormat(eventResult.end as TZDate, 'HH:mm')).toBe('03:00');\n    });\n\n    it('should render an event during fallback transition', () => {\n      // Given\n      const baseDate = new Date('2022-04-04T00:00:00Z');\n      jest.setSystemTime(baseDate);\n\n      const { instance } = setup(options, []);\n      instance.on('afterRenderEvent', resultSpy);\n\n      // When\n      act(() => {\n        instance.createEvents([\n          {\n            id: 'fallback',\n            title: 'Fallback Transition',\n            category: 'time',\n            start: '2022-04-02T12:00:00Z', // 01:00 in UTC+13\n            end: '2022-04-02T14:00:00Z', // 02:00 in UTC+12 - Fallback gap\n          },\n        ]);\n      });\n\n      // Then\n      expect(resultSpy).toHaveBeenCalled();\n      expect(toFormat(eventResult.start as TZDate, 'YYYYMMDD')).toBe('20220403');\n      expect(toFormat(eventResult.start as TZDate, 'HH:mm')).toBe('01:00');\n      expect(toFormat(eventResult.end as TZDate, 'YYYYMMDD')).toBe('20220403');\n      expect(toFormat(eventResult.end as TZDate, 'HH:mm')).toBe('02:00');\n    });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/factory/week.tsx",
    "content": "import { h } from 'preact';\n\nimport { Week as WeekComponent } from '@src/components/view/week';\nimport CalendarCore from '@src/factory/calendarCore';\n\nimport type { Options } from '@t/options';\n\nexport default class Week extends CalendarCore {\n  constructor(container: Element, options: Options = {}) {\n    super(container, options);\n\n    this.render();\n  }\n\n  protected getComponent() {\n    return <WeekComponent />;\n  }\n}\n"
  },
  {
    "path": "apps/calendar/src/helpers/css.spec.ts",
    "content": "import { cls, CSS_PREFIX } from '@src/helpers/css';\n\ndescribe('cssHelper', () => {\n  describe('cls', () => {\n    it('should return css selector with prefix \"tui-calendar\"', () => {\n      expect(cls('view')).toBe(`${CSS_PREFIX}view`);\n    });\n\n    it('should handle object literal as arguments', () => {\n      const classNameMap = {\n        'is-ok': true,\n        'is-not-ok': false,\n        hidden: null,\n      };\n      expect(cls(classNameMap)).toBe(`${CSS_PREFIX}is-ok`);\n    });\n\n    it('should handle multiple argument types', () => {\n      const strClassName = 'str';\n      const classNameMap = {\n        a: true,\n        b: false,\n        c: true,\n        // eslint-disable-next-line no-undefined\n        d: undefined,\n      };\n      expect(cls(strClassName, classNameMap)).toBe(\n        `${CSS_PREFIX}${strClassName} ${CSS_PREFIX}a ${CSS_PREFIX}c`\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/helpers/css.ts",
    "content": "import { DEFAULT_EVENT_COLORS } from '@src/constants/style';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport { isString } from '@src/utils/type';\n\nimport type { CalendarColor } from '@t/options';\n\nexport const CSS_PREFIX = 'toastui-calendar-';\n\ninterface ClassNameDictionary {\n  [id: string]: boolean | undefined | null;\n}\n\ntype ClassNameValue = string | ClassNameDictionary | undefined | null;\n\nexport function cls(...args: ClassNameValue[]): string {\n  const result: string[] = [];\n\n  args.forEach((arg) => {\n    if (!arg) {\n      return;\n    }\n\n    if (isString(arg)) {\n      result.push(arg);\n    } else {\n      Object.keys(arg).forEach((className) => {\n        if (arg[className]) {\n          result.push(className);\n        }\n      });\n    }\n  });\n\n  return result.map((str: string) => `${CSS_PREFIX}${str}`).join(' ');\n}\n\nexport function toPercent(value: number) {\n  return `${value}%`;\n}\n\nexport function toPx(value: number) {\n  return `${value}px`;\n}\n\n/**\n * ex)\n * extractPercentPx('calc(100% - 22px)') // { percent: 100, px: -22 }\n * extractPercentPx('100%') // { percent: 100, px: 0 }\n * extractPercentPx('-22px') // { percent: 0, px: -22 }\n */\nexport function extractPercentPx(value: string) {\n  const percentRegexp = /(\\d+)%/;\n  const percentResult = value.match(percentRegexp);\n  const pxRegexp = /(-?)\\s?(\\d+)px/;\n  const pxResult = value.match(pxRegexp);\n\n  return {\n    percent: percentResult ? parseInt(percentResult[1], 10) : 0,\n    px: pxResult ? parseInt(`${pxResult[1]}${pxResult[2]}`, 10) : 0,\n  };\n}\n\nexport function getEventColors(uiModel: EventUIModel, calendarColor: CalendarColor) {\n  const eventColors = uiModel.model.getColors();\n\n  return Object.keys(DEFAULT_EVENT_COLORS).reduce<CalendarColor>((colors, _key) => {\n    const key = _key as keyof CalendarColor;\n    colors[key] = eventColors[key] ?? calendarColor[key] ?? DEFAULT_EVENT_COLORS[key];\n\n    return colors;\n  }, {});\n}\n"
  },
  {
    "path": "apps/calendar/src/helpers/dayName.ts",
    "content": "import type TZDate from '@src/time/date';\nimport { capitalize } from '@src/utils/string';\n\nimport type { WeekOptions } from '@t/options';\nimport type { TemplateWeekDayName } from '@t/template';\n\nexport const DEFAULT_DAY_NAMES = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];\n\nexport const getDayName = (dayIndex: number) => {\n  return DEFAULT_DAY_NAMES[dayIndex];\n};\n\nexport function getDayNames(\n  days: TZDate[],\n  weekDayNamesOption: Required<WeekOptions>['dayNames'] | []\n) {\n  return days.map<TemplateWeekDayName>((day) => {\n    const dayIndex = day.getDay();\n    const dayName =\n      weekDayNamesOption.length > 0\n        ? weekDayNamesOption[dayIndex]\n        : capitalize(getDayName(dayIndex));\n\n    return {\n      date: day.getDate(),\n      day: day.getDay(),\n      dayName,\n      isToday: true,\n      renderDate: 'date',\n      dateInstance: day,\n    };\n  });\n}\n"
  },
  {
    "path": "apps/calendar/src/helpers/drag.ts",
    "content": "import type { GridSelectionType } from '@src/slices/gridSelection';\n\nimport type { DraggingTypes, EventDraggingArea } from '@t/drag';\n\nexport const DRAGGING_TYPE_CONSTANTS: {\n  [K in Extract<DraggingTypes, 'panelResizer'>]: DraggingTypes;\n} = {\n  panelResizer: 'panelResizer',\n};\n\nexport const DRAGGING_TYPE_CREATORS = {\n  resizeEvent: (area: EventDraggingArea, id: string) => `event/${area}/resize/${id}` as const,\n  moveEvent: (area: EventDraggingArea, id: string) => `event/${area}/move/${id}` as const,\n  gridSelection: (type: GridSelectionType) => `gridSelection/${type}` as const,\n};\n"
  },
  {
    "path": "apps/calendar/src/helpers/events.spec.ts",
    "content": "import { collidesWith } from '@src/helpers/events';\nimport { createDate } from '@src/test/helpers';\nimport TZDate from '@src/time/date';\n\ndescribe('collidesWith', () => {\n  it('should calculate collision only from start and end dates, if travel time is not used', () => {\n    const hasCollision = collidesWith({\n      start: Number(createDate(2021, 5, 14)),\n      end: Number(createDate(2021, 5, 16)),\n      targetStart: Number(createDate(2021, 5, 16)),\n      targetEnd: Number(createDate(2021, 5, 17)),\n      goingDuration: 0,\n      comingDuration: 30,\n      targetGoingDuration: 30,\n      targetComingDuration: 0,\n      usingTravelTime: false,\n    });\n\n    expect(hasCollision).toBe(false);\n  });\n\n  it('should calculate collision with start time, end time and travel time, when using travel time (for date)', () => {\n    const hasCollision = collidesWith({\n      start: Number(createDate(2021, 5, 14)),\n      end: Number(createDate(2021, 5, 16)),\n      targetStart: Number(createDate(2021, 5, 16)),\n      targetEnd: Number(createDate(2021, 5, 17)),\n      goingDuration: 0,\n      comingDuration: 30,\n      targetGoingDuration: 30,\n      targetComingDuration: 3,\n      usingTravelTime: true,\n    });\n    expect(hasCollision).toBe(true);\n  });\n\n  it('should calculate collision with start time, end time and travel time, when using travel time (for time)', () => {\n    const hasCollision = collidesWith({\n      start: Number(new TZDate(2021, 4, 16, 9)),\n      end: Number(new TZDate(2021, 4, 16, 10)),\n      targetStart: Number(new TZDate(2021, 4, 16, 11)),\n      targetEnd: Number(new TZDate(2021, 4, 16, 12)),\n      goingDuration: 30,\n      comingDuration: 60,\n      targetGoingDuration: 30,\n      targetComingDuration: 30,\n      usingTravelTime: true,\n    });\n    expect(hasCollision).toBe(true);\n  });\n\n  it('should not have a collision when there is no overlap between two times', () => {\n    const hasCollision = collidesWith({\n      start: Number(new TZDate(2021, 4, 16, 9)),\n      end: Number(new TZDate(2021, 4, 16, 10)),\n      targetStart: Number(new TZDate(2021, 4, 16, 11)),\n      targetEnd: Number(new TZDate(2021, 4, 16, 12)),\n      goingDuration: 30,\n      comingDuration: 0,\n      targetGoingDuration: 0,\n      targetComingDuration: 30,\n      usingTravelTime: true,\n    });\n\n    expect(hasCollision).toBe(false);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/helpers/events.ts",
    "content": "import type EventModel from '@src/model/eventModel';\nimport { millisecondsFrom, MS_EVENT_MIN_DURATION } from '@src/time/datetime';\n\ntype CollisionParam = {\n  start: number;\n  end: number;\n  targetStart: number;\n  targetEnd: number;\n  goingDuration: number;\n  comingDuration: number;\n  targetGoingDuration: number;\n  targetComingDuration: number;\n  usingTravelTime: boolean;\n};\n\nfunction hasCollision(start: number, end: number, targetStart: number, targetEnd: number) {\n  return (\n    (targetStart > start && targetStart < end) ||\n    (targetEnd > start && targetEnd < end) ||\n    (targetStart <= start && targetEnd >= end)\n  );\n}\n\nexport function collidesWith({\n  start,\n  end,\n  targetStart,\n  targetEnd,\n  goingDuration,\n  comingDuration,\n  targetGoingDuration,\n  targetComingDuration,\n  usingTravelTime,\n}: CollisionParam) {\n  if (Math.abs(end - start) < MS_EVENT_MIN_DURATION) {\n    end += MS_EVENT_MIN_DURATION;\n  }\n\n  if (Math.abs(end - start) < MS_EVENT_MIN_DURATION) {\n    end += MS_EVENT_MIN_DURATION;\n  }\n\n  if (usingTravelTime) {\n    start -= millisecondsFrom('minute', goingDuration);\n    end += millisecondsFrom('minute', comingDuration);\n    targetStart -= millisecondsFrom('minute', targetGoingDuration);\n    targetEnd += millisecondsFrom('minute', targetComingDuration);\n  }\n\n  return hasCollision(start, end, targetStart, targetEnd);\n}\n\nexport function isSameEvent(event: EventModel, eventId: string, calendarId: string) {\n  return event.id === eventId && event.calendarId === calendarId;\n}\n\nexport function isVisibleEvent(event: EventModel) {\n  return event.isVisible;\n}\n"
  },
  {
    "path": "apps/calendar/src/helpers/grid.spec.ts",
    "content": "import range from 'tui-code-snippet/array/range';\n\nimport { createEventCollection } from '@src/controller/base';\nimport {\n  createDateMatrixOfMonth,\n  createGridPositionFinder,\n  createTimeGridData,\n  getColumnsData,\n  getExceedCount,\n  getGridWidthAndLeftPercentValues,\n  getRenderedEventUIModels,\n  getWeekDates,\n  getWidth,\n  isWithinHeight,\n} from '@src/helpers/grid';\nimport EventModel from '@src/model/eventModel';\nimport EventUIModel from '@src/model/eventUIModel';\nimport { createDate } from '@src/test/helpers';\nimport TZDate from '@src/time/date';\nimport { addDate, Day, isWeekend, WEEK_DAYS } from '@src/time/datetime';\nimport { noop } from '@src/utils/noop';\n\nimport type { CalendarData } from '@t/events';\nimport type { GridPosition, GridPositionFinder, TimeGridRow } from '@t/grid';\n\nfunction createResultMatrix({\n  startFrom,\n  rows,\n  rangeStart,\n  rangeEnd,\n}: {\n  startFrom: TZDate;\n  rows: number;\n  rangeStart: number;\n  rangeEnd: number;\n}) {\n  return range(rows).map((rowCount) =>\n    range(rangeStart, rangeEnd + 1).map((num) => addDate(startFrom, num + rowCount * WEEK_DAYS))\n  );\n}\n\ndescribe('getWidth', () => {\n  const widthList = [1, 2, 3, 4, 5];\n\n  it.each([\n    [0, 0, 1],\n    [0, 1, 3],\n    [0, 2, 6],\n    [0, 3, 10],\n    [0, 4, 15],\n    [1, 1, 2],\n    [1, 2, 5],\n    [1, 3, 9],\n    [1, 4, 14],\n    [2, 2, 3],\n    [2, 3, 7],\n    [2, 4, 12],\n    [3, 3, 4],\n    [3, 4, 9],\n    [4, 4, 5],\n  ])('should return sum of width from %i to %i', (start, end, expected) => {\n    const result = getWidth(widthList, start, end);\n\n    expect(result).toBe(expected);\n  });\n});\n\ndescribe('getGridWidthAndLeftPercentValues', () => {\n  const totalWidth = 100;\n  let narrowWeekend: boolean;\n  let row: TZDate[];\n\n  describe('narrowWeekend is true', () => {\n    beforeAll(() => {\n      narrowWeekend = true;\n    });\n\n    it('should return single PanelEventInfo', () => {\n      row = [createDate(2021, 4, 16)];\n\n      const { widthList, leftList } = getGridWidthAndLeftPercentValues(\n        row,\n        narrowWeekend,\n        totalWidth\n      );\n\n      expect(widthList).toHaveLength(1);\n      expect(widthList).toEqual([100]);\n      expect(leftList).toHaveLength(1);\n      expect(leftList).toEqual([0]);\n    });\n\n    it('should return PanelEventInfo list (only weekday)', () => {\n      // Mon, Tue, Wed, Thu, Fri\n      row = [12, 13, 14, 15, 16].map((d) => createDate(2021, 4, d));\n\n      const { widthList, leftList } = getGridWidthAndLeftPercentValues(\n        row,\n        narrowWeekend,\n        totalWidth\n      );\n\n      expect(widthList).toHaveLength(5);\n      expect(widthList).toEqual([20, 20, 20, 20, 20]);\n      expect(leftList).toHaveLength(5);\n      expect(leftList).toEqual([0, 20, 40, 60, 80]);\n    });\n\n    it('should return PanelEventInfo list (only weekend)', () => {\n      // Sat, Sun\n      row = [17, 18].map((d) => createDate(2021, 4, d));\n\n      const { widthList, leftList } = getGridWidthAndLeftPercentValues(\n        row,\n        narrowWeekend,\n        totalWidth\n      );\n\n      expect(widthList).toHaveLength(2);\n      expect(widthList).toEqual([50, 50]);\n      expect(leftList).toHaveLength(2);\n      expect(leftList).toEqual([0, 50]);\n    });\n\n    it('should return PanelEventInfo list', () => {\n      // Thu, Fri, Sat\n      row = [15, 16, 17].map((d) => createDate(2021, 4, d));\n\n      const { widthList, leftList } = getGridWidthAndLeftPercentValues(\n        row,\n        narrowWeekend,\n        totalWidth\n      );\n\n      expect(widthList).toHaveLength(3);\n      expect(widthList).toEqual([40, 40, 20]);\n      expect(leftList).toHaveLength(3);\n      expect(leftList).toEqual([0, 40, 80]);\n    });\n  });\n\n  describe('narrowWeekend is false', () => {\n    beforeAll(() => {\n      narrowWeekend = false;\n    });\n\n    it('should return single grid width and left percent value', () => {\n      row = [createDate(2021, 4, 16)];\n\n      const { widthList, leftList } = getGridWidthAndLeftPercentValues(\n        row,\n        narrowWeekend,\n        totalWidth\n      );\n\n      expect(widthList).toHaveLength(1);\n      expect(widthList).toEqual([100]);\n      expect(leftList).toHaveLength(1);\n      expect(leftList).toEqual([0]);\n    });\n\n    it('should return list for grid width and left percent values (only weekday)', () => {\n      // Mon, Tue, Wed, Thu, Fri\n      row = [12, 13, 14, 15, 16].map((d) => createDate(2021, 4, d));\n\n      const { widthList, leftList } = getGridWidthAndLeftPercentValues(\n        row,\n        narrowWeekend,\n        totalWidth\n      );\n\n      expect(widthList).toHaveLength(5);\n      expect(widthList).toEqual([20, 20, 20, 20, 20]);\n      expect(leftList).toHaveLength(5);\n      expect(leftList).toEqual([0, 20, 40, 60, 80]);\n    });\n\n    it('should return list for grid width and left percent values (only weekend)', () => {\n      // Sat, Sun\n      row = [17, 18].map((d) => createDate(2021, 4, d));\n\n      const { widthList, leftList } = getGridWidthAndLeftPercentValues(\n        row,\n        narrowWeekend,\n        totalWidth\n      );\n\n      expect(widthList).toHaveLength(2);\n      expect(widthList).toEqual([50, 50]);\n      expect(leftList).toHaveLength(2);\n      expect(leftList).toEqual([0, 50]);\n    });\n\n    it('should return list for grid width and left percent values', () => {\n      // Thu, Fri, Sat, Sun\n      row = [15, 16, 17, 18].map((d) => createDate(2021, 4, d));\n\n      const { widthList, leftList } = getGridWidthAndLeftPercentValues(\n        row,\n        narrowWeekend,\n        totalWidth\n      );\n\n      expect(widthList).toHaveLength(4);\n      expect(widthList).toEqual([25, 25, 25, 25]);\n      expect(leftList).toHaveLength(4);\n      expect(leftList).toEqual([0, 25, 50, 75]);\n    });\n  });\n});\n\ndescribe('getRenderedEventUIModels', () => {\n  it('should get rendered event ui models', () => {\n    const narrowWeekend = false;\n    const row: TZDate[] = [\n      new TZDate(2021, 5, 2),\n      new TZDate(2021, 5, 3),\n      new TZDate(2021, 5, 4),\n      new TZDate(2021, 5, 5),\n    ];\n    const calendarData: CalendarData = {\n      calendars: [],\n      events: createEventCollection(),\n      idsOfDay: {},\n    };\n\n    expect(getRenderedEventUIModels(row, calendarData, narrowWeekend)).toEqual({\n      uiModels: [],\n      gridDateEventModelMap: {},\n    });\n  });\n});\n\ndescribe('isWithinHeight', () => {\n  it('should return a callback function that checks whether do not exceed height of container', () => {\n    expect(isWithinHeight(100, 20)({ top: 1 } as EventUIModel)).toBe(true);\n    expect(isWithinHeight(100, 20)({ top: 6 } as EventUIModel)).toBe(false);\n  });\n});\n\ndescribe('getExceedCount', () => {\n  const data = [\n    { start: createDate(2021, 4, 30), end: createDate(2021, 5, 2) }, // Fri ~ Sun\n    { start: createDate(2021, 5, 2), end: createDate(2021, 5, 4) }, // Sun ~ Tue\n    { start: createDate(2021, 5, 4), end: createDate(2021, 5, 6) }, // Tue ~ Thu\n  ];\n\n  it('should calculate the number of events that exceed height of container', () => {\n    const uiModels = data.map((e) => {\n      const event = new EventModel(e);\n      event.isAllday = true;\n\n      return new EventUIModel(event);\n    });\n\n    expect(getExceedCount(uiModels, 200, 30)).toBe(0);\n  });\n});\n\ndescribe('createDateMatrixOfMonth', () => {\n  it('should create matrix of dates of given month with empty option', () => {\n    const targetMonth = new TZDate('2021-12-01T00:00:00');\n    const expectedStartDateOfMonth = new TZDate('2021-11-28T00:00:00');\n\n    const expected = createResultMatrix({\n      startFrom: expectedStartDateOfMonth,\n      rows: 6,\n      rangeStart: Day.SUN,\n      rangeEnd: Day.SAT,\n    });\n\n    const result = createDateMatrixOfMonth(targetMonth, {});\n\n    expect(result).toEqual(expected);\n  });\n\n  it('should create matrix of dates less than 6 weeks', () => {\n    const targetMonth = new TZDate('2021-12-01T00:00:00');\n    const expectedStartDateOfMonth = new TZDate('2021-11-28T00:00:00');\n\n    const expected = createResultMatrix({\n      startFrom: expectedStartDateOfMonth,\n      rows: 4,\n      rangeStart: Day.SUN,\n      rangeEnd: Day.SAT,\n    });\n\n    const result = createDateMatrixOfMonth(targetMonth, {\n      visibleWeeksCount: 4,\n    });\n\n    expect(result).toEqual(expected);\n  });\n\n  it('should create matrix of dates less than 6 weeks, even though target date is not the first day of the month', () => {\n    const targetDate = new TZDate('2021-12-15T00:00:00');\n    const expectedStartDateOfMonth = new TZDate('2021-12-12T00:00:00');\n\n    const expected = createResultMatrix({\n      startFrom: expectedStartDateOfMonth,\n      rows: 2,\n      rangeStart: Day.SUN,\n      rangeEnd: Day.SAT,\n    });\n\n    const result = createDateMatrixOfMonth(targetDate, {\n      visibleWeeksCount: 2,\n    });\n\n    expect(result).toEqual(expected);\n  });\n\n  it('should exclude weekends when workweek option is enabled', () => {\n    const targetMonth = new TZDate('2021-12-01T00:00:00');\n    const expectedStartDateOfMonth = new TZDate('2021-11-28T00:00:00');\n\n    const expected = createResultMatrix({\n      startFrom: expectedStartDateOfMonth,\n      rows: 6,\n      rangeStart: Day.MON,\n      rangeEnd: Day.FRI,\n    });\n\n    const result = createDateMatrixOfMonth(targetMonth, {\n      workweek: true,\n    });\n\n    expect(result).toEqual(expected);\n  });\n\n  it('should ignore isAlways6Weeks option when visibleWeeksCount option is enabled', () => {\n    const targetMonth = new TZDate('2021-12-01T00:00:00');\n    const expectedStartDateOfMonth = new TZDate('2021-11-28T00:00:00');\n\n    const expected = createResultMatrix({\n      startFrom: expectedStartDateOfMonth,\n      rows: 4,\n      rangeStart: Day.SUN,\n      rangeEnd: Day.SAT,\n    });\n\n    const result = createDateMatrixOfMonth(targetMonth, {\n      visibleWeeksCount: 4,\n      isAlways6Weeks: true,\n    });\n\n    expect(result).toEqual(expected);\n  });\n\n  it('should create 5 weeks for month has only 5 weeks when isAlways6Weeks option is disabled', () => {\n    const targetMonth = new TZDate('2021-08-01T00:00:00');\n    const expectedStartDateOfMonth = new TZDate('2021-08-01T00:00:00');\n\n    const expected = createResultMatrix({\n      startFrom: expectedStartDateOfMonth,\n      rows: 5,\n      rangeStart: Day.SUN,\n      rangeEnd: Day.SAT,\n    });\n\n    const result = createDateMatrixOfMonth(targetMonth, {\n      isAlways6Weeks: false,\n    });\n\n    expect(result).toEqual(expected);\n  });\n\n  it('should create 6 weeks even though target month has only 5 weeks when isAlways6Weeks option is enabled', () => {\n    const targetMonth = new TZDate('2021-08-01T00:00:00');\n    const expectedStartDateOfMonth = new TZDate('2021-08-01T00:00:00');\n\n    const expected = createResultMatrix({\n      startFrom: expectedStartDateOfMonth,\n      rows: 6,\n      rangeStart: Day.SUN,\n      rangeEnd: Day.SAT,\n    });\n\n    const result = createDateMatrixOfMonth(targetMonth, {\n      isAlways6Weeks: true,\n    });\n\n    expect(result).toEqual(expected);\n  });\n\n  it('should not start from sunday when startDayOfWeek option is provided', () => {\n    const targetMonth = new TZDate('2021-12-01T00:00:00');\n    const createExpected = (startFrom: TZDate) =>\n      createResultMatrix({\n        startFrom,\n        rows: 6,\n        rangeStart: Day.SUN,\n        rangeEnd: Day.SAT,\n      });\n\n    const startingMonday = new TZDate('2021-11-29T00:00:00');\n    const expectedStartFromMonday = createExpected(startingMonday);\n    const resultStartFromMonday = createDateMatrixOfMonth(targetMonth, {\n      startDayOfWeek: 1,\n    });\n\n    expect(resultStartFromMonday).toEqual(expectedStartFromMonday);\n\n    const startingWednesday = new TZDate('2021-12-01T00:00:00');\n    const expectStartFromWednesday = createExpected(startingWednesday);\n    const resultStartFromWednesday = createDateMatrixOfMonth(targetMonth, {\n      startDayOfWeek: 3,\n    });\n\n    expect(resultStartFromWednesday).toEqual(expectStartFromWednesday);\n\n    const startingFriday = new TZDate('2021-11-26T00:00:00');\n    const expectStartFromFriday = createExpected(startingFriday);\n    const resultStartFromFriday = createDateMatrixOfMonth(targetMonth, {\n      startDayOfWeek: 5,\n    });\n\n    expect(resultStartFromFriday).toEqual(expectStartFromFriday);\n  });\n});\n\ndescribe('getColumnStyles', () => {\n  it('should create default styles of a week', () => {\n    // Given\n    const weekDates = getWeekDates(new TZDate('2021-01-28T00:00:00'), {\n      startDayOfWeek: Day.SUN,\n      workweek: false,\n    });\n    const expectedWidth = 100 / weekDates.length;\n    const getExpectedLeft = (index: number) => expectedWidth * index;\n\n    // When\n    const result = getColumnsData(weekDates);\n    const totalWidth = result.reduce((acc, curr) => acc + curr.width, 0);\n\n    // Then\n    expect(result).toHaveLength(7);\n    expect(totalWidth).toBeCloseTo(100, 0);\n    weekDates.forEach((date, index) => {\n      expect(result[index]).toEqual({\n        date,\n        width: expectedWidth,\n        left: getExpectedLeft(index),\n      });\n    });\n  });\n\n  it('should create styles of a workweek', () => {\n    // Given\n    const weekDates = getWeekDates(new TZDate('2021-01-28T00:00:00'), {\n      startDayOfWeek: Day.SUN,\n      workweek: true,\n    });\n    const expectedWidth = 100 / weekDates.length;\n    const getExpectedLeft = (index: number) => expectedWidth * index;\n\n    // When\n    const result = getColumnsData(weekDates);\n    const totalWidth = result.reduce((acc, curr) => acc + curr.width, 0);\n\n    // Then\n    expect(result).toHaveLength(5);\n    expect(totalWidth).toBeCloseTo(100, 0);\n    weekDates.forEach((date, index) => {\n      expect(result[index]).toEqual({\n        date,\n        width: expectedWidth,\n        left: getExpectedLeft(index),\n      });\n    });\n  });\n\n  it('should create styles of a week with narrowWeekend option', () => {\n    // Given\n    const weekDates = getWeekDates(new TZDate('2021-01-28T00:00:00'), {\n      startDayOfWeek: Day.SUN,\n      workweek: false,\n    });\n    const expectedBasicWidth = 100 / (weekDates.length - 1);\n    const expectedNarrowWidth = expectedBasicWidth / 2;\n    let expectedLeft = 0;\n\n    // When\n    const result = getColumnsData(weekDates, true);\n    const totalWidth = result.reduce((acc, curr) => acc + curr.width, 0);\n\n    // Then\n    expect(result).toHaveLength(7);\n    expect(totalWidth).toBeCloseTo(100, 0);\n    weekDates.forEach((date, index) => {\n      expect(result[index]).toEqual({\n        date,\n        width: isWeekend(date.getDay()) ? expectedNarrowWidth : expectedBasicWidth,\n        left: expectedLeft,\n      });\n\n      expectedLeft += result[index].width;\n    });\n  });\n});\n\ndescribe('createTimeGridData', () => {\n  function assertTimeGridDataRows(\n    expectedRows: TimeGridRow[],\n    options: { hourStart: number; hourEnd: number }\n  ) {\n    const steps = (options.hourEnd - options.hourStart) * 2;\n    const expectedRowHeight = 100 / steps;\n\n    expect(expectedRows).toHaveLength(steps);\n    range(steps).forEach((step, index) => {\n      const isOdd = index % 2 === 1;\n      const hour = options.hourStart + Math.floor(step / 2);\n\n      expect(expectedRows[index]).toEqual({\n        top: expectedRowHeight * index,\n        height: expectedRowHeight,\n        startTime: `${hour}:${isOdd ? '30' : '00'}`.padStart(5, '0'),\n        endTime: (isOdd ? `${hour + 1}:00` : `${hour}:30`).padStart(5, '0'),\n      });\n    });\n  }\n\n  it('should create data by default values', () => {\n    // Given\n    const rows = getWeekDates(new TZDate('2021-01-28T00:00:00'), {\n      startDayOfWeek: Day.SUN,\n    });\n    const options = { hourStart: 0, hourEnd: 24 };\n\n    // When\n    const result = createTimeGridData(rows, options);\n\n    // Then\n    expect(result.columns).toEqual(getColumnsData(rows));\n    assertTimeGridDataRows(result.rows, options);\n  });\n\n  it('should create data when rendering 00:00 to 12:00', () => {\n    // Given\n    const rows = getWeekDates(new TZDate('2021-01-28T00:00:00'), {\n      startDayOfWeek: Day.SUN,\n    });\n    const options = { hourStart: 0, hourEnd: 12 };\n\n    // When\n    const result = createTimeGridData(rows, options);\n\n    // Then\n    expect(result.columns).toEqual(getColumnsData(rows));\n    assertTimeGridDataRows(result.rows, options);\n  });\n\n  it('should create data when rendering 12:00 to 24:00', () => {\n    // Given\n    const rows = getWeekDates(new TZDate('2021-01-28T00:00:00'), {\n      startDayOfWeek: Day.SUN,\n    });\n    const options = { hourStart: 12, hourEnd: 24 };\n\n    // When\n    const result = createTimeGridData(rows, options);\n\n    // Then\n    expect(result.columns).toEqual(getColumnsData(rows));\n    assertTimeGridDataRows(result.rows, options);\n  });\n\n  it('should create narrow weekends with option', () => {\n    // Given\n    const narrowWeekend = true;\n    const rows = getWeekDates(new TZDate('2021-01-28T00:00:00'), {\n      startDayOfWeek: Day.SUN,\n    });\n    const options = { hourStart: 0, hourEnd: 24, narrowWeekend };\n\n    // When\n    const result = createTimeGridData(rows, options);\n\n    // Then\n    expect(result.columns).toEqual(getColumnsData(rows, narrowWeekend));\n  });\n});\n\ndescribe('createGridPositionFinder', () => {\n  const container = document.createElement('div');\n  let gridPositionFinder: GridPositionFinder;\n\n  function assertGridPosition(results: GridPosition[], expected: GridPosition[]) {\n    expect(results.length).toBe(expected.length);\n    results.forEach((result, index) => {\n      expect(result).toEqual(expected[index]);\n    });\n  }\n\n  afterEach(() => {\n    jest.clearAllMocks();\n  });\n\n  describe('when narrowWeekend is false', () => {\n    const narrowWeekend = false;\n\n    it('should be null returning function if container is null', () => {\n      // Given\n      gridPositionFinder = createGridPositionFinder({\n        columnsCount: 7,\n        rowsCount: 6,\n        container: null,\n        narrowWeekend,\n      });\n\n      // When\n      const result = gridPositionFinder({ clientX: 100, clientY: 100 });\n\n      // Then\n      expect(result).toBeNull();\n    });\n\n    it('should return null if mouse position is out of container', () => {\n      // Given\n      jest.spyOn(container, 'getBoundingClientRect').mockReturnValue({\n        x: 0,\n        y: 0,\n        top: 0,\n        bottom: 0,\n        left: 0,\n        right: 0,\n        width: 100,\n        height: 100,\n        toJSON: noop,\n      });\n\n      gridPositionFinder = createGridPositionFinder({\n        columnsCount: 7,\n        rowsCount: 6,\n        container,\n        narrowWeekend,\n      });\n\n      const wrongCases = [\n        { clientX: -1, clientY: -1 },\n        { clientX: -1, clientY: 50 },\n        { clientX: 50, clientY: -1 },\n        { clientX: 50, clientY: 101 },\n        { clientX: 101, clientY: 101 },\n        { clientX: 101, clientY: 50 },\n        { clientX: 101, clientY: -1 },\n        { clientX: 50, clientY: -1 },\n      ];\n\n      // When\n      const results = wrongCases.map(({ clientX, clientY }) =>\n        gridPositionFinder({ clientX, clientY })\n      );\n\n      // Then\n      results.forEach((result) => expect(result).toBeNull());\n    });\n\n    it('should calculate columnIndex & rowIndex of grid in month', () => {\n      // Given\n      jest.spyOn(container, 'getBoundingClientRect').mockReturnValue({\n        x: 0,\n        y: 0,\n        top: 0,\n        bottom: 0,\n        left: 0,\n        right: 0,\n        width: 70,\n        height: 100,\n        toJSON: noop,\n      });\n      gridPositionFinder = createGridPositionFinder({\n        columnsCount: 7,\n        rowsCount: 2,\n        container,\n        narrowWeekend,\n      });\n      const cases = [\n        {\n          clientX: 9,\n          clientY: 20,\n          expected: {\n            columnIndex: 0,\n            rowIndex: 0,\n          },\n        },\n        {\n          clientX: 55,\n          clientY: 60,\n          expected: {\n            columnIndex: 5,\n            rowIndex: 1,\n          },\n        },\n      ];\n\n      // When\n      const results = cases.map(({ clientX, clientY }) => gridPositionFinder({ clientX, clientY }));\n\n      // Then\n      assertGridPosition(\n        results as GridPosition[],\n        cases.map(({ expected }) => expected)\n      );\n    });\n\n    it('should calculate columnIndex & rowIndex of grid in week', () => {\n      // Given\n      jest.spyOn(container, 'getBoundingClientRect').mockReturnValue({\n        x: 0,\n        y: 0,\n        top: 0,\n        bottom: 0,\n        left: 0,\n        right: 0,\n        width: 560,\n        height: 100,\n        toJSON: noop,\n      });\n\n      gridPositionFinder = createGridPositionFinder({\n        columnsCount: 7,\n        rowsCount: 1,\n        container,\n        narrowWeekend,\n      });\n\n      const cases = [\n        {\n          clientX: 0,\n          clientY: 20,\n          expected: {\n            columnIndex: 0,\n            rowIndex: 0,\n          },\n        },\n        {\n          clientX: 100,\n          clientY: 40,\n          expected: {\n            columnIndex: 1,\n            rowIndex: 0,\n          },\n        },\n        {\n          clientX: 390,\n          clientY: 50,\n          expected: {\n            columnIndex: 4,\n            rowIndex: 0,\n          },\n        },\n        {\n          clientX: 500,\n          clientY: 60,\n          expected: {\n            columnIndex: 6,\n            rowIndex: 0,\n          },\n        },\n      ];\n\n      // When\n      const results = cases.map(({ clientX, clientY }) => gridPositionFinder({ clientX, clientY }));\n\n      // Then\n      assertGridPosition(\n        results as GridPosition[],\n        cases.map(({ expected }) => expected)\n      );\n    });\n\n    it('should calculate columnIndex & rowIndex of grid in time grid', () => {\n      // Given\n      jest.spyOn(container, 'getBoundingClientRect').mockReturnValue({\n        x: 0,\n        y: 0,\n        top: 0,\n        bottom: 0,\n        left: 0,\n        right: 0,\n        width: 700,\n        height: 960,\n        toJSON: noop,\n      });\n\n      gridPositionFinder = createGridPositionFinder({\n        columnsCount: 7,\n        rowsCount: 48,\n        container,\n        narrowWeekend,\n      });\n\n      const cases = [\n        {\n          clientX: 0,\n          clientY: 0,\n          expected: {\n            columnIndex: 0,\n            rowIndex: 0,\n          },\n        },\n        {\n          clientX: 250,\n          clientY: 130,\n          expected: {\n            columnIndex: 2,\n            rowIndex: 6,\n          },\n        },\n        {\n          clientX: 450,\n          clientY: 230,\n          expected: {\n            columnIndex: 4,\n            rowIndex: 11,\n          },\n        },\n        {\n          clientX: 650,\n          clientY: 450,\n          expected: {\n            columnIndex: 6,\n            rowIndex: 22,\n          },\n        },\n        {\n          clientX: 700,\n          clientY: 720,\n          expected: {\n            columnIndex: 6,\n            rowIndex: 36,\n          },\n        },\n        {\n          clientX: 700,\n          clientY: 730,\n          expected: {\n            columnIndex: 6,\n            rowIndex: 36,\n          },\n        },\n        {\n          clientX: 700,\n          clientY: 935,\n          expected: {\n            columnIndex: 6,\n            rowIndex: 46,\n          },\n        },\n        {\n          clientX: 700,\n          clientY: 960,\n          expected: {\n            columnIndex: 6,\n            rowIndex: 47,\n          },\n        },\n      ];\n\n      // When\n      const results = cases.map(({ clientX, clientY }) =>\n        gridPositionFinder({\n          clientX,\n          clientY,\n        })\n      );\n\n      // Then\n      assertGridPosition(\n        results as GridPosition[],\n        cases.map(({ expected }) => expected)\n      );\n    });\n  });\n\n  describe('when narrowWeekend is true', () => {\n    const narrowWeekend = true;\n\n    it('should be null returning function if container is null', () => {\n      // Given\n      gridPositionFinder = createGridPositionFinder({\n        columnsCount: 7,\n        rowsCount: 6,\n        container: null,\n        narrowWeekend,\n      });\n\n      // When\n      const result = gridPositionFinder({ clientX: 100, clientY: 100 });\n\n      // Then\n      expect(result).toBeNull();\n    });\n\n    it('should return null if mouse position is out of container', () => {\n      // Given\n      jest.spyOn(container, 'getBoundingClientRect').mockReturnValue({\n        x: 0,\n        y: 0,\n        top: 0,\n        bottom: 0,\n        left: 0,\n        right: 0,\n        width: 100,\n        height: 100,\n        toJSON: noop,\n      });\n\n      gridPositionFinder = createGridPositionFinder({\n        columnsCount: 7,\n        rowsCount: 6,\n        container,\n        narrowWeekend,\n      });\n\n      const wrongCases = [\n        { clientX: -1, clientY: -1 },\n        { clientX: -1, clientY: 50 },\n        { clientX: 50, clientY: -1 },\n        { clientX: 50, clientY: 101 },\n        { clientX: 101, clientY: 101 },\n        { clientX: 101, clientY: 50 },\n        { clientX: 101, clientY: -1 },\n        { clientX: 50, clientY: -1 },\n      ];\n\n      // When\n      const results = wrongCases.map(({ clientX, clientY }) =>\n        gridPositionFinder({ clientX, clientY })\n      );\n\n      // Then\n      results.forEach((result) => expect(result).toBeNull());\n    });\n\n    it('should calculate columnIndex & rowIndex of grid in month', () => {\n      // Given\n      jest.spyOn(container, 'getBoundingClientRect').mockReturnValue({\n        x: 0,\n        y: 0,\n        top: 0,\n        bottom: 0,\n        left: 0,\n        right: 0,\n        width: 60,\n        height: 100,\n        toJSON: noop,\n      });\n      gridPositionFinder = createGridPositionFinder({\n        columnsCount: 7,\n        rowsCount: 2,\n        container,\n        narrowWeekend,\n      });\n      const cases = [\n        {\n          clientX: 4,\n          clientY: 20,\n          expected: {\n            columnIndex: 0,\n            rowIndex: 0,\n          },\n        },\n        {\n          clientX: 5,\n          clientY: 20,\n          expected: {\n            columnIndex: 1,\n            rowIndex: 0,\n          },\n        },\n        {\n          clientX: 54,\n          clientY: 60,\n          expected: {\n            columnIndex: 5,\n            rowIndex: 1,\n          },\n        },\n        {\n          clientX: 55,\n          clientY: 60,\n          expected: {\n            columnIndex: 6,\n            rowIndex: 1,\n          },\n        },\n      ];\n\n      // When\n      const results = cases.map(({ clientX, clientY }) => gridPositionFinder({ clientX, clientY }));\n\n      // Then\n      assertGridPosition(\n        results as GridPosition[],\n        cases.map(({ expected }) => expected)\n      );\n    });\n\n    it('should calculate columnIndex & rowIndex of grid in week', () => {\n      // Given\n      jest.spyOn(container, 'getBoundingClientRect').mockReturnValue({\n        x: 0,\n        y: 0,\n        top: 0,\n        bottom: 0,\n        left: 0,\n        right: 0,\n        width: 600,\n        height: 100,\n        toJSON: noop,\n      });\n\n      gridPositionFinder = createGridPositionFinder({\n        columnsCount: 7,\n        rowsCount: 1,\n        container,\n        narrowWeekend,\n      });\n\n      const cases = [\n        {\n          clientX: 0,\n          clientY: 20,\n          expected: {\n            columnIndex: 0,\n            rowIndex: 0,\n          },\n        },\n        {\n          clientX: 50,\n          clientY: 30,\n          expected: {\n            columnIndex: 1,\n            rowIndex: 0,\n          },\n        },\n        {\n          clientX: 100,\n          clientY: 40,\n          expected: {\n            columnIndex: 1,\n            rowIndex: 0,\n          },\n        },\n        {\n          clientX: 400,\n          clientY: 50,\n          expected: {\n            columnIndex: 4,\n            rowIndex: 0,\n          },\n        },\n        {\n          clientX: 500,\n          clientY: 50,\n          expected: {\n            columnIndex: 5,\n            rowIndex: 0,\n          },\n        },\n        {\n          clientX: 550,\n          clientY: 60,\n          expected: {\n            columnIndex: 6,\n            rowIndex: 0,\n          },\n        },\n      ];\n\n      // When\n      const results = cases.map(({ clientX, clientY }) => gridPositionFinder({ clientX, clientY }));\n\n      // Then\n      assertGridPosition(\n        results as GridPosition[],\n        cases.map(({ expected }) => expected)\n      );\n    });\n\n    it('should calculate columnIndex & rowIndex of grid in time grid', () => {\n      // Given\n      jest.spyOn(container, 'getBoundingClientRect').mockReturnValue({\n        x: 0,\n        y: 0,\n        top: 0,\n        bottom: 0,\n        left: 0,\n        right: 0,\n        width: 600,\n        height: 960,\n        toJSON: noop,\n      });\n\n      gridPositionFinder = createGridPositionFinder({\n        columnsCount: 7,\n        rowsCount: 48,\n        container,\n        narrowWeekend,\n      });\n\n      const cases = [\n        {\n          clientX: 0,\n          clientY: 0,\n          expected: {\n            columnIndex: 0,\n            rowIndex: 0,\n          },\n        },\n        {\n          clientX: 50,\n          clientY: 130,\n          expected: {\n            columnIndex: 1,\n            rowIndex: 6,\n          },\n        },\n        {\n          clientX: 200,\n          clientY: 130,\n          expected: {\n            columnIndex: 2,\n            rowIndex: 6,\n          },\n        },\n        {\n          clientX: 400,\n          clientY: 230,\n          expected: {\n            columnIndex: 4,\n            rowIndex: 11,\n          },\n        },\n        {\n          clientX: 500,\n          clientY: 330,\n          expected: {\n            columnIndex: 5,\n            rowIndex: 16,\n          },\n        },\n        {\n          clientX: 550,\n          clientY: 450,\n          expected: {\n            columnIndex: 6,\n            rowIndex: 22,\n          },\n        },\n        {\n          clientX: 600,\n          clientY: 720,\n          expected: {\n            columnIndex: 6,\n            rowIndex: 36,\n          },\n        },\n        {\n          clientX: 600,\n          clientY: 730,\n          expected: {\n            columnIndex: 6,\n            rowIndex: 36,\n          },\n        },\n        {\n          clientX: 600,\n          clientY: 935,\n          expected: {\n            columnIndex: 6,\n            rowIndex: 46,\n          },\n        },\n        {\n          clientX: 600,\n          clientY: 960,\n          expected: {\n            columnIndex: 6,\n            rowIndex: 47,\n          },\n        },\n      ];\n\n      // When\n      const results = cases.map(({ clientX, clientY }) =>\n        gridPositionFinder({\n          clientX,\n          clientY,\n        })\n      );\n\n      // Then\n      assertGridPosition(\n        results as GridPosition[],\n        cases.map(({ expected }) => expected)\n      );\n    });\n  });\n});\n\ndescribe('getWeekDates', () => {\n  // 2022-07-17(Sun) ~ 2022-07-23(Sat)\n  const todayList = [\n    new TZDate(2022, 6, 17),\n    new TZDate(2022, 6, 18),\n    new TZDate(2022, 6, 19),\n    new TZDate(2022, 6, 20),\n    new TZDate(2022, 6, 21),\n    new TZDate(2022, 6, 22),\n    new TZDate(2022, 6, 23),\n  ];\n  afterEach(() => {\n    jest.clearAllMocks();\n  });\n\n  // startDayOfWeek, dayOfToday, prevDateCountOfToday, nextDateCountOfToday\n  test.concurrent.each([\n    [Day.SUN, Day.SUN, 0, 6],\n    [Day.SUN, Day.MON, 1, 5],\n    [Day.SUN, Day.TUE, 2, 4],\n    [Day.SUN, Day.WED, 3, 3],\n    [Day.SUN, Day.THU, 4, 2],\n    [Day.SUN, Day.FRI, 5, 1],\n    [Day.SUN, Day.SAT, 6, 0],\n    [Day.MON, Day.SUN, 6, 0],\n    [Day.MON, Day.MON, 0, 6],\n    [Day.MON, Day.TUE, 1, 5],\n    [Day.MON, Day.WED, 2, 4],\n    [Day.MON, Day.THU, 3, 3],\n    [Day.MON, Day.FRI, 4, 2],\n    [Day.MON, Day.SAT, 5, 1],\n    [Day.TUE, Day.SUN, 5, 1],\n    [Day.TUE, Day.MON, 6, 0],\n    [Day.TUE, Day.TUE, 0, 6],\n    [Day.TUE, Day.WED, 1, 5],\n    [Day.TUE, Day.THU, 2, 4],\n    [Day.TUE, Day.FRI, 3, 3],\n    [Day.TUE, Day.SAT, 4, 2],\n    [Day.WED, Day.SUN, 4, 2],\n    [Day.WED, Day.MON, 5, 1],\n    [Day.WED, Day.TUE, 6, 0],\n    [Day.WED, Day.WED, 0, 6],\n    [Day.WED, Day.THU, 1, 5],\n    [Day.WED, Day.FRI, 2, 4],\n    [Day.WED, Day.SAT, 3, 3],\n    [Day.THU, Day.SUN, 3, 3],\n    [Day.THU, Day.MON, 4, 2],\n    [Day.THU, Day.TUE, 5, 1],\n    [Day.THU, Day.WED, 6, 0],\n    [Day.THU, Day.THU, 0, 6],\n    [Day.THU, Day.FRI, 1, 5],\n    [Day.THU, Day.SAT, 2, 4],\n    [Day.FRI, Day.SUN, 2, 4],\n    [Day.FRI, Day.MON, 3, 3],\n    [Day.FRI, Day.TUE, 4, 2],\n    [Day.FRI, Day.WED, 5, 1],\n    [Day.FRI, Day.THU, 6, 0],\n    [Day.FRI, Day.FRI, 0, 6],\n    [Day.FRI, Day.SAT, 1, 5],\n    [Day.SAT, Day.SUN, 1, 5],\n    [Day.SAT, Day.MON, 2, 4],\n    [Day.SAT, Day.TUE, 3, 3],\n    [Day.SAT, Day.WED, 4, 2],\n    [Day.SAT, Day.THU, 5, 1],\n    [Day.SAT, Day.FRI, 6, 0],\n    [Day.SAT, Day.SAT, 0, 6],\n  ])(\n    'startDayOfWeek: %i, today: %i',\n    // eslint-disable-next-line require-await\n    async (startDayOfWeek, dayOfToday, prevDateCount, nextDateCount) => {\n      // Given\n      const today = todayList[dayOfToday];\n      const prevDateList = range(-prevDateCount, 0).map((step) => addDate(today, step));\n      const nextDateList = range(1, nextDateCount + 1).map((step) => addDate(today, step));\n      const expected = [...prevDateList, today, ...nextDateList];\n\n      // When\n      const dates = getWeekDates(today, { startDayOfWeek, workweek: false });\n\n      // Then\n      expect(dates).toEqual(expected);\n    }\n  );\n});\n"
  },
  {
    "path": "apps/calendar/src/helpers/grid.ts",
    "content": "import range from 'tui-code-snippet/array/range';\n\nimport { DEFAULT_VISIBLE_WEEKS } from '@src/constants/grid';\nimport { findByDateRange } from '@src/controller/month';\nimport { findByDateRange as findByDateRangeForWeek } from '@src/controller/week';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport TZDate from '@src/time/date';\nimport {\n  addDate,\n  Day,\n  getDateDifference,\n  isWeekend,\n  subtractDate,\n  toEndOfDay,\n  toEndOfMonth,\n  toStartOfDay,\n  toStartOfMonth,\n  WEEK_DAYS,\n} from '@src/time/datetime';\nimport { findLastIndex } from '@src/utils/array';\nimport { limit, ratio } from '@src/utils/math';\nimport { isNil } from '@src/utils/type';\n\nimport type {\n  CalendarData,\n  DayGridEventMatrix,\n  EventModelMap,\n  Matrix3d,\n  TimeGridEventMatrix,\n} from '@t/events';\nimport type { CommonGridColumn, GridPositionFinder, TimeGridData } from '@t/grid';\nimport type { ClientMousePosition } from '@t/mouse';\nimport type { MonthOptions, WeekOptions } from '@t/options';\nimport type { Panel } from '@t/panel';\nimport type { FormattedTimeString } from '@t/time/datetime';\n\nexport const EVENT_HEIGHT = 22;\nexport const TOTAL_WIDTH = 100;\n\nfunction forEachMatrix3d<T>(matrices: Matrix3d<T>, iteratee: (target: T, index?: number) => void) {\n  matrices.forEach((matrix) => {\n    matrix.forEach((row) => {\n      row.forEach((value, index) => {\n        iteratee(value, index);\n      });\n    });\n  });\n}\n\nexport function isWithinHeight(containerHeight: number, eventHeight: number) {\n  return ({ top }: EventUIModel) => containerHeight >= top * eventHeight;\n}\n\nexport function isExceededHeight(containerHeight: number, eventHeight: number) {\n  return ({ top }: EventUIModel) => containerHeight < top * eventHeight;\n}\n\nexport function getExceedCount(\n  uiModel: EventUIModel[],\n  containerHeight: number,\n  eventHeight: number\n) {\n  return uiModel.filter(isExceededHeight(containerHeight, eventHeight)).length;\n}\n\nconst getWeekendCount = (row: TZDate[]) => row.filter((cell) => isWeekend(cell.getDay())).length;\n\nexport function getGridWidthAndLeftPercentValues(\n  row: TZDate[],\n  narrowWeekend: boolean,\n  totalWidth: number\n) {\n  const weekendCount = getWeekendCount(row);\n  const gridCellCount = row.length;\n  const isAllWeekend = weekendCount === gridCellCount;\n  const widthPerDay =\n    totalWidth /\n    (narrowWeekend && !isAllWeekend ? gridCellCount * 2 - weekendCount : gridCellCount);\n\n  const widthList: number[] = row.map((cell) => {\n    const day = cell.getDay();\n\n    if (!narrowWeekend || isAllWeekend) {\n      return widthPerDay;\n    }\n\n    return isWeekend(day) ? widthPerDay : widthPerDay * 2;\n  });\n\n  const leftList = widthList.reduce<number[]>(\n    (acc, _, index) => (index ? [...acc, acc[index - 1] + widthList[index - 1]] : [0]),\n    []\n  );\n\n  return {\n    widthList,\n    leftList,\n  };\n}\n\nexport function getWidth(widthList: number[], start: number, end: number) {\n  return widthList.reduce((acc, width, index) => {\n    if (start <= index && index <= end) {\n      return acc + width;\n    }\n\n    return acc;\n  }, 0);\n}\n\nexport const isInGrid = (gridDate: TZDate) => {\n  return (uiModel: EventUIModel) => {\n    const eventStart = toStartOfDay(uiModel.getStarts());\n    const eventEnd = toStartOfDay(uiModel.getEnds());\n\n    return eventStart <= gridDate && gridDate <= eventEnd;\n  };\n};\n\nexport function getGridDateIndex(date: TZDate, row: TZDate[]) {\n  return row.findIndex((cell) => date >= toStartOfDay(cell) && date <= toEndOfDay(cell));\n}\n\nexport const getLeftAndWidth = (\n  startIndex: number,\n  endIndex: number,\n  row: TZDate[],\n  narrowWeekend: boolean\n) => {\n  const { widthList } = getGridWidthAndLeftPercentValues(row, narrowWeekend, TOTAL_WIDTH);\n\n  return {\n    left: !startIndex ? 0 : getWidth(widthList, 0, startIndex - 1),\n    width: getWidth(widthList, startIndex ?? 0, endIndex < 0 ? row.length - 1 : endIndex),\n  };\n};\n\nexport const getEventLeftAndWidth = (\n  start: TZDate,\n  end: TZDate,\n  row: TZDate[],\n  narrowWeekend: boolean\n) => {\n  const { widthList } = getGridWidthAndLeftPercentValues(row, narrowWeekend, TOTAL_WIDTH);\n\n  let gridStartIndex = 0;\n  let gridEndIndex = row.length - 1;\n\n  row.forEach((cell, index) => {\n    if (cell <= start) {\n      gridStartIndex = index;\n    }\n    if (cell <= end) {\n      gridEndIndex = index;\n    }\n  });\n\n  return {\n    width: getWidth(widthList, gridStartIndex, gridEndIndex),\n    left: !gridStartIndex ? 0 : getWidth(widthList, 0, gridStartIndex - 1),\n  };\n};\n\nfunction getEventUIModelWithPosition(\n  uiModel: EventUIModel,\n  row: TZDate[],\n  narrowWeekend = false\n): EventUIModel {\n  const modelStart = uiModel.getStarts();\n  const modelEnd = uiModel.getEnds();\n  const { width, left } = getEventLeftAndWidth(modelStart, modelEnd, row, narrowWeekend);\n\n  uiModel.width = width;\n  uiModel.left = left;\n\n  return uiModel;\n}\n\nexport function getRenderedEventUIModels(\n  row: TZDate[],\n  calendarData: CalendarData,\n  narrowWeekend: boolean\n) {\n  const { idsOfDay } = calendarData;\n  const eventUIModels = findByDateRange(calendarData, {\n    start: row[0],\n    end: toEndOfDay(row[row.length - 1]),\n  });\n  const idEventModelMap: Record<number, EventUIModel> = [];\n\n  forEachMatrix3d(eventUIModels, (uiModel) => {\n    const cid = uiModel.model.cid();\n    idEventModelMap[cid] = getEventUIModelWithPosition(uiModel, row, narrowWeekend);\n  });\n\n  const gridDateEventModelMap = Object.keys(idsOfDay).reduce<Record<string, EventUIModel[]>>(\n    (acc, ymd) => {\n      const ids = idsOfDay[ymd];\n\n      acc[ymd] = ids.map((cid) => idEventModelMap[cid]).filter((vm) => !!vm);\n\n      return acc;\n    },\n    {}\n  );\n\n  return {\n    uiModels: Object.values(idEventModelMap),\n    gridDateEventModelMap,\n  };\n}\n\nconst getDayGridEventModels = (\n  eventModels: DayGridEventMatrix,\n  row: TZDate[],\n  narrowWeekend = false\n): EventUIModel[] => {\n  forEachMatrix3d(eventModels, (uiModel) => {\n    const modelStart = uiModel.getStarts();\n    const modelEnd = uiModel.getEnds();\n    const { width, left } = getEventLeftAndWidth(modelStart, modelEnd, row, narrowWeekend);\n\n    uiModel.width = width;\n    uiModel.left = left;\n    uiModel.top += 1;\n  });\n\n  return flattenMatrix3d(eventModels);\n};\n\nconst getModels = (models: EventUIModel[]) => models.filter((model) => !!model);\n\nfunction flattenMatrix3d(matrices: DayGridEventMatrix): EventUIModel[] {\n  return matrices.flatMap((matrix) => matrix.flatMap((models) => getModels(models)));\n}\n\n// TODO: Check it works well when the `narrowWeekend` option is true\nconst getTimeGridEventModels = (eventMatrix: TimeGridEventMatrix): EventUIModel[] =>\n  // NOTE: there are same ui models in different rows. so we need to get unique ui models.\n  Array.from(\n    new Set(\n      Object.values(eventMatrix).reduce<EventUIModel[]>(\n        (result, matrix3d) => result.concat(...flattenMatrix3d(matrix3d)),\n        []\n      )\n    )\n  );\n\nexport const getWeekViewEvents = (\n  row: TZDate[],\n  calendarData: CalendarData,\n  {\n    narrowWeekend,\n    hourStart,\n    hourEnd,\n    weekStartDate,\n    weekEndDate,\n  }: WeekOptions & {\n    weekStartDate: TZDate;\n    weekEndDate: TZDate;\n  }\n): EventModelMap => {\n  const panels: Panel[] = [\n    {\n      name: 'milestone',\n      type: 'daygrid',\n      show: true,\n    },\n    {\n      name: 'task',\n      type: 'daygrid',\n      show: true,\n    },\n    {\n      name: 'allday',\n      type: 'daygrid',\n      show: true,\n    },\n    {\n      name: 'time',\n      type: 'timegrid',\n      show: true,\n    },\n  ];\n  const eventModels = findByDateRangeForWeek(calendarData, {\n    start: weekStartDate,\n    end: weekEndDate,\n    panels,\n    andFilters: [],\n    options: {\n      hourStart,\n      hourEnd,\n    },\n  });\n\n  return Object.keys(eventModels).reduce<EventModelMap>(\n    (acc, cur) => {\n      const events = eventModels[cur as keyof EventModelMap];\n\n      return {\n        ...acc,\n        [cur]: Array.isArray(events)\n          ? getDayGridEventModels(events, row, narrowWeekend)\n          : getTimeGridEventModels(events),\n      };\n    },\n    {\n      milestone: [],\n      allday: [],\n      task: [],\n      time: [],\n    }\n  );\n};\n\nexport function createDateMatrixOfMonth(\n  renderTargetDate: Date | TZDate,\n  {\n    workweek = false,\n    visibleWeeksCount = 0,\n    startDayOfWeek = 0,\n    isAlways6Weeks = true,\n  }: MonthOptions\n) {\n  const targetDate = new TZDate(renderTargetDate);\n  const shouldApplyVisibleWeeksCount = visibleWeeksCount > 0;\n  const baseDate = shouldApplyVisibleWeeksCount ? targetDate : toStartOfMonth(targetDate);\n  const firstDateOfMatrix = subtractDate(\n    baseDate,\n    baseDate.getDay() - startDayOfWeek + (baseDate.getDay() < startDayOfWeek ? WEEK_DAYS : 0)\n  );\n  const dayOfFirstDateOfMatrix = firstDateOfMatrix.getDay();\n\n  const totalDatesCountOfMonth = toEndOfMonth(targetDate).getDate();\n  const initialDifference = getDateDifference(firstDateOfMatrix, baseDate);\n  const totalDatesOfMatrix = totalDatesCountOfMonth + Math.abs(initialDifference);\n\n  let totalWeeksOfMatrix = DEFAULT_VISIBLE_WEEKS;\n  if (shouldApplyVisibleWeeksCount) {\n    totalWeeksOfMatrix = visibleWeeksCount;\n  } else if (isAlways6Weeks === false) {\n    totalWeeksOfMatrix = Math.ceil(totalDatesOfMatrix / WEEK_DAYS);\n  }\n\n  return range(0, totalWeeksOfMatrix).map((weekIndex) =>\n    range(0, WEEK_DAYS).reduce((weekRow, dayOfWeek) => {\n      const steps = weekIndex * WEEK_DAYS + dayOfWeek;\n      const currentDay = (steps + dayOfFirstDateOfMatrix) % WEEK_DAYS;\n      if (!workweek || (workweek && !isWeekend(currentDay))) {\n        const date = addDate(firstDateOfMatrix, steps);\n        weekRow.push(date);\n      }\n\n      return weekRow;\n    }, [] as TZDate[])\n  );\n}\n\nexport function getWeekDates(\n  renderDate: TZDate,\n  { startDayOfWeek = Day.SUN, workweek }: WeekOptions\n): TZDate[] {\n  const now = toStartOfDay(renderDate);\n  const nowDay = now.getDay();\n  const prevDateCount = nowDay - startDayOfWeek;\n\n  const weekDayList =\n    prevDateCount >= 0\n      ? range(-prevDateCount, WEEK_DAYS - prevDateCount)\n      : range(-WEEK_DAYS - prevDateCount, -prevDateCount);\n\n  return weekDayList.reduce<TZDate[]>((acc, day) => {\n    const date = addDate(now, day);\n\n    if (workweek && isWeekend(date.getDay())) {\n      return acc;\n    }\n    acc.push(date);\n\n    return acc;\n  }, []);\n}\n\n// @TODO: replace `getRowStyleInfo` to this function\nexport function getColumnsData(\n  datesOfWeek: TZDate[], // 5 or 7 dates\n  narrowWeekend = false\n): CommonGridColumn[] {\n  const datesCount = datesOfWeek.length;\n  const shouldApplyNarrowWeekend = datesCount > 5 && narrowWeekend;\n  const defaultWidthByColumns = shouldApplyNarrowWeekend\n    ? 100 / (datesCount - 1)\n    : 100 / datesCount;\n\n  return datesOfWeek\n    .map((date) => {\n      const width =\n        shouldApplyNarrowWeekend && isWeekend(date.getDay())\n          ? defaultWidthByColumns / 2\n          : defaultWidthByColumns;\n\n      return {\n        date,\n        width,\n      };\n    })\n    .reduce<CommonGridColumn[]>((result, currentDateAndWidth, index) => {\n      const prev = result[index - 1];\n\n      result.push({\n        ...currentDateAndWidth,\n        left: index === 0 ? 0 : prev.left + prev.width,\n      });\n\n      return result;\n    }, []);\n}\n\nexport function createTimeGridData(\n  datesOfWeek: TZDate[],\n  options: {\n    hourStart: number;\n    hourEnd: number;\n    narrowWeekend?: boolean;\n  }\n): TimeGridData {\n  const columns = getColumnsData(datesOfWeek, options.narrowWeekend ?? false);\n\n  const steps = (options.hourEnd - options.hourStart) * 2;\n  const baseHeight = 100 / steps;\n  const rows = range(steps).map((step, index) => {\n    const isOdd = index % 2 === 1;\n    const hour = options.hourStart + Math.floor(step / 2);\n    const startTime = `${hour}:${isOdd ? '30' : '00'}`.padStart(5, '0') as FormattedTimeString;\n    const endTime = (isOdd ? `${hour + 1}:00` : `${hour}:30`).padStart(\n      5,\n      '0'\n    ) as FormattedTimeString;\n\n    return {\n      top: baseHeight * index,\n      height: baseHeight,\n      startTime,\n      endTime,\n    };\n  });\n\n  return {\n    columns,\n    rows,\n  };\n}\n\ninterface ContainerPosition {\n  left: number;\n  top: number;\n  clientLeft: number;\n  clientTop: number;\n}\n\nfunction getRelativeMousePosition(\n  { clientX, clientY }: ClientMousePosition,\n  { left, top, clientLeft, clientTop }: ContainerPosition\n) {\n  return [clientX - left - clientLeft, clientY - top - clientTop];\n}\n\nfunction getIndexFromPosition(arrayLength: number, maxRange: number, currentPosition: number) {\n  const calculatedIndex = Math.floor(ratio(maxRange, arrayLength, currentPosition));\n\n  return limit(calculatedIndex, [0], [arrayLength - 1]);\n}\n\nexport function createGridPositionFinder({\n  rowsCount,\n  columnsCount,\n  container,\n  narrowWeekend = false,\n  startDayOfWeek = Day.SUN,\n}: {\n  rowsCount: number;\n  columnsCount: number;\n  container: HTMLElement | null;\n  narrowWeekend?: boolean;\n  startDayOfWeek?: Day;\n}): GridPositionFinder {\n  if (isNil(container)) {\n    return (() => null) as GridPositionFinder;\n  }\n\n  const dayRange = range(startDayOfWeek, startDayOfWeek + columnsCount).map(\n    (day) => day % WEEK_DAYS\n  );\n  const narrowColumnCount = narrowWeekend ? dayRange.filter((day) => isWeekend(day)).length : 0;\n\n  return function gridPositionFinder(mousePosition) {\n    const {\n      left: containerLeft,\n      top: containerTop,\n      width: containerWidth,\n      height: containerHeight,\n    } = container.getBoundingClientRect();\n    const [left, top] = getRelativeMousePosition(mousePosition, {\n      left: containerLeft,\n      top: containerTop,\n      clientLeft: container.clientLeft,\n      clientTop: container.clientTop,\n    });\n\n    if (left < 0 || top < 0 || left > containerWidth || top > containerHeight) {\n      return null;\n    }\n\n    const unitWidth = narrowWeekend\n      ? containerWidth / (columnsCount - narrowColumnCount + 1)\n      : containerWidth / columnsCount;\n    const columnWidthList = dayRange.map((dayOfWeek) =>\n      narrowWeekend && isWeekend(dayOfWeek) ? unitWidth / 2 : unitWidth\n    );\n    const columnLeftList: number[] = [];\n    columnWidthList.forEach((width, index) => {\n      if (index === 0) {\n        columnLeftList.push(0);\n      } else {\n        columnLeftList.push(columnLeftList[index - 1] + columnWidthList[index - 1]);\n      }\n    });\n    const columnIndex = findLastIndex(columnLeftList, (columnLeft) => left >= columnLeft);\n\n    return {\n      columnIndex,\n      rowIndex: getIndexFromPosition(rowsCount, containerHeight, top),\n    };\n  };\n}\n"
  },
  {
    "path": "apps/calendar/src/helpers/gridSelection.ts",
    "content": "import type { useGridSelection } from '@src/hooks/gridSelection/useGridSelection';\nimport type TZDate from '@src/time/date';\nimport { setTimeStrToDate } from '@src/time/datetime';\nimport { isBetween, isBetween as isBetweenValue } from '@src/utils/math';\nimport { isNil, isPresent } from '@src/utils/type';\n\nimport type {\n  GridSelectionData,\n  GridSelectionDataByRow,\n  TimeGridSelectionDataByCol,\n} from '@t/components/gridSelection';\nimport type { GridPosition, TimeGridData } from '@t/grid';\n\ntype RequiredGridSelectionHookParams = Pick<\n  Parameters<typeof useGridSelection>[0],\n  'selectionSorter' | 'dateGetter'\n>;\ntype GridSelectionHelper<\n  SelectionCalculator extends (\n    gridSelection: GridSelectionData | null,\n    ...rest: any[]\n  ) => (TimeGridSelectionDataByCol | null) | (GridSelectionDataByRow | null)\n> = {\n  sortSelection: RequiredGridSelectionHookParams['selectionSorter'];\n  getDateFromCollection: RequiredGridSelectionHookParams['dateGetter'];\n  calculateSelection: SelectionCalculator;\n};\n\nfunction createSortedGridSelection(\n  initPos: GridPosition,\n  currentPos: GridPosition,\n  isReversed: boolean\n) {\n  return {\n    startColumnIndex: isReversed ? currentPos.columnIndex : initPos.columnIndex,\n    startRowIndex: isReversed ? currentPos.rowIndex : initPos.rowIndex,\n    endColumnIndex: isReversed ? initPos.columnIndex : currentPos.columnIndex,\n    endRowIndex: isReversed ? initPos.rowIndex : currentPos.rowIndex,\n  };\n}\n\nfunction calculateTimeGridSelectionByCurrentIndex(\n  timeGridSelection: GridSelectionData | null,\n  columnIndex: number,\n  maxRowIndex: number // maxRowIndex is the last row index of the `timeGridData.row`\n) {\n  if (isNil(timeGridSelection)) {\n    return null;\n  }\n\n  const { startColumnIndex, endColumnIndex, endRowIndex, startRowIndex } = timeGridSelection;\n\n  if (!isBetweenValue(columnIndex, startColumnIndex, endColumnIndex)) {\n    return null;\n  }\n\n  const hasMultipleColumns = startColumnIndex !== endColumnIndex;\n  const isStartingColumn = columnIndex === startColumnIndex;\n  const resultGridSelection: TimeGridSelectionDataByCol = {\n    startRowIndex,\n    endRowIndex,\n    isSelectingMultipleColumns: hasMultipleColumns,\n    isStartingColumn,\n  };\n\n  if (startColumnIndex < columnIndex && columnIndex < endColumnIndex) {\n    resultGridSelection.startRowIndex = 0;\n    resultGridSelection.endRowIndex = maxRowIndex;\n  } else if (startColumnIndex !== endColumnIndex) {\n    if (startColumnIndex === columnIndex) {\n      resultGridSelection.endRowIndex = maxRowIndex;\n    } else if (endColumnIndex === columnIndex) {\n      resultGridSelection.startRowIndex = 0;\n    }\n  }\n\n  return resultGridSelection;\n}\n\nexport const timeGridSelectionHelper: GridSelectionHelper<\n  typeof calculateTimeGridSelectionByCurrentIndex\n> = {\n  sortSelection: (initPos, currentPos) => {\n    const isReversed =\n      initPos.columnIndex > currentPos.columnIndex ||\n      (initPos.columnIndex === currentPos.columnIndex && initPos.rowIndex > currentPos.rowIndex);\n\n    return createSortedGridSelection(initPos, currentPos, isReversed);\n  },\n  getDateFromCollection: (dateCollection, gridSelection) => {\n    const timeGridData = dateCollection as TimeGridData;\n\n    const startDate = setTimeStrToDate(\n      timeGridData.columns[gridSelection.startColumnIndex].date,\n      timeGridData.rows[gridSelection.startRowIndex].startTime\n    );\n    const endDate = setTimeStrToDate(\n      timeGridData.columns[gridSelection.endColumnIndex].date,\n      timeGridData.rows[gridSelection.endRowIndex].endTime\n    );\n\n    return [startDate, endDate];\n  },\n  calculateSelection: calculateTimeGridSelectionByCurrentIndex,\n};\n\nfunction calculateDayGridMonthSelectionByCurrentIndex(\n  gridSelection: GridSelectionData | null,\n  currentIndex: number,\n  weekLength: number\n) {\n  if (!(isPresent(gridSelection) && isPresent(currentIndex) && isPresent(weekLength))) {\n    return null;\n  }\n\n  const { startRowIndex, startColumnIndex, endRowIndex, endColumnIndex } = gridSelection;\n\n  if (\n    !isBetween(\n      currentIndex,\n      Math.min(startRowIndex, endRowIndex),\n      Math.max(startRowIndex, endRowIndex)\n    )\n  ) {\n    return null;\n  }\n\n  let startCellIndex = startColumnIndex;\n  let endCellIndex = endColumnIndex;\n\n  if (startRowIndex < currentIndex) {\n    startCellIndex = 0;\n  }\n\n  if (endRowIndex > currentIndex) {\n    endCellIndex = weekLength - 1;\n  }\n\n  return { startCellIndex, endCellIndex };\n}\n\nexport const dayGridMonthSelectionHelper: GridSelectionHelper<\n  typeof calculateDayGridMonthSelectionByCurrentIndex\n> = {\n  sortSelection: (initPos, currentPos) => {\n    const isReversed =\n      initPos.rowIndex > currentPos.rowIndex ||\n      (initPos.rowIndex === currentPos.rowIndex && initPos.columnIndex > currentPos.columnIndex);\n\n    return createSortedGridSelection(initPos, currentPos, isReversed);\n  },\n  getDateFromCollection: (dateCollection, gridSelection) => {\n    const dateMatrix = dateCollection as TZDate[][];\n\n    return [\n      dateMatrix[gridSelection.startRowIndex][gridSelection.startColumnIndex],\n      dateMatrix[gridSelection.endRowIndex][gridSelection.endColumnIndex],\n    ];\n  },\n  calculateSelection: calculateDayGridMonthSelectionByCurrentIndex,\n};\n\nfunction calculateAlldayGridRowSelectionByCurrentIndex(gridSelection: GridSelectionData | null) {\n  return isPresent(gridSelection)\n    ? {\n        startCellIndex: gridSelection.startColumnIndex,\n        endCellIndex: gridSelection.endColumnIndex,\n      }\n    : null;\n}\n\nexport const alldayGridRowSelectionHelper: GridSelectionHelper<\n  typeof calculateAlldayGridRowSelectionByCurrentIndex\n> = {\n  sortSelection: (initPos, currentPos) => {\n    const isReversed = initPos.columnIndex > currentPos.columnIndex;\n\n    return createSortedGridSelection(initPos, currentPos, isReversed);\n  },\n  getDateFromCollection: (dateCollection, gridSelection) => {\n    const weekDates = dateCollection as TZDate[];\n\n    return [weekDates[gridSelection.startColumnIndex], weekDates[gridSelection.endColumnIndex]];\n  },\n  calculateSelection: calculateAlldayGridRowSelectionByCurrentIndex,\n};\n"
  },
  {
    "path": "apps/calendar/src/helpers/popup.ts",
    "content": "import type { Rect } from '@t/store';\n\nexport function isTopOutOfLayout(top: number, layoutRect: Rect, popupRect: Rect): boolean {\n  return top + popupRect.height > layoutRect.top + layoutRect.height;\n}\n\nexport function isLeftOutOfLayout(left: number, layoutRect: Rect, popupRect: Rect): boolean {\n  return left + popupRect.width > layoutRect.left + layoutRect.width;\n}\n"
  },
  {
    "path": "apps/calendar/src/helpers/view.ts",
    "content": "import { DEFAULT_EVENT_PANEL, DEFAULT_TASK_PANEL } from '@src/constants/view';\n\nimport type { EventView, TaskView, WeekOptions } from '@t/options';\n\nexport function getActivePanels(\n  taskView: Required<WeekOptions>['taskView'],\n  eventView: Required<WeekOptions>['eventView']\n): (TaskView | EventView)[] {\n  const activePanels: (TaskView | EventView)[] = [];\n\n  if (taskView === true) {\n    activePanels.push(...DEFAULT_TASK_PANEL);\n  } else if (Array.isArray(taskView)) {\n    activePanels.push(...taskView);\n  }\n\n  if (eventView === true) {\n    activePanels.push(...DEFAULT_EVENT_PANEL);\n  } else if (Array.isArray(eventView)) {\n    activePanels.push(...eventView);\n  }\n\n  return activePanels;\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/calendar/useCalendarById.ts",
    "content": "import { useCallback } from 'preact/hooks';\n\nimport { useStore } from '@src/contexts/calendarStore';\n\nexport function useCalendarById(calendarId: string | null) {\n  return useStore(\n    useCallback(\n      (state) => state.calendar.calendars.find((cal) => cal.id === calendarId),\n      [calendarId]\n    )\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/calendar/useCalendarColor.ts",
    "content": "import { useMemo } from 'preact/hooks';\n\nimport { useCalendarById } from '@src/hooks/calendar/useCalendarById';\nimport type EventModel from '@src/model/eventModel';\n\nimport type { CalendarColor } from '@t/options';\n\nexport function useCalendarColor(model?: EventModel): CalendarColor {\n  const calendar = useCalendarById(model?.calendarId ?? null);\n\n  return useMemo(\n    () => ({\n      color: calendar?.color,\n      borderColor: calendar?.borderColor,\n      backgroundColor: calendar?.backgroundColor,\n      dragBackgroundColor: calendar?.dragBackgroundColor,\n    }),\n    [calendar]\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/calendar/useCalendarData.ts",
    "content": "import { useMemo } from 'preact/hooks';\n\nimport { useEventsWithTimezone } from '@src/hooks/timezone/useEventsWithTimezone';\nimport type EventModel from '@src/model/eventModel';\nimport type { Filter } from '@src/utils/collection';\nimport Collection from '@src/utils/collection';\n\nimport type { CalendarData } from '@t/events';\n\nexport function useCalendarData(calendar: CalendarData, ...filters: Filter<EventModel>[]) {\n  const filteredEvents = useMemo(\n    () => calendar.events.filter(Collection.and<EventModel>(...filters)),\n    [calendar.events, filters]\n  );\n\n  const filteredEventsWithTimezone = useEventsWithTimezone(filteredEvents);\n\n  return useMemo(\n    () => ({\n      ...calendar,\n      events: filteredEventsWithTimezone,\n    }),\n    [calendar, filteredEventsWithTimezone]\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/common/useClickPrevention.spec.tsx",
    "content": "import { h } from 'preact';\n\nimport { useClickPrevention } from '@src/hooks/common/useClickPrevention';\nimport { fireEvent, render, screen } from '@src/test/utils';\n\ndescribe('useClickPrevention', () => {\n  const onClick = jest.fn();\n  const onDblClick = jest.fn();\n  const delay = 300;\n\n  const Button = () => {\n    const [handleClick, handleDoubleClick] = useClickPrevention({ onClick, onDblClick, delay });\n\n    return <button onClick={handleClick} onDblClick={handleDoubleClick} />;\n  };\n\n  beforeEach(() => {\n    render(<Button />);\n  });\n\n  afterEach(() => {\n    jest.useRealTimers();\n  });\n\n  it('should prevent the click event on double click.', () => {\n    // Given\n    const button = screen.getByRole('button');\n\n    // When\n    fireEvent.dblClick(button);\n\n    // Then\n    expect(onClick).not.toHaveBeenCalled();\n    expect(onDblClick).toHaveBeenCalled();\n  });\n\n  it('should fire the click event when the second click fires later than delay.', () => {\n    // Given\n    jest.useFakeTimers();\n    const button = screen.getByRole('button');\n\n    // When\n    fireEvent.click(button);\n    jest.advanceTimersByTime(delay + 30);\n    fireEvent.click(button);\n    jest.advanceTimersByTime(delay + 50);\n\n    // Then\n    expect(onClick).toHaveBeenCalledTimes(2);\n    expect(onDblClick).not.toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/hooks/common/useClickPrevention.ts",
    "content": "import { useEffect, useRef } from 'preact/hooks';\n\nimport { noop } from '@src/utils/noop';\nimport { requestTimeout } from '@src/utils/requestTimeout';\n\n// Reference: https://medium.com/trabe/preventing-click-events-on-double-click-with-react-the-performant-way-1416ab03b835\nexport function useClickPrevention({\n  onClick,\n  onDblClick,\n  delay = 300,\n}: {\n  onClick: (e: MouseEvent) => void;\n  onDblClick: (e: MouseEvent) => void;\n  delay?: number;\n}) {\n  const cancelCallback = useRef<Function>(noop);\n  const registerCancel = (fn: Function) => {\n    cancelCallback.current = fn;\n  };\n  const cancelScheduledWork = () => {\n    cancelCallback.current();\n  };\n\n  // Cancels the current scheduled work before the \"unmount\"\n  useEffect(() => cancelScheduledWork, []);\n\n  const handleClick = (e: MouseEvent) => {\n    cancelScheduledWork();\n    requestTimeout(onClick.bind(null, e), delay, registerCancel);\n  };\n\n  const handleDblClick = (e: MouseEvent) => {\n    cancelScheduledWork();\n    onDblClick(e);\n  };\n\n  return [handleClick, handleDblClick];\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/common/useDOMNode.ts",
    "content": "import type { RefCallback } from 'preact';\nimport { useCallback, useState } from 'preact/hooks';\n\nexport function useDOMNode<Node extends HTMLElement>(): [Node | null, RefCallback<Node>] {\n  const [node, setNode] = useState<Node | null>(null);\n  const setNodeRef: RefCallback<Node> = useCallback((ref) => {\n    if (ref) {\n      setNode(ref);\n    }\n  }, []);\n\n  return [node, setNodeRef];\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/common/useDrag.spec.tsx",
    "content": "import { h } from 'preact';\n\nimport { MINIMUM_DRAG_MOUSE_DISTANCE } from '@src/constants/mouse';\nimport { initCalendarStore, StoreProvider } from '@src/contexts/calendarStore';\nimport type { DragListeners } from '@src/hooks/common/useDrag';\nimport { useDrag } from '@src/hooks/common/useDrag';\nimport { DraggingState } from '@src/slices/dnd';\nimport { dragAndDrop, fireEvent, render, screen } from '@src/test/utils';\n\nimport type { PropsWithChildren } from '@t/components/common';\nimport type { DraggingTypes } from '@t/drag';\nimport type { CalendarStore, InternalStoreAPI } from '@t/store';\n\ndescribe('drag hook', () => {\n  const TEST_DRAGGING_TYPE = 'drag-test';\n  let store: InternalStoreAPI<CalendarStore>;\n  const listeners: DragListeners = {\n    onInit: jest.fn(),\n    onDragStart: jest.fn(),\n    onDrag: jest.fn(),\n    onMouseUp: jest.fn(),\n    onPressESCKey: jest.fn(),\n  };\n  const wrapper = ({ children }: PropsWithChildren) => {\n    store = initCalendarStore();\n\n    return <StoreProvider store={store}>{children}</StoreProvider>;\n  };\n  const Component = () => {\n    const onMouseDown = useDrag(TEST_DRAGGING_TYPE as DraggingTypes, listeners);\n\n    return <button onMouseDown={onMouseDown} />;\n  };\n  const setup = () => render(<Component />, { wrapper });\n\n  afterEach(() => {\n    jest.clearAllMocks();\n  });\n\n  it('should fire onInit when mouse down', () => {\n    // Given\n    setup();\n    const button = screen.getByRole('button');\n\n    // When\n    fireEvent.mouseDown(button);\n\n    // Then\n    expect(listeners.onInit).toBeCalledWith(\n      expect.objectContaining({\n        button: 0,\n      }),\n      expect.objectContaining({\n        draggingItemType: TEST_DRAGGING_TYPE,\n        initX: 0,\n        initY: 0,\n        x: null,\n        y: null,\n      })\n    );\n  });\n\n  it('should fire onDragStart when move more than equal 3px from the staring point', () => {\n    // Given\n    setup();\n    const button = screen.getByRole('button');\n    const targetCoords = {\n      clientX: MINIMUM_DRAG_MOUSE_DISTANCE + 1,\n      clientY: MINIMUM_DRAG_MOUSE_DISTANCE + 1,\n    };\n\n    // When\n    fireEvent.mouseDown(button);\n    fireEvent.mouseMove(document, targetCoords);\n\n    // Then\n    expect(listeners.onDragStart).toBeCalledWith(\n      expect.objectContaining(targetCoords),\n      expect.objectContaining({\n        draggingItemType: TEST_DRAGGING_TYPE,\n        initX: 0,\n        initY: 0,\n        x: targetCoords.clientX,\n        y: targetCoords.clientY,\n      })\n    );\n  });\n\n  it('should not fire onDragStart when move less than 3px from the staring point', () => {\n    // Given\n    setup();\n    const button = screen.getByRole('button');\n\n    // When\n    fireEvent.mouseDown(button);\n    fireEvent.mouseMove(document, {\n      clientX: MINIMUM_DRAG_MOUSE_DISTANCE - 1,\n      clientY: MINIMUM_DRAG_MOUSE_DISTANCE - 1,\n    });\n\n    // Then\n    expect(listeners.onDragStart).not.toBeCalled();\n  });\n\n  it('fires onDrag when mousemove after onDragStart', () => {\n    // Given\n    setup();\n    const button = screen.getByRole('button');\n    const targetCoords = {\n      clientX: MINIMUM_DRAG_MOUSE_DISTANCE + 1,\n      clientY: MINIMUM_DRAG_MOUSE_DISTANCE + 1,\n    };\n\n    // When\n    dragAndDrop({\n      element: button,\n      initPosition: { clientX: 0, clientY: 0 },\n      targetPosition: targetCoords,\n      hold: true,\n    });\n\n    // Then\n    expect(listeners.onDrag).toBeCalledWith(\n      expect.objectContaining(targetCoords),\n      expect.objectContaining({\n        draggingItemType: TEST_DRAGGING_TYPE,\n        initX: 0,\n        initY: 0,\n        x: targetCoords.clientX,\n        y: targetCoords.clientY,\n      })\n    );\n  });\n\n  it('fires onMouseUp right after dragging is started', () => {\n    // Given\n    setup();\n    const button = screen.getByRole('button');\n\n    // When\n    fireEvent.mouseDown(button);\n    fireEvent.mouseUp(button);\n\n    // Then\n    expect(listeners.onMouseUp).toBeCalled();\n  });\n\n  it('fires onPressESCKey when press ESC key', () => {\n    // Given\n    setup();\n    const button = screen.getByRole('button');\n\n    // When\n    fireEvent.mouseDown(button);\n    fireEvent.keyDown(document, { key: 'Escape' });\n\n    // Then\n    expect(listeners.onPressESCKey).toBeCalled();\n  });\n  describe('draggingState', () => {\n    const getDraggingState = () => store.getState().dnd.draggingState;\n\n    it('should be an idle state when a drag is not started', () => {\n      // Given\n      setup();\n\n      // When\n      // Nothing\n\n      // Then\n      expect(getDraggingState()).toBe(DraggingState.IDLE);\n    });\n\n    it('should be an init state when a drag is started (after mousedown and before mousemove)', () => {\n      // Given\n      setup();\n      const button = screen.getByRole('button');\n\n      // When\n      fireEvent.mouseDown(button);\n\n      // Then\n      expect(getDraggingState()).toBe(DraggingState.INIT);\n    });\n\n    it('should be a dragging state when a drag is working (after mousemove)', () => {\n      // Given\n      setup();\n      const button = screen.getByRole('button');\n\n      // When\n      dragAndDrop({\n        element: button,\n        targetPosition: {\n          clientX: MINIMUM_DRAG_MOUSE_DISTANCE,\n          clientY: MINIMUM_DRAG_MOUSE_DISTANCE,\n        },\n        hold: true,\n      });\n\n      // Then\n      expect(getDraggingState()).toBe(DraggingState.DRAGGING);\n    });\n\n    it('should be an idle state when a drag is finished (after mouseup)', () => {\n      // Given\n      setup();\n      const button = screen.getByRole('button');\n\n      // When\n      dragAndDrop({\n        element: button,\n        targetPosition: {\n          clientX: MINIMUM_DRAG_MOUSE_DISTANCE,\n          clientY: MINIMUM_DRAG_MOUSE_DISTANCE,\n        },\n        hold: false,\n      });\n\n      // Then\n      expect(getDraggingState()).toBe(DraggingState.IDLE);\n    });\n\n    it('should be a canceled state when a drag is canceled by pressing an ESC key', () => {\n      // Given\n      setup();\n      const button = screen.getByRole('button');\n\n      // When\n      dragAndDrop({\n        element: button,\n        targetPosition: {\n          clientX: MINIMUM_DRAG_MOUSE_DISTANCE,\n          clientY: MINIMUM_DRAG_MOUSE_DISTANCE,\n        },\n        hold: true,\n      });\n      fireEvent.keyDown(document, { key: 'Escape' });\n\n      // Then\n      expect(getDraggingState()).toBe(DraggingState.CANCELED);\n    });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/hooks/common/useDrag.ts",
    "content": "import { useCallback, useEffect, useRef, useState } from 'preact/hooks';\n\nimport { KEY } from '@src/constants/keyboard';\nimport { MINIMUM_DRAG_MOUSE_DISTANCE } from '@src/constants/mouse';\nimport { useDispatch, useInternalStore } from '@src/contexts/calendarStore';\nimport { useTransientUpdate } from '@src/hooks/common/useTransientUpdate';\nimport { dndSelector } from '@src/selectors';\nimport type { DndSlice } from '@src/slices/dnd';\nimport { DraggingState } from '@src/slices/dnd';\nimport { isKeyPressed } from '@src/utils/keyboard';\nimport { noop } from '@src/utils/noop';\nimport { isPresent } from '@src/utils/type';\n\nimport type { DraggingTypes } from '@t/drag';\nimport type { KeyboardEventListener, MouseEventListener } from '@t/util';\n\ntype MouseListener = (e: MouseEvent, dndSlice: DndSlice['dnd']) => void;\ntype KeyboardListener = (e: KeyboardEvent, dndSlice: DndSlice['dnd']) => void;\n\nexport interface DragListeners {\n  // when press the mouse button\n  onInit?: MouseListener;\n  // when the mouse moving is recognized as a drag\n  onDragStart?: MouseListener;\n  // while dragging\n  onDrag?: MouseListener;\n  // when the mouse button is released while dragging or just after onInit\n  onMouseUp?: MouseListener;\n  // when press the escape key while dragging\n  onPressESCKey?: KeyboardListener;\n}\n\nfunction isLeftClick(buttonNum: number) {\n  return buttonNum === 0;\n}\n\nfunction isMouseMoved(initX: number, initY: number, x: number, y: number) {\n  return (\n    Math.abs(initX - x) >= MINIMUM_DRAG_MOUSE_DISTANCE ||\n    Math.abs(initY - y) >= MINIMUM_DRAG_MOUSE_DISTANCE\n  );\n}\n\nexport function useDrag(\n  draggingItemType: DraggingTypes,\n  { onInit, onDragStart, onDrag, onMouseUp, onPressESCKey }: DragListeners = {}\n) {\n  const { initDrag, setDragging, cancelDrag, reset } = useDispatch('dnd');\n\n  const store = useInternalStore();\n  const dndSliceRef = useRef(store.getState().dnd);\n  useTransientUpdate(dndSelector, (dndState) => {\n    dndSliceRef.current = dndState;\n  });\n\n  const [isStarted, setStarted] = useState(false);\n\n  const handleMouseMoveRef = useRef<MouseEventListener | null>(null);\n  const handleMouseUpRef = useRef<MouseEventListener | null>(null);\n  const handleKeyDownRef = useRef<KeyboardEventListener | null>(null);\n\n  const handleMouseDown = useCallback<MouseEventListener>(\n    (e) => {\n      if (!isLeftClick(e.button)) {\n        return;\n      }\n\n      if (e.currentTarget) {\n        (e.currentTarget as HTMLElement).ondragstart = function () {\n          return false;\n        };\n      }\n\n      // prevent text selection on dragging\n      e.preventDefault();\n\n      setStarted(true);\n      initDrag({\n        draggingItemType,\n        initX: e.clientX,\n        initY: e.clientY,\n      });\n      onInit?.(e, dndSliceRef.current);\n    },\n    [onInit, draggingItemType, initDrag]\n  );\n\n  const handleMouseMove = useCallback<MouseEventListener>(\n    (e) => {\n      const {\n        initX,\n        initY,\n        draggingState,\n        draggingItemType: currentDraggingItemType,\n      } = dndSliceRef.current;\n\n      if (currentDraggingItemType !== draggingItemType) {\n        setStarted(false);\n        reset();\n\n        return;\n      }\n\n      if (\n        isPresent(initX) &&\n        isPresent(initY) &&\n        !isMouseMoved(initX, initY, e.clientX, e.clientY)\n      ) {\n        return;\n      }\n\n      if (draggingState <= DraggingState.INIT) {\n        setDragging({ x: e.clientX, y: e.clientY });\n        onDragStart?.(e, dndSliceRef.current);\n\n        return;\n      }\n\n      setDragging({ x: e.clientX, y: e.clientY });\n      onDrag?.(e, dndSliceRef.current);\n    },\n    [draggingItemType, onDrag, onDragStart, setDragging, reset]\n  );\n\n  const handleMouseUp = useCallback<MouseEventListener>(\n    (e) => {\n      e.stopPropagation();\n\n      if (isStarted) {\n        onMouseUp?.(e, dndSliceRef.current);\n        setStarted(false);\n        reset();\n      }\n    },\n    [isStarted, onMouseUp, reset]\n  );\n\n  const handleKeyDown = useCallback<KeyboardEventListener>(\n    (e) => {\n      if (isKeyPressed(e, KEY.ESCAPE)) {\n        setStarted(false);\n        cancelDrag();\n        onPressESCKey?.(e, dndSliceRef.current);\n      }\n    },\n    [onPressESCKey, cancelDrag]\n  );\n\n  useEffect(() => {\n    handleMouseMoveRef.current = handleMouseMove;\n    handleMouseUpRef.current = handleMouseUp;\n    handleKeyDownRef.current = handleKeyDown;\n  }, [handleKeyDown, handleMouseMove, handleMouseUp]);\n\n  useEffect(() => {\n    const wrappedHandleMouseMove: MouseEventListener = (e) => handleMouseMoveRef.current?.(e);\n    const wrappedHandleMouseUp: MouseEventListener = (e) => handleMouseUpRef.current?.(e);\n    const wrappedHandleKeyDown: KeyboardEventListener = (e) => handleKeyDownRef.current?.(e);\n\n    if (isStarted) {\n      document.addEventListener('mousemove', wrappedHandleMouseMove);\n      document.addEventListener('mouseup', wrappedHandleMouseUp);\n      document.addEventListener('keydown', wrappedHandleKeyDown);\n\n      return () => {\n        document.removeEventListener('mousemove', wrappedHandleMouseMove);\n        document.removeEventListener('mouseup', wrappedHandleMouseUp);\n        document.removeEventListener('keydown', wrappedHandleKeyDown);\n      };\n    }\n\n    return noop;\n  }, [isStarted, reset]);\n\n  return handleMouseDown;\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/common/useDropdownState.ts",
    "content": "import { useState } from 'preact/hooks';\n\nexport function useDropdownState() {\n  const [isOpened, setOpened] = useState(false);\n  const toggleDropdown = () => setOpened((prev) => !prev);\n\n  return { isOpened, setOpened, toggleDropdown };\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/common/useInterval.spec.ts",
    "content": "import { useInterval } from '@src/hooks/common/useInterval';\nimport { renderHook } from '@src/test/utils';\n\nlet callback: jest.Mock;\nlet setIntervalSpy: jest.SpyInstance;\n\nbeforeEach(() => {\n  callback = jest.fn();\n  setIntervalSpy = jest.spyOn(global, 'setInterval');\n});\n\nbeforeAll(() => {\n  jest.useFakeTimers();\n});\n\nafterEach(() => {\n  callback.mockRestore();\n  setIntervalSpy.mockClear();\n  jest.clearAllTimers();\n});\n\nafterAll(() => {\n  jest.useRealTimers();\n});\n\nit('should init hook', () => {\n  // Given\n  const delay = 1000;\n\n  // When\n  const { result } = renderHook(() => useInterval(callback, delay));\n\n  // Then\n  expect(result.current).toBeUndefined();\n  expect(setInterval).toHaveBeenCalledWith(expect.any(Function), delay);\n});\n\nit('should not init hook if delay is null', () => {\n  // Given\n  const delay = null;\n\n  // When\n  const { result } = renderHook(() => useInterval(callback, delay));\n\n  // Then\n  expect(result.current).toBeUndefined();\n  expect(setInterval).not.toHaveBeenCalled();\n});\n\nit('should repeatedly call provided callback with a delay', () => {\n  // Given\n  const delay = 1000;\n  renderHook(() => useInterval(callback, delay));\n\n  // When\n  jest.advanceTimersByTime(delay);\n\n  // Then\n  expect(callback).toHaveBeenCalledTimes(1);\n\n  // When\n  jest.advanceTimersByTime(delay);\n\n  // Then\n  expect(callback).toHaveBeenCalledTimes(2);\n});\n\nit('should update interval if delay changes', () => {\n  // Given\n  let delay = 1000;\n  const { rerender } = renderHook(() => useInterval(callback, delay));\n\n  jest.advanceTimersByTime(delay);\n  expect(callback).toHaveBeenCalledTimes(1);\n\n  // When\n  delay = 2000;\n  rerender();\n\n  // Then\n  // New interval should be created. so the callback is not called yet with the previous delay.\n  jest.advanceTimersByTime(1000);\n  expect(callback).toHaveBeenCalledTimes(1);\n\n  // fast-forward remaining time for the new delay.\n  jest.advanceTimersByTime(1000);\n  expect(callback).toHaveBeenCalledTimes(2);\n});\n\nit('should stop calling callback if delay is changed to null', () => {\n  // Given\n  let delay: number | null = 1000;\n  const { rerender } = renderHook(() => useInterval(callback, delay));\n\n  jest.advanceTimersByTime(delay);\n  expect(callback).toHaveBeenCalledTimes(1);\n\n  // When\n  delay = null;\n  rerender();\n\n  // Then\n  // Even though the time is advanced, the callback is not called.\n  jest.advanceTimersByTime(1000);\n  expect(callback).toHaveBeenCalledTimes(1);\n});\n\nit('should stop calling provided callback when unmounted', () => {\n  // Given\n  const delay = 1000;\n  const { unmount } = renderHook(() => useInterval(callback, delay));\n\n  jest.advanceTimersByTime(delay);\n  expect(callback).toHaveBeenCalledTimes(1);\n\n  // When\n  unmount();\n  jest.advanceTimersByTime(delay);\n\n  // Then\n  expect(callback).toHaveBeenCalledTimes(1);\n});\n"
  },
  {
    "path": "apps/calendar/src/hooks/common/useInterval.ts",
    "content": "import { useEffect, useRef } from 'preact/hooks';\n\nexport function useInterval(callback: () => void, delay: number | null) {\n  const savedCallback = useRef<() => void>(callback);\n\n  // Remember the latest callback.\n  useEffect(() => {\n    savedCallback.current = callback;\n  }, [callback]);\n\n  // Set up the interval.\n  // eslint-disable-next-line consistent-return\n  useEffect(() => {\n    const tick = () => savedCallback.current();\n    const intervalDelay = delay ?? -1;\n\n    if (intervalDelay > 0) {\n      const id = setInterval(tick, intervalDelay);\n      return () => clearInterval(id);\n    }\n  }, [delay]);\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/common/useIsMounted.spec.ts",
    "content": "import { useIsMounted } from '@src/hooks/common/useIsMounted';\nimport { act, cleanup, renderHook } from '@src/test/utils';\n\nafterEach(cleanup);\n\nit('returns true when the hook is mounted', () => {\n  // When\n  const { result } = renderHook(() => useIsMounted());\n\n  // Then\n  expect(result.current?.()).toBe(true);\n});\n\nit('returns false when the hook is unmounted', () => {\n  // Given\n  const { result, unmount } = renderHook(() => useIsMounted());\n  expect(result.current?.()).toBe(true);\n\n  // When\n  act(() => {\n    unmount();\n  });\n\n  // Then\n  expect(result.current?.()).toBe(false);\n});\n"
  },
  {
    "path": "apps/calendar/src/hooks/common/useIsMounted.ts",
    "content": "import { useCallback, useEffect, useRef } from 'preact/hooks';\n\nexport function useIsMounted(): () => boolean {\n  const isMountedRef = useRef(true);\n\n  useEffect(() => {\n    return () => {\n      isMountedRef.current = false;\n    };\n  }, []);\n\n  return useCallback(() => isMountedRef.current, []);\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/common/useKeydownEvent.ts",
    "content": "import { useEffect, useRef } from 'preact/hooks';\n\nimport type { KEY } from '@src/constants/keyboard';\nimport { isKeyPressed } from '@src/utils/keyboard';\n\nexport function useKeydownEvent(targetKey: KEY, handler: (event: KeyboardEvent) => void): void {\n  const handlerRef = useRef(handler);\n\n  useEffect(() => {\n    handlerRef.current = handler;\n  }, [handler]);\n\n  useEffect(() => {\n    const handleKeydown = (event: KeyboardEvent) => {\n      if (isKeyPressed(event, targetKey)) {\n        handlerRef.current(event);\n      }\n    };\n\n    window.addEventListener('keydown', handleKeydown);\n\n    return () => window.removeEventListener('keydown', handleKeydown);\n  }, [targetKey]);\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/common/useTransientUpdate.ts",
    "content": "import { useEffect, useRef } from 'preact/hooks';\n\nimport { useInternalStore } from '@src/contexts/calendarStore';\n\nimport type { CalendarState } from '@t/store';\n\ntype Slice<S> = S extends (state: CalendarState) => infer T ? T : never;\n\n// Transient Updates for better performance\n// Reference: https://github.com/pmndrs/zustand#transient-updates-for-often-occuring-state-changes\nexport function useTransientUpdate<\n  Selector extends (state: CalendarState) => any,\n  Subscriber extends (slice: Slice<Selector>) => void\n>(selector: Selector, subscriber: Subscriber) {\n  const store = useInternalStore();\n  const selectorRef = useRef(selector);\n  const subscriberRef = useRef(subscriber);\n\n  useEffect(() => {\n    selectorRef.current = selector;\n    subscriberRef.current = subscriber;\n  }, [selector, subscriber]);\n\n  useEffect(\n    () =>\n      store.subscribe(\n        (slice) => subscriberRef.current(slice as any),\n        (state) => selectorRef.current(state)\n      ),\n    [selector, store]\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/common/useWhen.ts",
    "content": "import { useEffect, useRef } from 'preact/hooks';\n\n/**\n * Check the condition and call the callback if the condition is true.\n * callback is always referencing the latest value\n * so that it doesn't have to register all values in the callback as deps to useEffect.\n * But it's not suitable when you need to keep tracking the value related to condition.\n *\n * @example\n * // when the condition is true, the callback is called.\n * useWhen(() => {\n *   if (shouldUpdateEvent) {\n *     // update event\n *   }\n * }, isDraggingEnd)\n *\n * @example\n * // avoid this when you need to keep updating `setGridDiff` by `currentGridPos` and `initGridPosition`.\n * useWhen(() => {\n *   // it will fire once.\n *   setGridDiff({\n *     columnIndex: currentGridPos.columnIndex - initGridPosition.columnIndex,\n *     rowIndex: currentGridPos.rowIndex - initGridPosition.rowIndex,\n *   });\n * }, isPresent(currentGridPos) && isPresent(initGridPosition));\n *\n * // You need to use `useEffect` this time.\n * useEffect(() => {\n *   setGridDiff({\n *     columnIndex: currentGridPos.columnIndex - initGridPosition.columnIndex,\n *     rowIndex: currentGridPos.rowIndex - initGridPosition.rowIndex,\n *   });\n * }, [currentGridPos, initGridPosition]);\n */\nexport function useWhen(callback: () => void, condition: boolean) {\n  const callbackRef = useRef(callback);\n\n  useEffect(() => {\n    callbackRef.current = callback;\n  }, [callback]);\n\n  useEffect(() => {\n    const invoke = () => callbackRef.current();\n\n    if (condition) {\n      invoke();\n    }\n  }, [condition]);\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/dayGridMonth/useDayGridMonthEventMove.ts",
    "content": "import type { ComponentProps } from 'preact';\nimport { useMemo } from 'preact/hooks';\n\nimport type { MovingEventShadow } from '@src/components/dayGridMonth/movingEventShadow';\nimport { useEventBus } from '@src/contexts/eventBus';\nimport { useWhen } from '@src/hooks/common/useWhen';\nimport { useCurrentPointerPositionInGrid } from '@src/hooks/event/useCurrentPointerPositionInGrid';\nimport { useDraggingEvent } from '@src/hooks/event/useDraggingEvent';\nimport TZDate from '@src/time/date';\nimport { getDateDifference, MS_PER_DAY } from '@src/time/datetime';\nimport { isPresent } from '@src/utils/type';\n\nexport function useDayGridMonthEventMove({\n  dateMatrix,\n  rowInfo,\n  gridPositionFinder,\n  rowIndex,\n}: ComponentProps<typeof MovingEventShadow>) {\n  const eventBus = useEventBus();\n  const {\n    isDraggingEnd,\n    isDraggingCanceled,\n    draggingEvent: movingEvent,\n    clearDraggingEvent,\n  } = useDraggingEvent('dayGrid', 'move');\n\n  const [currentGridPos, clearCurrentGridPos] = useCurrentPointerPositionInGrid(gridPositionFinder);\n\n  const movingEventUIModel = useMemo(() => {\n    let shadowEventUIModel = null;\n\n    if (movingEvent && currentGridPos?.rowIndex === rowIndex) {\n      shadowEventUIModel = movingEvent;\n      shadowEventUIModel.left = rowInfo[currentGridPos?.columnIndex ?? 0].left;\n      shadowEventUIModel.width = rowInfo[currentGridPos?.columnIndex ?? 0].width;\n    }\n\n    return shadowEventUIModel;\n  }, [movingEvent, currentGridPos?.rowIndex, currentGridPos?.columnIndex, rowIndex, rowInfo]);\n\n  useWhen(() => {\n    const shouldUpdate =\n      !isDraggingCanceled && isPresent(movingEventUIModel) && isPresent(currentGridPos);\n    if (shouldUpdate) {\n      const preStartDate = movingEventUIModel.model.getStarts();\n      const eventDuration = movingEventUIModel.duration();\n      const currentDate = dateMatrix[currentGridPos.rowIndex][currentGridPos.columnIndex];\n\n      const timeOffsetPerDay = getDateDifference(currentDate, preStartDate) * MS_PER_DAY;\n\n      const newStartDate = new TZDate(preStartDate.getTime() + timeOffsetPerDay);\n      const newEndDate = new TZDate(newStartDate.getTime() + eventDuration);\n\n      eventBus.fire('beforeUpdateEvent', {\n        event: movingEventUIModel.model.toEventObject(),\n        changes: {\n          start: newStartDate,\n          end: newEndDate,\n        },\n      });\n    }\n\n    clearDraggingEvent();\n    clearCurrentGridPos();\n  }, isDraggingEnd);\n\n  return movingEventUIModel;\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/dayGridMonth/useDayGridMonthEventResize.ts",
    "content": "import { useCallback, useEffect, useMemo, useState } from 'preact/hooks';\n\nimport { useEventBus } from '@src/contexts/eventBus';\nimport type { getRenderedEventUIModels } from '@src/helpers/grid';\nimport { getGridDateIndex } from '@src/helpers/grid';\nimport { useWhen } from '@src/hooks/common/useWhen';\nimport { useCurrentPointerPositionInGrid } from '@src/hooks/event/useCurrentPointerPositionInGrid';\nimport { useDraggingEvent } from '@src/hooks/event/useDraggingEvent';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport type TZDate from '@src/time/date';\nimport { findLastIndex } from '@src/utils/array';\nimport { isNil, isPresent } from '@src/utils/type';\n\nimport type { GridPositionFinder } from '@t/grid';\n\nfunction getRowPosOfUIModel(uiModel: EventUIModel, dateRow: TZDate[]) {\n  const startColumnIndex = Math.max(getGridDateIndex(uiModel.getStarts(), dateRow), 0);\n  const endColumnIndex = getGridDateIndex(uiModel.getEnds(), dateRow);\n\n  return {\n    startColumnIndex,\n    endColumnIndex,\n  };\n}\n\ninterface EventResizeHookParams {\n  dateMatrix: TZDate[][];\n  renderedUIModels: ReturnType<typeof getRenderedEventUIModels>[];\n  cellWidthMap: string[][];\n  gridPositionFinder: GridPositionFinder;\n  rowIndex: number;\n}\n\ntype FilteredUIModelRow = [] | [EventUIModel];\n\nexport function useDayGridMonthEventResize({\n  dateMatrix,\n  gridPositionFinder,\n  renderedUIModels,\n  cellWidthMap,\n  rowIndex,\n}: EventResizeHookParams) {\n  const eventBus = useEventBus();\n  const {\n    isDraggingEnd,\n    isDraggingCanceled,\n    draggingEvent: resizingStartUIModel,\n    clearDraggingEvent,\n  } = useDraggingEvent('dayGrid', 'resize');\n  const [currentGridPos, clearCurrentGridPos] = useCurrentPointerPositionInGrid(gridPositionFinder);\n  const [guideProps, setGuideProps] = useState<[EventUIModel, string] | null>(null); // Shadow -> Guide\n\n  const clearStates = useCallback(() => {\n    setGuideProps(null);\n    clearCurrentGridPos();\n    clearDraggingEvent();\n  }, [clearCurrentGridPos, clearDraggingEvent]);\n\n  const baseResizingInfo = useMemo(() => {\n    if (isNil(resizingStartUIModel)) {\n      return null;\n    }\n    /**\n     * Filter UIModels that are made from the target event.\n     */\n    const resizeTargetUIModelRows = renderedUIModels.map(\n      ({ uiModels }) =>\n        uiModels.filter(\n          (uiModel) => uiModel.cid() === resizingStartUIModel.cid()\n        ) as FilteredUIModelRow\n    );\n\n    const eventStartDateRowIndex = resizeTargetUIModelRows.findIndex((row) => row.length > 0);\n    const eventEndDateRowIndex = findLastIndex(resizeTargetUIModelRows, (row) => row.length > 0);\n    const eventStartUIModelPos = getRowPosOfUIModel(\n      resizeTargetUIModelRows[eventStartDateRowIndex][0] as EventUIModel,\n      dateMatrix[eventStartDateRowIndex]\n    );\n    const eventEndUIModelPos = getRowPosOfUIModel(\n      resizeTargetUIModelRows[eventEndDateRowIndex][0] as EventUIModel,\n      dateMatrix[eventEndDateRowIndex]\n    );\n\n    return {\n      eventStartDateColumnIndex: eventStartUIModelPos.startColumnIndex,\n      eventStartDateRowIndex,\n      eventEndDateColumnIndex: eventEndUIModelPos.endColumnIndex,\n      eventEndDateRowIndex,\n      resizeTargetUIModelRows,\n    };\n  }, [dateMatrix, renderedUIModels, resizingStartUIModel]);\n\n  const canCalculateProps =\n    isPresent(baseResizingInfo) && isPresent(resizingStartUIModel) && isPresent(currentGridPos);\n\n  // Calculate the first row of the dragging event\n  useEffect(() => {\n    if (canCalculateProps && rowIndex === baseResizingInfo.eventStartDateRowIndex) {\n      const { eventStartDateRowIndex, eventStartDateColumnIndex } = baseResizingInfo;\n      const clonedUIModel = (\n        baseResizingInfo.resizeTargetUIModelRows[eventStartDateRowIndex][0] as EventUIModel\n      ).clone();\n\n      let height: string;\n      if (eventStartDateRowIndex === currentGridPos.rowIndex) {\n        height =\n          cellWidthMap[eventStartDateColumnIndex][\n            Math.max(eventStartDateColumnIndex, currentGridPos.columnIndex)\n          ];\n      } else if (eventStartDateRowIndex > currentGridPos.rowIndex) {\n        height = cellWidthMap[eventStartDateColumnIndex][eventStartDateColumnIndex];\n      } else {\n        height = cellWidthMap[eventStartDateColumnIndex][dateMatrix[rowIndex].length - 1];\n        clonedUIModel.setUIProps({ exceedRight: true });\n      }\n\n      setGuideProps([clonedUIModel, height]);\n    }\n  }, [baseResizingInfo, canCalculateProps, cellWidthMap, currentGridPos, dateMatrix, rowIndex]);\n\n  // Calculate middle rows of the dragging event\n  useEffect(() => {\n    if (\n      canCalculateProps &&\n      baseResizingInfo.eventStartDateRowIndex < rowIndex &&\n      rowIndex < currentGridPos.rowIndex\n    ) {\n      const clonedUIModel = resizingStartUIModel.clone();\n      clonedUIModel.setUIProps({ left: 0, exceedLeft: true, exceedRight: true });\n      setGuideProps([clonedUIModel, '100%']);\n    }\n  }, [baseResizingInfo, canCalculateProps, currentGridPos, resizingStartUIModel, rowIndex]);\n\n  // Calculate the last row of the dragging event\n  useEffect(() => {\n    if (\n      canCalculateProps &&\n      baseResizingInfo.eventStartDateRowIndex < currentGridPos.rowIndex &&\n      rowIndex === currentGridPos.rowIndex\n    ) {\n      const clonedUIModel = resizingStartUIModel.clone();\n      clonedUIModel.setUIProps({ left: 0, exceedLeft: true });\n      setGuideProps([clonedUIModel, cellWidthMap[0][currentGridPos.columnIndex]]);\n    }\n  }, [\n    baseResizingInfo,\n    canCalculateProps,\n    cellWidthMap,\n    currentGridPos,\n    resizingStartUIModel,\n    rowIndex,\n  ]);\n\n  // Reset props on out of bound\n  useEffect(() => {\n    if (\n      canCalculateProps &&\n      rowIndex > baseResizingInfo.eventStartDateRowIndex &&\n      rowIndex > currentGridPos.rowIndex\n    ) {\n      setGuideProps(null);\n    }\n  }, [canCalculateProps, currentGridPos, baseResizingInfo, rowIndex]);\n\n  useWhen(() => {\n    if (canCalculateProps) {\n      /**\n       * Is current grid position is the same or later comparing to the position of the start date?\n       */\n      const { eventStartDateColumnIndex, eventStartDateRowIndex } = baseResizingInfo;\n      const shouldUpdate =\n        !isDraggingCanceled &&\n        ((currentGridPos.rowIndex === eventStartDateRowIndex &&\n          currentGridPos.columnIndex >= eventStartDateColumnIndex) ||\n          currentGridPos.rowIndex > eventStartDateRowIndex);\n\n      if (shouldUpdate) {\n        const targetEndDate = dateMatrix[currentGridPos.rowIndex][currentGridPos.columnIndex];\n\n        eventBus.fire('beforeUpdateEvent', {\n          event: resizingStartUIModel.model.toEventObject(),\n          changes: {\n            end: targetEndDate,\n          },\n        });\n      }\n    }\n\n    clearStates();\n  }, isDraggingEnd);\n\n  return guideProps;\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/dayGridWeek/useAlldayGridRowEventMove.ts",
    "content": "import { useEffect, useMemo, useRef } from 'preact/hooks';\n\nimport { useEventBus } from '@src/contexts/eventBus';\nimport { useWhen } from '@src/hooks/common/useWhen';\nimport { useCurrentPointerPositionInGrid } from '@src/hooks/event/useCurrentPointerPositionInGrid';\nimport { useDraggingEvent } from '@src/hooks/event/useDraggingEvent';\nimport TZDate from '@src/time/date';\nimport { isNil, isPresent } from '@src/utils/type';\n\nimport type { GridPositionFinder } from '@t/grid';\nimport type { CellStyle } from '@t/time/datetime';\n\ninterface Params {\n  rowStyleInfo: CellStyle[];\n  gridPositionFinder: GridPositionFinder;\n}\n\nexport function useAlldayGridRowEventMove({ rowStyleInfo, gridPositionFinder }: Params) {\n  const eventBus = useEventBus();\n  const {\n    isDraggingEnd,\n    isDraggingCanceled,\n    draggingEvent: movingEvent,\n    clearDraggingEvent,\n  } = useDraggingEvent('dayGrid', 'move');\n  const startGridXRef = useRef<number | null>(null);\n\n  const [currentGridPos, clearCurrentGridPos] = useCurrentPointerPositionInGrid(gridPositionFinder);\n  const { columnIndex } = currentGridPos ?? {};\n\n  const targetEventStartGridX = useMemo(\n    () =>\n      isNil(movingEvent) ? null : rowStyleInfo.findIndex(({ left }) => left === movingEvent.left),\n    [rowStyleInfo, movingEvent]\n  );\n\n  const currentMovingLeft = useMemo(() => {\n    if (isNil(columnIndex) || isNil(startGridXRef.current) || isNil(targetEventStartGridX)) {\n      return null;\n    }\n\n    const newColumnIndex = targetEventStartGridX + columnIndex - startGridXRef.current;\n\n    return newColumnIndex < 0\n      ? -rowStyleInfo[-newColumnIndex].left\n      : rowStyleInfo[newColumnIndex].left;\n  }, [columnIndex, rowStyleInfo, targetEventStartGridX]);\n\n  useEffect(() => {\n    if (isNil(startGridXRef.current) && isPresent(columnIndex)) {\n      startGridXRef.current = columnIndex;\n    }\n  }, [columnIndex]);\n\n  useWhen(() => {\n    const shouldUpdate =\n      !isDraggingCanceled &&\n      isPresent(movingEvent) &&\n      isPresent(columnIndex) &&\n      isPresent(currentMovingLeft) &&\n      columnIndex !== startGridXRef.current;\n\n    if (shouldUpdate && isPresent(startGridXRef.current)) {\n      const dateOffset = columnIndex - startGridXRef.current;\n      const newStartDate = new TZDate(movingEvent.model.getStarts());\n      const newEndDate = new TZDate(movingEvent.model.getEnds());\n      newStartDate.addDate(dateOffset);\n      newEndDate.addDate(dateOffset);\n\n      eventBus.fire('beforeUpdateEvent', {\n        event: movingEvent.model.toEventObject(),\n        changes: {\n          start: newStartDate,\n          end: newEndDate,\n        },\n      });\n    }\n\n    clearDraggingEvent();\n    clearCurrentGridPos();\n    startGridXRef.current = null;\n  }, isDraggingEnd);\n\n  return useMemo(\n    () => ({\n      movingEvent,\n      movingLeft: currentMovingLeft,\n    }),\n    [currentMovingLeft, movingEvent]\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/dayGridWeek/useAlldayGridRowEventResize.ts",
    "content": "import { useMemo } from 'preact/hooks';\n\nimport { useEventBus } from '@src/contexts/eventBus';\nimport { getGridDateIndex } from '@src/helpers/grid';\nimport { useWhen } from '@src/hooks/common/useWhen';\nimport { useCurrentPointerPositionInGrid } from '@src/hooks/event/useCurrentPointerPositionInGrid';\nimport { useDraggingEvent } from '@src/hooks/event/useDraggingEvent';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport type TZDate from '@src/time/date';\nimport { isPresent } from '@src/utils/type';\n\nimport type { GridPositionFinder } from '@t/grid';\n\nfunction getEventColIndex(uiModel: EventUIModel, row: TZDate[]) {\n  const start = getGridDateIndex(uiModel.getStarts(), row);\n  const end = getGridDateIndex(uiModel.getEnds(), row);\n\n  return { start, end };\n}\n\ninterface Params {\n  weekDates: TZDate[];\n  gridColWidthMap: string[][];\n  gridPositionFinder: GridPositionFinder;\n}\n\nexport function useAlldayGridRowEventResize({\n  weekDates,\n  gridColWidthMap,\n  gridPositionFinder,\n}: Params) {\n  const eventBus = useEventBus();\n  const {\n    isDraggingEnd,\n    isDraggingCanceled,\n    draggingEvent: resizingEvent,\n    clearDraggingEvent,\n  } = useDraggingEvent('dayGrid', 'resize');\n\n  const [currentGridPos, clearCurrentGridPos] = useCurrentPointerPositionInGrid(gridPositionFinder);\n  const { columnIndex } = currentGridPos ?? {};\n\n  const targetEventGridIndices = useMemo(() => {\n    if (resizingEvent) {\n      return getEventColIndex(resizingEvent, weekDates);\n    }\n\n    return { start: -1, end: -1 };\n  }, [weekDates, resizingEvent]);\n\n  const resizingWidth = useMemo(() => {\n    if (targetEventGridIndices.start > -1 && isPresent(columnIndex)) {\n      return gridColWidthMap[targetEventGridIndices.start][columnIndex];\n    }\n\n    return null;\n  }, [columnIndex, gridColWidthMap, targetEventGridIndices.start]);\n\n  useWhen(() => {\n    const shouldUpdateEvent =\n      !isDraggingCanceled &&\n      isPresent(resizingEvent) &&\n      isPresent(columnIndex) &&\n      targetEventGridIndices.start <= columnIndex &&\n      targetEventGridIndices.end !== columnIndex;\n\n    if (shouldUpdateEvent) {\n      const targetDate = weekDates[columnIndex];\n\n      eventBus.fire('beforeUpdateEvent', {\n        event: resizingEvent.model.toEventObject(),\n        changes: { end: targetDate },\n      });\n    }\n\n    clearCurrentGridPos();\n    clearDraggingEvent();\n  }, isDraggingEnd);\n\n  return useMemo(\n    () => ({\n      resizingEvent,\n      resizingWidth,\n    }),\n    [resizingWidth, resizingEvent]\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/dayGridWeek/useGridRowHeightController.ts",
    "content": "import { useCallback, useState } from 'preact/hooks';\n\nimport { DEFAULT_PANEL_HEIGHT } from '@src/constants/style';\nimport { useDispatch } from '@src/contexts/calendarStore';\nimport { EVENT_HEIGHT } from '@src/helpers/grid';\nimport type { WeekGridRows } from '@src/slices/layout';\n\nimport type { AlldayEventCategory } from '@t/panel';\n\nexport function useGridRowHeightController(maxTop: number, category: AlldayEventCategory) {\n  const [clickedIndex, setClickedIndex] = useState(0);\n  const [isClickedCount, setClickedCount] = useState(false);\n  const { updateDayGridRowHeight } = useDispatch('weekViewLayout');\n\n  const onClickExceedCount = useCallback(\n    (index: number) => {\n      setClickedCount(true);\n      setClickedIndex(index);\n      updateDayGridRowHeight({\n        rowName: category as WeekGridRows,\n        height: (maxTop + 1) * EVENT_HEIGHT,\n      });\n    },\n    [category, maxTop, updateDayGridRowHeight]\n  );\n\n  const onClickCollapseButton = useCallback(() => {\n    setClickedCount(false);\n    updateDayGridRowHeight({\n      rowName: category as WeekGridRows,\n      height: DEFAULT_PANEL_HEIGHT,\n    });\n  }, [category, updateDayGridRowHeight]);\n\n  return {\n    clickedIndex,\n    isClickedCount,\n    onClickExceedCount,\n    onClickCollapseButton,\n  };\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/event/useCurrentPointerPositionInGrid.ts",
    "content": "import { useCallback, useState } from 'preact/hooks';\n\nimport { useTransientUpdate } from '@src/hooks/common/useTransientUpdate';\nimport { dndSelector } from '@src/selectors';\nimport { isPresent } from '@src/utils/type';\n\nimport type { GridPosition, GridPositionFinder } from '@t/grid';\n\nexport function useCurrentPointerPositionInGrid(\n  gridPositionFinder: GridPositionFinder\n): [GridPosition | null, () => void] {\n  const [currentGridPos, setCurrentGridPos] = useState<GridPosition | null>(null);\n\n  useTransientUpdate(dndSelector, (dndState) => {\n    if (isPresent(dndState.x) && isPresent(dndState.y)) {\n      const gridPosition = gridPositionFinder({\n        clientX: dndState.x,\n        clientY: dndState.y,\n      });\n      if (gridPosition) {\n        setCurrentGridPos(gridPosition);\n      }\n    }\n  });\n\n  const clearCurrentGridPos = useCallback(() => setCurrentGridPos(null), []);\n\n  return [currentGridPos, clearCurrentGridPos];\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/event/useDraggingEvent.ts",
    "content": "import { useState } from 'preact/hooks';\n\nimport { useTransientUpdate } from '@src/hooks/common/useTransientUpdate';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport { dndSelector } from '@src/selectors';\nimport { DraggingState } from '@src/slices/dnd';\nimport { last } from '@src/utils/array';\nimport { isNil, isPresent } from '@src/utils/type';\n\nimport type { EventDragging, EventDraggingArea, EventDraggingBehavior } from '@t/drag';\n\nconst getTargetEventId = (\n  itemType: string | null,\n  area: EventDraggingArea,\n  behavior: EventDraggingBehavior\n) => {\n  function isEventDraggingType(_itemType: string): _itemType is EventDragging {\n    return new RegExp(`^event/${area}/${behavior}/\\\\d+$`).test(_itemType);\n  }\n\n  if (isNil(itemType)) {\n    return null;\n  }\n\n  return isEventDraggingType(itemType) ? last(itemType.split('/')) : null;\n};\n\nexport function useDraggingEvent(area: EventDraggingArea, behavior: EventDraggingBehavior) {\n  const [isDraggingEnd, setIsDraggingEnd] = useState(false);\n  const [isDraggingCanceled, setIsDraggingCanceled] = useState(false);\n  const [draggingEvent, setDraggingEvent] = useState<EventUIModel | null>(null);\n\n  useTransientUpdate(dndSelector, ({ draggingItemType, draggingEventUIModel, draggingState }) => {\n    const targetEventId = getTargetEventId(draggingItemType, area, behavior);\n    const hasMatchingTargetEvent = Number(targetEventId) === draggingEventUIModel?.cid();\n    const isIdle = draggingState === DraggingState.IDLE;\n    const isCanceled = draggingState === DraggingState.CANCELED;\n\n    if (isNil(draggingEvent) && hasMatchingTargetEvent) {\n      setDraggingEvent(draggingEventUIModel);\n    }\n\n    if (isPresent(draggingEvent) && (isIdle || isCanceled)) {\n      setIsDraggingEnd(true);\n      setIsDraggingCanceled(isCanceled);\n    }\n  });\n\n  const clearDraggingEvent = () => {\n    setDraggingEvent(null);\n    setIsDraggingEnd(false);\n    setIsDraggingCanceled(false);\n  };\n\n  return {\n    isDraggingEnd,\n    isDraggingCanceled,\n    draggingEvent,\n    clearDraggingEvent,\n  };\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/gridSelection/useGridSelection.spec.tsx",
    "content": "import { h } from 'preact';\n\nimport { initCalendarStore, StoreProvider } from '@src/contexts/calendarStore';\nimport { EventBusProvider } from '@src/contexts/eventBus';\nimport { createGridPositionFinder, createTimeGridData, getWeekDates } from '@src/helpers/grid';\nimport { timeGridSelectionHelper } from '@src/helpers/gridSelection';\nimport { useGridSelection } from '@src/hooks/gridSelection/useGridSelection';\nimport type { GridSelectionType } from '@src/slices/gridSelection';\nimport { dragAndDrop, renderHook, screen } from '@src/test/utils';\nimport TZDate from '@src/time/date';\nimport { EventBusImpl } from '@src/utils/eventBus';\nimport { noop } from '@src/utils/noop';\n\nimport type { PropsWithChildren } from '@t/components/common';\nimport type { GridSelectionData } from '@t/components/gridSelection';\nimport type { ExternalEventTypes } from '@t/eventBus';\nimport type { GridPosition } from '@t/grid';\nimport type { GridSelectionOptions } from '@t/options';\nimport type { CalendarStore, InternalStoreAPI } from '@t/store';\n\nconst CONTAINER_WIDTH = 70;\nconst CONTAINER_HEIGHT = 480;\n\ndescribe('useGridSelection', () => {\n  let store: InternalStoreAPI<CalendarStore>;\n  const eventBus = new EventBusImpl<ExternalEventTypes>();\n\n  const wrapper = ({ children }: PropsWithChildren) => (\n    <EventBusProvider value={eventBus}>\n      <StoreProvider store={store}>{children}</StoreProvider>\n    </EventBusProvider>\n  );\n\n  function setup<DateCollection>({\n    type = 'timeGrid',\n    gridSelection = { enableDblClick: true, enableClick: true },\n    useFormPopup = false,\n    selectionSorter = jest.fn((init, current) => ({\n      startColumnIndex: current.columnIndex,\n      startRowIndex: current.rowIndex,\n      endColumnIndex: current.columnIndex,\n      endRowIndex: current.rowIndex,\n    })),\n    dateGetter = jest.fn(() => [new TZDate(), new TZDate()]),\n    dateCollection = {} as DateCollection,\n  }: {\n    type?: GridSelectionType;\n    gridSelection?: GridSelectionOptions;\n    useFormPopup?: boolean;\n    dateCollection?: DateCollection;\n    selectionSorter?: (\n      initPosition: GridPosition,\n      currentPosition: GridPosition\n    ) => GridSelectionData;\n    dateGetter?: (\n      dateCollection: DateCollection,\n      gridSelection: GridSelectionData\n    ) => [TZDate, TZDate];\n  } = {}) {\n    const container = document.createElement('div');\n    container.setAttribute('data-testid', 'container');\n    document.body.appendChild(container);\n    jest.spyOn(container, 'getBoundingClientRect').mockReturnValue({\n      x: 0,\n      y: 0,\n      top: 0,\n      bottom: 0,\n      left: 0,\n      right: 0,\n      width: CONTAINER_WIDTH,\n      height: CONTAINER_HEIGHT,\n      toJSON: noop,\n    });\n\n    store.getState().dispatch.options.setOptions({\n      useFormPopup,\n      gridSelection,\n    });\n\n    const gridPositionFinder = createGridPositionFinder({\n      rowsCount: 48,\n      columnsCount: 7,\n      container,\n    });\n\n    const { result } = renderHook(\n      () =>\n        useGridSelection({\n          type,\n          selectionSorter,\n          dateGetter,\n          dateCollection,\n          gridPositionFinder,\n        }),\n      {\n        wrapper,\n      }\n    );\n\n    if (result.current) {\n      container.addEventListener('mousedown', result.current);\n    }\n\n    return result;\n  }\n  function getGridSelectionState() {\n    return store.getState().gridSelection;\n  }\n\n  beforeEach(() => {\n    store = initCalendarStore();\n    store.getState().dispatch.popup.showFormPopup = jest.fn();\n  });\n\n  afterEach(() => {\n    document.body.innerHTML = '';\n    jest.clearAllMocks();\n    jest.useRealTimers();\n  });\n\n  it('should return null if mousedown not fired', () => {\n    // Given\n    setup();\n\n    // When\n    // Do nothing\n\n    // Then\n    expect(getGridSelectionState().timeGrid).toBeNull();\n  });\n\n  describe('when drag start', () => {\n    const cases = [\n      {\n        initX: 0,\n        initY: 0,\n        expected: {\n          endColumnIndex: 0,\n          endRowIndex: 0,\n          startColumnIndex: 0,\n          startRowIndex: 0,\n        },\n      },\n      {\n        initX: 0,\n        initY: CONTAINER_HEIGHT,\n        expected: {\n          endColumnIndex: 0,\n          endRowIndex: 47,\n          startColumnIndex: 0,\n          startRowIndex: 47,\n        },\n      },\n      {\n        initX: CONTAINER_WIDTH,\n        initY: 0,\n        expected: {\n          endColumnIndex: 6,\n          endRowIndex: 0,\n          startColumnIndex: 6,\n          startRowIndex: 0,\n        },\n      },\n      {\n        initX: CONTAINER_WIDTH,\n        initY: CONTAINER_HEIGHT,\n        expected: {\n          endColumnIndex: 6,\n          endRowIndex: 47,\n          startColumnIndex: 6,\n          startRowIndex: 47,\n        },\n      },\n      {\n        initX: 15,\n        initY: 35,\n        expected: {\n          endColumnIndex: 1,\n          endRowIndex: 3,\n          startColumnIndex: 1,\n          startRowIndex: 3,\n        },\n      },\n      {\n        initX: 35,\n        initY: 165,\n        expected: {\n          endColumnIndex: 3,\n          endRowIndex: 16,\n          startColumnIndex: 3,\n          startRowIndex: 16,\n        },\n      },\n    ];\n\n    cases.forEach(({ initX, initY, expected }) => {\n      it(`should return grid selection data when just mousedown and mousemove at (${initX}, ${initY})`, () => {\n        // Given\n        setup();\n        const container = screen.getByTestId('container');\n\n        // When\n        dragAndDrop({\n          element: container,\n          initPosition: { clientX: initX, clientY: initY },\n          targetPosition: {\n            clientX: initX < CONTAINER_WIDTH ? initX + 3 : initX - 3,\n            clientY: initY < CONTAINER_HEIGHT ? initY + 3 : initY - 3,\n          },\n          hold: true,\n        });\n\n        // Then\n        expect(getGridSelectionState().timeGrid).toEqual(expected);\n      });\n    });\n  });\n\n  describe('when dragging', () => {\n    // NOTE: center of container, index is (3, 24)\n    const initX = 35;\n    const initY = 240;\n    /*\n       index of directions\n       0  1  2\n       7     3\n       6  5  4\n    */\n    const cases = [\n      {\n        x: 0,\n        y: 0,\n        expected: {\n          startColumnIndex: 0,\n          startRowIndex: 0,\n          endColumnIndex: 3,\n          endRowIndex: 24,\n        },\n      },\n      {\n        x: initX,\n        y: 0,\n        expected: {\n          startColumnIndex: 3,\n          startRowIndex: 0,\n          endColumnIndex: 3,\n          endRowIndex: 24,\n        },\n      },\n      {\n        x: CONTAINER_WIDTH,\n        y: 0,\n        expected: {\n          startColumnIndex: 3,\n          startRowIndex: 24,\n          endColumnIndex: 6,\n          endRowIndex: 0,\n        },\n      },\n      {\n        x: CONTAINER_WIDTH,\n        y: initY,\n        expected: {\n          startColumnIndex: 3,\n          startRowIndex: 24,\n          endColumnIndex: 6,\n          endRowIndex: 24,\n        },\n      },\n      {\n        x: CONTAINER_WIDTH,\n        y: CONTAINER_HEIGHT,\n        expected: {\n          startColumnIndex: 3,\n          startRowIndex: 24,\n          endColumnIndex: 6,\n          endRowIndex: 47,\n        },\n      },\n      {\n        x: initX,\n        y: CONTAINER_HEIGHT,\n        expected: {\n          startColumnIndex: 3,\n          startRowIndex: 24,\n          endColumnIndex: 3,\n          endRowIndex: 47,\n        },\n      },\n      {\n        x: 0,\n        y: CONTAINER_HEIGHT,\n        expected: {\n          startColumnIndex: 0,\n          startRowIndex: 47,\n          endColumnIndex: 3,\n          endRowIndex: 24,\n        },\n      },\n      {\n        x: 0,\n        y: initY,\n        expected: {\n          startColumnIndex: 0,\n          startRowIndex: 24,\n          endColumnIndex: 3,\n          endRowIndex: 24,\n        },\n      },\n    ];\n\n    cases.forEach(({ x, y, expected }) => {\n      it(`should return grid selection data if drag event is fired at center to (${x}, ${y})`, () => {\n        // Given\n        setup({\n          selectionSorter: timeGridSelectionHelper.sortSelection,\n        });\n        const container = screen.getByTestId('container');\n\n        // When\n        dragAndDrop({\n          element: container,\n          initPosition: { clientX: initX, clientY: initY },\n          targetPosition: { clientX: x, clientY: y },\n        });\n\n        // Then\n        expect(getGridSelectionState().timeGrid).toEqual(expected);\n      });\n    });\n  });\n\n  describe('Accumulated gridSelection', () => {\n    const initPosition = { clientX: 0, clientY: 0 };\n    const targetPosition = { clientX: 50, clientY: 50 };\n\n    it('should not add grid selection in dayGridMonth when useFormPopup is true', () => {\n      // Given\n      setup({\n        type: 'dayGridMonth',\n        useFormPopup: true,\n      });\n      const container = screen.getByTestId('container');\n\n      // When\n      dragAndDrop({ element: container, initPosition, targetPosition });\n\n      // Then\n      expect(store.getState().gridSelection.accumulated.dayGridMonth).toEqual([]);\n    });\n\n    it('should add grid selection in dayGridMonth when useFormPopup is false', () => {\n      // Given\n      setup({\n        type: 'dayGridMonth',\n      });\n      const container = screen.getByTestId('container');\n\n      // When\n      dragAndDrop({ element: container, initPosition, targetPosition });\n      dragAndDrop({ element: container, initPosition, targetPosition });\n\n      // Then\n      const accumulatedGridSelection = store.getState().gridSelection.accumulated.dayGridMonth;\n      expect(accumulatedGridSelection).not.toEqual([]);\n      expect(accumulatedGridSelection).toHaveLength(1);\n    });\n  });\n\n  const weekDates = getWeekDates(new TZDate('2022-02-14T00:00:00.000'), {\n    startDayOfWeek: 0,\n    workweek: false,\n  });\n\n  const timeGridData = createTimeGridData(weekDates, { hourStart: 0, hourEnd: 24 });\n\n  describe('Opening popup', () => {\n    it('should open event form popup after dragging when the `useFormPopup` option is enabled', () => {\n      // Given\n      const showFormPopupAction = store.getState().dispatch.popup.showFormPopup;\n      setup({\n        useFormPopup: true,\n        selectionSorter: timeGridSelectionHelper.sortSelection,\n        dateCollection: timeGridData,\n        dateGetter: timeGridSelectionHelper.getDateFromCollection,\n      });\n      const container = screen.getByTestId('container');\n\n      // When\n      dragAndDrop({\n        element: container,\n        initPosition: { clientX: 35, clientY: 240 },\n        targetPosition: { clientX: 35, clientY: 480 },\n      });\n\n      // Then\n      expect(showFormPopupAction).toHaveBeenCalledWith(\n        expect.objectContaining({\n          start: new TZDate('2022-02-16T12:00:00.000'),\n          end: new TZDate('2022-02-17T00:00:00.000'),\n        })\n      );\n    });\n\n    // FIXME\n    // it('should open event form popup after clicking when the `useFormPopup` option is enabled', () => {\n    //   // Given\n    //   jest.useFakeTimers(); // Test for debounced click handler.\n    //   const showFormPopupAction = store.getState().dispatch.popup.showFormPopup;\n    //   setup({\n    //     useFormPopup: true,\n    //     selectionSorter: timeGridSelectionHelper.sortSelection,\n    //     dateCollection: timeGridData,\n    //     dateGetter: timeGridSelectionHelper.getDateFromCollection,\n    //   });\n    //   const container = screen.getByTestId('container');\n    //\n    //   // When\n    //   userEvent.click(container, { clientX: 35, clientY: 240 });\n    //   jest.advanceTimersByTime(1000);\n    //\n    //   // Then\n    //   expect(showFormPopupAction).toHaveBeenCalledWith(\n    //     expect.objectContaining({\n    //       start: new TZDate('2022-02-16T12:00:00.000'),\n    //       end: new TZDate('2022-02-16T12:30:00.000'),\n    //     })\n    //   );\n    // });\n  });\n\n  describe('Invoking Custom Event', () => {\n    function wrapMockHandler(mockFn: jest.Mock) {\n      return (...args: any[]) => mockFn(...args);\n    }\n    const mockHandler = jest.fn();\n\n    beforeEach(() => {\n      eventBus.on('selectDateTime', wrapMockHandler(mockHandler));\n      setup({\n        useFormPopup: true,\n        selectionSorter: timeGridSelectionHelper.sortSelection,\n        dateCollection: timeGridData,\n        dateGetter: timeGridSelectionHelper.getDateFromCollection,\n      });\n    });\n\n    afterEach(() => {\n      mockHandler.mockReset();\n    });\n\n    it('should fire `selectDateTime` custom event after dragging', () => {\n      // Given\n      const container = screen.getByTestId('container');\n\n      // When\n      dragAndDrop({\n        element: container,\n        initPosition: { clientX: 35, clientY: 240 },\n        targetPosition: { clientX: 35, clientY: 480 },\n      });\n\n      // Then\n      expect(mockHandler).toHaveBeenCalledWith(\n        expect.objectContaining({\n          start: new Date('2022-02-16T12:00:00.000'),\n          end: new Date('2022-02-17T00:00:00.000'),\n        })\n      );\n    });\n\n    // FIXME\n    // it('should fire `selectDateTime` custom event after clicking', () => {\n    //   // Given\n    //   jest.useFakeTimers(); // Test for debounced click handler.\n    //   const container = screen.getByTestId('container');\n    //\n    //   // When\n    //   userEvent.click(container, { clientX: 35, clientY: 240 });\n    //   jest.advanceTimersByTime(1000);\n    //\n    //   // Then\n    //   expect(mockHandler).toHaveBeenCalledWith(\n    //     expect.objectContaining({\n    //       start: new Date('2022-02-16T12:00:00.000'),\n    //       end: new Date('2022-02-16T12:30:00.000'),\n    //     })\n    //   );\n    // });\n  });\n\n  describe('GridSelection options', () => {\n    const cases: GridSelectionOptions[] = [\n      {\n        enableDblClick: true,\n        enableClick: true,\n      },\n      {\n        enableDblClick: true,\n        enableClick: false,\n      },\n      {\n        enableDblClick: false,\n        enableClick: true,\n      },\n      {\n        enableDblClick: false,\n        enableClick: false,\n      },\n    ];\n\n    cases.forEach(({ enableDblClick, enableClick }) => {\n      // FIXME\n      // it(`should make enable/disable the click event depending on gridSelection options: { enableDblClick: ${enableDblClick}, enableClick: ${enableClick} }`, () => {\n      //   // Given\n      //   jest.useFakeTimers(); // Test for debounced click handler.\n      //   setup({ gridSelection: { enableDblClick, enableClick } });\n      //   const container = screen.getByTestId('container');\n      //\n      //   // When\n      //   userEvent.click(container, {\n      //     clientX: 0,\n      //     clientY: 0,\n      //   });\n      //   jest.advanceTimersByTime(1000);\n      //\n      //   // Then\n      //   if (enableClick) {\n      //     expect(getGridSelectionState().timeGrid).not.toBeNull();\n      //   } else {\n      //     expect(getGridSelectionState().timeGrid).toBeNull();\n      //   }\n      // });\n      //\n      // it(`should make enable/disable the double click event depending on gridSelection options: { enableDblClick: ${enableDblClick}, enableClick: ${enableClick} }`, () => {\n      //   // Given\n      //   setup({ gridSelection: { enableDblClick, enableClick } });\n      //   const container = screen.getByTestId('container');\n      //\n      //   // When\n      //   userEvent.dblClick(container, {\n      //     clientX: 0,\n      //     clientY: 0,\n      //   });\n      //\n      //   // Then\n      //   if (enableDblClick) {\n      //     expect(getGridSelectionState().timeGrid).not.toBeNull();\n      //   } else {\n      //     expect(getGridSelectionState().timeGrid).toBeNull();\n      //   }\n      // });\n\n      it(`should not affect the drag event: { enableDblClick: ${enableDblClick}, enableClick: ${enableClick} }`, () => {\n        // Given\n        setup({ gridSelection: { enableDblClick, enableClick } });\n        const container = screen.getByTestId('container');\n\n        // When\n        dragAndDrop({\n          element: container,\n          initPosition: { clientX: 0, clientY: 0 },\n          targetPosition: { clientX: 35, clientY: 240 },\n        });\n\n        // Then\n        expect(getGridSelectionState().timeGrid).not.toBeNull();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/hooks/gridSelection/useGridSelection.ts",
    "content": "import { useCallback, useEffect, useRef, useState } from 'preact/hooks';\n\nimport { useDispatch, useStore } from '@src/contexts/calendarStore';\nimport { useEventBus } from '@src/contexts/eventBus';\nimport { useLayoutContainer } from '@src/contexts/layoutContainer';\nimport { cls } from '@src/helpers/css';\nimport { DRAGGING_TYPE_CREATORS } from '@src/helpers/drag';\nimport { useClickPrevention } from '@src/hooks/common/useClickPrevention';\nimport { useDrag } from '@src/hooks/common/useDrag';\nimport { useTransientUpdate } from '@src/hooks/common/useTransientUpdate';\nimport { dndSelector, optionsSelector } from '@src/selectors';\nimport { DraggingState } from '@src/slices/dnd';\nimport type { GridSelectionType } from '@src/slices/gridSelection';\nimport type TZDate from '@src/time/date';\nimport { isPresent } from '@src/utils/type';\n\nimport type { GridSelectionData } from '@t/components/gridSelection';\nimport type { GridPosition, GridPositionFinder } from '@t/grid';\nimport type { Coordinates } from '@t/mouse';\nimport type { CalendarState } from '@t/store';\n\nconst GRID_SELECTION_TYPE_MAP = {\n  dayGridMonth: 'month',\n  dayGridWeek: 'allday',\n  timeGrid: 'time',\n};\n\nfunction sortDates(a: TZDate, b: TZDate) {\n  const isIncreased = a < b;\n\n  return isIncreased ? [a, b] : [b, a];\n}\n\nexport function useGridSelection<DateCollection>({\n  type,\n  selectionSorter,\n  dateGetter,\n  dateCollection,\n  gridPositionFinder,\n}: {\n  type: GridSelectionType;\n  selectionSorter: (initPos: GridPosition, currentPos: GridPosition) => GridSelectionData;\n  dateGetter: (\n    dateCollection: DateCollection,\n    gridSelection: GridSelectionData\n  ) => [TZDate, TZDate];\n  dateCollection: DateCollection;\n  gridPositionFinder: GridPositionFinder;\n}) {\n  const { useFormPopup, gridSelection: gridSelectionOptions } = useStore(optionsSelector);\n  const { enableDblClick, enableClick } = gridSelectionOptions;\n\n  const { setGridSelection, addGridSelection, clearAll } = useDispatch('gridSelection');\n  const { hideAllPopup, showFormPopup } = useDispatch('popup');\n  const eventBus = useEventBus();\n  const layoutContainer = useLayoutContainer();\n\n  const [initMousePosition, setInitMousePosition] = useState<Coordinates | null>(null);\n  const [initGridPosition, setInitGridPosition] = useState<GridPosition | null>(null);\n  const isSelectingGridRef = useRef(false);\n  const gridSelectionRef = useRef<GridSelectionData | null>(null);\n\n  useTransientUpdate(\n    useCallback((state: CalendarState) => state.gridSelection[type], [type]),\n    (gridSelection) => {\n      gridSelectionRef.current = gridSelection;\n    }\n  );\n\n  useTransientUpdate(dndSelector, ({ draggingState, draggingItemType }) => {\n    isSelectingGridRef.current =\n      draggingItemType === currentGridSelectionType && draggingState >= DraggingState.INIT;\n  });\n\n  const currentGridSelectionType = DRAGGING_TYPE_CREATORS.gridSelection(type);\n\n  const setGridSelectionByPosition = (e: MouseEvent) => {\n    const gridPosition = gridPositionFinder(e);\n\n    if (isPresent(initGridPosition) && isPresent(gridPosition)) {\n      setGridSelection(type, selectionSorter(initGridPosition, gridPosition));\n    }\n  };\n\n  const [handleClickWithDebounce, handleDblClickPreventingClick] = useClickPrevention({\n    onClick: (e: MouseEvent) => {\n      if (enableClick) {\n        onMouseUp(e, true);\n      }\n    },\n    onDblClick: (e: MouseEvent) => {\n      if (enableDblClick) {\n        onMouseUp(e, true);\n      }\n    },\n    delay: 250, // heuristic value\n  });\n\n  const onMouseUpWithClick = (e: MouseEvent) => {\n    const isClick = e.detail <= 1;\n\n    if (!enableClick && (!enableDblClick || isClick)) {\n      return;\n    }\n\n    if (enableClick) {\n      if (isClick) {\n        handleClickWithDebounce(e);\n      } else {\n        handleDblClickPreventingClick(e);\n      }\n\n      return;\n    }\n\n    onMouseUp(e, true);\n  };\n\n  const onMouseUp = (e: MouseEvent, isClickEvent: boolean) => {\n    // The grid selection is created on mouseup in case of the click event.\n    if (isClickEvent) {\n      setGridSelectionByPosition(e);\n    }\n\n    if (isPresent(gridSelectionRef.current)) {\n      const [startDate, endDate] = sortDates(\n        ...dateGetter(dateCollection, gridSelectionRef.current)\n      );\n\n      if (useFormPopup && isPresent(initMousePosition)) {\n        const popupArrowPointPosition = {\n          top: (e.clientY + initMousePosition.y) / 2,\n          left: (e.clientX + initMousePosition.x) / 2,\n        };\n\n        showFormPopup({\n          isCreationPopup: true,\n          title: '',\n          location: '',\n          start: startDate,\n          end: endDate,\n          isAllday: type !== 'timeGrid',\n          isPrivate: false,\n          popupArrowPointPosition,\n          close: clearAll,\n        });\n      }\n\n      const gridSelectionSelector = `.${cls(GRID_SELECTION_TYPE_MAP[type])}.${cls(\n        'grid-selection'\n      )}`;\n      const gridSelectionElements = Array.from(\n        layoutContainer?.querySelectorAll(gridSelectionSelector) ?? []\n      );\n\n      eventBus.fire('selectDateTime', {\n        start: startDate.toDate(),\n        end: endDate.toDate(),\n        isAllday: type !== 'timeGrid',\n        nativeEvent: e,\n        gridSelectionElements,\n      });\n    }\n  };\n\n  const clearGridSelection = useCallback(() => {\n    setInitMousePosition(null);\n    setInitGridPosition(null);\n    setGridSelection(type, null);\n  }, [setGridSelection, type]);\n\n  const onMouseDown = useDrag(currentGridSelectionType, {\n    onInit: (e) => {\n      if (useFormPopup) {\n        setInitMousePosition({\n          x: e.clientX,\n          y: e.clientY,\n        });\n        hideAllPopup();\n      }\n\n      const gridPosition = gridPositionFinder(e);\n\n      if (isPresent(gridPosition)) {\n        setInitGridPosition(gridPosition);\n      }\n\n      if (!useFormPopup) {\n        addGridSelection(type, gridSelectionRef.current);\n      }\n    },\n    onDragStart: (e) => {\n      // The grid selection is created on mousemove in case of the drag event.\n      setGridSelectionByPosition(e);\n    },\n    onDrag: (e) => {\n      if (isSelectingGridRef.current) {\n        setGridSelectionByPosition(e);\n      }\n    },\n    onMouseUp: (e, { draggingState }) => {\n      e.stopPropagation();\n\n      const isClickEvent = draggingState <= DraggingState.INIT;\n\n      if (isClickEvent) {\n        onMouseUpWithClick(e);\n      } else {\n        onMouseUp(e, isClickEvent);\n      }\n    },\n    onPressESCKey: clearGridSelection,\n  });\n\n  useEffect(() => clearGridSelection, [clearGridSelection]);\n\n  return onMouseDown;\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/popup/useFormState.ts",
    "content": "import { useReducer } from 'preact/hooks';\n\nimport type { EventObject, EventState } from '@t/events';\n\nexport enum FormStateActionType {\n  init = 'init',\n  setCalendarId = 'setCalendarId',\n  setTitle = 'setTitle',\n  setLocation = 'setLocation',\n  setPrivate = 'setPrivate',\n  setAllday = 'setAllday',\n  setState = 'setState',\n  reset = 'reset',\n}\n\ntype FormStateAction =\n  | { type: FormStateActionType.init; event: EventObject }\n  | { type: FormStateActionType.setCalendarId; calendarId: string }\n  | { type: FormStateActionType.setTitle; title: string }\n  | { type: FormStateActionType.setLocation; location: string }\n  | { type: FormStateActionType.setPrivate; isPrivate: boolean }\n  | { type: FormStateActionType.setAllday; isAllday: boolean }\n  | { type: FormStateActionType.setState; state: EventState }\n  | { type: FormStateActionType.reset };\n\nexport type FormStateDispatcher = (action: FormStateAction) => void;\n\nconst defaultFormState: EventObject = {\n  title: '',\n  location: '',\n  isAllday: false,\n  isPrivate: false,\n  state: 'Busy',\n};\n\n// eslint-disable-next-line complexity\nfunction formStateReducer(state: EventObject, action: FormStateAction): EventObject {\n  switch (action.type) {\n    case FormStateActionType.init:\n      return { ...defaultFormState, ...action.event };\n    case FormStateActionType.setCalendarId:\n      return { ...state, calendarId: action.calendarId };\n    case FormStateActionType.setTitle:\n      return { ...state, title: action.title };\n    case FormStateActionType.setLocation:\n      return { ...state, location: action.location };\n    case FormStateActionType.setPrivate:\n      return { ...state, isPrivate: action.isPrivate };\n    case FormStateActionType.setAllday:\n      return { ...state, isAllday: action.isAllday };\n    case FormStateActionType.setState:\n      return { ...state, state: action.state };\n    case FormStateActionType.reset:\n      return { ...state, ...defaultFormState };\n\n    default:\n      return state;\n  }\n}\n\nexport function useFormState(initCalendarId?: string) {\n  return useReducer(formStateReducer, { calendarId: initCalendarId, ...defaultFormState });\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/template/useStringOnlyTemplate.ts",
    "content": "import { useStore } from '@src/contexts/calendarStore';\nimport { templateSelector } from '@src/selectors';\nimport type { TemplateName } from '@src/template/default';\nimport { isNil, isString } from '@src/utils/type';\n\nexport function useStringOnlyTemplate({\n  template,\n  model,\n  defaultValue = '',\n}: {\n  template: TemplateName;\n  model?: any;\n  defaultValue?: string;\n}) {\n  const templates = useStore(templateSelector);\n  const templateFunc: Function = templates[template];\n\n  if (isNil(templateFunc)) {\n    return defaultValue;\n  }\n\n  let result = templateFunc(model);\n  if (!isString(result)) {\n    result = defaultValue;\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/timeGrid/useTimeGridEventMove.spec.tsx",
    "content": "import { h } from 'preact';\n\nimport { initCalendarStore, StoreProvider } from '@src/contexts/calendarStore';\nimport { EventBusProvider } from '@src/contexts/eventBus';\nimport { DRAGGING_TYPE_CREATORS } from '@src/helpers/drag';\nimport { createGridPositionFinder, createTimeGridData, getWeekDates } from '@src/helpers/grid';\nimport { useDrag } from '@src/hooks/common/useDrag';\nimport { useTimeGridEventMove } from '@src/hooks/timeGrid/useTimeGridEventMove';\nimport EventModel from '@src/model/eventModel';\nimport EventUIModel from '@src/model/eventUIModel';\nimport { createDate } from '@src/test/helpers';\nimport { dragAndDrop, renderHook, screen, userEvent } from '@src/test/utils';\nimport { Day, setTimeStrToDate, toFormat } from '@src/time/datetime';\nimport { EventBusImpl } from '@src/utils/eventBus';\nimport { noop } from '@src/utils/noop';\nimport { isNil } from '@src/utils/type';\n\ndescribe('useTimeGridEventMove', () => {\n  let eventModel: EventModel;\n  let eventUIModel: EventUIModel;\n  const rows = getWeekDates(createDate(2021, 2, 14), {\n    startDayOfWeek: Day.SUN,\n  });\n  const timeGridData = createTimeGridData(rows, { hourStart: 0, hourEnd: 24 });\n  const DEFAULT_CONTAINER_WIDTH = 70;\n  const DEFAULT_CONTAINER_HEIGHT = 480;\n\n  // Starts from the third day of the week, 10:00 AM ~ 12:00 PM\n  const DEFAULT_START_ROW_INDEX = 20;\n  const DEFAULT_END_ROW_INDEX = 28;\n  const DEFAULT_COLUMN_INDEX = 2;\n\n  const BASE_ROW_HEIGHT = timeGridData.rows[0].height;\n  const BASE_COLUMN_WIDTH = timeGridData.columns[0].width;\n\n  function toMousePosition(gridX: number, gridY: number) {\n    return {\n      clientX: gridX * 10 + 5,\n      clientY: gridY * 10 + 5,\n    };\n  }\n\n  const setup = () => {\n    const container = document.createElement('div');\n    container.setAttribute('data-testid', 'container');\n    document.body.appendChild(container);\n    jest.spyOn(container, 'getBoundingClientRect').mockReturnValue({\n      x: 0,\n      y: 0,\n      top: 0,\n      bottom: 0,\n      left: 0,\n      right: 0,\n      width: DEFAULT_CONTAINER_WIDTH,\n      height: DEFAULT_CONTAINER_HEIGHT,\n      toJSON: noop,\n    });\n    const eventBus = new EventBusImpl();\n    const store = initCalendarStore();\n    const gridPositionFinder = createGridPositionFinder({\n      rowsCount: timeGridData.rows.length,\n      columnsCount: timeGridData.columns.length,\n      container,\n    });\n    store.getState().dispatch.calendar.createEvents([eventModel]);\n    const MockBody = () => {\n      const onMouseDown = useDrag(\n        DRAGGING_TYPE_CREATORS.moveEvent('timeGrid', `${eventUIModel.cid()}`),\n        {\n          onInit: () => {\n            store.getState().dispatch.dnd.setDraggingEventUIModel(eventUIModel);\n          },\n        }\n      );\n      return <div onMouseDown={onMouseDown}>Event 1</div>;\n    };\n    const { result } = renderHook(\n      () =>\n        useTimeGridEventMove({\n          gridPositionFinder,\n          timeGridData,\n        }),\n      {\n        wrapper: ({ children }) => (\n          <EventBusProvider value={eventBus}>\n            <StoreProvider store={store}>\n              <MockBody />\n              {children}\n            </StoreProvider>\n          </EventBusProvider>\n        ),\n      }\n    );\n\n    return result;\n  };\n\n  beforeEach(() => {\n    // eslint-disable-next-line prefer-destructuring\n    const { startTime, top: startRowTop } = timeGridData.rows[DEFAULT_START_ROW_INDEX];\n    // eslint-disable-next-line prefer-destructuring\n    const {\n      top: endRowTop,\n      height: endRowHeight,\n      endTime,\n    } = timeGridData.rows[DEFAULT_END_ROW_INDEX];\n\n    eventModel = new EventModel({\n      start: setTimeStrToDate(timeGridData.columns[DEFAULT_COLUMN_INDEX].date, startTime),\n      end: setTimeStrToDate(timeGridData.columns[DEFAULT_COLUMN_INDEX].date, endTime),\n    });\n    eventUIModel = new EventUIModel(eventModel);\n    eventUIModel.setUIProps({\n      top: startRowTop,\n      left: timeGridData.columns[DEFAULT_COLUMN_INDEX].left,\n      height: endRowTop + endRowHeight - startRowTop,\n    });\n  });\n\n  it('should return null when not dragging', () => {\n    // Given\n    const user = userEvent.setup();\n    const result = setup();\n    const event = screen.getByText('Event 1');\n\n    // When\n    user.click(event);\n\n    // Then\n    expect(result.current?.movingEvent).toBeNull();\n  });\n\n  /**\n   * Starting from the top of the event, then clockwise\n   */\n  const cases = [\n    {\n      name: 'to the top',\n      targetPosition: toMousePosition(DEFAULT_COLUMN_INDEX, DEFAULT_START_ROW_INDEX - 2),\n      expected: {\n        topDiff: -BASE_ROW_HEIGHT * 2,\n        leftDiff: 0,\n        timeLabel: '09:00',\n      },\n    },\n    {\n      name: 'to the upper right',\n      targetPosition: toMousePosition(DEFAULT_COLUMN_INDEX + 1, DEFAULT_START_ROW_INDEX - 2),\n      expected: {\n        topDiff: -BASE_ROW_HEIGHT * 2,\n        leftDiff: BASE_COLUMN_WIDTH,\n        timeLabel: '09:00',\n      },\n    },\n    {\n      name: 'to the right',\n      targetPosition: toMousePosition(DEFAULT_COLUMN_INDEX + 1, DEFAULT_START_ROW_INDEX),\n      expected: {\n        topDiff: 0,\n        leftDiff: BASE_COLUMN_WIDTH,\n        timeLabel: '10:00',\n      },\n    },\n    {\n      name: 'to the lower right',\n      targetPosition: toMousePosition(DEFAULT_COLUMN_INDEX + 1, DEFAULT_START_ROW_INDEX + 2),\n      expected: {\n        topDiff: BASE_ROW_HEIGHT * 2,\n        leftDiff: BASE_COLUMN_WIDTH,\n        timeLabel: '11:00',\n      },\n    },\n    {\n      name: 'to the bottom',\n      targetPosition: toMousePosition(DEFAULT_COLUMN_INDEX, DEFAULT_START_ROW_INDEX + 2),\n      expected: {\n        topDiff: BASE_ROW_HEIGHT * 2,\n        leftDiff: 0,\n        timeLabel: '11:00',\n      },\n    },\n    {\n      name: 'to the lower left',\n      targetPosition: toMousePosition(DEFAULT_COLUMN_INDEX - 1, DEFAULT_START_ROW_INDEX + 2),\n      expected: {\n        topDiff: BASE_ROW_HEIGHT * 2,\n        leftDiff: -BASE_COLUMN_WIDTH,\n        timeLabel: '11:00',\n      },\n    },\n    {\n      name: 'to the left',\n      targetPosition: toMousePosition(DEFAULT_COLUMN_INDEX - 1, DEFAULT_START_ROW_INDEX),\n      expected: {\n        topDiff: 0,\n        leftDiff: -BASE_COLUMN_WIDTH,\n        timeLabel: '10:00',\n      },\n    },\n    {\n      name: 'to the upper left',\n      targetPosition: toMousePosition(DEFAULT_COLUMN_INDEX - 1, DEFAULT_START_ROW_INDEX - 2),\n      expected: {\n        topDiff: -BASE_ROW_HEIGHT * 2,\n        leftDiff: -BASE_COLUMN_WIDTH,\n        timeLabel: '09:00',\n      },\n    },\n  ];\n\n  it.each(cases)(\n    'should change top & left value of moving event while dragging $name',\n    ({ targetPosition, expected }) => {\n      // Given\n      const result = setup();\n      const event = screen.getByText('Event 1');\n      const { top: initTop, left: initLeft } = eventUIModel.getUIProps();\n\n      // When\n      dragAndDrop({\n        element: event,\n        initPosition: toMousePosition(DEFAULT_COLUMN_INDEX, DEFAULT_START_ROW_INDEX),\n        targetPosition,\n        hold: true,\n      });\n\n      // Then\n      if (\n        isNil(result.current) ||\n        isNil(result.current.movingEvent) ||\n        isNil(result.current.nextStartTime)\n      ) {\n        throw new Error('movingEvent is null');\n      } else {\n        const topDiff = result.current.movingEvent.top - initTop;\n        const leftDiff = result.current.movingEvent.left - initLeft;\n\n        expect(topDiff).toBeCloseTo(expected.topDiff, 1);\n        expect(leftDiff).toBeCloseTo(expected.leftDiff, 1);\n        expect(toFormat(result.current.nextStartTime, 'HH:mm')).toBe(expected.timeLabel);\n      }\n    }\n  );\n});\n"
  },
  {
    "path": "apps/calendar/src/hooks/timeGrid/useTimeGridEventMove.ts",
    "content": "import { useCallback, useEffect, useMemo, useRef } from 'preact/hooks';\n\nimport { useStore } from '@src/contexts/calendarStore';\nimport { useEventBus } from '@src/contexts/eventBus';\nimport { useWhen } from '@src/hooks/common/useWhen';\nimport { useCurrentPointerPositionInGrid } from '@src/hooks/event/useCurrentPointerPositionInGrid';\nimport { useDraggingEvent } from '@src/hooks/event/useDraggingEvent';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport type TZDate from '@src/time/date';\nimport { addMilliseconds, addMinutes, MS_PER_DAY, MS_PER_THIRTY_MINUTES } from '@src/time/datetime';\nimport { isNil, isPresent } from '@src/utils/type';\n\nimport type { GridPosition, GridPositionFinder, TimeGridData } from '@t/grid';\nimport type { CalendarState } from '@t/store';\n\nconst THIRTY_MINUTES = 30;\n\nfunction getCurrentIndexByTime(time: TZDate, hourStart: number) {\n  const hour = time.getHours() - hourStart;\n  const minutes = time.getMinutes();\n\n  return hour * 2 + Math.floor(minutes / THIRTY_MINUTES);\n}\n\nfunction getMovingEventPosition({\n  draggingEvent,\n  columnDiff,\n  rowDiff,\n  timeGridDataRows,\n  currentDate,\n}: {\n  draggingEvent: EventUIModel;\n  columnDiff: number;\n  rowDiff: number;\n  timeGridDataRows: TimeGridData['rows'];\n  currentDate: TZDate;\n}) {\n  const rowHeight = timeGridDataRows[0].height;\n  const maxHeight = rowHeight * timeGridDataRows.length;\n  const millisecondsDiff = rowDiff * MS_PER_THIRTY_MINUTES + columnDiff * MS_PER_DAY;\n  const hourStart = Number(timeGridDataRows[0].startTime.split(':')[0]);\n\n  const { goingDuration = 0, comingDuration = 0 } = draggingEvent.model;\n  const goingStart = addMinutes(draggingEvent.getStarts(), -goingDuration);\n  const comingEnd = addMinutes(draggingEvent.getEnds(), comingDuration);\n  const nextStart = addMilliseconds(goingStart, millisecondsDiff);\n  const nextEnd = addMilliseconds(comingEnd, millisecondsDiff);\n  const startIndex = Math.max(getCurrentIndexByTime(nextStart, hourStart), 0);\n  const endIndex = Math.min(getCurrentIndexByTime(nextEnd, hourStart), timeGridDataRows.length - 1);\n\n  const isStartAtPrevDate =\n    nextStart.getFullYear() < currentDate.getFullYear() ||\n    nextStart.getMonth() < currentDate.getMonth() ||\n    nextStart.getDate() < currentDate.getDate();\n  const isEndAtNextDate =\n    nextEnd.getFullYear() > currentDate.getFullYear() ||\n    nextEnd.getMonth() > currentDate.getMonth() ||\n    nextEnd.getDate() > currentDate.getDate();\n  const indexDiff = endIndex - (isStartAtPrevDate ? 0 : startIndex);\n\n  const top = isStartAtPrevDate ? 0 : timeGridDataRows[startIndex].top;\n  const height = isEndAtNextDate ? maxHeight : Math.max(indexDiff, 1) * rowHeight;\n\n  return { top, height };\n}\n\nconst initXSelector = (state: CalendarState) => state.dnd.initX;\nconst initYSelector = (state: CalendarState) => state.dnd.initY;\n\nexport function useTimeGridEventMove({\n  gridPositionFinder,\n  timeGridData,\n}: {\n  gridPositionFinder: GridPositionFinder;\n  timeGridData: TimeGridData;\n}) {\n  const initX = useStore(initXSelector);\n  const initY = useStore(initYSelector);\n\n  const eventBus = useEventBus();\n  const { isDraggingEnd, isDraggingCanceled, draggingEvent, clearDraggingEvent } = useDraggingEvent(\n    'timeGrid',\n    'move'\n  );\n\n  const [currentGridPos, clearCurrentGridPos] = useCurrentPointerPositionInGrid(gridPositionFinder);\n  const initGridPosRef = useRef<GridPosition | null>(null);\n\n  useEffect(() => {\n    if (isPresent(initX) && isPresent(initY)) {\n      initGridPosRef.current = gridPositionFinder({\n        clientX: initX,\n        clientY: initY,\n      });\n    }\n  }, [gridPositionFinder, initX, initY]);\n\n  const gridDiff = useMemo(() => {\n    if (isNil(initGridPosRef.current) || isNil(currentGridPos)) {\n      return null;\n    }\n\n    return {\n      columnDiff: currentGridPos.columnIndex - initGridPosRef.current.columnIndex,\n      rowDiff: currentGridPos.rowIndex - initGridPosRef.current.rowIndex,\n    };\n  }, [currentGridPos]);\n  const startDateTime = useMemo(() => {\n    if (isNil(draggingEvent)) {\n      return null;\n    }\n\n    return draggingEvent.getStarts();\n  }, [draggingEvent]);\n\n  const clearState = useCallback(() => {\n    clearCurrentGridPos();\n    clearDraggingEvent();\n    initGridPosRef.current = null;\n  }, [clearCurrentGridPos, clearDraggingEvent]);\n\n  const nextStartTime = useMemo(() => {\n    if (isNil(gridDiff) || isNil(startDateTime)) {\n      return null;\n    }\n\n    return addMilliseconds(\n      startDateTime,\n      gridDiff.rowDiff * MS_PER_THIRTY_MINUTES + gridDiff.columnDiff * MS_PER_DAY\n    );\n  }, [gridDiff, startDateTime]);\n\n  const movingEvent = useMemo(() => {\n    if (isNil(draggingEvent) || isNil(currentGridPos) || isNil(gridDiff)) {\n      return null;\n    }\n\n    const clonedEvent = draggingEvent.clone();\n    const { top, height } = getMovingEventPosition({\n      draggingEvent: clonedEvent,\n      columnDiff: gridDiff.columnDiff,\n      rowDiff: gridDiff.rowDiff,\n      timeGridDataRows: timeGridData.rows,\n      currentDate: timeGridData.columns[currentGridPos.columnIndex].date,\n    });\n\n    clonedEvent.setUIProps({\n      left: timeGridData.columns[currentGridPos.columnIndex].left,\n      width: timeGridData.columns[currentGridPos.columnIndex].width,\n      top,\n      height,\n    });\n\n    return clonedEvent;\n  }, [currentGridPos, draggingEvent, gridDiff, timeGridData.columns, timeGridData.rows]);\n\n  useWhen(() => {\n    const shouldUpdate =\n      !isDraggingCanceled &&\n      isPresent(draggingEvent) &&\n      isPresent(currentGridPos) &&\n      isPresent(gridDiff) &&\n      isPresent(nextStartTime) &&\n      (gridDiff.rowDiff !== 0 || gridDiff.columnDiff !== 0);\n    if (shouldUpdate) {\n      const duration = draggingEvent.duration();\n      const nextEndTime = addMilliseconds(nextStartTime, duration);\n\n      eventBus.fire('beforeUpdateEvent', {\n        event: draggingEvent.model.toEventObject(),\n        changes: {\n          start: nextStartTime,\n          end: nextEndTime,\n        },\n      });\n    }\n\n    clearState();\n  }, isDraggingEnd);\n\n  return {\n    movingEvent,\n    nextStartTime,\n  };\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/timeGrid/useTimeGridEventResize.ts",
    "content": "import type { ComponentProps } from 'preact';\nimport { useCallback, useEffect, useMemo, useState } from 'preact/hooks';\n\nimport type { ResizingGuideByColumn } from '@src/components/timeGrid/resizingGuideByColumn';\nimport { useEventBus } from '@src/contexts/eventBus';\nimport { useWhen } from '@src/hooks/common/useWhen';\nimport { useCurrentPointerPositionInGrid } from '@src/hooks/event/useCurrentPointerPositionInGrid';\nimport { useDraggingEvent } from '@src/hooks/event/useDraggingEvent';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport type TZDate from '@src/time/date';\nimport { addMinutes, max, setTimeStrToDate } from '@src/time/datetime';\nimport { findLastIndex } from '@src/utils/array';\nimport { isNil, isPresent } from '@src/utils/type';\n\nimport type { TimeGridRow } from '@t/grid';\n\ntype FilteredUIModelRow = [] | [EventUIModel];\n\nexport function useTimeGridEventResize({\n  gridPositionFinder,\n  totalUIModels,\n  columnIndex,\n  timeGridData,\n}: ComponentProps<typeof ResizingGuideByColumn>) {\n  const eventBus = useEventBus();\n  const {\n    isDraggingEnd,\n    isDraggingCanceled,\n    draggingEvent: resizingStartUIModel,\n    clearDraggingEvent,\n  } = useDraggingEvent('timeGrid', 'resize');\n  const [currentGridPos, clearCurrentGridPos] = useCurrentPointerPositionInGrid(gridPositionFinder);\n  const [guideUIModel, setGuideUIModel] = useState<EventUIModel | null>(null);\n\n  const clearStates = useCallback(() => {\n    setGuideUIModel(null);\n    clearDraggingEvent();\n    clearCurrentGridPos();\n  }, [clearCurrentGridPos, clearDraggingEvent]);\n\n  const baseResizingInfo = useMemo(() => {\n    if (isNil(resizingStartUIModel)) {\n      return null;\n    }\n\n    const { columns, rows } = timeGridData;\n\n    /**\n     * Filter UIModels that are made from the target event.\n     */\n    const resizeTargetUIModelColumns = totalUIModels.map(\n      (uiModels) =>\n        uiModels.filter(\n          (uiModel) => uiModel.cid() === resizingStartUIModel.cid()\n        ) as FilteredUIModelRow\n    );\n\n    const findRowIndexOf =\n      (targetDate: TZDate, targetColumnIndex: number) => (row: TimeGridRow) => {\n        const rowStartTZDate = setTimeStrToDate(columns[targetColumnIndex].date, row.startTime);\n        const rowEndTZDate = setTimeStrToDate(\n          timeGridData.columns[targetColumnIndex].date,\n          row.endTime\n        );\n\n        return rowStartTZDate <= targetDate && targetDate < rowEndTZDate;\n      };\n\n    const eventStartDateColumnIndex = resizeTargetUIModelColumns.findIndex((row) => row.length > 0);\n    const resizingStartEventUIModel = resizeTargetUIModelColumns[\n      eventStartDateColumnIndex\n    ][0] as EventUIModel;\n    const { goingDuration = 0 } = resizingStartEventUIModel.model;\n    const renderStart = addMinutes(resizingStartEventUIModel.getStarts(), -goingDuration);\n    const eventStartDateRowIndex = Math.max(\n      rows.findIndex(findRowIndexOf(renderStart, eventStartDateColumnIndex)),\n      0\n    ); // when it is -1, the event starts before the current view.\n\n    const eventEndDateColumnIndex = findLastIndex(\n      resizeTargetUIModelColumns,\n      (row) => row.length > 0\n    );\n    const resizingEndEventUIModel = resizeTargetUIModelColumns[\n      eventEndDateColumnIndex\n    ][0] as EventUIModel;\n    const { comingDuration = 0 } = resizingEndEventUIModel.model;\n    const renderEnd = addMinutes(resizingEndEventUIModel.getStarts(), comingDuration);\n    let eventEndDateRowIndex = rows.findIndex(findRowIndexOf(renderEnd, eventEndDateColumnIndex)); // when it is -1, the event ends after the current view.\n    eventEndDateRowIndex = eventEndDateRowIndex >= 0 ? eventEndDateRowIndex : rows.length - 1;\n\n    return {\n      eventStartDateColumnIndex,\n      eventStartDateRowIndex,\n      eventEndDateColumnIndex,\n      eventEndDateRowIndex,\n      resizeTargetUIModelColumns,\n    };\n  }, [resizingStartUIModel, timeGridData, totalUIModels]);\n\n  const canCalculateGuideUIModel =\n    isPresent(baseResizingInfo) && isPresent(resizingStartUIModel) && isPresent(currentGridPos);\n\n  const oneRowHeight = useMemo(\n    () => (baseResizingInfo ? timeGridData.rows[0].height : 0),\n    [baseResizingInfo, timeGridData.rows]\n  );\n\n  // When drag an one-day event\n  useEffect(() => {\n    if (canCalculateGuideUIModel) {\n      const { eventStartDateRowIndex, eventStartDateColumnIndex, eventEndDateColumnIndex } =\n        baseResizingInfo;\n      if (\n        columnIndex === eventEndDateColumnIndex &&\n        eventStartDateColumnIndex === eventEndDateColumnIndex\n      ) {\n        const clonedUIModel = resizingStartUIModel.clone();\n        const { height, goingDurationHeight, comingDurationHeight } = clonedUIModel;\n        const newHeight = Math.max(\n          oneRowHeight +\n            (goingDurationHeight * height) / 100 +\n            (comingDurationHeight * height) / 100,\n          timeGridData.rows[currentGridPos.rowIndex].top -\n            timeGridData.rows[eventStartDateRowIndex].top +\n            oneRowHeight\n        );\n        const newGoingDurationHeight = (goingDurationHeight * height) / newHeight;\n        const newComingDurationHeight = (comingDurationHeight * height) / newHeight;\n\n        clonedUIModel.setUIProps({\n          height: newHeight,\n          goingDurationHeight: newGoingDurationHeight,\n          comingDurationHeight: newComingDurationHeight,\n          modelDurationHeight: 100 - (newGoingDurationHeight + newComingDurationHeight),\n        });\n        setGuideUIModel(clonedUIModel);\n      }\n    }\n  }, [\n    baseResizingInfo,\n    canCalculateGuideUIModel,\n    columnIndex,\n    currentGridPos,\n    resizingStartUIModel,\n    timeGridData.rows,\n    oneRowHeight,\n  ]);\n\n  // When drag a two-day event (but less than 24 hours)\n  useEffect(() => {\n    if (canCalculateGuideUIModel) {\n      const { resizeTargetUIModelColumns, eventStartDateColumnIndex, eventEndDateColumnIndex } =\n        baseResizingInfo;\n      if (\n        (columnIndex === eventStartDateColumnIndex || columnIndex === eventEndDateColumnIndex) &&\n        eventStartDateColumnIndex !== eventEndDateColumnIndex\n      ) {\n        let clonedUIModel;\n        if (columnIndex === eventStartDateColumnIndex) {\n          // first column\n          clonedUIModel = (resizeTargetUIModelColumns[columnIndex][0] as EventUIModel).clone();\n        } else {\n          // last column\n          clonedUIModel = resizingStartUIModel.clone();\n          clonedUIModel.setUIProps({\n            height: timeGridData.rows[currentGridPos.rowIndex].top + oneRowHeight,\n          });\n        }\n        setGuideUIModel(clonedUIModel);\n      }\n    }\n  }, [\n    baseResizingInfo,\n    canCalculateGuideUIModel,\n    columnIndex,\n    currentGridPos,\n    resizingStartUIModel,\n    timeGridData.rows,\n    oneRowHeight,\n  ]);\n\n  useWhen(() => {\n    const shouldUpdate =\n      !isDraggingCanceled &&\n      isPresent(baseResizingInfo) &&\n      isPresent(currentGridPos) &&\n      isPresent(resizingStartUIModel) &&\n      baseResizingInfo.eventEndDateColumnIndex === columnIndex;\n\n    if (shouldUpdate) {\n      const { comingDuration = 0 } = resizingStartUIModel.model;\n\n      const targetEndDate = addMinutes(\n        setTimeStrToDate(\n          timeGridData.columns[columnIndex].date,\n          timeGridData.rows[currentGridPos.rowIndex].endTime\n        ),\n        -comingDuration\n      );\n      const minEndDate = addMinutes(resizingStartUIModel.getStarts(), 30);\n\n      eventBus.fire('beforeUpdateEvent', {\n        event: resizingStartUIModel.model.toEventObject(),\n        changes: {\n          end: max(minEndDate, targetEndDate),\n        },\n      });\n    }\n\n    clearStates();\n  }, isDraggingEnd);\n\n  return guideUIModel;\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/timeGrid/useTimeGridScrollSync.ts",
    "content": "import { useTransientUpdate } from '@src/hooks/common/useTransientUpdate';\nimport { dndSelector } from '@src/selectors';\nimport { DraggingState } from '@src/slices/dnd';\nimport { isPresent } from '@src/utils/type';\n\nimport type { DraggingTypes } from '@t/drag';\n\nfunction isTimeGridDraggingType(draggingItemType: DraggingTypes | null) {\n  return /^(event|gridSelection)\\/timeGrid/.test(draggingItemType ?? '');\n}\n\nexport function useTimeGridScrollSync(scrollArea: HTMLDivElement | null, rowCount: number) {\n  useTransientUpdate(dndSelector, ({ y, draggingItemType, draggingState }) => {\n    if (\n      isPresent(scrollArea) &&\n      isTimeGridDraggingType(draggingItemType) &&\n      draggingState === DraggingState.DRAGGING &&\n      isPresent(y)\n    ) {\n      const { offsetTop, offsetHeight, scrollHeight } = scrollArea;\n      // Set minimum scroll boundary to the height of one row.\n      const scrollBoundary = Math.floor(scrollHeight / rowCount);\n      const layoutHeight = offsetTop + offsetHeight;\n\n      if (y < offsetTop + scrollBoundary) {\n        const scrollDiff = y - (offsetTop + scrollBoundary);\n        scrollArea.scrollTop = Math.max(0, scrollArea.scrollTop + scrollDiff);\n      } else if (y > layoutHeight - scrollBoundary) {\n        const scrollDiff = y - (layoutHeight - scrollBoundary);\n        scrollArea.scrollTop = Math.min(offsetHeight, scrollArea.scrollTop + scrollDiff);\n      }\n    }\n  });\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/timeGrid/useTimezoneLabelsTop.ts",
    "content": "import { useLayoutEffect, useState } from 'preact/hooks';\n\nimport { useStore } from '@src/contexts/calendarStore';\nimport { isPresent } from '@src/utils/type';\n\nimport type { CalendarState } from '@t/store';\n\nfunction timegridHeightSelector(state: CalendarState) {\n  // TODO: change `dayGridRows` to `panels`\n  return state.weekViewLayout?.dayGridRows?.time?.height;\n}\n\nexport function useTimezoneLabelsTop(timePanel: HTMLDivElement | null): number | null {\n  const timeGridPanelHeight = useStore(timegridHeightSelector);\n  const [stickyTop, setStickyTop] = useState<number | null>(null);\n\n  useLayoutEffect(() => {\n    if (isPresent(timeGridPanelHeight) && timePanel) {\n      setStickyTop(timePanel.offsetTop);\n    }\n  }, [timeGridPanelHeight, timePanel]);\n\n  return stickyTop;\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/timezone/useEventsWithTimezone.ts",
    "content": "import { useMemo } from 'preact/hooks';\n\nimport { useStore } from '@src/contexts/calendarStore';\nimport { createEventCollection } from '@src/controller/base';\nimport { useTZConverter } from '@src/hooks/timezone/useTZConverter';\nimport type EventModel from '@src/model/eventModel';\nimport { primaryTimezoneSelector } from '@src/selectors/timezone';\nimport TZDate from '@src/time/date';\nimport { isUsingDST } from '@src/time/timezone';\nimport type Collection from '@src/utils/collection';\nimport { clone } from '@src/utils/object';\n\nexport function useEventsWithTimezone(events: Collection<EventModel>) {\n  const primaryTimezoneName = useStore(primaryTimezoneSelector);\n  const tzConverter = useTZConverter();\n\n  return useMemo(() => {\n    if (primaryTimezoneName === 'Local') {\n      return events;\n    }\n\n    const isSystemUsingDST = isUsingDST(new TZDate());\n    const {\n      timedEvents = createEventCollection(),\n      totalEvents = createEventCollection(),\n    }: Record<'timedEvents' | 'totalEvents', Collection<EventModel>> = events.groupBy(\n      (eventModel) => (eventModel.category === 'time' ? 'timedEvents' : 'totalEvents')\n    );\n\n    timedEvents.each((eventModel) => {\n      const clonedEventModel = clone(eventModel);\n\n      let zonedStart = tzConverter(primaryTimezoneName, clonedEventModel.start);\n      let zonedEnd = tzConverter(primaryTimezoneName, clonedEventModel.end);\n\n      // Adjust the start and end time to the system timezone.\n      if (isSystemUsingDST) {\n        if (!isUsingDST(zonedStart)) {\n          zonedStart = zonedStart.addHours(1);\n        }\n        if (!isUsingDST(zonedEnd)) {\n          zonedEnd = zonedEnd.addHours(1);\n        }\n      } else {\n        if (isUsingDST(zonedStart)) {\n          zonedStart = zonedStart.addHours(-1);\n        }\n        if (isUsingDST(zonedEnd)) {\n          zonedEnd = zonedEnd.addHours(-1);\n        }\n      }\n\n      clonedEventModel.start = zonedStart;\n      clonedEventModel.end = zonedEnd;\n\n      totalEvents.add(clonedEventModel);\n    });\n\n    return totalEvents;\n  }, [events, primaryTimezoneName, tzConverter]);\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/timezone/usePrimaryTimezone.ts",
    "content": "import { useCallback } from 'preact/hooks';\n\nimport { useStore } from '@src/contexts/calendarStore';\nimport { useTZConverter } from '@src/hooks/timezone/useTZConverter';\nimport { primaryTimezoneSelector } from '@src/selectors/timezone';\nimport type TZDate from '@src/time/date';\n\nexport function usePrimaryTimezone(): [string, () => TZDate] {\n  const primaryTimezoneName = useStore(primaryTimezoneSelector);\n  const tzConverter = useTZConverter();\n\n  const getNow = useCallback(\n    () => tzConverter(primaryTimezoneName),\n    [primaryTimezoneName, tzConverter]\n  );\n\n  return [primaryTimezoneName, getNow];\n}\n"
  },
  {
    "path": "apps/calendar/src/hooks/timezone/useTZConverter.spec.ts",
    "content": "import { register, unregister } from 'timezone-mock';\n\nimport { initCalendarStore } from '@src/contexts/calendarStore';\nimport { useTZConverter } from '@src/hooks/timezone/useTZConverter';\nimport { renderHook } from '@src/test/utils';\nimport TZDate from '@src/time/date';\n\nimport type { Options } from '@t/options';\n\nbeforeEach(() => {\n  register('UTC');\n});\n\nafterEach(() => {\n  unregister();\n});\n\ndescribe('When it has customOffsetCalculator', () => {\n  const customOffsetCalculator = jest.fn().mockImplementation((timezoneName, timestamp) => {\n    if (timezoneName === 'UTC') {\n      return 0;\n    }\n\n    if (timezoneName === 'America/New_York') {\n      return -360; // It's mocked and impossible value because New York is UTC -4~-5.\n    }\n\n    return new Date(timestamp).getTimezoneOffset();\n  });\n\n  it('should convert TZDate following customOffsetCalculator', () => {\n    // Given\n    const timezoneName = 'America/New_York';\n    const options: Options = {\n      timezone: {\n        customOffsetCalculator,\n        zones: [\n          {\n            timezoneName,\n          },\n        ],\n      },\n    };\n    const store = initCalendarStore(options);\n    const date = new TZDate('2022-05-22T06:00:00Z');\n\n    // When\n    const { result } = renderHook(() => useTZConverter(), { store });\n\n    // Then\n    expect(result.current?.(timezoneName, date)?.toDate()).toEqual(\n      new TZDate('2022-05-22T00:00:00Z').toDate()\n    );\n  });\n});\n\ndescribe('When it does not have customOffsetCalculator', () => {\n  it('should convert TZDate following Intl.DateTimeFormat', () => {\n    // Given\n    const timezoneName = 'America/New_York';\n    const options: Options = {\n      timezone: {\n        zones: [\n          {\n            timezoneName,\n          },\n        ],\n      },\n    };\n    const store = initCalendarStore(options);\n    const date = new TZDate('2022-05-22T06:00:00Z');\n\n    // When\n    const { result } = renderHook(() => useTZConverter(), { store });\n\n    // Then\n    expect(result.current?.(timezoneName, date)?.toDate()).toEqual(\n      new TZDate('2022-05-22T02:00:00Z').toDate() // in EDT (UTC -4)\n    );\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/hooks/timezone/useTZConverter.ts",
    "content": "import { useCallback } from 'preact/hooks';\n\nimport { useStore } from '@src/contexts/calendarStore';\nimport { customOffsetCalculatorSelector } from '@src/selectors/timezone';\nimport TZDate from '@src/time/date';\nimport { isPresent } from '@src/utils/type';\n\nexport function useTZConverter() {\n  const customOffsetCalculator = useStore(customOffsetCalculatorSelector);\n\n  const hasCustomOffsetCalculator = isPresent(customOffsetCalculator);\n\n  return useCallback(\n    (timezoneName: string, tzDate: TZDate = new TZDate()) =>\n      tzDate.tz(\n        hasCustomOffsetCalculator\n          ? customOffsetCalculator(timezoneName, tzDate.getTime())\n          : timezoneName\n      ),\n    [customOffsetCalculator, hasCustomOffsetCalculator]\n  );\n}\n"
  },
  {
    "path": "apps/calendar/src/index.ts",
    "content": "import Calendar from '@src/factory/calendar';\nimport Day from '@src/factory/day';\nimport Month from '@src/factory/month';\nimport Week from '@src/factory/week';\nimport TZDate from '@src/time/date';\n\nimport type { ExternalEventTypes } from '@t/eventBus';\nimport type { EventObjectWithDefaultValues } from '@t/events';\nimport type { Options } from '@t/options';\n\nexport default Calendar;\n\nexport { Day, Month, TZDate, Week };\nexport type { EventObjectWithDefaultValues as EventObject, ExternalEventTypes, Options };\n"
  },
  {
    "path": "apps/calendar/src/jest.d.ts",
    "content": "import type TZDate from '@src/time/date';\n\ndeclare global {\n  namespace jest {\n    interface Matchers<R> {\n      toEqualMatricesTitle(expected: Record<string, any>): R;\n      toEqualMatricesTop(expected: Record<string, any>): R;\n      toEqualUIModelByTitle(expected: Record<string, string[]>): R;\n      toBeSameDate(expected: number | string | TZDate | Date): R;\n    }\n  }\n}\n"
  },
  {
    "path": "apps/calendar/src/model/eventModel.spec.ts",
    "content": "import { advanceTo } from 'jest-date-mock';\n\nimport EventModel from '@src/model/eventModel';\nimport TZDate from '@src/time/date';\n\nimport type { EventObject } from '@t/events';\n\ndescribe('model/event basic', () => {\n  let event: EventModel;\n\n  beforeEach(() => {\n    event = new EventModel();\n  });\n\n  it('creation', () => {\n    expect(event.isAllday).toBe(false);\n  });\n\n  describe('init()', () => {\n    beforeEach(() => {\n      advanceTo(new Date('2015/05/01 00:00:00').getTime());\n    });\n\n    it('initialize event with supplied options.', () => {\n      const expected = {\n        id: '123',\n        title: 'Go home',\n        isAllday: false,\n        start: new TZDate('2015-05-01T00:00:00'),\n        end: new TZDate('2015-05-02T00:00:00'),\n      };\n\n      event.init({\n        id: '123',\n        title: 'Go home',\n        isAllday: false,\n        start: '2015-05-01T00:00:00',\n        end: '2015-05-02T00:00:00',\n      });\n\n      expect(event).toMatchObject(expected);\n    });\n  });\n\n  describe('equals()', () => {\n    let event2: EventModel;\n\n    beforeEach(() => {\n      event = new EventModel();\n      event2 = new EventModel();\n    });\n\n    it(\"return true when event's property are same\", () => {\n      event.title = 'dance';\n      event2.title = 'dance';\n      event.isAllday = true;\n      event2.isAllday = true;\n      event.start = new TZDate('2015/05/01');\n      event2.start = new TZDate('2015/05/01');\n      event.end = new TZDate('2015/05/02');\n      event2.end = new TZDate('2015/05/02');\n\n      expect(event.equals(event2)).toBe(true);\n    });\n\n    it('return false when title is not equals.', () => {\n      event.title = 'meeting';\n      event2.title = 'working';\n\n      expect(event.equals(event2)).toBe(false);\n    });\n\n    it('return false when two event has different all day flags.', () => {\n      event.title = 'dance';\n      event2.title = 'dance';\n      event.isAllday = true;\n      event2.isAllday = false;\n\n      expect(event.equals(event2)).toBe(false);\n    });\n\n    it('return false when two event has different start or end.', () => {\n      event.title = 'dance';\n      event2.title = 'dance';\n      event.isAllday = true;\n      event2.isAllday = true;\n      event.start = new TZDate('2015/05/01');\n      event2.start = new TZDate('2015/04/01');\n\n      expect(event.equals(event2)).toBe(false);\n\n      event2.start = new TZDate('2015/05/01');\n\n      event.end = new TZDate('2015/06/01');\n      event2.end = new TZDate('2015/07/01');\n\n      expect(event.equals(event2)).toBe(false);\n    });\n  });\n\n  describe('duration()', () => {\n    beforeEach(() => {\n      event.start = new TZDate('2015-09-25T05:00:00');\n      event.end = new TZDate('2015-09-26T05:00:00');\n    });\n\n    it('can calculate duration between start and end.', () => {\n      expect(+Number(event.duration())).toBe(+Number(new TZDate('1970-01-02T00:00:00Z')));\n    });\n\n    it('return 24 hours when event is all day event.', () => {\n      event.isAllday = true;\n\n      expect(+Number(event.duration())).toBe(+Number(new TZDate('1970-01-02T23:59:59.999Z')));\n    });\n  });\n\n  describe('new EventModel()', () => {\n    it('create event model instance from data object.', () => {\n      const mockEventObject = {\n        title: 'hunting',\n        isAllday: true,\n        start: new TZDate('2015/05/02'),\n        end: new TZDate('2015/05/02'),\n      };\n      mockEventObject.start.setHours(0, 0, 0);\n      mockEventObject.end.setHours(23, 59, 59);\n\n      const mockEventModel = new EventModel(mockEventObject);\n\n      expect(mockEventModel).toMatchObject(mockEventObject);\n    });\n  });\n\n  describe('collidesWith()', () => {\n    /**\n     * type - A\n     * |---|\n     * | A |---|\n     * |---| B |\n     *     |---|\n     *\n     * type - B\n     *     |---|\n     * |---| B |\n     * | A |---|\n     * |---|\n     */\n    it('Check type A, B', () => {\n      const a = new EventModel({\n        title: 'A',\n        isAllday: false,\n        start: '2015-05-01T09:30:00',\n        end: '2015-05-01T10:00:00',\n      });\n      const b = new EventModel({\n        title: 'B',\n        isAllday: false,\n        start: '2015-05-01T09:40:00',\n        end: '2015-05-01T10:10:00',\n      });\n\n      expect(a.collidesWith(b)).toBe(true);\n      expect(b.collidesWith(a)).toBe(true);\n    });\n\n    /**\n     * type - C\n     *\n     * |---|\n     * |   |---|\n     * | A | B |\n     * |   |---|\n     * |---|\n     *\n     * type - D\n     *     |---|\n     * |---|   |\n     * | A | B |\n     * |---|   |\n     *     |---|\n     */\n    it('check type C, D', () => {\n      const a = new EventModel({\n        title: 'A',\n        isAllday: false,\n        start: '2015-05-01T09:30:00',\n        end: '2015-05-01T10:00:00',\n      });\n      const b = new EventModel({\n        title: 'B',\n        isAllday: false,\n        start: '2015-05-01T09:00:00',\n        end: '2015-05-01T10:30:00',\n      });\n\n      expect(a.collidesWith(b)).toBe(true);\n      expect(b.collidesWith(a)).toBe(true);\n    });\n\n    /**\n     * type - E\n     * |---|\n     * | A |\n     * |---|\n     *     |---|\n     *     | B |\n     *     |---|\n     *\n     * type - F\n     *     |---|\n     *     | B |\n     *     |---|\n     * |---|\n     * | A |\n     * |---|\n     */\n    it('check type E, F', () => {\n      const a = new EventModel({\n        title: 'A',\n        isAllday: false,\n        start: '2015-05-01T09:30:00',\n        end: '2015-05-01T10:00:00',\n      });\n      const b = new EventModel({\n        title: 'B',\n        isAllday: false,\n        start: '2015-05-01T10:00:00',\n        end: '2015-05-01T10:30:00',\n      });\n\n      expect(a.collidesWith(b)).toBe(false);\n      expect(b.collidesWith(a)).toBe(false);\n    });\n\n    /**\n     * type - G\n     * |---|\n     * | A |\n     * |---|\n     *\n     *     |---|\n     *     | B |\n     *     |---|\n     *\n     * type - H\n     *     |---|\n     *     | B |\n     *     |---|\n     *\n     * |---|\n     * | A |\n     * |---|\n     */\n    it('check type G, H', () => {\n      const a = new EventModel({\n        title: 'A',\n        isAllday: false,\n        start: '2015-05-01T09:30:00',\n        end: '2015-05-01T09:50:00',\n      });\n      const b = new EventModel({\n        title: 'B',\n        isAllday: false,\n        start: '2015-05-01T10:10:00',\n        end: '2015-05-01T10:30:00',\n      });\n\n      expect(a.collidesWith(b)).toBe(false);\n      expect(b.collidesWith(a)).toBe(false);\n    });\n  });\n});\n\ndescribe('model/EventModel advanced', () => {\n  let eventData: EventObject[];\n\n  beforeEach(() => {\n    eventData = [\n      {\n        title: '스크럼',\n        category: 'time',\n        dueDateClass: '',\n        start: '2015-10-26T09:40:00',\n        end: '2015-10-26T10:00:00',\n      },\n      {\n        title: '[홍길동]연차',\n        category: 'allday',\n        dueDateClass: '',\n        start: '2015-10-26T00:00:00',\n        end: '2015-10-26T23:59:59',\n      },\n      {\n        title: '테스트 마일스톤1',\n        category: 'milestone',\n        dueDateClass: '',\n        start: '',\n        end: '2015-10-26T23:59:59',\n      },\n      {\n        title: '테스트 업무',\n        category: 'task',\n        dueDateClass: 'morning',\n        start: '',\n        end: '2015-10-26T23:59:59',\n      },\n    ];\n  });\n\n  it('factory function (create())', () => {\n    let e = new EventModel(eventData[0]);\n\n    let expected: EventObject = {\n      title: '스크럼',\n      category: 'time',\n      dueDateClass: '',\n      isAllday: false,\n      start: new TZDate('2015-10-26T09:40:00'),\n      end: new TZDate('2015-10-26T10:00:00'),\n    };\n\n    expect(e).toMatchObject(expected);\n\n    e = new EventModel(eventData[1]);\n    expected = {\n      title: '[홍길동]연차',\n      category: 'allday',\n      dueDateClass: '',\n      isAllday: true,\n      start: new TZDate(2015, 9, 26),\n      end: new TZDate(2015, 9, 26, 23, 59, 59),\n    };\n\n    expect(e).toMatchObject(expected);\n\n    e = new EventModel(eventData[2]);\n    expected = {\n      title: '테스트 마일스톤1',\n      category: 'milestone',\n      dueDateClass: '',\n      isAllday: false,\n      start: new TZDate('2015-10-26T23:59:59'),\n      end: new TZDate('2015-10-26T23:59:59'),\n    };\n\n    expect(e).toMatchObject(expected);\n\n    e = new EventModel(eventData[3]);\n    expected = {\n      title: '테스트 업무',\n      category: 'task',\n      dueDateClass: 'morning',\n      isAllday: false,\n      start: new TZDate('2015-10-26T23:59:59'),\n      end: new TZDate('2015-10-26T23:59:59'),\n    };\n\n    expect(e).toMatchObject(expected);\n  });\n\n  it('raw data', () => {\n    const raw: {\n      hello: string;\n      hello2?: string;\n    } = {\n      hello: 'world',\n    };\n    const e = new EventModel({\n      title: '굿',\n      category: 'task',\n      dueDateClass: 'morning',\n      isAllday: false,\n      start: new TZDate('2015-10-26T23:59:59'),\n      end: new TZDate('2015-10-26T23:59:59'),\n      raw,\n    });\n\n    expect(e.raw).toMatchObject({ hello: 'world' });\n\n    raw.hello2 = 'good';\n\n    expect(e.raw).toMatchObject({ hello: 'world', hello2: 'good' });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/model/eventModel.ts",
    "content": "import { collidesWith } from '@src/helpers/events';\nimport EventUIModel from '@src/model/eventUIModel';\nimport TZDate from '@src/time/date';\nimport { compare, MS_PER_DAY, parse, toEndOfDay, toStartOfDay } from '@src/time/datetime';\nimport { stamp } from '@src/utils/stamp';\nimport { isString } from '@src/utils/type';\n\nimport type {\n  DateType,\n  EventCategory,\n  EventObject,\n  EventObjectWithDefaultValues,\n  EventState,\n} from '@t/events';\n\nexport default class EventModel implements Omit<EventObjectWithDefaultValues, '__cid'> {\n  id = '';\n\n  calendarId = '';\n\n  title = '';\n\n  body = '';\n\n  isAllday = false;\n\n  start: TZDate = new TZDate();\n\n  end: TZDate = new TZDate();\n\n  goingDuration = 0;\n\n  comingDuration = 0;\n\n  location = '';\n\n  attendees: string[] = [];\n\n  category: EventCategory = 'time';\n\n  dueDateClass = '';\n\n  recurrenceRule = '';\n\n  state: EventState = 'Busy';\n\n  isVisible = true;\n\n  isPending = false;\n\n  isFocused = false;\n\n  isReadOnly = false;\n\n  isPrivate = false;\n\n  color?: string;\n\n  backgroundColor?: string;\n\n  dragBackgroundColor?: string;\n\n  borderColor?: string;\n\n  customStyle = {};\n\n  raw: any = null;\n\n  /**\n   * whether the event includes multiple dates\n   */\n  hasMultiDates = false;\n\n  constructor(event: EventObject = {}) {\n    // initialize model id\n    stamp(this);\n\n    this.init(event);\n  }\n\n  static schema = {\n    required: ['title'],\n    dateRange: ['start', 'end'],\n  };\n\n  init({\n    id = '',\n    calendarId = '',\n    title = '',\n    body = '',\n    isAllday = false,\n    start = new TZDate(),\n    end = new TZDate(),\n    goingDuration = 0,\n    comingDuration = 0,\n    location = '',\n    attendees = [],\n    category = 'time',\n    dueDateClass = '',\n    recurrenceRule = '',\n    state = 'Busy',\n    isVisible = true,\n    isPending = false,\n    isFocused = false,\n    isReadOnly = false,\n    isPrivate = false,\n    color,\n    backgroundColor,\n    dragBackgroundColor,\n    borderColor,\n    customStyle = {},\n    raw = null,\n  }: EventObject = {}) {\n    this.id = id;\n    this.calendarId = calendarId;\n    this.title = title;\n    this.body = body;\n    this.isAllday = category === 'allday' ? true : isAllday;\n    this.goingDuration = goingDuration;\n    this.comingDuration = comingDuration;\n    this.location = location;\n    this.attendees = attendees;\n    this.category = category;\n    this.dueDateClass = dueDateClass;\n    this.recurrenceRule = recurrenceRule;\n    this.state = state;\n    this.isVisible = isVisible;\n    this.isPending = isPending;\n    this.isFocused = isFocused;\n    this.isReadOnly = isReadOnly;\n    this.isPrivate = isPrivate;\n    this.color = color;\n    this.backgroundColor = backgroundColor;\n    this.dragBackgroundColor = dragBackgroundColor;\n    this.borderColor = borderColor;\n    this.customStyle = customStyle;\n    this.raw = raw;\n\n    if (this.isAllday) {\n      this.setAlldayPeriod(start, end);\n    } else {\n      this.setTimePeriod(start, end);\n    }\n\n    if (category === 'milestone' || category === 'task') {\n      this.start = new TZDate(this.end);\n    }\n  }\n\n  setAlldayPeriod(start?: DateType, end?: DateType) {\n    // If it is an all-day, only the date information of the string is used.\n    let startedAt: TZDate;\n    let endedAt: TZDate;\n\n    if (isString(start)) {\n      startedAt = parse(start.substring(0, 10));\n    } else {\n      startedAt = new TZDate(start || Date.now());\n    }\n\n    if (isString(end)) {\n      endedAt = parse(end.substring(0, 10));\n    } else {\n      endedAt = new TZDate(end || this.start);\n    }\n\n    this.start = startedAt;\n    this.start.setHours(0, 0, 0);\n    this.end = (endedAt as TZDate) || new TZDate(this.start);\n    this.end.setHours(23, 59, 59);\n  }\n\n  setTimePeriod(start?: DateType, end?: DateType) {\n    this.start = new TZDate(start || Date.now());\n    this.end = new TZDate(end || this.start);\n\n    if (!end) {\n      this.end.setMinutes(this.end.getMinutes() + 30);\n    }\n\n    // if over 24 hours\n    this.hasMultiDates = this.end.getTime() - this.start.getTime() > MS_PER_DAY;\n  }\n\n  /**\n   * @returns {TZDate} render start date.\n   */\n  getStarts() {\n    return this.start;\n  }\n\n  /**\n   * @returns {TZDate} render end date.\n   */\n  getEnds() {\n    return this.end;\n  }\n\n  /**\n   * @returns {number} instance unique id.\n   */\n  cid(): number {\n    return stamp(this);\n  }\n\n  /**\n   * Check two  are equals (means title, isAllday, start, end are same)\n   * @param {EventModel}  event model instance to compare.\n   * @returns {boolean} Return false when not same.\n   */\n  // eslint-disable-next-line complexity\n  equals(event: EventModel) {\n    if (this.id !== event.id) {\n      return false;\n    }\n\n    if (this.title !== event.title) {\n      return false;\n    }\n\n    if (this.body !== event.body) {\n      return false;\n    }\n\n    if (this.isAllday !== event.isAllday) {\n      return false;\n    }\n\n    if (compare(this.getStarts(), event.getStarts()) !== 0) {\n      return false;\n    }\n\n    if (compare(this.getEnds(), event.getEnds()) !== 0) {\n      return false;\n    }\n\n    if (this.color !== event.color) {\n      return false;\n    }\n\n    if (this.backgroundColor !== event.backgroundColor) {\n      return false;\n    }\n\n    if (this.dragBackgroundColor !== event.dragBackgroundColor) {\n      return false;\n    }\n\n    if (this.borderColor !== event.borderColor) {\n      return false;\n    }\n\n    return true;\n  }\n\n  /**\n   * return duration between start and end.\n   * @returns {number} duration milliseconds (UTC)\n   */\n  duration(): number {\n    const start = Number(this.getStarts());\n    const end = Number(this.getEnds());\n    let duration: number;\n\n    if (this.isAllday) {\n      duration = Number(toEndOfDay(end)) - Number(toStartOfDay(start));\n    } else {\n      duration = end - start;\n    }\n\n    return duration;\n  }\n\n  valueOf() {\n    return this;\n  }\n\n  /**\n   * Returns true if the given EventModel coincides with the same time as the\n   * calling EventModel.\n   * @param {EventModel | EventUIModel} event The other event to compare with this EventModel.\n   * @param {boolean = true} usingTravelTime When calculating collision, whether to calculate with travel time.\n   * @returns {boolean} If the other event occurs within the same time as the first object.\n   */\n  collidesWith(event: EventModel | EventUIModel, usingTravelTime = true) {\n    event = event instanceof EventUIModel ? event.model : event;\n\n    return collidesWith({\n      start: Number(this.getStarts()),\n      end: Number(this.getEnds()),\n      targetStart: Number(event.getStarts()),\n      targetEnd: Number(event.getEnds()),\n      goingDuration: this.goingDuration,\n      comingDuration: this.comingDuration,\n      targetGoingDuration: event.goingDuration,\n      targetComingDuration: event.comingDuration,\n      usingTravelTime, // Daygrid does not use travelTime, TimeGrid uses travelTime.\n    });\n  }\n\n  toEventObject(): EventObjectWithDefaultValues {\n    return {\n      id: this.id,\n      calendarId: this.calendarId,\n      __cid: this.cid(),\n      title: this.title,\n      body: this.body,\n      isAllday: this.isAllday,\n      start: this.start,\n      end: this.end,\n      goingDuration: this.goingDuration,\n      comingDuration: this.comingDuration,\n      location: this.location,\n      attendees: this.attendees,\n      category: this.category,\n      dueDateClass: this.dueDateClass,\n      recurrenceRule: this.recurrenceRule,\n      state: this.state,\n      isVisible: this.isVisible,\n      isPending: this.isPending,\n      isFocused: this.isFocused,\n      isReadOnly: this.isReadOnly,\n      isPrivate: this.isPrivate,\n      color: this.color,\n      backgroundColor: this.backgroundColor,\n      dragBackgroundColor: this.dragBackgroundColor,\n      borderColor: this.borderColor,\n      customStyle: this.customStyle,\n      raw: this.raw,\n    };\n  }\n\n  getColors() {\n    return {\n      color: this.color,\n      backgroundColor: this.backgroundColor,\n      dragBackgroundColor: this.dragBackgroundColor,\n      borderColor: this.borderColor,\n    };\n  }\n}\n\n// export function isBackgroundEvent({ model }: EventUIModel) {\n//   return model.category === 'background';\n// }\n\nexport function isTimeEvent({ model }: EventUIModel) {\n  const { category, isAllday, hasMultiDates } = model;\n\n  return category === 'time' && !isAllday && !hasMultiDates;\n}\n"
  },
  {
    "path": "apps/calendar/src/model/eventUIModel.ts",
    "content": "import { collidesWith } from '@src/helpers/events';\nimport type EventModel from '@src/model/eventModel';\nimport TZDate from '@src/time/date';\nimport { pick } from '@src/utils/object';\n\ninterface EventUIProps {\n  top: number;\n  left: number;\n  width: number;\n  height: number;\n  exceedLeft: boolean;\n  exceedRight: boolean;\n  croppedStart: boolean;\n  croppedEnd: boolean;\n  goingDurationHeight: number;\n  modelDurationHeight: number;\n  comingDurationHeight: number;\n  duplicateEvents: EventUIModel[];\n  duplicateEventIndex: number;\n  duplicateStarts?: TZDate;\n  duplicateEnds?: TZDate;\n  duplicateLeft: string;\n  duplicateWidth: string;\n  collapse: boolean;\n  isMain: boolean;\n}\n\nconst eventUIPropsKey: (keyof EventUIProps)[] = [\n  'top',\n  'left',\n  'width',\n  'height',\n  'exceedLeft',\n  'exceedRight',\n  'croppedStart',\n  'croppedEnd',\n  'goingDurationHeight',\n  'modelDurationHeight',\n  'comingDurationHeight',\n  'duplicateEvents',\n  'duplicateEventIndex',\n  'duplicateStarts',\n  'duplicateEnds',\n  'duplicateLeft',\n  'duplicateWidth',\n  'collapse',\n  'isMain',\n];\n\n/**\n * Set of UI-related properties for calendar event.\n * @class\n * @param {EventModel} event EventModel instance.\n */\nexport default class EventUIModel implements EventUIProps {\n  model: EventModel;\n\n  top = 0;\n\n  // If it is one of duplicate events, represents the left value of a group of duplicate events.\n  left = 0;\n\n  // If it is one of duplicate events, represents the width value of a group of duplicate events.\n  width = 0;\n\n  height = 0;\n\n  /**\n   * represent render start date used at rendering.\n   *\n   * if set null then use model's 'start' property.\n   * @type {TZDate}\n   */\n  renderStarts?: TZDate;\n\n  /**\n   * represent render end date used at rendering.\n   *\n   * if set null then use model's 'end' property.\n   * @type {TZDate}\n   */\n  renderEnds?: TZDate;\n\n  /**\n   * whether the actual start-date is before the render-start-date\n   * @type {boolean}\n   */\n  exceedLeft = false;\n\n  /**\n   * whether the actual end-date is after the render-end-date\n   * @type {boolean}\n   */\n  exceedRight = false;\n\n  /**\n   * whether the actual start-date is before the render-start-date for column\n   * @type {boolean}\n   */\n  croppedStart = false;\n\n  /**\n   * whether the actual end-date is after the render-end-date for column\n   * @type {boolean}\n   */\n  croppedEnd = false;\n\n  /**\n   * @type {number} percent\n   */\n  goingDurationHeight = 0;\n\n  /**\n   * @type {number} percent\n   */\n  modelDurationHeight = 100;\n\n  /**\n   * @type {number} percent\n   */\n  comingDurationHeight = 0;\n\n  /**\n   * the sorted list of duplicate events.\n   * @type {EventUIModel[]}\n   */\n  duplicateEvents: EventUIModel[] = [];\n\n  /**\n   * the index of this event among the duplicate events.\n   * @type {number}\n   */\n  duplicateEventIndex = -1;\n\n  /**\n   * represent the start date of a group of duplicate events.\n   *\n   * the earliest value among the duplicate events' starts and going durations.\n   * @type {TZDate}\n   */\n  duplicateStarts?: TZDate;\n\n  /**\n   * represent the end date of a group of duplicate events.\n   *\n   * the latest value among the duplicate events' ends and coming durations.\n   * @type {TZDate}\n   */\n  duplicateEnds?: TZDate;\n\n  /**\n   * represent the left value of a duplicate event.\n   * ex) calc(50% - 24px), calc(50%), ...\n   *\n   * @type {string}\n   */\n  duplicateLeft = '';\n\n  /**\n   * represent the width value of a duplicate event.\n   * ex) calc(50% - 24px), 9px, ...\n   *\n   * @type {string}\n   */\n  duplicateWidth = '';\n\n  /**\n   * whether the event is collapsed or not among the duplicate events.\n   * @type {boolean}\n   */\n  collapse = false;\n\n  /**\n   * whether the event is main or not.\n   * The main event is expanded on the initial rendering.\n   * @type {boolean}\n   */\n  isMain = false;\n\n  constructor(event: EventModel) {\n    this.model = event;\n  }\n\n  getUIProps(): EventUIProps {\n    return pick(this, ...eventUIPropsKey);\n  }\n\n  setUIProps(props: Partial<EventUIProps>) {\n    Object.assign(this, props);\n  }\n\n  /**\n   * return renderStarts property to render properly when specific event that exceed rendering date range.\n   *\n   * if renderStarts is not set. return model's start property.\n   */\n  getStarts(): TZDate {\n    if (this.renderStarts) {\n      return this.renderStarts;\n    }\n\n    return this.model.getStarts();\n  }\n\n  /**\n   * return renderStarts property to render properly when specific event that exceed rendering date range.\n   *\n   * if renderEnds is not set. return model's end property.\n   */\n  getEnds(): TZDate {\n    if (this.renderEnds) {\n      return this.renderEnds;\n    }\n\n    return this.model.getEnds();\n  }\n\n  /**\n   * @returns {number} unique number for model.\n   */\n  cid() {\n    return this.model.cid();\n  }\n\n  /**\n   * Shadowing valueOf method for event sorting.\n   */\n  valueOf(): EventModel {\n    return this.model;\n  }\n\n  /**\n   * Link duration method\n   * @returns {number} EventModel#duration result.\n   */\n  duration() {\n    return this.model.duration();\n  }\n\n  collidesWith(uiModel: EventModel | EventUIModel, usingTravelTime = true) {\n    const infos: { start: TZDate; end: TZDate; goingDuration: number; comingDuration: number }[] =\n      [];\n    [this, uiModel].forEach((event) => {\n      const isDuplicateEvent = event instanceof EventUIModel && event.duplicateEvents.length > 0;\n\n      if (isDuplicateEvent) {\n        infos.push({\n          start: event.duplicateStarts as TZDate,\n          end: event.duplicateEnds as TZDate,\n          goingDuration: 0,\n          comingDuration: 0,\n        });\n      } else {\n        infos.push({\n          start: event.getStarts(),\n          end: event.getEnds(),\n          goingDuration: event.valueOf().goingDuration,\n          comingDuration: event.valueOf().comingDuration,\n        });\n      }\n    });\n    const [thisInfo, targetInfo] = infos;\n\n    return collidesWith({\n      start: thisInfo.start.getTime(),\n      end: thisInfo.end.getTime(),\n      targetStart: targetInfo.start.getTime(),\n      targetEnd: targetInfo.end.getTime(),\n      goingDuration: thisInfo.goingDuration,\n      comingDuration: thisInfo.comingDuration,\n      targetGoingDuration: targetInfo.goingDuration,\n      targetComingDuration: targetInfo.comingDuration,\n      usingTravelTime, // Daygrid does not use travelTime, TimeGrid uses travelTime.\n    });\n  }\n\n  clone() {\n    const eventUIModelProps = this.getUIProps();\n    const clonedEventUIModel = new EventUIModel(this.model);\n    clonedEventUIModel.setUIProps(eventUIModelProps);\n\n    if (this.renderStarts) {\n      clonedEventUIModel.renderStarts = new TZDate(this.renderStarts);\n    }\n\n    if (this.renderEnds) {\n      clonedEventUIModel.renderEnds = new TZDate(this.renderEnds);\n    }\n\n    return clonedEventUIModel;\n  }\n}\n"
  },
  {
    "path": "apps/calendar/src/selectors/index.ts",
    "content": "import type { CalendarState } from '@t/store';\n\nexport function topLevelStateSelector<State, Group extends keyof State>(\n  group: Group\n): (state: State) => State[Group] {\n  return (state: State) => state[group];\n}\n\nexport const popupSelector = topLevelStateSelector<CalendarState, 'popup'>('popup');\nexport const calendarSelector = topLevelStateSelector<CalendarState, 'calendar'>('calendar');\nexport const weekViewLayoutSelector = topLevelStateSelector<CalendarState, 'weekViewLayout'>(\n  'weekViewLayout'\n);\nexport const templateSelector = topLevelStateSelector<CalendarState, 'template'>('template');\nexport const viewSelector = topLevelStateSelector<CalendarState, 'view'>('view');\nexport const optionsSelector = topLevelStateSelector<CalendarState, 'options'>('options');\nexport const dndSelector = topLevelStateSelector<CalendarState, 'dnd'>('dnd');\n"
  },
  {
    "path": "apps/calendar/src/selectors/options.ts",
    "content": "import type { CalendarState } from '@t/store';\n\nexport const monthVisibleEventCountSelector = (state: CalendarState) =>\n  state.options.month.visibleEventCount ?? 6;\n\nexport const showNowIndicatorOptionSelector = (state: CalendarState) =>\n  state.options.week.showNowIndicator;\n\nexport const showTimezoneCollapseButtonOptionSelector = (state: CalendarState) =>\n  state.options.week.showTimezoneCollapseButton ?? false;\n\nexport const timezonesCollapsedOptionSelector = (state: CalendarState) =>\n  state.options.week.timezonesCollapsed ?? false;\n"
  },
  {
    "path": "apps/calendar/src/selectors/popup.ts",
    "content": "import { PopupType } from '@src/slices/popup';\n\nimport type { CalendarState, PopupParamMap } from '@t/store';\n\nexport const eventFormPopupParamSelector = (state: CalendarState) => {\n  return state.popup[PopupType.Form] as PopupParamMap[PopupType.Form];\n};\n\nexport const eventDetailPopupParamSelector = (state: CalendarState) => {\n  return state.popup[PopupType.Detail] as PopupParamMap[PopupType.Detail];\n};\n\nexport const seeMorePopupParamSelector = (state: CalendarState) => {\n  return state.popup[PopupType.SeeMore] as PopupParamMap[PopupType.SeeMore];\n};\n"
  },
  {
    "path": "apps/calendar/src/selectors/theme.ts",
    "content": "import { topLevelStateSelector } from '@src/selectors';\n\nimport type { ThemeState } from '@t/theme';\n\n/**\n * Selectors for the theme state.\n * Use selectors with `useTheme` hooks only.\n */\nexport const commonThemeSelector = topLevelStateSelector<ThemeState, 'common'>('common');\nexport const weekThemeSelector = topLevelStateSelector<ThemeState, 'week'>('week');\nexport const monthThemeSelector = topLevelStateSelector<ThemeState, 'month'>('month');\n\nexport const weekDayGridLeftSelector = (theme: ThemeState) => theme.week.dayGridLeft;\nexport const weekTimeGridLeftSelector = (theme: ThemeState) => theme.week.timeGridLeft;\n\nexport const monthMoreViewSelector = (theme: ThemeState) => theme.month.moreView;\nexport const monthGridCellSelector = (theme: ThemeState) => theme.month.gridCell;\n"
  },
  {
    "path": "apps/calendar/src/selectors/timezone.ts",
    "content": "import type { CalendarState } from '@t/store';\n\nexport const primaryTimezoneSelector = (state: CalendarState) =>\n  state.options?.timezone?.zones?.[0]?.timezoneName ?? 'Local';\n\nexport const customOffsetCalculatorSelector = (state: CalendarState) =>\n  state.options?.timezone?.customOffsetCalculator;\n\nexport const timezonesSelector = (state: CalendarState) => state.options.timezone.zones ?? [];\n"
  },
  {
    "path": "apps/calendar/src/setupTests.ts",
    "content": "// reference: https://github.com/kkomelin/isomorphic-dompurify/issues/91#issuecomment-1012645198\nimport '@testing-library/jest-dom';\n\nimport { TextDecoder, TextEncoder } from 'util';\n\nglobal.TextEncoder = TextEncoder;\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore\nglobal.TextDecoder = TextDecoder;\n\nprocess.env.TZ = 'Asia/Seoul';\n"
  },
  {
    "path": "apps/calendar/src/slices/calendar.ts",
    "content": "import produce from 'immer';\n\nimport {\n  clearEvents,\n  createEventCollection,\n  createEvents,\n  deleteEvent,\n  updateEvent,\n} from '@src/controller/base';\nimport type EventModel from '@src/model/eventModel';\n\nimport type { CalendarData, EventObject } from '@t/events';\nimport type { CalendarColor, CalendarInfo } from '@t/options';\nimport type { CalendarState, CalendarStore, SetState } from '@t/store';\n\nexport type CalendarSlice = { calendar: CalendarData };\n\ntype UpdateEventParams = { event: EventModel; eventData: EventObject };\n\nexport type CalendarDispatchers = {\n  createEvents: (events: EventObject[]) => void;\n  updateEvent: (params: UpdateEventParams) => void;\n  deleteEvent: (event: EventModel) => void;\n  clearEvents: () => void;\n  setCalendars: (calendars: CalendarInfo[]) => void;\n  setCalendarColor: (calendarId: string, colorOptions: CalendarColor) => void;\n  setCalendarVisibility: (calendarIds: string[], isVisible: boolean) => void;\n};\n\nexport function createCalendarSlice(calendars: CalendarInfo[] = []): CalendarSlice {\n  return {\n    calendar: {\n      calendars,\n      events: createEventCollection(),\n      idsOfDay: {},\n    },\n  };\n}\n\nexport function createCalendarDispatchers(set: SetState<CalendarStore>): CalendarDispatchers {\n  return {\n    createEvents: (events) =>\n      set(\n        produce<CalendarState>((state) => {\n          createEvents(state.calendar as Parameters<typeof createEvents>[0], events);\n        })\n      ),\n    updateEvent: ({ event, eventData }) =>\n      set(\n        produce<CalendarState>((state) => {\n          updateEvent(\n            state.calendar as Parameters<typeof updateEvent>[0],\n            event.id,\n            event.calendarId,\n            eventData\n          );\n        })\n      ),\n    deleteEvent: (event) =>\n      set(\n        produce<CalendarState>((state) => {\n          deleteEvent(state.calendar as Parameters<typeof deleteEvent>[0], event);\n        })\n      ),\n    clearEvents: () =>\n      set(\n        produce<CalendarState>((state) => {\n          clearEvents(state.calendar as Parameters<typeof clearEvents>[0]);\n        })\n      ),\n    setCalendars: (calendars) =>\n      set(\n        produce<CalendarState>((state) => {\n          state.calendar.calendars = calendars;\n        })\n      ),\n    setCalendarColor: (calendarId, colorOptions) =>\n      set(\n        produce<CalendarState>((state) => {\n          const calendars = state.calendar.calendars.map((calendar) => {\n            if (calendar.id === calendarId) {\n              return {\n                ...calendar,\n                ...colorOptions,\n              };\n            }\n\n            return calendar;\n          });\n          const events = state.calendar.events.toArray().map((event) => {\n            if (event.calendarId === calendarId) {\n              event.color = colorOptions.color ?? event.color;\n              event.backgroundColor = colorOptions.backgroundColor ?? event.backgroundColor;\n              event.borderColor = colorOptions.borderColor ?? event.borderColor;\n              event.dragBackgroundColor =\n                colorOptions.dragBackgroundColor ?? event.dragBackgroundColor;\n            }\n\n            return event;\n          });\n          const collection = createEventCollection<EventModel>(...events);\n\n          state.calendar.calendars = calendars;\n          state.calendar.events = collection;\n        })\n      ),\n    setCalendarVisibility: (calendarIds, isVisible) =>\n      set(\n        produce<CalendarState>((state) => {\n          const events = state.calendar.events.toArray();\n\n          state.calendar.events = createEventCollection<EventModel>(\n            ...events.map((event) => {\n              if (calendarIds.includes(event.calendarId)) {\n                event.isVisible = isVisible;\n              }\n\n              return event;\n            })\n          );\n        })\n      ),\n  };\n}\n"
  },
  {
    "path": "apps/calendar/src/slices/dnd.ts",
    "content": "import produce from 'immer';\n\nimport type EventUIModel from '@src/model/eventUIModel';\n\nimport type { DraggingTypes } from '@t/drag';\nimport type { CalendarState, CalendarStore, SetState } from '@t/store';\n\nexport enum DraggingState {\n  IDLE,\n  INIT,\n  DRAGGING,\n  CANCELED,\n}\n\nexport interface DndSlice {\n  dnd: {\n    draggingItemType: DraggingTypes | null;\n    draggingState: DraggingState;\n    initX: number | null;\n    initY: number | null;\n    x: number | null;\n    y: number | null;\n    draggingEventUIModel: EventUIModel | null;\n  };\n}\n\nexport interface DndDispatchers {\n  initDrag: (initState: Pick<DndSlice['dnd'], 'initX' | 'initY' | 'draggingItemType'>) => void;\n  setDragging: (newState: Partial<Omit<DndSlice['dnd'], 'draggingState'>>) => void;\n  cancelDrag: () => void;\n  reset: () => void;\n  setDraggingEventUIModel: (eventUIModel: EventUIModel | null) => void;\n}\n\nexport function createDndSlice(): DndSlice {\n  return {\n    dnd: {\n      draggingItemType: null,\n      draggingState: DraggingState.IDLE,\n      initX: null,\n      initY: null,\n      x: null,\n      y: null,\n      draggingEventUIModel: null,\n    },\n  };\n}\n\nexport function createDndDispatchers(set: SetState<CalendarStore>): DndDispatchers {\n  return {\n    initDrag: (initState) => {\n      set(\n        produce<CalendarState>((state) => {\n          state.dnd = {\n            ...state.dnd,\n            ...initState,\n            draggingState: DraggingState.INIT,\n          };\n        })\n      );\n    },\n    setDragging: (newState) => {\n      set(\n        produce<CalendarState>((state) => {\n          state.dnd = {\n            ...state.dnd,\n            ...newState,\n            draggingState: DraggingState.DRAGGING,\n          };\n        })\n      );\n    },\n    cancelDrag: () => {\n      set(\n        produce<CalendarState>((state) => {\n          state.dnd = createDndSlice().dnd;\n          state.dnd.draggingState = DraggingState.CANCELED;\n        })\n      );\n    },\n    reset: () => {\n      set(\n        produce<CalendarState>((state) => {\n          state.dnd = createDndSlice().dnd;\n        })\n      );\n    },\n    setDraggingEventUIModel: (eventUIModel) => {\n      set(\n        produce<CalendarState>((state) => {\n          state.dnd.draggingEventUIModel = eventUIModel?.clone() ?? null;\n        })\n      );\n    },\n  };\n}\n"
  },
  {
    "path": "apps/calendar/src/slices/gridSelection.ts",
    "content": "import produce from 'immer';\n\nimport type { GridSelectionData } from '@t/components/gridSelection';\nimport type { CalendarState, CalendarStore, SetState } from '@t/store';\n\nexport type GridSelectionSlice = {\n  gridSelection: {\n    dayGridMonth: GridSelectionData | null;\n    dayGridWeek: GridSelectionData | null;\n    timeGrid: GridSelectionData | null;\n    accumulated: {\n      dayGridMonth: GridSelectionData[] | [];\n    };\n  };\n};\n\nexport type GridSelectionType = Exclude<keyof GridSelectionSlice['gridSelection'], 'accumulated'>;\n\nexport type GridSelectionDispatchers = {\n  setGridSelection: (type: GridSelectionType, gridSelection: GridSelectionData | null) => void;\n  addGridSelection: (type: GridSelectionType, gridSelection: GridSelectionData | null) => void;\n  clearAll: () => void;\n};\n\nexport function createGridSelectionSlice(): GridSelectionSlice {\n  return {\n    gridSelection: {\n      dayGridMonth: null,\n      dayGridWeek: null,\n      timeGrid: null,\n      accumulated: {\n        dayGridMonth: [],\n      },\n    },\n  };\n}\n\nexport function createGridSelectionDispatchers(\n  set: SetState<CalendarStore>\n): GridSelectionDispatchers {\n  return {\n    setGridSelection: (type, gridSelection) => {\n      set(\n        produce<CalendarState>((state) => {\n          state.gridSelection[type] = gridSelection;\n        })\n      );\n    },\n    addGridSelection: (type, gridSelection) => {\n      set(\n        produce<CalendarState>((state) => {\n          if (type === 'dayGridMonth' && gridSelection) {\n            state.gridSelection.accumulated[type] = [\n              ...state.gridSelection.accumulated[type],\n              gridSelection,\n            ];\n            state.gridSelection.dayGridMonth = null;\n          }\n        })\n      );\n    },\n    clearAll: () =>\n      set(\n        produce<CalendarState>((state) => {\n          state.gridSelection = createGridSelectionSlice().gridSelection;\n        })\n      ),\n  };\n}\n"
  },
  {
    "path": "apps/calendar/src/slices/layout.ts",
    "content": "import produce from 'immer';\n\nimport { DEFAULT_DUPLICATE_EVENT_CID, DEFAULT_RESIZER_LENGTH } from '@src/constants/layout';\nimport { DEFAULT_PANEL_HEIGHT } from '@src/constants/style';\n\nimport type { CalendarState, CalendarStore, SetState } from '@t/store';\n\nexport type WeekGridRows = 'milestone' | 'task' | 'allday' | 'time' | string;\n\n// @TODO: Change name to layout & merge slice into layout\nexport type WeekViewLayoutSlice = {\n  layout: number;\n  weekViewLayout: {\n    lastPanelType: string | null;\n    dayGridRows: {\n      [row in WeekGridRows]: {\n        height: number;\n      };\n    };\n    selectedDuplicateEventCid: number;\n  };\n};\n\ntype UpdateGridRowHeightParams = { rowName: WeekGridRows; height: number };\ntype UpdateGridRowHeightByDiffParams = { rowName: WeekGridRows; diff: number };\n\nexport type WeekViewLayoutDispatchers = {\n  setLastPanelType: (type: string | null) => void;\n  updateLayoutHeight: (height: number) => void;\n  updateDayGridRowHeight: (params: UpdateGridRowHeightParams) => void;\n  updateDayGridRowHeightByDiff: (params: UpdateGridRowHeightByDiffParams) => void;\n  setSelectedDuplicateEventCid: (cid?: number) => void;\n};\n\nfunction getRestPanelHeight(\n  dayGridRowsState: WeekViewLayoutSlice['weekViewLayout']['dayGridRows'],\n  lastPanelType: string,\n  initHeight: number\n) {\n  return Object.keys(dayGridRowsState).reduce((acc, rowName) => {\n    if (rowName === lastPanelType) {\n      return acc;\n    }\n\n    return acc - dayGridRowsState[rowName].height - DEFAULT_RESIZER_LENGTH;\n  }, initHeight);\n}\n\nexport function createWeekViewLayoutSlice(): WeekViewLayoutSlice {\n  return {\n    layout: 500,\n    weekViewLayout: {\n      lastPanelType: null,\n      dayGridRows: {} as WeekViewLayoutSlice['weekViewLayout']['dayGridRows'],\n      selectedDuplicateEventCid: DEFAULT_DUPLICATE_EVENT_CID,\n    },\n  };\n}\n\nexport function createWeekViewLayoutDispatchers(\n  set: SetState<CalendarStore>\n): WeekViewLayoutDispatchers {\n  return {\n    setLastPanelType: (type) => {\n      set(\n        produce<CalendarState>((state) => {\n          state.weekViewLayout.lastPanelType = type;\n\n          if (type) {\n            state.weekViewLayout.dayGridRows[type].height = getRestPanelHeight(\n              state.weekViewLayout.dayGridRows,\n              type,\n              state.layout\n            );\n          }\n        })\n      );\n    },\n    updateLayoutHeight: (height) =>\n      set(\n        produce<CalendarState>((state) => {\n          const { lastPanelType } = state.weekViewLayout;\n\n          state.layout = height;\n          if (lastPanelType) {\n            state.weekViewLayout.dayGridRows[lastPanelType].height = getRestPanelHeight(\n              state.weekViewLayout.dayGridRows,\n              lastPanelType,\n              height\n            );\n          }\n        })\n      ),\n    updateDayGridRowHeight: ({ rowName, height }) =>\n      set(\n        produce<CalendarState>((state) => {\n          const { lastPanelType } = state.weekViewLayout;\n\n          state.weekViewLayout.dayGridRows[rowName] = { height };\n          if (lastPanelType) {\n            state.weekViewLayout.dayGridRows[lastPanelType].height = getRestPanelHeight(\n              state.weekViewLayout.dayGridRows,\n              lastPanelType,\n              state.layout\n            );\n          }\n        })\n      ),\n    updateDayGridRowHeightByDiff: ({ rowName, diff }) =>\n      set(\n        produce<CalendarState>((state) => {\n          const { lastPanelType } = state.weekViewLayout;\n          const height =\n            state.weekViewLayout.dayGridRows?.[rowName]?.height ?? DEFAULT_PANEL_HEIGHT;\n\n          state.weekViewLayout.dayGridRows[rowName] = { height: height + diff };\n          if (lastPanelType) {\n            state.weekViewLayout.dayGridRows[lastPanelType].height = getRestPanelHeight(\n              state.weekViewLayout.dayGridRows,\n              lastPanelType,\n              state.layout\n            );\n          }\n        })\n      ),\n    setSelectedDuplicateEventCid: (cid) =>\n      set(\n        produce<CalendarState>((state) => {\n          state.weekViewLayout.selectedDuplicateEventCid = cid ?? DEFAULT_DUPLICATE_EVENT_CID;\n        })\n      ),\n  };\n}\n"
  },
  {
    "path": "apps/calendar/src/slices/options.ts",
    "content": "import produce from 'immer';\n\nimport { DEFAULT_DAY_NAMES } from '@src/helpers/dayName';\nimport { compare, Day } from '@src/time/datetime';\nimport { last } from '@src/utils/array';\nimport { mergeObject } from '@src/utils/object';\nimport { isBoolean } from '@src/utils/type';\n\nimport type { EventObject, EventObjectWithDefaultValues } from '@t/events';\nimport type {\n  CollapseDuplicateEventsOptions,\n  GridSelectionOptions,\n  Options,\n  TimezoneOptions,\n} from '@t/options';\nimport type {\n  CalendarMonthOptions,\n  CalendarState,\n  CalendarStore,\n  CalendarWeekOptions,\n  SetState,\n} from '@t/store';\n\nfunction initializeCollapseDuplicateEvents(\n  options: boolean | Partial<CollapseDuplicateEventsOptions>\n): boolean | CollapseDuplicateEventsOptions {\n  if (!options) {\n    return false;\n  }\n\n  const initialCollapseDuplicateEvents = {\n    getDuplicateEvents: (\n      targetEvent: EventObjectWithDefaultValues,\n      events: EventObjectWithDefaultValues[]\n    ) =>\n      events\n        .filter(\n          (event: EventObjectWithDefaultValues) =>\n            event.title === targetEvent.title &&\n            compare(event.start, targetEvent.start) === 0 &&\n            compare(event.end, targetEvent.end) === 0\n        )\n        .sort((a, b) => (a.calendarId > b.calendarId ? 1 : -1)),\n    getMainEvent: (events: EventObjectWithDefaultValues[]) => last(events),\n  };\n\n  if (isBoolean(options)) {\n    return initialCollapseDuplicateEvents;\n  }\n\n  return { ...initialCollapseDuplicateEvents, ...options };\n}\n\nfunction initializeWeekOptions(weekOptions: Options['week'] = {}): CalendarWeekOptions {\n  const week: CalendarWeekOptions = {\n    startDayOfWeek: Day.SUN,\n    dayNames: [],\n    narrowWeekend: false,\n    workweek: false,\n    showNowIndicator: true,\n    showTimezoneCollapseButton: false,\n    timezonesCollapsed: false,\n    hourStart: 0,\n    hourEnd: 24,\n    eventView: true,\n    taskView: true,\n    collapseDuplicateEvents: false,\n    ...weekOptions,\n  };\n\n  week.collapseDuplicateEvents = initializeCollapseDuplicateEvents(week.collapseDuplicateEvents);\n\n  return week;\n}\n\nfunction initializeTimezoneOptions(timezoneOptions: Options['timezone'] = {}): TimezoneOptions {\n  return {\n    zones: [],\n    ...timezoneOptions,\n  };\n}\n\nfunction initializeMonthOptions(monthOptions: Options['month'] = {}): CalendarMonthOptions {\n  const month: CalendarMonthOptions = {\n    dayNames: [],\n    visibleWeeksCount: 0,\n    workweek: false,\n    narrowWeekend: false,\n    startDayOfWeek: Day.SUN,\n    isAlways6Weeks: true,\n    visibleEventCount: 6,\n    ...monthOptions,\n  };\n\n  if (month.dayNames.length === 0) {\n    month.dayNames = DEFAULT_DAY_NAMES.slice() as Exclude<CalendarMonthOptions['dayNames'], []>;\n  }\n\n  return month;\n}\n\nexport function initializeGridSelectionOptions(\n  options: Options['gridSelection']\n): GridSelectionOptions {\n  if (isBoolean(options)) {\n    return {\n      enableDblClick: options,\n      enableClick: options,\n    };\n  }\n\n  return {\n    enableDblClick: true,\n    enableClick: true,\n    ...options,\n  };\n}\n\nconst initialEventFilter = (event: EventObject) => !!event.isVisible;\n\n// TODO: some of options has default values. so it should be `Required` type.\n// But it needs a complex type such as `DeepRequired`.\n// maybe leveraging library like `ts-essential` might be helpful.\nexport type OptionsSlice = {\n  options: Omit<Required<Options>, 'template' | 'calendars' | 'theme'> & {\n    gridSelection: GridSelectionOptions;\n  };\n};\n\nexport type OptionsDispatchers = {\n  setOptions: (newOptions: Partial<OptionsSlice['options']>) => void;\n};\n\n// eslint-disable-next-line complexity\nexport function createOptionsSlice(options: Options = {}): OptionsSlice {\n  return {\n    options: {\n      defaultView: options.defaultView ?? 'week',\n      useFormPopup: options.useFormPopup ?? false,\n      useDetailPopup: options.useDetailPopup ?? false,\n      isReadOnly: options.isReadOnly ?? false,\n      week: initializeWeekOptions(options.week),\n      month: initializeMonthOptions(options.month),\n      gridSelection: initializeGridSelectionOptions(options.gridSelection),\n      usageStatistics: options.usageStatistics ?? true,\n      eventFilter: options.eventFilter ?? initialEventFilter,\n      timezone: initializeTimezoneOptions(options.timezone),\n    },\n  };\n}\n\nexport function createOptionsDispatchers(set: SetState<CalendarStore>): OptionsDispatchers {\n  return {\n    setOptions: (newOptions: Partial<OptionsSlice['options']> = {}) =>\n      set(\n        produce<CalendarState>((state) => {\n          if (newOptions.gridSelection) {\n            newOptions.gridSelection = initializeGridSelectionOptions(newOptions.gridSelection);\n          }\n\n          if (newOptions.week?.collapseDuplicateEvents) {\n            newOptions.week.collapseDuplicateEvents = initializeCollapseDuplicateEvents(\n              newOptions.week.collapseDuplicateEvents\n            );\n          }\n\n          mergeObject(state.options, newOptions);\n        })\n      ),\n  };\n}\n"
  },
  {
    "path": "apps/calendar/src/slices/popup.ts",
    "content": "import produce from 'immer';\n\nimport type { CalendarState, CalendarStore, PopupParamMap, SetState } from '@t/store';\n\nexport enum PopupType {\n  SeeMore = 'seeMore',\n  Form = 'form',\n  Detail = 'detail',\n}\n\nexport type PopupSlice = {\n  popup: {\n    [PopupType.SeeMore]: PopupParamMap[PopupType.SeeMore] | null;\n    [PopupType.Form]: PopupParamMap[PopupType.Form] | null;\n    [PopupType.Detail]: PopupParamMap[PopupType.Detail] | null;\n  };\n};\n\nexport type PopupDispatchers = {\n  showSeeMorePopup: (param: PopupParamMap[PopupType.SeeMore]) => void;\n  showFormPopup: (param: PopupParamMap[PopupType.Form]) => void;\n  showDetailPopup: (\n    param: PopupParamMap[PopupType.Detail],\n    isOpenedInSeeMorePopup: boolean\n  ) => void;\n  hideSeeMorePopup: () => void;\n  hideFormPopup: () => void;\n  hideDetailPopup: () => void;\n  hideAllPopup: () => void;\n};\n\nexport function createPopupSlice(): PopupSlice {\n  return {\n    popup: {\n      [PopupType.SeeMore]: null,\n      [PopupType.Form]: null,\n      [PopupType.Detail]: null,\n    },\n  };\n}\n\nexport function createPopupDispatchers(set: SetState<CalendarStore>): PopupDispatchers {\n  return {\n    showSeeMorePopup: (param: PopupParamMap[PopupType.SeeMore]) =>\n      set(\n        produce<CalendarState>((state) => {\n          state.popup[PopupType.SeeMore] = param;\n          state.popup[PopupType.Form] = null;\n          state.popup[PopupType.Detail] = null;\n        })\n      ),\n    showFormPopup: (param: PopupParamMap[PopupType.Form]) =>\n      set(\n        produce<CalendarState>((state) => {\n          state.popup[PopupType.Form] = param;\n          state.popup[PopupType.SeeMore] = null;\n          state.popup[PopupType.Detail] = null;\n        })\n      ),\n    showDetailPopup: (param: PopupParamMap[PopupType.Detail], isOpenedInSeeMorePopup) =>\n      set(\n        produce<CalendarState>((state) => {\n          state.popup[PopupType.Detail] = param;\n          state.popup[PopupType.Form] = null;\n          if (!isOpenedInSeeMorePopup) {\n            state.popup[PopupType.SeeMore] = null;\n          }\n        })\n      ),\n    hideSeeMorePopup: () =>\n      set(\n        produce<CalendarState>((state) => {\n          state.popup[PopupType.SeeMore] = null;\n        })\n      ),\n    hideFormPopup: () =>\n      set(\n        produce<CalendarState>((state) => {\n          state.popup[PopupType.Form] = null;\n        })\n      ),\n    hideDetailPopup: () =>\n      set(\n        produce<CalendarState>((state) => {\n          state.popup[PopupType.Detail] = null;\n        })\n      ),\n    hideAllPopup: () =>\n      set(\n        produce<CalendarState>((state) => {\n          state.popup[PopupType.SeeMore] = null;\n          state.popup[PopupType.Form] = null;\n          state.popup[PopupType.Detail] = null;\n        })\n      ),\n  };\n}\n"
  },
  {
    "path": "apps/calendar/src/slices/template.ts",
    "content": "import produce from 'immer';\n\nimport { templates } from '@src/template/default';\n\nimport type { CalendarState, CalendarStore, SetState } from '@t/store';\nimport type { Template, TemplateConfig } from '@t/template';\n\nexport type TemplateSlice = { template: Template };\n\nexport type TemplateDispatchers = {\n  setTemplate: (template: TemplateConfig) => void;\n};\n\nexport function createTemplateSlice(templateConfig: TemplateConfig = {}): TemplateSlice {\n  return {\n    template: {\n      ...templates,\n      ...templateConfig,\n    },\n  };\n}\n\nexport function createTemplateDispatchers(set: SetState<CalendarStore>): TemplateDispatchers {\n  return {\n    setTemplate: (template: TemplateConfig) =>\n      set(\n        produce<CalendarState>((state) => {\n          state.template = {\n            ...state.template,\n            ...template,\n          };\n        })\n      ),\n  };\n}\n"
  },
  {
    "path": "apps/calendar/src/slices/view.spec.ts",
    "content": "import type { ViewDispatchers, ViewSlice } from '@src/slices/view';\nimport { createViewDispatchers, createViewSlice } from '@src/slices/view';\nimport { createStore } from '@src/store/internal';\nimport TZDate from '@src/time/date';\nimport { addDate } from '@src/time/datetime';\n\nimport type { InternalStoreAPI, StoreCreator } from '@t/store';\n\ntype ViewSliceStore = ViewSlice & ViewDispatchers;\n\ndescribe('View Slice', () => {\n  let store: InternalStoreAPI<ViewSliceStore>;\n  const storeCreator: StoreCreator<ViewSlice & ViewDispatchers> = (set) => {\n    return {\n      ...createViewSlice(),\n      ...createViewDispatchers(set as any),\n    };\n  };\n  const getState = () => store.getState();\n\n  beforeEach(() => {\n    store = createStore(storeCreator);\n  });\n\n  it('should have default currentView value', () => {\n    // Given\n    const { view } = getState();\n\n    // When\n\n    // Then\n    expect(view.currentView).toBe('week');\n  });\n\n  it('should be able to change currentView', () => {\n    // Given\n    const targetView = 'day';\n    const { changeView } = getState();\n\n    // When\n    changeView(targetView);\n\n    // Then\n    expect(getState().view.currentView).toBe(targetView);\n  });\n\n  it('should have default renderDate', () => {\n    // Given\n    const today = new TZDate();\n    const {\n      view: { renderDate },\n    } = getState();\n\n    // When\n\n    // Then\n    expect(renderDate).toBeSameDate(today);\n  });\n\n  it('should be able to set renderDate', () => {\n    // Given\n    const today = new TZDate();\n    const newRenderDate = addDate(today, 1);\n    const { setRenderDate } = getState();\n\n    // When\n    setRenderDate(newRenderDate);\n\n    // Then\n    expect(getState().view.renderDate).toBeSameDate(newRenderDate);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/slices/view.ts",
    "content": "import produce from 'immer';\n\nimport TZDate from '@src/time/date';\nimport { toStartOfDay } from '@src/time/datetime';\n\nimport type { ViewType } from '@t/options';\nimport type { CalendarState, CalendarStore, SetState } from '@t/store';\n\nexport type ViewSlice = {\n  view: {\n    currentView: ViewType;\n    renderDate: TZDate;\n  };\n};\n\nexport type ViewDispatchers = {\n  changeView: (view: ViewType) => void;\n  setRenderDate: (date: TZDate) => void;\n};\n\nexport function createViewSlice(initialView: ViewType = 'week'): ViewSlice {\n  const renderDate = new TZDate();\n  renderDate.setHours(0, 0, 0, 0);\n\n  return {\n    view: {\n      currentView: initialView,\n      renderDate,\n    },\n  };\n}\n\nexport function createViewDispatchers(set: SetState<CalendarStore>): ViewDispatchers {\n  return {\n    changeView: (nextView: ViewType) =>\n      set(\n        produce<CalendarState>((state) => {\n          state.view.currentView = nextView;\n        })\n      ),\n    setRenderDate: (date: TZDate) =>\n      set(\n        produce<CalendarState>((state) => {\n          state.view.renderDate = toStartOfDay(date);\n        })\n      ),\n  };\n}\n"
  },
  {
    "path": "apps/calendar/src/store/index.spec.tsx",
    "content": "import { h } from 'preact';\nimport { useEffect, useState } from 'preact/hooks';\n\nimport { createStoreContext } from '@src/store/index';\nimport { createStore } from '@src/store/internal';\nimport { fireEvent, render, screen } from '@src/test/utils';\n\nimport type { InternalStoreAPI } from '@t/store';\n\ndescribe('Using store and store context', () => {\n  describe('Simple State', () => {\n    type CounterStore = {\n      count: number;\n      increment: () => void;\n      setCount: (n: number) => void;\n    };\n\n    let store: InternalStoreAPI<CounterStore>;\n    const { StoreProvider, useStore, useInternalStore } = createStoreContext<CounterStore>();\n\n    const Counter = () => {\n      const count = useStore((state) => state.count);\n\n      return <div>Current count is: {count}</div>;\n    };\n    const CounterButtons = () => {\n      const [incCount, setCount] = useStore((state) => [state.increment, state.setCount]);\n\n      return (\n        <div>\n          <button onClick={incCount}>+</button>\n          <button onClick={() => setCount(0)}>reset</button>\n        </div>\n      );\n    };\n\n    beforeEach(() => {\n      store = createStore((set) => ({\n        count: 0,\n        increment: () =>\n          set(({ count }) => ({\n            count: count + 1,\n          })),\n        setCount: (n: number) => set(() => ({ count: n })),\n      }));\n    });\n\n    it('should inject store to components with Provider', () => {\n      const { container } = render(\n        <StoreProvider store={store}>\n          <Counter />\n          <CounterButtons />\n        </StoreProvider>\n      );\n\n      expect(container).toHaveTextContent(/current count is: 0/i);\n\n      fireEvent.click(screen.getByText('+'));\n      expect(container).toHaveTextContent(/current count is: 1/i);\n\n      fireEvent.click(screen.getByText('reset'));\n      expect(container).toHaveTextContent(/current count is: 0/i);\n    });\n\n    it('should access store internal by `useStoreInternal`', () => {\n      const CounterMultiplier = () => {\n        const storeInternal = useInternalStore();\n        const [multipliedCounter, setMultipliedCounter] = useState(0);\n\n        useEffect(\n          () =>\n            storeInternal.subscribe(\n              (newCounter: number) => setMultipliedCounter(newCounter * 2),\n              (state) => state.count\n            ),\n          [storeInternal]\n        );\n\n        return <div>x2 count: {multipliedCounter}</div>;\n      };\n\n      const { container } = render(\n        <StoreProvider store={store}>\n          <CounterMultiplier />\n          <CounterButtons />\n        </StoreProvider>\n      );\n\n      expect(container).toHaveTextContent(/x2 count: 0/i);\n\n      fireEvent.click(screen.getByText('+'));\n      expect(container).toHaveTextContent(/x2 count: 2/i);\n    });\n  });\n\n  describe('Complex state', () => {\n    type Store = {\n      todo: {\n        todos: { id: string; title: string; isDone: boolean }[];\n        filter: 'all' | 'active' | 'done';\n      };\n      search: {\n        todoSearchKeyword: string;\n      };\n      dispatch: {\n        todo: {\n          addTodo: (title: string) => void;\n          changeTodoFilter: (filter: 'all' | 'active' | 'done') => void;\n        };\n        search: {\n          changeTodoSearchKeyword: (keyword: string) => void;\n        };\n      };\n    };\n\n    let store: InternalStoreAPI<Store>;\n    const { StoreProvider, useStore } = createStoreContext<Store>();\n\n    const SearchInput = () => {\n      const searchKeyword = useStore((state) => state.search.todoSearchKeyword);\n      const handleChange = useStore((state) => state.dispatch.search.changeTodoSearchKeyword);\n\n      return (\n        <div>\n          <span>Current search keyword: {searchKeyword}</span>\n          <label htmlFor=\"search-keyword\">\n            Search:\n            <input\n              id=\"search-keyword\"\n              type=\"text\"\n              value={searchKeyword}\n              onChange={(e) => handleChange(e.currentTarget.value ?? '')}\n            />\n          </label>\n        </div>\n      );\n    };\n\n    const Todos = () => {\n      const todos = useStore((state) => state.todo.todos);\n\n      return (\n        <ul role=\"list\">\n          {todos.map((todo) => (\n            <li key={todo.id}>{todo.title}</li>\n          ))}\n        </ul>\n      );\n    };\n\n    beforeEach(() => {\n      const todoNames = ['a', 'b', 'c', 'd'];\n      store = createStore<Store>((set) => ({\n        todo: {\n          todos: todoNames.map((name) => ({\n            title: name,\n            id: `id-${name}`,\n            isDone: name === 'd',\n          })),\n          filter: 'all',\n        },\n        search: {\n          todoSearchKeyword: '',\n        },\n        dispatch: {\n          todo: {\n            addTodo: (title) =>\n              set(({ todo }) => ({\n                todo: {\n                  ...todo,\n                  todos: todo.todos.concat({ title, id: `id-${title}`, isDone: false }),\n                },\n              })),\n            changeTodoFilter: (filter) =>\n              set(({ todo }) => ({\n                todo: {\n                  ...todo,\n                  filter,\n                },\n              })),\n          },\n          search: {\n            changeTodoSearchKeyword: (keyword) =>\n              set(({ search }) => ({\n                search: {\n                  ...search,\n                  todoSearchKeyword: keyword,\n                },\n              })),\n          },\n        },\n      }));\n    });\n\n    it('should not affect other states when modifying a certain scope of state', () => {\n      render(\n        <StoreProvider store={store}>\n          <SearchInput />\n          <Todos />\n        </StoreProvider>\n      );\n\n      const input = screen.getByLabelText('Search:');\n      const todolist = screen.getByRole('list');\n\n      expect(todolist.children).toHaveLength(4);\n\n      fireEvent.change(input, { target: { value: 'b' } });\n\n      const searchResult = screen.getByText(/current search keyword:\\sb/i);\n      expect(searchResult).toBeInTheDocument();\n      expect(todolist.children).toHaveLength(4);\n    });\n\n    it('should be able to create derived states with selectors', () => {\n      const FilteredTodo = () => {\n        const filteredTodos = useStore((state) => {\n          const { todos, filter } = state.todo;\n          const { todoSearchKeyword } = state.search;\n          const filterBySearchKeyword = todoSearchKeyword\n            ? (todo: Store['todo']['todos'][number]) => todo.title.startsWith(todoSearchKeyword)\n            : Boolean;\n\n          if (filter === 'all') {\n            return todos.filter(filterBySearchKeyword);\n          }\n\n          return todos\n            .filter((todo) => (filter === 'done' ? todo.isDone : !todo.isDone))\n            .filter(filterBySearchKeyword);\n        });\n\n        return (\n          <ul role=\"list\">\n            {filteredTodos.map((todo) => (\n              <li key={todo.id}>{todo.title}</li>\n            ))}\n          </ul>\n        );\n      };\n      const TodoFilter = () => {\n        const changeFilter = useStore((state) => state.dispatch.todo.changeTodoFilter);\n\n        return (\n          <div>\n            <button onClick={() => changeFilter('all')}>All</button>\n            <button onClick={() => changeFilter('active')}>Active</button>\n            <button onClick={() => changeFilter('done')}>Done</button>\n          </div>\n        );\n      };\n\n      render(\n        <StoreProvider store={store}>\n          <FilteredTodo />\n          <TodoFilter />\n        </StoreProvider>\n      );\n\n      const getTodoList = () => screen.getByRole('list');\n      const changeFilter = (filter: 'All' | 'Active' | 'Done') =>\n        fireEvent.click(screen.getByText(filter));\n\n      expect(getTodoList().children).toHaveLength(4);\n\n      changeFilter('Active');\n      expect(getTodoList().children).toHaveLength(3);\n\n      changeFilter('Done');\n      expect(getTodoList().children).toHaveLength(1);\n    });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/store/index.ts",
    "content": "import { createContext, createElement } from 'preact';\nimport { useContext, useEffect, useLayoutEffect, useMemo, useReducer, useRef } from 'preact/hooks';\n\nimport { isNil, isUndefined } from '@src/utils/type';\n\nimport type { PropsWithChildren } from '@t/components/common';\nimport type { EqualityChecker, InternalStoreAPI, StateSelector, StateWithActions } from '@t/store';\n\n/**\n * Inspired by Zustand\n *\n * See more: https://github.com/pmndrs/zustand\n */\n\nconst isSSR = isUndefined(window) || !window.navigator;\nconst useIsomorphicLayoutEffect = isSSR ? useEffect : useLayoutEffect;\n\nexport function createStoreContext<State extends StateWithActions>() {\n  const StoreContext = createContext<InternalStoreAPI<State> | null>(null);\n\n  function StoreProvider({\n    children,\n    store,\n  }: PropsWithChildren<{ store: InternalStoreAPI<State> }>) {\n    return createElement(StoreContext.Provider, { value: store, children });\n  }\n\n  const useStore = <StateSlice>(\n    selector: StateSelector<State, StateSlice>,\n    equalityFn: EqualityChecker<StateSlice> = Object.is\n  ) => {\n    const storeCtx = useContext(StoreContext);\n\n    if (isNil(storeCtx)) {\n      throw new Error('StoreProvider is not found');\n    }\n\n    // a little trick to invoke re-render to notify hook consumers(usually components)\n    const [, notify] = useReducer((notifyCount) => notifyCount + 1, 0) as [never, () => void];\n\n    const state = storeCtx.getState();\n    const stateRef = useRef(state);\n    const selectorRef = useRef(selector);\n    const equalityFnRef = useRef(equalityFn);\n    const hasErrorRef = useRef(false);\n    // `null` can be a valid state slice.\n    const currentSliceRef = useRef<StateSlice | undefined>();\n\n    if (isUndefined(currentSliceRef.current)) {\n      currentSliceRef.current = selector(state);\n    }\n\n    let newStateSlice: StateSlice | undefined;\n    let hasNewStateSlice = false;\n\n    const shouldGetNewSlice =\n      stateRef.current !== state ||\n      selectorRef.current !== selector ||\n      equalityFnRef.current !== equalityFn ||\n      hasErrorRef.current;\n    if (shouldGetNewSlice) {\n      newStateSlice = selector(state);\n      hasNewStateSlice = !equalityFn(currentSliceRef.current, newStateSlice);\n    }\n\n    useIsomorphicLayoutEffect(() => {\n      if (hasNewStateSlice) {\n        currentSliceRef.current = newStateSlice as StateSlice;\n      }\n\n      stateRef.current = state;\n      selectorRef.current = selector;\n      equalityFnRef.current = equalityFn;\n      hasErrorRef.current = false;\n    });\n\n    // NOTE: There is edge case that state is changed before subscription\n    const stateBeforeSubscriptionRef = useRef(state);\n    useIsomorphicLayoutEffect(() => {\n      const listener = () => {\n        try {\n          const nextState = storeCtx.getState();\n          const nextStateSlice = selectorRef.current(nextState);\n\n          const shouldUpdateState = !equalityFnRef.current(\n            currentSliceRef.current as StateSlice,\n            nextStateSlice\n          );\n          if (shouldUpdateState) {\n            stateRef.current = nextState;\n            currentSliceRef.current = newStateSlice;\n\n            notify();\n          }\n        } catch (e: unknown) {\n          // This will be rarely happened, unless we don't pass the arguments to actions properly.\n          // eslint-disable-next-line no-console\n          console.error('[toastui-calendar] failed to update state', (e as Error)?.message);\n          hasErrorRef.current = true;\n          notify();\n        }\n      };\n\n      const unsubscribe = storeCtx.subscribe(listener);\n      if (storeCtx.getState() !== stateBeforeSubscriptionRef.current) {\n        listener();\n      }\n\n      return unsubscribe;\n    }, []);\n\n    return hasNewStateSlice ? (newStateSlice as StateSlice) : currentSliceRef.current;\n  };\n\n  /**\n   * For handling often occurring state changes (Transient updates)\n   * See more: https://github.com/pmndrs/zustand/blob/master/readme.md#transient-updates-for-often-occuring-state-changes\n   */\n  const useInternalStore = () => {\n    const storeCtx = useContext(StoreContext);\n\n    if (isNil(storeCtx)) {\n      throw new Error('StoreProvider is not found');\n    }\n\n    return useMemo(() => storeCtx, [storeCtx]);\n  };\n\n  return {\n    StoreProvider,\n    useStore,\n    useInternalStore,\n  };\n}\n"
  },
  {
    "path": "apps/calendar/src/store/internal.ts",
    "content": "import type {\n  EqualityChecker,\n  GetState,\n  InternalStoreAPI,\n  SetState,\n  StateListener,\n  StateSelector,\n  StateSliceListener,\n  StateWithActions,\n  StoreCreator,\n  Subscribe,\n} from '@t/store';\n\nexport function createStore<State extends StateWithActions>(\n  storeCreator: StoreCreator<State>\n): InternalStoreAPI<State> {\n  let state: State;\n  const listeners = new Set<StateListener<State>>();\n\n  const setState: SetState<State> = (partialStateCreator) => {\n    const nextState = partialStateCreator(state);\n\n    if (nextState !== state) {\n      const previousState = state;\n      state = { ...state, ...nextState };\n      listeners.forEach((listener) => listener(state, previousState));\n    }\n  };\n\n  const getState: GetState<State> = () => state;\n\n  const subscribe: Subscribe<State> = <StateSlice>(\n    listener: StateListener<State> | StateSliceListener<StateSlice>,\n    selector?: StateSelector<State, StateSlice>,\n    equalityFn?: EqualityChecker<StateSlice>\n  ) => {\n    let _listener = listener;\n\n    if (selector) {\n      let currentSlice: StateSlice = selector(state);\n      const _equalityFn = equalityFn ?? Object.is;\n      _listener = () => {\n        const nextSlice = selector(state);\n        if (!_equalityFn(currentSlice, nextSlice)) {\n          const previousSlice = currentSlice;\n          currentSlice = nextSlice;\n          (listener as StateSliceListener<StateSlice>)(currentSlice, previousSlice);\n        }\n      };\n    }\n\n    listeners.add(_listener as StateListener<State>);\n\n    // eslint-disable-next-line dot-notation\n    return () => listeners.delete(_listener as StateListener<State>);\n  };\n\n  const clearListeners = () => listeners.clear();\n\n  const internal = { setState, getState, subscribe, clearListeners };\n  state = storeCreator(setState, getState, internal);\n\n  return internal;\n}\n"
  },
  {
    "path": "apps/calendar/src/template/default.tsx",
    "content": "import { Fragment, h } from 'preact';\n\nimport { cls } from '@src/helpers/css';\nimport { getDayName } from '@src/helpers/dayName';\nimport { isSameDate, leadingZero, toFormat } from '@src/time/datetime';\nimport { stripTags } from '@src/utils/dom';\nimport { capitalize } from '@src/utils/string';\nimport { isNil, isPresent } from '@src/utils/type';\n\nimport type { EventObjectWithDefaultValues } from '@t/events';\nimport type {\n  Template,\n  TemplateMonthDayName,\n  TemplateMonthGrid,\n  TemplateMoreTitleDate,\n  TemplateNow,\n  TemplateTimezone,\n  TemplateWeekDayName,\n} from '@t/template';\n\nconst SIXTY_MINUTES = 60;\n\nexport const templates: Template = {\n  milestone(model: EventObjectWithDefaultValues) {\n    const classNames = cls('icon', 'ic-milestone');\n\n    return (\n      <Fragment>\n        <span className={classNames} />\n        <span\n          style={{\n            backgroundColor: model.backgroundColor,\n          }}\n        >\n          {stripTags(model.title)}\n        </span>\n      </Fragment>\n    );\n  },\n\n  milestoneTitle() {\n    return <span className={cls('left-content')}>Milestone</span>;\n  },\n\n  task(model: EventObjectWithDefaultValues) {\n    return `#${model.title}`;\n  },\n\n  taskTitle() {\n    return <span className={cls('left-content')}>Task</span>;\n  },\n\n  alldayTitle() {\n    return <span className={cls('left-content')}>All Day</span>;\n  },\n\n  allday(model: EventObjectWithDefaultValues) {\n    return stripTags(model.title);\n  },\n\n  time(model: EventObjectWithDefaultValues) {\n    const { start, title } = model;\n\n    if (start) {\n      return (\n        <span>\n          <strong>{toFormat(start, 'HH:mm')}</strong>&nbsp;<span>{stripTags(title)}</span>\n        </span>\n      );\n    }\n\n    return stripTags(title);\n  },\n\n  goingDuration(model: EventObjectWithDefaultValues) {\n    const { goingDuration } = model;\n    const hour = Math.floor(goingDuration / SIXTY_MINUTES);\n    const minutes = goingDuration % SIXTY_MINUTES;\n\n    return `GoingTime ${leadingZero(hour, 2)}:${leadingZero(minutes, 2)}`;\n  },\n\n  comingDuration(model: EventObjectWithDefaultValues) {\n    const { comingDuration } = model;\n    const hour = Math.floor(comingDuration / SIXTY_MINUTES);\n    const minutes = comingDuration % SIXTY_MINUTES;\n\n    return `ComingTime ${leadingZero(hour, 2)}:${leadingZero(minutes, 2)}`;\n  },\n\n  monthMoreTitleDate(moreTitle: TemplateMoreTitleDate) {\n    const { date, day } = moreTitle;\n\n    const classNameDay = cls('more-title-date');\n    const classNameDayLabel = cls('more-title-day');\n    const dayName = capitalize(getDayName(day));\n\n    return (\n      <Fragment>\n        <span className={classNameDay}>{date}</span>\n        <span className={classNameDayLabel}>{dayName}</span>\n      </Fragment>\n    );\n  },\n\n  monthMoreClose() {\n    return '';\n  },\n\n  monthGridHeader(model: TemplateMonthGrid) {\n    const date = parseInt(model.date.split('-')[2], 10);\n    const classNames = cls('weekday-grid-date', { 'weekday-grid-date-decorator': model.isToday });\n\n    return <span className={classNames}>{date}</span>;\n  },\n\n  monthGridHeaderExceed(hiddenEvents: number) {\n    const className = cls('weekday-grid-more-events');\n\n    return <span className={className}>{hiddenEvents} more</span>;\n  },\n\n  monthGridFooter(_model: TemplateMonthGrid) {\n    return '';\n  },\n\n  monthGridFooterExceed(_hiddenEvents: number) {\n    return '';\n  },\n\n  monthDayName(model: TemplateMonthDayName) {\n    return model.label;\n  },\n\n  weekDayName(model: TemplateWeekDayName) {\n    const classDate = cls('day-name__date');\n    const className = cls('day-name__name');\n\n    return (\n      <Fragment>\n        <span className={classDate}>{model.date}</span>&nbsp;&nbsp;\n        <span className={className}>{model.dayName}</span>\n      </Fragment>\n    );\n  },\n\n  weekGridFooterExceed(hiddenEvents: number) {\n    return `+${hiddenEvents}`;\n  },\n\n  collapseBtnTitle() {\n    const className = cls('collapse-btn-icon');\n\n    return <span className={className} />;\n  },\n\n  timezoneDisplayLabel({ displayLabel, timezoneOffset }: TemplateTimezone) {\n    if (isNil(displayLabel) && isPresent(timezoneOffset)) {\n      const sign = timezoneOffset < 0 ? '-' : '+';\n      const hours = Math.abs(timezoneOffset / SIXTY_MINUTES);\n      const minutes = Math.abs(timezoneOffset % SIXTY_MINUTES);\n\n      return `GMT${sign}${leadingZero(hours, 2)}:${leadingZero(minutes, 2)}`;\n    }\n\n    return displayLabel as string;\n  },\n\n  timegridDisplayPrimaryTime(props: TemplateNow) {\n    const { time } = props;\n\n    return toFormat(time, 'hh tt');\n  },\n\n  timegridDisplayTime(props: TemplateNow) {\n    const { time } = props;\n\n    return toFormat(time, 'HH:mm');\n  },\n\n  timegridNowIndicatorLabel(timezone: TemplateNow) {\n    const { time, format = 'HH:mm' } = timezone;\n\n    return toFormat(time, format);\n  },\n\n  popupIsAllday() {\n    return 'All day';\n  },\n\n  popupStateFree() {\n    return 'Free';\n  },\n\n  popupStateBusy() {\n    return 'Busy';\n  },\n\n  titlePlaceholder() {\n    return 'Subject';\n  },\n\n  locationPlaceholder() {\n    return 'Location';\n  },\n\n  startDatePlaceholder() {\n    return 'Start date';\n  },\n\n  endDatePlaceholder() {\n    return 'End date';\n  },\n\n  popupSave() {\n    return 'Save';\n  },\n\n  popupUpdate() {\n    return 'Update';\n  },\n\n  popupEdit() {\n    return 'Edit';\n  },\n\n  popupDelete() {\n    return 'Delete';\n  },\n\n  popupDetailTitle({ title }: EventObjectWithDefaultValues) {\n    return title;\n  },\n\n  popupDetailDate({ isAllday, start, end }: EventObjectWithDefaultValues) {\n    const dayFormat = 'YYYY.MM.DD';\n    const timeFormat = 'hh:mm tt';\n    const detailFormat = `${dayFormat} ${timeFormat}`;\n    const startDate = toFormat(start, isAllday ? dayFormat : timeFormat);\n    const endDateFormat = isSameDate(start, end) ? timeFormat : detailFormat;\n\n    if (isAllday) {\n      return `${startDate}${isSameDate(start, end) ? '' : ` - ${toFormat(end, dayFormat)}`}`;\n    }\n\n    return `${toFormat(start, detailFormat)} - ${toFormat(end, endDateFormat)}`;\n  },\n\n  popupDetailLocation({ location }: EventObjectWithDefaultValues) {\n    return location;\n  },\n\n  popupDetailAttendees({ attendees = [] }: EventObjectWithDefaultValues) {\n    return attendees.join(', ');\n  },\n\n  popupDetailState({ state }: EventObjectWithDefaultValues) {\n    return state || 'Busy';\n  },\n\n  popupDetailRecurrenceRule({ recurrenceRule }: EventObjectWithDefaultValues) {\n    return recurrenceRule;\n  },\n\n  popupDetailBody({ body }: EventObjectWithDefaultValues) {\n    return body;\n  },\n};\n\nexport type TemplateName = keyof Template;\n"
  },
  {
    "path": "apps/calendar/src/template/index.ts",
    "content": "import { cls } from '@src/helpers/css';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport { templates } from '@src/template/default';\nimport type TZDate from '@src/time/date';\nimport { toFormat } from '@src/time/datetime';\nimport { isNumber } from '@src/utils/type';\n\nimport type { GridUIModel } from '@t/grid';\nimport type { Template, TemplateConfig } from '@t/template';\n\nexport function registerTemplateConfig(templateConfig: TemplateConfig = {}): Template {\n  return {\n    ...templates,\n    ...templateConfig,\n  };\n}\n\n/**\n * Get CSS syntax for element size\n * @param {number} value - size value to apply element\n * @param {string} postfix - postfix string ex) px, em, %\n * @param {string} prefix - property name ex) width, height\n * @returns {string} CSS syntax\n */\nfunction getElSize(value: number | string, postfix: string, prefix: string) {\n  prefix = prefix || '';\n  if (isNumber(value)) {\n    return `${prefix}:${value}${postfix}`;\n  }\n\n  return `${prefix}: auto`;\n}\n\n/**\n * Get element left based on narrowWeekend\n * @param {EventUIModel} uiModel - ui model\n * @param {Array} grids - dates information\n * @returns {number} element left\n */\nfunction getElLeft(uiModel: EventUIModel, grids: GridUIModel[]) {\n  return grids[uiModel.left] ? grids[uiModel.left].left : 0;\n}\n\n/**\n * Get element width based on narrowWeekend\n * @param {EventUIModel} uiModel - ui model\n * @param {Array} grids - dates information\n * @returns {number} element width\n */\nfunction getElWidth(uiModel: EventUIModel, grids: GridUIModel[]) {\n  let width = 0;\n  const { length } = grids;\n\n  for (let i = 0; i < uiModel.width; i += 1) {\n    let left = (uiModel.left + i) % length;\n    left += (uiModel.left + i) / length;\n    if (left < length) {\n      width += grids[left] ? grids[left].width : 0;\n    }\n  }\n\n  return width;\n}\n\n/**\n * Get hhmm formatted time str\n * @param {TZDate} date - date object\n * @returns {string} formatted value\n */\nexport function getHhmm(date: TZDate) {\n  return toFormat(date, 'HH:mm');\n}\n\n/**\n * Get `width` stylesheet string\n * @param {number} width - width percentage\n * @returns {string} css style part\n */\nexport function getCommonWidth(width: number | string) {\n  return getElSize(width, '%', 'width');\n}\n\n/**\n * Get element left based on narrowWeekend\n * @param {EventUIModel} uiModel - ui model\n * @param {Array} grids - dates information\n * @returns {number} element left\n */\nexport function getGridLeft(uiModel: EventUIModel, grids: GridUIModel[]) {\n  return getElLeft(uiModel, grids);\n}\n\n/**\n * Get element width based on narrowWeekend\n * @param {EventUIModel} uiModel - ui model\n * @param {Array} grids - dates information\n * @returns {number} element width\n */\nexport function getGridWidth(uiModel: EventUIModel, grids: GridUIModel[]) {\n  return getElWidth(uiModel, grids);\n}\n\n/**\n * Use in time.hbs\n * @param {EventUIModel} uiModel ui model\n * @returns {string} element size css class\n */\nexport function getTimeEventBlock(uiModel: EventUIModel) {\n  const top = getElSize(uiModel.top, 'px', 'top');\n  const left = getElSize(uiModel.left, '%', 'left');\n  const width = getElSize(uiModel.width, '%', 'width');\n  const height = getElSize(uiModel.height, 'px', 'height');\n\n  return [top, left, width, height].join(';');\n}\n\nexport function getMonthEventBlock(\n  uiModel: EventUIModel,\n  grids: GridUIModel[],\n  blockHeight: number,\n  paddingTop: number\n) {\n  const top = getElSize((uiModel.top - 1) * blockHeight + paddingTop, 'px', 'top');\n  const left = getElSize(grids[uiModel.left] ? grids[uiModel.left].left : 0, '%', 'left');\n  const width = getElSize(getElWidth(uiModel, grids), '%', 'width');\n  const height = getElSize(uiModel.height, 'px', 'height');\n\n  return [top, left, width, height].join(';');\n}\n\nexport function getHolidayClass(day: number) {\n  if (day === 0) {\n    return cls('holiday-sun');\n  }\n\n  if (day === 6) {\n    return cls('holiday-sat');\n  }\n\n  return '';\n}\n\nexport function getRight(a: number, b: number) {\n  return Math.max(0, 100 - (a + b));\n}\n"
  },
  {
    "path": "apps/calendar/src/template/template.spec.tsx",
    "content": "import { h } from 'preact';\nimport renderToString from 'preact-render-to-string';\n\nimport { Template } from '@src/components/template';\nimport { initCalendarStore, StoreProvider } from '@src/contexts/calendarStore';\nimport EventModel from '@src/model/eventModel';\nimport EventUIModel from '@src/model/eventUIModel';\nimport { templates } from '@src/template/default';\nimport {\n  getCommonWidth,\n  getMonthEventBlock,\n  getTimeEventBlock,\n  registerTemplateConfig,\n} from '@src/template/index';\n\ndescribe('Render Template', () => {\n  it('registerTemplateConfig() returns Template instance with given config and defaults.', () => {\n    const templateConfig = { time: ({ title }: { title: string }) => title };\n    const template = registerTemplateConfig(templateConfig);\n\n    expect(template.time).toBe(template.time);\n    expect(template.time).not.toBe(templates.time);\n    expect(template.allday).not.toBeNull();\n  });\n\n  it(`template function returns given config function's return value.`, () => {\n    const templateConfig = { popupSave: () => 'given' };\n    const template = registerTemplateConfig(templateConfig);\n\n    expect(templates.popupSave()).toBe('Save');\n    expect(template.popupSave()).toBe('given');\n  });\n\n  it('Template component renders html string with given template after sanitizing', () => {\n    const store = initCalendarStore();\n    const vdom = (\n      <StoreProvider store={store}>\n        <Template template=\"time\" param={{ title: '<script></script>Custom Title 4' }} />\n      </StoreProvider>\n    );\n    const html = renderToString(vdom);\n\n    expect(html).toBe('<div class=\"toastui-calendar-template-time\">Custom Title 4</div>');\n  });\n\n  it('getCommonWidth() returns css width percentage with given number.', () => {\n    const result = getCommonWidth(30);\n\n    expect(result).toBe('width:30%');\n  });\n\n  it('getCommonWidth() returns width: auto with not a number type.', () => {\n    const result = getCommonWidth('30');\n\n    expect(result).toBe('width: auto');\n  });\n\n  it('getTimeEventBlock() returns top(px), left(%), width(%), height(px) css.', () => {\n    const uiModel = new EventUIModel(new EventModel());\n    uiModel.top = 30;\n    uiModel.left = 40;\n    uiModel.width = 50;\n    uiModel.height = 60;\n\n    expect(getTimeEventBlock(uiModel)).toBe('top:30px;left:40%;width:50%;height:60px');\n  });\n\n  it('getMonthEventBlock() returns top(px), left(%), width(%), height(px) css.', () => {\n    const uiModel = new EventUIModel(new EventModel());\n    uiModel.top = 30;\n    uiModel.left = 40;\n    uiModel.width = 50;\n    uiModel.height = 60;\n\n    const result = getMonthEventBlock(uiModel, [], 2, 10);\n\n    expect(result).toBe('top:68px;left:0%;width:0%;height:60px');\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/test/cssFileMock.ts",
    "content": "export default {};\n"
  },
  {
    "path": "apps/calendar/src/test/helpers.ts",
    "content": "import TZDate from '@src/time/date';\n\nexport function createDate(y: number, M: number, d: number): TZDate {\n  const year = String(y);\n  let month = String(M);\n  let day = String(d);\n\n  if (month.length < 2) {\n    month = `0${month}`;\n  }\n  if (day.length < 2) {\n    day = `0${day}`;\n  }\n\n  return new TZDate(`${[year, month, day].join('-')}T00:00:00`);\n}\n"
  },
  {
    "path": "apps/calendar/src/test/matchers.ts",
    "content": "import type EventModel from '@src/model/eventModel';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport TZDate from '@src/time/date';\nimport { isSameDate } from '@src/time/datetime';\n\n// eslint-disable-next-line no-undefined\nconst undef = undefined;\nfunction pickTitle(matrix: EventUIModel[]) {\n  const titleList = [];\n\n  for (let i = 0, cnt = matrix.length; i < cnt; i += 1) {\n    if (!matrix[i]) {\n      titleList.push(undef);\n      continue;\n    }\n\n    titleList.push(matrix[i].valueOf().title);\n  }\n\n  return titleList;\n}\n\nfunction fail(message: string) {\n  return {\n    message: () => message,\n    pass: false,\n  };\n}\n\nfunction titleComparator(uiModel: EventUIModel, title: string | number) {\n  return uiModel.model.title === title;\n}\n\nfunction topComparator(uiModel: EventUIModel, top: string | number) {\n  return uiModel.top === top;\n}\n\nfunction getMatcher(comparator: (uiModel: EventUIModel, value: string | number) => boolean) {\n  return function matcher(actual: Array<EventUIModel[]>, expected: Array<string[] | number[]>) {\n    let aMatrix;\n    let aLength;\n    let bMatrix;\n    let bLength;\n    let aColumn;\n    let aUIModel;\n    let bValue;\n\n    if (actual.length !== expected.length) {\n      return fail(`Matrix number mismatch\\nactual: ${actual}\\nexpected: ${expected}`);\n    }\n\n    for (let i = 0, cnt = actual.length; i < cnt; i += 1) {\n      aMatrix = actual[i];\n      aLength = aMatrix.length;\n      bMatrix = expected[i];\n      bLength = bMatrix.length;\n\n      if (aLength !== bLength) {\n        return fail(\n          `${i}th matrix is different\\n` +\n            `actual: ${pickTitle(aMatrix)}\\n` +\n            `expected: ${bMatrix}`\n        );\n      }\n\n      for (let j = 0, cnt2 = aMatrix.length; j < cnt2; j += 1) {\n        aColumn = aMatrix[j];\n\n        if (!aColumn) {\n          continue;\n        }\n\n        aUIModel = aColumn;\n        bValue = bMatrix[j];\n\n        if (!comparator(aUIModel, bValue)) {\n          return fail(\n            `[${i}][${j}] th matrix is different\\n` +\n              `actual: ${aUIModel}\\n` +\n              `expected: ${bValue}`\n          );\n        }\n      }\n    }\n\n    return {\n      message: () => 'Matrix match',\n      pass: true,\n    };\n  };\n}\n\nexpect.extend({\n  toEqualMatricesTitle: getMatcher(titleComparator),\n  toEqualMatricesTop: getMatcher(topComparator),\n  toEqualUIModelByTitle(actual: Record<string, EventModel[]>, expected: Record<string, string[]>) {\n    const result = {\n      pass: false,\n      message: () => '',\n    };\n    let isEqual = true;\n\n    Object.keys(expected).findIndex((ymd) => {\n      const models = actual[ymd];\n\n      if (!models) {\n        isEqual = false;\n\n        return false;\n      }\n\n      const titleList = models.map((item) => item.title);\n\n      isEqual = this.equals(titleList.sort(), expected[ymd].sort());\n\n      return isEqual;\n    });\n\n    result.pass = isEqual;\n\n    return result;\n  },\n  toBeSameDate(actual: number | string | TZDate | Date, expected: number | string | TZDate | Date) {\n    return {\n      pass: isSameDate(new TZDate(actual), new TZDate(expected)),\n      message: () => `${expected} is not the same date as ${actual}.`,\n    };\n  },\n});\n"
  },
  {
    "path": "apps/calendar/src/test/testIds.ts",
    "content": "export const TEST_IDS = {\n  NOW_INDICATOR: 'timegrid-now-indicator',\n  NOW_INDICATOR_LABEL: 'timegrid-now-indicator-label',\n};\n"
  },
  {
    "path": "apps/calendar/src/test/utils.tsx",
    "content": "import { h } from 'preact';\n\nimport type { RenderHookOptions } from '@testing-library/preact';\nimport {\n  fireEvent,\n  render as ptlRender,\n  renderHook as ptlRenderHook,\n} from '@testing-library/preact';\nimport { default as userEvent } from '@testing-library/user-event';\n\nimport { CalendarContainer } from '@src/calendarContainer';\nimport { initCalendarStore } from '@src/contexts/calendarStore';\nimport { initThemeStore } from '@src/contexts/themeStore';\nimport type { EventBus } from '@src/utils/eventBus';\nimport { EventBusImpl } from '@src/utils/eventBus';\nimport { isPresent } from '@src/utils/type';\n\nimport type { PropsWithChildren } from '@t/components/common';\nimport type { ClientMousePosition } from '@t/mouse';\nimport type { CalendarStore, InternalStoreAPI } from '@t/store';\nimport type { ThemeStore } from '@t/theme';\nimport type { FormattedTimeString } from '@t/time/datetime';\n\nfunction render(\n  component: Parameters<typeof ptlRender>[0],\n  {\n    eventBus = new EventBusImpl<Record<string, any>>(),\n    store = initCalendarStore(),\n    theme = initThemeStore(),\n    ...options\n  }: Parameters<typeof ptlRender>[1] &\n    Partial<{\n      eventBus: EventBus<Record<string, any>>;\n      store: InternalStoreAPI<CalendarStore>;\n      theme: InternalStoreAPI<ThemeStore>;\n    }> = {}\n) {\n  const Wrapper = ({ children }: PropsWithChildren) => (\n    <CalendarContainer theme={theme} eventBus={eventBus} store={store}>\n      {children}\n    </CalendarContainer>\n  );\n\n  return ptlRender(component, { wrapper: Wrapper, ...options });\n}\n\nfunction renderHook<R, P>(\n  hook: (initialProp: P) => R,\n  {\n    eventBus = new EventBusImpl<any>(),\n    store = initCalendarStore(),\n    theme = initThemeStore(),\n    ...options\n  }: RenderHookOptions<P> &\n    Partial<{\n      eventBus: EventBus<any>;\n      store: InternalStoreAPI<CalendarStore>;\n      theme: InternalStoreAPI<ThemeStore>;\n    }> = {}\n) {\n  const Wrapper = ({ children }: PropsWithChildren) => (\n    <CalendarContainer theme={theme} eventBus={eventBus} store={store}>\n      {children}\n    </CalendarContainer>\n  );\n\n  return ptlRenderHook<R, P>(hook, { wrapper: Wrapper, ...options });\n}\n\nexport function dragAndDrop({\n  element,\n  initPosition,\n  targetPosition,\n  hold = false,\n}: {\n  element: HTMLElement;\n  targetPosition: ClientMousePosition;\n  initPosition?: ClientMousePosition;\n  hold?: boolean;\n}) {\n  fireEvent.mouseDown(element, initPosition);\n  fireEvent.mouseMove(document, targetPosition);\n  fireEvent.mouseMove(document, targetPosition);\n  if (!hold) {\n    fireEvent.mouseUp(document);\n  }\n}\n\nexport function hasDesiredStartTime(el: HTMLElement, startTimeStr: FormattedTimeString) {\n  let node: HTMLElement | null = el;\n  while (isPresent(node)) {\n    if (node.textContent?.includes(startTimeStr)) {\n      return true;\n    }\n    node = node.parentElement;\n  }\n\n  return false;\n}\n\nexport * from '@testing-library/preact';\nexport * from '@testing-library/user-event';\nexport { render, renderHook, userEvent };\n"
  },
  {
    "path": "apps/calendar/src/theme/common.ts",
    "content": "import type { DeepPartial } from 'ts-essentials';\n\nimport { DEFAULT_COMMON_THEME } from '@src/constants/theme';\nimport { mergeObject } from '@src/utils/object';\n\nimport type { CommonTheme, ThemeState } from '@t/theme';\n\nexport function createCommonTheme(commonTheme: DeepPartial<CommonTheme> = {}): {\n  common: Required<ThemeState['common']>;\n} {\n  return {\n    common: mergeObject(DEFAULT_COMMON_THEME, commonTheme),\n  };\n}\n"
  },
  {
    "path": "apps/calendar/src/theme/dispatch.spec.tsx",
    "content": "import { h } from 'preact';\n\nimport {\n  initThemeStore,\n  useAllTheme,\n  useCommonTheme,\n  useMonthTheme,\n  useWeekTheme,\n} from '@src/contexts/themeStore';\nimport { act, render, screen } from '@src/test/utils';\n\ndescribe('setCommonTheme', () => {\n  const theme = initThemeStore();\n  const { setCommonTheme } = theme.getState().dispatch;\n  let beforeBackgroundColor: string;\n  let beforeGridSelectionBorder: string;\n\n  function CommonThemeComponent() {\n    const {\n      backgroundColor,\n      gridSelection: { border },\n    } = useCommonTheme();\n\n    return (\n      <div>\n        <div style={{ backgroundColor, border }}>common theme</div>\n      </div>\n    );\n  }\n\n  beforeEach(() => {\n    beforeBackgroundColor = theme.getState().common.backgroundColor ?? '';\n    beforeGridSelectionBorder = theme.getState().common.gridSelection.border ?? '';\n    render(<CommonThemeComponent />, { theme });\n  });\n\n  it('should set theme and other properties should not be changed', () => {\n    // Given\n    const backgroundColor = 'black';\n\n    // When\n    act(() => {\n      setCommonTheme({\n        backgroundColor,\n      });\n    });\n\n    // Then\n    expect(theme.getState().common.backgroundColor).toBe(backgroundColor);\n    expect(theme.getState().common.gridSelection.border).toBe(beforeGridSelectionBorder);\n  });\n\n  it('should rerender component that use useTheme when theme is changed', () => {\n    // Given\n    const backgroundColor = 'black';\n\n    // When\n    act(() => {\n      setCommonTheme({\n        backgroundColor,\n      });\n    });\n\n    // Then\n    expect(screen.getByText('common theme')).toHaveStyle({\n      backgroundColor,\n      border: beforeGridSelectionBorder,\n    });\n  });\n\n  it('should not change theme when unused theme is changed', () => {\n    // Given\n    const color = 'black';\n\n    // When\n    act(() => {\n      setCommonTheme({\n        today: {\n          color,\n        },\n      });\n    });\n\n    // Then\n    expect(screen.getByText('common theme')).toHaveStyle({\n      backgroundColor: beforeBackgroundColor,\n      border: beforeGridSelectionBorder,\n    });\n  });\n});\n\ndescribe('setWeekTheme', () => {\n  const theme = initThemeStore();\n  const { setWeekTheme } = theme.getState().dispatch;\n  let beforePastTimeColor: string;\n  let beforeNowIndicatorLabelColor: string;\n\n  function WeekThemeComponent() {\n    const {\n      pastTime: { color: pastTimeColor },\n      nowIndicatorLabel: { color: nowIndicatorLabelColor },\n    } = useWeekTheme();\n\n    return (\n      <div>\n        <div style={{ backgroundColor: pastTimeColor, color: nowIndicatorLabelColor }}>\n          week theme\n        </div>\n      </div>\n    );\n  }\n\n  beforeEach(() => {\n    beforePastTimeColor = theme.getState().week.pastTime.color ?? '';\n    beforeNowIndicatorLabelColor = theme.getState().week.nowIndicatorLabel.color ?? '';\n    render(<WeekThemeComponent />, { theme });\n  });\n\n  it('should set theme and other properties should not be changed', () => {\n    // Given\n    const color = 'black';\n\n    // When\n    act(() => {\n      setWeekTheme({\n        pastTime: {\n          color,\n        },\n      });\n    });\n\n    // Then\n    expect(theme.getState().week.pastTime.color).toBe(color);\n    expect(theme.getState().week.nowIndicatorLabel.color).toBe(beforeNowIndicatorLabelColor);\n  });\n\n  it('should rerender component that use useTheme when theme is changed', () => {\n    // Given\n    const color = 'black';\n\n    // When\n    act(() => {\n      setWeekTheme({\n        pastTime: {\n          color,\n        },\n      });\n    });\n\n    // Then\n    expect(screen.getByText('week theme')).toHaveStyle({\n      backgroundColor: color,\n      color: beforeNowIndicatorLabelColor,\n    });\n  });\n\n  it('should not change theme when unused theme is changed', () => {\n    // Given\n    const color = 'black';\n\n    // When\n    act(() => {\n      setWeekTheme({\n        today: {\n          color,\n        },\n      });\n    });\n\n    // Then\n    expect(screen.getByText('week theme')).toHaveStyle({\n      backgroundColor: beforePastTimeColor,\n      color: beforeNowIndicatorLabelColor,\n    });\n  });\n});\n\ndescribe('setMonthTheme', () => {\n  const theme = initThemeStore();\n  const { setMonthTheme } = theme.getState().dispatch;\n  let beforeDayExceptThisMonthColor: string;\n  let beforeHolidayExceptThisMonthColor: string;\n\n  function MonthThemeComponent() {\n    const {\n      dayExceptThisMonth: { color: dayExceptThisMonthColor },\n      holidayExceptThisMonth: { color: holidayExceptThisMonthColor },\n    } = useMonthTheme();\n\n    return (\n      <div>\n        <div\n          style={{ backgroundColor: dayExceptThisMonthColor, color: holidayExceptThisMonthColor }}\n        >\n          month theme\n        </div>\n      </div>\n    );\n  }\n\n  beforeEach(() => {\n    beforeDayExceptThisMonthColor = theme.getState().month.dayExceptThisMonth.color ?? '';\n    beforeHolidayExceptThisMonthColor = theme.getState().month.holidayExceptThisMonth.color ?? '';\n    render(<MonthThemeComponent />, { theme });\n  });\n\n  it('should set theme and other properties should not be changed', () => {\n    // Given\n    const color = 'black';\n\n    // When\n    act(() => {\n      setMonthTheme({\n        dayExceptThisMonth: {\n          color,\n        },\n      });\n    });\n\n    // Then\n    expect(theme.getState().month.dayExceptThisMonth.color).toBe(color);\n    expect(theme.getState().month.holidayExceptThisMonth.color).toBe(\n      beforeHolidayExceptThisMonthColor\n    );\n  });\n\n  it('should rerender component that use useTheme when theme is changed', () => {\n    // Given\n    const color = 'black';\n\n    // When\n    act(() => {\n      setMonthTheme({\n        dayExceptThisMonth: {\n          color,\n        },\n      });\n    });\n\n    // Then\n    expect(screen.getByText('month theme')).toHaveStyle({\n      backgroundColor: color,\n      color: beforeHolidayExceptThisMonthColor,\n    });\n  });\n\n  it('should not change theme when unused theme is changed', () => {\n    // Given\n    const backgroundColor = 'black';\n\n    // When\n    act(() => {\n      setMonthTheme({\n        weekend: {\n          backgroundColor,\n        },\n      });\n    });\n\n    // Then\n    expect(screen.getByText('month theme')).toHaveStyle({\n      backgroundColor: beforeDayExceptThisMonthColor,\n      color: beforeHolidayExceptThisMonthColor,\n    });\n  });\n});\n\ndescribe('setTheme', () => {\n  const theme = initThemeStore();\n  const { setTheme } = theme.getState().dispatch;\n  let beforeCommonBorder: string;\n  let beforeWeekTodayColor: string;\n  let beforeMonthDayExceptThisMonthColor: string;\n\n  function ThemeComponent() {\n    const { common, week, month } = useAllTheme();\n    const commonBorder = common.border ?? '';\n    const weekTodayColor = week.today.color ?? '';\n    const monthDayExceptThisMonthColor = month.dayExceptThisMonth.color ?? '';\n\n    return (\n      <div>\n        <div\n          style={{\n            border: commonBorder,\n            backgroundColor: weekTodayColor,\n            color: monthDayExceptThisMonthColor,\n          }}\n        >\n          all theme\n        </div>\n      </div>\n    );\n  }\n\n  beforeEach(() => {\n    beforeCommonBorder = theme.getState().common.border ?? '';\n    beforeWeekTodayColor = theme.getState().week.today.color ?? '';\n    beforeMonthDayExceptThisMonthColor = theme.getState().month.dayExceptThisMonth.color ?? '';\n    render(<ThemeComponent />, { theme });\n  });\n\n  it('should set theme and other properties should not be changed', () => {\n    // Given\n    const color = 'black';\n\n    // When\n    act(() => {\n      setTheme({\n        week: {\n          today: {\n            color,\n          },\n        },\n      });\n    });\n\n    // Then\n    expect(theme.getState().common.border).toBe(beforeCommonBorder);\n    expect(theme.getState().week.today.color).toBe(color);\n    expect(theme.getState().month.dayExceptThisMonth.color).toBe(\n      beforeMonthDayExceptThisMonthColor\n    );\n  });\n\n  it('should rerender component that use useTheme when theme is changed', () => {\n    // Given\n    const color = 'black';\n\n    // When\n    act(() => {\n      setTheme({\n        week: {\n          today: {\n            color,\n          },\n        },\n      });\n    });\n\n    // Then\n    expect(screen.getByText('all theme')).toHaveStyle({\n      border: beforeCommonBorder,\n      backgroundColor: color,\n      color: beforeMonthDayExceptThisMonthColor,\n    });\n  });\n\n  it('should not change theme when unused theme is changed', () => {\n    // Given\n    const backgroundColor = 'black';\n\n    // When\n    act(() => {\n      setTheme({\n        month: {\n          weekend: {\n            backgroundColor,\n          },\n        },\n      });\n    });\n\n    // Then\n    expect(screen.getByText('all theme')).toHaveStyle({\n      border: beforeCommonBorder,\n      backgroundColor: beforeWeekTodayColor,\n      color: beforeMonthDayExceptThisMonthColor,\n    });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/theme/dispatch.ts",
    "content": "import produce from 'immer';\n\nimport { mergeObject } from '@src/utils/object';\n\nimport type { SetState } from '@t/store';\nimport type { ThemeDispatchers, ThemeState, ThemeStore } from '@t/theme';\n\nexport function createThemeDispatch(set: SetState<ThemeStore>): ThemeDispatchers {\n  return {\n    setTheme: (theme) => {\n      set(\n        produce((state: ThemeState) => {\n          state.common = mergeObject(state.common, theme.common);\n          state.week = mergeObject(state.week, theme.week);\n          state.month = mergeObject(state.month, theme.month);\n        })\n      );\n    },\n    setCommonTheme: (commonTheme) => {\n      set(\n        produce((state: ThemeState) => {\n          state.common = mergeObject(state.common, commonTheme);\n        })\n      );\n    },\n    setWeekTheme: (weekTheme) => {\n      set(\n        produce((state: ThemeState) => {\n          state.week = mergeObject(state.week, weekTheme);\n        })\n      );\n    },\n    setMonthTheme: (monthTheme) => {\n      set(\n        produce((state: ThemeState) => {\n          state.month = mergeObject(state.month, monthTheme);\n        })\n      );\n    },\n  };\n}\n"
  },
  {
    "path": "apps/calendar/src/theme/month.ts",
    "content": "import type { DeepPartial } from 'ts-essentials';\n\nimport { DEFAULT_MONTH_THEME } from '@src/constants/theme';\nimport { mergeObject } from '@src/utils/object';\n\nimport type { MonthTheme, ThemeState } from '@t/theme';\n\nexport function createMonthTheme(monthTheme: DeepPartial<MonthTheme> = {}): {\n  month: Required<ThemeState>['month'];\n} {\n  return {\n    month: mergeObject(DEFAULT_MONTH_THEME, monthTheme),\n  };\n}\n"
  },
  {
    "path": "apps/calendar/src/theme/week.ts",
    "content": "import type { DeepPartial } from 'ts-essentials';\n\nimport { DEFAULT_WEEK_THEME } from '@src/constants/theme';\nimport { mergeObject } from '@src/utils/object';\n\nimport type { ThemeState, WeekTheme } from '@t/theme';\n\nexport function createWeekTheme(weekTheme: DeepPartial<WeekTheme> = {}): {\n  week: Required<ThemeState>['week'];\n} {\n  return {\n    week: mergeObject(DEFAULT_WEEK_THEME, weekTheme),\n  };\n}\n"
  },
  {
    "path": "apps/calendar/src/time/date.spec.ts",
    "content": "import TZDate from '@src/time/date';\nimport { MS_PER_HOUR } from '@src/time/datetime';\n\ndescribe('tz()', () => {\n  it('should return new TZDate instance following the given timezone name (not applicable to DST)', () => {\n    // Given\n    const date = new TZDate('2022-03-26T12:00:00+09:00');\n    const targetTimezone = 'Europe/Paris';\n    const expectedHourDiff = 8;\n\n    // When\n    const result = date.tz(targetTimezone); // UTC+1\n\n    // Then\n    expect(result).toBeInstanceOf(TZDate);\n    expect(result.getTime()).toBe(date.getTime() - expectedHourDiff * MS_PER_HOUR);\n  });\n\n  it('should return new TZDate instance following the given timezone name (applicable to DST)', () => {\n    // Given\n    const date = new TZDate('2022-03-30T12:00:00+09:00');\n    const targetTimezone = 'Europe/Paris';\n    const expectedHourDiff = 7;\n\n    // When\n    const result = date.tz(targetTimezone); // UTC+2\n\n    // Then\n    expect(result).toBeInstanceOf(TZDate);\n    expect(result.getTime()).toBe(date.getTime() - expectedHourDiff * MS_PER_HOUR);\n  });\n\n  it('should return new TZDate instance following the given timezone offset', () => {\n    // Given\n    const date = new TZDate('2022-03-26T12:00:00+09:00');\n    const targetTimezoneOffset = 300;\n    const expectedHourDiff = 4;\n\n    // When\n    const result = date.tz(targetTimezoneOffset); // UTC+5\n\n    // Then\n    expect(result).toBeInstanceOf(TZDate);\n    expect(result.getTime()).toBe(date.getTime() - expectedHourDiff * MS_PER_HOUR);\n  });\n});\n\ndescribe('local()', () => {\n  it(`should return the new TZDate instance having same value if there is no timezone value and it doesn't have timezone offset`, () => {\n    // Given\n    const date = new TZDate('2022-03-26T12:00:00+09:00');\n\n    // When\n    const result = date.local();\n\n    // Then\n    expect(result.getTime()).toBe(date.getTime());\n  });\n\n  it('should return the new TZDate instance following the local timezone when the origianl instance has timezone offset', () => {\n    // Given\n    const date = new TZDate('2022-03-26T12:00:00+09:00').tz('Europe/Paris');\n    const expectedHourDiff = 8;\n\n    // When\n    // Local timezone is 'Asia/Seoul'\n    const result = date.local();\n\n    // Then\n    expect(result).toBeInstanceOf(TZDate);\n    expect(result.getTime()).toBe(date.getTime() + expectedHourDiff * MS_PER_HOUR);\n  });\n\n  it('should return the new TZDate following the local timezone with the timezone value', () => {\n    // Given\n    const date = new TZDate('2022-03-26T12:00:00+01:00'); // already UTC+1\n    const givenTimezone = 'Europe/Paris';\n    const expectedHourDiff = 8;\n\n    // When\n    // Local timezone is 'Asia/Seoul'\n    const result = date.local(givenTimezone);\n\n    // Then\n    expect(result).toBeInstanceOf(TZDate);\n    expect(result.getTime()).toBe(date.getTime() + expectedHourDiff * MS_PER_HOUR);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/time/date.ts",
    "content": "import type { DateInterface } from '@toast-ui/date';\n\nimport { MS_PER_MINUTES } from '@src/time/datetime';\nimport {\n  calculateTimezoneOffset,\n  date as createDate,\n  getLocalTimezoneOffset,\n} from '@src/time/timezone';\nimport { isPresent, isString } from '@src/utils/type';\n\nfunction getTZOffsetMSDifference(offset: number) {\n  return (getLocalTimezoneOffset() - offset) * MS_PER_MINUTES;\n}\n\n/**\n * Custom Date Class to handle timezone offset.\n *\n * For more information, see {@link https://github.com/nhn/tui.calendar/blob/main/docs/en/apis/tzdate.md|TZDate} in guide.\n *\n * @class TZDate\n * @param {number|TZDate|Date|string} date - date value to be converted. If date is number or string, it should be eligible to parse by Date constructor.\n */\nexport default class TZDate {\n  private tzOffset: number | null = null;\n\n  private d: DateInterface;\n\n  constructor(...args: any[]) {\n    if (args[0] instanceof TZDate) {\n      this.d = createDate(args[0].getTime());\n    } else {\n      this.d = createDate(...args);\n    }\n  }\n\n  /**\n   * Get the string representation of the date.\n   * @returns {string} string representation of the date.\n   */\n  toString() {\n    return this.d.toString();\n  }\n\n  /**\n   * Add years to the instance.\n   * @param {number} y - number of years to be added.\n   * @returns {TZDate} - returns the instance itself.\n   */\n  addFullYear(y: number): TZDate {\n    this.setFullYear(this.getFullYear() + y);\n\n    return this;\n  }\n\n  /**\n   * Add months to the instance.\n   * @param {number} m - number of months to be added.\n   * @returns {TZDate} - returns the instance itself.\n   */\n  addMonth(m: number): TZDate {\n    this.setMonth(this.getMonth() + m);\n\n    return this;\n  }\n\n  /**\n   * Add dates to the instance.\n   * @param {number} d - number of days to be added.\n   * @returns {TZDate} - returns the instance itself.\n   */\n  addDate(d: number): TZDate {\n    this.setDate(this.getDate() + d);\n\n    return this;\n  }\n\n  /**\n   * Add hours to the instance.\n   * @param {number} h - number of hours to be added.\n   * @returns {TZDate} - returns the instance itself.\n   */\n  addHours(h: number): TZDate {\n    this.setHours(this.getHours() + h);\n\n    return this;\n  }\n\n  /**\n   * Add minutes to the instance.\n   * @param {number} M - number of minutes to be added.\n   * @returns {TZDate} - returns the instance itself.\n   */\n  addMinutes(M: number): TZDate {\n    this.setMinutes(this.getMinutes() + M);\n\n    return this;\n  }\n\n  /**\n   * Add seconds to the instance.\n   * @param {number} s - number of seconds to be added.\n   * @returns {TZDate} - returns the instance itself.\n   */\n  addSeconds(s: number): TZDate {\n    this.setSeconds(this.getSeconds() + s);\n\n    return this;\n  }\n\n  /**\n   * Add milliseconds to the instance.\n   * @param {number} ms - number of milliseconds to be added.\n   * @returns {TZDate} - returns the instance itself.\n   */\n  addMilliseconds(ms: number): TZDate {\n    this.setMilliseconds(this.getMilliseconds() + ms);\n\n    return this;\n  }\n\n  /* eslint-disable max-params*/\n  /**\n   * Set the date and time all at once.\n   * @param {number} y - year\n   * @param {number} m - month\n   * @param {number} d - date\n   * @param {number} h - hours\n   * @param {number} M - minutes\n   * @param {number} s - seconds\n   * @param {number} ms - milliseconds\n   * @returns {TZDate} - returns the instance itself.\n   */\n  setWithRaw(y: number, m: number, d: number, h: number, M: number, s: number, ms: number): TZDate {\n    this.setFullYear(y, m, d);\n    this.setHours(h, M, s, ms);\n\n    return this;\n  }\n\n  /**\n   * Convert the instance to the native `Date` object.\n   * @returns {Date} - The native `Date` object.\n   */\n  toDate(): Date {\n    return this.d.toDate();\n  }\n\n  /**\n   * Get the value of the date. (milliseconds since 1970-01-01 00:00:00 (UTC+0))\n   * @returns {number} - value of the date.\n   */\n  valueOf(): number {\n    return this.getTime();\n  }\n\n  /**\n   * Get the timezone offset from UTC in minutes.\n   * @returns {number} - timezone offset in minutes.\n   */\n  getTimezoneOffset() {\n    return this.tzOffset ?? this.d.getTimezoneOffset();\n  }\n\n  // Native properties\n  /**\n   * Get milliseconds which is converted by timezone\n   * @returns {number} milliseconds\n   */\n  getTime(): number {\n    return this.d.getTime();\n  }\n\n  /**\n   * Get the year of the instance.\n   * @returns {number} - full year\n   */\n  getFullYear(): number {\n    return this.d.getFullYear();\n  }\n\n  /**\n   * Get the month of the instance. (zero-based)\n   * @returns {number} - month\n   */\n  getMonth(): number {\n    return this.d.getMonth();\n  }\n\n  /**\n   * Get the date of the instance.\n   * @returns {number} - date\n   */\n  getDate(): number {\n    return this.d.getDate();\n  }\n\n  /**\n   * Get the hours of the instance.\n   * @returns {number} - hours\n   */\n  getHours(): number {\n    return this.d.getHours();\n  }\n\n  /**\n   * Get the minutes of the instance.\n   * @returns {number} - minutes\n   */\n  getMinutes(): number {\n    return this.d.getMinutes();\n  }\n\n  /**\n   * Get the seconds of the instance.\n   * @returns {number} - seconds\n   */\n  getSeconds(): number {\n    return this.d.getSeconds();\n  }\n\n  /**\n   * Get the milliseconds of the instance.\n   * @returns {number} - milliseconds\n   */\n  getMilliseconds(): number {\n    return this.d.getMilliseconds();\n  }\n\n  /**\n   * Get the day of the week of the instance.\n   * @returns {number} - day of the week\n   */\n  getDay(): number {\n    return this.d.getDay();\n  }\n\n  /**\n   * Sets the instance to the time represented by a number of milliseconds since 1970-01-01 00:00:00 (UTC+0).\n   * @param {number} t - number of milliseconds\n   * @returns {number} - Passed milliseconds of the instance since 1970-01-01 00:00:00 (UTC+0).\n   */\n  setTime(t: number): number {\n    return this.d.setTime(t);\n  }\n\n  /**\n   * Sets the year-month-date of the instance. Equivalent to calling `setFullYear` of `Date` object.\n   * @param {number} y - year\n   * @param {number} m - month (zero-based)\n   * @param {number} d - date\n   * @returns {number} - Passed milliseconds of the instance since 1970-01-01 00:00:00 (UTC+0).\n   */\n  setFullYear(y: number, m = this.getMonth(), d = this.getDate()): number {\n    return this.d.setFullYear(y, m, d);\n  }\n\n  /**\n   * Sets the month of the instance. Equivalent to calling `setMonth` of `Date` object.\n   * @param {number} m - month (zero-based)\n   * @param {number} d - date\n   * @returns {number} - Passed milliseconds of the instance since 1970-01-01 00:00:00 (UTC+0).\n   */\n  setMonth(m: number, d = this.getDate()): number {\n    return this.d.setMonth(m, d);\n  }\n\n  /**\n   * Sets the date of the instance. Equivalent to calling `setDate` of `Date` object.\n   * @param {number} d - date\n   * @returns {number} - Passed milliseconds of the instance since 1970-01-01 00:00:00 (UTC+0).\n   */\n  setDate(d: number): number {\n    return this.d.setDate(d);\n  }\n\n  /**\n   * Sets the hours of the instance. Equivalent to calling `setHours` of `Date` object.\n   * @param {number} h - hours\n   * @param {number} M - minutes\n   * @param {number} s - seconds\n   * @param {number} ms - milliseconds\n   * @returns {number} - Passed milliseconds of the instance since 1970-01-01 00:00:00 (UTC+0).\n   */\n  setHours(\n    h: number,\n    M = this.getMinutes(),\n    s = this.getSeconds(),\n    ms = this.getMilliseconds()\n  ): number {\n    return this.d.setHours(h, M, s, ms);\n  }\n\n  /**\n   * Sets the minutes of the instance. Equivalent to calling `setMinutes` of `Date` object.\n   * @param {number} M - minutes\n   * @param {number} s - seconds\n   * @param {number} ms - milliseconds\n   * @returns {number} - Passed milliseconds of the instance since 1970-01-01 00:00:00 (UTC+0).\n   */\n  setMinutes(M: number, s = this.getSeconds(), ms = this.getMilliseconds()): number {\n    return this.d.setMinutes(M, s, ms);\n  }\n\n  /**\n   * Sets the seconds of the instance. Equivalent to calling `setSeconds` of `Date` object.\n   * @param {number} s - seconds\n   * @param {number} ms - milliseconds\n   * @returns {number} - Passed milliseconds of the instance since 1970-01-01 00:00:00 (UTC+0).\n   */\n  setSeconds(s: number, ms = this.getMilliseconds()): number {\n    return this.d.setSeconds(s, ms);\n  }\n\n  /**\n   * Sets the milliseconds of the instance. Equivalent to calling `setMilliseconds` of `Date` object.\n   * @param {number} ms - milliseconds\n   * @returns {number} - Passed milliseconds of the instance since 1970-01-01 00:00:00 (UTC+0).\n   */\n  setMilliseconds(ms: number): number {\n    return this.d.setMilliseconds(ms);\n  }\n\n  /**\n   * Set the timezone offset of the instance.\n   * @param {string|number} tzValue - The name of timezone(IANA name) or timezone offset(in minutes).\n   * @returns {TZDate} - New instance with the timezone offset.\n   */\n  tz(tzValue: string | 'Local' | number) {\n    if (tzValue === 'Local') {\n      return new TZDate(this.getTime());\n    }\n\n    const tzOffset = isString(tzValue) ? calculateTimezoneOffset(tzValue, this) : tzValue;\n\n    const newTZDate = new TZDate(this.getTime() - getTZOffsetMSDifference(tzOffset));\n    newTZDate.tzOffset = tzOffset;\n\n    return newTZDate;\n  }\n\n  /**\n   * Get the new instance following the system's timezone.\n   * If the system timezone is different from the timezone of the instance,\n   * the instance is converted to the system timezone.\n   *\n   * Instance's `tzOffset` property will be ignored if there is a `tzValue` parameter.\n   *\n   * @param {string|number} tzValue - The name of timezone(IANA name) or timezone offset(in minutes).\n   * @returns {TZDate} - New instance with the system timezone.\n   */\n  local(tzValue?: string | number) {\n    if (isPresent(tzValue)) {\n      const tzOffset = isString(tzValue) ? calculateTimezoneOffset(tzValue, this) : tzValue;\n      return new TZDate(this.getTime() + getTZOffsetMSDifference(tzOffset));\n    }\n\n    return new TZDate(\n      this.getTime() + (isPresent(this.tzOffset) ? getTZOffsetMSDifference(this.tzOffset) : 0)\n    );\n  }\n}\n"
  },
  {
    "path": "apps/calendar/src/time/datetime.spec.ts",
    "content": "import { INVALID_DATETIME_FORMAT } from '@src/constants/error';\nimport TZDate from '@src/time/date';\nimport {\n  addMonths,\n  clone,\n  compare,\n  isSameDate,\n  isSameMonth,\n  leadingZero,\n  makeDateRange,\n  millisecondsFrom,\n  MS_PER_DAY,\n  MS_PER_HOUR,\n  parse,\n  toEndOfDay,\n  toEndOfMonth,\n  toFormat,\n  toStartOfDay,\n  toStartOfMonth,\n  toStartOfYear,\n} from '@src/time/datetime';\n\ndescribe('millisecondsFrom', () => {\n  it('should convert value to milliseconds', () => {\n    // Given\n\n    // When\n    const result = millisecondsFrom('hour', 24);\n\n    // Then\n    expect(result).toBe(MS_PER_DAY);\n  });\n});\n\ndescribe('makeDateRange', () => {\n  it('should make date array by supplied dates', () => {\n    // Given\n    const start = new TZDate('2015/05/01');\n    const end = new TZDate('2015/05/03');\n\n    // When\n    const result = makeDateRange(start, end, MS_PER_DAY);\n\n    // Then\n    expect(result).toEqual([\n      new TZDate('2015/05/01'),\n      new TZDate('2015/05/02'),\n      new TZDate('2015/05/03'),\n    ]);\n  });\n\n  it('should have given steps', () => {\n    const start = new TZDate('2015/05/01 09:30:00');\n    const end = new TZDate('2015/05/01 18:30:00');\n\n    expect(makeDateRange(start, end, MS_PER_HOUR)).toEqual([\n      new TZDate('2015/05/01 09:30:00'),\n      new TZDate('2015/05/01 10:30:00'),\n      new TZDate('2015/05/01 11:30:00'),\n      new TZDate('2015/05/01 12:30:00'),\n      new TZDate('2015/05/01 13:30:00'),\n      new TZDate('2015/05/01 14:30:00'),\n      new TZDate('2015/05/01 15:30:00'),\n      new TZDate('2015/05/01 16:30:00'),\n      new TZDate('2015/05/01 17:30:00'),\n      new TZDate('2015/05/01 18:30:00'),\n    ]);\n  });\n});\n\ndescribe('toStartOfDay', () => {\n  it('should return 00:00:00 of supplied date', () => {\n    // Given\n    const date = new TZDate('2015/05/21 18:30:00');\n    const expected = new TZDate('2015/05/21 00:00:00');\n\n    // When\n    const result = toStartOfDay(date);\n\n    // Then\n    expect(result).toEqual(expected);\n  });\n});\n\ndescribe('toEndOfDay', () => {\n  it('should return 23:59:59.999 of supplied date', () => {\n    // Given\n    const date = new TZDate('2015/05/21 18:30:00');\n    const expected = new TZDate('2015-05-21T23:59:59.999');\n\n    // When\n    const result = toEndOfDay(date);\n\n    // Then\n    expect(result).toEqual(expected);\n  });\n});\n\ndescribe('clone', () => {\n  it('should clone TZDate object', () => {\n    // Given\n    const date = new TZDate();\n\n    // When\n    const clonedDate = clone(date);\n\n    // Then\n    expect(date).toEqual(clonedDate);\n  });\n});\n\ndescribe('compare', () => {\n  it('should return 0 when two parameters are equals', () => {\n    // Given\n    const d1 = new TZDate();\n    const d2 = new TZDate(d1.getTime());\n\n    // When\n    const result = compare(d1, d2);\n\n    // Then\n    expect(result).toBe(0);\n  });\n\n  it('should return 1 when the first parameter is later than second parameter', () => {\n    // Given\n    const date1 = new TZDate();\n    date1.setMinutes(date1.getMinutes() + 30);\n    const date2 = new TZDate();\n\n    // When\n    const result = compare(date1, date2);\n\n    // Then\n    expect(result).toBe(1);\n  });\n\n  it('should return -1 when the first parameter is early than second parameter', () => {\n    // Given\n    const date1 = new TZDate();\n    const date2 = new TZDate();\n    date2.setMinutes(date2.getMinutes() + 30);\n\n    // When\n    const result = compare(date1, date2);\n\n    expect(result).toBe(-1);\n  });\n});\n\ndescribe('leadingZero', () => {\n  it('should pad zero to supplied number and length', () => {\n    // Given\n\n    // When\n\n    // Then\n    expect(leadingZero(2, 2)).toBe('02');\n    expect(leadingZero(2, 3)).toBe('002');\n    expect(leadingZero(2300, 5)).toBe('02300');\n  });\n\n  it('should just convert to string and return if number length is longer than the given length', () => {\n    // Given\n\n    // When\n\n    // Then\n    expect(leadingZero(3000, 2)).toBe('3000');\n  });\n});\n\ndescribe('parse', () => {\n  it('should parse date string for safe usage', () => {\n    // Given\n    const dateStr1 = '2015-06-01 12:20:00';\n    const dateStr2 = '2015/06/01 10:00:00';\n    const dateStr3 = '20150601';\n\n    // When\n\n    // Then\n    expect(parse(dateStr1)).toEqual(new TZDate(2015, 5, 1, 12, 20, 0));\n    expect(parse(dateStr2)).toEqual(new TZDate(2015, 5, 1, 10, 0, 0));\n    expect(parse(dateStr3)).toEqual(new TZDate(2015, 5, 1, 0, 0, 0));\n  });\n\n  it('should return false when supplied date string is not valid', () => {\n    // Given\n    const validDateStr = '2015-05-01 00:00:00';\n    const invalidDateStr1 = '2015-5-1 3:00:00';\n    const invalidDateStr2 = '2015-06-21T22:00:00Z'; // ISO date format.\n\n    // When\n\n    // Then\n    expect(() => parse(validDateStr)).not.toThrow();\n    expect(() => parse(invalidDateStr1)).toThrowError(INVALID_DATETIME_FORMAT);\n    expect(() => parse(invalidDateStr2)).toThrowError(INVALID_DATETIME_FORMAT);\n  });\n\n  it('should adjust month value with fix options', () => {\n    // Given\n    const dateStr = '2015-05-01';\n    const expected = new TZDate(2015, 6, 1, 0, 0, 0);\n\n    // When\n    const result = parse(dateStr, 1);\n\n    // Then\n    expect(result).toEqual(expected);\n  });\n});\n\ndescribe('toFormat', () => {\n  it('should return formatted date string as basis of supplied string', () => {\n    // Given\n    const date = new TZDate('1988-09-25T15:30:00');\n\n    // When\n\n    // Then\n    expect(toFormat(date, 'YYYY')).toBe('1988');\n    expect(toFormat(date, 'MM')).toBe('09');\n    expect(toFormat(date, 'DD')).toBe('25');\n    expect(toFormat(date, 'YYYYMMDD')).toBe('19880925');\n    expect(toFormat(date, 'HH:mm')).toBe('15:30');\n  });\n});\n\ndescribe('isSameMonth', () => {\n  it('should return whether the 2 dates are the same month', () => {\n    // Given\n    const date1 = new TZDate('2015-06-12T09:30:00');\n    const date2 = new TZDate('2015-06-13T09:30:00');\n    const date3 = new TZDate('2015-07-12T09:30:00');\n\n    // When\n\n    // Then\n    expect(isSameMonth(date1, date2)).toBe(true);\n    expect(isSameMonth(date1, date3)).toBe(false);\n  });\n});\n\ndescribe('isSameDate', () => {\n  it('should return whether the 2 dates are the same date', () => {\n    // Given\n    const date1 = new TZDate('2015-06-12T09:30:00');\n    const date2 = new TZDate('2015-06-13T09:30:00');\n    const date3 = new TZDate('2015-07-12T09:30:00');\n\n    // When\n\n    // Then\n    expect(isSameDate(date1, date2)).toBe(false);\n    expect(isSameDate(date1, date3)).toBe(false);\n  });\n});\n\ndescribe('toStartOfMonth', () => {\n  it('should change the given date to the start date of month', () => {\n    // Given\n    const date = new TZDate('2015-11-24T09:30:00');\n\n    // When\n    const result = toStartOfMonth(date);\n\n    // Then\n    expect(result).toEqual(new TZDate('2015-11-01T00:00:00'));\n  });\n});\n\ndescribe('toEndOfMonth', () => {\n  it('should change the given date to the end date of month', () => {\n    // Given\n    const month = new TZDate('2015-11-24T09:30:00');\n\n    // When\n    const result = toEndOfMonth(month);\n\n    // Then\n    expect(result).toEqual(new TZDate('2015-11-30T23:59:59.999'));\n  });\n});\n\ndescribe('toStartOfYear', () => {\n  it('should change the given date to the start date of year', () => {\n    // Given\n    const date = new TZDate('2020-03-24T09:30:00');\n    const startOfYear = new TZDate('2020-01-01T00:00:00');\n\n    // When\n    const result = toStartOfYear(date);\n\n    // Then\n    expect(result).toEqual(startOfYear);\n  });\n});\n\ndescribe('addMonth/subtractMonth', () => {\n  it('should return the date on which the month was added to the given date', () => {\n    // Given\n    const date = new TZDate('2022-03-15T09:30:00');\n    const expected = new TZDate('2022-04-15T09:30:00');\n\n    // When\n    const result = addMonths(date, 1);\n\n    // Then\n    expect(result).toEqual(expected);\n  });\n\n  it('should return the date on which the month was added to the given date even if the month is a month without a calculated day', () => {\n    // Given\n    const date = new TZDate('2022-01-29T09:30:00');\n    const expected = new TZDate('2022-02-28T09:30:00');\n\n    // When\n    const result = addMonths(date, 1);\n\n    // Then\n    expect(result).toEqual(expected);\n  });\n\n  it('should return the date on which the month was added to the given date even if the month is in the leap year', () => {\n    // Given\n    const date = new TZDate('2020-01-29T09:30:00');\n    const expected = new TZDate('2020-02-29T09:30:00'); // 2020 year is a leap year\n\n    // When\n    const result = addMonths(date, 1);\n\n    // Then\n    expect(result).toEqual(expected);\n  });\n\n  it('should return the date on which the month was subtracted to the given date', () => {\n    // Given\n    const date = new TZDate('2022-04-15T09:30:00');\n    const expected = new TZDate('2022-03-15T09:30:00');\n\n    // When\n    const result = addMonths(date, -1);\n\n    // Then\n    expect(result).toEqual(expected);\n  });\n\n  it('should return the date on which the month was subtracted to the given date even if the month is a month without a calculated day', () => {\n    // Given\n    const date = new TZDate('2022-03-29T09:30:00');\n    const expected = new TZDate('2022-02-28T09:30:00');\n\n    // When\n    const result = addMonths(date, -1);\n\n    // Then\n    expect(result).toEqual(expected);\n  });\n\n  it('should return the date on which the month was subtracted to the given date even if the month is in the leap year', () => {\n    // Given\n    const date = new TZDate('2020-03-31T09:30:00');\n    const expected = new TZDate('2020-02-29T09:30:00'); // 2020 year is a leap year\n\n    // When\n    const result = addMonths(date, -1);\n\n    // Then\n    expect(result).toEqual(expected);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/time/datetime.ts",
    "content": "import range from 'tui-code-snippet/array/range';\n\nimport { toPercent } from '@src/helpers/css';\nimport TZDate from '@src/time/date';\nimport { fill } from '@src/utils/array';\nimport { InvalidDateTimeFormatError } from '@src/utils/error';\n\nimport type { TimeUnit } from '@t/events';\nimport type { CellStyle, FormattedTimeString } from '@t/time/datetime';\n\nexport enum Day {\n  SUN,\n  MON,\n  TUE,\n  WED,\n  THU,\n  FRI,\n  SAT,\n}\n\nexport const WEEK_DAYS = 7;\n\ninterface ReduceIteratee {\n  (previousValue: number, currentValue: number, currentIndex: number, array: number[]): number;\n}\n\nconst dateFormatRx = /^(\\d{4}[-|/]*\\d{2}[-|/]*\\d{2})\\s?(\\d{2}:\\d{2}:\\d{2})?$/;\n\nconst memo: {\n  millisecondsTo: Record<string, number>;\n  millisecondsFrom: Record<string, number>;\n} = {\n  millisecondsTo: {},\n  millisecondsFrom: {},\n};\n\nconst convByTimeUnit = [24, 60, 60, 1000];\n\n/**\n * pad left zero characters\n */\nexport function leadingZero(number: number, length: number): string {\n  let zero = '';\n  let i = 0;\n\n  if (String(number).length > length) {\n    return String(number);\n  }\n\n  for (; i < length - 1; i += 1) {\n    zero += '0';\n  }\n\n  return (zero + number).slice(length * -1);\n}\n\nfunction getHourForMeridiem(date: TZDate) {\n  let hour = date.getHours();\n\n  if (hour === 0) {\n    hour = 12;\n  }\n\n  if (hour > 12) {\n    hour = hour % 12;\n  }\n\n  return hour;\n}\n\nconst tokenFunc = {\n  YYYYMMDD(date: TZDate): string {\n    return [\n      date.getFullYear(),\n      leadingZero(date.getMonth() + 1, 2),\n      leadingZero(date.getDate(), 2),\n    ].join('');\n  },\n  YYYY(date: TZDate): string {\n    return String(date.getFullYear());\n  },\n  MM(date: TZDate): string {\n    return leadingZero(date.getMonth() + 1, 2);\n  },\n  DD(date: TZDate): string {\n    return leadingZero(date.getDate(), 2);\n  },\n  'HH:mm': function (date: TZDate): string {\n    const hour = date.getHours();\n    const minutes = date.getMinutes();\n\n    return `${leadingZero(hour, 2)}:${leadingZero(minutes, 2)}`;\n  },\n  'hh:mm': function (date: TZDate): string {\n    const hour = getHourForMeridiem(date);\n    const minutes = date.getMinutes();\n\n    return `${leadingZero(hour, 2)}:${leadingZero(minutes, 2)}`;\n  },\n  hh(date: TZDate): string {\n    const hour = getHourForMeridiem(date);\n\n    return String(hour);\n  },\n  tt(date: TZDate): string {\n    const hour = date.getHours();\n\n    return hour < 12 ? 'am' : 'pm';\n  },\n};\n\nexport const MS_PER_DAY = 86400000;\nexport const MS_PER_HOUR = 3600000;\nexport const MS_PER_MINUTES = 60000;\n\n/**\n * The number of milliseconds 20 minutes for event min duration\n */\nexport const MS_EVENT_MIN_DURATION = 20 * MS_PER_MINUTES;\nexport const MS_PER_THIRTY_MINUTES = 30 * 60 * 1000;\nexport const SIXTY_SECONDS = 60;\n\n/**\n * Return formatted string as basis of supplied string.\n *\n * Supported Token Lists.\n *\n * - YYYY => 1988\n * - MM => 01 ~ 12\n * - DD => 01 ~ 31\n * - YYYYMMDD => 19880925\n */\nexport function toFormat(date: TZDate, strFormat: string): string {\n  let result = strFormat;\n\n  Object.entries(tokenFunc).forEach(([token, converter]: [string, (d: TZDate) => string]) => {\n    result = result.replace(token, converter(date));\n  });\n\n  return result;\n}\n\n/**\n * convert to milliseconds\n */\nfunction convMilliseconds(type: TimeUnit, value: number, iteratee: ReduceIteratee): number {\n  const index: Partial<Record<TimeUnit, number>> = {\n    date: 0,\n    hour: 1,\n    minute: 2,\n    second: 3,\n  };\n\n  if (!(type in index) || isNaN(value)) {\n    return 0;\n  }\n\n  return [value].concat(convByTimeUnit.slice(index[type])).reduce(iteratee);\n}\n\n/**\n * Convert value to milliseconds\n */\nexport function millisecondsFrom(type: TimeUnit, value: number): number {\n  const cache = memo.millisecondsFrom;\n  const key = type + value;\n\n  if (cache[key]) {\n    return cache[key];\n  }\n\n  const result = convMilliseconds(type, value, (m: number, v: number) => m * v);\n\n  if (!result) {\n    return 0;\n  }\n\n  cache[key] = result;\n\n  return cache[key];\n}\n\n/**\n * Return 00:00:00 supplied date\n */\nexport function toStartOfDay(date?: number | TZDate | Date): TZDate {\n  const d = date ? new TZDate(date) : new TZDate();\n  d.setHours(0, 0, 0, 0);\n\n  return d;\n}\n\n/**\n * Make date array from supplied parameters\n */\nexport function makeDateRange(startDate: TZDate, endDate: TZDate, step: number): TZDate[] {\n  const startTime = startDate.getTime();\n  const endTime = endDate.getTime();\n  const date = new TZDate(startDate);\n  const result: TZDate[] = [];\n  let cursor = startTime;\n\n  while (cursor <= endTime && endTime >= date.getTime()) {\n    result.push(new TZDate(date));\n    cursor = cursor + step;\n    date.addMilliseconds(step);\n  }\n\n  return result;\n}\n\n/**\n * Clone supplied date\n */\nexport function clone(date: TZDate): TZDate {\n  return new TZDate(date);\n}\n\n/**\n * Compare two dates.\n *\n * when first date is latest then seconds then return -1.\n *\n * return +1 reverse, and return 0 is same.\n */\nexport function compare(d1: TZDate, d2: TZDate): number {\n  const _d1 = d1.getTime();\n  const _d2 = d2.getTime();\n\n  if (_d1 < _d2) {\n    return -1;\n  }\n  if (_d1 > _d2) {\n    return 1;\n  }\n\n  return 0;\n}\n\nexport function isSameYear(d1: TZDate, d2: TZDate): boolean {\n  return d1.getFullYear() === d2.getFullYear();\n}\n\nexport function isSameMonth(d1: TZDate, d2: TZDate): boolean {\n  return isSameYear(d1, d2) && d1.getMonth() === d2.getMonth();\n}\n\nexport function isSameDate(d1: TZDate, d2: TZDate): boolean {\n  return isSameMonth(d1, d2) && d1.getDate() === d2.getDate();\n}\n\nexport function max(d1: TZDate, d2: TZDate): TZDate {\n  return compare(d1, d2) === 1 ? d1 : d2;\n}\n\nexport function min(d1: TZDate, d2: TZDate): TZDate {\n  return compare(d1, d2) === -1 ? d1 : d2;\n}\n\n/**\n * Convert date string to date object.\n * Only listed below formats available.\n *\n * - YYYYMMDD\n * - YYYY/MM/DD\n * - YYYY-MM-DD\n * - YYYY/MM/DD HH:mm:SS\n * - YYYY-MM-DD HH:mm:SS\n */\nexport function parse(str: string, fixMonth = -1): TZDate {\n  const matches = str.match(dateFormatRx);\n  let separator;\n  let ymd;\n  let hms;\n\n  if (!matches) {\n    throw new InvalidDateTimeFormatError(str);\n  }\n\n  if (str.length > 8) {\n    // YYYY/MM/DD\n    // YYYY-MM-DD\n    // YYYY/MM/DD HH:mm:SS\n    // YYYY-MM-DD HH:mm:SS\n    separator = ~str.indexOf('/') ? '/' : '-';\n    const result = matches.splice(1);\n\n    ymd = result[0].split(separator);\n    hms = result[1] ? result[1].split(':') : [0, 0, 0];\n  } else {\n    // YYYYMMDD\n    const [result] = matches;\n    ymd = [result.substr(0, 4), result.substr(4, 2), result.substr(6, 2)];\n    hms = [0, 0, 0];\n  }\n\n  return new TZDate().setWithRaw(\n    Number(ymd[0]),\n    Number(ymd[1]) + fixMonth,\n    Number(ymd[2]),\n    Number(hms[0]),\n    Number(hms[1]),\n    Number(hms[2]),\n    0\n  );\n}\n\n/**\n * Return 23:59:59 supplied date.\n * If you want to use milliseconds, use format 'YYYY-MM-DDTHH:mm:ss.sssZ' based on http://www.ecma-international.org/ecma-262/5.1/#sec-15.9.1.15\n */\nexport function toEndOfDay(date?: number | TZDate): TZDate {\n  const d = date ? new TZDate(date) : new TZDate();\n  d.setHours(23, 59, 59, 999);\n\n  return d;\n}\n\nexport function isWeekend(day: Day): boolean {\n  return day === Day.SUN || day === Day.SAT;\n}\n\nexport function isSunday(day: Day): boolean {\n  return day === Day.SUN;\n}\n\nexport function isSaturday(day: Day): boolean {\n  return day === Day.SAT;\n}\n\n/**\n * Whether date is between supplied dates with date value?\n */\nexport function isBetweenWithDate(d: TZDate, d1: TZDate, d2: TZDate): boolean {\n  const format = 'YYYYMMDD';\n  const n = parseInt(toFormat(d, format), 10);\n  const n1 = parseInt(toFormat(d1, format), 10);\n  const n2 = parseInt(toFormat(d2, format), 10);\n\n  return n1 <= n && n <= n2;\n}\n\nexport function toStartOfMonth(date: TZDate): TZDate {\n  const startDate = new TZDate(date);\n\n  startDate.setDate(1);\n  startDate.setHours(0, 0, 0, 0);\n\n  return startDate;\n}\n\nexport function toStartOfYear(d: TZDate): TZDate {\n  return new TZDate(d.getFullYear(), 0, 1, 0, 0, 0, 0);\n}\n\nexport function toEndOfMonth(date: TZDate): TZDate {\n  const endDate = toStartOfMonth(date);\n\n  endDate.setMonth(endDate.getMonth() + 1);\n  endDate.setDate(endDate.getDate() - 1);\n  endDate.setHours(23, 59, 59, 999);\n\n  return endDate;\n}\n\n/**\n * Calculate grid left(%), width(%) by narrowWeekend, startDayOfWeek, workweek\n */\nexport function getRowStyleInfo(\n  days: number,\n  narrowWeekend: boolean,\n  startDayOfWeek: number,\n  workweek: boolean\n): { rowStyleInfo: CellStyle[]; cellWidthMap: string[][] } {\n  const limitDaysToApplyNarrowWeekend = 5;\n  const uniformWidth = 100 / days;\n  const wideWidth = days > limitDaysToApplyNarrowWeekend ? 100 / (days - 1) : uniformWidth;\n  let accumulatedWidth = 0;\n  const dates = range(startDayOfWeek, WEEK_DAYS).concat(range(days)).slice(0, WEEK_DAYS);\n\n  narrowWeekend = workweek ? false : narrowWeekend;\n\n  const rowStyleInfo = dates.map((day: number) => {\n    let width = narrowWeekend ? wideWidth : uniformWidth;\n    if (days > limitDaysToApplyNarrowWeekend && narrowWeekend && isWeekend(day)) {\n      width = wideWidth / 2;\n    }\n\n    const model = {\n      width,\n      left: accumulatedWidth,\n    };\n\n    accumulatedWidth += width;\n\n    return model;\n  });\n\n  const { length } = rowStyleInfo;\n  const cellWidthMap = fill(length, fill(length, 0));\n\n  rowStyleInfo.forEach(({ width }, index) => {\n    for (let i = 0; i <= index; i += 1) {\n      for (let j = index; j < length; j += 1) {\n        cellWidthMap[i][j] += width;\n      }\n    }\n  });\n\n  cellWidthMap[0][length - 1] = 100;\n\n  return {\n    rowStyleInfo,\n    cellWidthMap: cellWidthMap.map((widthList) => widthList.map(toPercent)),\n  };\n}\n\nexport function addMilliseconds(d: TZDate, step: number) {\n  const date = clone(d);\n  date.setMilliseconds(d.getMilliseconds() + step);\n\n  return date;\n}\n\nexport function addMinutes(d: TZDate, step: number) {\n  const date = clone(d);\n  date.setMinutes(d.getMinutes() + step);\n\n  return date;\n}\n\nexport function addHours(d: TZDate, step: number) {\n  const date = clone(d);\n  date.setHours(d.getHours() + step);\n\n  return date;\n}\n\nexport function setTimeStrToDate(d: TZDate, timeStr: FormattedTimeString) {\n  const date = clone(d);\n  date.setHours(...(timeStr.split(':').map(Number) as [number, number]));\n\n  return date;\n}\n\nexport function addDate(d: TZDate, step: number) {\n  const date = clone(d);\n  date.setDate(d.getDate() + step);\n\n  return date;\n}\n\nexport function subtractDate(d: TZDate, steps: number) {\n  const date = clone(d);\n  date.setDate(d.getDate() - steps);\n\n  return date;\n}\n\n/**\n * Inspired by `date-fns`\n *\n * See more: https://github.com/date-fns/date-fns/blob/master/src/addMonths/index.ts\n */\nexport function addMonths(d: TZDate, step = 1) {\n  const date = clone(d);\n\n  if (step !== 0) {\n    const dayOfMonth = date.getDate();\n    const endOfDesiredMonth = new TZDate(date.getTime());\n    endOfDesiredMonth.setMonth(date.getMonth() + step + 1, 0);\n    const daysInMonth = endOfDesiredMonth.getDate();\n\n    if (dayOfMonth >= daysInMonth) {\n      return endOfDesiredMonth;\n    }\n\n    date.setFullYear(endOfDesiredMonth.getFullYear(), endOfDesiredMonth.getMonth(), dayOfMonth);\n  }\n\n  return date;\n}\n\nexport function addYear(d: TZDate, step: number) {\n  const date = clone(d);\n  date.setFullYear(d.getFullYear() + step);\n\n  return date;\n}\n\nexport function getDateDifference(d1: TZDate, d2: TZDate) {\n  const _d1 = new TZDate(d1.getFullYear(), d1.getMonth(), d1.getDate()).getTime();\n  const _d2 = new TZDate(d2.getFullYear(), d2.getMonth(), d2.getDate()).getTime();\n\n  return Math.round((_d1 - _d2) / MS_PER_DAY);\n}\n"
  },
  {
    "path": "apps/calendar/src/time/timezone.spec.ts",
    "content": "import { expect } from '@playwright/test';\nimport { LocalDate, MomentDate, UTCDate } from '@toast-ui/date';\nimport { advanceTo } from 'jest-date-mock';\nimport moment from 'moment-timezone';\nimport type { TimeZone } from 'timezone-mock';\nimport { register, unregister } from 'timezone-mock';\n\nimport TZDate from '@src/time/date';\nimport { calculateTimezoneOffset, date, isUsingDST, setDateConstructor } from '@src/time/timezone';\n\nMomentDate.setMoment(moment);\n\n/**\n * First mocking jest and then mocking timezone\n * @param {TimeZone} timezoneName\n * @param {string} initialDate\n */\nfunction startMockingTimezone(timezoneName: TimeZone, initialDate: string) {\n  advanceTo(new Date(initialDate).getTime());\n  register(timezoneName);\n}\n\n/**\n * First uninstall jest and then unregister.\n */\nfunction finishMockingTimezone() {\n  unregister();\n}\n\ndescribe('UTCDate', () => {\n  beforeEach(() => {\n    setDateConstructor(UTCDate);\n  });\n\n  afterEach(() => {\n    setDateConstructor(LocalDate);\n  });\n\n  it('use UTC+0', () => {\n    const utcDate = new Date(2020, 0, 20, 0, 0, 0);\n    const tzDate = date('2020-01-20T00:00:00');\n\n    expect(tzDate.getTime()).toBe(utcDate.getTime());\n    expect(tzDate.getMonth()).toBe(utcDate.getUTCMonth());\n    expect(tzDate.getDate()).toBe(utcDate.getUTCDate());\n    expect(tzDate.getHours()).toBe(utcDate.getUTCHours());\n  });\n});\n\ndescribe('LocalDate', () => {\n  it('use local timezone offset', () => {\n    const localDate = new Date(2020, 0, 20, 0, 0, 0);\n    const tzDate = date('2020-01-20T00:00:00');\n\n    expect(tzDate.getTime()).toBe(localDate.getTime());\n    expect(tzDate.getMonth()).toBe(localDate.getMonth());\n    expect(tzDate.getDate()).toBe(localDate.getDate());\n    expect(tzDate.getHours()).toBe(localDate.getHours());\n  });\n});\n\ndescribe('MomentDate', () => {\n  beforeEach(() => {\n    setDateConstructor(MomentDate);\n  });\n\n  afterEach(() => {\n    setDateConstructor(LocalDate);\n  });\n\n  it('use moment instance with local date', () => {\n    const localDate = new Date('2020-01-20T00:00:00Z');\n    const tzDate = date('2020-01-20T00:00:00Z');\n\n    expect(tzDate.getTime()).toBe(localDate.getTime());\n    expect(tzDate.getMonth()).toBe(localDate.getMonth());\n    expect(tzDate.getDate()).toBe(localDate.getDate());\n    expect(tzDate.getHours()).toBe(localDate.getHours());\n  });\n\n  it('use moment instance with utc date', () => {\n    const utcDate = new Date('2020-01-20T00:00:00Z');\n    const tzDate = date('2020-01-20T00:00:00Z').setTimezoneOffset(0);\n\n    expect(tzDate.getTime()).toBe(utcDate.getTime());\n    expect(tzDate.getMonth()).toBe(utcDate.getUTCMonth());\n    expect(tzDate.getDate()).toBe(utcDate.getUTCDate());\n    expect(tzDate.getHours()).toBe(utcDate.getUTCHours());\n  });\n\n  it('use moment instance with timezone name, PST', () => {\n    startMockingTimezone('US/Pacific', '2020-01-20T00:00:00');\n\n    const localDate = new Date('2020-06-20T00:00:00');\n    const tzDate = date('2020-06-20T00:00:00').setTimezoneName('US/Pacific');\n\n    expect(tzDate.getTime()).toBe(localDate.getTime());\n    expect(tzDate.getMonth()).toBe(localDate.getMonth());\n    expect(tzDate.getDate()).toBe(localDate.getDate());\n    expect(tzDate.getHours()).toBe(localDate.getHours());\n\n    finishMockingTimezone();\n  });\n\n  it('use moment instance with timezone name, PDT', () => {\n    startMockingTimezone('US/Pacific', '2020-06-20T00:00:00');\n\n    const localDate = new Date('2021-01-20T00:00:00');\n    const tzDate = date('2021-01-20T00:00:00').setTimezoneName('US/Pacific');\n\n    expect(tzDate.getTime()).toBe(localDate.getTime());\n    expect(tzDate.getMonth()).toBe(localDate.getMonth());\n    expect(tzDate.getDate()).toBe(localDate.getDate());\n    expect(tzDate.getHours()).toBe(localDate.getHours());\n\n    finishMockingTimezone();\n  });\n});\n\ndescribe('calculateTimezoneOffset', () => {\n  beforeEach(() => {\n    register('UTC');\n  });\n\n  afterEach(() => {\n    unregister();\n  });\n\n  it('should calculate timezone offset of date which is applicable DST - Northern Hemisphere', () => {\n    // Given\n    const timezoneName = 'US/Pacific';\n    const tzDate = new TZDate('2022-04-12T00:00:00');\n\n    // When\n    const offset = calculateTimezoneOffset(timezoneName, tzDate);\n\n    // Then\n    // Pacific Daylight Time (PDT) is UTC -7.\n    expect(offset).toBe(-420);\n  });\n\n  it('should calculate timezone offset of date which is not applicable DST - Northern Hemisphere', () => {\n    // Given\n    const timezoneName = 'US/Pacific';\n    // Pacific Daylight Time (PDS) is end on 2022/11/06 02:00 in Pacific Time.\n    // So add 7 hours to get UTC time.\n    const tzDate = new TZDate('2022-11-06T09:00:00');\n\n    // When\n    const offset = calculateTimezoneOffset(timezoneName, tzDate);\n\n    // Then\n    expect(offset).toBe(-480);\n  });\n\n  it('should calculate timezone offset of date which is applicable DST - Southern Hemisphere', () => {\n    // Given\n    const timezoneName = 'Australia/Sydney';\n    // Australian Eastern Daylight Time (AEDT) is UTC +11.\n    const tzDate = new TZDate('2022-11-06T09:00:00');\n\n    // When\n    const offset = calculateTimezoneOffset(timezoneName, tzDate);\n\n    // Then\n    expect(offset).toBe(660);\n  });\n\n  it('should calculate timezone offset of date which is not applicable DST - Southern Hemisphere', () => {\n    // Given\n    const timezoneName = 'Australia/Sydney';\n    // Australian Eastern Standard Time (AEST) is UTC +10.\n    const tzDate = new TZDate('2022-04-06T09:00:00');\n\n    // When\n    const offset = calculateTimezoneOffset(timezoneName, tzDate);\n\n    // Then\n    expect(offset).toBe(600);\n  });\n\n  it('should return positive number when the timezone is UTC+', () => {\n    // Given\n    const timezoneName = 'Asia/Seoul'; // UTC+9\n    const tzDate = new TZDate('2022-11-06T09:00:00');\n\n    // When\n    const offset = calculateTimezoneOffset(timezoneName, tzDate);\n\n    // Then\n    expect(offset).toBe(540);\n  });\n\n  it('should throw if the timezone name is invalid', () => {\n    // Given\n    const invalidTimezoneName = 'Invalid/Timezone';\n    const tzDate = new TZDate('2022-04-12T00:00:00');\n\n    // When\n    const fn = () => calculateTimezoneOffset(invalidTimezoneName, tzDate);\n\n    // Then\n    expect(fn).toThrow();\n  });\n});\n\ndescribe('isUsingDST', () => {\n  afterEach(() => {\n    unregister();\n  });\n\n  it('should determine the OS timezone is using DST (UTC)', () => {\n    // Given\n    register('UTC');\n    const tzDate = new TZDate('2022-04-12T00:00:00');\n\n    // When\n    const result = isUsingDST(tzDate);\n\n    // Then\n    expect(result).toBe(false);\n  });\n\n  it('should determine the OS timezone is using DST', () => {\n    // Given\n    register('US/Pacific');\n    const tzDateInPDT = new TZDate('2022-04-12T00:00:00');\n    const tzDateInPST = new TZDate('2022-11-06T03:00:00');\n\n    // When\n    const result1 = isUsingDST(tzDateInPDT);\n    const result2 = isUsingDST(tzDateInPST);\n\n    // Then\n    expect(result1).toBe(true);\n    expect(result2).toBe(false);\n  });\n\n  it('should determine the given timezone is using DST', () => {\n    // Given\n    register('UTC');\n    const timezoneName = 'US/Pacific';\n    const tzDateInPDT = new TZDate('2022-04-12T00:00:00');\n    const tzDateInPST = new TZDate('2022-11-06T09:00:00'); // +6 hours because it is UTC\n\n    // When\n    const result1 = isUsingDST(tzDateInPDT, timezoneName);\n    const result2 = isUsingDST(tzDateInPST, timezoneName);\n\n    // Then\n    expect(result1).toBe(true);\n    expect(result2).toBe(false);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/time/timezone.ts",
    "content": "import type { TuiDateConstructor } from '@toast-ui/date';\nimport { LocalDate } from '@toast-ui/date';\n\nimport TZDate from '@src/time/date';\nimport { InvalidTimezoneNameError } from '@src/utils/error';\nimport { logger } from '@src/utils/logger';\nimport { isFunction, isPresent } from '@src/utils/type';\n\nlet Constructor: TuiDateConstructor = LocalDate;\n\nexport function setDateConstructor(constructor: TuiDateConstructor) {\n  Constructor = constructor;\n}\n\nexport function date(...args: any[]) {\n  return new Constructor(...args);\n}\n\n// Get the timezone offset from the system using the calendar.\nexport function getLocalTimezoneOffset() {\n  return -new Date().getTimezoneOffset();\n}\n\n/**\n * Calculate timezone offset from UTC.\n *\n * Target date is needed for the case when the timezone is applicable to DST.\n */\nexport function calculateTimezoneOffset(timezoneName: string, targetDate: TZDate = new TZDate()) {\n  if (!isIntlDateTimeFormatSupported()) {\n    logger.warn(\n      'Intl.DateTimeFormat is not fully supported. So It will return the local timezone offset only.\\nYou can use a polyfill to fix this issue.'\n    );\n\n    return -targetDate.toDate().getTimezoneOffset();\n  }\n\n  validateIANATimezoneName(timezoneName);\n\n  const token = tokenizeTZDate(targetDate, timezoneName);\n  const utcDate = tokenToUtcDate(token);\n\n  return Math.round((utcDate.getTime() - targetDate.getTime()) / 60 / 1000);\n}\n\n// Reference: https://stackoverflow.com/a/30280636/16702531\n// If there's no timezoneName, it handles Native OS timezone.\nexport function isUsingDST(targetDate: TZDate, timezoneName?: string) {\n  if (timezoneName) {\n    validateIANATimezoneName(timezoneName);\n  }\n\n  const jan = new TZDate(targetDate.getFullYear(), 0, 1);\n  const jul = new TZDate(targetDate.getFullYear(), 6, 1);\n\n  if (timezoneName) {\n    return (\n      Math.max(\n        -calculateTimezoneOffset(timezoneName, jan),\n        -calculateTimezoneOffset(timezoneName, jul)\n      ) !== -calculateTimezoneOffset(timezoneName, targetDate)\n    );\n  }\n\n  return (\n    Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset()) !==\n    targetDate.toDate().getTimezoneOffset()\n  );\n}\n\nconst dtfCache: Record<string, Intl.DateTimeFormat> = {};\nconst timezoneNameValidationCache: Record<string, boolean> = {};\n\nfunction isIntlDateTimeFormatSupported() {\n  /**\n   * Intl.DateTimeFormat & IANA Timezone Data should be supported.\n   * also, hourCycle options should be supported.\n   */\n  return isFunction(Intl?.DateTimeFormat?.prototype?.formatToParts);\n}\n\nfunction validateIANATimezoneName(timezoneName: string) {\n  if (timezoneNameValidationCache[timezoneName]) {\n    return true;\n  }\n\n  try {\n    // Just try to create a dtf with the timezoneName.\n    // eslint-disable-next-line new-cap\n    Intl.DateTimeFormat('en-US', { timeZone: timezoneName });\n    timezoneNameValidationCache[timezoneName] = true;\n\n    return true;\n  } catch {\n    // Usually it throws `RangeError` when the timezoneName is invalid.\n    throw new InvalidTimezoneNameError(timezoneName);\n  }\n}\n\nfunction getDateTimeFormat(timezoneName: string) {\n  if (dtfCache[timezoneName]) {\n    return dtfCache[timezoneName];\n  }\n  const dtf = new Intl.DateTimeFormat('en-US', {\n    timeZone: timezoneName,\n    hourCycle: 'h23',\n    hour12: false,\n    year: 'numeric',\n    month: 'numeric',\n    day: 'numeric',\n    hour: 'numeric',\n    minute: 'numeric',\n    second: 'numeric',\n  });\n  dtfCache[timezoneName] = dtf;\n\n  return dtf;\n}\n\ntype TokenizeTarget = Extract<\n  Intl.DateTimeFormatPartTypes,\n  'year' | 'month' | 'day' | 'hour' | 'minute' | 'second'\n>;\nconst typeToPos: Record<TokenizeTarget, number> = {\n  year: 0,\n  month: 1,\n  day: 2,\n  hour: 3,\n  minute: 4,\n  second: 5,\n};\n\ntype TokenizeResult = [number, number, number, number, number, number];\nfunction tokenizeTZDate(tzDate: TZDate, timezoneName: string): TokenizeResult {\n  const dtf = getDateTimeFormat(timezoneName);\n  const formatted = dtf.formatToParts(tzDate.toDate());\n\n  return formatted.reduce<TokenizeResult>((result, cur) => {\n    const pos = typeToPos[cur.type as TokenizeTarget];\n\n    if (isPresent(pos)) {\n      result[pos] = parseInt(cur.value, 10);\n    }\n\n    return result;\n  }, [] as unknown as TokenizeResult);\n}\n\nfunction tokenToUtcDate(token: TokenizeResult) {\n  const [year, monthPlusOne, day, hour, minute, second] = token;\n  const month = monthPlusOne - 1;\n\n  return new Date(Date.UTC(year, month, day, hour % 24, minute, second));\n}\n"
  },
  {
    "path": "apps/calendar/src/tui-code-snippet.d.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\ndeclare module 'tui-code-snippet/type/isUndefined' {\n  export default function isUndefined(value: unknown): value is undefined;\n}\n\ndeclare module 'tui-code-snippet/type/isObject' {\n  export default function isObject(value: unknown): value is object;\n}\n\ndeclare module 'tui-code-snippet/type/isString' {\n  export default function isString(value: unknown): value is string;\n}\n\ndeclare module 'tui-code-snippet/type/isNumber' {\n  export default function isNumber(value: unknown): value is number;\n}\n\ndeclare module 'tui-code-snippet/type/isBoolean' {\n  export default function isBoolean(value: unknown): value is boolean;\n}\n\ndeclare module 'tui-code-snippet/collection/pluck' {\n  export default function pluck<T, K extends keyof T>(arr: T[], property: K): T[K][];\n}\n\ndeclare module 'tui-code-snippet/array/range' {\n  export default function range(start: number, stop?: number, step?: number): number[];\n}\n\ndeclare module 'tui-code-snippet/domEvent/getMousePosition' {\n  export default function getMousePosition(\n    event: MouseEvent,\n    relativeElement?: HTMLElement | null\n  ): [number, number];\n}\n\ndeclare module 'tui-code-snippet/domEvent/getTarget' {\n  export default function getTarget(event: Event): HTMLElement;\n}\n\ndeclare module 'tui-code-snippet/domUtil/addClass' {\n  export default function addClass(element: Element, ...classes: string[]): void;\n}\n\ndeclare module 'tui-code-snippet/domUtil/removeClass' {\n  export default function removeClass(element: Element, ...classes: string[]): void;\n}\n\ndeclare module 'tui-code-snippet/browser/browser' {\n  interface Browser {\n    chrome: boolean;\n    firefox: boolean;\n    safari: boolean;\n    msie: boolean;\n    edge: boolean;\n    others: boolean;\n    version: number;\n  }\n\n  const browser: Browser;\n  export default browser;\n}\n\ndeclare module 'tui-code-snippet/customEvents/customEvents' {\n  export default class CustomEvents {\n    public static mixin(func: Function): void;\n\n    public on(eventName: string, handler: Function, context?: object): void;\n\n    public once(eventName: string, handler: Function, context?: object): void;\n\n    public off(eventName?: string, handler?: Function): void;\n\n    public fire(eventName: string, ...args: any[]): void;\n\n    public invoke(eventName: string, ...args: any[]): boolean;\n  }\n}\n\ndeclare module 'tui-code-snippet/request/sendHostname' {\n  export default function sendHostname(appName: string, trackingId: string): void;\n}\n"
  },
  {
    "path": "apps/calendar/src/types/components/common.ts",
    "content": "import type { ComponentChildren, h } from 'preact';\n\nexport type PropsWithChildren<Props = {}> = Props & { children?: ComponentChildren };\n\nexport type StyleProp = h.JSX.CSSProperties;\n\nexport type FormEvent = h.JSX.TargetedEvent<HTMLFormElement, Event>;\n\nexport type CalendarViewType = 'week' | 'month';\n"
  },
  {
    "path": "apps/calendar/src/types/components/gridSelection.ts",
    "content": "export interface GridSelectionData {\n  startRowIndex: number;\n  startColumnIndex: number;\n  endRowIndex: number;\n  endColumnIndex: number;\n}\n\nexport interface GridSelectionDataByRow {\n  startCellIndex: number;\n  endCellIndex: number;\n}\n\nexport interface TimeGridSelectionDataByCol {\n  startRowIndex: number;\n  endRowIndex: number;\n  isStartingColumn: boolean;\n  isSelectingMultipleColumns: boolean;\n}\n"
  },
  {
    "path": "apps/calendar/src/types/drag.ts",
    "content": "import type { GridSelectionType } from '@src/slices/gridSelection';\n\nexport type EventDraggingArea = 'dayGrid' | 'timeGrid';\n\nexport type EventDraggingBehavior = 'move' | 'resize';\n\nexport type EventDragging<EventId extends string = any> =\n  `event/${EventDraggingArea}/${EventDraggingBehavior}/${EventId}`;\n\nexport type GridSelectionDragging = `gridSelection/${GridSelectionType}`;\n\nexport type DraggingTypes<EventId extends string = any> =\n  | GridSelectionDragging\n  | EventDragging<EventId>\n  | 'panelResizer';\n"
  },
  {
    "path": "apps/calendar/src/types/eventBus.ts",
    "content": "import type { EventObject, EventObjectWithDefaultValues } from '@t/events';\n\nexport type AnyFunc = (...args: any[]) => any;\n\nexport interface SelectDateTimeInfo {\n  start: Date;\n  end: Date;\n  isAllday: boolean;\n  nativeEvent?: MouseEvent;\n  gridSelectionElements: Element[];\n}\n\nexport interface UpdatedEventInfo {\n  event: EventObjectWithDefaultValues;\n  changes: EventObject;\n}\n\nexport interface DayNameInfo {\n  date: string;\n}\n\nexport interface EventInfo {\n  event: EventObjectWithDefaultValues;\n  nativeEvent: MouseEvent;\n}\n\nexport interface MoreEventsButton {\n  date: Date;\n  target: HTMLDivElement; // NOTE: More events popup element\n}\n\nexport type ScrollBehaviorOptions = ScrollToOptions['behavior'];\n\nexport type ExternalEventTypes = {\n  selectDateTime: (info: SelectDateTimeInfo) => void;\n  beforeCreateEvent: (event: EventObject) => void;\n  beforeUpdateEvent: (updatedEventInfo: UpdatedEventInfo) => void;\n  beforeDeleteEvent: (event: EventObjectWithDefaultValues) => void;\n  afterRenderEvent: (event: EventObjectWithDefaultValues) => void;\n  clickDayName: (dayNameInfo: DayNameInfo) => void;\n  clickEvent: (eventInfo: EventInfo) => void;\n  clickMoreEventsBtn: (moreEventsBtnInfo: MoreEventsButton) => void;\n  clickTimezonesCollapseBtn: (prevCollapsedState: boolean) => void;\n  [eventName: string]: AnyFunc;\n};\n\nexport type InternalEventTypes = {\n  scrollToNow: (scrollBehavior?: ScrollBehaviorOptions) => void;\n};\n"
  },
  {
    "path": "apps/calendar/src/types/events.ts",
    "content": "import type { MarkOptional } from 'ts-essentials';\n\nimport type EventModel from '@src/model/eventModel';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport type TZDate from '@src/time/date';\nimport type Collection from '@src/utils/collection';\n\nimport type { StyleProp } from '@t/components/common';\nimport type { CalendarInfo } from '@t/options';\n\nexport type Matrix<T> = T[][];\nexport type Matrix3d<T> = Matrix<T>[];\nexport type CollisionGroup = Matrix<number>;\n\nexport type DayGridEventMatrix = Matrix3d<EventUIModel>;\nexport type TimeGridEventMatrix = Record<string, Matrix3d<EventUIModel>>;\n\nexport type EventModelMap = {\n  milestone: EventUIModel[];\n  allday: EventUIModel[];\n  task: EventUIModel[];\n  time: EventUIModel[];\n};\n\nexport type EventGroupMap = Record<keyof EventModelMap, DayGridEventMatrix | TimeGridEventMatrix>;\n\nexport type DateType = Date | string | number | TZDate;\n\nexport type IDS_OF_DAY = Record<string, number[]>;\n\nexport interface CalendarData {\n  calendars: CalendarInfo[];\n  events: Collection<EventModel>;\n  idsOfDay: IDS_OF_DAY;\n}\n\nexport type EventCategory = 'milestone' | 'task' | 'allday' | 'time'; // | 'background';\n\nexport type EventState = 'Busy' | 'Free';\n\nexport type EventObjectWithDefaultValues = MarkOptional<\n  Required<EventObject>,\n  'color' | 'borderColor' | 'backgroundColor' | 'dragBackgroundColor'\n> & {\n  start: TZDate;\n  end: TZDate;\n  __cid: number;\n};\n\nexport interface EventObject {\n  /**\n   * `Optional` unique id for various use\n   */\n  id?: string;\n\n  /**\n   * Calendar ID\n   */\n  calendarId?: string;\n\n  /**\n   * Title for the event\n   */\n  title?: string;\n\n  /**\n   * Body for the event\n   */\n  body?: string;\n\n  /**\n   * Determine if the event is an all-day event\n   */\n  isAllday?: boolean;\n\n  /**\n   * When the event starts\n   */\n  start?: DateType;\n\n  /**\n   * When the event ends\n   */\n  end?: DateType;\n\n  /**\n   * Travel time which is taken to go\n   */\n  goingDuration?: number;\n\n  /**\n   * Travel time which is taken to come back\n   */\n  comingDuration?: number;\n\n  /**\n   * Location of the event\n   */\n  location?: string;\n\n  /**\n   * Attendees of the event\n   */\n  attendees?: string[];\n\n  /**\n   * Category of the event (milestone, task, allday, time)\n   */\n  category?: EventCategory;\n\n  /**\n   * Classification of work events (before work, before lunch, before work)\n   */\n  dueDateClass?: string;\n\n  /**\n   * Recurrence rule of the event\n   */\n  recurrenceRule?: string;\n\n  /**\n   * State of the event. The default is 'Busy'\n   */\n  state?: EventState;\n\n  /**\n   * Determine whether the event is shown or hidden\n   */\n  isVisible?: boolean;\n\n  /**\n   * Determine whether something is in progress\n   */\n  isPending?: boolean;\n\n  /**\n   * Determine whether the event is focused\n   */\n  isFocused?: boolean;\n\n  /**\n   * Determine whether the event is read-only\n   */\n  isReadOnly?: boolean;\n\n  /**\n   * Determine whether the event is private\n   */\n  isPrivate?: boolean;\n\n  /**\n   * Text color of the event element\n   */\n  color?: string;\n\n  /**\n   * Background color of the event element\n   */\n  backgroundColor?: string;\n\n  /**\n   * Background color of the dragging event element\n   */\n  dragBackgroundColor?: string;\n\n  /**\n   * Left border color of the event element\n   */\n  borderColor?: string;\n\n  /**\n   * Custom style for the event element\n   */\n  customStyle?: StyleProp;\n\n  /**\n   * Raw data for the event\n   */\n  raw?: any;\n}\n\nexport type BooleanKeyOfEventObject =\n  | 'isPrivate'\n  | 'isAllday'\n  | 'isPending'\n  | 'isFocused'\n  | 'isVisible'\n  | 'isReadOnly';\n\nexport type TimeUnit = 'second' | 'minute' | 'hour' | 'date' | 'month' | 'year';\n"
  },
  {
    "path": "apps/calendar/src/types/grid.ts",
    "content": "import type TZDate from '@src/time/date';\n\nimport type { ClientMousePosition } from '@t/mouse';\n\nimport type { FormattedTimeString } from './time/datetime';\n\nexport interface GridUIModel {\n  day: number;\n  width: number;\n  left: number;\n}\n\nexport interface GridPosition {\n  columnIndex: number;\n  rowIndex: number;\n}\n\nexport interface CommonGridColumn {\n  date: TZDate;\n  left: number;\n  width: number;\n}\n\nexport interface TimeGridRow {\n  top: number;\n  height: number;\n  startTime: FormattedTimeString;\n  endTime: FormattedTimeString;\n}\n\nexport interface TimeGridData {\n  rows: TimeGridRow[];\n  columns: CommonGridColumn[];\n}\n\nexport type GridPositionFinder = (mousePosition: ClientMousePosition) => GridPosition | null;\n"
  },
  {
    "path": "apps/calendar/src/types/mouse.ts",
    "content": "/**\n * Generic coordinates of mouse position.\n *\n * Can be `clientX` & `clientY` or `pageX`, `pageY`.\n */\nexport interface Coordinates {\n  x: number;\n  y: number;\n}\n\nexport type ClientMousePosition = Pick<MouseEvent, 'clientX' | 'clientY'>;\n"
  },
  {
    "path": "apps/calendar/src/types/options.ts",
    "content": "import type { ComponentType } from 'preact';\n\nimport type { DeepPartial } from 'ts-essentials';\n\nimport type { EventObject, EventObjectWithDefaultValues } from '@t/events';\nimport type { TemplateConfig } from '@t/template';\nimport type { ThemeState } from '@t/theme';\n\nexport type EventView = 'allday' | 'time';\nexport type TaskView = 'milestone' | 'task';\n\nexport interface CollapseDuplicateEventsOptions {\n  getDuplicateEvents: (\n    targetEvent: EventObjectWithDefaultValues,\n    events: EventObjectWithDefaultValues[]\n  ) => EventObjectWithDefaultValues[];\n  getMainEvent: (events: EventObjectWithDefaultValues[]) => EventObjectWithDefaultValues;\n}\n\nexport interface WeekOptions {\n  startDayOfWeek?: number;\n  dayNames?: [string, string, string, string, string, string, string] | [];\n  narrowWeekend?: boolean;\n  workweek?: boolean;\n  showNowIndicator?: boolean;\n  showTimezoneCollapseButton?: boolean;\n  timezonesCollapsed?: boolean;\n  hourStart?: number;\n  hourEnd?: number;\n  eventView?: boolean | EventView[];\n  taskView?: boolean | TaskView[];\n  collapseDuplicateEvents?: boolean | Partial<CollapseDuplicateEventsOptions>;\n}\n\nexport interface MonthOptions {\n  dayNames?: [string, string, string, string, string, string, string] | [];\n  startDayOfWeek?: number;\n  narrowWeekend?: boolean;\n  visibleWeeksCount?: number;\n  isAlways6Weeks?: boolean;\n  workweek?: boolean;\n  visibleEventCount?: number;\n}\n\nexport interface GridSelectionOptions {\n  enableDblClick?: boolean;\n  enableClick?: boolean;\n}\n\nexport interface TimezoneConfig {\n  timezoneName: string;\n  displayLabel?: string;\n  tooltip?: string;\n}\n\nexport interface TimezoneOptions {\n  zones?: TimezoneConfig[];\n  customOffsetCalculator?: (timezoneName: string, timestamp: number) => number;\n}\n\nexport interface CalendarColor {\n  color?: string;\n  backgroundColor?: string;\n  dragBackgroundColor?: string;\n  borderColor?: string;\n}\n\nexport interface CalendarInfo extends CalendarColor {\n  id: string;\n  name: string;\n}\n\nexport type ViewType = 'month' | 'week' | 'day';\n\nexport interface Options {\n  defaultView?: ViewType;\n  theme?: DeepPartial<ThemeState>;\n  template?: TemplateConfig;\n  week?: WeekOptions;\n  month?: MonthOptions;\n  calendars?: CalendarInfo[];\n  useFormPopup?: boolean;\n  useDetailPopup?: boolean;\n  gridSelection?: boolean | GridSelectionOptions;\n  isReadOnly?: boolean;\n  usageStatistics?: boolean;\n  eventFilter?: (event: EventObject) => boolean;\n  timezone?: TimezoneOptions;\n}\n\nexport interface ViewInfoUserInput {\n  component: ComponentType<any>;\n  router?: {\n    linkTitle: string;\n  };\n}\n\nexport type ViewListMap = {\n  [key: string]: ViewInfoUserInput;\n};\n"
  },
  {
    "path": "apps/calendar/src/types/panel.ts",
    "content": "export type AlldayEventCategory = 'milestone' | 'allday' | 'task';\n\nexport type PanelType = 'daygrid' | 'timegrid';\n\nexport interface Panel {\n  name: string;\n  type: PanelType;\n  minHeight?: number;\n  maxHeight?: number;\n  showExpandableButton?: boolean;\n  maxExpandableHeight?: number;\n  handlers?: ['click', 'creation', 'move', 'resize'];\n  show?: boolean;\n}\n"
  },
  {
    "path": "apps/calendar/src/types/store.ts",
    "content": "import type EventModel from '@src/model/eventModel';\nimport type EventUIModel from '@src/model/eventUIModel';\nimport type { CalendarDispatchers, CalendarSlice } from '@src/slices/calendar';\nimport type { DndDispatchers, DndSlice } from '@src/slices/dnd';\nimport type { GridSelectionDispatchers, GridSelectionSlice } from '@src/slices/gridSelection';\nimport type { WeekViewLayoutDispatchers, WeekViewLayoutSlice } from '@src/slices/layout';\nimport type { OptionsDispatchers, OptionsSlice } from '@src/slices/options';\nimport type { PopupDispatchers, PopupSlice } from '@src/slices/popup';\nimport type { TemplateDispatchers, TemplateSlice } from '@src/slices/template';\nimport type { ViewDispatchers, ViewSlice } from '@src/slices/view';\nimport type TZDate from '@src/time/date';\n\nimport type { EventState } from '@t/events';\nimport type { MonthOptions, WeekOptions } from '@t/options';\n\nexport type CalendarMonthOptions = Required<MonthOptions>;\nexport type CalendarWeekOptions = Required<WeekOptions>;\n\nexport type Rect = Pick<DOMRect, 'top' | 'left' | 'width' | 'height'>;\n\ninterface BasePopupParam {\n  popupPosition?: PopupPosition;\n  popupArrowPointPosition?: PopupArrowPointPosition;\n  close?: () => void;\n}\n\nexport type PopupParamMap = {\n  seeMore: SeeMorePopupParam;\n  form: EventFormPopupParam;\n  detail: EventDetailPopupParam;\n};\n\nexport interface SeeMorePopupParam extends BasePopupParam {\n  date: TZDate;\n  events: EventUIModel[];\n}\n\nexport interface EventFormPopupParam extends BasePopupParam {\n  isCreationPopup: boolean;\n  event?: EventModel;\n  title: string;\n  location: string;\n  start: TZDate;\n  end: TZDate;\n  isAllday: boolean;\n  isPrivate: boolean;\n  eventState?: EventState;\n}\n\nexport interface EventDetailPopupParam extends BasePopupParam {\n  event: EventModel;\n  eventRect: Rect;\n}\n\nexport type PopupPosition = {\n  top?: number | string;\n  bottom?: number | string;\n  left?: number | string;\n  right?: number | string;\n};\n\nexport type PopupArrowPointPosition = {\n  top: number;\n  left: number;\n};\n\nexport type StateWithActions = Record<string, any>;\ntype PartialStateCreator<State extends StateWithActions, Key extends keyof State = keyof State> = (\n  state: State\n) => Pick<State, Key> | State;\nexport type StateSelector<State extends StateWithActions, SelectedState> = (\n  state: State\n) => SelectedState;\nexport type EqualityChecker<State> = (state: State, newState: State) => boolean;\nexport type StateListener<State> = (state: State, previousState: State) => void;\nexport type StateSliceListener<StateSlice> = (slice: StateSlice, previousSlice: StateSlice) => void;\nexport type SetState<State extends StateWithActions> = <Key extends keyof Omit<State, 'dispatch'>>(\n  partialStateCreator: PartialStateCreator<Omit<State, 'dispatch'>, Key>\n) => void;\nexport type GetState<State extends StateWithActions> = () => State;\ntype Unsubscribe = () => void;\n\nexport interface Subscribe<State extends StateWithActions> {\n  (listener: StateListener<State>): Unsubscribe;\n\n  <StateSlice>(\n    listener: StateSliceListener<StateSlice>,\n    selector?: StateSelector<State, StateSlice>,\n    equalityFn?: EqualityChecker<StateSlice>\n  ): Unsubscribe;\n}\n\nexport interface InternalStoreAPI<State extends StateWithActions> {\n  setState: SetState<State>;\n  getState: GetState<State>;\n  subscribe: Subscribe<State>;\n  clearListeners: () => void;\n}\n\nexport type StoreCreator<State extends StateWithActions> = (\n  set: SetState<State>,\n  get: GetState<State>,\n  api: InternalStoreAPI<State>\n) => State;\n\nexport type CalendarState = OptionsSlice &\n  TemplateSlice &\n  PopupSlice &\n  WeekViewLayoutSlice &\n  CalendarSlice &\n  ViewSlice &\n  DndSlice &\n  GridSelectionSlice;\n\nexport type Dispatchers = {\n  options: OptionsDispatchers;\n  popup: PopupDispatchers;\n  weekViewLayout: WeekViewLayoutDispatchers;\n  calendar: CalendarDispatchers;\n  view: ViewDispatchers;\n  dnd: DndDispatchers;\n  gridSelection: GridSelectionDispatchers;\n  template: TemplateDispatchers;\n};\n\nexport type CalendarStore = CalendarState & {\n  dispatch: Dispatchers;\n};\n"
  },
  {
    "path": "apps/calendar/src/types/template.ts",
    "content": "import type { VNode } from 'preact';\n\nimport type TZDate from '@src/time/date';\n\nimport type { EventObjectWithDefaultValues, TimeUnit } from '@t/events';\n\nexport interface TemplateTimeGridHourLabel {\n  hidden: boolean;\n  hour: number;\n  minutes: number;\n}\n\nexport interface TemplateNow {\n  unit: TimeUnit;\n  time: TZDate;\n  format: string;\n}\n\nexport interface TemplateMonthGrid {\n  date: string;\n  day: number;\n  hiddenEventCount: number;\n  isOtherMonth: boolean;\n  isToday: boolean;\n  month: number;\n  ymd: string;\n}\n\nexport interface TemplateMoreTitleDate {\n  ymd: string;\n  date: number;\n  day: number;\n}\n\nexport interface TemplateWeekDayName {\n  date: number;\n  day: number;\n  dayName: string;\n  isToday: boolean;\n  renderDate: string;\n  dateInstance: TZDate;\n}\n\nexport interface TemplateMonthDayName {\n  day: number;\n  label: string;\n}\n\n/**\n * If display label does not exist in the timezone options,\n * timezone offset based on timezone name will be passed\n */\nexport type TemplateTimezone =\n  | { displayLabel: string; timezoneOffset: null }\n  | { displayLabel: null; timezoneOffset: number };\n\nexport type TemplateReturnType = string | VNode<{ className: string }>;\n\nexport interface Template {\n  milestoneTitle: () => TemplateReturnType;\n  milestone: (event: EventObjectWithDefaultValues) => TemplateReturnType;\n  taskTitle: () => TemplateReturnType;\n  task: (event: EventObjectWithDefaultValues) => TemplateReturnType;\n  alldayTitle: () => TemplateReturnType;\n  allday: (event: EventObjectWithDefaultValues) => TemplateReturnType;\n  time: (event: EventObjectWithDefaultValues) => TemplateReturnType;\n  goingDuration: (event: EventObjectWithDefaultValues) => TemplateReturnType;\n  comingDuration: (event: EventObjectWithDefaultValues) => TemplateReturnType;\n  monthMoreTitleDate: (moreTitle: TemplateMoreTitleDate) => TemplateReturnType;\n  monthMoreClose: () => TemplateReturnType;\n  monthGridHeader: (cellData: TemplateMonthGrid) => TemplateReturnType;\n  monthGridHeaderExceed: (hiddenEventsCount: number) => TemplateReturnType;\n  monthGridFooter: (cellData: TemplateMonthGrid) => TemplateReturnType;\n  monthGridFooterExceed: (hiddenEventsCount: number) => TemplateReturnType;\n  monthDayName: (monthDayNameData: TemplateMonthDayName) => TemplateReturnType;\n  weekDayName: (weekDayNameData: TemplateWeekDayName) => TemplateReturnType;\n  weekGridFooterExceed: (hiddenEventsCount: number) => TemplateReturnType;\n  collapseBtnTitle: () => TemplateReturnType;\n  timezoneDisplayLabel: (props: TemplateTimezone) => TemplateReturnType;\n  timegridDisplayPrimaryTime: (props: TemplateNow) => TemplateReturnType;\n  timegridDisplayTime: (props: TemplateNow) => TemplateReturnType;\n  timegridNowIndicatorLabel: (props: TemplateNow) => TemplateReturnType;\n  popupIsAllday: () => TemplateReturnType;\n  popupStateFree: () => TemplateReturnType;\n  popupStateBusy: () => TemplateReturnType;\n  titlePlaceholder: () => TemplateReturnType;\n  locationPlaceholder: () => TemplateReturnType;\n  startDatePlaceholder: () => TemplateReturnType;\n  endDatePlaceholder: () => TemplateReturnType;\n  popupSave: () => TemplateReturnType;\n  popupUpdate: () => TemplateReturnType;\n  popupDetailTitle: (event: EventObjectWithDefaultValues) => TemplateReturnType;\n  popupDetailDate: (event: EventObjectWithDefaultValues) => TemplateReturnType;\n  popupDetailLocation: (event: EventObjectWithDefaultValues) => TemplateReturnType;\n  popupDetailAttendees: (event: EventObjectWithDefaultValues) => TemplateReturnType;\n  popupDetailState: (event: EventObjectWithDefaultValues) => TemplateReturnType;\n  popupDetailRecurrenceRule: (event: EventObjectWithDefaultValues) => TemplateReturnType;\n  popupDetailBody: (event: EventObjectWithDefaultValues) => TemplateReturnType;\n  popupEdit: () => TemplateReturnType;\n  popupDelete: () => TemplateReturnType;\n}\n\nexport type TemplateConfig = Partial<Template>;\n"
  },
  {
    "path": "apps/calendar/src/types/theme.ts",
    "content": "import type { DeepPartial } from 'ts-essentials';\n\nexport type CommonTheme = {\n  backgroundColor: string;\n  border: string;\n  gridSelection: {\n    backgroundColor: string;\n    border: string;\n  };\n  dayName: { color: string };\n  holiday: { color: string };\n  saturday: { color: string };\n  today: { color: string };\n};\n\nexport type WeekDayNameTheme = {\n  borderLeft: string;\n  borderTop: string;\n  borderBottom: string;\n  backgroundColor: string;\n};\n\nexport type MonthDayNameTheme = {\n  borderLeft: string;\n  backgroundColor: string;\n};\n\nexport type DayGridTheme = {\n  borderRight: string;\n  backgroundColor: string;\n};\n\nexport type DayGridLeftTheme = {\n  borderRight: string;\n  backgroundColor: string;\n  width: string;\n};\n\nexport type TimeGridLeftTheme = {\n  borderRight: string;\n  backgroundColor: string;\n  width: string;\n};\n\nexport type WeekTheme = {\n  dayName: WeekDayNameTheme;\n  dayGrid: DayGridTheme;\n  dayGridLeft: DayGridLeftTheme;\n  timeGrid: { borderRight: string };\n  timeGridLeft: TimeGridLeftTheme;\n  timeGridLeftAdditionalTimezone: { backgroundColor: string };\n  timeGridHalfHourLine: { borderBottom: string };\n  timeGridHourLine: { borderBottom: string };\n  nowIndicatorLabel: { color: string };\n  nowIndicatorPast: { border: string };\n  nowIndicatorBullet: { backgroundColor: string };\n  nowIndicatorToday: { border: string };\n  nowIndicatorFuture: { border: string };\n  pastTime: { color: string };\n  futureTime: { color: string };\n  weekend: { backgroundColor: string };\n  today: { color: string; backgroundColor: string };\n  pastDay: { color: string };\n  panelResizer: { border: string };\n  gridSelection: { color: string };\n};\n\nexport type MonthTheme = {\n  dayExceptThisMonth: { color: string };\n  dayName: MonthDayNameTheme;\n  holidayExceptThisMonth: { color: string };\n  moreView: {\n    backgroundColor: string;\n    border: string;\n    boxShadow: string;\n    width: number | null;\n    height: number | null;\n  };\n  moreViewTitle: {\n    backgroundColor: string;\n  };\n  gridCell: {\n    headerHeight: number | null;\n    footerHeight: number | null;\n  };\n  weekend: { backgroundColor: string };\n};\n\nexport type ThemeState = {\n  common: CommonTheme;\n  week: WeekTheme;\n  month: MonthTheme;\n};\n\nexport type ThemeDispatchers = {\n  setTheme: (theme: DeepPartial<ThemeState>) => void;\n  setCommonTheme: (commonTheme: DeepPartial<CommonTheme>) => void;\n  setWeekTheme: (weekTheme: DeepPartial<WeekTheme>) => void;\n  setMonthTheme: (monthTheme: DeepPartial<MonthTheme>) => void;\n};\n\nexport type ThemeStore = ThemeState & {\n  dispatch: ThemeDispatchers;\n};\n"
  },
  {
    "path": "apps/calendar/src/types/time/datetime.ts",
    "content": "import type TZDate from '@src/time/date';\n\nexport type RawDate = {\n  y: number;\n  M: number;\n  d: number;\n  h: number;\n  m: number;\n  s: number;\n  ms: number;\n};\n\nexport interface CellStyle {\n  width: number;\n  left: number;\n}\n\nexport interface CellInfo extends CellStyle {\n  date: TZDate;\n}\n\nexport type HoursString =\n  | `${2}${0 | 1 | 2 | 3 | 4}`\n  | `${0 | 1}${0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`;\nexport type FormattedTimeString = `${HoursString}:${0 | 3}0`;\n"
  },
  {
    "path": "apps/calendar/src/types/util.ts",
    "content": "export interface Element {\n  msMatchesSelector: (selectors: string) => boolean;\n}\n\nexport type MouseEventListener = (e: MouseEvent) => void;\nexport type KeyboardEventListener = (e: KeyboardEvent) => void;\n"
  },
  {
    "path": "apps/calendar/src/utils/array.spec.ts",
    "content": "/* eslint-disable no-undefined */\nimport pluck from 'tui-code-snippet/collection/pluck';\n\nimport EventModel from '@src/model/eventModel';\nimport array from '@src/utils/array';\n\nimport type { EventObject } from '@t/events';\n\ndescribe('common/array', () => {\n  describe('common compare methods', () => {\n    describe('compare.num', () => {\n      it('asc', () => {\n        const arr = [8, 3, 11, 29, 31, 55, 25, 1];\n        arr.sort(array.compare.num.asc);\n\n        expect(arr).toEqual([1, 3, 8, 11, 25, 29, 31, 55]);\n      });\n    });\n\n    describe('EventModel', () => {\n      let mockData: EventObject[];\n      let events: EventModel[];\n\n      beforeEach(() => {\n        mockData = [\n          {\n            title: 'hunting',\n            isAllday: true,\n            start: '2015/05/01',\n            end: '2015/05/02',\n          },\n          {\n            title: 'meeting',\n            isAllday: false,\n            start: '2015/05/03 12:30:00',\n            end: '2015/05/03 16:00:00',\n          },\n          {\n            title: 'physical training',\n            isAllday: false,\n            start: '2015/05/03 18:30:00',\n            end: '2015/05/03 19:30:00',\n          },\n          {\n            title: 'logical training2',\n            isAllday: false,\n            start: '2015/05/03 18:30:00',\n            end: '2015/05/03 19:20:00',\n          },\n          {\n            title: 'logical training',\n            isAllday: false,\n            start: '2015/05/03 18:30:00',\n            end: '2015/05/03 19:20:00',\n          },\n          {\n            title: '평가기간',\n            isAllday: true,\n            start: '2015/05/03',\n            end: '2015/05/12',\n          },\n          {\n            title: 'drawing study',\n            isAllday: true,\n            start: '2015/05/04 18:40:00',\n            end: '2015/05/04 19:40:00',\n          },\n        ];\n        events = [];\n      });\n\n      it('isAllday ASC, start ASC, duration DESC, id ASC', () => {\n        mockData.forEach((data) => {\n          events.push(new EventModel(data));\n        });\n\n        events.sort(array.compare.event.asc);\n\n        expect(pluck(events, 'title')).toEqual([\n          'hunting',\n          '평가기간',\n          'drawing study',\n          'meeting',\n          'physical training',\n          'logical training2',\n          'logical training',\n        ]);\n      });\n\n      it('duration', () => {\n        const expected = ['B', 'A'];\n\n        const fixtures = [\n          {\n            title: 'A',\n            isAllday: false,\n            start: '2015/05/03 12:00:00',\n            end: '2015/05/03 12:10:00',\n          },\n          {\n            title: 'B',\n            isAllday: false,\n            start: '2015/05/03 12:00:00',\n            end: '2015/05/03 12:20:00',\n          },\n        ];\n\n        fixtures.forEach((data) => {\n          events.push(new EventModel(data));\n        });\n\n        events.sort(array.compare.event.asc);\n\n        expect(pluck(events, 'title')).toEqual(expected);\n      });\n    });\n  });\n\n  describe('binary search', () => {\n    let arr: string[];\n\n    beforeEach(() => {\n      arr = ['B', 'Z', 'a', 'c', 'd', 'e', 'f', 'x']; // asc\n    });\n\n    it('return correct index to insertion.', () => {\n      arr = ['CA', 'WA'];\n\n      expect(array.bsearch(arr, 'TX')).toBe(-1);\n    });\n\n    it('search item index using binary search.', () => {\n      expect(array.bsearch(arr, 'd')).toBe(4);\n      expect(array.bsearch(arr, 'f')).toBe(6);\n      expect(array.bsearch(arr, 'B')).not.toBe(1);\n      expect(array.bsearch(arr, 'q')).toBeLessThan(0);\n    });\n\n    it('can be used to insert the element.', () => {\n      arr.splice(Math.abs(array.bsearch(arr, 'g')), 0, 'g');\n\n      expect(arr.indexOf('g')).toBe(7);\n    });\n\n    it('search by custom functions.', () => {\n      const items = [{ a: 'B' }, { a: 'e' }, { a: 'f' }, { a: 'x' }, { a: 'z' }]; // asc\n\n      expect(\n        array.bsearch(items, 'f', (item) => {\n          return item.a;\n        })\n      ).toBe(2);\n    });\n\n    it('can search complicated sort type array.', () => {\n      interface Item {\n        f: number;\n        s: number;\n      }\n\n      const items = [\n        {\n          f: 2,\n          s: 13,\n        },\n        {\n          f: 1,\n          s: 5,\n        },\n        {\n          f: 1,\n          s: 3,\n        },\n        {\n          f: 2,\n          s: 2,\n        },\n        {\n          f: 0,\n          s: 15,\n        },\n        {\n          f: 8,\n          s: 1,\n        },\n        {\n          f: 5,\n          s: 12,\n        },\n        {\n          f: 5,\n          s: 2,\n        },\n      ];\n\n      // f ASC, b DESC\n      function compare(a: Item, b: Item) {\n        if (a.f > b.f) {\n          return 1;\n        }\n        if (a.f < b.f) {\n          return -1;\n        }\n        if (a.s > b.s) {\n          return -1;\n        }\n        if (a.s < b.s) {\n          return 1;\n        }\n\n        return 0;\n      }\n      items.sort(compare);\n\n      expect(array.bsearch(items, { f: 5 }, undefined, compare)).toBe(5);\n      expect(array.bsearch(items, { f: 5, s: 2 }, undefined, compare)).toBe(6);\n    });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/utils/array.ts",
    "content": "import type EventModel from '@src/model/eventModel';\nimport EventUIModel from '@src/model/eventUIModel';\nimport { compare } from '@src/time/datetime';\n\nfunction compareBooleansASC(a: boolean, b: boolean) {\n  if (a !== b) {\n    return a ? -1 : 1;\n  }\n\n  return 0;\n}\n\nfunction compareNumbersASC(a: any, b: any) {\n  return Number(a) - Number(b);\n}\n\nfunction compareStringsASC(_a: any, _b: any) {\n  const a = String(_a);\n  const b = String(_b);\n\n  if (a === b) {\n    return 0;\n  }\n\n  return a > b ? 1 : -1;\n}\n\n// eslint-disable-next-line complexity\nfunction compareEventsASC(a: EventModel | EventUIModel, b: EventModel | EventUIModel) {\n  const modelA = a instanceof EventUIModel ? a.model : a;\n  const modelB = b instanceof EventUIModel ? b.model : b;\n  const alldayCompare = compareBooleansASC(\n    modelA.isAllday || modelA.hasMultiDates,\n    modelB.isAllday || modelB.hasMultiDates\n  );\n\n  if (alldayCompare) {\n    return alldayCompare;\n  }\n\n  const startsCompare = compare(a.getStarts(), b.getStarts());\n\n  if (startsCompare) {\n    return startsCompare;\n  }\n\n  const durationA = a.duration();\n  const durationB = b.duration();\n\n  if (durationA < durationB) {\n    return 1;\n  }\n  if (durationA > durationB) {\n    return -1;\n  }\n\n  return modelA.cid() - modelB.cid();\n}\n\nexport function bsearch(\n  arr: any[],\n  search: any,\n  fn?: (item: any) => any,\n  compareFn?: (item: any, searchArg: any) => number\n) {\n  let minIndex = 0;\n  let maxIndex = arr.length - 1;\n  let currentIndex;\n  let value;\n  let comp;\n\n  compareFn = compareFn || compareStringsASC;\n\n  while (minIndex <= maxIndex) {\n    currentIndex = ((minIndex + maxIndex) / 2) | 0; // Math.floor\n    value = fn ? fn(arr[currentIndex]) : arr[currentIndex];\n    comp = compareFn(value, search);\n\n    if (comp < 0) {\n      minIndex = currentIndex + 1;\n    } else if (comp > 0) {\n      maxIndex = currentIndex - 1;\n    } else {\n      return currentIndex;\n    }\n  }\n\n  return ~maxIndex;\n}\n\nexport default {\n  bsearch,\n  compare: {\n    event: {\n      asc: compareEventsASC,\n    },\n    num: {\n      asc: compareNumbersASC,\n    },\n  },\n};\n\nexport function first<T>(array: Array<T>) {\n  return array[0];\n}\n\nexport function last<T>(array: Array<T>) {\n  return array[array.length - 1];\n}\n\nexport function findLastIndex<T>(array: T[], predicate: (value: T) => boolean): number {\n  for (let i = array.length - 1; i >= 0; i -= 1) {\n    if (predicate(array[i])) {\n      return i;\n    }\n  }\n\n  return -1;\n}\n\nexport function fill<T>(length: number, value: T): T[] {\n  if (length > 0) {\n    return Array.from<T, T>({ length }, () => {\n      if (Array.isArray(value)) {\n        return value.slice() as unknown as T;\n      }\n\n      return value;\n    });\n  }\n\n  return [];\n}\n"
  },
  {
    "path": "apps/calendar/src/utils/collection.spec.ts",
    "content": "import Collection from '@src/utils/collection';\n\ntype Item = Record<string, any>;\n\ndescribe('Collection', () => {\n  let c: Collection<Item>;\n\n  beforeEach(() => {\n    c = new Collection<Item>();\n  });\n\n  describe('constructor()', () => {\n    it('can customize method that extract ID from item.', () => {\n      const col = new Collection(function (item) {\n        return item.myID;\n      });\n\n      col.add({ myID: 3 });\n\n      expect(col.get(3)).toEqual({ myID: 3 });\n    });\n  });\n\n  describe('getItemID()', () => {\n    it('get ID from item.', () => {\n      const item = { _id: 7 };\n\n      expect(c.getItemID(item)).toBe(7);\n    });\n  });\n\n  describe('getFirstItem()', () => {\n    it('get the first item.', () => {\n      c.add({ _id: 1 }, { _id: 2 });\n\n      expect(c.getFirstItem()).toEqual({ _id: 1 });\n\n      c.remove(1);\n\n      expect(c.getFirstItem()).toEqual({ _id: 2 });\n    });\n  });\n\n  describe('add()', () => {\n    it('add item to collection.', () => {\n      c.add({ _id: 25 });\n\n      expect(c.size).toBe(1);\n      expect(c.get(25)).toBeDefined();\n    });\n\n    it('add duplicated model then overwrite it.', () => {\n      c.add({ _id: 25 });\n      c.add({ _id: 25, hello: 'world' });\n\n      const item = c.get(25);\n      expect(item).toBeDefined();\n      expect(item?.hello).toBe('world');\n    });\n\n    it('can use multiple arguments item.', () => {\n      c.add({ _id: 2 }, { _id: 4 });\n\n      expect(c.size).toBe(2);\n      expect(c.get(2)).toEqual({ _id: 2 });\n      expect(c.get(4)).toEqual({ _id: 4 });\n    });\n  });\n\n  describe('remove()', () => {\n    let item1: Item;\n    let item2: Item;\n    let item3: Item;\n\n    beforeEach(() => {\n      item1 = { _id: 1 };\n      item2 = { _id: 2 };\n      item3 = { _id: 4 };\n\n      c.add(item1, item2, item3);\n    });\n\n    it(\"method doesn't work when collection is empty\", () => {\n      const col = new Collection();\n      col.remove(2);\n\n      expect(col.size).toBe(0);\n    });\n\n    it(\"can't delete other item\", () => {\n      c.remove(3);\n\n      expect(c.size).toBe(3);\n    });\n\n    it('remove own item.', () => {\n      expect(c.remove(2)).toBe(item2);\n      expect(c.size).toBe(2);\n    });\n\n    it('can remove multiple item.', () => {\n      expect(c.remove(1, 2)).toEqual([item1, item2]);\n      expect(c.size).toBe(1);\n    });\n\n    it('also accept item itself.', () => {\n      c.remove(item1, item2);\n\n      expect(c.size).toBe(1);\n      expect(c.get(4)).toBe(item3);\n      expect(c.get(2)).toBeNull();\n    });\n  });\n\n  describe('has()', () => {\n    let item1: Item;\n    let item2: Item;\n    let item3: Item;\n\n    beforeEach(() => {\n      item1 = { _id: 1 };\n      item2 = { _id: 2 };\n      item3 = { _id: 4 };\n\n      c.add(item1, item2, item3);\n    });\n\n    it(\"method doesn't work when collection is empty\", () => {\n      const col = new Collection();\n\n      expect(col.has(item1)).toBe(false);\n    });\n\n    it('return true when collection has that object.', () => {\n      expect(c.has(item1)).toBe(true);\n      expect(c.has({})).toBe(false);\n    });\n\n    it('check item existance by id.', () => {\n      expect(c.has(2)).toBe(true);\n      expect(c.has(14)).toBe(false);\n    });\n  });\n\n  describe('filter()', () => {\n    let item1: Item;\n    let item2: Item;\n    let item3: Item;\n\n    beforeEach(() => {\n      item1 = { _id: 1, value: 20 };\n      item2 = { _id: 2, value: 50 };\n      item3 = { _id: 4, value: 2 };\n\n      c.add(item1, item2, item3);\n    });\n\n    it('return new collection that filled with filtered items.', () => {\n      const filtered = c.filter((item) => {\n        if (!item.value) {\n          return false;\n        }\n\n        return item.value >= 20;\n      });\n\n      expect(filtered.size).toBe(2);\n    });\n\n    it(\"when collection's getItemID customized. then return collection has same func.\", () => {\n      const cust = function (item: Item & { ID: number }) {\n        return String(item.ID);\n      };\n      const col = new Collection(cust);\n\n      col.add({ ID: 3 });\n\n      const filtered = col.filter(function (item) {\n        return item.ID === 3;\n      });\n\n      expect(filtered.getItemID).toBe(cust);\n    });\n  });\n\n  describe('Collection.and()', () => {\n    let item1: Item;\n    let item2: Item;\n    let item3: Item;\n\n    beforeEach(() => {\n      item1 = { _id: 1, value: 20 };\n      item2 = { _id: 2, value: 50 };\n      item3 = { _id: 4, value: 2 };\n\n      c.add(item1, item2, item3);\n    });\n\n    it('combind multiple function filter AND clause.', () => {\n      function filter1(item: Item) {\n        return item._id === 2;\n      }\n\n      function filter2(item: Item) {\n        return item.value === 50;\n      }\n\n      function filter3(item: Item) {\n        return item.label === '';\n      }\n\n      const combinedFilter = Collection.and(filter1, filter2);\n      let result = c.filter(combinedFilter);\n\n      const expected = new Collection();\n      expected.add(item2);\n\n      expect(result).toEqual(expected);\n\n      result = c.filter(Collection.and(filter1, filter2, filter3));\n\n      expect(result.size).toBe(0);\n\n      expect(c.filter(combinedFilter).size).toBe(1);\n    });\n  });\n\n  describe('Collection.or()', () => {\n    let item1: Item;\n    let item2: Item;\n    let item3: Item;\n\n    beforeEach(() => {\n      item1 = { _id: 1, value: 20 };\n      item2 = { _id: 2, value: 50 };\n      item3 = { _id: 4, value: 2 };\n\n      c.add(item1, item2, item3);\n    });\n\n    it('combine multiple function filter with OR clause.', () => {\n      function filter1(item: Item) {\n        return item._id === 2;\n      }\n\n      function filter2(item: Item) {\n        return item.value === 2;\n      }\n\n      const combined = Collection.or(filter1, filter2);\n      const result = c.filter(combined);\n\n      expect(result.size).toBe(2);\n      expect(result.has(1)).toBe(false);\n\n      expect(\n        c.filter(function (model) {\n          return model._id === 2 || model.value === 2;\n        })\n      ).toEqual(result);\n    });\n  });\n\n  describe('Mixed and(), or() filter', () => {\n    let item1: Item;\n    let item2: Item;\n    let item3: Item;\n    let item4: Item;\n\n    beforeEach(() => {\n      item1 = { _id: 1, value: 20 };\n      item2 = { _id: 2, value: 50 };\n      item3 = { _id: 4, value: 2 };\n      item4 = { _id: 5, value: 50 };\n\n      c.add(item1, item2, item3, item4);\n    });\n\n    it('mixed and, or filter also available for find()', () => {\n      function filter1(item: Item) {\n        return item.value === 20;\n      }\n      function filter2(item: Item) {\n        return item._id === 1;\n      }\n      function filter3(item: Item) {\n        return item.value === 50;\n      }\n\n      const or = Collection.or(filter1, filter3);\n      const and = Collection.and(filter2, or);\n      // _id === 1 && ( value === 20 || value === 50 )\n\n      const result = c.filter(and);\n\n      expect(\n        c.filter(function (model) {\n          return model._id === 1 && (model.value === 20 || model.value === 50);\n        })\n      ).toEqual(result);\n    });\n\n    it('mixed and, or combined filter2', () => {\n      function filter1(item: Item) {\n        return item.value === 50;\n      }\n      function filter2(item: Item) {\n        return item._id === 2;\n      }\n      function filter3(item: Item) {\n        return item._id === 5;\n      }\n      const or = Collection.or(filter2, filter3);\n      const and = Collection.and(or, filter1);\n      const result = c.filter(and);\n\n      expect(\n        c.filter(function (model) {\n          return (model._id === 2 || model._id === 5) && model.value === 50;\n        })\n      ).toEqual(result);\n    });\n  });\n\n  describe('groupBy()', () => {\n    let item1: Item;\n    let item2: Item;\n    let item3: Item;\n\n    beforeEach(() => {\n      item1 = {\n        _id: 1,\n        value: 20,\n        isGood: false,\n        '30': 'a',\n        true: 'c',\n        no() {\n          return this.value;\n        },\n      };\n      item2 = {\n        _id: 2,\n        value: 50,\n        isGood: true,\n        '30': 'b',\n        true: 'c',\n        no() {\n          return this.value;\n        },\n      };\n      item3 = {\n        _id: 4,\n        value: 2,\n        isGood: true,\n        '30': 'b',\n        true: 'd',\n        no() {\n          return this.value;\n        },\n      };\n\n      c.add(item1, item2, item3);\n    });\n\n    it('group all elements by number values.', () => {\n      const grouped = c.groupBy('value');\n\n      const c1 = new Collection(c.getItemID);\n      c1.add(item1);\n      const c2 = new Collection(c.getItemID);\n      c2.add(item2);\n      const c3 = new Collection(c.getItemID);\n      c3.add(item3);\n\n      expect(grouped).toEqual({\n        '20': c1,\n        '50': c2,\n        '2': c3,\n      });\n    });\n\n    it('group by number property', () => {\n      const grouped = c.groupBy(30);\n\n      const c1 = new Collection(c.getItemID);\n      c1.add(item1);\n      const c2 = new Collection(c.getItemID);\n      c2.add(item2, item3);\n\n      expect(grouped.a).toEqual(c1);\n      expect(grouped.b).toEqual(c2);\n    });\n\n    it('group by boolean values.', () => {\n      const grouped = c.groupBy('isGood');\n\n      const c1 = new Collection(c.getItemID);\n      c1.add(item1);\n      const c2 = new Collection(c.getItemID);\n      c2.add(item2, item3);\n\n      expect(grouped).toEqual({\n        false: c1,\n        true: c2,\n      });\n    });\n\n    it('if base value is function then use returned value', () => {\n      const grouped = c.groupBy('no');\n\n      const c1 = new Collection(c.getItemID);\n      c1.add(item1);\n      const c2 = new Collection(c.getItemID);\n      c2.add(item2);\n      const c3 = new Collection(c.getItemID);\n      c3.add(item3);\n\n      expect(grouped).toEqual({\n        '20': c1,\n        '50': c2,\n        '2': c3,\n      });\n    });\n\n    it('group by custom functions', () => {\n      const grouped = c.groupBy((item: Item) => {\n        return item.value > 10 ? 'upper' : 'lower';\n      });\n\n      const c1 = new Collection(c.getItemID);\n      c1.add(item1, item2);\n      const c2 = new Collection(c.getItemID);\n      c2.add(item3);\n\n      expect(grouped.upper).toEqual(c1);\n      expect(grouped.lower).toEqual(c2);\n    });\n  });\n\n  describe('sort()', () => {\n    let item1: Item;\n    let item2: Item;\n    let item3: Item;\n\n    beforeEach(() => {\n      item1 = { _id: 1, value: 20 };\n      item2 = { _id: 2, value: 50 };\n      item3 = { _id: 4, value: 2 };\n\n      c.add(item1, item2, item3);\n    });\n\n    it('sort own items by given compare function.', () => {\n      const arr = c.sort(function (a, b) {\n        if (a.value < b.value) {\n          return -1;\n        }\n        if (a.value === b.value) {\n          return 0;\n        }\n\n        return 1;\n      });\n\n      expect(arr[0]).toBe(item3);\n      expect(arr[1]).toBe(item1);\n      expect(arr[2]).toBe(item2);\n    });\n  });\n\n  describe('each()', () => {\n    let spy: jest.Mock;\n    let item1: Item;\n    let item2: Item;\n    let item3: Item;\n\n    beforeEach(() => {\n      item1 = { _id: 1, value: 20 };\n      item2 = { _id: 2, value: 50 };\n      item3 = { _id: 4, value: 2 };\n\n      c.add(item1, item2, item3);\n\n      spy = jest.fn();\n    });\n\n    it('iterate own items.', () => {\n      c.each(spy);\n\n      expect(spy.mock.calls[2]).toEqual(expect.arrayContaining([{ _id: 4, value: 2 }, 4]));\n    });\n\n    it('break loop when iteratee return false.', () => {\n      spy.mockImplementation((item: Item) => {\n        if (item.value === 50) {\n          return false;\n        }\n\n        return true;\n      });\n\n      c.each(spy);\n\n      expect(spy).toHaveBeenCalledTimes(2);\n    });\n  });\n\n  describe('doWhenHas()', () => {\n    it('invoke supplied method when collection has model.', () => {\n      const item1 = { _id: 1 };\n\n      const spy1 = jest.fn();\n      const spy2 = jest.fn();\n\n      c.add(item1);\n\n      c.doWhenHas(1, spy1);\n      c.doWhenHas(2, spy2);\n\n      expect(spy1).toHaveBeenCalledWith(item1);\n      expect(spy2).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('find()', () => {\n    it('return first single item that meet with supplied function filter', () => {\n      const item1 = { _id: 1 };\n      const item2 = { _id: 2 };\n      const item3 = { _id: 5 };\n\n      c.add(item3, item2, item1);\n\n      const result = c.find(function (model) {\n        return model._id === 2;\n      });\n\n      expect(result).toBe(item2);\n    });\n\n    it('return null when no item.', () => {\n      const item1 = { _id: 1 };\n      c.add(item1);\n\n      expect(\n        c.find(() => {\n          return false;\n        })\n      ).toBe(null);\n    });\n  });\n\n  describe('toArray()', () => {\n    it('return new array with collection items.', () => {\n      const item1 = { _id: 1 };\n      const item2 = { _id: 2 };\n      const item3 = { _id: 5 };\n\n      c.add(item1, item2, item3);\n\n      expect(c.toArray()).toEqual([item1, item2, item3]);\n    });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/utils/collection.ts",
    "content": "import { isFunction, isNil, isNumber, isString } from '@src/utils/type';\n\nexport type ItemID = string | number;\nexport type Item = {\n  _id?: ItemID;\n  [k: string | number]: any;\n};\n\nexport type Filter<ItemType> = (item: ItemType) => boolean;\n\n/**\n * Generic collection base on ES6 Map.\n *\n * It needs function for get model's unique id.\n *\n * if the function is not supplied then it uses default function {@link Collection#getItemID}\n * @param {function} [getItemIDFn] function for get model's id.\n */\nexport default class Collection<ItemType extends Item> {\n  private internalMap: Map<ItemID, ItemType> = new Map();\n\n  constructor(getItemIDFn?: (item: ItemType) => ItemID) {\n    if (isFunction(getItemIDFn)) {\n      this.getItemID = getItemIDFn;\n    }\n  }\n\n  /**\n   * Combine supplied function filters and condition.\n   * @param {...Filter} filterFns - function filters\n   * @returns {function} combined filter\n   */\n  static and<ItemType>(...filterFns: Array<Filter<ItemType>>) {\n    const { length } = filterFns;\n\n    return (item: ItemType) => {\n      for (let i = 0; i < length; i += 1) {\n        if (!filterFns[i].call(null, item)) {\n          return false;\n        }\n      }\n\n      return true;\n    };\n  }\n\n  /**\n   * Combine multiple function filters with OR clause.\n   * @param {...function} filterFns - function filters\n   * @returns {function} combined filter\n   */\n  static or<ItemType>(...filterFns: Array<Filter<ItemType>>) {\n    const { length } = filterFns;\n\n    if (!length) {\n      return () => false;\n    }\n\n    return (item: ItemType) => {\n      let result = filterFns[0].call(null, item);\n\n      for (let i = 1; i < length; i += 1) {\n        result = result || filterFns[i].call(null, item);\n      }\n\n      return result;\n    };\n  }\n\n  /**\n   * get model's unique id.\n   * @param {object} item model instance.\n   * @returns {string | number} model unique id.\n   */\n  getItemID(item: ItemType): ItemID {\n    return item?._id ?? '';\n  }\n\n  getFirstItem(): ItemType | null {\n    const iterator = this.internalMap.values();\n\n    return iterator.next().value;\n  }\n\n  /**\n   * add models.\n   * @param {Object[]} items - models to add this collection.\n   */\n  add(...items: ItemType[]): Collection<ItemType> {\n    items.forEach((item) => {\n      const id = this.getItemID(item);\n\n      this.internalMap.set(id, item);\n    });\n\n    return this;\n  }\n\n  /**\n   * remove models.\n   * @param {Array.<(Object|string|number)>} items model instances or unique ids to delete.\n   */\n  remove(...items: Array<ItemType | ItemID>): ItemType[] | ItemType {\n    const removeResult: ItemType[] = [];\n\n    items.forEach((item) => {\n      const id: ItemID = isString(item) || isNumber(item) ? item : this.getItemID(item);\n\n      if (!this.internalMap.has(id)) {\n        return;\n      }\n\n      removeResult.push(this.internalMap.get(id) as ItemType);\n      this.internalMap['delete'](id);\n    });\n\n    return removeResult.length === 1 ? removeResult[0] : removeResult;\n  }\n\n  /**\n   * check collection has specific model.\n   * @param {(object|string|number)} id model instance or id to check\n   * @returns {boolean} is has model?\n   */\n  has(item: ItemType | ItemID): boolean {\n    const id: ItemID = isString(item) || isNumber(item) ? item : this.getItemID(item);\n\n    return this.internalMap.has(id);\n  }\n\n  get(item: ItemType | ItemID): ItemType | null {\n    const id: ItemID = isString(item) || isNumber(item) ? item : this.getItemID(item);\n\n    return this.internalMap.get(id) ?? null;\n  }\n\n  /**\n   * invoke callback when model exist in collection.\n   * @param {(string|number)} id model unique id.\n   * @param {function} callback the callback.\n   */\n  doWhenHas(id: ItemID, callback: (item: ItemType) => void) {\n    const item = this.internalMap.get(id);\n\n    if (isNil(item)) {\n      return;\n    }\n\n    callback(item);\n  }\n\n  /**\n   * Search model. and return new collection.\n   * @param {function} filterFn filter function.\n   * @returns {Collection} new collection with filtered models.\n   * @example\n   * collection.filter(function(item) {\n   *     return item.edited === true;\n   * });\n   *\n   * function filter1(item) {\n   *     return item.edited === false;\n   * }\n   *\n   * function filter2(item) {\n   *     return item.disabled === false;\n   * }\n   *\n   * collection.filter(Collection.and(filter1, filter2));\n   *\n   * collection.filter(Collection.or(filter1, filter2));\n   */\n  filter(filterFn: Filter<ItemType>): Collection<ItemType> {\n    const result = new Collection<ItemType>();\n\n    if (this.hasOwnProperty('getItemID')) {\n      result.getItemID = this.getItemID;\n    }\n\n    this.internalMap.forEach((item) => {\n      if (filterFn(item) === true) {\n        result.add(item);\n      }\n    });\n\n    return result;\n  }\n\n  /**\n   * Group element by specific key values.\n   *\n   * if key parameter is function then invoke it and use returned value.\n   * @param {(string|number|function)} groupByFn key property or getter function.\n   * @returns {object.<string|number, Collection>} grouped object\n   * @example\n   * // pass `string`, `number`, `boolean` type value then group by property value.\n   * collection.groupBy('gender');    // group by 'gender' property value.\n   * collection.groupBy(50);          // group by '50' property value.\n   *\n   * // pass `function` then group by return value. each invocation `function` is called with `(item)`.\n   * collection.groupBy(function(item) {\n   *     if (item.score > 60) {\n   *         return 'pass';\n   *     }\n   *     return 'fail';\n   * });\n   */\n  groupBy(\n    groupByFn: string | number | ((item: ItemType) => string | number)\n  ): Record<string, Collection<ItemType>> {\n    const result: Record<string, Collection<ItemType>> = {};\n\n    this.internalMap.forEach((item) => {\n      let key = isFunction(groupByFn) ? groupByFn(item) : item[groupByFn];\n\n      if (isFunction(key)) {\n        key = key.call(item);\n      }\n\n      result[key] ??= new Collection<ItemType>(this.getItemID);\n      result[key].add(item);\n    });\n\n    return result;\n  }\n\n  /**\n   * Return the first item in collection that satisfies the provided function.\n   * @param {function} [findFn] - function filter\n   * @returns {object|null} item.\n   */\n  find(findFn: Filter<ItemType>): ItemType | null {\n    let result: ItemType | null = null;\n    const items = this.internalMap.values();\n    let next = items.next();\n\n    while (next.done === false) {\n      if (findFn(next.value)) {\n        result = next.value;\n        break;\n      }\n      next = items.next();\n    }\n\n    return result;\n  }\n\n  /**\n   * sort a basis of supplied compare function.\n   * @param {function} compareFn compareFunction\n   * @returns {array} sorted array.\n   */\n  sort(compareFn: (a: ItemType, b: ItemType) => number): ItemType[] {\n    return this.toArray().sort(compareFn);\n  }\n\n  /**\n   * iterate each model element.\n   *\n   * when iteratee return false then break the loop.\n   * @param {function} iteratee iteratee(item, index, items)\n   */\n  each(iteratee: (item: ItemType, key: keyof ItemType) => boolean | void) {\n    const entries = this.internalMap.entries();\n    let next = entries.next();\n\n    while (next.done === false) {\n      const [key, value] = next.value;\n      if (iteratee(value, key) === false) {\n        break;\n      }\n      next = entries.next();\n    }\n  }\n\n  /**\n   * remove all models in collection.\n   */\n  clear() {\n    this.internalMap.clear();\n  }\n\n  /**\n   * return new array with collection items.\n   * @returns {array} new array.\n   */\n  toArray(): ItemType[] {\n    return Array.from(this.internalMap.values());\n  }\n\n  get size(): number {\n    return this.internalMap.size;\n  }\n}\n"
  },
  {
    "path": "apps/calendar/src/utils/dom.spec.ts",
    "content": "import { closest } from '@src/utils/dom';\n\nit('closest', () => {\n  const depth0 = document.createElement('div');\n  depth0.classList.add('depth-0');\n\n  const depth1 = document.createElement('div');\n  depth1.classList.add('depth-1');\n\n  const depth2 = document.createElement('div');\n  depth2.classList.add('depth-2');\n\n  depth1.appendChild(depth2);\n  depth0.appendChild(depth1);\n\n  expect(closest(depth2, '.depth-0')).toEqual(depth0);\n  expect(closest(depth2, '.depth-1')).toEqual(depth1);\n  expect(closest(depth0, '.no-depth')).toBe(null);\n});\n"
  },
  {
    "path": "apps/calendar/src/utils/dom.ts",
    "content": "import { noop } from '@src/utils/noop';\nimport { isString } from '@src/utils/type';\n\nconst CSS_AUTO_REGEX = /^auto$|^$|%/;\n\nfunction getStyle(el: HTMLElement, style: keyof CSSStyleDeclaration) {\n  let value = el.style[style];\n\n  if ((!value || value === 'auto') && document.defaultView) {\n    const css = document.defaultView.getComputedStyle(el, null);\n    value = css ? css[style] : null;\n  }\n\n  return value === 'auto' ? null : value;\n}\n\n// eslint-disable-next-line complexity\nexport function getPosition(el: HTMLElement) {\n  if (\n    (CSS_AUTO_REGEX.test(el.style.left || '') || CSS_AUTO_REGEX.test(el.style.top || '')) &&\n    'getBoundingClientRect' in el\n  ) {\n    // When the element's left or top is 'auto'\n    const { left, top } = el.getBoundingClientRect();\n\n    return { x: left, y: top };\n  }\n\n  return {\n    x: parseFloat(el.style.left || String(0)),\n    y: parseFloat(el.style.top || String(0)),\n  };\n}\n\ntype SizeValue = 'auto' | string | null;\n\nfunction invalidateSizeValue(value: SizeValue) {\n  if (isString(value)) {\n    return CSS_AUTO_REGEX.test(value);\n  }\n\n  return value === null;\n}\n\nexport function getSize(el: HTMLElement): { width: number; height: number } {\n  const w = getStyle(el, 'width') as SizeValue;\n  const h = getStyle(el, 'height') as SizeValue;\n\n  if ((invalidateSizeValue(w) || invalidateSizeValue(h)) && el.getBoundingClientRect) {\n    const { width, height } = el.getBoundingClientRect();\n\n    return {\n      width: width || el.offsetWidth,\n      height: height || el.offsetHeight,\n    };\n  }\n\n  return {\n    width: parseFloat(w ?? '0'),\n    height: parseFloat(h ?? '0'),\n  };\n}\n\nexport function isOverlapped(el1: Element, el2: Element) {\n  const r1 = el1.getBoundingClientRect();\n  const r2 = el2.getBoundingClientRect();\n\n  return !(r1.top > r2.bottom || r1.right < r2.left || r1.bottom < r2.top || r1.left > r2.right);\n}\n\n// for ssr\n// eslint-disable-next-line @typescript-eslint/no-empty-function\nconst ElementClass = typeof Element === 'undefined' ? noop : Element;\nconst elProto = ElementClass.prototype;\nconst matchSelector =\n  elProto.matches ||\n  elProto.webkitMatchesSelector ||\n  elProto.msMatchesSelector ||\n  function (this: Element, selector: string) {\n    return Array.from(document.querySelectorAll(selector)).includes(this);\n  };\n\nfunction matches(element: Node & ParentNode, selector: string) {\n  return matchSelector.call(element, selector);\n}\n\nexport function closest(element: HTMLElement, selector: string) {\n  if (matches(element, selector)) {\n    return element;\n  }\n\n  let parent = element.parentNode;\n\n  while (parent && parent !== document) {\n    if (matches(parent, selector)) {\n      return parent as HTMLElement;\n    }\n\n    parent = parent.parentNode;\n  }\n\n  return null;\n}\n\nexport function stripTags(str: string) {\n  return str.replace(/<([^>]+)>/gi, '');\n}\n"
  },
  {
    "path": "apps/calendar/src/utils/error.ts",
    "content": "import {\n  INVALID_DATETIME_FORMAT,\n  INVALID_TIMEZONE_NAME,\n  INVALID_VIEW_TYPE,\n} from '@src/constants/error';\nimport { MESSAGE_PREFIX } from '@src/constants/message';\n\n/**\n * Define custom errors for calendar\n * These errors are exposed to the user.\n *\n * We can throw the default `Error` instance for internal errors.\n */\n\nexport class InvalidTimezoneNameError extends Error {\n  constructor(timezoneName: string) {\n    super(`${MESSAGE_PREFIX}${INVALID_TIMEZONE_NAME} - ${timezoneName}`);\n    this.name = 'InvalidTimezoneNameError';\n  }\n}\n\nexport class InvalidDateTimeFormatError extends Error {\n  constructor(dateTimeString: string) {\n    super(`${MESSAGE_PREFIX}${INVALID_DATETIME_FORMAT} - ${dateTimeString}`);\n    this.name = 'InvalidDateTimeFormatError';\n  }\n}\n\nexport class InvalidViewTypeError extends Error {\n  constructor(viewType: string) {\n    super(`${MESSAGE_PREFIX}${INVALID_VIEW_TYPE} - ${viewType}`);\n    this.name = 'InvalidViewTypeError';\n  }\n}\n"
  },
  {
    "path": "apps/calendar/src/utils/eventBus.ts",
    "content": "import CustomEvents from 'tui-code-snippet/customEvents/customEvents';\n\nimport type { AnyFunc } from '@t/eventBus';\n\nexport interface EventBus<\n  EventTypes extends {\n    [key: string]: AnyFunc;\n  }\n> {\n  on<EventName extends keyof EventTypes>(\n    eventName: EventName,\n    handler: EventTypes[EventName]\n  ): EventBus<EventTypes>;\n  off<EventName extends keyof EventTypes>(\n    eventName?: EventName,\n    handler?: EventTypes[EventName]\n  ): EventBus<EventTypes>;\n  once<EventName extends keyof EventTypes>(\n    eventName: EventName,\n    handler: EventTypes[EventName]\n  ): EventBus<EventTypes>;\n  fire<EventName extends keyof EventTypes>(\n    eventName: EventName,\n    ...args: Parameters<EventTypes[EventName]>\n  ): EventBus<EventTypes>;\n}\n\nexport class EventBusImpl<\n    EventTypes extends {\n      [key: string]: AnyFunc;\n    }\n  >\n  extends CustomEvents\n  implements EventBus<EventTypes>\n{\n  on<EventName extends keyof EventTypes>(eventName: EventName, handler: EventTypes[EventName]) {\n    super.on(eventName as string, handler);\n\n    return this;\n  }\n\n  off<EventName extends keyof EventTypes>(eventName?: EventName, handler?: EventTypes[EventName]) {\n    super.off(eventName as string, handler);\n\n    return this;\n  }\n\n  fire<EventName extends keyof EventTypes>(\n    eventName: EventName,\n    ...args: Parameters<EventTypes[EventName]>\n  ) {\n    super.fire(eventName as string, ...args);\n\n    return this;\n  }\n\n  once<EventName extends keyof EventTypes>(eventName: EventName, handler: EventTypes[EventName]) {\n    super.once(eventName as string, handler);\n\n    return this;\n  }\n}\n"
  },
  {
    "path": "apps/calendar/src/utils/keyboard.ts",
    "content": "import type { KEY } from '@src/constants/keyboard';\nimport { KEYCODE } from '@src/constants/keyboard';\n\nexport function isKeyPressed(e: KeyboardEvent, key: KEY) {\n  return e.key ? e.key === key : e.keyCode === KEYCODE[key];\n}\n"
  },
  {
    "path": "apps/calendar/src/utils/logger.ts",
    "content": "import { MESSAGE_PREFIX } from '@src/constants/message';\n\n/* eslint-disable no-console */\nexport const logger = {\n  error: (firstArg: any, ...restArgs: any[]) => {\n    console.error(`${MESSAGE_PREFIX}${firstArg}`, ...restArgs);\n  },\n  warn: (firstArg: any, ...restArgs: any[]) => {\n    console.warn(`${MESSAGE_PREFIX}${firstArg}`, ...restArgs);\n  },\n};\n"
  },
  {
    "path": "apps/calendar/src/utils/math.spec.ts",
    "content": "import { isBetween, limit, ratio } from '@src/utils/math';\n\ndescribe('math util test', () => {\n  it('should return a number between minArr and maxArr', () => {\n    const target = [-1, 0, 4];\n    const minArr = [1];\n    const maxArr = [2, 3];\n    const expected = [1, 1, 2];\n\n    target.forEach((value, index) => {\n      const result = limit(value, minArr, maxArr);\n\n      expect(result).toBe(expected[index]);\n    });\n  });\n\n  it('should find x satisfying the following expression(a : b = y : x)', () => {\n    const a = [1, 2, 3];\n    const b = [2, 4, 6];\n    const y = [3, 6, 9];\n    const expected = [6, 12, 18];\n\n    a.forEach((value, index) => {\n      const result = ratio(value, b[index], y[index]);\n\n      expect(result).toBe(expected[index]);\n    });\n  });\n\n  it('should check if the value exists between', () => {\n    const target = 3;\n    const min = [1, 2, 4];\n    const max = [2, 4, 5];\n    const expected = [false, true, false];\n\n    min.forEach((value, index) => {\n      const result = isBetween(target, value, max[index]);\n\n      expect(result).toBe(expected[index]);\n    });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/utils/math.ts",
    "content": "export function limit(value: number, minArr: number[], maxArr: number[]) {\n  const v = Math.max(value, ...minArr);\n\n  return Math.min(v, ...maxArr);\n}\n\n/**\n * a : b = y : x;\n * ==\n * x = (b * y) / a;\n */\nexport function ratio(a: number, b: number, y: number) {\n  return (b * y) / a;\n}\n\nexport function isBetween(value: number, min: number, max: number) {\n  return min <= value && value <= max;\n}\n"
  },
  {
    "path": "apps/calendar/src/utils/noop.ts",
    "content": "export const noop = () => {\n  // do nothing\n};\n"
  },
  {
    "path": "apps/calendar/src/utils/object.spec.ts",
    "content": "import { expect } from '@playwright/test';\n\nimport TZDate from '@src/time/date';\nimport { mergeObject } from '@src/utils/object';\n\ndescribe('mergeObject', () => {\n  it('should merge two objects', () => {\n    const target = {\n      a: 1,\n      b: 2,\n      c: 3,\n    };\n    const source = {\n      a: 2,\n      b: 3,\n      d: 4,\n    };\n\n    const result = mergeObject(target, source);\n\n    expect(result).toEqual({\n      a: 2,\n      b: 3,\n      c: 3,\n      d: 4,\n    });\n  });\n\n  it('should merge two nested object', () => {\n    const target = {\n      a: 1,\n      b: {\n        b1: 1,\n        b2: 2,\n        b3: {\n          b31: 31,\n          b32: 32,\n        },\n      },\n    };\n    const source = {\n      b: {\n        b3: {\n          b32: 35,\n        },\n      },\n    };\n\n    const result = mergeObject(target, source);\n\n    expect(result).toEqual({\n      a: 1,\n      b: {\n        b1: 1,\n        b2: 2,\n        b3: {\n          b31: 31,\n          b32: 35,\n        },\n      },\n    });\n  });\n\n  it('should accept null in nested object', () => {\n    type Expected = {\n      a: number;\n      b: number;\n      c: {\n        // Allow null only if proper type is defined\n        c1: string | null;\n        c2: string | null;\n      };\n    };\n\n    const target: Expected = {\n      a: 1,\n      b: 2,\n      c: {\n        c1: 'c1',\n        c2: 'c2',\n      },\n    };\n    const source = {\n      c: {\n        c2: null,\n      },\n    };\n\n    const result = mergeObject(target, source);\n\n    expect(result).toEqual({\n      a: 1,\n      b: 2,\n      c: {\n        c1: 'c1',\n        c2: null,\n      },\n    });\n  });\n\n  it('should replace arrays in nested object', () => {\n    const target = {\n      a: [0, 1],\n      b: {\n        c: ['2', '3'],\n      },\n    };\n    const source = {\n      b: { c: ['4', '5'] },\n    };\n\n    const result = mergeObject(target, source);\n\n    expect(result).toEqual({\n      a: [0, 1],\n      b: {\n        c: ['4', '5'],\n      },\n    });\n  });\n\n  it('should replace TZDate instance in nested object', () => {\n    const target = {\n      a: 1,\n      b: '2',\n      c: ['3', '4'],\n      d: new TZDate(2021, 11, 15),\n    };\n    const source = {\n      d: new TZDate(2021, 11, 16),\n    };\n\n    const result = mergeObject(target, source);\n\n    expect(result).toEqual({\n      a: 1,\n      b: '2',\n      c: ['3', '4'],\n      d: new TZDate(2021, 11, 16),\n    });\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/utils/object.ts",
    "content": "import type { DeepPartial } from 'ts-essentials';\n\nimport TZDate from '@src/time/date';\nimport { isObject } from '@src/utils/type';\n\nexport function pick<T extends object, K extends keyof T>(obj: T, ...propNames: K[]) {\n  return propNames.reduce((acc, key) => {\n    if (obj.hasOwnProperty(key)) {\n      acc[key] = obj[key];\n    }\n\n    return acc;\n  }, {} as Pick<T, K>);\n}\n\n/**\n * Clone an instance of a ES6 class.\n *\n * The cloned instance will have the (most of) same properties as the original.\n *\n * Reference: https://stackoverflow.com/a/44782052\n */\nexport function clone<T extends object>(source: T): T {\n  return Object.assign(Object.create(Object.getPrototypeOf(source)), source);\n}\n\n/**\n * Merge two objects together. And It has some pitfalls.\n *\n * For performance reason this function only mutates the target object.\n *\n * Also, it only merges values of nested objects. Array or TZDate instance will be totally replaced.\n *\n * Other non-basic objects are not supported.\n *\n * Since it mutates the target object, avoid using it outside immer `produce` function.\n */\nexport function mergeObject<Target, Source extends DeepPartial<Target>>(\n  target: Target,\n  source: Source = {} as Source\n) {\n  if (!isObject(source)) {\n    return target;\n  }\n\n  Object.keys(source).forEach((k) => {\n    const targetKey = k as keyof Target;\n    const sourceKey = k as keyof Source;\n\n    if (\n      !Array.isArray(source[sourceKey]) &&\n      isObject(target[targetKey]) &&\n      isObject(source[sourceKey]) &&\n      !(source[sourceKey] instanceof TZDate)\n    ) {\n      target[targetKey] = mergeObject(\n        target[targetKey],\n        source[sourceKey] as DeepPartial<Target[keyof Target]>\n      );\n    } else {\n      target[targetKey] = source[sourceKey] as unknown as Target[keyof Target];\n    }\n  });\n\n  return target;\n}\n"
  },
  {
    "path": "apps/calendar/src/utils/preact.ts",
    "content": "/**\n * Pass the prop to component conditionally.\n * just passing `undefined` violates the ESLint rule, and it's less readable.\n * So let's use this function to pass the conditional prop.\n */\nexport function passConditionalProp<P>(condition: boolean, prop: P) {\n  // eslint-disable-next-line no-undefined\n  return condition ? prop : undefined;\n}\n"
  },
  {
    "path": "apps/calendar/src/utils/requestTimeout.spec.ts",
    "content": "import { noop } from '@src/utils/noop';\nimport { requestTimeout } from '@src/utils/requestTimeout';\n\ndescribe('requestTimeout', () => {\n  let fn: jest.Mock;\n\n  beforeAll(() => {\n    jest.useFakeTimers();\n  });\n\n  beforeEach(() => {\n    fn = jest.fn();\n  });\n\n  afterAll(() => {\n    jest.useRealTimers();\n  });\n\n  it('should execute the callback function after a specific time.', () => {\n    // Given\n    const delay = 1000;\n    requestTimeout(fn, delay, noop);\n\n    expect(fn).not.toHaveBeenCalled();\n\n    // When\n    jest.advanceTimersByTime(delay + 30);\n\n    // Then\n    expect(fn).toHaveBeenCalled();\n  });\n\n  it('should execute the callback function only one time.', () => {\n    // Given\n    const delay = 100;\n    requestTimeout(fn, delay, noop);\n\n    // When\n    jest.advanceTimersByTime(delay * 3);\n\n    // Then\n    expect(fn).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/utils/requestTimeout.ts",
    "content": "import { noop } from '@src/utils/noop';\n\n// Reference: https://medium.com/trabe/preventing-click-events-on-double-click-with-react-the-performant-way-1416ab03b835\nexport function requestTimeout(fn: Function, delay: number, registerCancel: Function) {\n  let start: DOMHighResTimeStamp;\n\n  const loop = (timestamp: DOMHighResTimeStamp) => {\n    if (!start) {\n      start = timestamp;\n    }\n    const elapsed = timestamp - start;\n\n    if (elapsed >= delay) {\n      fn();\n      registerCancel(noop);\n\n      return;\n    }\n\n    const raf = requestAnimationFrame(loop);\n    registerCancel(() => cancelAnimationFrame(raf));\n  };\n\n  const raf = requestAnimationFrame(loop);\n  registerCancel(() => cancelAnimationFrame(raf));\n}\n"
  },
  {
    "path": "apps/calendar/src/utils/sanitizer.ts",
    "content": "import DOMPurify from 'isomorphic-dompurify';\n\n// For temporarily saving original target value\nconst TEMP_TARGET_ATTRIBUTE = 'data-target-temp';\n\n/**\n * Add DOMPurify hook to handling exceptional rules for certain HTML attributes.\n * Should be set when the calendar instance is created.\n */\nexport function addAttributeHooks() {\n  DOMPurify.addHook('beforeSanitizeAttributes', (node) => {\n    // Preserve default target attribute value\n    if (node.tagName === 'A') {\n      const targetValue = node.getAttribute('target');\n\n      if (targetValue) {\n        node.setAttribute(TEMP_TARGET_ATTRIBUTE, targetValue);\n      } else {\n        node.setAttribute('target', '_self'); // set default value\n      }\n    }\n  });\n\n  DOMPurify.addHook('afterSanitizeAttributes', (node) => {\n    if (node.tagName === 'A' && node.hasAttribute(TEMP_TARGET_ATTRIBUTE)) {\n      node.setAttribute('target', node.getAttribute(TEMP_TARGET_ATTRIBUTE) as string);\n      node.removeAttribute(TEMP_TARGET_ATTRIBUTE);\n      // Additionally set `rel=\"noopener\"` to prevent another security issue.\n      if (node.getAttribute('target') === '_blank') {\n        node.setAttribute('rel', 'noopener');\n      }\n    }\n  });\n}\n\n/**\n * Remove all attribute sanitizing hooks.\n * Use it in `Calendar#destroy`.\n */\nexport function removeAttributeHooks() {\n  DOMPurify.removeAllHooks();\n}\n\n/**\n * Prevent XSS attack by sanitizing input string values via DOMPurify\n */\nexport function sanitize(str: string) {\n  return DOMPurify.sanitize(str);\n}\n"
  },
  {
    "path": "apps/calendar/src/utils/stamp.ts",
    "content": "import { isNil } from '@src/utils/type';\n\ninterface StampObj extends Record<string, any> {\n  // eslint-disable-next-line camelcase\n  __fe_id?: number;\n}\n\nfunction idGenerator() {\n  let id = 0;\n\n  return {\n    next() {\n      id += 1;\n\n      return id;\n    },\n  };\n}\n\nconst getId = (function () {\n  const generator = idGenerator();\n\n  return () => generator.next();\n})();\n\nexport function stamp(obj: StampObj): number {\n  if (!obj.__fe_id) {\n    // eslint-disable-next-line camelcase\n    obj.__fe_id = getId();\n  }\n\n  return obj.__fe_id;\n}\n\nexport function hasStamp(obj: StampObj): boolean {\n  return !isNil(obj.__fe_id);\n}\n"
  },
  {
    "path": "apps/calendar/src/utils/string.spec.ts",
    "content": "import { capitalize } from '@src/utils/string';\n\nit('should return capitalized string if all character is lower case', () => {\n  // Given\n  const str = 'abc';\n\n  // When\n  const result = capitalize(str);\n\n  // Then\n  expect(result).toBe('Abc');\n});\n\nit('should return same string if given string is already capitalized', () => {\n  // Given\n  const str = 'Abc';\n\n  // When\n  const result = capitalize(str);\n\n  // Then\n  expect(result).toBe(str);\n});\n"
  },
  {
    "path": "apps/calendar/src/utils/string.ts",
    "content": "export function capitalize(str: string) {\n  return str.charAt(0).toUpperCase() + str.slice(1);\n}\n"
  },
  {
    "path": "apps/calendar/src/utils/type.spec.ts",
    "content": "import { isFunction, isNil } from '@src/utils/type';\n\ndescribe('utils', () => {\n  it('isFunction', () => {\n    expect(isFunction(() => 'test')).toBe(true);\n    expect(isFunction(1)).toBe(false);\n    expect(isFunction('String')).toBe(false);\n    expect(isFunction([1, 2, 3, 4, 'str'])).toBe(false);\n  });\n\n  it('isNil', () => {\n    // eslint-disable-next-line no-undefined\n    expect(isNil(undefined)).toBe(true);\n    expect(isNil(null)).toBe(true);\n    expect(isNil([])).toBe(false);\n    expect(isNil(1)).toBe(false);\n  });\n});\n"
  },
  {
    "path": "apps/calendar/src/utils/type.ts",
    "content": "import isUndefined from 'tui-code-snippet/type/isUndefined';\n\nexport function isNil(value: unknown): value is null | undefined {\n  return isUndefined(value) || value === null;\n}\n\nexport function isPresent<T>(value: T | null | undefined): value is T {\n  return !isNil(value);\n}\n\nexport function isFunction(value: unknown): value is Function {\n  return typeof value === 'function';\n}\n\nexport { default as isBoolean } from 'tui-code-snippet/type/isBoolean';\nexport { default as isNumber } from 'tui-code-snippet/type/isNumber';\nexport { default as isObject } from 'tui-code-snippet/type/isObject';\nexport { default as isString } from 'tui-code-snippet/type/isString';\nexport { default as isUndefined } from 'tui-code-snippet/type/isUndefined';\n"
  },
  {
    "path": "apps/calendar/stories/column.stories.tsx",
    "content": "// @FIXME\n\nimport type { ComponentProps } from 'preact';\nimport { h } from 'preact';\n\nimport type { StoryFn } from '@storybook/preact';\n\nimport { Column } from '@src/components/timeGrid/column';\nimport { cls } from '@src/helpers/css';\nimport { createTimeGridData, getWeekDates } from '@src/helpers/grid';\nimport TZDate from '@src/time/date';\n\nimport type { PropsWithChildren } from '@t/components/common';\n\nexport default { title: 'Components/Column', component: Column };\n\nfunction Wrapper({ children }: PropsWithChildren) {\n  return (\n    <div className={cls('layout')} style={{ position: 'relative' }}>\n      {children}\n    </div>\n  );\n}\n\nfunction getTimeGridData() {\n  const now = new TZDate();\n  const weekDates = getWeekDates(now, { startDayOfWeek: 0, workweek: false });\n  return createTimeGridData(weekDates, { hourStart: 0, hourEnd: 24 });\n}\n\nconst Template: StoryFn<ComponentProps<typeof Column>> = (args) => (\n  <Wrapper>\n    <Column {...args} />\n  </Wrapper>\n);\n\nexport const Default = Template.bind({});\nDefault.args = {\n  columnDate: new TZDate(),\n  timeGridData: getTimeGridData(),\n  totalUIModels: [],\n  columnWidth: '20%',\n};\n\n// const getBackgroundEvents = () => {\n//   const start = toStartOfDay(new TZDate());\n//   start.setHours(8);\n//\n//   const data: EventObject[] = [\n//     {\n//       category: 'background',\n//       start,\n//       end: addHours(start, 1),\n//       backgroundColor: 'rgba(100, 100, 100, .3)',\n//     },\n//     {\n//       category: 'background',\n//       start: addMinutes(start, 150),\n//       end: addHours(start, 3),\n//       backgroundColor: 'rgba(200, 100, 100, .3)',\n//     },\n//     {\n//       category: 'background',\n//       start: addHours(start, 4),\n//       end: addHours(start, 6),\n//       backgroundColor: 'rgba(100, 200, 100, .3)',\n//     },\n//   ];\n//   return createEventModels(data);\n// };\n// export const WithBackgroundEvents = Template.bind({});\n// WithBackgroundEvents.args = {\n//   columnDate: new TZDate(),\n//   timeGridData: getTimeGridData(),\n//   totalUIModels: [getBackgroundEvents()],\n//   columnWidth: '20%',\n// };\n"
  },
  {
    "path": "apps/calendar/stories/data/events.json",
    "content": "[\n  {\n    \"title\": \"[류선임] 휴가\",\n    \"isAllday\": true,\n    \"start\": \"2020-06-15T00:00:00\",\n    \"end\": \"2020-06-21T23:59:59\"\n  },\n  {\n    \"title\": \"조정은[일본출장]\",\n    \"isAllday\": true,\n    \"start\": \"2020-06-16T00:00:00\",\n    \"end\": \"2020-06-18T23:59:59\"\n  },\n  {\n    \"title\": \"[유동식] 오전반차\",\n    \"isAllday\": true,\n    \"start\": \"2020-06-16T00:00:00\",\n    \"end\": \"2020-06-16T23:59:59\"\n  },\n  {\n    \"title\": \"[일본 출장] 류선임, 조정은\",\n    \"isAllday\": true,\n    \"start\": \"2020-06-17T00:00:00\",\n    \"end\": \"2020-06-20T23:59:59\"\n  },\n  {\n    \"title\": \"FE 스크럼 - 1\",\n    \"isAllday\": false,\n    \"start\": \"2020-06-16T08:40:00\",\n    \"end\": \"2020-06-16T10:00:00\"\n  },\n  {\n    \"title\": \"[조정은] 연차\",\n    \"isAllday\": true,\n    \"start\": \"2020-06-17T00:00:00\",\n    \"end\": \"2020-06-17T23:59:59\"\n  },\n  {\n    \"title\": \"FE 스크럼 - 2\",\n    \"isAllday\": false,\n    \"start\": \"2020-06-17T08:50:00\",\n    \"end\": \"2020-06-17T10:00:00\"\n  },\n  {\n    \"title\": \"팀 스터디 - A\",\n    \"isAllday\": false,\n    \"start\": \"2020-06-17T12:30:00\",\n    \"end\": \"2020-06-17T13:00:00\"\n  },\n  {\n    \"title\": \"FE 스크럼 - 3\",\n    \"isAllday\": false,\n    \"start\": \"2020-06-18T09:10:00\",\n    \"end\": \"2020-06-18T10:20:00\"\n  },\n  {\n    \"title\": \"[코드리뷰] v2 리펙토링\",\n    \"isAllday\": false,\n    \"start\": \"2020-06-18T10:00:00\",\n    \"end\": \"2020-06-18T11:00:00\"\n  },\n  {\n    \"title\": \"FE 스크럼 - 4\",\n    \"isAllday\": false,\n    \"start\": \"2020-06-19T09:30:00\",\n    \"end\": \"2020-06-19T10:40:00\"\n  },\n  {\n    \"title\": \"팀 스터디 - B\",\n    \"isAllday\": false,\n    \"start\": \"2020-06-19T12:00:00\",\n    \"end\": \"2020-06-19T13:00:00\"\n  },\n  {\n    \"title\": \"캘린더이야기\",\n    \"isAllday\": false,\n    \"start\": \"2020-06-19T12:30:00\",\n    \"end\": \"2020-06-19T13:00:00\"\n  },\n  {\n    \"title\": \"FE 스크럼 - 5\",\n    \"isAllday\": false,\n    \"start\": \"2020-06-20T09:40:00\",\n    \"end\": \"2020-06-20T10:00:00\"\n  },\n  {\n    \"title\": \"주간보고 작성\",\n    \"isAllday\": false,\n    \"start\": \"2020-06-20T14:00:00\",\n    \"end\": \"2020-06-20T15:00:00\",\n    \"goingDuration\": 30,\n    \"comingDuration\": 40\n  },\n  {\n    \"title\": \"14일 일정\",\n    \"isAllday\": false,\n    \"start\": \"2020-06-14T22:00:00\",\n    \"end\": \"2020-06-15T00:00:00\",\n    \"goingDuration\": 30,\n    \"comingDuration\": 40\n  },\n  {\n    \"title\": \"14-15일 일정\",\n    \"isAllday\": false,\n    \"start\": \"2020-06-14T23:00:00\",\n    \"end\": \"2020-06-15T01:00:00\",\n    \"goingDuration\": 30,\n    \"comingDuration\": 40\n  },\n  {\n    \"title\": \"15일 끝일정\",\n    \"isAllday\": false,\n    \"start\": \"2020-06-15T20:00:00\",\n    \"end\": \"2020-06-16T00:00:00\"\n  },\n  {\n    \"title\": \"16일 첫일정\",\n    \"isAllday\": false,\n    \"start\": \"2020-06-16T00:00:00\",\n    \"end\": \"2020-06-16T03:00:00\"\n  },\n  {\n    \"title\": \"매우 짧은 일정\",\n    \"isAllday\": false,\n    \"start\": \"2020-06-15T08:00:00\",\n    \"end\": \"2020-06-15T08:10:00\",\n    \"goingDuration\": 30,\n    \"comingDuration\": 50\n  },\n  {\n    \"title\": \"읽기 전용\",\n    \"isAllday\": false,\n    \"isReadOnly\": true,\n    \"start\": \"2020-06-16T04:00:00\",\n    \"end\": \"2020-06-16T05:30:00\",\n    \"goingDuration\": 30,\n    \"comingDuration\": 30\n  }\n]\n"
  },
  {
    "path": "apps/calendar/stories/dayGridMonth.stories.tsx",
    "content": "import { h } from 'preact';\n\nimport { DayGridMonth } from '@src/components/dayGridMonth/dayGridMonth';\nimport { GridCell } from '@src/components/dayGridMonth/gridCell';\nimport { GridRow } from '@src/components/dayGridMonth/gridRow';\nimport { Layout } from '@src/components/layout';\nimport { createDateMatrixOfMonth } from '@src/helpers/grid';\nimport TZDate from '@src/time/date';\nimport { getRowStyleInfo } from '@src/time/datetime';\n\nimport { getWeekDates } from '@stories/util/mockCalendarDates';\nimport { ProviderWrapper } from '@stories/util/providerWrapper';\n\nimport type { CalendarMonthOptions } from '@t/store';\n\nexport default { title: 'Components/DayGridMonth', component: DayGridMonth };\n\nexport const Cell = () => {\n  const date = new TZDate();\n\n  return (\n    <ProviderWrapper>\n      <Layout>\n        <GridCell date={date} style={{ width: 100, height: 100 }} contentAreaHeight={100} />\n      </Layout>\n    </ProviderWrapper>\n  );\n};\n\nexport const Week = () => {\n  const weekDates = getWeekDates();\n\n  const { rowStyleInfo } = getRowStyleInfo(weekDates.length, false, 0, false);\n\n  return (\n    <ProviderWrapper>\n      <Layout>\n        <GridRow rowInfo={rowStyleInfo} contentAreaHeight={100} week={weekDates} />\n      </Layout>\n    </ProviderWrapper>\n  );\n};\n\nexport const Month = () => {\n  const monthOptions: CalendarMonthOptions = {\n    visibleWeeksCount: 3,\n    workweek: false,\n    narrowWeekend: false,\n    startDayOfWeek: 0,\n    isAlways6Weeks: true,\n    dayNames: [],\n    visibleEventCount: 6,\n  };\n\n  const dateMatrix = createDateMatrixOfMonth(new Date(), monthOptions);\n\n  const { rowStyleInfo, cellWidthMap } = getRowStyleInfo(\n    dateMatrix[0].length,\n    monthOptions.narrowWeekend,\n    monthOptions.startDayOfWeek,\n    monthOptions.workweek\n  );\n\n  const rowInfo = rowStyleInfo.map((cellStyleInfo, index) => ({\n    ...cellStyleInfo,\n    date: dateMatrix[0][index],\n  }));\n\n  return (\n    <ProviderWrapper>\n      <Layout>\n        <DayGridMonth dateMatrix={dateMatrix} rowInfo={rowInfo} cellWidthMap={cellWidthMap} />\n      </Layout>\n    </ProviderWrapper>\n  );\n};\n"
  },
  {
    "path": "apps/calendar/stories/dayView.stories.tsx",
    "content": "import { h } from 'preact';\n\nimport type { Story } from '@storybook/preact';\n\nimport { Day } from '@src/components/view/day';\nimport EventModel from '@src/model/eventModel';\nimport TZDate from '@src/time/date';\nimport { addDate } from '@src/time/datetime';\n\nimport { ProviderWrapper } from '@stories/util/providerWrapper';\nimport { createRandomEventModelsForMonth, createRandomEvents } from '@stories/util/randomEvents';\n\nexport default { title: 'Views/DayView', component: Day };\n\nfunction createTimeGridEvents() {\n  const today = new TZDate();\n  const start = addDate(new TZDate(), -today.getDay());\n  const end = addDate(start, 6);\n\n  return createRandomEvents('week', start, end).map((event) => new EventModel(event));\n}\n\nconst Template: Story = (args) => (\n  <ProviderWrapper options={args.options} events={args.events}>\n    <Day />\n  </ProviderWrapper>\n);\n\nexport const basic = Template.bind({});\n\nexport const randomEvents = Template.bind({});\nrandomEvents.args = {\n  events: [...createRandomEventModelsForMonth(40), ...createTimeGridEvents()],\n  options: { useFormPopup: true, useDetailPopup: true },\n};\n"
  },
  {
    "path": "apps/calendar/stories/e2e/day.stories.tsx",
    "content": "import { h } from 'preact';\n\nimport { mockDayViewEvents } from '@stories/mocks/mockDayViewEvents';\nimport type { CalendarExampleStory } from '@stories/util/calendarExample';\nimport { CalendarExample } from '@stories/util/calendarExample';\n\nexport default { title: 'E2E/Day View' };\n\nconst Template: CalendarExampleStory = (args) => <CalendarExample {...args} />;\nTemplate.args = {\n  options: {\n    defaultView: 'day',\n    useFormPopup: true,\n    useDetailPopup: true,\n    week: {\n      showNowIndicator: false,\n    },\n  },\n  containerHeight: '100vh',\n};\n\nexport const FixedEvents = Template.bind({});\nFixedEvents.args = {\n  ...Template.args,\n  onInit: (cal) => {\n    cal.createEvents(mockDayViewEvents);\n    cal.on('beforeUpdateEvent', ({ event, changes }) => {\n      cal.updateEvent(event.id, event.calendarId, changes);\n    });\n  },\n};\n\nexport const ReadOnly = Template.bind({});\nReadOnly.args = {\n  ...Template.args,\n  options: {\n    ...Template.args.options,\n    isReadOnly: true,\n  },\n  onInit: FixedEvents.args.onInit,\n};\n\nexport const MultipleTimezones = Template.bind({});\nMultipleTimezones.args = {\n  ...Template.args,\n  options: {\n    ...Template.args.options,\n    week: {\n      showTimezoneCollapseButton: true,\n    },\n    theme: {\n      week: {\n        dayGridLeft: {\n          width: '120px',\n        },\n        timeGridLeft: {\n          width: '120px',\n        },\n      },\n    },\n    timezone: {\n      zones: [\n        {\n          timezoneName: 'Asia/Karachi',\n          displayLabel: '+05:00',\n        },\n        {\n          timezoneName: 'US/Samoa',\n          displayLabel: '-11:00',\n        },\n      ],\n    },\n  },\n};\n"
  },
  {
    "path": "apps/calendar/stories/e2e/month.stories.tsx",
    "content": "import { h } from 'preact';\n\nimport {\n  MOCK_MONTH_VIEW_BASE_DATE,\n  mockMonthViewEventsFixed,\n} from '@stories/mocks/mockMonthViewEvents';\nimport type { CalendarExampleStory } from '@stories/util/calendarExample';\nimport { CalendarExample } from '@stories/util/calendarExample';\n\nexport default { title: 'E2E/Month View' };\n\nconst Template: CalendarExampleStory = (args) => <CalendarExample {...args} />;\nTemplate.args = {\n  containerHeight: '100vh',\n};\n\nexport const Empty = Template.bind({});\nEmpty.args = {\n  ...Template.args,\n  options: {\n    defaultView: 'month',\n  },\n};\n\nexport const FixedEvents = Template.bind({});\nFixedEvents.args = {\n  ...Template.args,\n  options: {\n    defaultView: 'month',\n    useFormPopup: true,\n    useDetailPopup: true,\n  },\n  onInit: (cal) => {\n    cal.setDate(MOCK_MONTH_VIEW_BASE_DATE);\n    cal.createEvents(mockMonthViewEventsFixed);\n    cal.on('beforeUpdateEvent', ({ event, changes }) => {\n      cal.updateEvent(event.id, event.calendarId, changes);\n    });\n  },\n};\n\nexport const ReadOnly = Template.bind({});\nReadOnly.args = {\n  ...Template.args,\n  options: {\n    defaultView: 'month',\n    useFormPopup: true,\n    useDetailPopup: true,\n    isReadOnly: true,\n  },\n  onInit: FixedEvents.args.onInit,\n};\n"
  },
  {
    "path": "apps/calendar/stories/e2e/week.stories.tsx",
    "content": "import { h } from 'preact';\n\nimport moment from 'moment-timezone';\n\nimport { last } from '@src/utils/array';\n\nimport { mockCalendars } from '@stories/mocks/mockCalendars';\nimport { mockWeekViewEvents } from '@stories/mocks/mockWeekViewEvents';\nimport type { CalendarExampleStory } from '@stories/util/calendarExample';\nimport { CalendarExample } from '@stories/util/calendarExample';\n\nimport type { EventObject } from '@t/events';\n\nexport default { title: 'E2E/Week View' };\n\nconst Template: CalendarExampleStory = (args) => <CalendarExample {...args} />;\nTemplate.args = {\n  options: {\n    defaultView: 'week',\n    useFormPopup: true,\n    useDetailPopup: true,\n    week: {\n      showNowIndicator: false,\n    },\n    calendars: mockCalendars,\n  },\n  containerHeight: '100vh',\n  onInit: (cal) => {\n    cal.createEvents(mockWeekViewEvents);\n    cal.on('beforeUpdateEvent', ({ event, changes }) => {\n      cal.updateEvent(event.id, event.calendarId, changes);\n    });\n  },\n};\n\nexport const FixedEvents = Template.bind({});\nFixedEvents.args = {\n  ...Template.args,\n};\n\nexport const ReadOnly = Template.bind({});\nReadOnly.args = {\n  ...Template.args,\n  options: {\n    ...Template.args.options,\n    isReadOnly: true,\n  },\n};\n\nexport const HourStartOption = Template.bind({});\nHourStartOption.args = {\n  ...Template.args,\n  options: {\n    ...Template.args.options,\n    week: {\n      hourStart: 4,\n      showNowIndicator: false,\n    },\n  },\n};\n\nexport const DifferentPrimaryTimezone = Template.bind({});\nDifferentPrimaryTimezone.args = {\n  ...Template.args,\n  options: {\n    ...Template.args.options,\n    timezone: {\n      zones: [\n        {\n          timezoneName: 'Asia/Karachi',\n        },\n      ],\n    },\n  },\n};\n\nexport const MultipleTimezones = Template.bind({});\nMultipleTimezones.args = {\n  ...Template.args,\n  options: {\n    ...Template.args.options,\n    week: {\n      showTimezoneCollapseButton: true,\n    },\n    theme: {\n      week: {\n        dayGridLeft: {\n          width: '120px',\n        },\n        timeGridLeft: {\n          width: '120px',\n        },\n      },\n    },\n    timezone: {\n      zones: [\n        {\n          timezoneName: 'Asia/Karachi',\n          displayLabel: '+05:00',\n        },\n        {\n          timezoneName: 'US/Samoa',\n          displayLabel: '-11:00',\n        },\n      ],\n    },\n  },\n  onInit: (cal) => {\n    cal.createEvents(mockWeekViewEvents);\n    cal.on('beforeUpdateEvent', ({ event, changes }) => {\n      cal.updateEvent(event.id, event.calendarId, changes);\n    });\n    cal.on('clickTimezonesCollapseBtn', (prevState) => {\n      cal.setOptions({\n        week: {\n          timezonesCollapsed: !prevState,\n        },\n      });\n    });\n  },\n};\n\nexport const DaylightSavingTimeTransition = Template.bind({});\nDaylightSavingTimeTransition.args = {\n  ...Template.args,\n  options: {\n    ...Template.args.options,\n    timezone: {\n      zones: [\n        {\n          timezoneName: 'America/Los_Angeles',\n        },\n      ],\n    },\n  },\n  onInit: (cal) => {\n    cal.setDate('2022-11-07T00:00:00Z');\n    cal.createEvents([\n      {\n        id: 'forward',\n        title: 'Forward Transition',\n        category: 'time',\n        start: '2022-03-13T09:00:00Z',\n        end: '2022-03-13T10:00:00Z',\n      },\n      {\n        id: 'fallback',\n        title: 'Fallback Transition',\n        category: 'time',\n        start: '2022-11-06T08:00:00Z',\n        end: '2022-11-06T09:00:00Z',\n      },\n    ]);\n  },\n};\n\nexport const DaylightSavingTimeTransitionSouthern = Template.bind({});\nDaylightSavingTimeTransitionSouthern.args = {\n  ...Template.args,\n  options: {\n    ...Template.args.options,\n    timezone: {\n      zones: [\n        {\n          timezoneName: 'Pacific/Auckland',\n        },\n      ],\n    },\n  },\n  onInit: (cal) => {\n    cal.setDate('2022-04-03T00:00:00Z');\n    cal.createEvents([\n      {\n        id: 'forward',\n        title: 'Forward Transition',\n        category: 'time',\n        start: '2022-09-25T01:00:00+12:00',\n        end: '2022-09-25T03:00:00+13:00',\n      },\n      {\n        id: 'fallback',\n        title: 'Fallback Transition',\n        category: 'time',\n        start: '2022-04-02T12:00:00Z',\n        end: '2022-04-02T14:00:00Z',\n      },\n    ]);\n  },\n};\n\n// NOTE: For manual testing purposes\nconst timezoneNameForSystemTimezoneTest = 'US/Pacific';\n// const timezoneNameForSystemTimezoneTest = 'Asia/Seoul';\nexport const SystemTimezoneTest = Template.bind({});\nSystemTimezoneTest.args = {\n  ...Template.args,\n  options: {\n    ...Template.args.options,\n    useFormPopup: false,\n    timezone: {\n      zones: [\n        {\n          timezoneName: timezoneNameForSystemTimezoneTest,\n        },\n      ],\n    },\n  },\n  onInit: (cal) => {\n    const convert = (d: Date) =>\n      moment\n        .tz(\n          moment([\n            d.getFullYear(),\n            d.getMonth(),\n            d.getDate(),\n            d.getHours(),\n            d.getMinutes(),\n            d.getSeconds(),\n          ]).format('YYYY-MM-DD HH:mm:ss'),\n          timezoneNameForSystemTimezoneTest\n        )\n        .toISOString();\n\n    cal.setDate('2022-03-09T00:00:00Z');\n\n    cal.createEvents([\n      {\n        id: 'pst',\n        title: 'PST',\n        start: '2022-03-09T09:00:00Z', // 01:00 UTC-08, 18:00 UTC+09\n        end: '2022-03-09T10:00:00Z', // 02:00 UTC-08, 19:00 UTC+09\n      },\n      {\n        id: 'pdt',\n        title: 'PDT',\n        start: '2022-03-16T08:00:00Z', // 01:00 UTC-07, 17:00 UTC+09\n        end: '2022-03-16T09:00:00Z', // 02:00 UTC-07, 18:00 UTC+09\n      },\n    ]);\n\n    cal.on('selectDateTime', (info) => {\n      const startDate = info.start;\n      const endDate = info.end;\n\n      cal.createEvents([\n        {\n          id: Date.now().toString(32).slice(0, 8),\n          title: 'New Event',\n          start: convert(startDate),\n          end: convert(endDate),\n          category: 'time',\n        },\n      ]);\n\n      cal.clearGridSelections();\n    });\n\n    cal.on('beforeUpdateEvent', ({ event, changes }) => {\n      const zonedChanges = Object.keys(changes).reduce<EventObject>((acc, _k) => {\n        const key = _k as keyof EventObject;\n        acc[key] = convert(changes[key]);\n\n        return acc;\n      }, {});\n\n      cal.updateEvent(event.id, event.calendarId, zonedChanges);\n    });\n  },\n};\n\nexport const CustomTemplate = Template.bind({});\nCustomTemplate.args = {\n  ...Template.args,\n  options: {\n    ...Template.args.options,\n    template: {\n      alldayTitle() {\n        // Insert <script> for DOM Purify Test\n        return '<span><script></script>CUSTOM All Day</span>';\n      },\n      taskTitle() {\n        return '<span>CUSTOM TASK</span>';\n      },\n    },\n  },\n};\n\nexport const DuplicateEvents = Template.bind({});\nDuplicateEvents.args = {\n  ...Template.args,\n  options: {\n    ...Template.args.options,\n    week: {\n      ...Template.args.options?.week,\n      collapseDuplicateEvents: {\n        getDuplicateEvents: (targetEvent, events) =>\n          events\n            .filter((event) => event.id === targetEvent.id)\n            .sort((a, b) => (b.calendarId > a.calendarId ? 1 : -1)), // descending order\n        getMainEvent: (events) =>\n          events.find(({ title }) => title.includes('- main')) || last(events),\n      },\n    },\n    useDetailPopup: false,\n  },\n};\n"
  },
  {
    "path": "apps/calendar/stories/eventDetailPopup.stories.tsx",
    "content": "import { Fragment, h } from 'preact';\n\nimport type { Story } from '@storybook/preact';\n\nimport { EventDetailPopup } from '@src/components/popup/eventDetailPopup';\nimport { EventFormPopup } from '@src/components/popup/eventFormPopup';\nimport { useDispatch } from '@src/contexts/calendarStore';\nimport EventModel from '@src/model/eventModel';\nimport TZDate from '@src/time/date';\n\nimport { ProviderWrapper } from '@stories/util/providerWrapper';\n\nimport type { PropsWithChildren } from '@t/components/common';\nimport type { EventDetailPopupParam } from '@t/store';\n\nexport default {\n  component: EventDetailPopup,\n  title: 'Popups/EventDetailPopup',\n};\n\nfunction Wrapper({ children, event, eventRect }: PropsWithChildren<EventDetailPopupParam>) {\n  const { showDetailPopup } = useDispatch('popup');\n  showDetailPopup(\n    {\n      event,\n      eventRect,\n    },\n    false\n  );\n\n  return <Fragment>{children}</Fragment>;\n}\n\nconst Template: Story<EventDetailPopupParam> = ({ event }) => (\n  <ProviderWrapper>\n    <Wrapper\n      event={event}\n      eventRect={{\n        top: 0,\n        left: 0,\n        width: 100,\n        height: 100,\n      }}\n    >\n      <EventDetailPopup />\n      <EventFormPopup />\n    </Wrapper>\n  </ProviderWrapper>\n);\n\nexport const EventDetailPopupWithCalendars = Template.bind({});\nEventDetailPopupWithCalendars.args = {\n  event: new EventModel({\n    id: 'id',\n    calendarId: 'calendar id',\n    title: 'title',\n    body: 'body',\n    start: new TZDate(),\n    end: new TZDate(),\n    isAllday: false,\n    location: 'location',\n    attendees: ['attendee1', 'attendee2'],\n    recurrenceRule: 'recurrence rule',\n    isReadOnly: false,\n    backgroundColor: '#03bd9e',\n    state: 'Busy',\n  }),\n};\n"
  },
  {
    "path": "apps/calendar/stories/eventFormPopup.stories.tsx",
    "content": "import { Fragment, h } from 'preact';\n\nimport type { Story } from '@storybook/preact';\n\nimport { EventFormPopup } from '@src/components/popup/eventFormPopup';\nimport { useDispatch } from '@src/contexts/calendarStore';\nimport TZDate from '@src/time/date';\n\nimport { calendars as mockCalendars } from '@stories/util/mockCalendars';\nimport { ProviderWrapper } from '@stories/util/providerWrapper';\n\nimport type { PropsWithChildren } from '@t/components/common';\nimport type { CalendarInfo } from '@t/options';\nimport type { EventFormPopupParam } from '@t/store';\n\nexport default {\n  component: EventFormPopup,\n  title: 'Popups/EventFormPopup',\n};\n\ninterface EventFormPopupStoryProps extends EventFormPopupParam {\n  calendars?: CalendarInfo[];\n}\n\nfunction Wrapper({\n  children,\n  title,\n  location,\n  start,\n  end,\n  isAllday,\n  isPrivate,\n  isCreationPopup,\n}: PropsWithChildren<EventFormPopupParam>) {\n  const { showFormPopup } = useDispatch('popup');\n  showFormPopup({\n    isCreationPopup,\n    title,\n    location,\n    start,\n    end,\n    isAllday,\n    isPrivate,\n  });\n\n  return <Fragment>{children}</Fragment>;\n}\n\nconst Template: Story<EventFormPopupStoryProps> = ({\n  calendars,\n  title,\n  location,\n  start,\n  end,\n  isAllday = false,\n  isPrivate = false,\n}) => (\n  <ProviderWrapper options={{ calendars }}>\n    <Wrapper\n      title={title}\n      location={location}\n      start={start}\n      end={end}\n      isAllday={isAllday}\n      isPrivate={isPrivate}\n      isCreationPopup={true}\n    >\n      <EventFormPopup />\n    </Wrapper>\n  </ProviderWrapper>\n);\n\nexport const EventFormPopupWithCalendars = Template.bind({});\nEventFormPopupWithCalendars.args = {\n  start: new TZDate(),\n  end: new TZDate(),\n  calendars: mockCalendars,\n};\n\nexport const EventFormPopupWithoutCalendars = Template.bind({});\nEventFormPopupWithoutCalendars.args = {\n  start: new TZDate(),\n  end: new TZDate(),\n};\n"
  },
  {
    "path": "apps/calendar/stories/events.stories.tsx",
    "content": "import { h } from 'preact';\n\nimport { BackgroundEvent } from '@src/components/events/backgroundEvent';\nimport { TimeEvent } from '@src/components/events/timeEvent';\nimport EventModel from '@src/model/eventModel';\nimport EventUIModel from '@src/model/eventUIModel';\n\nimport { ProviderWrapper } from '@stories/util/providerWrapper';\n\nexport default { title: 'Components/EventBlocks' };\n\nexport const timeEvent = () => {\n  const event = new EventModel({\n    title: 'Time Event 2',\n    backgroundColor: 'green',\n  });\n  const uiModel = new EventUIModel(event);\n\n  return (\n    <ProviderWrapper>\n      <TimeEvent uiModel={uiModel} />\n    </ProviderWrapper>\n  );\n};\n\nexport const backgroundEvent = () => {\n  const uiModel = new EventUIModel(\n    new EventModel({\n      backgroundColor: 'rgba(100, 100, 100, .3)',\n    })\n  );\n\n  return <BackgroundEvent uiModel={uiModel} />;\n};\n"
  },
  {
    "path": "apps/calendar/stories/gridHeader.stories.tsx",
    "content": "import { h } from 'preact';\n\nimport type { Story } from '@storybook/preact';\n\nimport { GridHeader } from '@src/components/dayGridCommon/gridHeader';\nimport { getRowStyleInfo } from '@src/time/datetime';\n\nimport { ProviderWrapper } from '@stories/util/providerWrapper';\n\nimport type { TemplateMonthDayName } from '@t/template';\n\nexport default { title: 'Components/GridHeader', component: GridHeader };\n\ninterface DayNamesStory {\n  dayNames: TemplateMonthDayName[];\n  marginLeft?: string;\n}\n\nconst Template: Story<DayNamesStory> = ({ dayNames, marginLeft }) => {\n  const { rowStyleInfo } = getRowStyleInfo(dayNames.length, true, 0, true);\n\n  return (\n    <ProviderWrapper>\n      <GridHeader\n        type=\"month\"\n        dayNames={dayNames}\n        marginLeft={marginLeft}\n        rowStyleInfo={rowStyleInfo}\n      />\n    </ProviderWrapper>\n  );\n};\n\nconst oneDayName = [\n  {\n    label: 'Mon',\n    day: 1,\n  },\n];\n\nconst threeDayNames = [\n  {\n    label: 'Mon',\n    day: 1,\n  },\n  {\n    label: 'Wed',\n    day: 3,\n  },\n  {\n    label: 'Fri',\n    day: 5,\n  },\n];\n\nexport const oneDay = Template.bind({});\noneDay.args = {\n  dayNames: oneDayName,\n};\n\nexport const threeDays = Template.bind({});\nthreeDays.args = {\n  dayNames: threeDayNames,\n};\n\nexport const oneDayWithMargin = Template.bind({});\noneDayWithMargin.args = {\n  dayNames: oneDayName,\n  marginLeft: '60px',\n};\n\nexport const threeDaysWithMargin = Template.bind({});\nthreeDaysWithMargin.args = {\n  dayNames: threeDayNames,\n  marginLeft: '60px',\n};\n"
  },
  {
    "path": "apps/calendar/stories/gridRow.stories.tsx",
    "content": "import { h } from 'preact';\n\nimport type { Story } from '@storybook/preact';\n\nimport { OtherGridRow } from '@src/components/dayGridWeek/otherGridRow';\nimport { Layout } from '@src/components/layout';\nimport { Panel } from '@src/components/panel';\nimport { createEventCollection } from '@src/controller/base';\nimport { getWeekDates, getWeekViewEvents } from '@src/helpers/grid';\nimport TZDate from '@src/time/date';\nimport { Day, getRowStyleInfo } from '@src/time/datetime';\nimport { first, last } from '@src/utils/array';\n\nimport { ProviderWrapper } from '@stories/util/providerWrapper';\nimport { createRandomEventModelsForMonth } from '@stories/util/randomEvents';\n\nimport type { CalendarData } from '@t/events';\n\nexport default {\n  title: 'Components/WeekGridRow',\n  component: OtherGridRow,\n  args: { primary: true },\n};\n\nconst events = createRandomEventModelsForMonth(40);\n\nconst weekDates = getWeekDates(new TZDate(), { startDayOfWeek: Day.SUN, workweek: false });\nconst calendarData: CalendarData = {\n  calendars: [],\n  events: createEventCollection(...events),\n  idsOfDay: {},\n};\nconst dayGridEvents = getWeekViewEvents(weekDates, calendarData, {\n  narrowWeekend: false,\n  weekStartDate: first(weekDates),\n  weekEndDate: last(weekDates),\n});\n\nconst Template: Story = (args) => {\n  const { cellWidthMap } = getRowStyleInfo(weekDates.length, true, 0, true);\n\n  return (\n    <ProviderWrapper options={args.options} events={events}>\n      <Layout height={500}>\n        <Panel name=\"milestone\" resizable minHeight={20} maxHeight={args.maxHeight}>\n          <OtherGridRow\n            weekDates={weekDates}\n            events={dayGridEvents.milestone}\n            category=\"milestone\"\n            options={{ narrowWeekend: false }}\n            gridColWidthMap={cellWidthMap}\n          />\n        </Panel>\n      </Layout>\n    </ProviderWrapper>\n  );\n};\n\nexport const milestone = Template.bind({});\n\nmilestone.storyName = 'random events milestone';\n"
  },
  {
    "path": "apps/calendar/stories/helper/event.ts",
    "content": "import EventModel from '@src/model/eventModel';\nimport EventUIModel from '@src/model/eventUIModel';\n\nimport type { EventObject } from '@t/events';\n\nexport function createEventModels(data: EventObject[]): EventUIModel[] {\n  return data.map((datum) => new EventUIModel(new EventModel(datum)));\n}\n"
  },
  {
    "path": "apps/calendar/stories/layout.stories.tsx",
    "content": "import { h } from 'preact';\n\nimport { Layout } from '@src/components/layout';\nimport { Panel } from '@src/components/panel';\n\nimport { ProviderWrapper } from '@stories/util/providerWrapper';\n\nexport default { title: 'Components/Layout', component: Layout };\n\nexport const vertical = () => (\n  <ProviderWrapper>\n    <Layout>\n      <Panel name=\"dayName\" initialHeight={50}>\n        <div style=\"border-bottom: 1px solid #bbb; height: 100%;\">DayName Panel</div>\n      </Panel>\n      <Panel name=\"milestone\" resizable initialHeight={50}>\n        <div>Milestone Panel</div>\n      </Panel>\n      <Panel name=\"task\" resizable>\n        <div>Task Panel</div>\n      </Panel>\n      <Panel name=\"allday\" resizable minHeight={30}>\n        <div>All Day Panel</div>\n      </Panel>\n      <Panel name=\"time\" autoSize={1}>\n        <div style=\"height: 100%; border-bottom: 1px solid #bbb\">Time Panel</div>\n      </Panel>\n    </Layout>\n  </ProviderWrapper>\n);\n\nexport const verticalWithOverflow = () => (\n  <ProviderWrapper>\n    <Layout>\n      <Panel name=\"dayName\" initialHeight={50} overflowY>\n        <div style=\"border-bottom: 1px solid #bbb; height: 300px;\">Overflow-Y Panel</div>\n      </Panel>\n    </Layout>\n  </ProviderWrapper>\n);\n\nexport const horizontalWithOverflow = () => (\n  <ProviderWrapper>\n    <Layout>\n      <Panel name=\"dayName\" initialWidth={200} initialHeight={200} overflowX>\n        <div style=\"border-right: 1px solid #bbb; width: 300px;\">Overflow-X Panel</div>\n      </Panel>\n    </Layout>\n  </ProviderWrapper>\n);\n"
  },
  {
    "path": "apps/calendar/stories/main.stories.tsx",
    "content": "import { h } from 'preact';\n\nimport type { Story } from '@storybook/preact';\n\nimport { Main } from '@src/components/view/main';\nimport { useDispatch } from '@src/contexts/calendarStore';\nimport { cls } from '@src/helpers/css';\n\nimport { ProviderWrapper } from '@stories/util/providerWrapper';\nimport { createRandomEventModelsForMonth } from '@stories/util/randomEvents';\n\nimport type { PropsWithChildren } from '@t/components/common';\n\nexport default { title: 'Views/Main', component: Main };\n\nconst style = {\n  position: 'absolute',\n  left: 0,\n  right: 0,\n  bottom: 5,\n  top: 5,\n};\n\nconst Wrapper = ({ children }: PropsWithChildren) => (\n  <div className={cls('layout')} style={style}>\n    {children}\n  </div>\n);\n\nconst events = createRandomEventModelsForMonth();\n\nconst Toolbar = () => {\n  const { changeView } = useDispatch('view');\n\n  return (\n    <div>\n      <button onClick={() => changeView('month')}>Month</button>\n      <button onClick={() => changeView('week')}>Week</button>\n      <button onClick={() => changeView('day')}>Day</button>\n    </div>\n  );\n};\n\nconst Template: Story = (args) => (\n  <ProviderWrapper options={args.options} events={args.events}>\n    <Wrapper>\n      <Toolbar />\n      <Main />\n    </Wrapper>\n  </ProviderWrapper>\n);\n\nexport const Default = Template.bind({});\nDefault.args = {\n  events,\n  options: { useFormPopup: true, useDetailPopup: true },\n};\n"
  },
  {
    "path": "apps/calendar/stories/mocks/mockCalendars.ts",
    "content": "import type { CalendarInfo } from '@src/types/options';\n\nexport const mockCalendars: CalendarInfo[] = [\n  {\n    id: 'cal1',\n    name: 'First',\n  },\n  {\n    id: 'cal2',\n    name: 'Second',\n    borderColor: '#00a9ff',\n    backgroundColor: '#00a9ff',\n    dragBackgroundColor: '#00a9ff',\n  },\n  {\n    id: 'cal3',\n    name: 'Third',\n    borderColor: '#03bd9e',\n    backgroundColor: '#03bd9e',\n    dragBackgroundColor: '#03bd9e',\n  },\n];\n"
  },
  {
    "path": "apps/calendar/stories/mocks/mockDayViewEvents.ts",
    "content": "import TZDate from '@src/time/date';\nimport { addDate, setTimeStrToDate } from '@src/time/datetime';\n\nimport type { MockedWeekViewEvents } from '@stories/mocks/types';\n\nconst today = new TZDate();\nconst yesterday = addDate(today, -1);\nconst tomorrow = addDate(today, 1);\n\nexport const mockDayViewEvents: MockedWeekViewEvents[] = [\n  {\n    id: '1',\n    calendarId: 'cal1',\n    title: 'yesterday ~ today',\n    category: 'allday',\n    isAllday: true,\n    start: yesterday,\n    end: today,\n  },\n  {\n    id: '2',\n    calendarId: 'cal1',\n    title: 'today ~ today',\n    category: 'allday',\n    isAllday: true,\n    start: today,\n    end: today,\n  },\n  {\n    id: '3',\n    calendarId: 'cal1',\n    title: 'today ~ tomorrow',\n    category: 'allday',\n    isAllday: true,\n    start: today,\n    end: tomorrow,\n  },\n  {\n    id: '4',\n    calendarId: 'cal1',\n    title: 'long time',\n    category: 'time',\n    isAllday: false,\n    start: setTimeStrToDate(yesterday, '10:00'),\n    end: setTimeStrToDate(today, '06:00'),\n  },\n  {\n    id: '5',\n    calendarId: 'cal1',\n    title: 'short time',\n    category: 'time',\n    isAllday: false,\n    start: setTimeStrToDate(today, '04:00'),\n    end: setTimeStrToDate(today, '06:00'),\n  },\n  {\n    id: '6',\n    calendarId: 'cal1',\n    title: 'short + duration',\n    category: 'time',\n    isAllday: false,\n    start: setTimeStrToDate(today, '04:00'),\n    end: setTimeStrToDate(today, '06:00'),\n    goingDuration: 60,\n    comingDuration: 120,\n  },\n];\n"
  },
  {
    "path": "apps/calendar/stories/mocks/mockMonthViewEvents.ts",
    "content": "import TZDate from '@src/time/date';\nimport { addDate } from '@src/time/datetime';\n\nimport type { MockedMonthViewEvents } from '@stories/mocks/types';\n\nconst DAYS_OF_WEEK = 7;\n\nexport function createMockMonthViewEvents(baseDate?: string): MockedMonthViewEvents[] {\n  const today = baseDate ? new TZDate(baseDate) : new TZDate();\n  const thisSunday = addDate(today, -today.getDay());\n  const sundayDate = thisSunday.getDate();\n  const sundayMonth = thisSunday.getMonth();\n  const todayMonth = today.getMonth();\n  const weekCount =\n    sundayMonth !== todayMonth\n      ? -1\n      : Math.floor(\n          sundayDate % DAYS_OF_WEEK ? sundayDate / DAYS_OF_WEEK : (sundayDate - 1) / DAYS_OF_WEEK\n        );\n  const firstSunday = addDate(thisSunday, -weekCount * DAYS_OF_WEEK);\n  const firstTuesday = addDate(firstSunday, 2);\n  const secondTuesday = addDate(firstTuesday, DAYS_OF_WEEK);\n  const secondThursday = addDate(secondTuesday, 2);\n  const thirdThursday = addDate(secondThursday, DAYS_OF_WEEK);\n  const thirdSaturday = addDate(thirdThursday, 2);\n\n  const mockMonthViewEvents = [\n    {\n      id: '0',\n      calendarId: 'cal1',\n      title: 'event1',\n      start: firstSunday,\n      end: secondTuesday,\n    },\n    {\n      id: '1',\n      calendarId: 'cal1',\n      title: 'event2',\n      start: secondTuesday,\n      end: secondThursday,\n    },\n    {\n      id: '2',\n      calendarId: 'cal1',\n      title: 'event3',\n      start: thirdThursday,\n      end: thirdSaturday,\n    },\n  ];\n\n  for (let i = 0; i < 10; i += 1) {\n    mockMonthViewEvents.push({\n      id: `${i}${i}`,\n      calendarId: 'cal2',\n      title: `event2-${i}`,\n      start: secondTuesday,\n      end: secondThursday,\n    });\n  }\n\n  return mockMonthViewEvents;\n}\n\nexport const mockMonthViewEvents = createMockMonthViewEvents();\n\n// For E2E tests, set the base date in order to guarantee the same events are returned\nexport const MOCK_MONTH_VIEW_BASE_DATE = '2022-04-01';\nexport const mockMonthViewEventsFixed = createMockMonthViewEvents(MOCK_MONTH_VIEW_BASE_DATE);\n"
  },
  {
    "path": "apps/calendar/stories/mocks/mockWeekViewEvents.ts",
    "content": "import TZDate from '@src/time/date';\nimport { addDate, setTimeStrToDate } from '@src/time/datetime';\n\nimport type { MockedWeekViewEvents } from '@stories/mocks/types';\n\nconst today = new TZDate();\nconst sunday = addDate(today, -today.getDay());\nconst monday = addDate(sunday, 1);\nconst tuesday = addDate(sunday, 2);\nconst wednesday = addDate(sunday, 3);\nconst thursday = addDate(sunday, 4);\nconst friday = addDate(sunday, 5);\nconst saturday = addDate(sunday, 6);\n\nexport const mockWeekViewEvents: MockedWeekViewEvents[] = [\n  {\n    id: '1',\n    calendarId: 'cal1',\n    title: 'event1',\n    category: 'allday',\n    isAllday: true,\n    start: sunday,\n    end: tuesday,\n  },\n  {\n    id: '2',\n    calendarId: 'cal1',\n    title: 'event2',\n    category: 'allday',\n    isAllday: true,\n    start: tuesday,\n    end: thursday,\n  },\n  {\n    id: '3',\n    calendarId: 'cal1',\n    title: 'event3',\n    category: 'allday',\n    isAllday: true,\n    start: thursday,\n    end: saturday,\n  },\n  {\n    id: '4',\n    calendarId: 'cal1',\n    title: 'two-view event',\n    category: 'time',\n    isAllday: false,\n    start: setTimeStrToDate(addDate(sunday, -1), '10:00'),\n    end: setTimeStrToDate(sunday, '06:00'),\n  },\n  {\n    id: '5',\n    calendarId: 'cal1',\n    title: 'short time event',\n    category: 'time',\n    isAllday: false,\n    start: setTimeStrToDate(wednesday, '04:00'),\n    end: setTimeStrToDate(wednesday, '06:00'),\n  },\n  {\n    id: '6',\n    calendarId: 'cal1',\n    title: 'long time event',\n    category: 'time',\n    isAllday: false,\n    start: setTimeStrToDate(friday, '10:00'),\n    end: setTimeStrToDate(saturday, '06:00'),\n  },\n  {\n    id: '7',\n    calendarId: 'cal1',\n    title: 'short + duration',\n    category: 'time',\n    isAllday: false,\n    start: setTimeStrToDate(tuesday, '05:00'),\n    end: setTimeStrToDate(tuesday, '06:00'),\n    goingDuration: 60,\n    comingDuration: 120,\n  },\n  {\n    id: '8',\n    calendarId: 'cal1',\n    title: 'same start and end',\n    category: 'time',\n    isAllday: false,\n    start: setTimeStrToDate(monday, '05:00'),\n    end: setTimeStrToDate(monday, '05:00'),\n  },\n  {\n    id: '9',\n    calendarId: 'cal3',\n    title: 'duplicate event',\n    category: 'time',\n    isAllday: false,\n    start: setTimeStrToDate(monday, '06:00'),\n    end: setTimeStrToDate(monday, '07:00'),\n  },\n  {\n    id: '9',\n    calendarId: 'cal2',\n    title: 'duplicate event with attendee',\n    category: 'time',\n    isAllday: false,\n    start: setTimeStrToDate(monday, '06:00'),\n    end: setTimeStrToDate(monday, '07:00'),\n    comingDuration: 30,\n    attendees: ['a', 'b', 'c'],\n  },\n  {\n    id: '9',\n    calendarId: 'cal1',\n    title: 'duplicate event - main',\n    category: 'time',\n    isAllday: false,\n    start: setTimeStrToDate(monday, '06:00'),\n    end: setTimeStrToDate(monday, '07:00'),\n    goingDuration: 30,\n    comingDuration: 60,\n  },\n  {\n    id: '9-1',\n    calendarId: 'cal1',\n    title: 'normal event',\n    category: 'time',\n    isAllday: false,\n    start: setTimeStrToDate(monday, '08:00'),\n    end: setTimeStrToDate(monday, '09:00'),\n  },\n  {\n    id: '9-2',\n    calendarId: 'cal2',\n    title: 'other event',\n    category: 'time',\n    isAllday: false,\n    start: setTimeStrToDate(monday, '08:00'),\n    end: setTimeStrToDate(monday, '09:00'),\n  },\n  {\n    id: '10',\n    calendarId: 'cal1',\n    title: 'duplicate event 2',\n    category: 'time',\n    isAllday: false,\n    start: setTimeStrToDate(monday, '09:00'),\n    end: setTimeStrToDate(monday, '10:00'),\n  },\n  {\n    id: '10',\n    calendarId: 'cal2',\n    title: 'duplicate event 2 with attendee',\n    category: 'time',\n    isAllday: false,\n    start: setTimeStrToDate(monday, '09:00'),\n    end: setTimeStrToDate(monday, '10:00'),\n    attendees: ['a', 'b', 'c'],\n    goingDuration: 30,\n  },\n  {\n    id: '10',\n    calendarId: 'cal3',\n    title: 'duplicate event 2 - main',\n    category: 'time',\n    isAllday: false,\n    start: setTimeStrToDate(monday, '09:00'),\n    end: setTimeStrToDate(monday, '10:00'),\n    goingDuration: 30,\n    comingDuration: 60,\n  },\n  {\n    id: '11',\n    calendarId: 'cal1',\n    title: 'duplicate long event',\n    category: 'time',\n    isAllday: false,\n    start: setTimeStrToDate(wednesday, '07:00'),\n    end: setTimeStrToDate(thursday, '04:00'),\n  },\n  {\n    id: '11',\n    calendarId: 'cal2',\n    title: 'duplicate long event - main',\n    category: 'time',\n    isAllday: false,\n    start: setTimeStrToDate(wednesday, '07:00'),\n    end: setTimeStrToDate(thursday, '04:00'),\n    attendees: ['a', 'b', 'c'],\n  },\n];\n"
  },
  {
    "path": "apps/calendar/stories/mocks/types.ts",
    "content": "import type TZDate from '@src/time/date';\n\nimport type { EventObject } from '@t/events';\n\nexport type MockedWeekViewEvents = Required<\n  Pick<EventObject, 'id' | 'calendarId' | 'title' | 'category' | 'isAllday'>\n> & {\n  start: TZDate;\n  end: TZDate;\n  goingDuration?: number;\n  comingDuration?: number;\n  attendees?: string[];\n};\n\nexport type MockedMonthViewEvents = Omit<MockedWeekViewEvents, 'isAllday' | 'category'>;\n"
  },
  {
    "path": "apps/calendar/stories/monthView.stories.tsx",
    "content": "import { h } from 'preact';\n\nimport type { Story } from '@storybook/preact';\n\nimport { Month } from '@src/components/view/month';\n\nimport { ProviderWrapper } from '@stories/util/providerWrapper';\nimport { createRandomEventModelsForMonth } from '@stories/util/randomEvents';\n\nexport default { title: 'Views/MonthView', component: Month };\n\nconst Template: Story = (args) => (\n  <ProviderWrapper options={args.options} events={args.events}>\n    <Month />\n  </ProviderWrapper>\n);\n\nexport const basic = Template.bind({});\n\nexport const narrowWeekend = Template.bind({});\nnarrowWeekend.args = {\n  options: { month: { narrowWeekend: true } },\n};\n\nexport const startDayOfWeek = Template.bind({});\nstartDayOfWeek.args = {\n  options: { month: { startDayOfWeek: 3 } },\n};\n\nexport const dayNames = Template.bind({});\ndayNames.args = {\n  options: {\n    month: {\n      dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],\n    },\n  },\n};\n\nexport const workweek = Template.bind({});\nworkweek.args = {\n  options: { month: { workweek: true } },\n};\n\nexport const twoWeeks = Template.bind({});\ntwoWeeks.args = {\n  options: { month: { visibleWeeksCount: 2 } },\n};\n\nexport const randomEvents = Template.bind({});\nrandomEvents.args = {\n  options: { month: { narrowWeekend: true } },\n  events: createRandomEventModelsForMonth(40),\n};\n"
  },
  {
    "path": "apps/calendar/stories/timegrid.stories.tsx",
    "content": "import type { ComponentProps } from 'preact';\nimport { h } from 'preact';\n\nimport type { StoryFn } from '@storybook/preact';\n\nimport { TimeGrid } from '@src/components/timeGrid/timeGrid';\nimport { cls, toPercent } from '@src/helpers/css';\nimport { createTimeGridData, getWeekDates } from '@src/helpers/grid';\nimport TZDate from '@src/time/date';\nimport { addDate, toStartOfDay } from '@src/time/datetime';\n\nimport normalEvents from '@stories/data/events.json';\nimport { createEventModels } from '@stories/helper/event';\nimport { ProviderWrapper } from '@stories/util/providerWrapper';\nimport { createRandomEvents } from '@stories/util/randomEvents';\n\nimport type { EventObject } from '@t/events';\n\nexport default { title: 'Components/TimeGrid', component: TimeGrid };\n\nfunction toThisWeek(date: TZDate) {\n  const today = toStartOfDay(new TZDate());\n  const adjustForWeekStart = today.getDay();\n  const day = date.getDay();\n\n  date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate() - adjustForWeekStart);\n\n  return addDate(date, day);\n}\n\nfunction getNormalEvents() {\n  return normalEvents.map((event) => {\n    const start = toThisWeek(new TZDate(event.start));\n    const end = toThisWeek(new TZDate(event.end));\n\n    return {\n      ...event,\n      start,\n      end,\n    };\n  });\n}\n\nfunction getEvents() {\n  // const start = toStartOfDay(new TZDate());\n  // const adjustForWeekStart = start.getDay();\n  // const disabledAMEvents: EventObject[] = range(0, 7).map((date) => {\n  //   const eventStart = addDate(start, date - adjustForWeekStart);\n  //\n  //   return {\n  //     category: 'background',\n  //     start: eventStart,\n  //     end: addHours(eventStart, 9),\n  //     backgroundColor: 'rgba(100, 100, 100, .3)',\n  //   };\n  // });\n  // const disabledPMEvents: EventObject[] = range(0, 7).map((date) => {\n  //   const eventStart = addDate(start, date - adjustForWeekStart);\n  //\n  //   return {\n  //     category: 'background',\n  //     start: addHours(eventStart, 18),\n  //     end: addHours(eventStart, 24),\n  //     backgroundColor: 'rgba(100, 100, 100, .3)',\n  //   };\n  // });\n  // const disabledLunchEvents: EventObject[] = range(0, 7).map((date) => {\n  //   const eventStart = addDate(start, date - adjustForWeekStart);\n  //\n  //   return {\n  //     category: 'background',\n  //     start: addHours(eventStart, 12),\n  //     end: addHours(eventStart, 13),\n  //     backgroundColor: 'rgba(23, 255, 100, .3)',\n  //   };\n  // });\n  // const data: EventObject[] = disabledAMEvents.concat(\n  //   disabledPMEvents,\n  //   disabledLunchEvents,\n  //   getNormalEvents()\n  // );\n\n  return createEventModels(getNormalEvents());\n}\n\nfunction getTimeGridData() {\n  const now = new TZDate();\n  const weekDates = getWeekDates(now, { startDayOfWeek: 0, workweek: false });\n  return createTimeGridData(weekDates, { hourStart: 0, hourEnd: 24 });\n}\n\ntype TimeGridProps = ComponentProps<typeof TimeGrid>;\nconst Template: StoryFn<TimeGridProps> = (args) => (\n  <ProviderWrapper>\n    <div className={cls('layout')} style={{ height: toPercent(100) }}>\n      <TimeGrid {...args} />\n    </div>\n  </ProviderWrapper>\n);\n\nexport const Basic = Template.bind({});\nBasic.args = {\n  events: getEvents(),\n  timeGridData: getTimeGridData(),\n};\n\nexport const RandomEvents = Template.bind({});\nconst getRandomEvents = () => {\n  const today = new TZDate();\n  const start = addDate(new TZDate(), -today.getDay());\n  const end = addDate(start, 6);\n  const data: EventObject[] = createRandomEvents('week', start, end);\n  return createEventModels(data);\n};\nRandomEvents.args = {\n  events: getRandomEvents(),\n  timeGridData: getTimeGridData(),\n};\n"
  },
  {
    "path": "apps/calendar/stories/util/calendarExample.tsx",
    "content": "import { h } from 'preact';\nimport { useLayoutEffect, useRef } from 'preact/hooks';\n\nimport type { Story } from '@storybook/preact';\n\nimport Calendar from '@src/factory/calendar';\n\nimport type { Options } from '@t/options';\n\ninterface Props {\n  options?: Options;\n  containerHeight?: number | string;\n  onInit?: (instance: Calendar) => void;\n}\n\nexport type CalendarExampleStory = Story<Props>;\n\nexport function CalendarExample({ options, containerHeight = 600, onInit }: Props) {\n  const containerRef = useRef<HTMLDivElement | null>(null);\n  const calendarRef = useRef<Calendar | null>(null);\n\n  useLayoutEffect(() => {\n    if (containerRef.current) {\n      calendarRef.current = new Calendar(containerRef.current, options);\n\n      // For handling instance after initialization.\n      // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n      // @ts-ignore\n      window.$cal = calendarRef.current;\n\n      onInit?.(calendarRef.current);\n    }\n  }, [onInit, options]);\n\n  return <div className=\"container\" ref={containerRef} style={{ height: containerHeight }} />;\n}\n"
  },
  {
    "path": "apps/calendar/stories/util/mockCalendarDates.ts",
    "content": "import range from 'tui-code-snippet/array/range';\n\nimport TZDate from '@src/time/date';\n\nfunction getMockCurrentDate() {\n  const current = new Date();\n  const year = current.getFullYear();\n  const month = current.getMonth();\n  const date = current.getDate();\n  const dayOfWeek = current.getDay();\n\n  return { year, month, date, dayOfWeek };\n}\n\nexport function getWeekDates() {\n  const { year, month, date, dayOfWeek } = getMockCurrentDate();\n\n  return range(7).map((index) => new TZDate(year, month, date + (index - dayOfWeek)));\n}\n\nexport function getWeekendDates() {\n  const { year, month } = getMockCurrentDate();\n  const [, , , , , , saturday] = getWeekDates();\n\n  return [saturday, new TZDate(year, month, saturday.getDate() + 1)];\n}\n"
  },
  {
    "path": "apps/calendar/stories/util/mockCalendars.ts",
    "content": "import type { CalendarInfo } from '@t/options';\n\nexport const calendars: CalendarInfo[] = [];\n\nfunction addCalendar(calendar: CalendarInfo) {\n  calendars.push(calendar);\n}\n\nconst generateCalendarId = (function () {\n  let id = 0;\n\n  return function () {\n    id = id + 1;\n\n    return id;\n  };\n})();\n\nfunction initialize() {\n  const calendarNames = [\n    'My Calendar',\n    'Company',\n    'Family',\n    'Friend',\n    'Travel',\n    'etc',\n    'Birthdays',\n    'National Holidays',\n  ];\n  const calendarColors = [\n    '#9e5fff',\n    '#00a9ff',\n    '#ff5583',\n    '#03bd9e',\n    '#bbdc00',\n    '#9d9d9d',\n    '#ffbb3b',\n    '#ff4040',\n  ];\n\n  calendarNames.forEach((name, idx) => {\n    const color = calendarColors[idx];\n    addCalendar({\n      id: String(generateCalendarId()),\n      name,\n      color: '#000',\n      backgroundColor: color,\n      borderColor: color,\n      dragBackgroundColor: color,\n    });\n  });\n}\n\ninitialize();\n"
  },
  {
    "path": "apps/calendar/stories/util/providerWrapper.tsx",
    "content": "import type { RenderableProps } from 'preact';\nimport { h } from 'preact';\n\nimport { CalendarContainer } from '@src/calendarContainer';\nimport { initCalendarStore } from '@src/contexts/calendarStore';\nimport { initThemeStore } from '@src/contexts/themeStore';\nimport type EventModel from '@src/model/eventModel';\nimport { EventBusImpl } from '@src/utils/eventBus';\n\nimport type { Options } from '@t/options';\n\nconst rootContainerStyle = {\n  position: 'absolute',\n  left: 0,\n  right: 0,\n  bottom: 0,\n  top: 0,\n};\n\ntype Props = {\n  options?: Options;\n  events?: EventModel[];\n};\n\nexport function ProviderWrapper({\n  children,\n  options: optionsUserInput = {},\n  events = [],\n}: RenderableProps<Props>) {\n  const theme = initThemeStore();\n  const store = initCalendarStore(optionsUserInput);\n  const eventBus = new EventBusImpl();\n  const { dispatch } = store.getState();\n\n  dispatch.options.setOptions(optionsUserInput);\n  dispatch.calendar.clearEvents();\n\n  if (events.length) {\n    dispatch.calendar.createEvents(events);\n  }\n\n  return (\n    <CalendarContainer theme={theme} store={store} eventBus={eventBus}>\n      <div style={rootContainerStyle}>{children}</div>\n    </CalendarContainer>\n  );\n}\n"
  },
  {
    "path": "apps/calendar/stories/util/randomEvents.ts",
    "content": "import Chance from 'chance';\nimport moment from 'moment-timezone';\n\nimport { createDateMatrixOfMonth } from '@src/helpers/grid';\nimport EventModel from '@src/model/eventModel';\nimport type TZDate from '@src/time/date';\n\nimport { calendars } from '@stories/util/mockCalendars';\n\nimport type { EventCategory, EventObject } from '@t/events';\nimport type { CalendarInfo, ViewType } from '@t/options';\n\nconst chance = new Chance();\nconst EVENT_CATEGORY: EventCategory[] = ['milestone', 'task'];\n\n// eslint-disable-next-line complexity\nfunction createTime(event: EventObject, renderStart: TZDate, renderEnd: TZDate) {\n  const startDate = moment(renderStart.getTime());\n  let endDate = moment(renderEnd.getTime());\n  const diffDate = endDate.diff(startDate, 'days');\n\n  event.isAllday = chance.bool({ likelihood: 30 });\n  if (event.isAllday) {\n    event.category = 'allday';\n  } else if (chance.bool({ likelihood: 30 })) {\n    event.category = EVENT_CATEGORY[chance.integer({ min: 0, max: 1 })];\n    if (event.category === EVENT_CATEGORY[1]) {\n      event.dueDateClass = 'morning';\n    }\n  } else {\n    event.category = 'time';\n  }\n\n  startDate.add(chance.integer({ min: 0, max: diffDate }), 'days');\n  startDate.hours(chance.integer({ min: 0, max: 23 }));\n  startDate.minutes(chance.bool() ? 0 : 30);\n  event.start = startDate.toDate();\n\n  endDate = moment(startDate);\n  if (event.isAllday) {\n    endDate.add(chance.integer({ min: 0, max: 3 }), 'days');\n  }\n\n  event.end = endDate.add(chance.integer({ min: 1, max: 4 }), 'hour').toDate();\n\n  if (!event.isAllday && chance.bool({ likelihood: 20 })) {\n    event.goingDuration = chance.integer({ min: 30, max: 120 });\n    event.comingDuration = chance.integer({ min: 30, max: 120 });\n\n    if (chance.bool({ likelihood: 50 })) {\n      event.end = event.start;\n    }\n  }\n}\n\nfunction createNames() {\n  const names = [];\n  const length = chance.integer({ min: 1, max: 10 });\n\n  for (let i = 0; i < length; i += 1) {\n    names.push(chance.name());\n  }\n\n  return names;\n}\n\nfunction createRandomEvent(calendar: CalendarInfo, renderStart: TZDate, renderEnd: TZDate) {\n  const event: EventObject = {\n    raw: {\n      creator: {},\n    },\n  };\n\n  event.id = chance.guid();\n  event.calendarId = calendar.id;\n\n  event.title = chance.sentence({ words: 3 });\n  event.body = chance.bool({ likelihood: 20 }) ? chance.sentence({ words: 10 }) : '';\n  event.isReadOnly = chance.bool({ likelihood: 20 });\n  createTime(event, renderStart, renderEnd);\n\n  event.isPrivate = chance.bool({ likelihood: 10 });\n  event.location = chance.address();\n  event.attendees = chance.bool({ likelihood: 70 }) ? createNames() : [];\n  event.recurrenceRule = chance.bool({ likelihood: 20 }) ? 'repeated events' : '';\n\n  event.color = calendar.color;\n  event.backgroundColor = calendar.backgroundColor;\n  event.dragBackgroundColor = calendar.dragBackgroundColor;\n  event.borderColor = calendar.borderColor;\n\n  if (event.category === 'milestone') {\n    event.color = event.backgroundColor;\n    event.backgroundColor = 'transparent';\n    event.dragBackgroundColor = 'transparent';\n    event.borderColor = 'transparent';\n  }\n\n  event.raw.memo = chance.sentence();\n  event.raw.creator.name = chance.name();\n  event.raw.creator.avatar = chance.avatar();\n  event.raw.creator.company = chance.company();\n  event.raw.creator.email = chance.email();\n  event.raw.creator.phone = chance.phone();\n\n  if (chance.bool({ likelihood: 20 })) {\n    const travelTime = chance.minute();\n    event.goingDuration = travelTime;\n    event.comingDuration = travelTime;\n  }\n\n  return event;\n}\n\nconst defaultEventCount = { month: 3, week: 10, day: 4 };\n\nexport function createRandomEvents(\n  viewName: ViewType,\n  renderStart: TZDate,\n  renderEnd: TZDate,\n  eventCount?: number\n) {\n  const view = viewName ?? 'week';\n  const events: EventObject[] = [];\n  const count = eventCount ?? defaultEventCount[view];\n\n  calendars.forEach((calendar) => {\n    for (let i = 0; i < count; i += 1) {\n      const event = createRandomEvent(calendar, renderStart, renderEnd);\n      events.push(event);\n    }\n  });\n\n  return events;\n}\n\nexport function createRandomEventModelsForMonth(length = defaultEventCount.month) {\n  const calendar = createDateMatrixOfMonth(new Date(), {});\n  const data = createRandomEvents(\n    'month',\n    calendar[0][0],\n    calendar[calendar.length - 1][6],\n    length\n  );\n\n  return data.map((event: EventObject) => new EventModel(event));\n}\n"
  },
  {
    "path": "apps/calendar/stories/weekView.stories.tsx",
    "content": "import { h } from 'preact';\n\nimport type { Story } from '@storybook/preact';\n\nimport { Week } from '@src/components/view/week';\nimport EventModel from '@src/model/eventModel';\nimport TZDate from '@src/time/date';\nimport { addDate, Day } from '@src/time/datetime';\n\nimport { ProviderWrapper } from '@stories/util/providerWrapper';\nimport { createRandomEventModelsForMonth, createRandomEvents } from '@stories/util/randomEvents';\n\nexport default { title: 'Views/WeekView', component: Week };\n\nfunction createTimeGridEvents() {\n  const today = new TZDate();\n  const start = addDate(new TZDate(), -today.getDay());\n  const end = addDate(start, 6);\n\n  return createRandomEvents('week', start, end).map((event) => new EventModel(event));\n}\n\nconst Template: Story = (args) => (\n  <ProviderWrapper options={args.options} events={args.events}>\n    <Week />\n  </ProviderWrapper>\n);\n\nexport const basic = Template.bind({});\n\nexport const MondayStart = Template.bind({});\nMondayStart.args = {\n  options: {\n    week: {\n      startDayOfWeek: Day.MON,\n    },\n  },\n};\n\nexport const WorkWeek = Template.bind({});\nWorkWeek.args = {\n  options: {\n    week: {\n      workweek: true,\n    },\n  },\n};\n\nexport const RandomEvents = Template.bind({});\nRandomEvents.args = {\n  events: [...createRandomEventModelsForMonth(40), ...createTimeGridEvents()],\n};\n"
  },
  {
    "path": "apps/calendar/stylelint.config.js",
    "content": "module.exports = {\n  extends: 'stylelint-config-recommended',\n};\n"
  },
  {
    "path": "apps/calendar/tsconfig.declaration.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"plugins\": [\n      {\n        \"transform\": \"typescript-transform-paths\",\n        \"afterDeclarations\": true\n      }\n    ]\n  },\n  \"files\": [\"src/index.ts\", \"src/tui-code-snippet.d.ts\"],\n  \"include\": []\n}\n"
  },
  {
    "path": "apps/calendar/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"target\": \"es6\",\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"preact\",\n    \"esModuleInterop\": true,\n    \"isolatedModules\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"resolveJsonModule\": true,\n    \"moduleResolution\": \"node\",\n    \"strict\": true,\n    \"outDir\": \"./types\",\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@src/*\": [\"./src/*\"],\n      \"@t/*\": [\"./src/types/*\"],\n      \"@stories/*\": [\"./stories/*\"]\n    },\n    \"sourceMap\": true,\n    \"lib\": [\"esnext\", \"dom\"],\n    \"types\": [\"webpack-env\", \"node\", \"jest\"],\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src/**/*\", \"stories/**/*\", \"playwright/**/*\"]\n}\n"
  },
  {
    "path": "apps/calendar/tsconfig.test.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"preact\",\n    \"isolatedModules\": false,\n    \"resolveJsonModule\": false,\n    \"noEmit\": true,\n  },\n  \"include\": [\n    \"src/**/*\"\n  ]\n}\n"
  },
  {
    "path": "apps/calendar/tuidoc.config.json",
    "content": "{\n  \"header\": {\n    \"logo\": {\n      \"src\": \"https://uicdn.toast.com/toastui/img/tui-calendar-bi-white.png\"\n    },\n    \"title\": {\n      \"text\": \"repo\",\n      \"linkUrl\": \"https://github.com/nhn/tui.calendar\"\n    }\n  },\n  \"footer\": [\n    {\n      \"title\": \"NHN Cloud\",\n      \"linkUrl\": \"https://github.com/nhn\"\n    },\n    {\n      \"title\": \"FE Development Lab\",\n      \"linkUrl\": \"https://ui.toast.com/\"\n    }\n  ],\n  \"main\": {\n    \"filePath\": \"README.md\"\n  },\n  \"api\": {\n    \"filePath\": [\n      \"tmpdoc/src/factory/calendarCore.js\",\n      \"tmpdoc/src/factory/calendar.js\",\n      \"tmpdoc/src/time/date.js\"\n    ],\n    \"permalink\": false\n  },\n  \"examples\": {\n    \"filePath\": \"examples\",\n    \"titles\": {\n      \"00-calendar-app\": \"Calendar App\",\n      \"01-monthly-view-basic\": \"Monthly View\",\n      \"02-monthly-view-2weeks\": \"Monthly View (2 weeks)\",\n      \"03-monthly-view-3weeks\": \"Monthly View (3 weeks)\",\n      \"04-weekly-view\": \"Weekly View\",\n      \"05-weekly-view-no-event-view\": \"Weekly View (No Events View)\",\n      \"06-daily-view\": \"Daily View\",\n      \"07-narrow-weekends\": \"Narrow Weekends\",\n      \"08-hidden-weekends\": \"Hidden Weekends\",\n      \"09-timezone\": \"Timezone\",\n      \"10-theme-common\": \"Theme (Common)\",\n      \"11-theme-monthly\": \"Theme (Monthly)\",\n      \"12-theme-weekly\": \"Theme (Weekly)\",\n      \"13-template-monthly\": \"Template (Monthly)\",\n      \"14-template-weekly\": \"Template (Weekly)\",\n      \"15-template-popup\": \"Template (Popup)\"\n    }\n  },\n  \"pathPrefix\": \"tui.calendar\"\n}\n"
  },
  {
    "path": "apps/calendar/vite.config.ts",
    "content": "import * as path from 'path';\nimport { defineConfig } from 'vite';\n\n// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst pkg = require('./package.json');\n\nconst banner = [\n  '/*!',\n  ' * TOAST UI Calendar 2nd Edition',\n  ` * @version ${pkg.version} | ${new Date().toDateString()}`,\n  ` * @author ${pkg.author}`,\n  ` * @license ${pkg.license}`,\n  ' */',\n].join('\\n');\n\nexport default defineConfig(() => {\n  return {\n    build: {\n      emptyOutDir: false,\n      lib: {\n        entry: path.resolve(__dirname, 'src/index.ts'),\n        formats: ['es'],\n        fileName: () => 'toastui-calendar.mjs',\n      },\n      target: 'es2015',\n      rollupOptions: {\n        external: ['tui-date-picker', 'tui-time-picker'],\n        output: {\n          banner,\n        },\n      },\n    },\n    esbuild: {\n      jsxFactory: 'h',\n      jsxFragment: 'Fragment',\n    },\n    resolve: {\n      alias: {\n        '@src': path.resolve(__dirname, './src'),\n        '@t': path.resolve(__dirname, './src/types'),\n      },\n    },\n  };\n});\n"
  },
  {
    "path": "apps/calendar/webpack.config.js",
    "content": "/* eslint @typescript-eslint/no-var-requires: \"off\" */\nconst pkg = require('./package.json');\nconst path = require('path');\nconst webpack = require('webpack');\nconst TerserPlugin = require('terser-webpack-plugin');\nconst CssMinimizerPlugin = require('css-minimizer-webpack-plugin');\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin');\nconst InjectPlugin = require('webpack-inject-plugin')['default'];\n\nconst banner = [\n  'TOAST UI Calendar 2nd Edition',\n  `@version ${pkg.version} | ${new Date().toDateString()}`,\n  `@author ${pkg.author}`,\n  `@license ${pkg.license}`,\n].join('\\n');\n\nfunction getBabelConfig(isIE11) {\n  return {\n    presets: [\n      [\n        '@babel/preset-env',\n        {\n          useBuiltIns: 'usage',\n          corejs: '3.22',\n          targets: `defaults${isIE11 ? ', ie 11' : ''}`,\n        },\n      ],\n      ['@babel/preset-typescript', { jsxPragma: 'h' }],\n    ],\n    plugins: [['@babel/plugin-transform-react-jsx', { pragma: 'h', pragmaFrag: 'Fragment' }]],\n  };\n}\n\nmodule.exports = ({ minify, ie11 }) => {\n  const shouldMinify = !!minify;\n  const isIE11 = !!ie11;\n\n  const filenameBase = `toastui-calendar${isIE11 ? '.ie11' : ''}${shouldMinify ? '.min' : ''}`;\n\n  const plugins = [\n    new webpack.BannerPlugin({\n      banner,\n      entryOnly: true,\n    }),\n    new MiniCssExtractPlugin({ filename: `${filenameBase}.css` }),\n  ];\n\n  if (ie11) {\n    plugins.unshift(new InjectPlugin(() => `import { enableES5 } from 'immer';enableES5();`));\n  }\n\n  return {\n    mode: 'production',\n    output: {\n      library: {\n        name: ['tui', 'Calendar'],\n        type: 'umd',\n        export: 'default',\n      },\n      path: path.join(__dirname, 'dist'),\n      filename: `${filenameBase}.js`,\n      publicPath: '/dist',\n      globalObject: 'this',\n    },\n    externals: {\n      'tui-date-picker': {\n        commonjs: 'tui-date-picker',\n        commonjs2: 'tui-date-picker',\n        import: 'tui-date-picker',\n        amd: 'tui-date-picker',\n        root: ['tui', 'DatePicker'],\n      },\n      'tui-time-picker': {\n        commonjs: 'tui-time-picker',\n        commonjs2: 'tui-time-picker',\n        import: 'tui-time-picker',\n        amd: 'tui-time-picker',\n        root: ['tui', 'TimePicker'],\n      },\n    },\n    entry: isIE11 ? ['./src/index.ts'] : ['./src/css/index.css', './src/index.ts'],\n    module: {\n      rules: [\n        {\n          test: /\\.(ts|tsx|js)$/,\n          exclude: /node_modules/,\n          loader: 'babel-loader',\n          options: getBabelConfig(isIE11),\n        },\n        {\n          test: /\\.css$/,\n          use: [\n            MiniCssExtractPlugin.loader,\n            {\n              loader: 'css-loader',\n              options: {\n                importLoaders: 1,\n              },\n            },\n            'postcss-loader',\n          ],\n        },\n        {\n          test: /\\.(gif|png|jpe?g)$/,\n          type: 'asset/inline',\n        },\n      ],\n    },\n    resolve: {\n      extensions: ['.ts', '.tsx', '.js'],\n      alias: {\n        '@src': path.resolve(__dirname, './src/'),\n        '@t': path.resolve(__dirname, './src/types/'),\n      },\n    },\n    plugins,\n    optimization: shouldMinify\n      ? {\n          minimize: true,\n          minimizer: [new TerserPlugin({ extractComments: false }), new CssMinimizerPlugin()],\n        }\n      : {\n          minimize: false,\n        },\n  };\n};\n"
  },
  {
    "path": "apps/react-calendar/.browserslistrc",
    "content": "> 1%\nlast 2 versions\nnot ie <= 10\n"
  },
  {
    "path": "apps/react-calendar/.eslintrc.js",
    "content": "module.exports = {\n  rules: {\n    'react/prop-types': 0,\n    'react/react-in-jsx-scope': 'off',\n  },\n  settings: {\n    react: {\n      version: 'detect',\n    },\n  },\n};\n"
  },
  {
    "path": "apps/react-calendar/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\ntypes\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "apps/react-calendar/README.md",
    "content": "# TOAST UI Calendar for React\n\n> This is a React component wrapping [TOAST UI Calendar](/apps/calendar).\n\n[![React](https://img.shields.io/badge/react-61dafb.svg)](https://reactjs.org/)\n[![npm version](https://img.shields.io/npm/v/@toast-ui/react-calendar.svg)](https://www.npmjs.com/package/@toast-ui/react-calendar)\n[![license](https://img.shields.io/github/license/nhn/tui.calendar.svg)](https://github.com/nhn/tui.calendar/blob/master/LICENSE)\n[![PRs welcome](https://img.shields.io/badge/PRs-welcome-ff69b4.svg)](https://github.com/nhn/tui.calendar/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)\n[![code with hearth by NHN Cloud](https://img.shields.io/badge/%3C%2F%3E%20with%20%E2%99%A5%20by-NHN_Cloud-ff1414.svg)](https://github.com/nhn)\n\n## 🚩 Table of Contents\n\n- [📙 Documents](#-documents)\n- [Collect statistics on the use of open source](#collect-statistics-on-the-use-of-open-source)\n- [💾 Installation](#-installation)\n  - [npm](#npm)\n- [📅 Usage](#-usage)\n  - [Load](#load)\n- [🔧 Pull Request Steps](#-pull-request-steps)\n  - [Setup](#setup)\n  - [Develop](#develop)\n  - [Pull Request](#pull-request)\n- [💬 Contributing](#-contributing)\n- [📜 License](#-license)\n\n## 📙 Documents\n\n- [English](./docs/README.md)\n- [Korean](./docs/ko/README.md)\n\n## Collect statistics on the use of open source\n\nTOAST UI Calendar for React applies Google Analytics (GA) to collect statistics on the use of open source, in order to identify how widely TOAST UI Calendar is used throughout the world. It also serves as important index to determine the future course of projects. `location.hostname` (e.g. > “ui.toast.com\") is to be collected and the sole purpose is nothing but to measure statistics on the usage.\n\nTo disable GA, set the [`usageStatistics` option](/docs/en/apis/options.md#usagestatistics) to `false`:\n\n```jsx\nexport function MyCalendar() {\n  return (\n    <div>\n      <Calendar usageStatistics={false} />\n    </div>\n  );\n}\n```\n\n## 💾 Installation\n\n### npm\n\n```sh\nnpm install --save @toast-ui/react-calendar\n```\n\n## 📅 Usage\n\n### Load\n\nYou can use Toast UI Calendar for React as moudule format or namespace.\n\n```js\n/* ES6 module in Node.js environment */\nimport Calendar from '@toast-ui/react-calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n```\n\n```js\n/* CommonJS in Node.js environment */\nconst Calendar = require('@toast-ui/react-calendar');\nrequire('@toast-ui/calendar/dist/toastui-calendar.min.css');\n```\n\n```js\n// browser environment namespace\nconst Calendar = tui.ReactCalendar;\n```\n\n## 🔧 Pull Request Steps\n\nTOAST UI products are open source, so you can create a pull request(PR) after you fix issues.\nRun npm scripts and develop yourself with the following process.\n\n### Setup\n\nFork `main` branch into your personal repository.\nClone it to local computer. Install node modules.\nBefore starting development, you should check to have any errors.\n\n```sh\ngit clone https://github.com/{your-personal-repo}/[[repo name]].git\ncd [[repo name]]\nnpm install\n```\n\n### Develop\n\nLet's start development!\n\n### Pull Request\n\nBefore PR, check to test lastly and then check any errors.\nIf it has no error, commit and then push it!\n\nFor more information on PR's step, please see links of Contributing section.\n\n## 💬 Contributing\n\n- [Code of Conduct](/CODE_OF_CONDUCT.md)\n- [Contributing Guidelines](/CONTRIBUTING.md)\n- [Commit Message Convention](/docs/COMMIT_MESSAGE_CONVENTION.md)\n\n## 📜 License\n\nThis software is licensed under the [MIT](/LICENSE) © [NHN Cloud](https://github.com/nhn).\n"
  },
  {
    "path": "apps/react-calendar/docs/README.md",
    "content": "# 🗂️ Document Index\n\n[한글 문서 인덱스(Korean)](./ko/README.md)\n\n## Guides\n\n- [Getting started](./en/guide/getting-started.md)\n- [v2 Migration guide](./en/guide/migration-guide-v2.md)\n\n## Etc\n\n- [Code of Conduct](/CODE_OF_CONDUCT.md)\n- [Contributing Guidelines](/CONTRIBUTING.md)\n- [Commit Message Convention](/docs/COMMIT_MESSAGE_CONVENTION.md)\n"
  },
  {
    "path": "apps/react-calendar/docs/en/guide/getting-started.md",
    "content": "# Getting Started\n\n## Table of Contents\n\n- [Installation](#installation)\n  - [Using the package manager](#using-the-package-manager)\n    - [npm](#npm)\n- [How to use the calendar](#how-to-use-the-calendar)\n  - [JavaScript](#javascript)\n    - [Importing modules](#importing-modules)\n    - [Loading bundle files for legacy browsers](#loading-bundle-files-for-legacy-browsers)\n  - [CSS](#css)\n- [React](#react)\n  - [Props](#props)\n    - [Theme](#theme)\n  - [Instance Methods](#instance-methods)\n  - [Methods](#methods)\n    - [getRootElement](#getrootelement)\n    - [getInstance](#getinstance)\n- [Basic usage](#basic-usage)\n  - [Disable to collect hostname for Google Analytics(GA)](#disable-to-collect-hostname-for-google-analyticsga)\n  - [⚠️ Note for passing props](#️-note-for-passing-props)\n\n## Installation\n\nTOAST UI products can be used by using the package manager or by directly downloading the source code. However, it is recommended to use a package manager.\n\n### Using the package manager\n\nTOAST UI products are registered in the [npm](https://www.npmjs.com/) package registry. You can easily install packages using CLI tools provided by each package manager. To use npm, you need to install [Node.js](https://nodejs.org) in advance.\n\n#### npm\n\n```sh\nnpm install @toast-ui/react-calendar # latest version\nnpm install @toast-ui/react-calendar@<version> # specific version\n```\n\n## How to use the calendar\n\n### JavaScript\n\n#### Importing modules\n\nThere are three ways to import TOAST UI Calendar for React depending on the environment.\n\n```js\n/* ES6 module in Node.js environment */\nimport Calendar from '@toast-ui/react-calendar';\n```\n\n```js\n/* CommonJS in Node.js environment */\nconst Calendar = require('@toast-ui/react-calendar');\n```\n\n```js\n/* in the browser environment namespace */\nconst Calendar = tui.ReactCalendar;\n```\n\n#### Loading bundle files for legacy browsers\n\nTOAST UI Calendar for React provides a separate bundle file for legacy browsers. The basic bundle provides stable support for the latest two versions of the modern browser. However, the basic bundle does not include a polyfill for IE11, so to support IE11 or similar level of legacy browsers, you need to add the IE11 bundle that includes a polyfill as follows.\n\nSince the bundle size of IE11 is about 2x larger than that of the default bundle, you must take care not to increase the bundle size unnecessarily by considering the range of support.\n\n```js\n/* ES6 module in Node.js environment */\nimport Calendar from '@toast-ui/react-calendar/ie11';\n```\n\n```js\n/* CommonJS in Node.js environment */\nconst Calendar = require('@toast-ui/react-calendar/ie11');\n```\n\n### CSS\n\nTo use the calendar, you need to add a CSS file of TOAST UI Calendar. You can import CSS files through import and require, or you can import them through CDN.\n\n```js\n/* ES6 module in Node.js environment */\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css'; // Stylesheet for calendar\n```\n\n```js\n/* CommonJS in Node.js environment */\nrequire('@toast-ui/calendar/dist/toastui-calendar.min.css');\n```\n\n```html\n<!-- CDN -->\n<link rel=\"stylesheet\" href=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css\" />\n```\n\n## React\n\nYou can load a calendar component and add it to your component.\n\n```jsx\nimport Calendar from '@toast-ui/react-calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n\nexport function YourComponent() {\n  return (\n    <div>\n      <Calendar />\n    </div>\n  );\n}\n```\n\n### Props\n\nTOAST UI Calendar for React provide props for [options of TOAST UI Calendar](/docs/en/apis/options.md). Each name of props is same with options of Toast UI Calendar except `view` is for `defaultView`.\n\nAdditionally, it provides a `events` prop to add events.\n\n```jsx\nexport function MyComponent() {\n  const calendars = [{ id: 'cal1', name: 'Personal' }];\n  const initialEvents = [\n    {\n      id: '1',\n      calendarId: 'cal1',\n      title: 'Lunch',\n      category: 'time',\n      start: '2022-06-28T12:00:00',\n      end: '2022-06-28T13:30:00',\n    },\n    {\n      id: '2',\n      calendarId: 'cal1',\n      title: 'Coffee Break',\n      category: 'time',\n      start: '2022-06-28T15:00:00',\n      end: '2022-06-28T15:30:00',\n    },\n  ];\n\n  const onAfterRenderEvent = (event) => {\n    console.log(event.title);\n  };\n\n  return (\n    <div>\n      <Calendar\n        height=\"900px\"\n        view=\"month\"\n        month={{\n          dayNames: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],\n          visibleWeeksCount: 3,\n        }}\n        calendars={calendars}\n        events={initialEvents}\n        onAfterRenderEvent={onAfterRenderEvent}\n      />\n    </div>\n  );\n}\n```\n\n#### Theme\n\nYou can write your own theme object. For more information, refer to [`theme`](/docs/en/apis/theme.md).\n\n### Instance Methods\n\nFor using [instance methods of TOAST UI Calendar](/docs/en/apis/calendar.md#instance-methods), first thing to do is creating Refs of wrapper component using [`createRef()`](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs). But the wrapper component does not provide a way to call instance methods of TOAST UI Calendar directly. Instead, you can call `getInstance()` method of the wrapper component to get the instance, and call the methods on it.\n\n```js\nconst calendarOptions = {\n  // sort of option properties.\n};\n\nclass MyComponent extends React.Component {\n  calendarRef = React.createRef();\n\n  handleClickNextButton = () => {\n    const calendarInstance = this.calendarRef.current.getInstance();\n\n    calendarInstance.next();\n  };\n\n  render() {\n    return (\n      <>\n        <Calendar ref={this.calendarRef} {...calendarOptions} />\n        <button onClick={this.handleClickNextButton}>Go next!</button>\n      </>\n    );\n  }\n}\n```\n\n### Methods\n\n💡 Click on a method to see more detailed explanations and usage examples.\n\n| Method                            | Description                                          |\n| --------------------------------- | ---------------------------------------------------- |\n| [getRootElement](#getrootelement) | Return the element on which the calendar is mounted. |\n| [getInstance](#getinstance)       | Return the calendar instance.                        |\n\n#### getRootElement\n\n- Type: `getRootElement(): HTMLDivElement`\n- Returns: `HTMLDivElement` - the element on which the calendar is mounted\n\nReturn the element on which the calendar is mounted.\n\n#### getInstance\n\n- Type: `getInstance(): Calendar`\n- Returns: `Calendar` - the calendar instance\n\nReturn the calendar instance. You can use this to call the [calendar instance methods](/docs/en/apis/calendar.md#instance-methods).\n\n## Basic usage\n\n### Disable to collect hostname for Google Analytics(GA)\n\n[TOAST UI Calendar](https://github.com/nhn/tui.calendar) applies [GA](https://analytics.google.com/analytics/web/) to collect statistics on open source usage to see how widespread it is around the world. This serves as an important indicator to determine the future progress of the project. It collects `location.hostname` (e.g. \"ui.toast.com\") and is only used to measure usage statistics.\n\nTo disable GA, set the [`usageStatistics` prop](/docs/en/apis/options.md#usagestatistics) to `false`:\n\n```jsx\nexport function MyCalendar() {\n  return (\n    <div>\n      <Calendar usageStatistics={false} />\n    </div>\n  );\n}\n```\n\n### ⚠️ Note for passing props\n\nThe calendar component check deep equality of `props` when re-rendered. However, for performance and to avoid unnecessary re-rendering, it's recommended to extract props to the outside of the component or memoize them with `useMemo` when props don't have to be affected by component state changes.\n\n```jsx\nconst calendars = [\n  {\n    id: '0',\n    name: 'Private',\n    backgroundColor: '#9e5fff',\n    borderColor: '#9e5fff',\n  },\n  {\n    id: '1',\n    name: 'Company',\n    backgroundColor: '#00a9ff',\n    borderColor: '#00a9ff',\n  },\n];\n\n// Especially avoid to declare the `template` prop inside the component.\nconst template = {\n  milestone(event) {\n    return `<span style=\"color:#fff;background-color: ${event.backgroundColor};\">${event.title}</span>`;\n  },\n  milestoneTitle() {\n    return 'Milestone';\n  },\n  allday(event) {\n    return `${event.title}<i class=\"fa fa-refresh\"></i>`;\n  },\n  alldayTitle() {\n    return 'All Day';\n  },\n};\n\nfunction MyCalendar() {\n  // ...\n\n  return (\n    <Calendar\n      // ...\n      calendars={calendars}\n      template={template}\n    />\n  );\n}\n```\n"
  },
  {
    "path": "apps/react-calendar/docs/en/guide/migration-guide-v2.md",
    "content": "# v2 Migration Guide\n\n## Table of Contents\n\n- [Overview](#overview)\n- [Changed](#changed)\n  - [Change term from `schedule` to `event`](#change-term-from-schedule-to-event)\n\n## Overview\n\nChanges made in TOAST UI Calendar v2.0 are reflected in React Wrapper. For more information about changes in TOAST UI Calendar v2.0, refer to the [TOAST UI Caledar v2 Migration Guide](/docs/en/guide/migration-guide-v2.md).\n\nThis guide summarizes the changes only in React Wrapper.\n\n## Changed\n\n### Change term from `schedule` to `event`\n\nIn v2, the name was changed from `schedule` to `event` to match the meaning of events in the calendar. So, the `schedules` prop has been renamed to `events`.\n"
  },
  {
    "path": "apps/react-calendar/docs/ko/README.md",
    "content": "# 🗂️ 문서 인덱스\n\n## 가이드\n\n- [시작하기](./guide/getting-started.md)\n- [v2 마이그레이션 가이드](./guide/migration-guide-v2.md)\n"
  },
  {
    "path": "apps/react-calendar/docs/ko/guide/getting-started.md",
    "content": "# 시작하기\n\n## 목차\n\n- [설치하기](#설치하기)\n  - [패키지 매니저 사용하기](#패키지-매니저-사용하기)\n    - [npm](#npm)\n- [사용하기](#사용하기)\n  - [자바스크립트](#자바스크립트)\n    - [불러오기](#불러오기)\n    - [레거시 브라우저용 번들 파일 불러오기](#레거시-브라우저용-번들-파일-불러오기)\n  - [CSS](#css)\n- [React에서 사용하기](#react에서-사용하기)\n  - [Props](#props)\n    - [테마](#테마)\n  - [인스턴스 메서드](#인스턴스-메서드)\n  - [메서드](#메서드)\n    - [getRootElement](#getrootelement)\n    - [getInstance](#getinstance)\n- [기본적인 사용 방법](#기본적인-사용-방법)\n  - [Google Analytics(GA)를 위한 hostname 수집 거부하기](#google-analyticsga를-위한-hostname-수집-거부하기)\n  - [⚠️ Props를 넘길 때 주의할 점](#️-props를-넘길-때-주의할-점)\n\n## 설치하기\n\nTOAST UI 제품들은 패키지 매니저를 이용하거나, 직접 소스 코드를 다운받아 사용할 수 있다. 하지만 패키지 매니저 사용을 권장한다.\n\n### 패키지 매니저 사용하기\n\nTOAST UI 제품들은 [npm](https://www.npmjs.com/) 패키지 매니저에 등록되어 있다.\n각 패키지 매니저가 제공하는 CLI 도구를 사용하면 쉽게 패키지를 설치할 수 있다. npm 사용을 위해선 [Node.js](https://nodejs.org)를 미리 설치해야 한다.\n\n#### npm\n\n```sh\nnpm install @toast-ui/react-calendar # 최신 버전\nnpm install @toast-ui/react-calendar@<version> # 특정 버전\n```\n\n## 사용하기\n\n### 자바스크립트\n\n#### 불러오기\n\nTOAST UI 캘린더 React Wrapper는 아래 세 가지 방법으로 불러올 수 있다.\n\n```js\n/* Node.js 환경에서 ES6 모듈 */\nimport Calendar from '@toast-ui/react-calendar';\n```\n\n```js\n/* Node.js 환경에서 CommonJS */\nconst Calendar = require('@toast-ui/react-calendar');\n```\n\n```js\n/* 브라우저 환경에서 namespace */\nconst Calendar = tui.ReactCalendar;\n```\n\n#### 레거시 브라우저용 번들 파일 불러오기\n\nTOAST UI 캘린더 React Wrapper는 레거시 브라우저용 번들 파일을 따로 제공하고 있다. 기본 번들은 모던 브라우저의 최신 2개 버전을 안정적으로 지원한다. 하지만 기본 번들은 IE11을 위한 폴리필이 포함되어있지 않으므로 IE11 혹은 일정 수준 이하의 레거시 브라우저를 지원하기 위해서는 다음과 같이 폴리필이 포함된 IE11 번들을 추가해야 한다.\n\nIE11의 번들 크기는 기본 번들보다 2배 가량 크기 때문에 반드시 지원 범위를 잘 고려하여 불필요하게 번들 사이즈를 늘리지 않도록 유의해야 한다.\n\n```js\n/* Node.js 환경에서 ES6 모듈 */\nimport Calendar from '@toast-ui/react-calendar/ie11';\n```\n\n```js\n/* Node.js 환경에서 CommonJS */\nconst Calendar = require('@toast-ui/react-calendar/ie11');\n```\n\n### CSS\n\nCalendar를 사용하기 위해서는 TOAST UI 캘린더의 CSS 파일을 추가해야 한다. import, require를 통해 CSS 파일을 불러오거나, CDN을 통해 불러올 수 있다.\n\n```js\n/* Node.js 환경에서 ES6 모듈 */\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css'; // Calendar 스타일\n```\n\n```js\n/* Node.js 환경에서 CommonJS */\nrequire('@toast-ui/calendar/dist/toastui-calendar.min.css');\n```\n\n```html\n<!-- CDN -->\n<link rel=\"stylesheet\" href=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css\" />\n```\n\n## React에서 사용하기\n\n컴포넌트에서 TOAST UI 캘린더 React Wrapper를 불러와서 사용할 수 있다.\n\n```jsx\nimport Calendar from '@toast-ui/react-calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n\nexport function YourComponent() {\n  return (\n    <div>\n      <Calendar />\n    </div>\n  );\n}\n```\n\n### Props\n\nTOAST UI 캘린더의 [옵션](/docs/ko/apis/options.md)은 React 컴포넌트의 Props으로 구현되어 있다. `defaultView`는 `view`라는 이름이고, 그 외는 동일한 이름이다.\n\n옵션 외에도 `events` prop을 이용해 일정 데이터를 바로 추가할 수도 있다.\n\n```jsx\nexport function MyComponent() {\n  const calendars = [{ id: 'cal1', name: 'Personal' }];\n  const initialEvents = [\n    {\n      id: '1',\n      calendarId: 'cal1',\n      title: 'Lunch',\n      category: 'time',\n      start: '2022-06-28T12:00:00',\n      end: '2022-06-28T13:30:00',\n    },\n    {\n      id: '2',\n      calendarId: 'cal1',\n      title: 'Coffee Break',\n      category: 'time',\n      start: '2022-06-28T15:00:00',\n      end: '2022-06-28T15:30:00',\n    },\n  ];\n\n  const onAfterRenderEvent = (event) => {\n    console.log(event.title);\n  };\n\n  return (\n    <div>\n      <Calendar\n        height=\"900px\"\n        view=\"month\"\n        month={{\n          dayNames: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],\n          visibleWeeksCount: 3,\n        }}\n        calendars={calendars}\n        events={initialEvents}\n        onAfterRenderEvent={onAfterRenderEvent}\n      />\n    </div>\n  );\n}\n```\n\n#### 테마\n\ntheme 객체를 사용해서 자신만의 테마를 적용할 수 있다. 더 자세한 정보는 [`theme`](/docs/ko/apis/theme.md) 문서를 참고한다.\n\n### 인스턴스 메서드\n\n[TOAST UI Calendar의 인스턴스 메서드](/docs/ko/apis/calendar.md#instance-methods)를 사용하기 위해선, 먼저 [`createRef()`](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs)를 이용해서 wrapper 컴포넌트에 대한 ref를 만들어야한다.\n하지만 wrapper 컴포넌트에서 인스턴스 메서드를 직접 호출할 수 없다. 대신 `getInstance()` 메서드를 호출해서 인스턴스를 얻은 후에 인스턴스 메서드를 호출할 수 있다.\n\n### 메서드\n\n💡 메서드를 클릭하면 더 자세한 설명과 사용 예시를 볼 수 있다.\n\n| 메서드                            | 설명                                        |\n| --------------------------------- | ------------------------------------------- |\n| [getRootElement](#getrootelement) | TOAST UI 캘린더가 마운트된 요소를 리턴한다. |\n| [getInstance](#getinstance)       | TOAST UI 캘린더 인스턴스를 리턴한다.        |\n\n#### getRootElement\n\n- 타입: `getRootElement(): HTMLDivElement`\n- 리턴: `HTMLDivElement` - TOAST UI 캘린더가 마운트된 요소\n\nTOAST UI 캘린더가 마운트된 요소를 리턴한다.\n\n#### getInstance\n\n- 타입: `getInstance(): Calendar`\n- 리턴: `Calendar` - TOAST UI 캘린더 인스턴스\n\nTOAST UI 캘린더 인스턴스를 리턴한다. 이를 이용하여 [캘린더 인스턴스 메서드](/docs/ko/apis/calendar.md#인스턴스-메서드)를 사용할 수 있다.\n\n## 기본적인 사용 방법\n\n### Google Analytics(GA)를 위한 hostname 수집 거부하기\n\n[TOAST UI 캘린더](https://github.com/nhn/tui.calendar)는 [GA](https://analytics.google.com/analytics/web/)를 적용하여 오픈 소스 사용에 대한 통계를 수집하여 전 세계에서 얼마나 널리 사용되는지 확인한다.\n이는 프로젝트의 향후 진행을 결정하는 중요한 지표 역할을 한다.\n`location.hostname`(예를 들어 \"ui.toast.com\")을 수집하며 사용량에 대한 통계를 측정하기 위해서만 사용된다.\n\n만약 이를 거부하려면 [`usageStatistics` prop](/docs/ko/apis/options.md#usagestatistics)을 `false`로 설정한다.\n\n```jsx\nexport function MyCalendar() {\n  return (\n    <div>\n      <Calendar usageStatistics={false} />\n    </div>\n  );\n}\n```\n\n### ⚠️ Props를 넘길 때 주의할 점\n\n캘린더 React Wrapper 컴포넌트는 다시 렌더링할 때 `props`를 깊게 비교한다. 불필요한 재렌더링을 피하고 더 나은 성능을 위해서 props를 컴포넌트 밖에서 선언하거나, props가 컴포넌트 상태 변경에 영향을 받을 필요가 없는 경우 `useMemo`를 사용하는 것을 추천한다.\n\n```jsx\nconst calendars = [\n  {\n    id: '0',\n    name: 'Private',\n    backgroundColor: '#9e5fff',\n    borderColor: '#9e5fff',\n  },\n  {\n    id: '1',\n    name: 'Company',\n    backgroundColor: '#00a9ff',\n    borderColor: '#00a9ff',\n  },\n];\n\n// 특히 컴포넌트 내에서 `template` prop을 선언하지 않는다.\nconst template = {\n  milestone(event) {\n    return `<span style=\"color: #fff; background-color: ${event.backgroundColor};\">${event.title}</span>`;\n  },\n  allday(event) {\n    return `[All day] ${event.title}`;\n  },\n};\n\nfunction MyCalendar() {\n  // ...\n\n  return (\n    <Calendar\n      // ...\n      calendars={calendars}\n      template={template}\n    />\n  );\n}\n```\n"
  },
  {
    "path": "apps/react-calendar/docs/ko/guide/migration-guide-v2.md",
    "content": "# v2 마이그레이션 가이드\n\n## 목차\n\n- [개요](#개요)\n- [변경](#변경)\n  - [`schedule`에서 `event`로 용어 변경](#schedule에서-event로-용어-변경)\n\n## 개요\n\nTOAST UI Calendar v2.0에서 변경된 점이 React Wrapper에 그대로 반영되었다. TOAST UI Calendar의 변경점은 [TOAST UI Caledar v2 마이그레이션 가이드](/docs/ko/guide/migration-guide-v2.md)를 참고한다.\n\n해당 문서에서는 React Wrapper에서만 변경된 점을 정리한다.\n\n## 변경\n\n### `schedule`에서 `event`로 용어 변경\n\nv2에서는 일정이라는 의미에 맞게 기존 `schedule`에서 `event`로 네이밍이 변경되었다. 이에 일정\n데이터를 넘기는 `schedules`라는 Prop이 `events`로 바뀌었다.\n"
  },
  {
    "path": "apps/react-calendar/example/app.css",
    "content": "body {\n  font-family: 'Noto Sans', sans-serif;\n  color: #333;\n  font-size: 12px;\n}\n\n/**  custom bootstrap - start */\n.btn {\n  border-radius: 25px;\n  border-color: #ddd;\n}\n\n.btn:hover {\n  border: solid 1px #bbb;\n  background-color: #fff;\n}\n\n.btn:active {\n  background-color: #f9f9f9;\n  border: solid 1px #bbb;\n  outline: none;\n}\n\n.btn:disabled {\n  background-color: #f9f9f9;\n  border: solid 1px #ddd;\n  color: #bbb;\n}\n\n.btn:focus:active,\n.btn:focus,\n.btn:active {\n  outline: none;\n}\n\n.move-today {\n  padding: 0 16px;\n  line-height: 30px;\n}\n\n.move-day {\n  padding: 8px;\n}\n\n.render-range {\n  padding-left: 12px;\n  font-size: 19px;\n  vertical-align: middle;\n}\n\n/** custom fontawesome */\n.fa {\n  width: 12px;\n  height: 12px;\n  margin-right: 2px;\n}\n"
  },
  {
    "path": "apps/react-calendar/example/app.tsx",
    "content": "/* eslint-disable no-console */\nimport './app.css';\n\nimport type { EventObject, ExternalEventTypes, Options } from '@toast-ui/calendar';\nimport { TZDate } from '@toast-ui/calendar';\nimport type { ChangeEvent, MouseEvent } from 'react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport Calendar from '../src';\nimport { theme } from './theme';\nimport { addDate, addHours, subtractDate } from './utils';\n\ntype ViewType = 'month' | 'week' | 'day';\n\nconst today = new TZDate();\nconst viewModeOptions = [\n  {\n    title: 'Monthly',\n    value: 'month',\n  },\n  {\n    title: 'Weekly',\n    value: 'week',\n  },\n  {\n    title: 'Daily',\n    value: 'day',\n  },\n];\n\nexport function App({ view }: { view: ViewType }) {\n  const calendarRef = useRef<typeof Calendar>(null);\n  const [selectedDateRangeText, setSelectedDateRangeText] = useState('');\n  const [selectedView, setSelectedView] = useState(view);\n  const initialCalendars: Options['calendars'] = [\n    {\n      id: '0',\n      name: 'Private',\n      backgroundColor: '#9e5fff',\n      borderColor: '#9e5fff',\n      dragBackgroundColor: '#9e5fff',\n    },\n    {\n      id: '1',\n      name: 'Company',\n      backgroundColor: '#00a9ff',\n      borderColor: '#00a9ff',\n      dragBackgroundColor: '#00a9ff',\n    },\n  ];\n  const initialEvents: Partial<EventObject>[] = [\n    {\n      id: '1',\n      calendarId: '0',\n      title: 'TOAST UI Calendar Study',\n      category: 'time',\n      start: today,\n      end: addHours(today, 3),\n    },\n    {\n      id: '2',\n      calendarId: '0',\n      title: 'Practice',\n      category: 'milestone',\n      start: addDate(today, 1),\n      end: addDate(today, 1),\n      isReadOnly: true,\n    },\n    {\n      id: '3',\n      calendarId: '0',\n      title: 'FE Workshop',\n      category: 'allday',\n      start: subtractDate(today, 2),\n      end: subtractDate(today, 1),\n      isReadOnly: true,\n    },\n    {\n      id: '4',\n      calendarId: '0',\n      title: 'Report',\n      category: 'time',\n      start: today,\n      end: addHours(today, 1),\n    },\n  ];\n\n  // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n  // @ts-ignore\n  const getCalInstance = useCallback(() => calendarRef.current?.getInstance?.(), []);\n\n  const updateRenderRangeText = useCallback(() => {\n    const calInstance = getCalInstance();\n    if (!calInstance) {\n      setSelectedDateRangeText('');\n    }\n\n    const viewName = calInstance.getViewName();\n    const calDate = calInstance.getDate();\n    const rangeStart = calInstance.getDateRangeStart();\n    const rangeEnd = calInstance.getDateRangeEnd();\n\n    let year = calDate.getFullYear();\n    let month = calDate.getMonth() + 1;\n    let date = calDate.getDate();\n    let dateRangeText: string;\n\n    switch (viewName) {\n      case 'month': {\n        dateRangeText = `${year}-${month}`;\n        break;\n      }\n      case 'week': {\n        year = rangeStart.getFullYear();\n        month = rangeStart.getMonth() + 1;\n        date = rangeStart.getDate();\n        const endMonth = rangeEnd.getMonth() + 1;\n        const endDate = rangeEnd.getDate();\n\n        const start = `${year}-${month < 10 ? '0' : ''}${month}-${date < 10 ? '0' : ''}${date}`;\n        const end = `${year}-${endMonth < 10 ? '0' : ''}${endMonth}-${\n          endDate < 10 ? '0' : ''\n        }${endDate}`;\n        dateRangeText = `${start} ~ ${end}`;\n        break;\n      }\n      default:\n        dateRangeText = `${year}-${month}-${date}`;\n    }\n\n    setSelectedDateRangeText(dateRangeText);\n  }, [getCalInstance]);\n\n  useEffect(() => {\n    setSelectedView(view);\n  }, [view]);\n\n  useEffect(() => {\n    updateRenderRangeText();\n  }, [selectedView, updateRenderRangeText]);\n\n  const onAfterRenderEvent: ExternalEventTypes['afterRenderEvent'] = (res) => {\n    console.group('onAfterRenderEvent');\n    console.log('Event Info : ', res.title);\n    console.groupEnd();\n  };\n\n  const onBeforeDeleteEvent: ExternalEventTypes['beforeDeleteEvent'] = (res) => {\n    console.group('onBeforeDeleteEvent');\n    console.log('Event Info : ', res.title);\n    console.groupEnd();\n\n    const { id, calendarId } = res;\n\n    getCalInstance().deleteEvent(id, calendarId);\n  };\n\n  const onChangeSelect = (ev: ChangeEvent<HTMLSelectElement>) => {\n    setSelectedView(ev.target.value as ViewType);\n  };\n\n  const onClickDayName: ExternalEventTypes['clickDayName'] = (res) => {\n    console.group('onClickDayName');\n    console.log('Date : ', res.date);\n    console.groupEnd();\n  };\n\n  const onClickNavi = (ev: MouseEvent<HTMLButtonElement>) => {\n    if ((ev.target as HTMLButtonElement).tagName === 'BUTTON') {\n      const button = ev.target as HTMLButtonElement;\n      const actionName = (button.getAttribute('data-action') ?? 'month').replace('move-', '');\n      getCalInstance()[actionName]();\n      updateRenderRangeText();\n    }\n  };\n\n  const onClickEvent: ExternalEventTypes['clickEvent'] = (res) => {\n    console.group('onClickEvent');\n    console.log('MouseEvent : ', res.nativeEvent);\n    console.log('Event Info : ', res.event);\n    console.groupEnd();\n  };\n\n  const onClickTimezonesCollapseBtn: ExternalEventTypes['clickTimezonesCollapseBtn'] = (\n    timezoneCollapsed\n  ) => {\n    console.group('onClickTimezonesCollapseBtn');\n    console.log('Is Timezone Collapsed?: ', timezoneCollapsed);\n    console.groupEnd();\n\n    const newTheme = {\n      'week.daygridLeft.width': '100px',\n      'week.timegridLeft.width': '100px',\n    };\n\n    getCalInstance().setTheme(newTheme);\n  };\n\n  const onBeforeUpdateEvent: ExternalEventTypes['beforeUpdateEvent'] = (updateData) => {\n    console.group('onBeforeUpdateEvent');\n    console.log(updateData);\n    console.groupEnd();\n\n    const targetEvent = updateData.event;\n    const changes = { ...updateData.changes };\n\n    getCalInstance().updateEvent(targetEvent.id, targetEvent.calendarId, changes);\n  };\n\n  const onBeforeCreateEvent: ExternalEventTypes['beforeCreateEvent'] = (eventData) => {\n    const event = {\n      calendarId: eventData.calendarId || '',\n      id: String(Math.random()),\n      title: eventData.title,\n      isAllday: eventData.isAllday,\n      start: eventData.start,\n      end: eventData.end,\n      category: eventData.isAllday ? 'allday' : 'time',\n      dueDateClass: '',\n      location: eventData.location,\n      state: eventData.state,\n      isPrivate: eventData.isPrivate,\n    };\n\n    getCalInstance().createEvents([event]);\n  };\n\n  return (\n    <div>\n      <h1>🍞📅 TOAST UI Calendar + React.js</h1>\n      <div>\n        <select onChange={onChangeSelect} value={selectedView}>\n          {viewModeOptions.map((option, index) => (\n            <option value={option.value} key={index}>\n              {option.title}\n            </option>\n          ))}\n        </select>\n        <span>\n          <button\n            type=\"button\"\n            className=\"btn btn-default btn-sm move-today\"\n            data-action=\"move-today\"\n            onClick={onClickNavi}\n          >\n            Today\n          </button>\n          <button\n            type=\"button\"\n            className=\"btn btn-default btn-sm move-day\"\n            data-action=\"move-prev\"\n            onClick={onClickNavi}\n          >\n            Prev\n          </button>\n          <button\n            type=\"button\"\n            className=\"btn btn-default btn-sm move-day\"\n            data-action=\"move-next\"\n            onClick={onClickNavi}\n          >\n            Next\n          </button>\n        </span>\n        <span className=\"render-range\">{selectedDateRangeText}</span>\n      </div>\n      <Calendar\n        height=\"900px\"\n        calendars={initialCalendars}\n        month={{ startDayOfWeek: 1 }}\n        events={initialEvents}\n        template={{\n          milestone(event) {\n            return `<span style=\"color: #fff; background-color: ${event.backgroundColor};\">${event.title}</span>`;\n          },\n          allday(event) {\n            return `[All day] ${event.title}`;\n          },\n        }}\n        theme={theme}\n        timezone={{\n          zones: [\n            {\n              timezoneName: 'Asia/Seoul',\n              displayLabel: 'Seoul',\n              tooltip: 'UTC+09:00',\n            },\n            {\n              timezoneName: 'Pacific/Guam',\n              displayLabel: 'Guam',\n              tooltip: 'UTC+10:00',\n            },\n          ],\n        }}\n        useDetailPopup={true}\n        useFormPopup={true}\n        view={selectedView}\n        week={{\n          showTimezoneCollapseButton: true,\n          timezonesCollapsed: false,\n          eventView: true,\n          taskView: true,\n        }}\n        // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n        // @ts-ignore\n        ref={calendarRef}\n        onAfterRenderEvent={onAfterRenderEvent}\n        onBeforeDeleteEvent={onBeforeDeleteEvent}\n        onClickDayname={onClickDayName}\n        onClickEvent={onClickEvent}\n        onClickTimezonesCollapseBtn={onClickTimezonesCollapseBtn}\n        onBeforeUpdateEvent={onBeforeUpdateEvent}\n        onBeforeCreateEvent={onBeforeCreateEvent}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/react-calendar/example/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>TOAST UI Calendar for React</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/react-calendar/example/main.tsx",
    "content": "import '@toast-ui/calendar/toastui-calendar.css';\nimport 'tui-date-picker/dist/tui-date-picker.min.css';\nimport 'tui-time-picker/dist/tui-time-picker.min.css';\n\nimport React from 'react';\nimport ReactDOM from 'react-dom';\n\nimport { App } from './app';\n\nReactDOM.render(\n  <React.StrictMode>\n    <App view=\"month\" />\n  </React.StrictMode>,\n  document.getElementById('app')\n);\n"
  },
  {
    "path": "apps/react-calendar/example/theme.ts",
    "content": "import type { Options } from '@toast-ui/calendar';\n\nexport const theme: Options['theme'] = {\n  common: {\n    border: '1px solid #ddd',\n    backgroundColor: 'white',\n    holiday: { color: '#f54f3d' },\n    saturday: { color: '#135de6' },\n    dayName: { color: '#333' },\n    today: { color: '#009688' },\n    gridSelection: {\n      backgroundColor: 'rgba(19, 93, 230, 0.1)',\n      border: '1px solid #135de6',\n    },\n  },\n  month: {\n    dayName: {\n      borderLeft: 'none',\n      backgroundColor: 'inherit',\n    },\n    holidayExceptThisMonth: { color: '#f3acac' },\n    dayExceptThisMonth: { color: '#bbb' },\n    weekend: { backgroundColor: '#fafafa' },\n    moreView: { boxShadow: 'none' },\n    moreViewTitle: { backgroundColor: '#f4f4f4' },\n  },\n  week: {\n    dayName: {\n      borderTop: '1px solid #ddd',\n      borderBottom: '1px solid #ddd',\n      borderLeft: '1px solid #ddd',\n      backgroundColor: 'inherit',\n    },\n    today: {\n      color: '#009688',\n      backgroundColor: 'inherit',\n    },\n    pastDay: { color: '#999' },\n    panelResizer: { border: '1px solid #ddd' },\n    dayGrid: { borderRight: '1px solid #ddd' },\n    dayGridLeft: {\n      width: '100px',\n      backgroundColor: '',\n      borderRight: '1px solid #ddd',\n    },\n    weekend: { backgroundColor: 'inherit' },\n    timeGridLeft: {\n      width: '100px',\n      backgroundColor: '#fafafa',\n      borderRight: '1px solid #ddd',\n    },\n    timeGridLeftAdditionalTimezone: { backgroundColor: '#fdfdfd' },\n    timeGridHourLine: { borderBottom: '1px solid #eee' },\n    timeGridHalfHourLine: { borderBottom: '1px dotted #f9f9f9' },\n    timeGrid: { borderRight: '1px solid #ddd' },\n    nowIndicatorLabel: { color: '#135de6' },\n    nowIndicatorPast: { border: '1px solid rgba(19, 93, 230, 0.3)' },\n    nowIndicatorBullet: { backgroundColor: '#135de6' },\n    nowIndicatorToday: { border: '1px solid #135de6' },\n    nowIndicatorFuture: { border: '1px solid #135de6' },\n    pastTime: { color: '#999' },\n    futureTime: { color: '#333' },\n    gridSelection: { color: '#135de6' },\n  },\n};\n"
  },
  {
    "path": "apps/react-calendar/example/utils.ts",
    "content": "import { TZDate } from '@toast-ui/calendar';\n\nexport function clone(date: TZDate): TZDate {\n  return new TZDate(date);\n}\n\nexport function addHours(d: TZDate, step: number) {\n  const date = clone(d);\n  date.setHours(d.getHours() + step);\n\n  return date;\n}\n\nexport function addDate(d: TZDate, step: number) {\n  const date = clone(d);\n  date.setDate(d.getDate() + step);\n\n  return date;\n}\n\nexport function subtractDate(d: TZDate, steps: number) {\n  const date = clone(d);\n  date.setDate(d.getDate() - steps);\n\n  return date;\n}\n"
  },
  {
    "path": "apps/react-calendar/package.json",
    "content": "{\n  \"name\": \"@toast-ui/react-calendar\",\n  \"version\": \"2.1.3\",\n  \"license\": \"MIT\",\n  \"description\": \"TOAST UI Calendar for React\",\n  \"author\": \"NHN Cloud FE Development Lab <dl_javascript@nhn.com>\",\n  \"homepage\": \"https://github.com/nhn/tui.calendar\",\n  \"bugs\": \"https://github.com/nhn/tui.calendar/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/nhn/tui.calendar.git\",\n    \"directory\": \"apps/react-calendar\"\n  },\n  \"main\": \"./dist/toastui-react-calendar.js\",\n  \"module\": \"./dist/toastui-react-calendar.mjs\",\n  \"types\": \"./types/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/toastui-react-calendar.mjs\",\n      \"require\": \"./dist/toastui-react-calendar.js\"\n    },\n    \"./ie11\": \"./dist/toastui-react-calendar.ie11.js\",\n    \"./esm\": \"./dist/toastui-react-calendar.mjs\"\n  },\n  \"files\": [\n    \"dist\",\n    \"types/index.d.ts\"\n  ],\n  \"typesVersions\": {\n    \"*\": {\n      \"*\": [\n        \"./types/index.d.ts\"\n      ]\n    }\n  },\n  \"keywords\": [\n    \"nhn\",\n    \"toast\",\n    \"toastui\",\n    \"toast-ui\",\n    \"calendar\",\n    \"fullcalendar\",\n    \"daily\",\n    \"weekly\",\n    \"monthly\",\n    \"business week\",\n    \"milestone\",\n    \"task\",\n    \"allday\"\n  ],\n  \"scripts\": {\n    \"develop\": \"vite\",\n    \"build\": \"rimraf dist/ && concurrently 'npm:build:*'\",\n    \"build:modern\": \"vite build && vite build --mode minify\",\n    \"build:ie11\": \"vite build --mode ie11 && vite build --mode ie11_minify\",\n    \"build:esm\": \"vite build --mode esm\",\n    \"build:types\": \"rimraf types/ && tsc -p ./tsconfig.declaration.json\"\n  },\n  \"dependencies\": {\n    \"@toast-ui/calendar\": \"^2.1.3\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^17.0.2\",\n    \"@types/react-dom\": \"^17.0.2\",\n    \"@vitejs/plugin-react\": \"^1.3.2\",\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8.0\"\n  }\n}\n"
  },
  {
    "path": "apps/react-calendar/src/index.tsx",
    "content": "import type { EventObject, ExternalEventTypes, Options } from '@toast-ui/calendar';\nimport ToastUICalendar from '@toast-ui/calendar';\nimport React from 'react';\n\nimport { isEqual } from './isEqual';\n\ntype ReactCalendarOptions = Omit<Options, 'defaultView'>;\ntype CalendarView = Required<Options>['defaultView'];\n\ntype CalendarExternalEventNames = Extract<keyof ExternalEventTypes, string>;\ntype ReactCalendarEventNames = `on${Capitalize<CalendarExternalEventNames>}`;\ntype ReactCalendarEventHandler = ExternalEventTypes[CalendarExternalEventNames];\ntype ReactCalendarExternalEvents = {\n  [events in ReactCalendarEventNames]: ReactCalendarEventHandler;\n};\n\ntype Props = ReactCalendarOptions & {\n  height: string;\n  events?: Partial<EventObject>[];\n  view?: CalendarView;\n} & ReactCalendarExternalEvents;\n\nconst optionsProps: (keyof ReactCalendarOptions)[] = [\n  'useFormPopup',\n  'useDetailPopup',\n  'isReadOnly',\n  'week',\n  'month',\n  'gridSelection',\n  'usageStatistics',\n  'eventFilter',\n  'timezone',\n  'template',\n];\n\nconst reactCalendarEventNames: ReactCalendarEventNames[] = [\n  'onSelectDateTime',\n  'onBeforeCreateEvent',\n  'onBeforeUpdateEvent',\n  'onBeforeDeleteEvent',\n  'onAfterRenderEvent',\n  'onClickDayName',\n  'onClickEvent',\n  'onClickMoreEventsBtn',\n  'onClickTimezonesCollapseBtn',\n];\n\nexport default class ToastUIReactCalendar extends React.Component<Props> {\n  containerElementRef = React.createRef<HTMLDivElement>();\n\n  calendarInstance: ToastUICalendar | null = null;\n\n  static defaultProps = {\n    height: '800px',\n    view: 'week',\n  };\n\n  componentDidMount() {\n    const { height, events = [], view, ...options } = this.props;\n    const container = this.containerElementRef.current;\n\n    if (container) {\n      this.calendarInstance = new ToastUICalendar(container, { ...options, defaultView: view });\n\n      container.style.height = height;\n    }\n\n    this.setEvents(events);\n    this.bindEventHandlers(options);\n  }\n\n  shouldComponentUpdate(nextProps: Readonly<Props>) {\n    const { calendars, height, events, theme, view } = this.props;\n    const {\n      calendars: nextCalendars,\n      height: nextHeight,\n      events: nextEvents,\n      theme: nextTheme = {},\n      view: nextView = 'week',\n    } = nextProps;\n\n    if (!isEqual(height, nextHeight) && this.containerElementRef.current) {\n      this.containerElementRef.current.style.height = nextHeight;\n    }\n\n    if (!isEqual(calendars, nextCalendars)) {\n      this.setCalendars(nextCalendars);\n    }\n\n    if (!isEqual(events, nextEvents)) {\n      this.calendarInstance?.clear();\n      this.setEvents(nextEvents);\n    }\n\n    if (!isEqual(theme, nextTheme)) {\n      this.calendarInstance?.setTheme(nextTheme);\n    }\n\n    if (!isEqual(view, nextView)) {\n      this.calendarInstance?.changeView(nextView);\n    }\n\n    const nextOptions = optionsProps.reduce((acc, key) => {\n      if (!isEqual(this.props[key], nextProps[key])) {\n        acc[key] = nextProps[key];\n      }\n\n      return acc;\n    }, {} as Record<keyof Options, any>);\n\n    this.calendarInstance?.setOptions(nextOptions);\n\n    this.bindEventHandlers(nextProps);\n\n    return false;\n  }\n\n  componentWillUnmount() {\n    this.calendarInstance?.destroy();\n  }\n\n  setCalendars(calendars?: Options['calendars']) {\n    if (calendars) {\n      this.calendarInstance?.setCalendars(calendars);\n    }\n  }\n\n  setEvents(events?: Partial<EventObject>[]) {\n    if (events) {\n      this.calendarInstance?.createEvents(events);\n    }\n  }\n\n  bindEventHandlers(externalEvents: ReactCalendarExternalEvents) {\n    const eventNames = Object.keys(externalEvents).filter((key) =>\n      reactCalendarEventNames.includes(key as ReactCalendarEventNames)\n    );\n\n    eventNames.forEach((key) => {\n      const eventName = key[2].toLowerCase() + key.slice(3);\n\n      if (this.calendarInstance) {\n        this.calendarInstance.off(eventName);\n        this.calendarInstance.on(eventName, externalEvents[key as ReactCalendarEventNames]);\n      }\n    });\n  }\n\n  getInstance() {\n    return this.calendarInstance;\n  }\n\n  getRootElement() {\n    return this.containerElementRef.current;\n  }\n\n  render() {\n    return <div className=\"container\" ref={this.containerElementRef} />;\n  }\n}\n"
  },
  {
    "path": "apps/react-calendar/src/isEqual.ts",
    "content": "/* eslint-disable complexity,prefer-destructuring */\n\nconst hasOwnProp = (obj: any, key: any) => Object.prototype.hasOwnProperty.call(obj, key);\n\n/**\n * Compare two objects deeply\n *\n * It doesn't care about some types like RegExp, Map, Set etc.\n * Because they are not proper types for props of the Calendar component.\n *\n * Comparing two functions with `toString` is not completely reliable\n * because their closure variables might not the same.\n *\n * @param {any} a the first object\n * @param {any} b the second object to compare\n * @returns {boolean}\n */\nexport function isEqual(a: any, b: any) {\n  if (a === b) {\n    return true;\n  }\n\n  if (a instanceof Function) {\n    return a.toString() === b.toString();\n  }\n\n  if (a && b && typeof a === 'object' && typeof b === 'object') {\n    let length;\n\n    if (a.constructor !== b.constructor) {\n      return false;\n    }\n    if (Array.isArray(a)) {\n      length = a.length;\n      if (length !== b.length) {\n        return false;\n      }\n      for (let i = 0; i < length; i += 1) {\n        // eslint-disable-next-line max-depth\n        if (!isEqual(a[i], b[i])) {\n          return false;\n        }\n      }\n\n      return true;\n    }\n    if (a.valueOf !== Object.prototype.valueOf) {\n      return a.valueOf() === b.valueOf();\n    }\n    if (a.toString !== Object.prototype.toString) {\n      return a.toString() === b.toString();\n    }\n    const keys = Object.keys(a);\n    length = keys.length;\n    if (length !== Object.keys(b).length) {\n      return false;\n    }\n    for (let i = 0; i < length; i += 1) {\n      const key = keys[i];\n      if (!hasOwnProp(b, key) || !isEqual(a[key], b[key])) {\n        return false;\n      }\n    }\n\n    return true;\n  }\n\n  // eslint-disable-next-line no-self-compare\n  return a !== a && b !== b;\n}\n"
  },
  {
    "path": "apps/react-calendar/tsconfig.declaration.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"noEmit\": false,\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"plugins\": [\n      {\n        \"transform\": \"typescript-transform-paths\",\n        \"afterDeclarations\": true\n      }\n    ],\n    \"outDir\": \"types\"\n  },\n  \"files\": [\"src/index.tsx\"],\n  \"include\": []\n}\n"
  },
  {
    "path": "apps/react-calendar/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"types\": [\"vite/client\"]\n  },\n  \"include\": [\"src\", \"example\"],\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "apps/react-calendar/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\"\n  },\n  \"include\": [\"./vite.config.ts\"]\n}\n"
  },
  {
    "path": "apps/react-calendar/vite.config.ts",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport react from '@vitejs/plugin-react';\nimport * as path from 'path';\nimport type { UserConfig, UserConfigExport } from 'vite';\nimport { defineConfig } from 'vite';\n\nconst commonConfig: UserConfigExport = {\n  plugins: [\n    react({\n      include: '**/*.tsx',\n    }),\n  ],\n};\n\nexport default defineConfig(({ command, mode }) => {\n  // for dev config\n  if (command === 'serve') {\n    return {\n      ...commonConfig,\n      server: {\n        open: '/example/index.html',\n      },\n    };\n  }\n\n  // for build config\n  const shouldMinify = mode.includes('minify');\n  const isESM = mode.includes('esm');\n  const isIE11 = mode.includes('ie11');\n\n  const filenameBase = `toastui-react-calendar${isIE11 ? '.ie11' : ''}${\n    shouldMinify ? '.min' : ''\n  }`;\n\n  const buildConfig: UserConfig = {\n    ...commonConfig,\n    build: {\n      emptyOutDir: false,\n      lib: {\n        entry: path.resolve(__dirname, 'src/index.tsx'),\n        name: 'tui.ReactCalendar',\n        formats: isESM ? ['es'] : ['umd'],\n        fileName: (format) => `${filenameBase}${format === 'es' ? '.m' : '.'}js`,\n      },\n      rollupOptions: {\n        external: ['react', 'react-dom'],\n        output: {\n          globals: {\n            react: 'React',\n            'react-dom': 'ReactDOM',\n          },\n        },\n      },\n      minify: shouldMinify ? 'esbuild' : false,\n    },\n  };\n\n  if (isIE11) {\n    Object.assign(buildConfig, {\n      resolve: {\n        alias: {\n          '@toast-ui/calendar': '@toast-ui/calendar/ie11',\n        },\n      },\n    });\n    buildConfig.plugins.push(\n      commonjs({\n        transformMixedEsModules: true,\n      })\n    );\n  }\n\n  return buildConfig;\n});\n"
  },
  {
    "path": "apps/vue-calendar/.eslintignore",
    "content": "build/\ndist/\nnode_modules/\n"
  },
  {
    "path": "apps/vue-calendar/.eslintrc.js",
    "content": "module.exports = {\n  root: true,\n  env: {\n    browser: true,\n    es6: true,\n    node: true,\n  },\n  parser: 'vue-eslint-parser',\n  parserOptions: {\n    parser: '@typescript-eslint/parser',\n    ecmaVersion: 2018,\n    sourceType: 'module',\n  },\n  plugins: ['prettier', '@typescript-eslint'],\n  extends: [\n    'tui',\n    'prettier',\n    'plugin:@typescript-eslint/recommended',\n    'plugin:prettier/recommended',\n    'plugin:vue/recommended',\n  ],\n  ignorePatterns: ['**/*.d.ts'],\n  rules: {\n    'vue/require-default-prop': 0,\n  },\n};\n"
  },
  {
    "path": "apps/vue-calendar/.lintstagedrc.js",
    "content": "module.exports = {\n  \"**/*.js\": \"eslint --fix\",\n}\n"
  },
  {
    "path": "apps/vue-calendar/README.md",
    "content": "# TOAST UI Calendar for Vue 2\n\n> This is a Vue component wrapping [TOAST UI Calendar](/apps/calendar/).\n\n[![vue2](https://img.shields.io/badge/vue-2.x-4fc08d.svg)](https://v2.vuejs.org/)\n[![npm version](https://img.shields.io/npm/v/@toast-ui/vue-calendar.svg)](https://www.npmjs.com/package/@toast-ui/vue-calendar)\n[![license](https://img.shields.io/github/license/nhn/tui.calendar.svg)](https://github.com/nhn/tui.calendar/blob/master/LICENSE)\n[![PRs welcome](https://img.shields.io/badge/PRs-welcome-ff69b4.svg)](https://github.com/nhn/tui.calendar/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)\n[![code with hearth by NHN Cloud](https://img.shields.io/badge/%3C%2F%3E%20with%20%E2%99%A5%20by-NHN_Cloud-ff1414.svg)](https://github.com/nhn)\n\n## 🚩 Table of Contents\n\n- [📙 Documents](#-documents)\n- [Collect statistics on the use of open source](#collect-statistics-on-the-use-of-open-source)\n- [💾 Install](#-install)\n  - [Using npm](#using-npm)\n- [📅 Usage](#-usage)\n  - [Install Vue 2](#install-vue-2)\n  - [Load](#load)\n  - [Implement](#implement)\n- [🔧 Pull Request Steps](#-pull-request-steps)\n  - [Setup](#setup)\n  - [Develop](#develop)\n  - [Pull Request](#pull-request)\n- [💬 Contributing](#-contributing)\n- [📜 License](#-license)\n\n## 📙 Documents\n\n- [English](./docs/README.md)\n- [Korean](./docs/ko/README.md)\n\n## Collect statistics on the use of open source\n\nTOAST UI Calendar for Vue applies Google Analytics (GA) to collect statistics on the use of open source, in order to identify how widely TOAST UI Calendar is used throughout the world. It also serves as important index to determine the future course of projects. `location.hostname` (e.g. > “ui.toast.com\") is to be collected and the sole purpose is nothing but to measure statistics on the usage.\n\nTo disable GA, set the [`usageStatistics` option](/docs/en/apis/options.md#usagestatistics) to `false`:\n\n```html\n<template>\n  <ToastUICalendar :usage-statistics=\"false\" />\n</template>\n```\n\n## 💾 Install\n\n### Using npm\n\n```sh\nnpm install --save @toast-ui/vue-calendar\n```\n\n## 📅 Usage\n\n### Install Vue 2\n\nTo use TOAST UI Calendar for Vue, [Vue 2](https://v2.vuejs.org/) should be installed. Vue 3 is not supported.\n\n### Load\n\nYou can use Toast UI Calendar for Vue as moudule format or namespace. Also you can use Single File Component (SFC of Vue).\n\n```js\n/* ES6 module in Node.js environment */\nimport ToastUICalendar from '@toast-ui/vue-calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.css';\n```\n\n```js\n/* CommonJS in Node.js environment */\nconst Calendar = require('@toast-ui/vue-calendar');\nrequire('@toast-ui/calendar/dist/toastui-calendar.css');\n```\n\n```js\n// browser environment namespace\nconst Calendar = tui.VueCalendar;\n```\n\n### Implement\n\n```html\n<template>\n  <Calendar style=\"height: 800px\" />\n</template>\n\n<script>\nimport Calendar from '@toast-ui/vue-calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n\nexport default {\n  name: 'YourComponent',\n  components: {\n    Calendar,\n  },\n};\n</script>\n```\n\n```js\nimport Calendar from '@toast-ui/vue-calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n\nnew Vue({\n  el: '#app',\n  components: {\n    Calendar,\n  },\n});\n```\n\n## 🔧 Pull Request Steps\n\nTOAST UI products are open source, so you can create a pull request(PR) after you fix issues.\nRun npm scripts and develop yourself with the following process.\n\n### Setup\n\nFork `main` branch into your personal repository.\nClone it to local computer. Install node modules.\nBefore starting development, you should check to have any errors.\n\n``` sh\ngit clone https://github.com/{your-personal-repo}/[[repo name]].git\ncd [[repo name]]\nnpm install\n```\n\n### Develop\n\nLet's start development!\n\n### Pull Request\n\nBefore PR, check to test lastly and then check any errors.\nIf it has no error, commit and then push it!\n\nFor more information on PR's step, please see links of Contributing section.\n\n## 💬 Contributing\n\n- [Code of Conduct](/CODE_OF_CONDUCT.md)\n- [Contributing Guidelines](/CONTRIBUTING.md)\n- [Commit Message Convention](/docs/COMMIT_MESSAGE_CONVENTION.md)\n\n## 📜 License\n\nThis software is licensed under the [MIT](/LICENSE) © [NHN Cloud](https://github.com/nhn).\n"
  },
  {
    "path": "apps/vue-calendar/docs/README.md",
    "content": "# 🗂️ Document Index\n\n[한글 문서 인덱스(Korean)](./ko/README.md)\n\n## Guides\n\n- [Getting started](./en/guide/getting-started.md)\n- [v2 Migration guide](./en/guide/migration-guide-v2.md)\n\n## Etc\n\n- [Code of Conduct](/CODE_OF_CONDUCT.md)\n- [Contributing Guidelines](/CONTRIBUTING.md)\n- [Commit Message Convention](/docs/COMMIT_MESSAGE_CONVENTION.md)\n"
  },
  {
    "path": "apps/vue-calendar/docs/en/guide/getting-started.md",
    "content": "# Getting Started\n\n## Table of Contents\n\n- [Install Vue 2](#install-vue-2)\n- [Installation](#installation)\n  - [Using the package manager](#using-the-package-manager)\n    - [npm](#npm)\n- [How to use the calendar](#how-to-use-the-calendar)\n  - [JavaScript](#javascript)\n    - [Importing modules](#importing-modules)\n    - [Loading bundle files for legacy browsers](#loading-bundle-files-for-legacy-browsers)\n  - [CSS](#css)\n- [Vue](#vue)\n  - [Props](#props)\n  - [Events](#events)\n  - [Methods](#methods)\n    - [getRootElement](#getrootelement)\n    - [getInstance](#getinstance)\n- [Basic usage](#basic-usage)\n  - [Disable to collect hostname for Google Analytics(GA)](#disable-to-collect-hostname-for-google-analyticsga)\n\n## Install Vue 2\n\nTo use TOAST UI Calendar for Vue, [Vue 2](https://v2.vuejs.org/) should be installed. Vue 3 is not supported.\n\n## Installation\n\nTOAST UI products can be used by using the package manager or by directly downloading the source code. However, it is recommended to use a package manager.\n\n### Using the package manager\n\nTOAST UI products are registered in the [npm](https://www.npmjs.com/) package registry. You can easily install packages using CLI tools provided by each package manager. To use npm, you need to install [Node.js](https://nodejs.org) in advance.\n\n#### npm\n\n```sh\nnpm install @toast-ui/vue-calendar # latest version\nnpm install @toast-ui/vue-calendar@<version> # specific version\n```\n\n## How to use the calendar\n\n### JavaScript\n\n#### Importing modules\n\nThere are three ways to import TOAST UI Calendar for Vue depending on the environment.\n\n```js\n/* ES6 module in Node.js environment */\nimport Calendar from '@toast-ui/vue-calendar';\n```\n\n```js\n/* CommonJS in Node.js environment */\nconst Calendar = require('@toast-ui/vue-calendar');\n```\n\n```js\n/* in the browser environment namespace */\nconst Calendar = tui.VueCalendar;\n```\n\n#### Loading bundle files for legacy browsers\n\nTOAST UI Calendar for Vue provides a separate bundle file for legacy browsers. The basic bundle provides stable support for the latest two versions of the modern browser. However, the basic bundle does not include a polyfill for IE11, so to support IE11 or similar level of legacy browsers, you need to add the IE11 bundle that includes a polyfill as follows.\n\nSince the bundle size of IE11 is about 2x larger than that of the default bundle, you must take care not to increase the bundle size unnecessarily by considering the range of support.\n\n```js\n/* ES6 module in Node.js environment */\nimport Calendar from '@toast-ui/vue-calendar/ie11';\n```\n\n```js\n/* CommonJS in Node.js environment */\nconst Calendar = require('@toast-ui/vue-calendar/ie11');\n```\n\n### CSS\n\nTo use the calendar, you need to add a CSS file of TOAST UI Calendar. You can import CSS files through import and require, or you can import them through CDN.\n\n```js\n/* ES6 module in Node.js environment */\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css'; // Stylesheet for calendar\n```\n\n```js\n/* CommonJS in Node.js environment */\nrequire('@toast-ui/calendar/dist/toastui-calendar.min.css');\n```\n\n```html\n<!-- CDN -->\n<link rel=\"stylesheet\" href=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css\" />\n```\n\n## Vue\n\nYou can load a calendar component and add it to the `components` in your component or Vue instance.\n\n```html\n<template>\n  <Calendar style=\"height: 800px\" />\n</template>\n\n<script>\nimport Calendar from '@toast-ui/vue-calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n\nexport default {\n  name: 'YourComponent',\n  components: {\n    Calendar,\n  },\n};\n</script>\n```\n\n```js\nimport Calendar from '@toast-ui/vue-calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n\nnew Vue({\n  el: '#app',\n  components: {\n    Calendar,\n  },\n});\n```\n\n### Props\n\nTOAST UI Calendar for Vue provide props for [options of TOAST UI Calendar](/docs/en/apis/options.md). Each name of props is same with options of Toast UI Calendar except `view` is for `defaultView`.\n\nAdditionally, it provides a `events` prop to add events.\n\n```html\n<template>\n  <Calendar\n    style=\"height: 800px\"\n    :view=\"view\"\n    :use-detail-popup=\"true\"\n    :month=\"month\"\n    :calendars=\"calendars\"\n    :events=\"events\"\n  />\n</template>\n\n<script>\nimport Calendar from '@toast-ui/vue-calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n\nexport default {\n  name: 'YourComponent',\n  components: {\n    Calendar,\n  },\n  data() {\n    return {\n      view: 'month',\n      month: {\n        dayNames: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],\n        visibleWeeksCount: 3,\n      },\n      calendars: [{ id: 'cal1', name: 'Personal' }],\n      events: [\n        {\n          id: '1',\n          calendarId: 'cal1',\n          title: 'Lunch',\n          category: 'time',\n          start: '2022-06-28T12:00:00',\n          end: '2022-06-28T13:30:00',\n        },\n        {\n          id: '2',\n          calendarId: 'cal1',\n          title: 'Coffee Break',\n          category: 'time',\n          start: '2022-06-28T15:00:00',\n          end: '2022-06-28T15:30:00',\n        },\n      ],\n    };\n  },\n};\n</script>\n```\n\n### Events\n\nYou can use the `v-on` directive to handle the calendar instance events. For more information such as the parameters of each event, see [TOAST UI Calendar Instance Events](/docs/en/apis/calendar.md#instance-events).\n\n```html\n<template>\n  <Calendar\n    style=\"height: 800px\"\n    ref=\"calendar\"\n    @selectDateTime=\"onSelectDateTime\"\n    @beforeCreateSchedule=\"onBeforeCreateSchedule\"\n  />\n</template>\n\n<script>\nimport Calendar from '@toast-ui/vue-calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n\nexport default {\n  name: 'YourComponent',\n  components: {\n    Calendar,\n  },\n  methods: {\n    onSelectDateTime({ start, end }) {\n      alert(`Select ${start} ~ ${end}`);\n    },\n    onBeforeCreateSchedule(event) {\n      const calendarInstance = this.$refs.calendar.getInstance();\n      calendarInstance.createEvents([\n        {\n          ...event,\n          id: uuid(),\n        }\n      ]);\n    },\n  },\n};\n</script>\n```\n\n### Methods\n\n💡 Click on a method to see more detailed explanations and usage examples.\n\n| Method | Description |\n| --- | --- |\n| [getRootElement](#getrootelement) | Return the element on which the calendar is mounted. |\n| [getInstance](#getinstance) | Return the calendar instance. |\n\n#### getRootElement\n\n- Type: `getRootElement(): HTMLDivElement`\n- Returns: `HTMLDivElement` - the element on which the calendar is mounted\n\nReturn the element on which the calendar is mounted.\n\n#### getInstance\n\n- Type: `getInstance(): Calendar`\n- Returns: `Calendar` - the calendar instance\n\nReturn the calendar instance. You can use this to call the [calendar instance methods](/docs/en/apis/calendar.md#instance-methods).\n\n```html\n<template>\n  <Calendar\n    style=\"height: 800px\"\n    ref=\"calendar\"\n  />\n</template>\n\n<script>\nimport Calendar from '@toast-ui/vue-calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n\nexport default {\n  name: 'YourComponent',\n  components: {\n    Calendar,\n  },\n  computed: {\n    calendarInstance() {\n      return this.$refs.calendar.getInstance();\n    }\n  },\n  mounted() {\n    this.calendarInstance.setDate('2022-06-29T12:30:00');\n  }\n};\n</script>\n```\n\n## Basic usage\n\n### Disable to collect hostname for Google Analytics(GA)\n\n[TOAST UI Calendar](https://github.com/nhn/tui.calendar) applies [GA](https://analytics.google.com/analytics/web/) to collect statistics on open source usage to see how widespread it is around the world. This serves as an important indicator to determine the future progress of the project. It collects `location.hostname` (e.g. \"ui.toast.com\") and is only used to measure usage statistics.\n\nTo disable GA, set the [`usageStatistics` prop](/docs/en/apis/options.md#usagestatistics) to `false`:\n\n```html\n<template>\n  <Calendar :usage-statistics=\"false\"/>\n</template>\n```\n"
  },
  {
    "path": "apps/vue-calendar/docs/en/guide/migration-guide-v2.md",
    "content": "# v2 Migration Guide\n\n## Table of Contents\n\n- [Overview](#overview)\n- [Changed](#changed)\n  - [Change term from `schedule` to `event`](#change-term-from-schedule-to-event)\n  - [Use `getInstance` method instead of `invoke`](#use-getinstance-method-instead-of-invoke)\n\n## Overview\n\nChanges made in TOAST UI Calendar v2.0 are reflected in Vue Wrapper. For more information about changes in TOAST UI Calendar v2.0, refer to the [TOAST UI Caledar v2 Migration Guide](/docs/en/guide/migration-guide-v2.md).\n\nThis guide summarizes the changes only in Vue Wrapper.\n\n## Changed\n\n### Change term from `schedule` to `event`\n\nIn v2, the name was changed from `schedule` to `event` to match the meaning of events in the calendar. So, the `schedules` prop has been renamed to `events`.\n\n### Use `getInstance` method instead of `invoke`\n\nIn v1, the calendar instance methods were called indirectly by `invoke` method. Since v2, it provides `getInstance` method to get the calendar instance. You can use it to call the calendar instance methods.\n\n```html\n<template>\n  <Calendar\n    style=\"height: 800px\"\n    ref=\"calendar\"\n  />\n</template>\n\n<script>\nimport Calendar from '@toast-ui/vue-calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n\nexport default {\n  name: 'YourComponent',\n  components: {\n    Calendar,\n  },\n  computed: {\n    calendarInstance() {\n      return this.$refs.calendar.getInstance();\n    }\n  },\n  mounted() {\n    this.calendarInstance.setDate('2022-06-29T12:30:00');\n  }\n};\n</script>\n```\n"
  },
  {
    "path": "apps/vue-calendar/docs/ko/README.md",
    "content": "# 🗂️ 문서 인덱스\n\n## 가이드\n\n- [시작하기](./guide/getting-started.md)\n- [v2 마이그레이션 가이드](./guide/migration-guide-v2.md)\n"
  },
  {
    "path": "apps/vue-calendar/docs/ko/guide/getting-started.md",
    "content": "# 시작하기\n\n## 목차\n\n- [Vue 2 설치하기](#vue-2-설치하기)\n- [설치하기](#설치하기)\n  - [패키지 매니저 사용하기](#패키지-매니저-사용하기)\n    - [npm](#npm)\n- [사용하기](#사용하기)\n  - [자바스크립트](#자바스크립트)\n    - [불러오기](#불러오기)\n    - [레거시 브라우저용 번들 파일 불러오기](#레거시-브라우저용-번들-파일-불러오기)\n  - [CSS](#css)\n- [Vue에서 사용하기](#vue에서-사용하기)\n  - [Props](#props)\n  - [이벤트](#이벤트)\n  - [메서드](#메서드)\n    - [getRootElement](#getrootelement)\n    - [getInstance](#getinstance)\n- [기본적인 사용 방법](#기본적인-사용-방법)\n  - [Google Analytics(GA)를 위한 hostname 수집 거부하기](#google-analyticsga를-위한-hostname-수집-거부하기)\n\n## Vue 2 설치하기\n\nTOAST UI 캘린더 Vue Wrapper를 사용하려면 [Vue 2](https://v2.vuejs.org/)를 설치해야 한다. Vue 3는 지원하지 않는다.\n\n## 설치하기\n\nTOAST UI 제품들은 패키지 매니저를 이용하거나, 직접 소스 코드를 다운받아 사용할 수 있다. 하지만 패키지 매니저 사용을 권장한다.\n\n### 패키지 매니저 사용하기\n\nTOAST UI 제품들은 [npm](https://www.npmjs.com/) 패키지 매니저에 등록되어 있다.\n각 패키지 매니저가 제공하는 CLI 도구를 사용하면 쉽게 패키지를 설치할 수 있다. npm 사용을 위해선 [Node.js](https://nodejs.org)를 미리 설치해야 한다.\n\n#### npm\n\n```sh\nnpm install @toast-ui/vue-calendar # 최신 버전\nnpm install @toast-ui/vue-calendar@<version> # 특정 버전\n```\n\n## 사용하기\n\n### 자바스크립트\n\n#### 불러오기\n\nTOAST UI 캘린더 Vue Wrapper는 아래 세 가지 방법으로 불러올 수 있다.\n\n```js\n/* Node.js 환경에서 ES6 모듈 */\nimport Calendar from '@toast-ui/vue-calendar';\n```\n\n```js\n/* Node.js 환경에서 CommonJS */\nconst Calendar = require('@toast-ui/vue-calendar');\n```\n\n```js\n/* 브라우저 환경에서 namespace */\nconst Calendar = tui.VueCalendar;\n```\n\n#### 레거시 브라우저용 번들 파일 불러오기\n\nTOAST UI 캘린더 Vue Wrapper는 레거시 브라우저용 번들 파일을 따로 제공하고 있다. 기본 번들은 모던 브라우저의 최신 2개 버전을 안정적으로 지원한다. 하지만 기본 번들은 IE11을 위한 폴리필이 포함되어있지 않으므로 IE11 혹은 일정 수준 이하의 레거시 브라우저를 지원하기 위해서는 다음과 같이 폴리필이 포함된 IE11 번들을 추가해야 한다.\n\nIE11의 번들 크기는 기본 번들보다 2배 가량 크기 때문에 반드시 지원 범위를 잘 고려하여 불필요하게 번들 사이즈를 늘리지 않도록 유의해야 한다.\n\n```js\n/* Node.js 환경에서 ES6 모듈 */\nimport Calendar from '@toast-ui/vue-calendar/ie11';\n```\n\n```js\n/* Node.js 환경에서 CommonJS */\nconst Calendar = require('@toast-ui/vue-calendar/ie11');\n```\n\n### CSS\n\nCalendar를 사용하기 위해서는 TOAST UI 캘린더의 CSS 파일을 추가해야 한다. import, require를 통해 CSS 파일을 불러오거나, CDN을 통해 불러올 수 있다.\n\n```js\n/* Node.js 환경에서 ES6 모듈 */\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css'; // Calendar 스타일\n```\n\n```js\n/* Node.js 환경에서 CommonJS */\nrequire('@toast-ui/calendar/dist/toastui-calendar.min.css');\n```\n\n```html\n<!-- CDN -->\n<link rel=\"stylesheet\" href=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css\" />\n```\n\n## Vue에서 사용하기\n\nVue 인스턴스나 컴포넌트에서 TOAST UI 캘린더 Vue Wrapper를 불러와서 사용할 수 있다.\n\n```html\n<template>\n  <Calendar style=\"height: 800px\" />\n</template>\n\n<script>\nimport Calendar from '@toast-ui/vue-calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n\nexport default {\n  name: 'YourComponent',\n  components: {\n    Calendar,\n  },\n};\n</script>\n```\n\n```js\nimport Calendar from '@toast-ui/vue-calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n\nnew Vue({\n  el: '#app',\n  components: {\n    Calendar,\n  },\n});\n```\n\n### Props\n\nTOAST UI 캘린더의 [옵션](/docs/ko/apis/options.md)은 Vue 컴포넌트의 Props으로 구현되어 있다. `defaultView`는 `view`라는 이름이고, 그 외는 동일한 이름이다.\n\n옵션 외에도 `events` prop을 이용해 일정 데이터를 바로 추가할 수도 있다.\n\n```html\n<template>\n  <Calendar\n    style=\"height: 800px\"\n    :view=\"view\"\n    :use-detail-popup=\"true\"\n    :month=\"month\"\n    :calendars=\"calendars\"\n    :events=\"events\"\n  />\n</template>\n\n<script>\nimport Calendar from '@toast-ui/vue-calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n\nexport default {\n  name: 'YourComponent',\n  components: {\n    Calendar,\n  },\n  data() {\n    return {\n      view: 'month',\n      month: {\n        dayNames: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],\n        visibleWeeksCount: 3,\n      },\n      calendars: [{ id: 'cal1', name: 'Personal' }],\n      events: [\n        {\n          id: '1',\n          calendarId: 'cal1',\n          title: 'Lunch',\n          category: 'time',\n          start: '2022-06-28T12:00:00',\n          end: '2022-06-28T13:30:00',\n        },\n        {\n          id: '2',\n          calendarId: 'cal1',\n          title: 'Coffee Break',\n          category: 'time',\n          start: '2022-06-28T15:00:00',\n          end: '2022-06-28T15:30:00',\n        },\n      ],\n    };\n  },\n};\n</script>\n```\n\n### 이벤트\n\nVue의 `v-on` 디렉티브를 이용하여 캘린더 인스턴스 이벤트를 핸들링할 수 있다. 각 이벤트의 자세한 내용은 [TOAST UI 캘린더 인스턴스 이벤트 문서](/docs/ko/apis/calendar.md#인스턴스-이벤트)를 참고한다.\n\n```html\n<template>\n  <Calendar\n    style=\"height: 800px\"\n    ref=\"calendar\"\n    @selectDateTime=\"onSelectDateTime\"\n    @beforeCreateSchedule=\"onBeforeCreateSchedule\"\n  />\n</template>\n\n<script>\nimport Calendar from '@toast-ui/vue-calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n\nexport default {\n  name: 'YourComponent',\n  components: {\n    Calendar,\n  },\n  methods: {\n    onSelectDateTime({ start, end }) {\n      alert(`Select ${start} ~ ${end}`);\n    },\n    onBeforeCreateSchedule(event) {\n      const calendarInstance = this.$refs.calendar.getInstance();\n      calendarInstance.createEvents([\n        {\n          ...event,\n          id: uuid(),\n        }\n      ]);\n    },\n  },\n};\n</script>\n```\n\n### 메서드\n\n💡 메서드를 클릭하면 더 자세한 설명과 사용 예시를 볼 수 있다.\n\n| 메서드 | 설명 |\n| --- | --- |\n| [getRootElement](#getrootelement) | TOAST UI 캘린더가 마운트된 요소를 리턴한다. |\n| [getInstance](#getinstance) | TOAST UI 캘린더 인스턴스를 리턴한다. |\n\n#### getRootElement\n\n- 타입: `getRootElement(): HTMLDivElement`\n- 리턴: `HTMLDivElement` - TOAST UI 캘린더가 마운트된 요소\n\nTOAST UI 캘린더가 마운트된 요소를 리턴한다.\n\n#### getInstance\n\n- 타입: `getInstance(): Calendar`\n- 리턴: `Calendar` - TOAST UI 캘린더 인스턴스\n\nTOAST UI 캘린더 인스턴스를 리턴한다. 이를 이용하여 [캘린더 인스턴스 메서드](/docs/ko/apis/calendar.md#인스턴스-메서드)를 사용할 수 있다.\n\n```html\n<template>\n  <Calendar\n    style=\"height: 800px\"\n    ref=\"calendar\"\n  />\n</template>\n\n<script>\nimport Calendar from '@toast-ui/vue-calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n\nexport default {\n  name: 'YourComponent',\n  components: {\n    Calendar,\n  },\n  computed: {\n    calendarInstance() {\n      return this.$refs.calendar.getInstance();\n    }\n  },\n  mounted() {\n    this.calendarInstance.setDate('2022-06-29T12:30:00');\n  }\n};\n</script>\n```\n\n## 기본적인 사용 방법\n\n### Google Analytics(GA)를 위한 hostname 수집 거부하기\n\n[TOAST UI 캘린더](https://github.com/nhn/tui.calendar)는 [GA](https://analytics.google.com/analytics/web/)를 적용하여 오픈 소스 사용에 대한 통계를 수집하여 전 세계에서 얼마나 널리 사용되는지 확인한다.\n이는 프로젝트의 향후 진행을 결정하는 중요한 지표 역할을 한다.\n`location.hostname`(예를 들어 \"ui.toast.com\")을 수집하며 사용량에 대한 통계를 측정하기 위해서만 사용된다.\n\n만약 이를 거부하려면 [`usageStatistics` prop](/docs/ko/apis/options.md#usagestatistics)을 `false`로 설정한다.\n\n```html\n<template>\n  <Calendar :usage-statistics=\"false\"/>\n</template>\n```\n"
  },
  {
    "path": "apps/vue-calendar/docs/ko/guide/migration-guide-v2.md",
    "content": "# v2 마이그레이션 가이드\n\n## 목차\n\n- [개요](#개요)\n- [변경](#변경)\n  - [`schedule`에서 `event`로 용어 변경](#schedule에서-event로-용어-변경)\n  - [`invoke` 대신 `getInstance` 메서드 사용](#invoke-대신-getinstance-메서드-사용)\n\n## 개요\n\nTOAST UI Calendar v2.0에서 변경된 점이 Vue Wrapper에 그대로 반영되었다. TOAST UI Calendar의 변경점은 [TOAST UI Caledar v2 마이그레이션 가이드](/docs/ko/guide/migration-guide-v2.md)를 참고한다.\n\n해당 문서에서는 Vue Wrapper에서만 변경된 점을 정리한다.\n\n## 변경\n\n### `schedule`에서 `event`로 용어 변경\n\nv2에서는 일정이라는 의미에 맞게 기존 `schedule`에서 `event`로 네이밍이 변경되었다. 이에 일정\n데이터를 넘기는 `schedules`라는 Prop이 `events`로 바뀌었다.\n\n### `invoke` 대신 `getInstance` 메서드 사용\n\nv1에서는 `invoke`라는 함수를 이용하여 캘린더 인스턴스 메서드를 간접적으로 호출할 수 있었다. v2에서는 `getInstance`라는 메서드를 사용하여 캘린더 인스턴스를 리턴받아 직접 인스턴스 메서드를 호출할 수 있다.\n\n```html\n<template>\n  <Calendar\n    style=\"height: 800px\"\n    ref=\"calendar\"\n  />\n</template>\n\n<script>\nimport Calendar from '@toast-ui/vue-calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n\nexport default {\n  name: 'YourComponent',\n  components: {\n    Calendar,\n  },\n  computed: {\n    calendarInstance() {\n      return this.$refs.calendar.getInstance();\n    }\n  },\n  mounted() {\n    this.calendarInstance.setDate('2022-06-29T12:30:00');\n  }\n};\n</script>\n```\n"
  },
  {
    "path": "apps/vue-calendar/example/App.vue",
    "content": "<template>\n  <div>\n    <h1>🍞📅 TOAST UI Calendar + Vue</h1>\n    <select\n      v-model=\"selectedView\"\n      class=\"view-select\"\n    >\n      <option\n        v-for=\"view in viewOptions\"\n        :key=\"view.value\"\n        :value=\"view.value\"\n      >\n        {{ view.title }}\n      </option>\n    </select>\n    <div class=\"buttons\">\n      <button\n        type=\"button\"\n        @click=\"onClickTodayButton\"\n      >\n        Today\n      </button>\n      <button\n        type=\"button\"\n        @click=\"onClickMoveButton(-1)\"\n      >\n        Prev\n      </button>\n      <button\n        type=\"button\"\n        @click=\"onClickMoveButton(1)\"\n      >\n        Next\n      </button>\n    </div>\n    <span class=\"date-range\">{{ dateRangeText }}</span>\n    <ToastUICalendar\n      ref=\"calendar\"\n      style=\"height: 800px\"\n      :view=\"'month'\"\n      :use-form-popup=\"true\"\n      :use-detail-popup=\"true\"\n      :week=\"{\n        showTimezoneCollapseButton: true,\n        timezonesCollapsed: false,\n        eventView: true,\n        taskView: true,\n      }\"\n      :month=\"{ startDayOfWeek: 1 }\"\n      :timezone=\"{ zones }\"\n      :theme=\"theme\"\n      :template=\"{\n        milestone: getTemplateForMilestone,\n        allday: getTemplateForAllday,\n      }\"\n      :grid-selection=\"true\"\n      :calendars=\"calendars\"\n      :events=\"events\"\n      @selectDateTime=\"onSelectDateTime\"\n      @beforeCreateEvent=\"onBeforeCreateEvent\"\n      @beforeUpdateEvent=\"onBeforeUpdateEvent\"\n      @beforeDeleteEvent=\"onBeforeDeleteEvent\"\n      @afterRenderEvent=\"onAfterRenderEvent\"\n      @clickDayName=\"onClickDayName\"\n      @clickEvent=\"onClickEvent\"\n      @clickTimezonesCollapseBtn=\"onClickTimezonesCollapseBtn\"\n    />\n  </div>\n</template>\n\n<script>\n/* eslint-disable no-console */\nimport ToastUICalendar from '../src/Calendar';\nimport '@toast-ui/calendar/toastui-calendar.css';\nimport 'tui-date-picker/dist/tui-date-picker.min.css';\nimport 'tui-time-picker/dist/tui-time-picker.min.css';\n\nimport { events } from './mock-data';\nimport { theme } from './theme';\nimport './app.css';\n\nexport default {\n  components: {\n    ToastUICalendar,\n  },\n  data() {\n    return {\n      calendars: [\n        {\n          id: '0',\n          name: 'Private',\n          backgroundColor: '#9e5fff',\n          borderColor: '#9e5fff',\n          dragBackgroundColor: '#9e5fff',\n        },\n        {\n          id: '1',\n          name: 'Company',\n          backgroundColor: '#00a9ff',\n          borderColor: '#00a9ff',\n          dragBackgroundColor: '#00a9ff',\n        },\n      ],\n      events,\n      zones: [\n        {\n          timezoneName: 'Asia/Seoul',\n          displayLabel: 'Seoul',\n          tooltip: 'UTC+09:00',\n        },\n        {\n          timezoneName: 'Pacific/Guam',\n          displayLabel: 'Guam',\n          tooltip: 'UTC+10:00',\n        },\n      ],\n      theme,\n      selectedView: 'month',\n      viewOptions: [\n        {\n          title: 'Monthly',\n          value: 'month',\n        },\n        {\n          title: 'Weekly',\n          value: 'week',\n        },\n        {\n          title: 'Daily',\n          value: 'day',\n        },\n      ],\n      dateRangeText: '',\n    };\n  },\n  computed: {\n    calendarInstance() {\n      return this.$refs.calendar.getInstance();\n    },\n  },\n  watch: {\n    selectedView(newView) {\n      this.calendarInstance.changeView(newView);\n      this.setDateRangeText();\n    },\n  },\n  mounted() {\n    this.setDateRangeText();\n  },\n  methods: {\n    getTemplateForMilestone(event) {\n      return `<span style=\"color: #fff; background-color: ${event.backgroundColor};\">${event.title}</span>`;\n    },\n    getTemplateForAllday(event) {\n      return `[All day] ${event.title}`;\n    },\n    onSelectDateTime({ start, end }) {\n      console.group('onSelectDateTime');\n      console.log(`Date : ${start} ~ ${end}`);\n      console.groupEnd();\n    },\n    onBeforeCreateEvent(eventData) {\n      const event = {\n        calendarId: eventData.calendarId || '',\n        id: String(Math.random()),\n        title: eventData.title,\n        isAllday: eventData.isAllday,\n        start: eventData.start,\n        end: eventData.end,\n        category: eventData.isAllday ? 'allday' : 'time',\n        dueDateClass: '',\n        location: eventData.location,\n        state: eventData.state,\n        isPrivate: eventData.isPrivate,\n      };\n\n      this.calendarInstance.createEvents([event]);\n    },\n    onBeforeUpdateEvent(updateData) {\n      console.group('onBeforeUpdateEvent');\n      console.log(updateData);\n      console.groupEnd();\n\n      const targetEvent = updateData.event;\n      const changes = { ...updateData.changes };\n\n      this.calendarInstance.updateEvent(targetEvent.id, targetEvent.calendarId, changes);\n    },\n\n    onBeforeDeleteEvent({ title, id, calendarId }) {\n      console.group('onBeforeDeleteEvent');\n      console.log('Event Info : ', title);\n      console.groupEnd();\n\n      this.calendarInstance.deleteEvent(id, calendarId);\n    },\n    onAfterRenderEvent({ title }) {\n      console.group('onAfterRenderEvent');\n      console.log('Event Info : ', title);\n      console.groupEnd();\n    },\n    onClickDayName({ date }) {\n      console.group('onClickDayName');\n      console.log('Date : ', date);\n      console.groupEnd();\n    },\n    onClickEvent({ nativeEvent, event }) {\n      console.group('onClickEvent');\n      console.log('MouseEvent : ', nativeEvent);\n      console.log('Event Info : ', event);\n      console.groupEnd();\n    },\n    onClickTimezonesCollapseBtn(timezoneCollapsed) {\n      console.group('onClickTimezonesCollapseBtn');\n      console.log('Is Timezone Collapsed?: ', timezoneCollapsed);\n      console.groupEnd();\n\n      const newTheme = {\n        'week.daygridLeft.width': '100px',\n        'week.timegridLeft.width': '100px',\n      };\n\n      this.calendarInstance.setTheme(newTheme);\n    },\n    onClickTodayButton() {\n      this.calendarInstance.today();\n      this.setDateRangeText();\n    },\n    onClickMoveButton(offset) {\n      this.calendarInstance.move(offset);\n      this.setDateRangeText();\n    },\n    setDateRangeText() {\n      const date = this.calendarInstance.getDate();\n      const start = this.calendarInstance.getDateRangeStart();\n      const end = this.calendarInstance.getDateRangeEnd();\n\n      const startYear = start.getFullYear();\n      const endYear = end.getFullYear();\n\n      switch (this.selectedView) {\n        case 'month':\n          this.dateRangeText = `${date.getFullYear()}.${date.getMonth() + 1}`;\n\n          return;\n        case 'day':\n          this.dateRangeText = `${date.getFullYear()}.${date.getMonth() + 1}.${date.getDate()}`;\n\n          return;\n        default:\n          this.dateRangeText = `${startYear}.${start.getMonth() + 1}.${start.getDate()} - ${\n            startYear !== endYear ? `${endYear}.` : ''\n          }${end.getMonth() + 1}.${end.getDate()}`;\n      }\n    },\n  },\n};\n</script>\n"
  },
  {
    "path": "apps/vue-calendar/example/app.css",
    "content": ".buttons {\n  display: inline-block;\n  margin-left: 10px;\n}\n\n.buttons > button {\n  margin-right: 5px;\n}\n\n.buttons > button:last-child {\n  margin-right: 0;\n}\n\n.date-range {\n  margin-left: 10px;\n}"
  },
  {
    "path": "apps/vue-calendar/example/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>TOAST UI Calendar for Vue</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./main.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/vue-calendar/example/main.js",
    "content": "import Vue from 'vue';\nimport App from './App.vue';\n\n/* eslint-disable no-new */\nnew Vue({\n  el: '#app',\n  components: { App },\n  render: (h) => h(App),\n});\n"
  },
  {
    "path": "apps/vue-calendar/example/mock-data.js",
    "content": "import { TZDate } from '@toast-ui/calendar';\n\nimport { addDate, addHours, subtractDate } from './utils';\n\nconst today = new TZDate();\n\nexport const events = [\n  {\n    id: '1',\n    calendarId: '0',\n    title: 'TOAST UI Calendar Study',\n    category: 'time',\n    start: today,\n    end: addHours(today, 3),\n  },\n  {\n    id: '2',\n    calendarId: '0',\n    title: 'Practice',\n    category: 'milestone',\n    start: addDate(today, 1),\n    end: addDate(today, 1),\n    isReadOnly: true,\n  },\n  {\n    id: '3',\n    calendarId: '0',\n    title: 'FE Workshop',\n    category: 'allday',\n    start: subtractDate(today, 2),\n    end: subtractDate(today, 1),\n    isReadOnly: true,\n  },\n  {\n    id: '4',\n    calendarId: '0',\n    title: 'Report',\n    category: 'time',\n    start: today,\n    end: addHours(today, 1),\n  },\n];\n"
  },
  {
    "path": "apps/vue-calendar/example/theme.js",
    "content": "export const theme = {\n  common: {\n    border: '1px solid #ddd',\n    backgroundColor: 'white',\n    holiday: { color: '#f54f3d' },\n    saturday: { color: '#135de6' },\n    dayName: { color: '#333' },\n    today: { color: '#009688' },\n    gridSelection: {\n      backgroundColor: 'rgba(19, 93, 230, 0.1)',\n      border: '1px solid #135de6',\n    },\n  },\n  month: {\n    dayName: {\n      borderLeft: 'none',\n      backgroundColor: 'inherit',\n    },\n    holidayExceptThisMonth: { color: '#f3acac' },\n    dayExceptThisMonth: { color: '#bbb' },\n    weekend: { backgroundColor: '#fafafa' },\n    moreView: { boxShadow: 'none' },\n    moreViewTitle: { backgroundColor: '#f4f4f4' },\n  },\n  week: {\n    dayName: {\n      borderTop: '1px solid #ddd',\n      borderBottom: '1px solid #ddd',\n      borderLeft: '1px solid #ddd',\n      backgroundColor: 'inherit',\n    },\n    today: {\n      color: '#009688',\n      backgroundColor: 'inherit',\n    },\n    pastDay: { color: '#999' },\n    panelResizer: { border: '1px solid #ddd' },\n    dayGrid: { borderRight: '1px solid #ddd' },\n    dayGridLeft: {\n      width: '100px',\n      backgroundColor: '',\n      borderRight: '1px solid #ddd',\n    },\n    weekend: { backgroundColor: 'inherit' },\n    timeGridLeft: {\n      width: '100px',\n      backgroundColor: '#fafafa',\n      borderRight: '1px solid #ddd',\n    },\n    timeGridLeftAdditionalTimezone: { backgroundColor: '#fdfdfd' },\n    timeGridHourLine: { borderBottom: '1px solid #eee' },\n    timeGridHalfHourLine: { borderBottom: '1px dotted #f9f9f9' },\n    timeGrid: { borderRight: '1px solid #ddd' },\n    nowIndicatorLabel: { color: '#135de6' },\n    nowIndicatorPast: { border: '1px solid rgba(19, 93, 230, 0.3)' },\n    nowIndicatorBullet: { backgroundColor: '#135de6' },\n    nowIndicatorToday: { border: '1px solid #135de6' },\n    nowIndicatorFuture: { border: '1px solid #135de6' },\n    pastTime: { color: '#999' },\n    futureTime: { color: '#333' },\n    gridSelection: { color: '#135de6' },\n  },\n};\n"
  },
  {
    "path": "apps/vue-calendar/example/utils.js",
    "content": "import { TZDate } from '@toast-ui/calendar';\n\nexport function clone(date) {\n  return new TZDate(date);\n}\n\nexport function addHours(d, step) {\n  const date = clone(d);\n  date.setHours(d.getHours() + step);\n\n  return date;\n}\n\nexport function addDate(d, step) {\n  const date = clone(d);\n  date.setDate(d.getDate() + step);\n\n  return date;\n}\n\nexport function subtractDate(d, steps) {\n  const date = clone(d);\n  date.setDate(d.getDate() - steps);\n\n  return date;\n}\n"
  },
  {
    "path": "apps/vue-calendar/index.d.ts",
    "content": "import type Vue from 'vue';\nimport type Calendar from '@toast-ui/calendar';\n\nexport default class VueCalendar extends Vue {\n  getRootElement(): HTMLDivElement;\n  getInstance(): Calendar; \n}\n\ndeclare namespace tui {\n  export class VueCalendar extends Vue {\n    getRootElement(): HTMLDivElement;\n    getInstance(): Calendar; \n  }\n}\n"
  },
  {
    "path": "apps/vue-calendar/package.json",
    "content": "{\n  \"name\": \"@toast-ui/vue-calendar\",\n  \"description\": \"TOAST UI Calendar for Vue\",\n  \"author\": \"NHN Cloud FE Development Lab <dl_javascript@nhn.com>\",\n  \"version\": \"2.1.3\",\n  \"main\": \"./dist/toastui-vue-calendar.js\",\n  \"types\": \"./index.d.ts\",\n  \"module\": \"./dist/toastui-vue-calendar.mjs\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/toastui-vue-calendar.mjs\",\n      \"require\": \"./dist/toastui-vue-calendar.js\"\n    },\n    \"./ie11\": \"./dist/toastui-vue-calendar.ie11.js\",\n    \"./esm\": \"./dist/toastui-vue-calendar.mjs\"\n  },\n  \"typesVersions\": {\n    \"*\": {\n      \"*\": [\n        \"./index.d.ts\"\n      ]\n    }\n  },\n  \"files\": [\n    \"dist\",\n    \"index.d.ts\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/nhn/tui.calendar.git\",\n    \"directory\": \"apps/vue-calendar\"\n  },\n  \"keywords\": [\n    \"nhn\",\n    \"toast\",\n    \"toastui\",\n    \"toast-ui\",\n    \"calendar\",\n    \"fullcalendar\",\n    \"daily\",\n    \"weekly\",\n    \"monthly\",\n    \"business week\",\n    \"milestone\",\n    \"task\",\n    \"allday\"\n  ],\n  \"scripts\": {\n    \"lint\": \"eslint .\",\n    \"develop\": \"vite\",\n    \"build\": \"rimraf dist/ && concurrently 'npm:build:*'\",\n    \"build:modern\": \"vite build && vite build --mode minify\",\n    \"build:ie11\": \"vite build --mode ie11 && vite build --mode ie11_minify\",\n    \"build:esm\": \"vite build --mode esm\"\n  },\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@toast-ui/calendar\": \"^2.1.3\"\n  },\n  \"devDependencies\": {\n    \"@vue/cli\": \"^5.0.6\",\n    \"eslint-plugin-vue\": \"^9.1.1\",\n    \"vite-plugin-vue2\": \"^2.0.2\",\n    \"vue\": \"^2.6.14\"\n  },\n  \"peerDependencies\": {\n    \"vue\": \"^2.6.14\"\n  }\n}\n"
  },
  {
    "path": "apps/vue-calendar/src/Calendar.js",
    "content": "/* eslint-disable no-undefined */\nimport Calendar from '@toast-ui/calendar';\nimport Vue from 'vue';\n\nexport default Vue.component('ToastUICalendar', {\n  name: 'ToastUICalendar',\n  props: {\n    view: String,\n    useFormPopup: {\n      type: Boolean,\n      default: () => undefined,\n    },\n    useDetailPopup: {\n      type: Boolean,\n      default: () => undefined,\n    },\n    isReadOnly: {\n      type: Boolean,\n      default: () => undefined,\n    },\n    usageStatistics: {\n      type: Boolean,\n      default: () => undefined,\n    },\n    eventFilter: Function,\n    week: Object,\n    month: Object,\n    gridSelection: {\n      type: [Object, Boolean],\n      default: () => undefined,\n    },\n    timezone: Object,\n    theme: Object,\n    template: Object,\n    calendars: Array,\n    events: Array,\n  },\n  data() {\n    return {\n      calendarInstance: null,\n    };\n  },\n  watch: {\n    view(value) {\n      this.calendarInstance.changeView(value);\n    },\n    useFormPopup(value) {\n      this.calendarInstance.setOptions({ useFormPopup: value });\n    },\n    useDetailPopup(value) {\n      this.calendarInstance.setOptions({ useDetailPopup: value });\n    },\n    isReadOnly(value) {\n      this.calendarInstance.setOptions({ isReadOnly: value });\n    },\n    eventFilter(value) {\n      this.calendarInstance.setOptions({ eventFilter: value });\n    },\n    week(value) {\n      this.calendarInstance.setOptions({ week: value });\n    },\n    month(value) {\n      this.calendarInstance.setOptions({ month: value });\n    },\n    gridSelection(value) {\n      this.calendarInstance.setOptions({ gridSelection: value });\n    },\n    timezone(value) {\n      this.calendarInstance.setOptions({ timezone: value });\n    },\n    theme(value) {\n      this.calendarInstance.setTheme(value);\n    },\n    template(value) {\n      this.calendarInstance.setOptions({ template: value });\n    },\n    calendars(value) {\n      this.calendarInstance.setCalendars(value);\n    },\n    events(value) {\n      this.calendarInstance.clear();\n      this.calendarInstance.createEvents(value);\n    },\n  },\n  mounted() {\n    this.calendarInstance = new Calendar(this.$refs.container, {\n      defaultView: this.view,\n      useFormPopup: this.useFormPopup,\n      useDetailPopup: this.useDetailPopup,\n      isReadOnly: this.isReadOnly,\n      usageStatistics: this.usageStatistics,\n      eventFilter: this.eventFilter,\n      week: this.week,\n      month: this.month,\n      gridSelection: this.gridSelection,\n      timezone: this.timezone,\n      theme: this.theme,\n      template: this.template,\n      calendars: this.calendars,\n    });\n    this.addEventListeners();\n    this.calendarInstance.createEvents(this.events);\n  },\n  beforeDestroy() {\n    this.calendarInstance.off();\n    this.calendarInstance.destroy();\n  },\n  methods: {\n    addEventListeners() {\n      Object.keys(this.$listeners).forEach((eventName) => {\n        this.calendarInstance.on(eventName, (...args) => this.$emit(eventName, ...args));\n      });\n    },\n    getRootElement() {\n      return this.$refs.container;\n    },\n    getInstance() {\n      return this.calendarInstance;\n    },\n  },\n  template: '<div ref=\"container\" class=\"toastui-vue-calendar\" />',\n});\n"
  },
  {
    "path": "apps/vue-calendar/vite.config.js",
    "content": "import { defineConfig } from 'vite';\nimport { createVuePlugin } from 'vite-plugin-vue2';\nimport commonjs from '@rollup/plugin-commonjs';\nimport path from 'path';\n\nconst commonConfig = {\n  plugins: [createVuePlugin()],\n};\n\nexport default defineConfig(({ command, mode }) => {\n  // dev config\n  if (command === 'serve') {\n    return {\n      ...commonConfig,\n      resolve: {\n        alias: {\n          vue: 'vue/dist/vue',\n        },\n      },\n      server: {\n        open: '/example/index.html',\n      },\n    };\n  }\n\n  // build config\n  const shouldMinify = mode.includes('minify');\n  const isESM = mode.includes('esm');\n  const isIE11 = mode.includes('ie11');\n\n  const filenameBase = `toastui-vue-calendar${isIE11 ? '.ie11' : ''}${shouldMinify ? '.min' : ''}`;\n\n  const buildConfig = {\n    ...commonConfig,\n    build: {\n      emptyOutDir: false,\n      lib: {\n        entry: path.resolve(__dirname, 'src/Calendar.js'),\n        name: 'tui.VueCalendar',\n        formats: isESM ? ['es'] : ['umd'],\n        fileName: (format) => `${filenameBase}${format === 'es' ? '.m' : '.'}js`,\n      },\n      rollupOptions: {\n        external: ['vue'],\n        output: {\n          globals: {\n            vue: 'Vue',\n          },\n        },\n      },\n      minify: shouldMinify,\n    },\n  };\n\n  if (isIE11) {\n    Object.assign(buildConfig, {\n      resolve: {\n        alias: {\n          '@toast-ui/calendar': '@toast-ui/calendar/ie11',\n        },\n      },\n    });\n    buildConfig.plugins.push(\n      commonjs({\n        transformMixedEsModules: true,\n      })\n    );\n  }\n\n  return buildConfig;\n});\n"
  },
  {
    "path": "babel.config.json",
    "content": "{\n  \"presets\": [\n    [\"@babel/preset-env\"],\n    [\"@babel/preset-typescript\", { \"jsxPragma\": \"h\" }]\n  ],\n  \"plugins\": [[\"@babel/plugin-transform-react-jsx\", { \"pragma\": \"h\", \"pragmaFrag\": \"Fragment\" }]]\n}\n"
  },
  {
    "path": "docs/COMMIT_MESSAGE_CONVENTION.md",
    "content": "# Commit Message Convention\n\n## Commit Message Format\n\n```\n<Type>: Short description (fix #1234)\n\nLogger description here if necessary\n\nBREAKING CHANGE: only contain breaking change\n```\n\n* Any line of the commit message cannot be longer 100 characters!\n\n## Revert\n\n```\nrevert: commit <short-hash>\n\nThis reverts commit <full-hash>\nMore description if needed\n```\n\n## Type\n\nMust be one of the following:\n\n* **feat**: A new feature\n* **fix**: A bug fix\n* **docs**: Documentation only changes\n* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)\n* **refactor**: A code change that neither fixes a bug nor adds a feature\n* **perf**: A code change that improves performance\n* **test**: Adding missing or correcting existing tests\n* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation\n\n## Subject\n\n* use the imperative, **present** tense: \"change\" not \"changed\" nor \"changes\"\n* don't capitalize the first letter\n* no dot (.) at the end\n* reference GitHub issues at the end. If the commit doesn’t completely fix the issue, then use `(refs #1234)` instead of `(fixes #1234)`.\n\n## Body\n\n* use the imperative, **present** tense: \"change\" not \"changed\" nor \"changes\".\n* the motivation for the change and contrast this with previous behavior.\n\n## BREAKING CHANGE\n\n* This commit contains breaking change(s).\n* start with the word BREAKING CHANGE: with a space or two newlines. The rest of the commit message is then used for this.\n\nThis convention is based on [AngularJS](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits) and [ESLint](https://eslint.org/docs/developer-guide/contributing/pull-requests#step2)\n"
  },
  {
    "path": "docs/ISSUE_TEMPLATE.md",
    "content": "<!--\nThank you for your contribution.\n\nWhen writing an issue, please, use the template below.\nIt's mandatory to use the template for submitting the new issue.\nWe don't reply to the issue not following the template.\n-->\n\n<!-- BUG ISSUE TEMPLATE -->\n## Version\n<!-- Write the version of the calendar you are currently using. -->\n\n## Test Environment\n<!-- Write the browser type, OS and so on -->\n\n## Current Behavior\n<!-- Write steps to reproduce the current behaviour in detail.\nYou can add sample code, 'CodePen' or 'jsfiddle' links. -->\n\n```js\n// Write example code\n```\n\n## Expected Behavior\n<!-- Write a description for future action. -->\n"
  },
  {
    "path": "docs/PULL_REQUEST_TEMPLATE.md",
    "content": "<!-- EDIT TITLE PLEASE -->\n<!-- It should be one of them\n  <ISSUE TYPE>: Short Description (<CLOSING TYPE> #<ISSUE NUMBERS>)\n  ex)\n  feat: add new feature (close #111)\n  fix: wrong behavior (fix #111)\n  chore: change build tool (ref #111)\n-->\n\n<!-- SPECIFY A ISSUE TYPE AT HEAD\n  feat: A new feature\n  fix: A bug fix\n  docs: Documentation only changes\n  style: Changes that do not affect the meaning of the code (white-space, formatting etc)\n  refactor: A code change that neither fixes a bug or adds a feature\n  perf: A code change that improves performance\n  test: Adding missing tests\n  chore: Changes to the build process or auxiliary tools and libraries such as documentation generation\n-->\n\n<!-- ADD CLOSING TYPE AND ISSUE NUMBER AT TAIL\n  (<CLOSING TYPE> #<ISSUE NUMBERS>)\n  close: resolve not a bug(feature, docs, etc) completely\n  fix: resolve a bug completely\n  ref: not fully resolved or related to\n-->\n\n### Please check if the PR fulfills these requirements\n\n- [ ] It's the right issue type on the title\n- [ ] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where \"xxx\" is the issue number)\n- [ ] The commit message follows our guidelines\n- [ ] Tests for the changes have been added (for bug fixes/features)\n- [ ] Docs have been added/updated (for bug fixes/features)\n- [ ] It does not introduce a breaking change or has a description of the breaking change\n\n### Description\n\n\n\n---\nThank you for your contribution to TOAST UI product. 🎉 😘 ✨\n"
  },
  {
    "path": "docs/README.md",
    "content": "# 🗂️ Document Index\n\n[한글 문서 인덱스(Korean)](./ko/README.md)\n\n## Guides\n\n- [Getting started](./en/guide/getting-started.md)\n- [v2 Migration guide](./en/guide/migration-guide-v2.md)\n\n## API Documentation\n\n- [Calendar class](./en/apis/calendar.md)\n- [Event Object](./en/apis/event-object.md)\n- [Options](./en/apis/options.md)\n- [Template](./en/apis/template.md)\n- [Theme](./en/apis/theme.md)\n- [TZDate class](./en/apis/tzdate.md)\n\n## Etc\n\n- [Code of Conduct](/CODE_OF_CONDUCT.md)\n- [Contributing Guidelines](/CONTRIBUTING.md)\n- [Commit Message Convention](/docs/COMMIT_MESSAGE_CONVENTION.md)\n"
  },
  {
    "path": "docs/en/apis/calendar.md",
    "content": "# Calendar class\n\n## Description\n\nYou can use the `Calendar` class to create a calendar instance. You need to specify where you want the instance to be created (the HTML element), and set the appropriate options if necessary.\n\nAdd a container element where TOAST UI Calendar will be created. **This element must have an appropriate height. (600px or higher is recommended)**\n\n```js\nimport { Calendar } from '@toast-ui/calendar';\n\n// Passing elements directly\nconst container = document.querySelector('#container');\nconst calendar = new Calendar(container);\n\n// Using CSS selectors\nconst calendar = new Calendar('#container');\n```\n\nOptions can be set as the second argument when creating a calendar instance. Options that are not specified are set to default values. For more information on options, see the [Options documentation](./options.md).\n\n```js\nconst calendar = new Calendar(container, {\n  // Options of the calendar instance\n  defaultView: 'month',\n  isReadOnly: true,\n  timezone: {\n    // ...\n  },\n  theme: {\n    // ...\n  },\n  template: {\n    // ...\n  },\n});\n```\n\nAfter creating an instance, you can control the operation of the calendar using the calendar [instance method](#instance-methods) and register event handlers in the [instance event](#instance-events).\n\n```js\nconst calendar = new Calendar('#container');\n\n// Registering an instance event\ncalendar.on('beforeCreateEvent', (eventObj) => {\n  // Calling the instance method when the instance event is invoked\n  calendar.createEvents([\n    {\n      ...eventObj,\n      id: uuid(),\n    },\n  ]);\n});\n```\n\n## Instance methods\n\n💡 Click on a method to see more detailed explanations and usage examples.\n\n| Method                                          | Description                                                                                                                           |\n| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |\n| [render](#render)                               | Renders the calendar instance to the screen.                                                                                          |\n| [renderToString](#rendertostring)               | Returns the rendering result of the current calendar instance as an HTML string for use in server-side rendering.                     |\n| [destroy](#destroy)                             | Destroys the calendar instance.                                                                                                       |\n| [getEvent](#getevent)                           | Gets the data of the target event.                                                                                                    |\n| [createEvents](#createevents)                   | Creates one or more calendar events.                                                                                                  |\n| [updateEvent](#updateevent)                     | Updates the contents of the target event.                                                                                             |\n| [deleteEvent](#deleteevent)                     | Deletes the target event.                                                                                                             |\n| [clear](#clear)                                 | Removes all events stored in the calendar instance.                                                                                   |\n| [today](#today)                                 | Moves to the range containing the current date.                                                                                       |\n| [move](#move)                                   | According to the view, it moves the range by a given number.                                                                          |\n| [prev](#prev)                                   | Moves to the previous range of the current screen. The range of movement depends on the range of the view.                            |\n| [next](#next)                                   | Moves to the next range of the current screen. The range of movement depends on the range of the view.                                |\n| [setDate](#setdate)                             | Moves to the range containing the specified date.                                                                                     |\n| [changeView](#changeview)                       | Changes the view of the calendar instance to Monthly/Weekly/Day.                                                                      |\n| [getElement](#getelement)                       | Finds the HTML element where a specific event was rendered. If not found, `null` is returned.                                         |\n| [setTheme](#settheme)                           | Changes the theme of the calendar instance.                                                                                           |\n| [getOptions](#getoptions)                       | Gets the options set in the calendar instance.                                                                                        |\n| [setOptions](#setoptions)                       | Changes the options of the calendar instance.                                                                                         |\n| [getDate](#getdate)                             | Gets the base date for displaying range of the calendar instance.                                                                     |\n| [getDateRangeStart](#getdaterangestart)         | Gets the start date for displaying range of the calendar instance.                                                                    |\n| [getDateRangeEnd](#getdaterangeend)             | Gets the end date for displaying range of the calendar instance.                                                                      |\n| [getViewName](#getviewname)                     | Gets the view type of the calendar instance. (Monthly / Weekly / Daily)                                                               |\n| [setCalendars](#setcalendars)                   | Changes calendar information.                                                                                                         |\n| [setCalendarVisibility](#setcalendarvisibility) | Hides or shows all events included in the specified event group.                                                                      |\n| [setCalendarColor](#setcalendarcolor)           | Changes the color value of all events included in the specified event group.                                                          |\n| [scrollToNow](#scrolltonow)                     | If a displayed range contains the current time in the weekly/daily view, it immediately scrolls to the current time area.             |\n| [openFormPopup](#openformpopup)                 | Displays the popup for creating the event. The value of the popup is filled according to the parameter.                               |\n| [clearGridSelections](#cleargridselections)     | Removes all date/time selection elements currently displayed in the calendar.                                                         |\n| [fire](#fire)                                   | Executes arbitrary instance events. A detailed description is provided in the [Instance Events](#instance-events) section.            |\n| [off](#off)                                     | Unregisters an instance event. A detailed description is provided in the [Instance Events](#instance-events) section.                 |\n| [on](#on)                                       | Registers an instance event. A detailed description is provided in the [Instance Events](#instance-events) section.                   |\n| [once](#once)                                   | Registers an instance event to fire only once. A detailed description is provided in the [Instance Events](#instance-events) section. |\n\n### render\n\n- Type: `render(): Calendar`\n- Returns: `Calendar` - the calendar instance\n\nThis method is automatically called when a calendar instance is created in the browser environment.\n\nWhen called for the first time, the calendar element is inserted under the HTML element passed as an argument when creating the instance, and if the `render` method is called directly, it is re-rendered.\n\nIf no HTML container is passed when instantiating, nothing happens.\n\nAfter calling the method, it returns the calendar instance itself.\n\n```js\n// Nothing happens\nconst calendar = new Calendar();\ncalendar.render();\n\n// Automatically rendered once when there is an element\nconst calendar = new Calendar('#container');\n\n// Re-render the instance above\ncalendar.render();\n```\n\n[⬆️ Back to the list](#instance-methods)\n\n### renderToString\n\n- Type: `renderToString(): string`\n- Returns: `string` - HTML string\n\nGenerates and returns an HTML string to be rendered by the calendar instance based on the given option value. It can be used in a server-side rendering environment.\n\n```js\nconst isSSR = typeof window === 'undefined';\n\n// For client, the calendar will be mounted to the `#container` after initializing the instance, but nothing happens in the server. \nconst calendar = new Calendar('#container');\n\nif (isSSR) {\n  const calendarHTML = calendar.renderToString();\n  // Send the HTML to the client from the server\n}\n```\n\n[⬆️ Back to the list](#instance-methods)\n\n### destroy\n\n- Type: `destroy(): void`\n\nDestroys the element rendered via the calendar instance, making the instance an empty object.\n\n[⬆️ Back to the list](#instance-methods)\n\n### getEvent\n\n- Type: `getEvent(eventId: string, calendarId: string): EventObject`\n- Parameters\n  - `eventId` - the unique ID of the event\n  - `calendarId` - the unique ID of the calendar\n- Returns: `EventObject` - an object containing event information\n\nFinds events stored within the calendar instance. Event-specific `eventId` and calendar-specific `calendarId` values are required.\n\n```js\nconst calendar = new Calendar('#container', {\n  calendars: [\n    {\n      id: 'cal1',\n      name: 'Work',\n    },\n  ],\n});\n\ncalendar.createEvents([\n  {\n    id: 'event1',\n    calendarId: 'cal1',\n    title: 'Weekly Meeting',\n    start: '2022-05-30T09:00:00',\n    end: '2022-05-30T10:00:00',\n  },\n]);\n\nconst firstEvent = calendar.getEvent('event1', 'cal1');\n\nconsole.log(firstEvent.title); // 'Weekly Meeting'\n```\n\n[⬆️ Back to the list](#instance-methods)\n\n### createEvents\n\n- Type: `createEvents(events: EventObject[]): void`\n- Parameters\n  - `EventObject[]` - an array of event information to create\n\nCreates one or more events. An array must also be passed even creating a single event.\n\nRefer to the [EventObject](./event-object.md) document for information required to create an event.\n\n```js\n// Creating a single event\nconst calendar = new Calendar('#container', {\n  calendars: [\n    {\n      id: 'cal1',\n      name: 'Work',\n    },\n  ],\n});\n\n// Creating multiple events\ncalendar.createEvents([\n  {\n    id: 'event1',\n    calendarId: 'cal1',\n    title: 'Weekly Meeting',\n    start: '2022-05-30T09:00:00',\n    end: '2022-05-30T10:00:00',\n  },\n]);\n\nconst firstEvent = calendar.getEvent('event1', 'cal1');\n\nconsole.log(firstEvent.title); // 'Weekly Meeting'\n```\n\n[⬆️ Back to the list](#instance-methods)\n\n### updateEvent\n\n- Type: `updateEvent(eventId: string, calendarId: string, changes: EventObject): void`\n- Parameters\n  - `eventId` - the unique ID of the event\n  - `calendarId` - the unique ID of the calendar\n  - `changes` - the changes you want to apply\n\nChanges the information of the generated event. To find an event, you need an event-specific `eventId`, a calendar-specific `calendarId`, and an object containing the information you want to change. The properties and values to be changed must match the properties of the `EventObject`.\n\n```js\n// First, assume that the event is created as shown below..\ncalendar.createEvents([\n  {\n    id: 'event1',\n    calendarId: 'cal1',\n    title: 'Weekly Meeting',\n    start: '2022-05-30T09:00:00',\n    end: '2022-05-30T10:00:00',\n  },\n]);\n\n// When changing only one property, title\ncalendar.updateEvent('event1', 'cal1', {\n  title: 'Weekly Meeting (Canceled)',\n});\n\n// When changing multiple properties\ncalendar.updateEvent('event1', 'cal1', {\n  title: 'Going vacation',\n  state: 'Free',\n  start: '2022-05-30T00:00:00',\n  end: '2022-06-03T23:59:59',\n});\n```\n\n[⬆️ Back to the list](#instance-methods)\n\n### deleteEvent\n\n- Type: `deleteEvent(eventId: string, calendarId: string): void`\n- Parameters\n  - `eventId` - the unique ID of the event\n  - `calendarId` - the unique ID of the calendar\n\nDeletes the target event.\n\n```js\n// First, assume that the event is created as shown below..\ncalendar.createEvents([\n  {\n    id: 'event1',\n    calendarId: 'cal1',\n    title: 'Weekly Meeting',\n    start: '2022-05-30T09:00:00',\n    end: '2022-05-30T10:00:00',\n  },\n]);\n\ncalendar.deleteEvent('event1', 'cal1');\n\n// If you try to find the event, it does not exist.\nconst deletedEvent = calendar.getEvent('event1', 'cal1');\nconsole.log(deletedEvent); // null\n```\n\n[⬆️ Back to the list](#instance-methods)\n\n### clear\n\n- Type: `clear(): void`\n\nRemoves all events stored in the calendar instance.\n\n[⬆️ Back to the list](#instance-methods)\n\n### today\n\n- Type: `today(): void`\n\nMoves the display range of the calendar instance to the current date.\n\n[⬆️ Back to the list](#instance-methods)\n\n### move\n\n- Type: `move(offset: number): void`\n- Parameters\n  - `offset` - Enter the range to move to as an integer. Nothing happens when there is no parameter.\n\nMoves the display range of the calendar instance forward or backward. A positive number moves the display range to the future based on the current range, and a negative number moves it to the past.\n\nThe moving range is different for each month/week/day depending on the monthly/weekly/daily view, and there may be detailed differences depending on the set option.\n\n```js\n// Move to last year in month view\ncalendar.move(-12);\n\n// Move back 3 days in the day view\ncalendar.move(3);\n```\n\n[⬆️ Back to the list](#instance-methods)\n\n### prev\n\n- Type: `prev(): void`\n\nMoves the display range of the calendar instance to the previous range by one unit.\n\nThe moving range is different for each month/week/day depending on the monthly/weekly/daily view, and there may be detailed differences depending on the set option.\n\n[⬆️ Back to the list](#instance-methods)\n\n### next\n\n- Type: `next(): void`\n\nMoves the display range of the calendar instance to the next range by one unit.\n\nThe moving range is different for each month/week/day depending on the monthly/weekly/daily view, and there may be detailed differences depending on the set option.\n\n[⬆️ Back to the list](#instance-methods)\n\n### setDate\n\n- Type: `setDate(date: DateType): void`\n- Parameters\n  - `date` - an object or string containing time information. You can pass a `Date` object, a string used to create a `Date` object, or a `TZDate` object.\n\nChanges the date on which the calendar instance displays the view. As a result, the display range is moved based on `date`.\n\nUnlike the `move` method, which moves the view by range, `setDate` allows you to move the view directly to a specific date.\n\n```js\n// Going to March 2022 in the monthly view (string)\ncalendar.setDate('2022-03-01');\n\n// Passing the Date object directly\ncalendar.setDate(new Date(2022, 4, 1));\n```\n\n[⬆️ Back to the list](#instance-methods)\n\n### changeView\n\n- Type: `changeView(viewName: ViewType): void`\n- Parameters\n  - `viewName` - The type of view you want to select. You can pass `'month'`, `'week'` or `'day'`.\n\nChanges the view of the calendar instance to Monthly/Weekly/Day.\n\n```js\n// Changing to monthly view\ncalendar.changeView('month');\n\n// Changing to weekly view\ncalendar.changeView('week');\n\n// Changing to daily view\ncalendar.changeView('day');\n```\n\n[⬆️ Back to the list](#instance-methods)\n\n### getElement\n\n- Type: `getElement(eventId: string, calendarId: string): HTMLElement | null`\n- Parameters\n  - `eventId` - the unique ID of the event\n  - `calendarId` - the unique ID of the calendar\n- Returns: `HTMLElement | null` - If an event is found, the HTML element of the event is returned, otherwise `null` is returned.\n\nRetrieves and returns the actual HTML element of the event that the calendar instance is rendering.\n\nIf no event is found, `null` is returned.\n\n[⬆️ Back to the list](#instance-methods)\n\n### setTheme\n\n- Type: `setTheme(theme: DeepPartial<ThemeState>): void`\n- Parameters: `theme` - object containing theme settings\n\nChanges the theme of the calendar instance. For available themes, refer to the [theme documentation](./theme.md).\n\n```js\n// Example of changing the weekend background color in the monthly view\ncalendar.setTheme({\n  month: {\n    weekend: {\n      backgroundColor: 'aliceblue',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#instance-methods)\n\n### getOptions\n\n- Type: `getOptions(): void`\n\nReturns all options for the current calendar instance.\n\n[⬆️ Back to the list](#instance-methods)\n\n### setOptions\n\n- Type: `setOptions(options: Options): void`\n- Parameters\n  - `options` - the options object used by the calendar instance\n\nChanges the options of the current calendar instance. For each option and detailed operation, refer to the [option document](./options.md).\n\n```js\n// Example of changing the primary time zone\ncalendar.setOptions({\n  timezone: {\n    zones: [\n      {\n        timezoneName: 'Europe/London',\n      },\n    ],\n  },\n});\n```\n\n[⬆️ Back to the list](#instance-methods)\n\n### getDate\n\n- Type: `getDate(): TZDate`\n- Returns: `TZDate` - object containing time information\n\nReturns information of the base date used to render the current view of the calendar instance.\n\n[⬆️ Back to the list](#instance-methods)\n\n### getDateRangeStart\n\n- Type: `getDateRangeStart(): TZDate`\n- Returns: `TZDate` - object containing time information\n\nReturns the start time of the range of dates the calendar instance is currently rendering.\n\n[⬆️ Back to the list](#instance-methods)\n\n### getDateRangeEnd\n\n- Type: `getDateRangeEnd(): TZDate`\n- Returns: `TZDate` - object containing time information\n\nReturns the end time of the range of dates the calendar instance is currently rendering.\n\n[⬆️ Back to the list](#instance-methods)\n\n### getViewName\n\n- Type: `getViewName(): ViewType`\n- Returns: `ViewType` - the type of the current calendar view. It is divided into `month`, `week`, and `day`.\n\nReturns the view type currently displayed by the calendar instance.\n\n[⬆️ Back to the list](#instance-methods)\n\n### setCalendars\n\n- Type: `setCalendars(calendars: CalendarInfo[]): void`\n- Parameters\n  - `calendars` - an array of calendar information. Calendar information has the following types. See the [calendar](./event-object.md#calendarcalendarid) documentation for details.\n\n```ts\ninterface CalendarInfo {\n  id: string;\n  name: string;\n  color?: string;\n  bgColor?: string;\n  dragBgColor?: string;\n  borderColor?: string;\n}\n```\n\nSet up one or more calendars.\n\n```js\ncalendar.setCalendars([\n  {\n    id: 'cal1',\n    name: 'Personal',\n    color: '#ffffff',\n    backgroundColor: '#9e5fff',\n    dragBackgroundColor: '#9e5fff',\n    borderColor: '#9e5fff',\n  },\n  {\n    id: 'cal2',\n    name: 'Work',\n    color: '#00a9ff',\n    backgroundColor: '#00a9ff',\n    dragBackgroundColor: '#00a9ff',\n    borderColor: '#00a9ff',\n  },\n]);\n```\n\n[⬆️ Back to the list](#instance-methods)\n\n### setCalendarVisibility\n\n- Type: `setCalendarVisibility(calendarId: string | string[], isVisible: boolean): void`\n- Parameters\n  - `calendarId` - Unique ID of the calendar to show/hide. You can pass one or several as an array.\n  - `isVisible` - A value to show or hide all events belonging to this calendar. If `true`, all events are visible, and if `false`, all events are invisible.\n\nShows or hides all events included in a specific calendar.\n\n[⬆️ Back to the list](#instance-methods)\n\n### setCalendarColor\n\n- Type: `setCalendarColor(calendarId: string, colorOptions: CalendarColor): void`\n- Parameters\n  - `calendarId` - the unique ID of the calendar\n  - `colorOptions` - The color values to apply. Please refer to the [calendar documentation](./event-object.md#calendarcalendarid) for details.\n\nChanges the color value of all events included in the specified event group.\n\n[⬆️ Back to the list](#instance-methods)\n\n### scrollToNow\n\n- Type: `scrollToNow(scrollBehavior?: 'auto' | 'smooth'): void`\n\nWhen the current time is displayed in the weekly view or the daily view, the scroll moves to the position where the current time is. In IE11, even if `'smooth'` is passed, it does not work.\n\n[⬆️ Back to the list](#instance-methods)\n\n### openFormPopup\n\n- Type: `createEvents(events: EventObject[]): void`\n- Parameters\n  - `event` - an object containing event information. For details, refer to the [EventObject document](./event-object.md).\n\nWhen the `useFormPopup` option is `true`, a popup for creating an event is displayed without going through an interaction.\n\nThe event data passed as a parameter is displayed as specified in the input field in the popup.\n\n```js\ncalendar.openFormPopup({\n  id: 'some-event-id',\n  calendarId: 'cal1',\n  title: 'Go to live concert',\n  start: '2022-05-31T09:00:00',\n  end: '2022-05-31T12:00:00',\n  category: 'time',\n});\n```\n\n[⬆️ Back to the list](#instance-methods)\n\n### clearGridSelections\n\n- Type: `clearGridSelections(): void`\n\nRemoves all date/time selection elements currently displayed in the calendar.\n\n```js\ncalendar.clearGridSelections();\n```\n\n[⬆️ Back to the list](#instance-methods)\n\n### fire\n\n- Type: `fire(eventName: string, ...args: any[]): Calendar`\n- Parameters\n  - `eventName` - the name of the event\n  - `...args` - parameters to pass to the event handler\n- Returns: current calendar instance\n\nExecutes arbitrary [instance events](#instance-events). If an event is registered, all parameters after the first parameter are passed to the registered event handler.\n\nTo register an instance event. A detailed description is provided in the [Instance Events](#instance-events) section.\n\n```js\n// Let’s assume that the following event is registered.\ncalendar.on('beforeCreateEvent', (data) => {\n  console.log(`from: ${data.start.toDateString()} to ${data.end.toDateString()}`);\n});\n\ncalendar.fire('beforeCreateEvent', {\n  start: new Date('2022-05-31'),\n  end: new Date('2022-06-01'),\n});\n// output: 'from Tue May 31 2022 to Wed Jun 01 2022'\n```\n\n[⬆️ Back to the list](#instance-methods)\n\n### off\n\n- Type: `off(eventName: string, handler?: (...args: any[]) => void): Calendar`\n- Parameters\n  - `eventName` - the name of the event\n  - `handler` - the handler function registered for the event\n- Returns: current calendar instance\n\nCancels the registered instance event. If a handler is not passed, all handlers registered for the event are unregistered.\n\nIf a handler is passed as a parameter, only that handler is unregistered.\n\nTo register an instance event. A detailed description is provided in the [Instance Events](#instance-events) section.\n\n```js\nconst someEventHandler = () => {\n  console.log('some event fired');\n};\n\ncalendar.on('some-event', someEventHandler);\n\ncalendar.fire('some-event');\n// output: 'some event fired'\n\ncalendar.off('some-event', someEventHandler);\n\ncalendar.fire('some-event'); // Nothing happens\n```\n\n[⬆️ Back to the list](#instance-methods)\n\n### on\n\n- Type: `on(eventName: string, handler: (...args: any[]) => void): Calendar`\n- Parameters\n  - `eventName` - the name of the event\n  - `handler` - the handler function registered for the event\n- Returns: current calendar instance\n\nRegisters an instance event. If the registered event name is called through the `fire` method, all handlers registered through `on` are executed.\n\nTo register an instance event. A detailed description is provided in the [Instance Events](#instance-events) section.\n\n```js\n// Registering an event\ncalendar.on('beforeCreateEvent', (data) => {\n  console.log(`from: ${data.start.toDateString()} to ${data.end.toDateString()}`);\n});\n\ncalendar.fire('beforeCreateEvent', {\n  start: new Date('2022-05-31'),\n  end: new Date('2022-06-01'),\n});\n// output: 'from Tue May 31 2022 to Wed Jun 01 2022'\n```\n\n[⬆️ Back to the list](#instance-methods)\n\n### once\n\n- Type: `once(eventName: string, handler: (...args: any[]) => void): Calendar`\n- Parameters\n  - `eventName` - the name of the event\n  - `handler` - the handler function registered for the event\n- Returns: current calendar instance\n\nRegisters an instance event. If the registered event name is called through the `fire` method, all handlers registered through `once` are executed only once.\n\nTo register an instance event. A detailed description is provided in the [Instance Events](#instance-events) section.\n\n```js\n// Registering an event once\ncalendar.once('beforeCreateEvent', (data) => {\n  console.log(`from: ${data.start.toDateString()} to ${data.end.toDateString()}`);\n});\n\ncalendar.fire('beforeCreateEvent', {\n  start: new Date('2022-05-31'),\n  end: new Date('2022-06-01'),\n});\n// output: 'from Tue May 31 2022 to Wed Jun 01 2022'\n\ncalendar.fire('beforeCreateEvent', {\n  start: new Date('2022-06-01'),\n  end: new Date('2022-06-02'),\n});\n// Nothing happens\n```\n\n[⬆️ Back to the list](#instance-methods)\n\n## Instance events\n\nAll actions of the calendar cannot be controlled by methods only. This is because you never know when a user interaction, such as a click or drag-and-drop, will occur.\n\nTOAST UI Calendar provides instance events. If necessary, you can set to receive events and execute desired actions. In addition, you can set up your own events.\n\n```js\n// Registering custom events and event handlers\ncalendar.on('myCustomEvent', (currentView) => {\n  calendar.changeView(currentView === 'week' ? 'day' : 'month');\n});\n\n// Executing custom events\ncalendar.fire('myCustomEvent', calendar.getViewName());\n```\n\n### List of instance events\n\nThe list of predefined instance events is as follows.\n\n| Event name                                            | Description                                                                                                                    |\n| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |\n| [selectDateTime](#selectdatetime)                     | Occurs when dragging and dropping a specific date or time then dropping                                                        |\n| [beforeCreateEvent](#beforecreateevent)               | Occurs when the ‘Save’ button is pressed in the default event creation/modification popup                                      |\n| [beforeUpdateEvent](#beforeupdateevent)               | Occurs when the ‘Save’ button is pressed in the default event creation/modification popup or an event is dragged and dropped   |\n| [beforeDeleteEvent](#beforedeleteevent)               | Occurs when the ‘Delete’ button is pressed in the default event details popup                                                   |\n| [afterRenderEvent](#afterrenderevent)                 | Occurs when each event is rendered                                                                                             |\n| [clickDayName](#clickdayname)                         | Occurs when a day of the week at the top of the calendar is clicked                                                            |\n| [clickEvent](#clickevent)                             | Occurs when an event is clicked                                                                                                |\n| [clickMoreEventsBtn](#clickmoreeventsbtn)             | Occurs when you click the 'More' button that appears because the number of events in each cell of the monthly view is exceeded |\n| [clickTimezoneCollapseBtn](#clicktimezonecollapsebtn) | Occurs when you click the collapse button that appears when multiple time zones are displayed in the weekly or daily view      |\n\nEach event passes specific parameters to the event handler function when it is executed.\n\n### selectDateTime\n\n- Parameters: `info: SelectDateTimeInfo` - Contains information about the selected time.\n\n```ts\ninterface SelectDateTimeInfo {\n  start: Date;\n  end: Date;\n  isAllday: boolean;\n  nativeEvent?: MouseEvent; // Native mouse event on mouse release\n  gridSelectionElements: Element[]; // List of elements corresponding to the selection area\n}\n```\n\nWhen `isReadOnly` among the calendar instance options is `false`, a specific date or time can be selected by clicking or dragging and dropping an empty space in the calendar area.\n\nAt this time, information of the selected time can be obtained through the `selectDateTime` event. And you can use the element directly through `gridSelectionElements` to calculate the position, etc.\n\n![Selecting dates in the month view](../../assets/select-date-time-1.gif)\n![Selecting time in the week/day view](../../assets/select-date-time-2.gif)\n\n[⬆️ Back to the list](#list-of-instance-events)\n\n### beforeCreateEvent\n\n- Parameters: `event: EventObject` - Information entered in the event creation popup is passed. For more information on the object to be passed, refer to the [EventObject document](./event-object.md).\n\nWhen `useFormPopup` of calendar instance options is `true`, the default event creation popup can be used.\n\nWhen the ‘Save’ button is pressed in this event creation popup, the `beforeCreateEvent` instance event occurs.\n\n```js\n// Creating an event through popup\ncalendar.on('beforeCreateEvent', (eventObj) => {\n  calendar.createEvents([\n    {\n      ...eventObj,\n      id: uuid(),\n    },\n  ]);\n});\n```\n\n[⬆️ Back to the list](#list-of-instance-events)\n\n### beforeUpdateEvent\n\n- Parameter: `updatedEventInfo: UpdatedEventInfo` - Contains existing event information and modified event information. For more information on the object to be passed, refer to the [EventObject document](./event-object.md).\n\n```ts\ninterface UpdatedEventInfo {\n  event: EventObject;\n  changes: EventObject;\n}\n```\n\nThe `event` property is information of the existing event, and the `changes` property contains only properties and values that are different from the existing event information.\n\nIn the following cases, `beforeUpdateEvent` is executed.\n\n- When `useFormPopup` and `useDetailPopup` of the calendar instance options are both `true` and the ‘Update’ button is pressed after modifying the event through the event details popup\n  - ![Event execution through popup](../../assets/before-update-event-1.gif)\n- When the `useDetailPopup` option is `true` and the `useFormPopup` is `false`, the 'Edit' button inside the event details popup is pressed. \n- When moving or resizing events by drag and drop, while `isReadOnly` is not `true` among calendar instance options and also `isReadOnly` is not `true` in the properties of individual events.\n  - ![Event execution via drag and drop](../../assets/before-update-event-2.gif)\n\n```js\n// Basic example of updating an event\ncalendar.on('beforeUpdateEvent', ({ event, change }) => {\n  calendar.updateEvent(event.id, event.calendarId, change);\n});\n```\n\n[⬆️ Back to the list](#list-of-instance-events)\n\n### beforeDeleteEvent\n\n- Parameters: `event: EventObject` - Information of the event to be deleted. For more information on the object to be passed, refer to the [EventObject document](./event-object.md).\n\nYou can select an event through the event details popup and press the ‘Delete’ button when `isReadOnly` in the calendar instance options and `isReadOnly` for individual event properties are not `true`, and also the `useDetailPopup` option is `true`.\n\nGiven that conditions above are satisfied, when the ‘Delete’ button is pressed, `beforeDeleteEvent` is executed.\n\n![Fire beforeDeleteEvent event via popup](../../assets/before-delete-event.gif)\n\n```js\n// Basic example of deleting an event\ncalendar.on('beforeDeleteEvent', (eventObj) => {\n  calendar.deleteEvent(eventObj.id, eventObj.calendarId);\n});\n```\n\n[⬆️ Back to the list](#list-of-instance-events)\n\n### afterRenderEvent\n\n- Parameters: `event: EventObject` - Information of the rendered event. For more information on the object to be passed, refer to the [EventObject document](./event-object.md).\n\nFired whenever each event is rendered within the calendar.\n\nWhenever an event is rendered due to event creation, modification, deletion, etc., **all events visible on the screen** execute `afterRenderEvent`.\n\n### clickDayName\n\n- Parameters: `dayNameInfo: DayNameInfo` - The information of the clicked date is displayed as a string in `YYYY-MM-DD` format.\n\nThe header area of the weekly/daily view displays the day of the week and date of the currently viewed time range. When a specific day of the week in this header is clicked, the `clickDayName` event is executed.\n\n**This event does not occur in the monthly view.**\n\n![Fire clickDayName event when a date is clicked](../../assets/click-dayName.gif)\n\n### clickEvent\n\n- Parameters: `eventInfo: EventInfo` - Information of the clicked event with a native `MouseEvent`.\n\n```ts\ninterface EventInfo {\n  event: EventObject;\n  nativeEvent: MouseEvent;\n}\n```\n\nFor more information about the event information, refer to the [EventObject document](./event-object.md).\n\nWhen clicking each event rendered in the calendar, a `clickEvent` event is fired.\n\n⚠️ This event is not executed when `isReadOnly` among the calendar instance options is `true` or `isReadOnly` among the properties of the event to be clicked is `true`.\n\n[⬆️ Back to the list](#list-of-instance-events)\n\n### clickMoreEventsBtn\n\n- Parameters: `moreEventsBtnInfo: MoreEventsBtnInfo` - Related information of the clicked button.\n\n```ts\ninterface MoreEventsBtnInfo {\n  date: Date; // Date of the clicked button\n  target: HTMLDivElement; // DOM element of the popup that appears when the button is clicked\n}\n```\n\nIn the monthly view, the 'more' button is displayed when there are too many events for one date and can no longer be displayed on the screen. When this button is clicked, the `clickMoreEventsBtn` event is executed.\n\n![more event button in monthly view](../../assets/click-more-events-btn.png)\n\n[⬆️ Back to the list](#list-of-instance-events)\n\n### clickTimezonesCollapseBtn\n\n- Parameters: `prevCollapsedState` - The previous collapsed state of the clicked button is passed as either `true` or `false`.\n\nIn the weekly/daily view, if two or more [timezones](./options.md#timezone) are used in the calendar instance option and the `week.showTimezoneCollapsedButton` option is `true`, a button to collapse or expand other time zones is displayed. When this button is clicked, the `clickTimezonesCollapseBtn` event is fired.\n\n![time zone collapse button](../../assets/click-timezones-collapse-btn.png)\n\n```js\n// Example changing the left side of the weekly/daily view when clicking the collapse button\ncalendar.on('clickTimezonesCollapseBtn', (prevCollapsedState) => {\n  const shouldCollapse = prevCollapsedState === false;\n\n  calendar.setOptions({\n    week: {\n      timezonesCollapsed: !prevCollapsedState,\n    },\n    theme: {\n      week: {\n        timeGridLeft: shouldCollapse ? '72px' : '150px',\n      },\n    },\n  });\n});\n```\n\n[⬆️ Back to the list](#list-of-instance-events)\n\n### Custom instance events\n\nApart from predefined events, users can register separate custom instance events as needed.\n\nWhen you use an event name not listed above and specify the event through `once` or `on` and then the `fire` method is called, the event is executed.\n\nThe methods required to set and execute the event are as follows.\n\n- [on](#on)\n- [once](#once)\n- [fire](#fire)\n- [off](#off)\n\n```js\ncalendar.on('myCustomEvent', (e) => {\n  console.log('myCustomEvent fired', e);\n});\n\ncalendar.fire('myCustomEvent', {\n  myCustomEvent: 'myCustomEvent',\n});\n```\n\n[⬆️ Back to the list](#list-of-instance-events)\n"
  },
  {
    "path": "docs/en/apis/event-object.md",
    "content": "# EventObject\n\n## Description\n\nEventObject is a pure JavaScript object containing event information. It is a value used in various APIs, such as when creating an event, when searching for a specific event, and when handling an instance event.\n\n```js\nconst calendar = new Calendar('#container');\ncalendar.createEvents([\n  {\n    id: '1',\n    calendarId: 'cal1',\n    title: 'timed event',\n    body: 'TOAST UI Calendar',\n    start: '2022-06-01T10:00:00',\n    end: '2022-06-01T11:00:00',\n    location: 'Meeting Room A',\n    attendees: ['A', 'B', 'C'],\n    category: 'time',\n    state: 'Free',\n    isReadOnly: true,\n    color: '#fff',\n    backgroundColor: '#ccc',\n    customStyle: {\n      fontStyle: 'italic',\n      fontSize: '15px',\n    },\n  }, // EventObject\n]);\n\nconst timedEvent = calendar.getEvent('1', 'cal1'); // EventObject\ncalendar.on('clickEvent', ({ event }) => {\n  console.log(event); // EventObject\n});\n```\n\n## The structure of EventObject\n\nEventObject consists of the following properties. Some items have additional explanations, and you can jump to them by clicking on the link.\n\n```ts\ninterface EventObject {\n  id?: string;\n  calendarId?: string;\n  title?: string;\n  body?: string;\n  isAllday?: boolean;\n  start?: Date | string | number | TZDate;\n  end?: Date | string | number | TZDate;\n  goingDuration?: number;\n  comingDuration?: number;\n  location?: string;\n  attendees?: string[];\n  category?: 'milestone' | 'task' | 'allday' | 'time';\n  recurrenceRule?: string;\n  state?: 'Busy' | 'Free';\n  isVisible?: boolean;\n  isPending?: boolean;\n  isFocused?: boolean;\n  isReadOnly?: boolean;\n  isPrivate?: boolean;\n  color?: string;\n  backgroundColor?: string;\n  dragBackgroundColor?: string;\n  borderColor?: string;\n  customStyle?: JSX.CSSProperties;\n  raw?: any;\n}\n```\n\n| Property                                         | Default value             | Description                                                                                                                                                                                                                                                       |\n| ------------------------------------------------ | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| id                                               | <code>''</code>           | Event ID                                                                                                                                                                                                                                                          |\n| [calendarId](#calendarcalendarid)                | <code>''</code>           | Calendar ID                                                                                                                                                                                                                                                       |\n| title                                            | <code>''</code>           | Title                                                                                                                                                                                                                                                             |\n| body                                             | <code>''</code>           | Event body content                                                                                                                                                                                                                                                |\n| [isAllday](#isallday)                            | <code>false</code>        | Whether the event is all-day or not                                                                                                                                                                                                                               |\n| start                                            | <code>new TZDate()</code> | The date and time the event starts. When creating an event, you can specify it as <code>Date</code>, <code>string</code>, <code>number</code>, or <code>TZDate</code>, and it is a <code>TZDate</code> object in some of calendar APIs parameter or return value. |\n| end                                              | <code>new TZDate()</code> | The date and time the event starts. When creating an event, you can specify it as <code>Date</code>, <code>string</code>, <code>number</code>, or <code>TZDate</code>, and it is a <code>TZDate</code> object in some of calendar APIs parameter or return value. |\n| goingDuration                                    | <code>0</code>            | The travel time taken to go to the event location. It is a number in minutes.                                                                                                                                                                                     |\n| comingDuration                                   | <code>0</code>            | The travel time taken to get back. It is a number in minutes.                                                                                                                                                                                                     |\n| location                                         | <code>''</code>           | Location of the event                                                                                                                                                                                                                                             |\n| attendees                                        | <code>[]</code>           | List of attendees                                                                                                                                                                                                                                                 |\n| [category](#category)                            | <code>'time'</code>       | Event category. One of <code>milestone</code>, <code>task</code>, <code>allday</code>, or <code>time</code>.                                                                                                                                                      |\n| dueDateClass                                     | <code>''</code>           | Category for task events. Any string is allowed.                                                                                                                                                                                                                  |\n| recurrenceRule                                   | <code>''</code>           | Event recurrence rule                                                                                                                                                                                                                                             |\n| state                                            | <code>'Busy'</code>       | Event state. One of <code>Busy</code> and <code>Free</code>.                                                                                                                                                                                                      |\n| isVisible                                        | <code>true</code>         | Whether the event is visible or not                                                                                                                                                                                                                               |\n| [isPending](#ispending-isfocused-isprivate)      | <code>false</code>        | Whether the event is pending or not                                                                                                                                                                                                                               |\n| [isFocused](#ispending-isfocused-isprivate)      | <code>false</code>        | Whether the event is focused or not                                                                                                                                                                                                                               |\n| [isReadOnly](#isreadonly)                        | <code>false</code>        | Whether the event is read-only or not                                                                                                                                                                                                                             |\n| [isPrivate](#ispending-isfocused-isprivate)      | <code>false</code>        | Whether the event is private or not                                                                                                                                                                                                                               |\n| [color](#style-related-properties)               | <code>'#000'</code>       | Text color for the event element                                                                                                                                                                                                                                  |\n| [backgroundColor](#style-related-properties)     | <code>'#a1b56c'</code>    | Background color for the event element                                                                                                                                                                                                                            |\n| [dragBackgroundColor](#style-related-properties) | <code>'#a1b56c'</code>    | Background color while dragging the event element                                                                                                                                                                                                                 |\n| [borderColor](#style-related-properties)         | <code>'#000'</code>       | Left border color of the event element                                                                                                                                                                                                                            |\n| [customStyle](#style-related-properties)         | <code>{}</code>           | The style to apply to the event element. [A JavaScript object with CSS properties in camelCase](https://reactjs.org/docs/dom-elements.html#style).                                                                                                                |\n| raw                                              | <code>null</code>         | Arbitrary data for the event. You can leverage the property for your own use cases.                                                                                                                                                                               |\n\n## Additional information\n\n### Calendar (calendarId)\n\nA calendar is an object used to group events. One event belongs to one calendar and has a unique ID, name, and color value.\n\n```ts\ninterface CalendarInfo {\n  id: string;\n  name: string;\n  color?: string;\n  backgroundColor?: string;\n  dragBackgroundColor?: string;\n  borderColor?: string;\n}\n```\n\nTo set one or more calendars, you can pass them as option values when creating an instance, or use the `setCalendars` method.\n\n```js\n// When creating an instance\nconst calendar = new Calendar('#container', {\n  calendars: [\n    {\n      id: 'cal1',\n      name: 'My Calendar',\n    },\n    {\n      id: 'cal2',\n      name: 'Another Calendar',\n    },\n  ],\n});\n\n// After creating an instance\ncalendar.setCalendars([\n  {\n    id: 'cal1',\n    name: 'My Calendar',\n  },\n  {\n    id: 'cal2',\n    name: 'Another Calendar',\n  },\n]);\n```\n\nWhen a color value is set in a calendar or a color value is set in an event, colors are applied in the following priority order.\n\n1. Event-specific color values\n2. Color values in calendar\n3. Default if neither is specified\n\n```js\ncalendar.setCalendars([\n  {\n    id: 'cal1',\n    name: 'My Calendar',\n    color: '#000',\n    backgroundColor: '#a1b56c',\n    dragBackgroundColor: '#a1b56c',\n    borderColor: '#000',\n  },\n  {\n    id: 'cal2',\n    name: 'Another Calendar',\n  },\n]);\n\ncalendar.createEvents([\n  {\n    id: '1',\n    calendarId: 'cal1',\n    title: 'Event 1',\n    isAllDay: true,\n    start: new Date('2018-08-01'),\n    end: new Date('2018-08-02'),\n    // The following three colors ignore the color value of cal1.\n    color: '#fff',\n    backgroundColor: '#3c056d',\n    dragBackgroundColor: '#3c056d',\n    // borderColor: '#a73eaf' // '#000' of cal1 is applied because it is commented out.\n  },\n  // This event belongs to cal2, but defaults to cal2 because no color is specified.\n  {\n    id: '2',\n    calendarId: 'cal2',\n    title: 'Event 2',\n    isAllDay: true,\n    start: new Date('2018-08-01'),\n    end: new Date('2018-08-02'),\n  },\n]);\n```\n\n### isAllday\n\n![isAllday](../../assets/EventObject_isAllday.png)\n\nIndicates whether the event is all-day or not.\n\nAn all-day event can be applied in several ways other than `isAllday`. If any of the cases below are true, it will appear in the All Day event panel.\n\n- When `isAllday` is `true`\n- When `category` is `allday`\n- When the event duration is more than 24 hours\n\n### category\n\n![category](../../assets/EventObject_category.png)\n\nEvents will be rendered in the corresponding panel depending on their category. Category is one of `milestone`, `task`, `allday`, or `time`.\n\n### isReadOnly\n\n![isReadOnly](../../assets/EventObject_isReadOnly.png)\n\nIndicates whether the event can be modified. If `isReadOnly` is `true`, you cannot move or resize the event, and the edit button is not exposed in the event details popup.\n\n### isPending, isFocused, isPrivate\n\n`isPending` indicates whether an event is pending or not, `isFocused` indicates whether an event is focused or not, and `isPrivate` indicates whether an event is private or not. Basically, it does not affect the rendering, and if you want to display the event differently according to these values, you can use the [template](./template.md) functionality.\n\n```js\nconst calendar = new Calendar('#container', {\n  template: {\n    time({ title, isPending, isFocused, isPrivate }) {\n      if (isPending) {\n        return `Pending: ${title}`;\n      }\n\n      if (isFocused) {\n        return `Focused: ${title}`;\n      }\n\n      if (isPrivate) {\n        return `Private: ${title}`;\n      }\n\n      return title;\n    },\n  },\n});\n```\n\nFor a detailed example, refer to the [template document](./template.md#time).\n\n### Style-related properties\n\n![style](../../assets/EventObject_style.png)\n\nYou can customize the style of event elements with `color`, `backgroundColor`, `dragBackgroundColor`, `borderColor`, and `customStyle`. `color`, `backgroundColor`, `dragBackgroundColor`, `borderColor` can be specified as a [CSS color data type](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value), and `customStyle` can be specified [as a JavaScript object with CSS properties in camelCase](https://reactjs.org/docs/dom-elements.html#style).\n"
  },
  {
    "path": "docs/en/apis/options.md",
    "content": "# Options\n\n## Description\n\nYou can customize the calendar in various ways with options. You can change options with the option object passed when creating an instance, or freely change options through the `setOptions` method.\n\n```js\n// Setting options when creating an instance\ncalendar = new Calendar('#container', {\n  defaultView: 'month',\n  isReadOnly: true,\n  // ...\n});\n\n// Changing options with the setOptions method\ncalendar.setOptions({\n  defaultView: 'week',\n  isReadOnly: false,\n  // ...\n});\n```\n\n## Option object\n\nAn option object is a nested object with the following properties. Click on the property name to go to the detailed description.\n\n| Property                            | Default value                             | Description                                                                                                   |\n| ----------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------- |\n| [defaultView](#defaultview)         | <code>'week'</code>                       | Default view type                                                                                             |\n| [useFormPopup](#useformpopup)       | <code>false</code>                        | Whether to use the built-in event creation/modification pop-up                                                |\n| [useDetailPopup](#usedetailpopup)   | <code>false</code>                        | Whether to use the built-in event details pop-up                                                              |\n| [isReadOnly](#isreadonly)           | <code>false</code>                        | Whether the entire calendar is read-only                                                                      |\n| [usageStatistics](#usagestatistics) | <code>true</code>                         | Whether to allow hostname collection for [Google Analytics (GA)](https://analytics.google.com/analytics/web/) |\n| [eventFilter](#eventfilter)         | <code>(event) => !!event.isVisible</code> | Event filter function across calendars                                                                        |\n| [week](#week)                       | <code>DEFAULT_WEEK_OPTIONS</code>         | Weekly/daily view related options                                                                             |\n| [month](#month)                     | <code>DEFAULT_MONTH_OPTIONS</code>        | Monthly view related options                                                                                  |\n| [gridSelection](#gridselection)     | <code>true</code>                         | Whether clicks and double-clicks are possible for date/time selection                                         |\n| [timezone](#timezone)               | <code>{ zones: [] }</code>                | Time zone options used by the calendar                                                                        |\n| [theme](#theme)                     | <code>DEFAULT_THEME</code>                | [Theme](./theme.md)                                                                                           |\n| [template](#template)               | <code>DEFAULT_TEMPLATE</code>             | [Template](./template.md)                                                                                     |\n| [calendars](#calendars)             | <code>[]</code>                           | List of calendars used by the calendar instance                                                               |\n\n## Usage examples\n\n### defaultView\n\n- Type: `'month' | 'week' | 'day'`\n- Default: `'week'`\n\nSpecifies the default view of the calendar. Monthly view, weekly view, and daily view can be specified and are `'month'`, `'week'`, and `'day'` respectively. The default is `'week'`.\n\n```js\nconst calendar = new Calendar('#container', {\n  defaultView: 'month',\n});\n```\n\n[⬆️ Back to the list](#option-object)\n\n### useFormPopup\n\n- Type: `boolean`\n- Default: `false`\n\nSpecifies whether to use the event form popup provided by default in the calendar. The default is `false`.\n\nWhen using the event form popup, you must import CSS files of [`tui-date-picker`](https://github.com/nhn/tui.date-picker) and [`tui-time-picker`](https://github.com/nhn/tui.time-picker) for the style to be applied properly.\n\n```sh\nnpm install tui-date-picker tui-time-picker\n```\n\n```js\n// Load css files of tui-date-picker and tui-time-picker to use the event form popup.\nimport 'tui-date-picker/dist/tui-date-picker.css';\nimport 'tui-time-picker/dist/tui-time-picker.css';\n\ncalendar.setOptions({\n  useFormPopup: true,\n  useDetailPopup: true,\n});\n```\n\n[⬆️ Back to the list](#option-object)\n\n### useDetailPopup\n\n- Type: `boolean`\n- Default: `false`\n\nSpecifies whether to use the event details popup provided by default in the calendar. The default is `false`.\n\n```js\nconst calendar = new Calendar('#container', {\n  useDetailPopup: true,\n});\n```\n\n[⬆️ Back to the list](#option-object)\n\n### isReadOnly\n\n- Type: `boolean`\n- Default: `false`\n\nSpecifies whether to make the calendar read-only. The default value is `false`. If set to `true`, users cannot create or edit events in the calendar.\n\n```js\nconst calendar = new Calendar('#container', {\n  isReadOnly: true,\n});\n```\n\n[⬆️ Back to the list](#option-object)\n\n### usageStatistics\n\n- Type: `boolean`\n- Default: `true`\n\nSpecifies whether to allow hostname collection for [Google Analytics (GA)](https://analytics.google.com/analytics/web/). The default value is `true`. If set to `false`, statistics are not collected.\n\n[TOAST UI Calendar](https://github.com/nhn/tui.calendar) applies [GA](https://analytics.google.com/analytics/web/) to collect statistics on open source usage to see how widely it is used around the world. This serves as an important indicator to determine the future progress of the project. It collects `location.hostname` (e.g. \"ui.toast.com\") and is only used to measure usage statistics.\n\n```js\nconst calendar = new Calendar('#container', {\n  usageStatistics: false,\n});\n```\n\n[⬆️ Back to the list](#option-object)\n\n### eventFilter\n\n- Type: `(event: EventObject) => boolean`\n- Default: `(event) => !!event.isVisible`\n\nSpecifies the event filtering conditions of the calendar. The default value is `(event) => !!event.isVisible`, which renders only events for which the `isVisible` property of the event is `true`.\n\nIf `isVisible` is not filtered when applying a custom event filter, events with `isVisible: false` may appear in the calendar.\n\n```js\nconst calendar = new Calendar('#container', {\n  eventFilter: (event) => event.isVisible && event.isAllday,\n});\n```\n\n[⬆️ Back to the list](#option-object)\n\n### week\n\n- Type: `WeekOptions`\n- Default: `DEFAULT_WEEK_OPTIONS`\n\n```ts\ntype EventView = 'allday' | 'time';\ntype TaskView = 'milestone' | 'task';\ninterface CollapseDuplicateEvents {\n  getDuplicateEvents: (targetEvent: EventObject, events: EventObject[]) => EventObject[];\n  getMainEvent: (events: EventObject[]) => EventObject;\n};\n\ninterface WeekOptions {\n  startDayOfWeek?: number;\n  dayNames?: [string, string, string, string, string, string, string];\n  narrowWeekend?: boolean;\n  workweek?: boolean;\n  showNowIndicator?: boolean;\n  showTimezoneCollapseButton?: boolean;\n  timezonesCollapsed?: boolean;\n  hourStart?: number;\n  hourEnd?: number;\n  eventView?: boolean | EventView[];\n  taskView?: boolean | TaskView[];\n  collapseDuplicateEvents?: boolean | CollapseDuplicateEvents;\n}\n```\n\n```js\nconst DEFAULT_WEEK_OPTIONS = {\n  startDayOfWeek: 0,\n  dayNames: [],\n  narrowWeekend: false,\n  workweek: false,\n  showNowIndicator: true,\n  showTimezoneCollapseButton: false,\n  timezonesCollapsed: false,\n  hourStart: 0,\n  hourEnd: 24,\n  eventView: true,\n  taskView: true,\n  collapseDuplicateEvents: false,\n};\n```\n\nSpecifies options related to weekly/daily views.\n\n[⬆️ Back to the list](#option-object)\n\n#### week.startDayOfWeek\n\n- Type: `number`\n- Default: `0`\n\nSpecifies the start day of the week in the daily/weekly view. The default is `0`, starting from Sunday. You can specify a value from `0` (Sunday) to `6` (Saturday).\n\n| Value | Day of the week |\n| ----- | --------------- |\n| 0     | Sunday          |\n| 1     | Monday          |\n| 2     | Tuesday         |\n| 3     | Wednesday       |\n| 4     | Thursday        |\n| 5     | Friday          |\n| 6     | Saturday        |\n\n```js\ncalendar.setOptions({\n  week: {\n    startDayOfWeek: 1,\n  },\n});\n```\n\n| Default                                                                             | Example                                                                            |\n| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |\n| ![week-startDayOfWeek-default](../../assets/options_week-startDayOfWeek-before.png) | ![week-startDayOfWeek-example](../../assets/options_week-startDayOfWeek-after.png) |\n\n[⬆️ Back to the list](#week)\n\n#### week.dayNames\n\n- Type: `[string, string, string, string, string, string, string]`\n- Default: `[]`\n\nYou can change the name of the day of the week in the daily/weekly view. The default value is `[]`, starting from the day of the week set by [startDayOfWeek](#week.startDayOfWeek).\n\nWhen giving this option, an array with all days of the week from Sunday to Monday must be entered. The index of each day is the same as the result of `Date.prototype.getDay`. ([Reference](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-week-day))\n\n```js\ncalendar.setOptions({\n  week: {\n    dayNames: ['월', '화', '수', '목', '금', '토', '일'],\n  },\n});\n```\n\n| Default                                                                 | Example                                                                |\n| ----------------------------------------------------------------------- | ---------------------------------------------------------------------- |\n| ![week-daynames-default](../../assets/options_week-dayNames-before.png) | ![week-daynames-example](../../assets/options_week-dayNames-after.png) |\n\n[⬆️ Back to the list](#week)\n\n#### week.narrowWeekend\n\n- Type: `boolean`\n- Default: `false`\n\nIn the daily/weekly view, the width of the weekend can be narrowed (1/2 of the existing width). The default value is `false`. In order to narrow the width of the weekend, set it to `true`.\n\n```js\ncalendar.setOptions({\n  week: {\n    narrowWeekend: true,\n  },\n});\n```\n\n| Default                                                                           | Example                                                                          |\n| --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |\n| ![week-narrowWeekend-default](../../assets/options_week-narrowWeekend-before.png) | ![week-narrowWeekend-example](../../assets/options_week-narrowWeekend-after.png) |\n\n[⬆️ Back to the list](#week)\n\n#### week.workweek\n\n- Type: `boolean`\n- Default: `false`\n\nWeekends can be excluded from the daily/weekly view. The default value is `false`, and to exclude weekends, set it to `true`.\n\n```js\ncalendar.setOptions({\n  week: {\n    workweek: true,\n  },\n});\n```\n\n| Default                                                                 | Example                                                                |\n| ----------------------------------------------------------------------- | ---------------------------------------------------------------------- |\n| ![week-workweek-default](../../assets/options_week-workweek-before.png) | ![week-workweek-example](../../assets/options_week-workweek-after.png) |\n\n[⬆️ Back to the list](#week)\n\n#### week.showNowIndicator\n\n- Type: `boolean`\n- Default: `true`\n\nYou can specify whether to display the current time indicator in the weekly/daily view. The default value is `true`, and if you do not want to display the current time indicator, set it to `false`.\n\n```js\ncalendar.setOptions({\n  week: {\n    showNowIndicator: false,\n  },\n});\n```\n\n| Default                                                                                 | Example                                                                                |\n| --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |\n| ![week-showNowIndicator-default](../../assets/options_week-showNowIndicator-before.png) | ![week-showNowIndicator-example](../../assets/options_week-showNowIndicator-after.png) |\n\n#### week.showTimezoneCollapseButton\n\n- Type: `boolean`\n- Default: `false`\n\nWhen using multiple time zones in the weekly/daily view, you can specify whether to display the button to collapse the sub time zones. The default value is `false`, and set it to `true` to display the collapse button.\n\n```js\ncalendar.setOptions({\n  week: {\n    showTimezoneCollapseButton: true,\n  },\n});\n```\n\n| Default                                                                                                     | Example                                                                                                    |\n| ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |\n| ![week-showTimezoneCollapseButton-default](../../assets/options_week-showTimezoneCollapseButton-before.png) | ![week-showTimezoneCollapseButton-example](../../assets/options_week-showTimezoneCollapseButton-after.png) |\n\n[⬆️ Back to the list](#week)\n\n#### week.timezonesCollapsed\n\n- Type: `boolean`\n- Default: `false`\n\nWhen using multiple time zones in the weekly/daily view, you can specify whether to display the sub time zones in a collapsed state. The default value is `false`, and set it to `true` to display in the collapsed state.\n\n```js\ncalendar.setOptions({\n  week: {\n    timezonesCollapsed: true,\n  },\n});\n```\n\n| Default                                                                                     | Example                                                                                    |\n| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |\n| ![week-timezonesCollapsed-default](../../assets/options_week-timezonesCollapsed-before.png) | ![week-timezonesCollapsed-example](../../assets/options_week-timezonesCollapsed-after.png) |\n\n[⬆️ Back to the list](#week)\n\n#### week.hourStart\n\n- Type: `number`\n- Default: `0`\n\nSpecifies the start time of each column in the weekly/daily view. The default value is `0`, and you can specify any desired start time.\n\n```js\ncalendar.setOptions({\n  week: {\n    hourStart: 9,\n  },\n});\n```\n\n| Default                                                                   | Example                                                                  |\n| ------------------------------------------------------------------------- | ------------------------------------------------------------------------ |\n| ![week-hourStart-default](../../assets/options_week-hourStart-before.png) | ![week-hourStart-example](../../assets/options_week-hourStart-after.png) |\n\n[⬆️ Back to the list](#week)\n\n#### week.hourEnd\n\n- Type: `number`\n- Default: `24`\n\nSpecifies the end time of each column in the weekly/daily view. The default value is `24`, and you can specify any desired end time.\n\n```js\ncalendar.setOptions({\n  week: {\n    hourEnd: 18,\n  },\n});\n```\n\n| Default                                                               | Example                                                              |\n| --------------------------------------------------------------------- | -------------------------------------------------------------------- |\n| ![week-hourEnd-default](../../assets/options_week-hourEnd-before.png) | ![week-hourEnd-example](../../assets/options_week-hourEnd-after.png) |\n\n[⬆️ Back to the list](#week)\n\n#### week.eventView\n\n- Type: `boolean | ('allday' | 'time')[]`\n- Default: `true`\n\nYou can specify whether to display the allday panel and the time panel in the weekly/daily view. The default is `true`, and both the allday panel and the time panel are displayed. If `false`, both panels are not displayed, and in case of `['allday']`, only the allday panel is displayed. In case of `['time']`, only the time panel is displayed.\n\n```js\ncalendar.setOptions({\n  week: {\n    eventView: false,\n  },\n});\n```\n\n| Default                                                                   | Example                                                                  |\n| ------------------------------------------------------------------------- | ------------------------------------------------------------------------ |\n| ![week-eventView-default](../../assets/options_week-eventView-before.png) | ![week-eventView-example](../../assets/options_week-eventView-after.png) |\n\n[⬆️ Back to the list](#week)\n\n#### week.taskView\n\n- Type: `boolean | ('milestone' | 'task')[]`\n- Default: `true`\n\nYou can specify whether to display the milestone panel and the task panel in the weekly/daily view. The default value is `true`, and both the milestone panel and the task panel are displayed. If `false`, both panels are not displayed, and in case of `['milestone']`, only the milestone panel is displayed. In case of `['task']`, only the task panel is displayed.\n\n```js\ncalendar.setOptions({\n  week: {\n    taskView: false,\n  },\n});\n```\n\n| Default                                                                 | Example                                                                |\n| ----------------------------------------------------------------------- | ---------------------------------------------------------------------- |\n| ![week-taskView-default](../../assets/options_week-taskView-before.png) | ![week-taskView-example](../../assets/options_week-taskView-after.png) |\n\n[⬆️ Back to the list](#week)\n\n#### week.collapseDuplicateEvents\n\n- Type: `boolean | CollapseDuplicateEventsOptions`\n- Default: `false`\n\n```ts\ninterface CollapseDuplicateEventsOptions {\n  getDuplicateEvents: (targetEvent: EventObject, events: EventObject[]) => EventObject[];\n  getMainEvent: (events: EventObject[]) => EventObject;\n};\n```\n\nYou can collapse duplicate events in the daily/weekly view. The default value is `false`. The calendar handles duplicate events in the same way as normal events. When it is `true`, **events with the same `title`, `start`, and `end`** are classified as duplicate events, and **the last event** among them is expanded during initial rendering. If you want to filter duplicate events based on your requirements, set `getDuplicateEvents`. And if you want to choose which event is expanded during initial rendering, set `getMainEvent`.\n\n`getDuplicateEvents` should **return sorted duplicate events in the order you want them to appear**. The return value of `getDuplicateEvents` is the parameter of `getMainEvent`.\n\n```js\ncalendar.setOptions({\n  week: {\n    collapseDuplicateEvents: {\n      getDuplicateEvents: (targetEvent, events) =>\n        events\n          .filter((event) =>\n            event.title === targetEvent.title &&\n            event.start.getTime() === targetEvent.start.getTime() &&\n            event.end.getTime() === targetEvent.end.getTime()\n          )\n          .sort((a, b) => (a.calendarId > b.calendarId ? 1 : -1)),\n      getMainEvent: (events) => events[events.length - 1], // events are the return value of getDuplicateEvents()\n    }\n  },\n});\n```\n\n| Default                                                             | Example                                                              |\n| ----------------------------------------------------------------------- | ---------------------------------------------------------------------- |\n| ![week-collapseDuplicateEvents-default](../../assets/options_week-collapseDuplicateEvents-before.png) | ![week-collapseDuplicateEvents-example](../../assets/options_week-collapseDuplicateEvents-after.png) |\n\n[⬆ Back to the list](#week)\n\n### month\n\n- Type: `MonthOptions`\n- Default: `DEFAULT_MONTH_OPTIONS`\n\n```ts\ninterface MonthOptions {\n  dayNames?: [string, string, string, string, string, string, string];\n  startDayOfWeek?: number;\n  narrowWeekend?: boolean;\n  visibleWeeksCount?: number;\n  isAlways6Weeks?: boolean;\n  workweek?: boolean;\n  visibleEventCount?: number;\n}\n```\n\n```js\nconst DEFAULT_MONTH_OPTIONS = {\n  dayNames: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'],\n  visibleWeeksCount: 0,\n  workweek: false,\n  narrowWeekend: false,\n  startDayOfWeek: 0,\n  isAlways6Weeks: true,\n  visibleEventCount: 6,\n};\n```\n\nSpecifies options related to the monthly view.\n\n[⬆️ Back to the list](#option-object)\n\n#### month.dayNames\n\n- Type: `[string, string, string, string, string, string, string]`\n- Default: `['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']`\n\nYou can change the name of the day of the week in the monthly view. The default value is `['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']`, starting from the day of the week set by [startDayOfWeek](#month.startDayOfWeek).\n\nWhen giving this option, an array with all days of the week from Sunday to Monday must be entered. The index of each day is the same as the result of `Date.prototype.getDay`. ([Reference](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-week-day))\n\n| Value | Day of the week |\n| ----- | --------------- |\n| 0     | Sunday          |\n| 1     | Monday          |\n| 2     | Tuesday         |\n| 3     | Wednesday       |\n| 4     | Thursday        |\n| 5     | Friday          |\n| 6     | Saturday        |\n\n```js\ncalendar.setOptions({\n  month: {\n    dayNames: ['일', '월', '화', '수', '목', '금', '토'],\n  },\n});\n```\n\n| Default                                                                   | Example                                                                  |\n| ------------------------------------------------------------------------- | ------------------------------------------------------------------------ |\n| ![month-daynames-default](../../assets/options_month-dayNames-before.png) | ![month-daynames-example](../../assets/options_month-dayNames-after.png) |\n\n[⬆️ Back to the list](#month)\n\n#### month.startDayOfWeek\n\n- Type: `number`\n- Default: `0`\n\nSpecifies the start day of the week in the monthly view. The default is `0`, starting from Sunday. You can specify a value from `0` (Sunday) to `6` (Saturday).\n\nThe index of each day of the week is the same as the result of `Date.prototype.getDay`. ([Reference](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-week-day))\n\n| Value | Day of the week |\n| ----- | --------------- |\n| 0     | Sunday          |\n| 1     | Monday          |\n| 2     | Tuesday         |\n| 3     | Wednesday       |\n| 4     | Thursday        |\n| 5     | Friday          |\n| 6     | Saturday        |\n\n```js\ncalendar.setOptions({\n  month: {\n    startDayOfWeek: 1,\n  },\n});\n```\n\n| Default                                                                               | Example                                                                              |\n| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |\n| ![month-startDayOfWeek-default](../../assets/options_month-startDayOfWeek-before.png) | ![month-startDayOfWeek-example](../../assets/options_month-startDayOfWeek-after.png) |\n\n[⬆️ Back to the list](#month)\n\n#### month.narrowWeekend\n\n- Type: `boolean`\n- Default: `false`\n\nYou can narrow the width of weekends (1/2 of the existing width) in the monthly view. The default value is `false`, and to narrow the width of the weekend, set it to `true`.\n\n```js\ncalendar.setOptions({\n  month: {\n    narrowWeekend: true,\n  },\n});\n```\n\n| Default                                                                             | Example                                                                            |\n| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |\n| ![month-narrowWeekend-default](../../assets/options_month-narrowWeekend-before.png) | ![month-narrowWeekend-example](../../assets/options_month-narrowWeekend-after.png) |\n\n[⬆️ Back to the list](#month)\n\n#### month.visibleWeeksCount\n\n- Type: `number`\n- Default: `0`\n\nSpecifies the number of weeks shown in the monthly view. The default value is `0`, indicating 6 weeks. You can specify a value from `1` to `6` to specify a different number of weeks.\n\n⚠️ If you set this option, the current date will always be in the first week.\n\n```js\ncalendar.setOptions({\n  month: {\n    visibleWeeksCount: 2,\n  },\n});\n```\n\n| Default                                                                                     | Example                                                                                    |\n| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |\n| ![month-visibleWeeksCount-default](../../assets/options_month-visibleWeeksCount-before.png) | ![month-visibleWeeksCount-example](../../assets/options_month-visibleWeeksCount-after.png) |\n\n[⬆️ Back to the list](#month)\n\n#### month.isAlways6Weeks\n\n- Type: `boolean`\n- Default: `true`\n\nDetermines whether to always display the calendar every six weeks in the monthly view. The default value is `true`, and 6 weeks are displayed regardless of the total number of weeks in the month being displayed.\n\nIf set to `false`, 4 to 6 weeks are displayed according to the number of displayable weeks in the month.\n\n```js\ncalendar.setOptions({\n  month: {\n    isAlways6Weeks: false,\n  },\n});\n```\n\n| Default                                                                               | Example                                                                              |\n| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |\n| ![month-isAlways6Weeks-default](../../assets/options_month-isAlways6Weeks-before.png) | ![month-isAlways6Weeks-example](../../assets/options_month-isAlways6Weeks-after.png) |\n\n[⬆️ Back to the list](#month)\n\n#### month.workweek\n\n- Type: `boolean`\n- Default: `false`\n\nWeekends can be excluded from the monthly view. The default value is `false`, and to exclude weekends, set it to `true`.\n\n```js\ncalendar.setOptions({\n  month: {\n    workweek: true,\n  },\n});\n```\n\n| Default                                                                   | Example                                                                  |\n| ------------------------------------------------------------------------- | ------------------------------------------------------------------------ |\n| ![month-workweek-default](../../assets/options_month-workweek-before.png) | ![month-workweek-example](../../assets/options_month-workweek-after.png) |\n\n[⬆️ Back to the list](#month)\n\n#### month.visibleEventCount\n\n- Type: `number`\n- Default: `6`\n\nSpecifies the maximum number of events displayed for each date in the monthly view. The default is `6`.\n\nEven though you set this option, if the height of the date is insufficient, the option is automatically ignored.\n\nIt is affected by the entire calendar area and [the gridCell property of the month theme](./theme.md#month-gridcell).\n\n```js\ncalendar.setOptions({\n  month: {\n    visibleEventCount: 2,\n  },\n});\n```\n\n| Default                                                                                     | Example                                                                                    |\n| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |\n| ![month-visibleEventCount-default](../../assets/options_month-visibleEventCount-before.png) | ![month-visibleEventCount-example](../../assets/options_month-visibleEventCount-after.png) |\n\n[⬆️ Back to the list](#month)\n\n### gridSelection\n\n- Type: `boolean | GridSelectionOptions`\n- Default: `true`\n\n```ts\ninterface GridSelectionOptions {\n  enableDblClick?: boolean;\n  enableClick?: boolean;\n}\n```\n\nSpecifies whether clicks and double-clicks are possible when selecting the date/time of the calendar. The default is `true`. When it is specified as a `boolean` type of `true` or `false`, both click and double click are enabled or disabled. You can also specify click and double click respectively with the object type like `{ enableDblClick: boolean;`  `enableClick: boolean }`.\n\n```js\nconst calendar = new Calendar('#container', {\n  gridSelection: {\n    enableDblClick: false,\n    enableClick: true,\n  },\n});\n```\n\n[⬆️ Back to the list](#option-object)\n\n### timezone\n\n- Type: `TimezoneOptions`\n- Default: `{ zones: [] }`\n\n⚠️ To use the time zone function, it needs a modern browser environment that supports the `Intl.DateTimeFormat` API as well as the IANA time zone database.\n⚠️ If you need to support Internet Explorer 11, you need to apply a polyfill or use the `customOffsetCalculator` option with a separate library.\n\n- [Support range (caniuse)](https://caniuse.com/mdn-javascript_builtins_date_tolocaletimestring_iana_time_zone_names)\n- [`Intl.DateTimeFormat polyfill`](https://formatjs.io/docs/polyfills/intl-datetimeformat/)\n\n```ts\ninterface TimezoneConfig {\n  timezoneName: string;\n  displayLabel?: string;\n  tooltip?: string;\n}\n\ninterface TimezoneOptions {\n  zones?: TimezoneConfig[];\n  customOffsetCalculator?: (timezoneName: string, timestamp: number) => number;\n}\n```\n\n#### Default time zone setting\n\nYou can set the time zones used in the calendar. The default is `{ zones: [] }`. `zones` is an array of time zone information. For the time zone information, you can specify with the name of the time zone (`timezoneName`) and the display label (`displayLabel`) and tooltip (`tooltip`) used in the weekly/daily view.\n\nIf there are more than one time zone information in the time zone information array, the calendar sets the first element of the array as the default time zone.\n\n```js\n// Setting the default time zone to London regardless of the time zone of the system the browser is running\nconst calendar = new Calendar('#container', {\n  timezone: {\n    zones: [\n      {\n        timezoneName: 'Europe/London',\n      },\n    ],\n  },\n});\n```\n\nIf two or more time zones are set and the weekly/daily view is displayed, the display label and tooltip are displayed on the left side. It doesn't affect anything else.\n\n![Set multiple time zones](../../assets/options_timezone-multiple-timezone.png)\n\n#### Using user-defined time zone offset calculator\n\nThe `customOffsetCalculator` must calculate the offset difference between the given time zone and UTC in a user-defined method and return it in minutes. For example, if the time zone name is `'Asia/Seoul'`, it is `UTC +9`, so `540` must be returned.\n\nIf the `customOffsetCalculator` is defined, the calendar uses this calculator for all logic that calculates the time zone. Otherwise, there is no need to define this option except for special cases because the calendar uses the [`Intl.DateTimeFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) by default.\n\nIt is recommended not to use the `customOffsetCalculator` option unless there are special cases such as using a separate library to support older browsers.\n\n```js\n// Calculating the offset with moment timezone\nfunction momentTZCalculator(timezoneName, timestamp) {\n  return moment.tz(timezoneName).utcOffset(timestamp);\n}\n\n// Calculating the offset with Luxon\nluxonTZCalculator(timezoneName, timestamp) {\n  return DateTime.fromMillis(timestamp).setZone(timezoneName).offset;\n}\n\n// Calculating the offset with date-fns-tz\nfunction dateFnsTZCalculator(timezoneName, timestamp) {\n  return getTimezoneOffset(timezoneName, new Date(timestamp));\n}\n\n// ...\n\nconst calendar = new Calendar('#container', {\n  timezone: {\n    zones: [\n      {\n        timezoneName: 'Asia/Seoul',\n        displayLabel: 'Seoul',\n        tooltip: 'Seoul Time',\n      },\n      {\n        timezoneName: 'Asia/Tokyo',\n        displayLabel: 'Tokyo',\n        tooltip: 'Tokyo Time',\n      },\n    ],\n    customOffsetCalculator: momentTZCalculator, // or luxonTZCalculator, dateFnsTZCalculator\n  },\n});\n```\n\n[⬆️ Back to the list](#option-object)\n\n### theme\n\n- Type: `ThemeObject`\n- Default: `DEFAULT_THEME`\n\nSpecifies the theme of the calendar. It can be applied when creating a calendar instance or can be changed with the `setTheme` method instead. More information can be found in the [theme documentation](./theme.md).\n\n[⬆️ Back to the list](#option-object)\n\n### template\n\n- Type: `TemplateObject`\n- Default: `DEFAULT_TEMPLATE`\n\nSpecifies the calendar template. More details can be found in the [Templates documentation](./template.md).\n\n[⬆️ Back to the list](#option-object)\n\n### calendars\n\n- Type: `CalendarInfo[]`\n- Default: `[]`\n\nSpecifies the list of calendars used in the calendar. Each calendar information in the calendar list can have the `id`, calendar name, and color information of the corresponding calendar. The default is `[]`. For a detailed description of calendar information, refer to the [EventObject document](./event-object.md#calendarcalendarid).\n\n```ts\ninterface CalendarInfo {\n  id: string;\n  name: string;\n  color?: string;\n  backgroundColor?: string;\n  dragBackgroundColor?: string;\n  borderColor?: string;\n}\n```\n\n[⬆️ Back to the list](#option-object)\n"
  },
  {
    "path": "docs/en/apis/template.md",
    "content": "# Template\n\n## Description\n\nTemplate is a feature that support custom rendering. When creating a calendar instance, custom rendering can be done with template options, and template options can be changed with `setOptions`.\n\n```js\nconst calendar = new Calendar('#container', {\n  template: {\n    milestone(event) {\n      return `<span style=\"color: red;\">${event.title}</span>`;\n    },\n  },\n});\n\ncalendar.setOptions({\n  template: {\n    milestone(event) {\n      return `<span style=\"color: blue;\">${event.title}</span>`;\n    },\n  },\n});\n```\n\n## Template list\n\nEach property of the template is a function that returns a VNode of `preact` or a string, and the parameters vary depending on the type of template. Below is the full list of templates.\n\n| Template name                                             | Parameters                       | Description                                                                                                |\n| --------------------------------------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------- |\n| [milestone](#milestone)                                   | [EventObject](./event-object.md) | Milestone events in weekly/daily view                                                                      |\n| [milestoneTitle](#milestonetitle)                         | None                             | The left area of the milestone panel in the weekly/daily view                                              |\n| [task](#task)                                             | [EventObject](./event-object.md) | Task events in weekly/daily view                                                                           |\n| [taskTitle](#tasktitle)                                   | None                             | The left area of the task panel in the weekly/daily view                                                   |\n| [allday](#allday)                                         | [EventObject](./event-object.md) | All day events in weekly/daily view                                                                        |\n| [alldayTitle](#alldaytitle)                               | None                             | The left area of the allday panel in the weekly/daily view                                                 |\n| [time](#time)                                             | [EventObject](./event-object.md) | Timed events in weekly/daily view                                                                          |\n| [goingDuration](#goingduration)                           | [EventObject](./event-object.md) | Travel time to a certain location of timed event in weekly/daily view                                      |\n| [comingDuration](#comingduration)                         | [EventObject](./event-object.md) | Return time of timed event of weekly/daily view                                                            |\n| [monthMoreTitleDate](#monthmoretitledate)                 | TemplateMoreTitleDate            | Title date of the ‘more events’ popup of monthly view                                                      |\n| [monthMoreClose](#monthmoreclose)                         | None                             | Close button of the ‘more events’ popup of monthly view                                                    |\n| [monthGridHeader](#monthgridheader)                       | TemplateMonthGrid                | Header area of cell in monthly view                                                                        |\n| [monthGridHeaderExceed](#monthgridheaderexceed)           | <code>number</code>              | A component that displays the number of exceeding events in the header area of a cell of the monthly view. |\n| [monthGridFooter](#monthgridfooter)                       | TemplateMonthGrid                | Footer area of cell in monthly view                                                                        |\n| [monthGridFooterExceed](#monthgridfooterexceed)           | <code>number</code>              | A component that displays the number of exceeding events in the footer area of a cell of the monthly view. |\n| [monthDayName](#monthdayname)                             | TemplateMonthDayName             | Day of the week names in the monthly view                                                                  |\n| [weekDayName](#weekdayname)                               | TemplateWeekDayName              | Day of the week names in the weekly view                                                                   |\n| [weekGridFooterExceed](#weekgridfooterexceed)             | <code>number</code>              | A component displaying exceeded events in allday panel in weekly/daily view                                |\n| [collapseBtnTitle](#collapsebtntitle)                     | None                             | Collapse button component of allday panel in weekly/daily view                                             |\n| [timezoneDisplayLabel](#timezonedisplaylabel)             | TemplateTimezone                 | Label of time zones in weekly/daily view                                                                   |\n| [timegridDisplayPrimaryTime](#timegriddisplayprimarytime) | TemplateNow                      | Hours of primary time zone in weekly/daily view                                                            |\n| [timegridDisplayTime](#timegriddisplaytime)               | TemplateNow                      | Hours of time zones other than the primary time zone of the weekly/daily view                              |\n| [timegridNowIndicatorLabel](#timegridnowindicatorlabel)   | TemplateNow                      | Current time in weekly/daily view                                                                          |\n| [popupIsAllday](#popupisallday)                           | None                             | Text of ‘All day’ in event form popup                                                                      |\n| [popupStateFree](#popupstatefree)                         | None                             | Text of ‘Free’ status in event form popup                                                                  |\n| [popupStateBusy](#popupstatebusy)                         | None                             | Text of ‘Busy’ status in event form popup                                                                  |\n| [titlePlaceholder](#titleplaceholder)                     | None                             | Event name placeholder in event form popup                                                                 |\n| [locationPlaceholder](#locationplaceholder)               | None                             | Event location placeholder in event form popup                                                             |\n| [startDatePlaceholder](#startdateplaceholder)             | None                             | Event start date placeholder in event form popup                                                           |\n| [endDatePlaceholder](#enddateplaceholder)                 | None                             | Event end date placeholder in event form popup                                                             |\n| [popupSave](#popupsave)                                   | None                             | Text of the save button in event form popup                                                                |\n| [popupUpdate](#popupupdate)                               | None                             | Text of the update button in event form popup                                                              |\n| [popupEdit](#popupedit)                                   | None                             | Text of the edit button in event details popup                                                             |\n| [popupDelete](#popupdelete)                               | None                             | Text of the delete button in event details popup                                                           |\n| [popupDetailTitle](#popupdetailtitle)                     | [EventObject](./event-object.md) | Event title in the event details popup                                                                     |\n| [popupDetailDate](#popupdetaildate)                       | [EventObject](./event-object.md) | Duration of the event in the event details popup                                                           |\n| [popupDetailLocation](#popupdetaillocation)               | [EventObject](./event-object.md) | Location of the event in the event details popup                                                           |\n| [popupDetailAttendees](#popupdetailattendees)             | [EventObject](./event-object.md) | Attendees of the event in the event details popup                                                          |\n| [popupDetailState](#popupdetailstate)                     | [EventObject](./event-object.md) | State of the event in the event details popup                                                              |\n| [popupDetailRecurrenceRule](#popupdetailrecurrencerule)   | [EventObject](./event-object.md) | Recurrence rule of the event in the event details popup                                                    |\n| [popupDetailBody](#popupdetailbody)                       | [EventObject](./event-object.md) | Event details of the event in the event details popup                                                      |\n\n## Usage examples\n\n### milestone panel\n\n![milestone](../../assets/template_milestone.png)\n\n#### milestone\n\nYou can customize the milestone event of the weekly/daily view by using the [`EventObject`](./event-object.md) parameter.\n\n```js\ncalendar.setOptions({\n  template: {\n    milestone(event) {\n      return `<span style:\"color: blue;\">${event.title}</span>`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### milestoneTitle\n\nYou can customize the area on the left side of the milestone panel in the weekly/daily view.\n\n```js\ncalendar.setOptions({\n  template: {\n    milestoneTitle() {\n      return `<span>Milestone events</span>`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n### task panel\n\n![task](../../assets/template_task.png)\n\n#### task\n\nYou can customize the task events of the weekly/daily view using the [`EventObject`](./event-object.md) parameter.\n\n```js\ncalendar.setOptions({\n  template: {\n    task(event) {\n      return `<span style=\"color: red;\">${event.title}</span>`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### taskTitle\n\nYou can customize the area on the left side of the task panel in the weekly/daily view.\n\n```js\ncalendar.setOptions({\n  template: {\n    taskTitle() {\n      return `<span>Task events</span>`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n### allday panel\n\n![allday](../../assets/template_allday.png)\n\n#### allday\n\nYou can customize the allday event of the weekly/daily view by using the [`EventObject`](./event-object.md) parameter.\n\n```js\ncalendar.setOptions({\n  template: {\n    allday(event) {\n      return `<span style=\"color: green;\">${event.title}</span>`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### alldayTitle\n\nYou can customize the area on the left side of the allday panel in the weekly/daily view.\n\n```js\ncalendar.setOptions({\n  template: {\n    allday() {\n      return `<span>Allday events</span>`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n### timed events\n\n![time](../../assets/template_timed.png)\n\n#### time\n\nYou can customize the timed event of the weekly/daily view by using the [`EventObject`](./event-object.md) parameter. This part excludes travel time and return time.\n\n```js\ncalendar.setOptions({\n  template: {\n    time(event) {\n      return `<span style=\"color: black;\">${event.title}</span>`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### goingDuration\n\nBy using the [`EventObject`](./event-object.md) parameter, you can customize the travel time from the weekly/daily view to a certain location of the timed event.\n\n```js\ncalendar.setOptions({\n  template: {\n    goingDuration(event) {\n      return `<span>${event.goingDuration}</span>`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### comingDuration\n\nBy using the [`EventObject`](./event-object.md) parameter, you can customize the travel time from the weekly/daily view to a certain location of the timed event.\n\n```js\ncalendar.setOptions({\n  template: {\n    comingDuration(event) {\n      return `<span>${event.comingDuration}</span>`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n### ‘More events’ popup of monthly view\n\n![more-events-popup](../../assets/template_moreEventsPopup.png)\n\n#### monthMoreTitleDate\n\n```ts\ninterface TemplateMoreTitleDate {\n  ymd: string; // `YYYY-MM-DD` string format data of the date\n  date: number; // Date number of the date\n  day: number; // Day of the week for the date\n}\n```\n\nYou can customize the date of the Show More popup.\n\n```js\ncalendar.setOptions({\n  template: {\n    monthMoreTitleDate(moreTitle) {\n      const { date } = moreTitle;\n\n      return `<span>${date}</span>`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### monthMoreClose\n\nYou can customize the close button for the Show More popup. By default, the close button is not displayed.\n\n```js\ncalendar.setOptions({\n  template: {\n    monthMoreClose() {\n      return '';\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n### Header and Footer of monthly view\n\n![grid-header-footer](../../assets/template_gridHeaderFooter.png)\n\n```ts\ninterface TemplateMonthGrid {\n  date: string; // day of the date\n  day: number; // day of the week on that date\n  hiddenEventCount: number; // number of events not displayed\n  isOtherMonth: boolean; // Whether the date is in a different month from the current month view\n  isToday: boolean; // Whether it is today's date\n  month: number; // the month number\n  ymd: string; // `YYYY-MM-DD` string format data of the date\n}\n```\n\n#### monthGridHeader\n\nThe header area of the monthly view cell can be customized. It receives the `TemplateMonthGrid` object as a parameter.\n\n```js\ncalendar.setOptions({\n  template: {\n    monthGridHeader(model) {\n      const date = parseInt(model.date.split('-')[2], 10);\n\n      return `<span>${date}</span>`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### monthGridHeaderExceed\n\nYou can customize the component that displays the number of events exceeding in the header area of the monthly view cell. Receives the number of events exceeded as a parameter.\n\n```js\ncalendar.setOptions({\n  template: {\n    monthGridHeaderExceed(hiddenEvents) {\n      return `<span>${hiddenEvents} more</span>`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### monthGridFooter\n\nThe footer area of the monthly view cell can be customized. It receives the `TemplateMonthGrid` object as a parameter. By default, nothing is displayed.\n\n```js\ncalendar.setOptions({\n  template: {\n    monthGridFooter() {\n      return '';\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### monthGridFooterExceed\n\nYou can customize the component that displays the number of events exceeding in the footer area of the monthly view cell. Receives the number of events exceeded as a parameter. By default, nothing is displayed.\n\n```js\ncalendar.setOptions({\n  template: {\n    monthGridFooterExceed() {\n      return '';\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n### Day names\n\n#### monthDayName\n\n![month-dayname](../../assets/template_monthDayName.png)\n\n```ts\ninterface TemplateMonthDayName {\n  day: number; // The day of the week for that date\n  label: string; // Basic English abbreviation string for the day of the week\n}\n```\n\nYou can customize the day name of the week in the monthly view.\n\n```js\ncalendar.setOptions({\n  template: {\n    monthDayName(model) {\n      return model.label;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### weekDayName\n\n![week-dayname](../../assets/template_weekDayName.png)\n\n```ts\ninterface TemplateWeekDayName {\n  date: number; // day of the week\n  day: number; // The day of the week\n  dayName: string; // Basic English abbreviation string for the day of the week\n  isToday: boolean; // Whether the day of the week is today\n  renderDate: string; // Base date of weekly/daily view rendering\n  dateInstance: TZDate; // `Date` object for the day of the week\n}\n```\n\nYou can customize the day name of the week in the weekly/daily view.\n\n```js\ncalendar.setOptions({\n  template: {\n    weekDayName(model) {\n      return `<span>${model.date}</span>&nbsp;&nbsp;<span>${model.dayName}</span>`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n### weekGridFooterExceed\n\n![week-exceed](../../assets/template_weekExceed.png)\n\nYou can customize the component that displays exceeding events in the allday panel of the weekly/daily view. Receives the number of events exceeded as a parameter.\n\n```js\ncalendar.setOptions({\n  template: {\n    weekGridFooterExceed(hiddenEvents) {\n      return `+${hiddenEvents}`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n### collapseBtnTitle\n\n![collapse-btn](../../assets/template_collapseBtn.png)\n\nYou can customize the collapse button component of the weekly/daily view.\n\n```js\ncalendar.setOptions({\n  template: {\n    collapseBtnTitle() {\n      return `<span>↑</span>`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n### timezoneDisplayLabel\n\n![timezone-display](../../assets/template_timezoneDisplay.png)\n\nYou can customize the label of the time zone in the weekly/daily view that uses two or more time zones.\n\n```js\ncalendar.setOptions({\n  template: {\n    timezoneDisplayLabel({ timezoneOffset }) {\n      const sign = timezoneOffset < 0 ? '-' : '+';\n      const hours = Math.abs(timezoneOffset / 60);\n      const minutes = Math.abs(timezoneOffset % 60);\n\n      return `GMT${sign}${hours}:${minutes}`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n### Display Time\n\n![timegrid-time](../../assets/template_timegridTime.png)\n\n```ts\ntype TimeUnit = 'second' | 'minute' | 'hour' | 'date' | 'month' | 'year';\n\ninterface TemplateNow {\n  unit: TimeUnit; // Unit of time\n  time: TZDate; // the time\n  format: string; // format of the time\n}\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### timegridDisplayPrimaryTime\n\nYou can customize the displayed time of the primary time zone.\n\n```js\ncalendar.setOptions({\n  template: {\n    timegridDisplayPrimaryTime({ time }) {\n      return `primary timezone: ${time}`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### timegridDisplayTime\n\nYou can customize the displayed time of time zones, except for the primary time zone.\n\n```js\ncalendar.setOptions({\n  template: {\n    timegridDisplayTime({ time }) {\n      return `sub timezone: ${time}`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### timegridNowIndicatorLabel\n\nYou can customize the current time text displayed on the current time indicator.\n\n```js\ncalendar.setOptions({\n  template: {\n    timegridNowIndicatorLabel({ time }) {\n      return `current time: ${time}`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n### Event form popup\n\n![popup-create](../../assets/template_popupCreate.png)\n\n#### popupIsAllday\n\nYou can customize the ‘All day’ text in the event form popup.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupIsAllday() {\n      return 'All day';\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### popupStateFree\n\nYou can customize the ‘Free’ state of the event in the event form popup.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupStateFree() {\n      return 'Free';\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### popupStateBusy\n\nYou can customize the ‘Busy’ state of the event in the event form popup.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupStateBusy() {\n      return 'Busy';\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### titlePlaceholder\n\nYou can customize the placeholder for the event title in the event form popup. It must return a string.\n\n```js\ncalendar.setOptions({\n  template: {\n    titlePlaceholder() {\n      return 'Title';\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### locationPlaceholder\n\nYou can customize the placeholder for the event location in the event form popup. It must return a string.\n\n```js\ncalendar.setOptions({\n  template: {\n    locationPlaceholder() {\n      return 'Location';\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### startDatePlaceholder\n\nYou can customize the start date placeholder for the event in the event form popup. It must return a string.\n\n```js\ncalendar.setOptions({\n  template: {\n    startDatePlaceholder() {\n      return 'Start date';\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### endDatePlaceholder\n\nYou can customize the end date placeholder for the event in the event form popup. It must return a string.\n\n```js\ncalendar.setOptions({\n  template: {\n    endDatePlaceholder() {\n      return 'End date';\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### popupSave\n\nYou can customize the text of the save button in the event form popup.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupSave() {\n      return 'Add';\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n### Event edit popup\n\n![popup-edit](../../assets/template_popupEdit.png)\n\n#### popupUpdate\n\nYou can customize the text of the update button in the event form popup.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupUpdate() {\n      return 'Update';\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n### Event details popup\n\n![popup-detail](../../assets/template_popupDetail.png)\n\n#### popupEdit\n\nYou can customize the text of the edit button in the event details popup.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupEdit() {\n      return 'Edit';\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### popupDelete\n\nYou can customize the text of the delete button in the event details popup.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupDelete() {\n      return 'Delete';\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### popupDetailTitle\n\nYou can customize the event title in the event details popup.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupDetailTitle({ title }) {\n      return title;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### popupDetailDate\n\nYou can customize the duration of the event in the event details popup.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupDetailDate({ start, end }) {\n      return `${start.toString()} - ${end.toString()}`;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### popupDetailLocation\n\nYou can customize the location of the event in the event details popup.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupDetailLocation({ location }) {\n      return location;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### popupDetailAttendees\n\nYou can customize the attendees of the event in the event details popup.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupDetailAttendees({ attendees = [] }) {\n      return attendees.join(', ');\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### popupDetailState\n\nYou can customize the state of the event in the event details popup.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupDetailState({ state }) {\n      return state || 'Busy';\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### popupDetailRecurrenceRule\n\nYou can customize the event recurrence rule in the event details popup.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupDetailRecurrenceRule({ recurrenceRule }) {\n      return recurrenceRule;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n\n#### popupDetailBody\n\nYou can customize the contents of the event in the event details popup.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupDetailBody({ body }) {\n      return body;\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#template-list)\n"
  },
  {
    "path": "docs/en/apis/theme.md",
    "content": "# Theme\n\n## Description\n\nYou can apply the theme you want by changing the color and background color. You can freely change the theme through the `setTheme` method.\n\n```js\n// Setting the theme while creating an instance\nconst calendar = new Calendar('#container', {\n  theme: {\n    week: {\n      today: {\n        color: 'blue',\n      },\n    },\n  },\n});\n\n// Change the theme of an instance with the setTheme method\ncalendar.setTheme({\n  week: {\n    today: {\n      color: 'red',\n    },\n  },\n});\n```\n\n## Theme Object\n\nThe theme object is a nested object divided into three parts: `common` for whole application, `week` for weekly/daily view, and `month` for monthly view. All values are CSS string values to the corresponding properties.\n\n```ts\ninterface ThemeObject {\n  common: CommonTheme;\n  week: WeekTheme;\n  month: MonthTheme;\n}\n```\n\n### Common Theme\n\n```ts\ninterface CommonTheme {\n  backgroundColor: string;\n  border: string;\n  gridSelection: {\n    backgroundColor: string;\n    border: string;\n  };\n  dayName: { color: string };\n  holiday: { color: string };\n  saturday: { color: string };\n  today: { color: string };\n}\n```\n\n| Theme                                      | Default value                       | Description                  |\n| ------------------------------------------ | ----------------------------------- | ---------------------------- |\n| [backgroundColor](#common-backgroundcolor) | <code>'white'</code>                | Background color of calendar |\n| [border](#common-border)                   | <code>'1px solid #e5e5e5'</code>    | Border of calendar           |\n| [gridSelection](#common-gridselection)     | <code>DEFAULT_GRID_SELECTION</code> | Selected date/time area      |\n| [dayName](#common-dayname)                 | <code>{ color: '#333' }</code>      | Day of the week              |\n| [holiday](#common-holiday)                 | <code>{ color: '#ff4040' }</code>   | Holiday                      |\n| [saturday](#common-saturday)               | <code>{ color: '#333' }</code>      | Saturday                     |\n| [today](#common-today)                     | <code>{ color: '#fff' }</code>      | The current day              |\n\n```ts\nconst DEFAULT_GRID_SELECTION = {\n  backgroundColor: 'rgba(81, 92, 230, 0.05)',\n  border: '1px solid #515ce6',\n};\n```\n\n### Week Theme\n\n```ts\ninterface WeekTheme {\n  dayName: {\n    borderLeft: string;\n    borderTop: string;\n    borderBottom: string;\n    backgroundColor: string;\n  };\n  dayGrid: {\n    borderRight: string;\n    backgroundColor: string;\n  };\n  dayGridLeft: {\n    borderRight: string;\n    backgroundColor: string;\n    width: string;\n  };\n  timeGrid: { borderRight: string };\n  timeGridLeft: {\n    borderRight: string;\n    backgroundColor: string;\n    width: string;\n  };\n  timeGridLeftAdditionalTimezone: { backgroundColor: string };\n  timeGridHalfHour: { borderBottom: string };\n  nowIndicatorLabel: { color: string };\n  nowIndicatorPast: { border: string };\n  nowIndicatorBullet: { backgroundColor: string };\n  nowIndicatorToday: { border: string };\n  nowIndicatorFuture: { border: string };\n  pastTime: { color: string };\n  futureTime: { color: string };\n  weekend: { backgroundColor: string };\n  today: { color: string; backgroundColor: string };\n  pastDay: { color: string };\n  panelResizer: { border: string };\n  gridSelection: { color: string };\n}\n```\n\n| Theme                                                                  | Default value                                      | Description                                                                                                               |\n| ---------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |\n| [dayName](#week-dayname)                                               | <code>DEFAULT_WEEK_DAYNAME</code>                  | Day of the week                                                                                                           |\n| [dayGrid](#week-daygrid)                                               | <code>DEFAULT_DAY_GRID</code>                      | Each cell in the panel in weekly/daily view                                                                               |\n| [dayGridLeft](#week-daygridleft)                                       | <code>DEFAULT_DAY_GRID_LEFT</code>                 | In the weekly/daily view, the area on the left side of the panel                                                          |\n| [timeGrid](#week-timegrid)                                             | <code>{ borderRight: '1px solid #e5e5e5' }</code>  | Timed event area in weekly/daily view                                                                                     |\n| [timeGridLeft](#week-timegridleft)                                     | <code>DEFAULT_TIME_GRID_LEFT</code>                | The left side of the timed event area in the weekly/daily view                                                            |\n| [timeGridLeftAdditionalTimezone](#week-timegridleftadditionaltimezone) | <code>{ backgroundColor: 'white' }</code>          | Sub-time zone displayed on the left side of the timed event area in the weekly/daily view                                 |\n| [timeGridHalfHourLine](#week-timegridhalfhourline)                     | <code>{ borderBottom: '1px solid #e5e5e5' }</code> | In the weekly/daily view, dividing line of every 30 minutes of an hour in the timed event area.                           |\n| [timeGridHourLine](#week-timegridhourline)                             | <code>{ borderBottom: '1px solid #e5e5e5' }</code> | In the weekly/daily view, dividing line of every hour in the timed event area.                                            |\n| [nowIndicatorLabel](#week-nowindicatorlabel)                           | <code>{ color: '#515ce6' }</code>                  | Current time text displayed on the current time indicator                                                                 |\n| [nowIndicatorPast](#week-nowindicatorpast)                             | <code>{ border: '1px dashed #515ce6' }</code>      | The line representing past of the current time indicator                                                                  |\n| [nowIndicatorBullet](#week-nowindicatorbullet)                         | <code>{ backgroundColor: '#515ce6' }</code>        | The dot representing today’s column of the current time indicator                                                         |\n| [nowIndicatorToday](#week-nowindicatortoday)                           | <code>{ border: '1px solid #515ce6' }</code>       | The line representing today of the current time indicator                                                                 |\n| [nowIndicatorFuture](#week-nowindicatorfuture)                         | <code>{ border: 'none' }</code>                    | The line representing future of the current time indicator                                                                |\n| [pastTime](#week-pasttime)                                             | <code>{ color: '#bbb' }</code>                     | The past time displayed on the left side of the timed event area in the weekly/daily view                                 |\n| [futureTime](#week-futuretime)                                         | <code>{ color: '#333' }</code>                     | Future time displayed on the left side of the timed event area in the weekly/daily view                                   |\n| [weekend](#week-weekend)                                               | <code>{ backgroundColor: 'inherit' }</code>        | Weekend column in timed event area in weekly/daily view                                                                   |\n| [today](#week-today)                                                   | <code>DEFAULT_TODAY</code>                         | Today column of timed event area in weekly/daily view (color is applied to dayName, backgroundColor is applied to column) |\n| [pastDay](#week-pastday)                                               | <code>{ color: '#bbb' }</code>                     | Past days in weekly/daily view                                                                                            |\n| [panelResizer](#week-panelresizer)                                     | <code>{ border: '1px solid #e5e5e5' }</code>       | Panel resizing component                                                                                                  |\n| [gridSelection](#week-gridselection)                                   | <code>{ color: '#515ce6' }</code>                  | Selected date/time in weekly/daily view                                                                                   |\n\n```ts\nconst DEFAULT_WEEK_DAYNAME = {\n  borderLeft: 'none',\n  borderTop: '1px solid #e5e5e5',\n  borderBottom: '1px solid #e5e5e5',\n  backgroundColor: 'inherit',\n};\n\nconst DEFAULT_DAY_GRID = {\n  borderRight: '1px solid #e5e5e5',\n  backgroundColor: 'inherit',\n};\n\nconst DEFAULT_DAY_GRID_LEFT = {\n  borderRight: '1px solid #e5e5e5',\n  backgroundColor: 'inherit',\n  width: '72px',\n};\n\nconst DEFAULT_TIME_GRID_LEFT = {\n  backgroundColor: 'inherit',\n  borderRight: '1px solid #e5e5e5',\n  width: '72px',\n};\n\nconst DEFAULT_TODAY = {\n  color: 'inherit',\n  backgroundColor: 'rgba(81, 92, 230, 0.05)',\n};\n```\n\n### Month Theme\n\n```ts\ninterface MonthTheme {\n  dayExceptThisMonth: { color: string };\n  dayName: {\n    borderLeft: string;\n    backgroundColor: string;\n  };\n  holidayExceptThisMonth: { color: string };\n  moreView: {\n    backgroundColor: string;\n    border: string;\n    boxShadow: string;\n    width: number | null,\n    height: number | null,\n  };\n  moreViewTitle: {\n    backgroundColor: string;\n  };\n  weekend: { backgroundColor: string };\n  gridCell: {\n    headerHeight: number | null;\n    footerHeight: number | null;\n  };\n}\n```\n\n| Theme                                                   | Default value                                         | Description                                           |\n| ------------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- |\n| [dayExceptThisMonth](#month-dayexceptthismonth)         | <code>{ color: 'rgba(51, 51, 51, 0.4)' }</code>       | Days except this month                                |\n| [holidayExceptThisMonth](#month-holidayexceptthismonth) | <code>{ color: 'rgba(255, 64, 64, 0.4)' }</code>      | Holidays except this month                            |\n| [dayName](#month-dayname)                               | <code>DEFAULT_MONTH_DAYNAME</code>                    | Day of the week                                       |\n| [moreView](#month-moreview)                             | <code>DEFAULT_MORE_VIEW</code>                        | ‘More events’ popup of monthly view                   |\n| [moreViewTitle](#month-moreviewtitle)                   | <code>{ backgroundColor: 'inherit' }</code>           | Header area of ‘more events’ popup of monthly view    |\n| [weekend](#month-weekend)                               | <code>{ backgroundColor: 'inherit' }</code>           | Weekend cell in monthly view                          |\n| [gridCell](#month-gridcell)                             | <code>{ headerHeight: 31, footerHeight: null }</code> | Header and footer height of all cells in monthly view |\n\n```ts\nconst DEFAULT_MONTH_DAYNAME = {\n  borderLeft: 'none',\n  backgroundColor: 'inherit',\n};\n\nconst DEFAULT_MORE_VIEW = {\n  border: '1px solid #d5d5d5',\n  boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.1)',\n  backgroundColor: 'white',\n  width: null,\n  height: null,\n};\n```\n\n## Usage examples\n\n### common\n\n#### common-backgroundColor\n\nSpecifies the background color. The default value is `'white'`.\n\n```js\ncalendar.setTheme({\n  common: {\n    backgroundColor: 'black',\n  },\n});\n```\n\n[⬆️ Back to the list](#common-theme)\n\n#### common-border\n\nSpecifies the border. The default value is `'1px solid #e5e5e5'`.\n\n```js\ncalendar.setTheme({\n  common: {\n    border: '1px dotted #e5e5e5',\n  },\n});\n```\n\n[⬆️ Back to the list](#common-theme)\n\n#### common-gridSelection\n\nSpecifies the background color and border of the date/time selection. The default value is `'rgba(81, 92, 230, 0.05)'` for `backgroundColor` and `'1px solid #515ce6'` for `border`.\n\n| Default                                                                       | Example                                                                      |\n| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |\n| ![common-gridSelection-default](../../assets/common-gridSelection-before.png) | ![common-gridSelection-example](../../assets/common-gridSelection-after.png) |\n\n```js\ncalendar.setTheme({\n  common: {\n    gridSelection: {\n      backgroundColor: 'rgba(81, 230, 92, 0.05)',\n      border: '1px dotted #515ce6',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#common-theme)\n\n#### common-dayName\n\nSpecifies the color of the day of the week. The default value is `'#333'`.\n\n| Default                                                           | Example                                                          |\n| ----------------------------------------------------------------- | ---------------------------------------------------------------- |\n| ![common-dayname-default](../../assets/common-dayName-before.png) | ![common-dayname-example](../../assets/common-dayName-after.png) |\n\n```js\ncalendar.setTheme({\n  common: {\n    dayName: {\n      color: '#515ce6',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#common-theme)\n\n#### common-holiday\n\nSpecifies the holiday color. The default value is `'#ff4040'`.\n\n| Default                                                           | Example                                                          |\n| ----------------------------------------------------------------- | ---------------------------------------------------------------- |\n| ![common-holiday-default](../../assets/common-holiday-before.png) | ![common-holiday-example](../../assets/common-holiday-after.png) |\n\n```js\ncalendar.setTheme({\n  common: {\n    holiday: {\n      color: 'rgba(255, 64, 64, 0.5)',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#common-theme)\n\n#### common-saturday\n\nSpecifies the color of Saturday. The default value is `'#333'`.\n\n| Default                                                             | Example                                                            |\n| ------------------------------------------------------------------- | ------------------------------------------------------------------ |\n| ![common-saturday-default](../../assets/common-saturday-before.png) | ![common-saturday-example](../../assets/common-saturday-after.png) |\n\n```js\ncalendar.setTheme({\n  common: {\n    saturday: {\n      color: 'rgba(64, 64, 255, 0.5)',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#common-theme)\n\n#### common-today\n\nSpecifies the color of today. The default value is `'#fff'`.\n\n| Default                                                       | Example                                                      |\n| ------------------------------------------------------------- | ------------------------------------------------------------ |\n| ![common-today-default](../../assets/common-today-before.png) | ![common-today-example](../../assets/common-today-after.png) |\n\n```js\ncalendar.setTheme({\n  common: {\n    today: {\n      color: 'grey',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#common-theme)\n\n### week\n\n#### week-dayName\n\nSpecifies the day of the week/daily view. You can specify the left, top, and bottom border and `background` colors with `borderLeft`, `borderTop`, `borderBottom`, and `backgroundColor`. The default values are `'none'`, `'1px solid #e5e5e5'`, `'1px solid #e5e5e5'`, and `'inherit'`.\n\n| Default                                                       | Example                                                      |\n| ------------------------------------------------------------- | ------------------------------------------------------------ |\n| ![week-dayname-default](../../assets/week-dayName-before.png) | ![week-dayname-example](../../assets/week-dayName-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    dayName: {\n      borderLeft: 'none',\n      borderTop: '1px dotted red',\n      borderBottom: '1px dotted red',\n      backgroundColor: 'rgba(81, 92, 230, 0.05)',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#week-theme)\n\n#### week-dayGrid\n\nSpecifies the cell of each panel of the weekly/daily view. You can specify the right border and background color with `borderRight` and `backgroundColor`, and the default values are `'1px solid #e5e5e5'` and `'inherit'`. When the background color is changed, the background color of the columns except for weekends will be changed.\n\n| Default                                                       | Example                                                      |\n| ------------------------------------------------------------- | ------------------------------------------------------------ |\n| ![week-dayGrid-default](../../assets/week-dayGrid-before.png) | ![week-dayGrid-example](../../assets/week-dayGrid-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    dayGrid: {\n      borderRight: 'none',\n      backgroundColor: 'rgba(81, 92, 230, 0.05)',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#week-theme)\n\n#### week-dayGridLeft\n\nSpecifies the left area of each panel in the weekly/daily view. You can specify the right border, background color, and width with `borderRight`, and width, and the default values are `'1px solid #e5e5e5'` and `'inherit'`, and `'72px'`.\n\n| Default                                                               | Example                                                              |\n| --------------------------------------------------------------------- | -------------------------------------------------------------------- |\n| ![week-dayGridLeft-default](../../assets/week-dayGridLeft-before.png) | ![week-dayGridLeft-example](../../assets/week-dayGridLeft-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    dayGridLeft: {\n      borderRight: 'none',\n      backgroundColor: 'rgba(81, 92, 230, 0.05)',\n      width: '144px',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#week-theme)\n\n#### week-timeGrid\n\nSpecifies the timed event area in the weekly/daily view. You can specify the right border with `borderRight` and the default value is `'1px solid #e5e5e5'`.\n\n| Default                                                         | Example                                                        |\n| --------------------------------------------------------------- | -------------------------------------------------------------- |\n| ![week-timeGrid-default](../../assets/week-timeGrid-before.png) | ![week-timeGrid-example](../../assets/week-timeGrid-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    timeGrid: {\n      borderRight: '1px solid #e5e5e5',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#week-theme)\n\n#### week-timeGridLeft\n\nSpecifies the left side of the timed event area in the weekly/daily view. You can specify the right border, background color, and width with `borderRight`, `backgroundColor`, and `width`. The default values are `'1px solid #e5e5e5'`, `'inherit'`, and `'72px'`.\n\n| Default                                                                 | Example                                                                |\n| ----------------------------------------------------------------------- | ---------------------------------------------------------------------- |\n| ![week-timeGridLeft-default](../../assets/week-timeGridLeft-before.png) | ![week-timeGridLeft-example](../../assets/week-timeGridLeft-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    timeGridLeft: {\n      borderRight: 'none',\n      backgroundColor: 'rgba(81, 92, 230, 0.05)',\n      width: '144px',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#week-theme)\n\n#### week-timeGridLeftAdditionalTimezone\n\nSpecifies sub-time zones displayed in the left area of the timed event area in the weekly/daily view. The background color can be specified with `backgroundColor`, and the default value is `'white'`.\n\n| Default                                                                                                     | Example                                                                                                    |\n| ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |\n| ![week-timeGridLeftAdditionalTimezone-default](../../assets/week-timeGridLeftAdditionalTimezone-before.png) | ![week-timeGridLeftAdditionalTimezone-example](../../assets/week-timeGridLeftAdditionalTimezone-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    timeGridLeftAdditionalTimezone: {\n      backgroundColor: '#e5e5e5',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#week-theme)\n\n#### week-timeGridHalfHourLine\n\nIn the weekly/daily view, specifies the dividing line of every 30 minutes of an hour in the timed event area. You can specify the bottom border with `borderBottom`, and the default value is `'none'`.\n\n| Default                                                                                 | Example                                                                                |\n| --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |\n| ![week-timeGridHalfHourLine-default](../../assets/week-timeGridHalfHourLine-before.png) | ![week-timeGridHalfHourLine-example](../../assets/week-timeGridHalfHourLine-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    timeGridHalfHourLine: {\n      borderBottom: '1px dotted #e5e5e5',\n    },\n  },\n});\n```\n\n#### week-timeGridHourLine\n\nIn the weekly/daily view, specifies dividing line of every hour in the timed event area. You can specify the bottom border with `borderBottom`, and the default value is `'none'`.\n\n| Default                                                                         | Example                                                                        |\n| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |\n| ![week-timeGridHourLine-default](../../assets/week-timeGridHourLine-before.png) | ![week-timeGridHourLine-example](../../assets/week-timeGridHourLine-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    timeGridHourLine: {\n      borderBottom: '1px solid #f9f9f9',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#week-theme)\n\n#### week-nowIndicatorLabel\n\nSpecifies the current time text displayed on the current time indicator. You can specify the text color with `color`, and the default value is `'#515ce6'`.\n\n| Default                                                                           | Example                                                                          |\n| --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |\n| ![week-nowIndicatorLabel-default](../../assets/week-nowIndicatorLabel-before.png) | ![week-nowIndicatorLabel-example](../../assets/week-nowIndicatorLabel-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    nowIndicatorLabel: {\n      color: 'red',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#week-theme)\n\n#### week-nowIndicatorPast\n\nSpecifies a line representing past of the current time indicator. You can specify the border of the line with `border`, and the default value is `'1px dashed #515ce6'`.\n\n| Default                                                                         | Example                                                                        |\n| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |\n| ![week-nowIndicatorPast-default](../../assets/week-nowIndicatorPast-before.png) | ![week-nowIndicatorPast-example](../../assets/week-nowIndicatorPast-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    nowIndicatorPast: {\n      border: '1px dashed red',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#week-theme)\n\n#### week-nowIndicatorBullet\n\nSpecifies the point displayed for today's date on the current time indicator. The background color can be specified with `backgroundColor`, and the default value is `'#515ce6'`.\n\n| Default                                                                             | Example                                                                            |\n| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |\n| ![week-nowIndicatorBullet-default](../../assets/week-nowIndicatorBullet-before.png) | ![week-nowIndicatorBullet-example](../../assets/week-nowIndicatorBullet-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    nowIndicatorBullet: {\n      backgroundColor: '#515ce6',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#week-theme)\n\n#### week-nowIndicatorToday\n\nSpecifies the line representing today in the current time indicator. You can specify the border of the line with `border`, and the default value is `'1px solid #515ce6'`.\n\n| Default                                                                           | Example                                                                          |\n| --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |\n| ![week-nowIndicatorToday-default](../../assets/week-nowIndicatorToday-before.png) | ![week-nowIndicatorToday-example](../../assets/week-nowIndicatorToday-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    nowIndicatorToday: {\n      border: '1px solid red',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#week-theme)\n\n#### week-nowIndicatorFuture\n\nSpecifies a line representing future from the current time indicator. You can specify the border of the line with `border`, and the default value is `'none'`.\n\n| Default                                                                             | Example                                                                            |\n| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |\n| ![week-nowIndicatorFuture-default](../../assets/week-nowIndicatorFuture-before.png) | ![week-nowIndicatorFuture-example](../../assets/week-nowIndicatorFuture-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    nowIndicatorFuture: {\n      border: '1px solid red',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#week-theme)\n\n#### week-pastTime\n\nSpecifies the past time displayed in the left area of the timed event area in the weekly/daily view. You can specify the text color with `color`, and the default value is `'#bbb'`.\n\n| Default                                                         | Example                                                        |\n| --------------------------------------------------------------- | -------------------------------------------------------------- |\n| ![week-pastTime-default](../../assets/week-pastTime-before.png) | ![week-pastTime-example](../../assets/week-pastTime-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    pastTime: {\n      color: 'red',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#week-theme)\n\n#### week-futureTime\n\nSpecifies the future time displayed in the left area of the timed event area in the weekly/daily view. You can specify the text color with `color`, and the default value is `'#333'`.\n\n| Default                                                             | Example                                                            |\n| ------------------------------------------------------------------- | ------------------------------------------------------------------ |\n| ![week-futureTime-default](../../assets/week-futureTime-before.png) | ![week-futureTime-example](../../assets/week-futureTime-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    futureTime: {\n      color: 'red',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#week-theme)\n\n#### week-weekend\n\nSpecifies weekend columns of the timed event area in the weekly/daily view. The background color can be specified with `backgroundColor`, and the default value is `'inherit'`.\n\n| Default                                                       | Example                                                      |\n| ------------------------------------------------------------- | ------------------------------------------------------------ |\n| ![week-weekend-default](../../assets/week-weekend-before.png) | ![week-weekend-example](../../assets/week-weekend-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    weekend: {\n      backgroundColor: 'rgba(255, 64, 64, 0.05)',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#week-theme)\n\n#### week-today\n\nSpecifies today's column of timed event area in weekly/daily view. You can specify text color with `color` and background color with `backgroundColor`. The default values are `'inherit'` and `'rgba(81, 92, 230, 0.05)'`. `color` is applied to the day of the week and `backgroundColor` is applied to the column.\n\n| Default                                                   | Example                                                  |\n| --------------------------------------------------------- | -------------------------------------------------------- |\n| ![week-today-default](../../assets/week-today-before.png) | ![week-today-example](../../assets/week-today-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    today: {\n      color: '#e5e5e5',\n      backgroundColor: 'rgba(229, 229, 229, 0.05)',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#week-theme)\n\n#### week-pastDay\n\nSpecify the past day in the weekly/daily view. You can specify the text color with `color`, and the default value is `'#bbb'`.\n\n| Default                                                       | Example                                                      |\n| ------------------------------------------------------------- | ------------------------------------------------------------ |\n| ![week-pastDay-default](../../assets/week-pastDay-before.png) | ![week-pastDay-example](../../assets/week-pastDay-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    pastDay: {\n      color: 'grey',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#week-theme)\n\n#### week-panelResizer\n\nSpecifies the panel resizing component. You can specify a border with `border`, and the default value is `'1px solid #e5e5e5'`.\n\n| Default                                                                 | Example                                                                |\n| ----------------------------------------------------------------------- | ---------------------------------------------------------------------- |\n| ![week-panelResizer-default](../../assets/week-panelResizer-before.png) | ![week-panelResizer-example](../../assets/week-panelResizer-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    panelResizer: {\n      border: '1px dotted #e5e5e5',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#week-theme)\n\n#### week-gridSelection\n\nSpecifies the date/time selection in the weekly/daily view. You can specify the text color with `color`, and the default value is `'#515ce6'`.\n\n| Default                                                                   | Example                                                                  |\n| ------------------------------------------------------------------------- | ------------------------------------------------------------------------ |\n| ![week-gridSelection-default](../../assets/week-gridSelection-before.png) | ![week-gridSelection-example](../../assets/week-gridSelection-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    gridSelection: {\n      color: 'grey',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#week-theme)\n\n### month\n\n#### month-dayExceptThisMonth\n\nSpecifies a different month from the current month. You can specify the text color with `color`, and the default value is `'rgba(51, 51, 51, 0.4)'`.\n\n| Default                                                                               | Example                                                                              |\n| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |\n| ![month-dayExceptThisMonth-default](../../assets/month-dayExceptThisMonth-before.png) | ![month-dayExceptThisMonth-example](../../assets/month-dayExceptThisMonth-after.png) |\n\n```js\ncalendar.setTheme({\n  month: {\n    dayExceptThisMonth: {\n      color: 'grey',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#month-theme)\n\n#### month-holidayExceptThisMonth\n\nSpecifies holiday that is in different months from the current month. You can specify the text color with `color`, and the default value is `'rgba(255, 64, 64, 0.4)'`.\n\n| Default                                                                                       | Example                                                                                      |\n| --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |\n| ![month-holidayExceptThisMonth-default](../../assets/month-holidayExceptThisMonth-before.png) | ![month-holidayExceptThisMonth-example](../../assets/month-holidayExceptThisMonth-after.png) |\n\n```js\ncalendar.setTheme({\n  month: {\n    holidayExceptThisMonth: {\n      color: 'blue',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#month-theme)\n\n#### month-dayName\n\nSpecify the day of the week. You can specify the left border and background color with `borderLeft` and `backgroundColor`, and the default values are `'none'` and `'inherit'` respectively.\n\n| Default                                                         | Example                                                        |\n| --------------------------------------------------------------- | -------------------------------------------------------------- |\n| ![month-dayname-default](../../assets/month-dayName-before.png) | ![month-dayname-example](../../assets/month-dayName-after.png) |\n\n```js\ncalendar.setTheme({\n  month: {\n    dayName: {\n      borderLeft: 'none',\n      backgroundColor: 'rgba(51, 51, 51, 0.4)',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#month-theme)\n\n#### month-moreView\n\nSpecifies the ‘more events’ pop-up of the monthly view. You can specify border, shadow, and background color with `border`, `boxShadow`, and `backgroundColor`, and the default values are `'1px solid #d5e5e5'`, `'0 2px 6px 0 rgba(0, 0, 0, 0.1)'`, `'white'`.\n\nYou can also set the size of the popup by specifying `width` and `height` values. The size of the popup can be input only as a pixel value, and it must be entered as a `number` type.\n\n| Default                                                           | Example                                                          |\n| ----------------------------------------------------------------- | ---------------------------------------------------------------- |\n| ![month-moreView-default](../../assets/month-moreView-before.png) | ![month-moreView-example](../../assets/month-moreView-after.png) |\n\n```js\ncalendar.setTheme({\n  month: {\n    moreView: {\n      border: '1px solid grey',\n      boxShadow: '0 2px 6px 0 grey',\n      backgroundColor: 'white',\n      width: 320,\n      height: 200,\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#month-theme)\n\n#### month-moreViewTitle\n\nSpecifies the header area of the 'more events' pop-up of the monthly view. The background color can be specified with `backgroundColor`, and the default value is `'inherit'`.\n\n| Default                                                                     | Example                                                                    |\n| --------------------------------------------------------------------------- | -------------------------------------------------------------------------- |\n| ![month-moreViewTitle-default](../../assets/month-moreViewTitle-before.png) | ![month-moreViewTitle-example](../../assets/month-moreViewTitle-after.png) |\n\n```js\ncalendar.setTheme({\n  month: {\n    moreViewTitle: {\n      backgroundColor: 'grey',\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#month-theme)\n\n#### month-weekend\n\nSpecifies the weekend cell of the monthly view. The background color can be specified with `backgroundColor`, and the default value is `'inherit'`.\n\n| Default                                                         | Example                                                        |\n| --------------------------------------------------------------- | -------------------------------------------------------------- |\n| ![month-weekend-default](../../assets/month-weekend-before.png) | ![month-weekend-example](../../assets/month-weekend-after.png) |\n\n```js\ncalendar.setTheme({\n  month: {\n    weekend: {\n      backgroundColor: 'rgba(255, 64, 64, 0.4)',\n    },\n  },\n});\n```\n\n#### month-gridCell\n\nSpecifies the header and footer height of each cell of the monthly view. By default, the footer is inactive, so to use the footer, an arbitrary `number` type value must be passed.\n\nThe default value of `headerHeight` is `31`, and the default value of `footerHeight` is `null`.\n\n⚠️ If the property value is `null`, the header or footer is not displayed.\n\n| Default                                                          | Example                                                        |\n| ---------------------------------------------------------------- | -------------------------------------------------------------- |\n| ![month-gridCell-before](../../assets/month-gridCell-before.png) | ![month-gridCell-after](../../assets/month-gridCell-after.png) |\n\n```js\ncalendar.setTheme({\n  month: {\n    gridCell: {\n      footerHeight: 31,\n    },\n  },\n});\n```\n\n[⬆️ Back to the list](#month-theme)\n"
  },
  {
    "path": "docs/en/apis/tzdate.md",
    "content": "# TZDate\n\n## Description\n\nTZDate is a custom date class created to handle timezones. When creating an event, the event start date or end date can be specified with TZDate, and the date and time-related values in the calendar API are returned as TZDate.\n\n```js\nimport Calendar, { TZDate } from '@toast-ui/calendar';\n\nconst calendar = new Calendar('#container');\ncalendar.createEvents([\n  {\n    id: '1',\n    calendarId: 'cal1',\n    title: 'event',\n    start: new TZDate('2022-06-01T10:00:00'), // TZDate\n    end: new TZDate('2022-06-01T11:00:00'), // TZDate\n  },\n]);\n\nconsole.log(calendar.getDate()); // TZDate\nconsole.log(calendar.getEvent('1', 'cal1').start); // TZDate\n```\n\n## Creating Instance\n\n- Type\n  - `new TZDate(date?: number | string | Date | TZDate)`\n  - `new TZDate(year: number, monthIndex: number, day?: number, hours?: number, minutes?: number, seconds?: number, milliseconds?: number)`\n- Parameters\n  - `date` or individual date and time components: values representing the date and time\n\nTZDate can be created with the following parameters.\n\n1. Without parameters: Without parameters, a TZDate instance with the date and time of creation is created.\n2. UNIX timestamp value\n3. Timestamp string\n4. Individual date and time components\n5. Date instance\n6. TZDate instance\n\nExcept for the TZDate instance, the rest of the parameters are the same as the `Date()` constructor parameters, so refer to [`Date()` constructor parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#parameters) for details.\n\n```js\nimport { TZDate } from '@toast-ui/calendar';\n\nconst now = new TZDate(); // no parameters\nconst date = new TZDate(1654052400000); // UNIX timestamp value\nconst date1 = new TZDate('2022-06-01T12:00:00'); // timestamp string\nconst date2 = new TZDate(2022, 5, 1, 12, 0, 0, 0); // individual date and time components const now1 = new TZDate(new Date()); // Date instance\nconst now2 = new TZDate(new TZDate()); // TZDate instance\n```\n\n## Instance methods\n\n💡 Click on a method to see more detailed explanations and usage examples.\n\n| Method                                  | Description                                                                                                                                    |\n| --------------------------------------- |------------------------------------------------------------------------------------------------------------------------------------------------|\n| [toString](#tostring)                   | Returns the date and time of the TZDate instance as a string.                                                                                  |\n| [toDate](#todate)                       | Returns the date and time of the TZDate instance as a Date object.                                                                             |\n| [valueOf](#valueof)                     | Returns the elapsed time (in milliseconds) from January 1, 1970 UTC to the date and time of the TZDate instance.                               |\n| [addFullYear](#addfullyear)             | Adds years by a given numeric value.                                                                                                           |\n| [addMonth](#addmonth)                   | Adds months by a given numeric value.                                                                                                          |\n| [addDate](#adddate)                     | Adds a number of days to a given numeric value.                                                                                                |\n| [addHours](#addhours)                   | Adds hours by a given numeric value.                                                                                                           |\n| [addMinutes](#addminutes)               | Adds minutes to a given numeric value.                                                                                                         |\n| [addSeconds](#addseconds)               | Adds seconds to the given numeric value.                                                                                                       |\n| [addMilliseconds](#addmilliseconds)     | Adds milliseconds to a given numeric value.                                                                                                    |\n| [getTime](#gettime)                     | Returns the elapsed time (in milliseconds) from January 1, 1970 UTC to the date and time of the TZDate instance.                               |\n| [getFullYear](#getfullyear)             | Returns the year of the TZDate instance date and time.                                                                                         |\n| [getMonth](#getmonth)                   | Returns the month of the TZDate instance date and time. Month is a zero-based value. (for example, `2` in case of March)                       |\n| [getDate](#getdate)                     | Returns the day of the TZDate instance date and time.                                                                                          |\n| [getDay](#getday)                       | Returns a numeric value corresponding to the day of the week of the TZDate instance date and time. 0 represents Sunday.                        |\n| [getHours](#gethours)                   | Returns the hours of the TZDate instance date and time.                                                                                        |\n| [getMinutes](#getminutes)               | Returns the minutes of the TZDate instanc date and timee.                                                                                      |\n| [getSeconds](#getseconds)               | Returns the seconds of the TZDate instanc date and timee.                                                                                      |\n| [getMilliseconds](#getmilliseconds)     | Returns the milliseconds of the TZDate instanc date and timee.                                                                                 |\n| [getTimezoneOffset](#gettimezoneoffset) | Returns the timezone offset of the TZDate instance in minutes.                                                                                 |\n| [setWithRaw](#setwithraw)               | Sets the day and time of the TZDate instance with individual date and time components.                                                         |\n| [setTime](#settime)                     | Sets the TZDate instance date and time in milliseconds since January 1, 1970 UTC.                                                              |\n| [setFullYear](#setfullyear)             | Sets the year and the date of the TZDate instance as a given numeric value.                                                                    |\n| [setMonth](#setmonth)                   | Sets the month of the TZDate instance date and time as a given numeric value. Month is a zero-based value. (for example, `2` in case of March) |\n| [setDate](#setdate)                     | Sets the date of the TZDate instance date and time as a given numeric value.                                                                   |\n| [setHours](#sethours)                   | Sets the hours of the TZDate instance date and time as a given numeric value.                                                                  |\n| [setMinutes](#setminutes)               | Sets the minutes of the TZDate instance date and time as a given numeric value.                                                                |\n| [setSeconds](#setseconds)               | Sets the seconds of the TZDate instance date and time as a given numeric value.                                                                |\n| [setMilliseconds](#setmilliseconds)     | Sets the milliseconds of the TZDate instance as a given numeric value.                                                                         |\n| [tz](#tz)                               | Returns a new TZDate instance following the given timezone.                                                                                    |\n| [local](#local)                         | Returns a new TZDate instance following the system timezone.                                                                                   |\n\n### toString\n\n- Type: `toString(): string`\n- Returns: `string` - a string representing the date and time of the TZDate instance\n\nReturns the TZDate instance date and time as a string. It has the same format as [the `toString()` function of the Date object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toString).\n\n[⬆️ Back to the list](#instance-methods)\n\n### toDate\n\n- Type: `toDate(): Date`\n- Returns: `Date` - a Date object representing the date and time of the TZDate instance\n\nReturns the date and time of the TZDate instance as a native Date object.\n\n[⬆️ Back to the list](#instance-methods)\n\n### valueOf\n\n- Type: `valueOf(): number`\n- Returns the elapsed time (in milliseconds) from January 1, 1970 UTC to the date and time of the TZDate instance.\n\nReturns the elapsed time (in milliseconds) from January 1, 1970 UTC to the date and time of the TZDate instance.\n\n[⬆️ Back to the list](#instance-methods)\n\n### addFullYear\n\n- Type: `addFullYear(y: number): TZDate`\n- Parameters\n  - `y` - the number of years to add\n- Returns: `TZDate` - changed TZDate\n\nAdds years by a given numeric value. Changes an existing TZDate instance and returns it.\n\n[⬆️ Back to the list](#instance-methods)\n\n### addMonth\n\n- Type: `addFullYear(y: number): TZDate`\n- Parameters\n  - `y` - the number of years to add\n- Returns: `TZDate` - changed TZDate\n\nAdds months by a given numeric value. Changes an existing TZDate instance and returns it.\n\n[⬆️ Back to the list](#instance-methods)\n\n### addDate\n\n- Type: `addDate(d: number): TZDate`\n- Parameters\n  - `d` - the number of days to append\n- Returns: `TZDate` - changed TZDate\n\nAdds a number of days to a given numeric value. Changes an existing TZDate instance and returns it.\n\n[⬆️ Back to the list](#instance-methods)\n\n### addHours\n\n- Type: `addHours(h: number): TZDate`\n- Parameters\n  - `h` - the number of times to append\n- Returns: `TZDate` - changed TZDate\n\nAdds hours by a given numeric value. Changes an existing TZDate instance and returns it.\n\n[⬆️ Back to the list](#instance-methods)\n\n### addMinutes\n\n- Type: `addMinutes(M: number): TZDate`\n- Parameters\n  - `M` - the number of minutes to add\n- Returns: `TZDate` - changed TZDate\n\nAdds minutes to a given numeric value. Changes an existing TZDate instance and returns it.\n\n[⬆️ Back to the list](#instance-methods)\n\n### addSeconds\n\n- Type: `addSeconds(s: number): TZDate`\n- Parameters\n  - `M` - the number of seconds to add\n- Returns: `TZDate` - changed TZDate\n\nAdds seconds to the given numeric value. Changes an existing TZDate instance and returns it.\n\n[⬆️ Back to the list](#instance-methods)\n\n### addMilliseconds\n\n- Type: `addMilliseconds(ms: number): TZDate`\n- Parameters\n  - `ms` - the number of milliseconds to add\n- Returns: `TZDate` - changed TZDate\n\nAdds milliseconds to a given numeric value. Changes an existing TZDate instance and returns it.\n\n[⬆️ Back to the list](#instance-methods)\n\n### getTime\n\n- Type: `getTime(): number`\n- Returns the elapsed time (in milliseconds) from January 1, 1970 UTC to the date and time of the TZDate instance.\n\nReturns the elapsed time (in milliseconds) from January 1, 1970 UTC to the date and time of the TZDate instance.\n\n[⬆️ Back to the list](#instance-methods)\n\n### getFullYear\n\n- Type: `getFullYear(): number`\n- Returns: `number` - the year of the date and time of the TZDate instance\n\nReturns the year of the TZDate instance date and time.\n\n[⬆️ Back to the list](#instance-methods)\n\n### getMonth\n\n- Type: `getMonth(): number`\n- Returns: `number` - the month of the date and time of the TZDate instance\n\nReturns the month of the TZDate instance date and time. Month is a zero-based value. (ex. `2` in case of March)\n\n[⬆️ Back to the list](#instance-methods)\n\n### getDate\n\n- Type: `getDate(): number`\n- Returns: `number` - the day of the TZDate instance date and time\n\nReturns the day of the TZDate instance.\n\n[⬆️ Back to the list](#instance-methods)\n\n### getDay\n\n- Type: `getDay(): number`\n- Returns: `number` - a numeric value corresponding to the day of the week for the date and time of the TZDate instance\n\nReturns a numeric value corresponding to the day of the week of the TZDate instance date and time. 0 represents Sunday.\n\n[⬆️ Back to the list](#instance-methods)\n\n### getHours\n\n- Type: `getHours(): number`\n- Returns: `number` - the hours of the TZDate instance date and time\n\nReturns the hours of the TZDate instance date and time.\n\n[⬆️ Back to the list](#instance-methods)\n\n### getMinutes\n\n- Type: `getMinutes(): number`\n- Returns: `number` - the minutes of the TZDate instance date and time\n\nReturns the minutes of the TZDate instance date and time.\n\n[⬆️ Back to the list](#instance-methods)\n\n### getSeconds\n\n- Type: `getSeconds(): number`\n- Returns: `number` - the seconds of the date and time of the TZDate instance\n\nReturns the seconds of the TZDate instance date and time.\n\n[⬆️ Back to the list](#instance-methods)\n\n### getMilliseconds\n\n- Type: `getMilliseconds(): number`\n- Returns: `number` - the milliseconds of the date and time of the TZDate instance\n\nReturns the milliseconds of the TZDate instance date and time.\n\n[⬆️ Back to the list](#instance-methods)\n\n### getTimezoneOffset\n\n- Type: `getTimezoneOffset(): number`\n- Returns: `number` - the timezone offset of the TZDate instance in minutes\n\nReturns the timezone offset of the TZDate instance in minutes.\n\n[⬆️ Back to the list](#instance-methods)\n\n### setWithRaw\n\n- Type: `setWithRaw(y: number, m: number, d: number, h: number, M: number, s: number, ms: number): TZDate`\n- Parameters\n  - `y` : a year to specify\n  - `m` : a month to specify\n  - `d` : a day to specify\n  - `h` : hours to specify\n  - `M` : minutes to specify\n  - `s` : seconds to specify\n  - `ms` : milliseconds to specify\n- Returns: `TZDate` - changed TZDate\n\nSpecifies the date and time of the TZDate instance with individual date and time components. Changes an existing TZDate instance and returns it.\n\n[⬆️ Back to the list](#instance-methods)\n\n### setTime\n\n- Type: `setTime(t: number): number`\n- Parameters\n  - `t` : the number of milliseconds since January 1, 1970 UTC to specify\n- Returns: `number` - the elapsed time (in milliseconds) from January 1, 1970 UTC to the date and time of the changed TZDate instance.\n\nSet the TZDate instance date and time with the elapsed time(in milliseconds) since January 1, 1970 UTC. Changes an existing TZDate instance.\n\n[⬆️ Back to the list](#instance-methods)\n\n### setFullYear\n\n- Type: `setFullYear(y: number, m?: number, d?: number): number`\n- Parameters\n  - `y` : a year to specify\n  - `m` : The month to specify. If not specified, the month of the existing TZDate instance date and time is maintained.\n  - `d` : The day to specify. If not specified, the day of the existing TZDate instance is maintained.\n- Returns: `number` - the elapsed time (in milliseconds) from January 1, 1970 UTC to the date and time of the changed TZDate instance.\n\nSpecifies the year of the date and time of the TZDate instance as a given numeric value. You can additionally specify the month and date. Change an existing TZDate instance.\n\n[⬆️ Back to the list](#instance-methods)\n\n### setMonth\n\n- Type: `setMonth(m: number, d?: number): number`\n- Parameters\n  - `m` : a month to specify\n  - `d` : The day to specify. If not specified, the day of the existing TZDate instance is maintained.\n- Returns: `number` - the elapsed time (in milliseconds) from January 1, 1970 UTC to the date and time of the changed TZDate instance.\n\nSpecifies the month of the date and time of the TZDate instance as a given numeric value. Month is a zero-based value. (for example, `2` in case of March) The day can also be additionally specified. Changes an existing TZDate instance.\n\n[⬆️ Back to the list](#instance-methods)\n\n### setDate\n\n- Type: `addDate(d: number): TZDate`\n- Parameters\n  - `d` : a day to specify\n- Returns: `number` - the elapsed time (in milliseconds) from January 1, 1970 UTC to the date and time of the changed TZDate instance.\n\nSpecifies the day of the TZDate instance as a given numeric value. Changes an existing TZDate instance.\n\n[⬆️ Back to the list](#instance-methods)\n\n### setHours\n\n- Type: `setHours(h: number, M?: number, s?: number, ms?: number): number`\n- Parameters\n  - `h` : hours to specify\n  - `M` : minutes to specify. If not specified, the minutes of the existing TZDate instance date and time are maintained.\n  - `s` : seconds to specify. If not specified, the seconds of the existing TZDate instance date and time are maintained.\n  - `ms` : milliseconds to specify. If not specified, the milliseconds of the old TZDate instance date and time are maintained.\n- Returns: `number` - the elapsed time (in milliseconds) from January 1, 1970 UTC to the date and time of the changed TZDate instance.\n\nSpecifies the hour of the TZDate instance date and time as a given numeric value. You can additionally specify minutes, seconds, and milliseconds. Changes an existing TZDate instance.TZDate\n\n[⬆️ Back to the list](#instance-methods)\n\n### setMinutes\n\n- Type: `setMinutes(M: number, s?: number, ms?: number): number`\n- Parameters\n  - `M` : minutes to specify\n  - `s` : seconds to specify. If not specified, the seconds of the existing TZDate instance date and time are maintained.\n  - `ms` : milliseconds to specify. If not specified, the milliseconds of the old TZDate instance date and time are maintained.\n- Returns: `number` - the elapsed time (in milliseconds) from January 1, 1970 UTC to the date and time of the changed TZDate instance.\n\nSpecifies the minute of the TZDate instance date and time as a given numeric value. Seconds and milliseconds can be additionally specified. Changes an existing TZDate instance.\n\n[⬆️ Back to the list](#instance-methods)\n\n### setSeconds\n\n- Type: `setSeconds(s: number, ms?: number): number`\n- Parameters\n  - `s` : seconds to specify\n  - `ms` : milliseconds to specify. If not specified, the milliseconds of the old TZDate instance date and time are maintained.\n- Returns: `number` - the elapsed time (in milliseconds) from January 1, 1970 UTC to the date and time of the changed TZDate instance.\n\nSpecifies the second of the TZDate instance date and time as a given numeric value. You can also specify additional milliseconds. Changes an existing TZDate instance.\n\n[⬆️ Back to the list](#instance-methods)\n\n### setMilliseconds\n\n- Type: `setMilliseconds(ms: number): number`\n- Parameters\n  - `ms` : milliseconds to specify\n- Returns: `number` - the elapsed time (in milliseconds) from January 1, 1970 UTC to the date and time of the changed TZDate instance.\n\nSpecifies the number of milliseconds of the date and time of the TZDate instance as a given numeric value. Changes an existing TZDate instance.\n\n[⬆️ Back to the list](#instance-methods)\n\n### tz\n\n- Type: `tz(tzValue: string | 'Local' | number): TZDate`\n- Parameters\n  - `tzValue` : timezone to specify\n- Returns: `TZDate` - a new TZDate instance following the given timezone.\n\nReturns a new TZDate instance following the given timezone. `tzValue` can be specified as the timezone name of the [IANA time zone database](https://www.iana.org/time-zones), the timezone offset numeric value, and `'Local'`. If `'Local'` is specified, the system timezone is followed. It does not change the existing TZDate instance, but returns a new TZDate instance.\n\n[⬆️ Back to the list](#instance-methods)\n\n### local\n\n- Type: `local(tzValue?: string | number): TZDate`\n- Parameters\n  - `tzValue` : The timezone the TZDate instance follows. If not specified, it is calculated based on the time zone of the TZDate instance.\n- Returns: `TZDate` - a new TZDate instance following the system timezone\n\nReturns a new TZDate instance conforming to the system timezone. `tzValue` can be used when the TZDate instance follows a timezone other than the one assigned to it. If there is no `tzValue`, it is calculated based on the time zone of the TZDate instance. It does not change the existing TZDate instance, but returns a new TZDate instance.\n\n[⬆️ Back to the list](#instance-methods)\n"
  },
  {
    "path": "docs/en/guide/getting-started.md",
    "content": "# Getting Started\n\n## Table of Contents\n\n- [Installation](#installation)\n  - [Using the package manager](#using-the-package-manager)\n    - [npm](#npm)\n  - [Using Contents Delivery Network](#using-contents-delivery-network)\n  - [Downloading source files](#downloading-source-files)\n- [How to use the calendar](#how-to-use-the-calendar)\n  - [HTML](#html)\n  - [JavaScript](#javascript)\n    - [Importing modules](#importing-modules)\n    - [Loading bundle files for legacy browsers](#loading-bundle-files-for-legacy-browsers)\n  - [CSS](#css)\n  - [Creating an instance](#creating-an-instance)\n- [Basic usage](#basic-usage)\n  - [Disable to collect hostname for Google Analytics(GA)](#disable-to-collect-hostname-for-google-analyticsga)\n  - [Creating events](#creating-events)\n  - [Using pop-ups](#using-pop-ups)\n  - [Applying the theme](#applying-the-theme)\n  - [Applying the template](#applying-the-template)\n  - [Applying instance events](#applying-instance-events)\n\n## Installation\n\nTOAST UI products can be used by using the package manager or by directly downloading the source code. However, it is recommended to use a package manager.\n\n### Using the package manager\n\nTOAST UI products are registered in the [npm](https://www.npmjs.com/) package registry. You can easily install packages using CLI tools provided by each package manager. To use npm, you need to install [Node.js](https://nodejs.org) in advance.\n\n#### npm\n\n```sh\nnpm install @toast-ui/calendar # latest version\nnpm install @toast-ui/calendar@<version> # specific version since 2.0\nnpm install tui-calendar@<version> # 1.x legacy version\n```\n\n### Using Contents Delivery Network\n\nTOAST UI Calendar is available through CDN.\n\n- You can get the calendar through the CDN with the code below.\n\n```html\n<link rel=\"stylesheet\" href=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css\" />\n<script src=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.min.js\"></script>\n\n<!-- To get bundle file for legacy browser -->\n<!-- <script src=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.ie11.min.js\"></script> -->\n\n<!-- Import as es module -->\n<!-- <script type=\"module\" src=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.mjs\"></script> -->\n```\n\n- CDN consists of the following directory structure.\n\n```\n- uicdn.toast.com/\n  ├─ calendar/\n  │  ├─ latest\n  │  │  ├─ toastui-calendar.css\n  │  │  ├─ toastui-calendar.js\n  │  │  ├─ toastui-calendar.min.css\n  │  │  ├─ toastui-calendar.min.js\n  │  │  ├─ toastui-calendar.ie11.js\n  │  │  ├─ toastui-calendar.ie11.min.js\n  │  │  │  toastui-calendar.mjs\n  │  ├─ v2.0.0/\n```\n\n### Downloading source files\n\n- [Download the source code for each version](https://github.com/nhn/tui.calendar/releases)\n\n## How to use the calendar\n\n### HTML\n\nAdd a container element where TOAST UI Calendar will be created. **This element must have a height value of the appropriate height. (at least 600px recommended)**\n\n```html\n<div id=\"calendar\" style=\"height: 600px;\"></div>\n```\n\n### JavaScript\n\n#### Importing modules\n\nTOAST UI Calendar can be instantiated through the constructor function. There are three ways to access the constructor function depending on the environment.\n\n```js\n/* ES6 module in Node.js environment */\nimport Calendar from '@toast-ui/calendar';\n```\n\n```js\n/* CommonJS in Node.js environment */\nconst Calendar = require('@toast-ui/calendar');\n```\n\n```js\n/* in the browser environment namespace */\nconst Calendar = tui.Calendar;\n```\n\n#### Loading bundle files for legacy browsers\n\nTOAST UI Calendar provides a separate bundle file for legacy browsers. The default bundle provides stable support for the latest two versions of the modern browser. However, the default bundle does not include a polyfill for IE11, so to support IE11 or a legacy browser below a certain level, you need to add the IE11 bundle that includes a polyfill as follows.\n\nSince the bundle size of IE11 is about 30% larger than that of the default bundle, you must take care not to increase the bundle size unnecessarily by considering the range of support.\n\n```js\n/* ES6 module in Node.js environment */\nimport Calendar from '@toast-ui/calendar/ie11';\n```\n\n```js\n/* CommonJS in Node.js environment */\nconst Calendar = require('@toast-ui/calendar/ie11');\n```\n\n```html\n<!-- with CDN and browser environment namespace -->\n<script src=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.ie11.min.js\"></script>\n<script>\n  const Calendar = tui.Calendar;\n</script>\n```\n\n### CSS\n\nTo use the calendar, you need to add a CSS file. You can import the CSS file through import or require, or you can import them through CDN.\n\n```js\n/* ES6 module in Node.js environment */\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css'; // Stylesheet for calendar\n```\n\n```js\n/* CommonJS in Node.js environment */\nrequire('@toast-ui/calendar/dist/toastui-calendar.min.css');\n```\n\n```html\n<!-- CDN -->\n<link rel=\"stylesheet\" href=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css\" />\n```\n\n### Creating an instance\n\nThe constructor takes two arguments: `container` and `options`.\n\n- `container` : HTML element that has TOAST UI Calendar as a child element or CSS selector string to get the HTML element\n- `options` : Options object that can customize TOAST UI Calendar, such as default view type, time zone, theme, and template. For more information, see the [Options documentation](../apis/options.md).\n\n```js\nconst container = document.getElementById('calendar');\nconst options = {\n  defaultView: 'week',\n  timezone: {\n    zones: [\n      {\n        timezoneName: 'Asia/Seoul',\n        displayLabel: 'Seoul',\n      },\n      {\n        timezoneName: 'Europe/London',\n        displayLabel: 'London',\n      },\n    ],\n  },\n  calendars: [\n    {\n      id: 'cal1',\n      name: 'Personal',\n      backgroundColor: '#03bd9e',\n    },\n    {\n      id: 'cal2',\n      name: 'Work',\n      backgroundColor: '#00a9ff',\n    },\n  ],\n};\n\nconst calendar = new Calendar(container, options);\n```\n\n![image](../../assets/gettingStarted_calendar.png)\n\n## Basic usage\n\n### Disable to collect hostname for Google Analytics(GA)\n\n[TOAST UI Calendar](https://github.com/nhn/tui.calendar) applies [GA](https://analytics.google.com/analytics/web/) to collect statistics on open source usage to see how widespread it is around the world. This serves as an important indicator to determine the future progress of the project. It collects `location.hostname` (e.g. \"ui.toast.com\") and is only used to measure usage statistics.\n\nTo disable GA, set the [`usageStatistics` option](/docs/en/apis/options.md#usagestatistics) to `false`:\n\n```js\nconst calendar = new Calendar('#calendar', {\n  usageStatistics: false\n});\n```\n\n### Creating events\n\nWhen creating an event, use the [`createEvents` method](../apis/calendar.md#createevents) of the Calendar instance.\n\nEvent information should be in the form of [EventObject](../apis/event-object.md).\n\n```js\ncalendar.createEvents([\n  {\n    id: 'event1',\n    calendarId: 'cal2',\n    title: 'Weekly meeting',\n    start: '2022-06-07T09:00:00',\n    end: '2022-06-07T10:00:00',\n  },\n  {\n    id: 'event2',\n    calendarId: 'cal1',\n    title: 'Lunch appointment',\n    start: '2022-06-08T12:00:00',\n    end: '2022-06-08T13:00:00',\n  },\n  {\n    id: 'event3',\n    calendarId: 'cal2',\n    title: 'Vacation',\n    start: '2022-06-08',\n    end: '2022-06-10',\n    isAllday: true,\n    category: 'allday',\n  },\n]);\n```\n\n![createEvents](../../assets/gettingStarted_createEvents.png)\n\n### Using pop-ups\n\nTOAST UI Calendar provides an event form popup and an event details popup by default. To use the popups, the [`useFormPopup`](../apis/options.md#useformpopup) and [`useDetailPopup`](../apis/options.md#usedetailpopup) options must be set to `true`. Options can be set when creating an instance or changed using the [`setOptions`](../apis/calendar.md#setoptions) method after instance creation.\n\nWhen using the event form popup, you must import CSS files of [`tui-date-picker`](https://github.com/nhn/tui.date-picker) and [`tui-time-picker`](https://github.com/nhn/tui.time-picker) for the style to be applied properly.\n\n```sh\nnpm install tui-date-picker tui-time-picker\n```\n\n```js\n// Load the css files of tui-date-picker and tui-time-picker to use the event creation popup.\nimport 'tui-date-picker/dist/tui-date-picker.css';\nimport 'tui-time-picker/dist/tui-time-picker.css';\n\ncalendar.setOptions({\n  useFormPopup: true,\n  useDetailPopup: true,\n});\n```\n\n| Event creation popup                                    | Event details pop-up                                        |\n| ------------------------------------------------ | ----------------------------------------------------- |\n| ![useFormPopup](../../assets/gettingStarted_useFormPopup.png) | ![useDetailPopup](../../assets/gettingStarted_useDetailPopup.png) |\n\n### Applying the theme\n\nUse themes when you want to change styles such as color and background color. The theme can be specified in [the `theme` property of the options object](../apis/options.md#theme) when an instance is created, or can be changed using the [`setTheme`](../apis/calendar.md#settheme) method after instance creation. For available themes, refer to the [theme documentation](../apis/theme.md).\n\n```js\ncalendar.setTheme({\n  common: {\n    gridSelection: {\n      backgroundColor: 'rgba(81, 230, 92, 0.05)',\n      border: '1px dotted #515ce6',\n    },\n  },\n});\n```\n\n![theme](../../assets/gettingStarted_theme.png)\n\n### Applying the template\n\nTemplates are features that support custom rendering. It can be specified in [the `template` property of the options object](../apis/options.md#template) when creating an instance, or can be changed using the [`setOptions`](../apis/calendar.md#setoptions) method after instance creation. For available templates, refer to the [Templates document](../apis/template.md).\n\n```js\nfunction formatTime(time) {\n  const hours = `${time.getHours()}`.padStart(2, '0');\n  const minutes = `${time.getMinutes()}`.padStart(2, '0');\n\n  return `${hours}:${minutes}`;\n}\n\ncalendar.setOptions({\n  template: {\n    time(event) {\n      const { start, end, title } = event;\n\n      return `<span style=\"color: white;\">${formatTime(start)}~${formatTime(end)} ${title}</span>`;\n    },\n    allday(event) {\n      return `<span style=\"color: gray;\">${event.title}</span>`;\n    },\n  },\n});\n```\n\n![template](../../assets/gettingStarted_template.png)\n\n### Applying instance events\n\nTOAST UI Calendar provides instance events. If necessary, you can set to receive events and execute desired actions. In addition, the user can set up their own event separately.\n\nYou can listen for instance events using the `on` method.\n\nFor details, refer to the [instance event documentation](../apis/calendar.md#instance-events).\n\n```js\ncalendar.on('clickEvent', ({ event }) => {\n  const el = document.getElementById('clicked-event');\n  el.innerText = event.title;\n});\n```\n\n![instance event](../../assets/gettingStarted_instanceEvent.gif)\n"
  },
  {
    "path": "docs/en/guide/migration-guide-v2.md",
    "content": "# v2 Migration Guide\n\n## Table of Contents\n\n- [Overview](#overview)\n- [Installation](#installation)\n  - [Rename package and files](#rename-package-and-files)\n  - [Change the structure of CDN directories](#change-the-structure-of-cdn-directories)\n- [Browser Compatibility Range (IE >= 11)](#browser-compatibility-range-ie--11)\n- [API migration](#api-migration)\n  - [Change term from `schedule` to `event`](#change-term-from-schedule-to-event)\n  - [Terms like `currentTimeIndicator` and `currentTimeLine` are changed to `nowIndicator`](#terms-like-currenttimeindicator-and-currenttimeline-are-changed-to-nowindicator)\n  - [Feature Improvements](#feature-improvements)\n    - [Rendering optimization](#rendering-optimization)\n    - [Theme improvement](#theme-improvement)\n    - [View related type improvement](#view-related-type-improvement)\n    - [Improved taskView and eventView types](#improved-taskview-and-eventview-types)\n  - [Changes](#changes)\n    - [Option changes](#option-changes)\n    - [Instance methods](#instance-methods)\n    - [Instance events](#instance-events)\n    - [Template changes](#template-changes)\n  - [Removals](#removals)\n    - [Rendering-related parameter changes](#rendering-related-parameter-changes)\n\n## Overview\n\nTOAST UI Calendar v2.0, which uses [preact](https://preactjs.com/) to render calendars more efficiently, has been released. In v2, we have improved the bundle size and upgraded to a modern development environment, laying the groundwork for making it easy to add other features. We provide a migration guide for better understanding of users who use Calendar.\n\n## Installation\n\n### Rename package and files\n\nThe package name has been changed from `tui-calendar` to `@toast-ui/calendar`.\n\n```sh\n# v1\nnpm install tui-calendar@<version> # 1.x legacy version\n\n# v2\nnpm install @toast-ui/calendar # latest version\nnpm install @toast-ui/calendar@<version> # specific version since 2.0\n```\n\nAlso, the filenames have been changed from `tui-calendar` to `toastui-calendar`.\n\n```js\n/* ES6 module in Node.js environment */\n// v1\nimport Calendar from 'tui-calendar';\nimport \"tui-calendar/dist/tui-calendar.min.css\";\n\n// v2\nimport Calendar from '@toast-ui/calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n```\n\n```html\n<!-- CDN -->\n<!-- v1 -->\n<link rel=\"stylesheet\" href=\"https://uicdn.toast.com/tui-calendar/latest/tui-calendar.min.css\" />\n<script src=\"https://uicdn.toast.com/tui-calendar/latest/tui-calendar.min.js\"></script>\n\n<!-- v2 -->\n<link rel=\"stylesheet\" href=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css\" />\n<script src=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.min.js\"></script>\n```\n\n### Change the structure of CDN directories\n\nThe directory structure and bundle file names of the CDN have been changed. In v1, the files named `tui-calendar` were in the `tui-calendar` folder like `https://uicdn.toast.com/tui-calendar/latest/tui-calendar.js`. However, from v2, the files named `toastui-calendar` are in the `calendar` folder like `https://uicdn.toast.com/calendar/latest/toastui-calendar.js`.\n\nThe CDN address used in v1 is maintained, but the files inside `/tui-calendar/latest/` are the latest version of v1, not the latest version of TOAST UI Calendar. If you need to use the latest version, you should use the files in the `/calendar/latest/`.\n\n```sh\n- uicdn.toast.com/\n  ├─ tui-calendar/ # v1\n  │  ├─ latest     # the latest version of v1\n  │  │  ├─ tui-calendar.css\n  │  │  ├─ tui-calendar.js\n  │  │  ├─ tui-calendar.min.css\n  │  │  ├─ tui-calendar.min.js\n  │  ├─ v1.0.0/    # specific version of v1\n  │  │  ├─ ...\n  ├─ calendar/     # since v2\n  │  ├─ latest     # the latest version\n  │  │  ├─ toastui-calendar.css\n  │  │  ├─ toastui-calendar.js\n  │  │  ├─ toastui-calendar.min.css\n  │  │  ├─ toastui-calendar.min.js\n  │  │  ├─ toastui-calendar.ie11.js\n  │  │  ├─ toastui-calendar.ie11.min.js\n  │  │  │  toastui-calendar.mjs\n  │  ├─ v2.0.0/    # specific version since v2\n  │  │  ├─ ...\n```\n\n## Browser Compatibility Range (IE >= 11)\n\nFrom v2, the supported browser range is changed to *Internet Explorer 11 or* later. In v1, Internet Explorer 9 or higher browsers were supported, but the scope of support was changed for the use of the latest development environment and [preact](https://preactjs.com/) X (version 10).\n\nThe default bundle provides stable support for the latest two versions of the modern browsers. However, the default bundle does not include a polyfill for IE 11, so to support IE 11 or a legacy browser below a certain level, you need to add the IE 11 bundle that includes a polyfill as follows. Since the bundle size of IE 11 is about 30% larger than that of the default bundle, you must take care not to increase the bundle size unnecessarily by considering the range of support.\n\n```ts\nimport Calendar from '@toast-ui/calendar/ie11';\n```\n\n## API migration\n\nThe APIs that require migration to use v2 are as follows.\n\n- [Options](../apis/options.md)\n- [Theme](../apis/theme.md)\n- [Instance events](../apis/calendar.md#instance-events)\n- [Instance methods](../apis/calendar.md#instance-methods)\n\nThe migration scope is largely divided into _feature improvements_, _changes_, and _removals_.\n\n- [Feature improvements](#feature-improvements) : APIs with improved or newly added features\n- [Changes](#changes) : APIs whose functionality is maintained but whose name, type, etc. have been changed\n- [Removals](#removals) : APIs removed as unnecessary or spec out\n\nIn v2, the `creationGuide` representing the area when selecting a date or time has been changed to `gridSelection`. The term `vpanelSplitter` that controls each panel has been changed to `panelResizer`. Terms that are not unified like `daygrid` or `dayGridSchedule` are unified like `dayGrid` or `timeGrid`.\n\n### Change term from `schedule` to `event`\n\nIn v2, the name was changed from `schedule` to `event` to match the meaning of events in the calendar. In addition to renaming, all related APIs such as instance methods including `schedule` and instance events have been changed as `event`.\n\n### Terms like `currentTimeIndicator` and `currentTimeLine` are changed to `nowIndicator`\n\nIn v1, `currentTimeIndicator` and `currentTimeLine` are used interchangeably as terms representing the current timeline. In v2, this was unified as `nowIndicator`.\n\n### Feature Improvements\n\n#### Rendering optimization\n\nIn v1, rendering of the calendar was handled as direct DOM manipulation. As a result, unnecessary rendering could occur whenever the calendar was manipulated.\n\nIn v2, [preact](https://preactjs.com/) was introduced to improve rendering speed by reducing unnecessary rendering using virtual DOM and to support server-side rendering (SSR) in future. Accordingly, parameters such as `force` and `silent` that control rendering when using the instance method have been removed, and rendering is controlled according to the internal state of the calendar, making it impossible to control rendering when using the instance method.\n\n#### Theme improvement\n\n[Theme](../apis/theme.md) feature is improved. It has been improved from the method of designating the theme with the string key value concatenated with `.`, to the method using the nested object. Accordingly, the `setTheme` method has also been improved in a way that receives and processes nested objects as parameters. More details can be found in [Theme](../apis/theme.md).\n\n```ts\n// v1\ncalendar.setTheme({\n  'common.dayName.color': '#333',\n});\n```\n\n```ts\n// v2\ncalendar.setTheme({\n  common: {\n    dayName: {\n      color: '#333',\n    },\n  },\n});\n```\n\nThe following properties used in v1 have been removed or renamed from the theme. All the theme values except for the various color values and the width of the left area of each panel of the weekly/daily view have been removed.\n\n- `month.dayname.height`\n- `month.dayname.paddingLeft`\n- `month.dayname.paddingRight`\n- `month.dayname.fontSize`\n- `month.dayname.fontWeight`\n- `month.dayname.textAlign`\n- `month.day.fontSize`\n- All of `month.schedule` related properties\n- `month.moreView.paddingBottom`\n- `month.moreViewTitle.height`\n- `month.moreViewTitle.marginBottom`\n- `month.moreViewTitle.borderBottom`\n- `month.moreViewTitle.padding`\n- `month.moreViewList.padding`\n- `week.dayname.height`\n- `week.dayname.paddingLeft`\n- `week.dayname.textAlign`\n- `week.vpanelSplitter.height`\n- `week.vpanelSplitter.border` -> `week.panelResizer.border`\n- `week.daygridLeft.paddingRight`\n- `week.timegridLeft.fontSize`\n- `week.timegridLeftTimezoneLabel.height`\n- `week.timegridOneHour.height`\n- `week.timegridHalfHour.height`\n- `week.timegridHalfHour.borderBottom` -> `week.timeGridHalfHourLine.borderBottm`\n- `week.timegridHorizontalLine.borderBottom` -> `week.timeGridHourLine.borderBottom`\n- `week.timegrid.paddingRight`\n- All of `week.timegridSchedule` related properties\n- `week.currentTime` -> `week.nowIndicatorLabel`\n- `week.currentTime.fontSize`\n- `week.currentTime.fontWeight`\n- `week.currentTimeLinePast` -> `week.nowIndicatorPast`\n- `week.currentTimeLineBullet` -> `week.nowIndicatorBullet`\n- `week.currentTimeLineToday` -> `week.nowIndicatorToday`\n- `week.currentTimeLineFuture` -> `week.nowIndicatorFuture`\n- `week.pastTime.fontWeight`\n- `week.futureTime.fontWeight`\n- `week.creationGuide.fontSize`\n- `week.creationGuide.fontWeight`\n- All of `week.dayGridSchedule` related properties\n\nThe removed theme value can be applied using CSS instead. The following is the CSS file associated with the removed theme value.\n\n| Removed theme values                                             | Associated file location                                         |\n| ---------------------------------------------------------------- | ---------------------------------------------------------------- |\n| Related to <code>month.dayname</code>                            | [dayNames.css](/apps/calendar/src/css/daygrid/dayNames.css)      |\n| Related to <code>month.schedule</code>                           | [dayGrid.css](/apps/calendar/src/css/daygrid/dayGrid.css)        |\n| Related to <code>month.moreView</code>                           | [seeMore.css](/apps/calendar/src/css/popup/seeMore.css)          |\n| Related to <code>week.dayname</code>                             | [dayNames.css](/apps/calendar/src/css/daygrid/dayNames.css)      |\n| Related to <code>week.dayGridLeft</code>                         | [allday.css](/apps/calendar/src/css/panel/allday.css)            |\n| Related to <code>week.timeGridLeft</code>                        | [timeColumn.css](/apps/calendar/src/css/timegrid/timeColumn.css) |\n| Related to <code>week.timegridSchedule</code>                    | [time.css](/apps/calendar/src/css/events/time.css)               |\n| Related to <code>week.gridSelection</code> (creationGuide in v1) | [column.css](/apps/calendar/src/css/timegrid/column.css)         |\n| Related to <code>week.dayGridSchedule</code>                     | [dayGrid.css](/apps/calendar/src/css/daygrid/dayGrid.css)        |\n\n#### View related type improvement\n\nThe parameters and return types of the instance methods related to the view are now clearer. The types of views used in the calendar are divided into three views: monthly view(`month`), weekly view(`week`), and daily view(`day`).\n\n| Method                   | Changes                                                         |\n| ------------------------ |-----------------------------------------------------------------|\n| <code>changeView</code>  | Improved the type of the view name parameter you want to change |\n| <code>getViewName</code> | Improved the type of returned view name                         |\n\n#### Improved taskView and eventView types\n\nIn the weekly/daily view, the types of the `taskView` option indicating whether to display the `milestone` and `task` panels and the `eventView` option indicating whether to display the `allday` and `time` panels have become clearer.\n\n```ts\nconst calendar = new Calendar('#calendar', {\n  week: {\n    taskView: ['task'],\n    eventView: ['time'],\n  },\n});\n```\n\n### Changes\n\n#### Option changes\n\nThe options below have been moved within the options object or moved to the theme.\n\n| Options                           | Changes                                              | Additional information                                       |\n|-----------------------------------| ---------------------------------------------------- | ------------------------------------------------------------ |\n| options.taskView                  | options.week.taskView                                |                                                              |\n| options.eventView                 | options.week.eventView                               |                                                              |\n| options.disableDblClick           | options.gridSelection.enableDblClick                 | Default changed from <code>false</code> to <code>true</code> |\n| options.disableClick              | options.gridSelection.enableClick                    | Default changed from <code>false</code> to <code>true</code> |\n| options.timezone.offsetCalculator | options.timezone.customOffsetCalculator              |                                                              |\n| options.month.grid                | Moved to [theme](../apis/theme.md)                   |                                                              |\n| options.month.moreLayerSize       | Moved to [theme](../apis/theme.md)                   |                                                              |\n| options.month.isAlways6Week       | Changed to <code>options.month.isAlways6Weeks</code> |                                                              |\n| options.month.daynames            | Changed to <code>options.month.dayNames</code>       |                                                              |\n| options.week.daynames             | Changed to <code>options.week.dayNames</code>        |                                                              |\n\n#### Instance methods\n\nAs the name of `creation popup` in v1 is changed to `form popup` in v2, the instance method `openCreationPopup` that opens a popup is also changed to `openFormPopup`.\n\n```ts\n// v1\ncalendar.openCreationPopup(schedule);\n```\n\n```ts\n// v2\ncalendar.openFormPopup(event);\n```\n\nThe `toggleSchedules` instance method of v1 that makes events with a specific calendar ID invisible or visible has been changed to `setCalendarVisibility` with a more precise meaning. The following is an example of hiding events which has their calendarId property as `'1'`.\n\n```ts\n// v1\ncalendar.toggleSchedules('1', true);\n```\n\n```ts\n// v2\ncalendar.setCalendarVisibility('1', false);\n```\n\n#### Instance events\n\nThe v1 `clickMore` instance event has been changed to `clickMoreEventsBtn` with a clearer meaning.\n\n```ts\n// v1\ncalendar.on('clickMore', (event) => {\n  console.log(event.date, event.target);\n});\n```\n\n```ts\n// v2\ncalendar.on('clickMoreEventsBtn', (event) => {\n  console.log(event.date, event.target);\n});\n```\n\n#### Template changes\n\nv1's `timegridCurrentTime` has been renamed to `timegridNowIndicatorLabel`.\n\n### Removals\n\nIn v2, support for [bower](https://bower.io/) and support for `jquery plugin` have been discontinued.\n\n#### Rendering-related parameter changes\n\nThe `force`, `silent`, or `immediately` parameters of the following instance methods have been removed. Since v2 uses the rendering method through [preact](https://preactjs.com/), the parameters that can artificially control rendering have been removed.\n\n- `changeView`\n- `clear`\n- `createEvents` ( `createSchedules` in v1)\n- `deleteEvent` ( `deleteSchedule` in v1 )\n- `render`\n- `setCalendarColor`\n- `setOptions`\n- `updateEvent`\n"
  },
  {
    "path": "docs/ko/README.md",
    "content": "# 🗂️ 문서 인덱스\n\n## 가이드\n\n- [시작하기](./guide/getting-started.md)\n- [v2 마이그레이션 가이드](./guide/migration-guide-v2.md)\n\n## API 문서\n\n- [Calendar 클래스](./apis/calendar.md)\n- [옵션](./apis/options.md)\n- [테마](./apis/theme.md)\n- [템플릿](./apis/template.md)\n- [EventObject](./apis/event-object.md)\n- [TZDate 클래스](./apis/tzdate.md)\n"
  },
  {
    "path": "docs/ko/apis/calendar.md",
    "content": "# Calendar 클래스\n\n## 설명\n\n`Calendar` 클래스를 사용하여 캘린더 인스턴스를 생성할 수 있다. 인스턴스를 생성하고 싶은 위치(HTML 요소)를 지정하고, 필요하다면 적절한 옵션을 설정해야 한다.\n\n캘린더 인스턴스 생성 시 첫 번째 인자로 캘린더 인스턴스가 마운트 될 요소를 설정한다. **이 요소는 적절한 높이의 `height` 값을 가지고 있어야 한다. (최소 600px 이상 권장)**\n\n```js\nimport { Calendar } from '@toast-ui/calendar';\n\n// 요소를 직접 전달하는 경우\nconst container = document.querySelector('#container');\nconst calendar = new Calendar(container);\n\n// CSS 선택자를 이용하는 경우\nconst calendar = new Calendar('#container');\n```\n\n캘린더 인스턴스 생성 시 두 번째 인자로 옵션을 설정할 수 있다. 별도로 지정하지 않은 옵션은 기본값이 설정된다. 옵션에 대한 자세한 정보는 [옵션 문서](./options.md) 살펴볼 수 있다.\n\n```js\nconst calendar = new Calendar(container, {\n  // 캘린더 인스턴스 옵션\n  defaultView: 'month',\n  isReadOnly: true,\n  timezone: {\n    // ...\n  },\n  theme: {\n    // ...\n  },\n  template: {\n    // ...\n  },\n});\n```\n\n인스턴스를 생성한 이후 캘린더 [인스턴스 메서드](#인스턴스-메서드)를 사용하여 캘린더의 동작을 제어할 수 있고, [인스턴스 이벤트](#인스턴스-이벤트)에 이벤트 핸들러를 등록할 수 있다.\n\n```js\nconst calendar = new Calendar('#container');\n\n// 인스턴스 이벤트 설정\ncalendar.on('beforeCreateEvent', (eventObj) => {\n  // 이벤트 실행 시 인스턴스 메서드 활용\n  calendar.createEvents([\n    {\n      ...eventObj,\n      id: uuid(),\n    },\n  ]);\n});\n```\n\n## 인스턴스 메서드\n\n💡 메서드를 클릭하면 더 자세한 설명과 사용 예시를 볼 수 있다.\n\n| 메서드                                          | 설명                                                                                                                       |\n| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |\n| [render](#render)                               | 캘린더 인스턴스를 화면에 렌더링한다.                                                                                       |\n| [renderToString](#rendertostring)               | 서버 사이드 렌더링에서 활용할 수 있도록 현재 캘린더 인스턴스의 렌더링 결과물을 HTML 문자열로 리턴한다.                     |\n| [destroy](#destroy)                             | 캘린더 인스턴스를 제거한다.                                                                                                |\n| [getEvent](#getevent)                           | 지정된 이벤트의 데이터를 가져온다.                                                                                         |\n| [createEvents](#createevents)                   | 한개 이상의 캘린더 이벤트를 생성한다.                                                                                      |\n| [updateEvent](#updateevent)                     | 지정된 이벤트의 내용을 업데이트한다.                                                                                       |\n| [deleteEvent](#deleteevent)                     | 지정된 이벤트를 삭제한다.                                                                                                  |\n| [clear](#clear)                                 | 캘린더 인스턴스에 저장된 모든 이벤트를 제거한다.                                                                           |\n| [today](#today)                                 | 현재 날짜가 포함된 범위로 이동한다.                                                                                        |\n| [move](#move)                                   | 뷰에 따라 주어진 숫자만큼 구간을 이동한다.                                                                                 |\n| [prev](#prev)                                   | 현재 화면의 이전 구간으로 이동한다. 이동 범위는 뷰의 범위에 따라 다르다.                                                   |\n| [next](#next)                                   | 현재 화면의 다음 구간으로 이동한다. 이동 범위는 뷰의 범위에 따라 다르다.                                                   |\n| [setDate](#setdate)                             | 지정된 날짜가 포함된 범위로 이동한다.                                                                                      |\n| [changeView](#changeview)                       | 캘린더 인스턴스의 뷰를 월간/주간/일간으로 변경한다.                                                                        |\n| [getElement](#getelement)                       | 특정 이벤트가 렌더링된 HTML 엘리먼트를 찾는다. 찾지 못한 경우 `null` 을 리턴한다.                                          |\n| [setTheme](#settheme)                           | 캘린더 인스턴스의 테마를 변경한다.                                                                                         |\n| [getOptions](#getoptions)                       | 현재 캘린더 인스턴스에 설정된 옵션 정보를 가져온다.                                                                        |\n| [setOptions](#setoptions)                       | 캘린더 인스턴스의 옵션을 변경한다.                                                                                         |\n| [getDate](#getdate)                             | 현재 캘린더 인스턴스의 화면을 표시하는 기준 날짜를 가져온다.                                                               |\n| [getDateRangeStart](#getdaterangestart)         | 현재 캘린더 인스턴스의 화면을 표시하는 날짜 범위 중 시작일을 가져온다.                                                     |\n| [getDateRangeEnd](#getdaterangeend)             | 현재 캘린더 인스턴스의 화면을 표시하는 날짜 범위 중 종료일을 가져온다.                                                     |\n| [getViewName](#getviewname)                     | 현재 캘린더 인스턴스의 화면의 타입을 가져온다. (월간 / 주간 / 일간)                                                        |\n| [setCalendars](#setcalendars)                   | 캘린더 정보를 변경한다.                                                                                                    |\n| [setCalendarVisibility](#setcalendarvisibility) | 지정된 이벤트 그룹에 포함되는 모든 이벤트를 숨기거나 표시한다.                                                             |\n| [setCalendarColor](#setcalendarcolor)           | 지정된 이벤트 그룹에 포함된 모든 이벤트의 컬러 값을 변경한다.                                                              |\n| [scrollToNow](#scrolltonow)                     | 일간 뷰 혹은 주간 뷰에서 현재 시간이 포함된 범위를 표시하고 있는 경우, 즉시 현재 시간 영역으로 스크롤을 이동한다.          |\n| [openFormPopup](#openformpopup)                 | 이벤트를 생성하는데 사용하는 팝업을 표시한다. 전달한 객체의 정보에 따라 팝업의 값이 채워져 있다.                           |\n| [clearGridSelections](#cleargridselections)     | 현재 캘린더에 표시된 모든 날짜/시간 선택 엘리먼트를 제거한다.                                                              |\n| [fire](#fire)                                   | 임의의 인스턴스 이벤트를 실행한다. 자세한 설명은 [인스턴스 이벤트](#인스턴스-이벤트) 단락에서 설명한다.                    |\n| [off](#off)                                     | 임의의 인스턴스 이벤트를 해제한다. 자세한 설명은 [인스턴스 이벤트](#인스턴스-이벤트) 단락에서 설명한다.                    |\n| [on](#on)                                       | 임의의 인스턴스 이벤트를 할당한다. 자세한 설명은 [인스턴스 이벤트](#인스턴스-이벤트) 단락에서 설명한다.                    |\n| [once](#once)                                   | 임의의 인스턴스 이벤트를 한 번만 실행되도록 할당한다. 자세한 설명은 [인스턴스 이벤트](#인스턴스-이벤트) 단락에서 설명한다. |\n\n### render\n\n- 타입: `render(): Calendar`\n- 리턴: `Calendar` - 캘린더 인스턴스\n\n브라우저 환경에서 캘린더 인스턴스를 생성하면 자동으로 호출되는 메서드이다.\n\n처음 호출될 때 인스턴스 생성 시 인자로 전달한 HTML 엘리먼트 아래에 캘린더 요소가 삽입되며, 직접 `render` 메서드를 호출하는 경우 다시 렌더링한다.\n\n만약 인스턴스 생성 시 HTML 컨테이너가 전달되지 않았다면 아무 일도 일어나지 않는다.\n\n호출 후 캘린더 인스턴스 자신을 반환한다.\n\n```js\n// 아무 일도 일어나지 않는 경우\nconst calendar = new Calendar();\ncalendar.render();\n\n// 엘리먼트가 있는 경우 자동으로 최초 1회 렌더링 됨\nconst calendar = new Calendar('#container');\n\n// 위의 상태에서 강제로 다시 렌더링\ncalendar.render();\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### renderToString\n\n- 타입: `renderToString(): string`\n- 리턴: `string` - HTML 문자열\n\n주어진 옵션 값을 기반으로 캘린더 인스턴스가 렌더링할 HTML 문자열을 생성하여 리턴한다. 서버 사이드 렌더링 환경에서 활용할 수 있다.\n\n```js\nconst isSSR = typeof window === 'undefined';\n\n// 클라이언트라면 인스턴스 생성 후 `#container`에 자동으로 마운트되지만, 서버에서는 아무 일도 일어나지 않는다.\nconst calendar = new Calendar('#container');\n\nif (isSSR) {\n  const calendarHTML = calendar.renderToString();\n  // 서버에서 클라이언트로 HTML 전송 처리\n}\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### destroy\n\n- 타입: `destroy(): void`\n\n캘린더 인스턴스를 통해 렌더링 된 요소를 삭제하고, 인스턴스를 빈 객체로 만든다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### getEvent\n\n- 타입: `getEvent(eventId: string, calendarId: string): EventObject`\n- 파라미터\n  - `eventId` - 이벤트의 고유 ID\n  - `calendarId` - 캘린더의 고유 ID\n- 리턴: `EventObject` - 이벤트 정보가 담긴 객체\n\n캘린더 인스턴스 안에 저장된 이벤트를 찾는다. 이벤트 고유의 `eventId` 와, 캘린더 고유의 `calendarId` 값이 필요하다.\n\n```js\nconst calendar = new Calendar('#container', {\n  calendars: [\n    {\n      id: 'cal1',\n      name: 'Work',\n    },\n  ],\n});\n\ncalendar.createEvents([\n  {\n    id: 'event1',\n    calendarId: 'cal1',\n    title: 'Weekly Meeting',\n    start: '2022-05-30T09:00:00',\n    end: '2022-05-30T10:00:00',\n  },\n]);\n\nconst firstEvent = calendar.getEvent('event1', 'cal1');\n\nconsole.log(firstEvent.title); // 'Weekly Meeting'\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### createEvents\n\n- 타입: `createEvents(events: EventObject[]): void`\n- 파라미터\n  - `EventObject[]` - 저장하려는 이벤트 정보의 배열\n\n한 개 이상의 이벤트를 생성한다. 한 개의 이벤트를 생성할 때에도 배열을 전달해야 한다.\n\n이벤트를 생성할 때 필요한 정보는 [EventObject](./event-object.md) 문서를 참고한다.\n\n```js\n// 한 개의 이벤트 생성\ncalendar.createEvents([\n  {\n    id: 'event1',\n    calendarId: 'cal1',\n    title: 'Weekly Meeting',\n    start: '2022-05-30T09:00:00',\n    end: '2022-05-30T10:00:00',\n  },\n]);\n\n// 여러 개의 이벤트 생성\ncalendar.createEvents([\n  {\n    id: 'event1',\n    calendarId: 'cal1',\n    title: 'Weekly Meeting',\n    start: '2022-05-30T09:00:00',\n    end: '2022-05-30T10:00:00',\n  },\n  {\n    id: 'event2',\n    calendarId: 'cal2',\n    title: 'Lunch with Teammates',\n    start: '2022-05-30T12:00:00',\n    end: '2022-05-30T13:00:00',\n  },\n]);\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### updateEvent\n\n- 타입: `updateEvent(eventId: string, calendarId: string, changes: EventObject): void`\n- 파라미터\n  - `eventId` - 이벤트의 고유 ID\n  - `calendarId` - 캘린더의 고유 ID\n  - `changes` - 변경하려는 내용\n\n생성된 이벤트의 정보를 변경한다. 이벤트를 찾기 위해 이벤트 고유의 `eventId`, 캘린더 고유의 `calendarId`,\n그리고 변경하려는 정보가 담긴 객체가 필요하다. 변경하려는 속성과 값이 `EventObject` 의 속성과 일치해야 한다.\n\n```js\n// 먼저 아래와 같이 이벤트가 생성되었다고 가정한다.\ncalendar.createEvents([\n  {\n    id: 'event1',\n    calendarId: 'cal1',\n    title: 'Weekly Meeting',\n    start: '2022-05-30T09:00:00',\n    end: '2022-05-30T10:00:00',\n  },\n]);\n\n// title 속성 하나만 바꾸는 경우\ncalendar.updateEvent('event1', 'cal1', {\n  title: 'Weekly Meeting (Canceled)',\n});\n\n// 여러 속성을 바꾸는 경우\ncalendar.updateEvent('event1', 'cal1', {\n  title: 'Going vacation',\n  state: 'Free',\n  start: '2022-05-30T00:00:00',\n  end: '2022-06-03T23:59:59',\n});\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### deleteEvent\n\n- 타입: `deleteEvent(eventId: string, calendarId: string): void`\n- 파라미터\n  - `eventId` - 이벤트의 고유 ID\n  - `calendarId` - 캘린더의 고유 ID\n\n생성된 이벤트를 삭제한다.\n\n```js\n// 먼저 아래와 같이 이벤트가 생성되었다고 가정한다.\ncalendar.createEvents([\n  {\n    id: 'event1',\n    calendarId: 'cal1',\n    title: 'Weekly Meeting',\n    start: '2022-05-30T09:00:00',\n    end: '2022-05-30T10:00:00',\n  },\n]);\n\ncalendar.deleteEvent('event1', 'cal1');\n\n// 이벤트를 찾으려 하면 없다.\nconst deletedEvent = calendar.getEvent('event1', 'cal1');\nconsole.log(deletedEvent); // null\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### clear\n\n- 타입: `clear(): void`\n\n캘린더 인스턴스 안의 모든 이벤트를 제거한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### today\n\n- 타입: `today(): void`\n\n캘린더 인스턴스의 표시 범위를 현재 날짜가 있는 곳으로 이동한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### move\n\n- 타입: `move(offset: number): void`\n- 파라미터\n  - `offset` - 이동할 범위를 정수로 입력한다. 범위가 주어지지 않으면 아무런 일도 일어나지 않는다.\n\n캘린더 인스턴스의 표시 범위를 앞이나 뒤로 이동한다. 양수를 넣으면 현재 범위 기준 미래로 이동하고, 음수를 넣으면 과거로 이동한다.\n\n이동하는 범위는 월간 / 주간 / 일간 뷰에 따라 각각 한 달 / 한 주 / 하루 단위로 다르며, 설정한 옵션에 따라 세세한 차이가 있을 수 있다.\n\n```js\n// 월간 뷰에서 작년으로 이동\ncalendar.move(-12);\n\n// 일간 뷰에서 3일 뒤로 이동\ncalendar.move(3);\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### prev\n\n- 타입: `prev(): void`\n\n캘린더 인스턴스의 표시 범위를 한 단위 이전 시간대로 이동한다.\n\n이동하는 범위는 월간 / 주간 / 일간 뷰에 따라 각각 한 달 / 한 주 / 하루 단위로 다르며, 설정한 옵션에 따라 세세한 차이가 있을 수 있다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### next\n\n- 타입: `next(): void`\n\n캘린더 인스턴스의 표시 범위를 한 단위 이후 시간대로 이동한다.\n\n이동하는 범위는 월간 / 주간 / 일간 뷰에 따라 각각 한 달 / 한 주 / 하루 단위로 다르며, 설정한 옵션에 따라 세세한 차이가 있을 수 있다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### setDate\n\n- 타입: `setDate(date: DateType): void`\n- 파라미터\n  - `date` - 시간 정보를 담고있는 객체 혹은 문자열. `Date` 객체나, `Date` 객체를 생성하는데 쓰이는 문자열, 혹은 `TZDate` 객체를 전달할 수 있다.\n\n캘린더 인스턴스가 화면을 표시하는 기준 날짜를 변경한다. 그 결과 `date` 를 기준으로 표시 범위가 이동한다.\n\n`move` 메서드가 범위 단위로 이동하는 것과 달리 `setDate` 는 특정 날짜로 바로 이동할 수 있다.\n\n```js\n// 월간 뷰에서 2022년 3월로 이동 (문자열)\ncalendar.setDate('2022-03-01');\n\n// Date 객체를 직접 전달\ncalendar.setDate(new Date(2022, 4, 1));\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### changeView\n\n- 타입: `changeView(viewName: ViewType): void`\n- 파라미터\n  - `viewName` - 선택하려는 뷰의 종류. `'month'`, `'week'`, `'day'` 를 전달할 수 있다.\n\n캘린더 인스턴스의 뷰 종류를 월간 / 주간 / 일간 뷰로 변경한다.\n\n```js\n// 월간 뷰로 변경\ncalendar.changeView('month');\n\n// 주간 뷰로 변경\ncalendar.changeView('week');\n\n// 일간 뷰로 변경\ncalendar.changeView('day');\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### getElement\n\n- 타입: `getElement(eventId: string, calendarId: string): HTMLElement | null`\n- 파라미터\n  - `eventId` - 이벤트의 고유 ID\n  - `calendarId` - 캘린더의 고유 ID\n- 리턴: `HTMLElement | null` - 이벤트를 찾았다면 해당 이벤트의 HTML 엘리먼트를 리턴하고 찾지 못했다면 `null` 을 리턴한다.\n\n캘린더 인스턴스가 렌더링하고 있는 이벤트의 실제 HTML 엘리먼트를 검색하여 리턴한다.\n\n이벤트를 찾지 못했을 때는 `null` 을 리턴한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### setTheme\n\n- 타입: `setTheme(theme: DeepPartial<ThemeState>): void`\n- 파라미터: `theme` - 테마 설정이 담긴 객체\n\n캘린더 인스턴스의 테마를 변경한다. 적용 가능한 테마는 [테마 문서](./theme.md)를 참고한다.\n\n```js\n// 월간 뷰에서 표현되는 주말의 배경색을 변경하는 예\ncalendar.setTheme({\n  month: {\n    weekend: {\n      backgroundColor: 'aliceblue',\n    },\n  },\n});\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### getOptions\n\n- 타입: `getOptions(): void`\n\n현재 캘린더 인스턴스의 옵션 전체를 리턴한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### setOptions\n\n- 타입: `setOptions(options: Options): void`\n- 파라미터\n  - `options` - 캘린더 인스턴스가 사용하는 옵션 객체\n\n현재 캘린더 인스턴스의 옵션을 변경한다. 각 옵션과 자세한 동작은 [옵션 문서](./options.md)를 참고한다.\n\n```js\n// 타임존 변경 예\ncalendar.setOptions({\n  timezone: {\n    zones: [\n      {\n        timezoneName: 'Europe/London',\n      },\n    ],\n  },\n});\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### getDate\n\n- 타입: `getDate(): TZDate`\n- 리턴: `TZDate` - 시간 정보를 갖고 있는 객체\n\n캘린더 인스턴스의 현재 화면을 렌더링하는데 사용되는 기준 날짜의 정보를 리턴한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### getDateRangeStart\n\n- 타입: `getDateRangeStart(): TZDate`\n- 리턴: `TZDate` - 시간 정보를 갖고 있는 객체\n\n캘린더 인스턴스가 현재 렌더링하고 있는 날짜의 범위 중 시작 시간을 리턴한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### getDateRangeEnd\n\n- 타입: `getDateRangeEnd(): TZDate`\n- 리턴: `TZDate` - 시간 정보를 갖고 있는 객체\n\n캘린더 인스턴스가 현재 렌더링하고 있는 날짜의 범위 중 종료 시간을 리턴한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### getViewName\n\n- 타입: `getViewName(): ViewType`\n- 리턴: `ViewType` - 현재 캘린더 뷰의 타입. `month`, `week`, `day` 로 나뉜다.\n\n현재 캘린더 인스턴스가 표시하고 있는 뷰 타입을 리턴한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### setCalendars\n\n- 타입: `setCalendars(calendars: CalendarInfo[]): void`\n- 파라미터\n  - `calendars` - 캘린더 정보의 배열. 캘린더 정보는 아래의 타입을 가진다. 자세한 내용은 [캘린더](./event-object.md#캘린더calendarid) 문서에서 확인하라.\n\n```ts\ninterface CalendarInfo {\n  id: string;\n  name: string;\n  color?: string;\n  bgColor?: string;\n  dragBgColor?: string;\n  borderColor?: string;\n}\n```\n\n하나 이상의 캘린더를 설정한다.\n\n```js\ncalendar.setCalendars([\n  {\n    id: 'cal1',\n    name: 'Personal',\n    color: '#ffffff',\n    backgroundColor: '#9e5fff',\n    dragBackgroundColor: '#9e5fff',\n    borderColor: '#9e5fff',\n  },\n  {\n    id: 'cal2',\n    name: 'Work',\n    color: '#00a9ff',\n    backgroundColor: '#00a9ff',\n    dragBackgroundColor: '#00a9ff',\n    borderColor: '#00a9ff',\n  },\n]);\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### setCalendarVisibility\n\n- 타입: `setCalendarVisibility(calendarId: string | string[], isVisible: boolean): void`\n- 파라미터\n  - `calendarId` - 보이기/숨기기를 적용할 캘린더의 고유 아이디. 한 개 혹은 배열로 여러 개를 전달할 수 있다.\n  - `isVisible` - 해당 캘린더에 속한 모든 이벤트를 보이거나 숨기기 위한 값. `true` 이면 모두 보이게 하고, `false` 이면 모두 보이지 않게 한다.\n\n특정 캘린더에 포함된 모든 이벤트를 보이게 하거나 숨긴다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### setCalendarColor\n\n- 타입: `setCalendarColor(calendarId: string, colorOptions: CalendarColor): void`\n- 파라미터\n  - `calendarId` - 캘린더의 고유 아이디\n  - `colorOptions` - 적용할 컬러 설정값. 자세한 내용은 [캘린더 문서](./event-object.md#캘린더calendarid)를 참고한다.\n\n특정 이벤트 그룹에 속한 모든 이벤트의 색상 값을 변경할 때 사용한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### scrollToNow\n\n- 타입: `scrollToNow(scrollBehavior?: 'auto' | 'smooth'): void`\n\n주간 뷰 혹은 일간 뷰인 상태에서 현재 시간이 포함된 시간 범위를 표시하고 있을 때, 현재 시간이 있는 위치로 스크롤을 이동한다. IE11에서는 `'smooth'`를 넣어도 동작하지 않는다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### openFormPopup\n\n- 타입: `openFormPopup(event: EventObject): void`\n- 파라미터\n  - `event` - 이벤트 정보를 가지고 있는 객체. 자세한 내용은 [EventObject 문서](./event-object.md)를 참고한다.\n\n`useFormPopup` 옵션이 `true` 일 때, 별도의 동작을 거치지 않고 바로 일정 생성을 하는 팝업을 표시한다.\n\n파라미터로 전달한 이벤트 데이터가 팝업 안의 입력 필드에 지정된 채로 표시된다.\n\n```js\ncalendar.openFormPopup({\n  id: 'some-event-id',\n  calendarId: 'cal1',\n  title: 'Go to live concert',\n  start: '2022-05-31T09:00:00',\n  end: '2022-05-31T12:00:00',\n  category: 'time',\n});\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### clearGridSelections\n\n- 타입: `clearGridSelections(): void`\n\n현재 캘린더에 표시된 모든 날짜/시간 선택 엘리먼트를 제거한다.\n\n```js\ncalendar.clearGridSelections();\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### fire\n\n- 타입: `fire(eventName: string, ...args: any[]): Calendar`\n- 파라미터\n  - `eventName` - 이벤트의 이름\n  - `...args` - 이벤트 핸들러에 전달할 파라미터\n- 리턴: 현재 캘린더 인스턴스\n\n임의의 [인스턴스 이벤트](#인스턴스-이벤트)를 실행한다. 이벤트가 등록되어있는 경우, 등록된 이벤트 핸들러에 첫 번째 파라미터 이후의 모든 파라미터를 전달한다.\n\n인스턴스 이벤트에 대한 자세한 설명은 [인스턴스 이벤트](#인스턴스-이벤트) 단락을 참고한다.\n\n```js\n// 먼저 다음과 같은 이벤트가 등록되어있다고 가정하자.\ncalendar.on('beforeCreateEvent', (data) => {\n  console.log(`from: ${data.start.toDateString()} to ${data.end.toDateString()}`);\n});\n\ncalendar.fire('beforeCreateEvent', {\n  start: new Date('2022-05-31'),\n  end: new Date('2022-06-01'),\n});\n// output: 'from Tue May 31 2022 to Wed Jun 01 2022'\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### off\n\n- 타입: `off(eventName: string, handler?: (...args: any[]) => void): Calendar`\n- 파라미터\n  - `eventName` - 이벤트의 이름\n  - `handler` - 이벤트에 등록된 핸들러 함수\n- 리턴: 현재 캘린더 인스턴스\n\n등록된 인스턴스 이벤트를 해제한다. 핸들러를 전달하지 않은 경우 해당 이벤트에 등록된 모든 핸들러가 해제된다.\n\n핸들러를 파라미터로 전달한 경우, 해당 핸들러만 해제한다.\n\n인스턴스 이벤트에 대한 자세한 설명은 [인스턴스 이벤트](#인스턴스-이벤트) 단락을 참고한다.\n\n```js\nconst someEventHandler = () => {\n  console.log('some event fired');\n};\n\ncalendar.on('some-event', someEventHandler);\n\ncalendar.fire('some-event');\n// output: 'some event fired'\n\ncalendar.off('some-event', someEventHandler);\n\ncalendar.fire('some-event'); // 아무 일도 일어나지 않는다.\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### on\n\n- 타입: `on(eventName: string, handler: (...args: any[]) => void): Calendar`\n- 파라미터\n  - `eventName` - 이벤트의 이름\n  - `handler` - 이벤트에 등록된 핸들러 함수\n- 리턴: 현재 캘린더 인스턴스\n\n인스턴스 이벤트를 등록한다. 등록된 이벤트 이름이 `fire` 메서드를 통해 호출된 경우, `on` 을 통해 등록된 모든 핸들러가 실행된다.\n\n인스턴스 이벤트에 대한 자세한 설명은 [인스턴스 이벤트](#인스턴스-이벤트) 단락을 참고한다.\n\n```js\n// 이벤트 등록\ncalendar.on('beforeCreateEvent', (data) => {\n  console.log(`from: ${data.start.toDateString()} to ${data.end.toDateString()}`);\n});\n\ncalendar.fire('beforeCreateEvent', {\n  start: new Date('2022-05-31'),\n  end: new Date('2022-06-01'),\n});\n// output: 'from Tue May 31 2022 to Wed Jun 01 2022'\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### once\n\n- 타입: `once(eventName: string, handler: (...args: any[]) => void): Calendar`\n- 파라미터\n  - `eventName` - 이벤트의 이름\n  - `handler` - 이벤트에 등록된 핸들러 함수\n- 리턴: 현재 캘린더 인스턴스\n\n인스턴스 이벤트를 등록한다. 등록된 이벤트 이름이 `fire` 메서드를 통해 호출된 경우, `once` 을 통해 등록된 모든 핸들러가 한 번만 실행된다.\n\n인스턴스 이벤트에 대한 자세한 설명은 [인스턴스 이벤트](#인스턴스-이벤트) 단락을 참고한다.\n\n```js\n// 이벤트를 한 번만 등록\ncalendar.once('beforeCreateEvent', (data) => {\n  console.log(`from: ${data.start.toDateString()} to ${data.end.toDateString()}`);\n});\n\ncalendar.fire('beforeCreateEvent', {\n  start: new Date('2022-05-31'),\n  end: new Date('2022-06-01'),\n});\n// output: 'from Tue May 31 2022 to Wed Jun 01 2022'\n\ncalendar.fire('beforeCreateEvent', {\n  start: new Date('2022-06-01'),\n  end: new Date('2022-06-02'),\n});\n// 아무 일도 일어나지 않는다.\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n## 인스턴스 이벤트\n\n캘린더의 모든 동작은 메서드만으로 제어할 수 없다. 클릭이나 드래그 앤 드랍같은 사용자의 인터랙션이 언제 일어날지 알 수 없기 때문이다.\n\n따라서 TOAST UI 캘린더는 인스턴스 이벤트를 제공한다. 일부 사용자가 일으킬 수 있는 인터랙션은 미리 정의해두고, 필요에 따라 이 이벤트를 수신하도록 설정하여\n원하는 동작을 실행시킬 수 있다. 또한 별도로 사용자가 자신만의 이벤트를 설정할 수도 있다.\n\n```js\n// 커스텀 이벤트와 이벤트 핸들러를 등록\ncalendar.on('myCustomEvent', (currentView) => {\n  calendar.changeView(currentView === 'week' ? 'day' : 'month');\n});\n\n// 커스텀 이벤트 실행\ncalendar.fire('myCustomEvent', calendar.getViewName());\n```\n\n### 인스턴스 이벤트 목록\n\n사전에 정의된 인스턴스 이벤트의 목록은 다음과 같다.\n\n| 이벤트 이름                                           | 설명                                                                                        |\n| ----------------------------------------------------- | ------------------------------------------------------------------------------------------- |\n| [selectDateTime](#selectdatetime)                     | 특정 날짜 혹은 시간을 드래그 앤 드랍했을 때 발생                                            |\n| [beforeCreateEvent](#beforecreateevent)               | 기본 일정 생성/수정 팝업에서 저장(Save) 버튼을 눌렀을 때 발생                               |\n| [beforeUpdateEvent](#beforeupdateevent)               | 기본 일정 생성/수정 팝업에서 저장(Save) 버튼을 누르거나 이벤트를 드래그 앤 드랍했을 때 발생 |\n| [beforeDeleteEvent](#beforedeleteevent)               | 기본 일정 상세 팝업에서 삭제(Delete) 버튼을 눌렀을 때 발생                                  |\n| [afterRenderEvent](#afterrenderevent)                 | 모든 이벤트가 렌더링 될 때 한 번씩 발생                                                     |\n| [clickDayName](#clickdayname)                         | 캘린더 상단의 요일을 클릭할 때 발생                                                         |\n| [clickEvent](#clickevent)                             | 이벤트를 클릭할 때 발생                                                                     |\n| [clickMoreEventsBtn](#clickmoreeventsbtn)             | 월간 뷰의 각 셀마다 이벤트 갯수가 초과되어 나타난 'More' 버튼을 클릭할 때 발생              |\n| [clickTimezoneCollapseBtn](#clicktimezonecollapsebtn) | 주간 뷰 혹은 일간 뷰에서 여러 개의 타임존을 표시한 경우 나타나는 접기 버튼을 클릭할 때 발생 |\n\n각각의 이벤트는 실행될 때 이벤트 핸들러 함수에 특정 파라미터를 전달한다.\n\n### selectDateTime\n\n- 파라미터: `info: SelectDateTimeInfo` - 선택된 시간의 정보 등을 가지고 있다.\n\n```ts\ninterface SelectDateTimeInfo {\n  start: Date;\n  end: Date;\n  isAllday: boolean;\n  nativeEvent?: MouseEvent; // 마우스를 떼었을 때의 네이티브 이벤트\n  gridSelectionElements: Element[]; // 선택 영역에 해당하는 엘리먼트 목록\n}\n```\n\n캘린더 인스턴스 옵션 중 `isReadOnly` 가 `false` 일 때, 캘린더 영역의 빈 공간을 클릭하거나 드래그 앤 드랍 했을 경우 특정 날짜 혹은 시간을 선택할 수 있다.\n\n이 때 `selectDateTime` 이벤트를 통해 선택된 시간의 정보를 얻을 수 있다. 그리고 `gridSelectionElements`를 통해 엘리먼트를 직접 활용하여 위치 계산 등을 할 수 있다.\n\n![월간 뷰에서 날짜 선택](../../assets/select-date-time-1.gif)\n![주간 / 일간 뷰에서 시간 선택](../../assets/select-date-time-2.gif)\n\n[⬆️ 목록으로 돌아가기](#인스턴스-이벤트-목록)\n\n### beforeCreateEvent\n\n- 파라미터: `event: EventObject` - 이벤트 생성 팝업에서 입력된 정보가 전달된다. 전달되는 객체의 자세한 정보는 [EventObject 문서](./event-object.md)를 참고한다.\n\n캘린더 인스턴스 옵션 중 `useFormPopup` 가 `true` 일 때, 기본 이벤트 생성 팝업을 사용할 수 있다.\n\n이 이벤트 생성 팝업에서 저장(Save) 버튼을 눌렀을 때 `beforeCreateEvent` 인스턴스 이벤트가 가 발생한다.\n\n```js\n// 팝업을 통해 이벤트를 생성\ncalendar.on('beforeCreateEvent', (eventObj) => {\n  calendar.createEvents([\n    {\n      ...eventObj,\n      id: uuid(),\n    },\n  ]);\n});\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-이벤트-목록)\n\n### beforeUpdateEvent\n\n- 파라미터: `updatedEventInfo: UpdatedEventInfo` - 기존 이벤트 정보와 수정된 이벤트 정보를 가지고 있다. 전달되는 객체의 자세한 정보는 [EventObject 문서](./event-object.md)를 참고한다.\n\n```ts\ninterface UpdatedEventInfo {\n  event: EventObject;\n  changes: EventObject;\n}\n```\n\n`event` 속성은 기존 이벤트의 정보이며, `changes` 속성은 기존 이벤트 정보와 다른 속성과 값만 포함하고 있다.\n\n다음의 경우 `beforeUpdateEvent` 가 실행된다.\n\n- 캘린더 인스턴스 옵션 중 `useFormPopup` 와 `useDetailPopup` 가 모두 `true` 이고, 이벤트 상세 팝업에서 Edit 버튼을 누른 후 표시되는 이벤트 폼 팝업에서 Update 버튼을 누를 때\n  - ![팝업을 통한 이벤트 실행](../../assets/before-update-event-1.gif)\n- 캘린더의 인스턴스 옵션 중 `useDetailPopup` 이 `true` 이고, `useFormPopup` 이 `false` 일 때, 이벤트 상세 팝업에서 Edit 버튼을 누른 경우\n- 캘린더 인스턴스 옵션 중 `isReadOnly` 가 `true` 가 아니며, 개별 이벤트의 속성에 `isReadOnly` 가 `true` 가 아닌 상태에서 드래그 앤 드랍으로 일정을 이동하거나 리사이징할 때\n  - ![드래그 앤 드랍을 통한 이벤트 실행](../../assets/before-update-event-2.gif)\n\n```js\n// 이벤트를 수정하는 기본적인 예\ncalendar.on('beforeUpdateEvent', ({ event, change }) => {\n  calendar.updateEvent(event.id, event.calendarId, change);\n});\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-이벤트-목록)\n\n### beforeDeleteEvent\n\n- 파라미터: `event: EventObject` - 삭제할 이벤트의 정보가 전달된다. 전달되는 객체의 자세한 정보는 [EventObject 문서](./event-object.md)를 참고한다.\n\n캘린더 인스턴스 옵션 중 `isReadOnly` 와 개별 이벤트 속성에 `isReadOnly` 모두 `true` 가 아니며, `useDetailPopup` 옵션이 `true` 일 때, 이벤트 상세 팝업을 통해 이벤트를 선택 후 Delete 버튼을 누를 수 있다.\n\n이 때 Delete 버튼을 누르면 `beforeDeleteEvent` 가 실행된다.\n\n![팝업을 통해 beforeDeleteEvent 이벤트 실행](../../assets/before-delete-event.gif)\n\n```js\n// 일정을 삭제하는 아주 기본적인 예\ncalendar.on('beforeDeleteEvent', (eventObj) => {\n  calendar.deleteEvent(eventObj.id, eventObj.calendarId);\n});\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-이벤트-목록)\n\n### afterRenderEvent\n\n- 파라미터: `event: EventObject` - 이벤트 정보가 전달된다. 전달되는 객체의 자세한 정보는 [EventObject 문서](./event-object.md)를 참고한다.\n\n각각의 이벤트가 캘린더 안에서 렌더링 될 때마다 실행된다.\n\n이벤트의 생성, 수정, 삭제 등으로 인해 이벤트가 렌더링 될 때마다 **화면에 보이는 모든 이벤트가** `afterRenderEvent` 를 실행한다.\n\n### clickDayName\n\n- 파라미터: `dayNameInfo: DayNameInfo` - 클릭한 날짜의 정보가 `YYYY-MM-DD` 포맷의 문자열로 나타난다.\n\n주간 / 일간 뷰의 헤더 영역은 현재 보고 있는 시간 범위의 요일과 날짜를 표시한다. 이 때 이 헤더의 특정 요일을 클릭할 때 `clickDayName` 이벤트가 실행된다.\n\n**월간 뷰에서는 이 이벤트가 발생하지 않는다.**\n\n![날짜를 클릭할 때 clickDayName 이벤트 실행](../../assets/click-dayName.gif)\n\n### clickEvent\n\n- 파라미터: `eventInfo: EventInfo` - 클릭한 이벤트의 정보와 함께 네이티브 `MouseEvent` 가 같이 전달된다.\n\n```ts\ninterface EventInfo {\n  event: EventObject;\n  nativeEvent: MouseEvent;\n}\n```\n\n이벤트 정보의 자세한 내용은 [EventObject 문서](./event-object.md)를 참고한다.\n\n캘린더에 렌더링 된 각각의 이벤트를 클릭할 때마다 `clickEvent` 이벤트가 실행된다.\n\n⚠️ 이 이벤트는 캘린더 인스턴스 옵션 중 `isReadOnly` 가 `true` 이거나, 클릭하려는 이벤트의 속성 중 `isReadOnly` 가 `true` 인 경우 실행되지 않는다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-이벤트-목록)\n\n### clickMoreEventsBtn\n\n- 파라미터: `moreEventsBtnInfo: MoreEventsBtnInfo` - 클릭한 버튼의 정보가 전달된다.\n\n```ts\ninterface MoreEventsBtnInfo {\n  date: Date; // 클릭한 버튼의 날짜\n  target: HTMLDivElement; // 버튼을 클릭하여 나타나는 팝업의 DOM 엘리먼트\n}\n```\n\n월간 뷰에서 한 날짜에 해당되는 이벤트 많아 더 이상 화면에 표시할 수 없는 상태일 때 'more' 버튼이 표시된다. 이 버튼을 클릭하면 `clickMoreEventsBtn` 이벤트가 실행된다.\n\n![월간 뷰의 more 이벤트 버튼](../../assets/click-more-events-btn.png)\n\n[⬆️ 목록으로 돌아가기](#인스턴스-이벤트-목록)\n\n### clickTimezonesCollapseBtn\n\n- 파라미터: `prevCollapsedState` - 클릭한 버튼의 이전 접힘 상태가 `true` 또는 `false` 가 전달된다.\n\n캘린더 인스턴스 옵션에서 [타임존](./options.md#timezone)을 두 개 이상 사용하고, `week.showTimezoneCollapsedButton` 이 `true` 인 경우\n주간 / 일간 뷰에서 타임존을 접거나 펼칠 수 있는 버튼이 표시된다. 이 버튼을 클릭하면 `clickTimezonesCollapseBtn` 이벤트가 실행된다.\n\n![타임존 접기 버튼](../../assets/click-timezones-collapse-btn.png)\n\n```js\n// 버튼을 누를 때 주간 / 일간 뷰의 왼쪽 영역을 조절하는 예제\ncalendar.on('clickTimezonesCollapseBtn', (prevCollapsedState) => {\n  const shouldCollapse = prevCollapsedState === false;\n\n  calendar.setOptions({\n    week: {\n      timezonesCollapsed: !prevCollapsedState,\n    },\n    theme: {\n      week: {\n        timeGridLeft: shouldCollapse ? '72px' : '150px',\n      },\n    },\n  });\n});\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-이벤트-목록)\n\n### 커스텀 인스턴스 이벤트\n\n위의 이벤트와 별도로 사용자는 필요에 따라 별도의 커스텀 인스턴스 이벤트를 지정할 수 있다.\n\n목록에 언급되지 않은 이벤트 이름을 사용하고 `once` 혹은 `on` 으로 이벤트를 지정한 뒤 `fire` 메서드를 호출하면 이벤트가 실행된다.\n\n이벤트를 설정하고 실행하기 위해 필요한 메서드는 다음과 같다.\n\n- [on](#on)\n- [once](#once)\n- [fire](#fire)\n- [off](#off)\n\n```js\ncalendar.on('myCustomEvent', (e) => {\n  console.log('myCustomEvent fired', e);\n});\n\ncalendar.fire('myCustomEvent', {\n  myCustomEvent: 'myCustomEvent',\n});\n```\n\n[⬆️ 목록으로 돌아가기](#인스턴스-이벤트-목록)\n"
  },
  {
    "path": "docs/ko/apis/event-object.md",
    "content": "# EventObject\n\n## 설명\n\nEventObject는 일정 정보를 담은 순수 자바스크립트 객체이다. 이벤트를 생성할 때, 특정 이벤트를 찾을 때, 인스턴스 이벤트를 다룰 때 등 다양한 API에서 사용되는 값이다.\n\n```js\nconst calendar = new Calendar('#container');\ncalendar.createEvents([\n  {\n    id: '1',\n    calendarId: 'cal1',\n    title: 'timed event',\n    body: 'TOAST UI Calendar',\n    start: '2022-06-01T10:00:00',\n    end: '2022-06-01T11:00:00',\n    location: 'Meeting Room A',\n    attendees: ['A', 'B', 'C'],\n    category: 'time',\n    state: 'Free',\n    isReadOnly: true,\n    color: '#fff',\n    backgroundColor: '#ccc',\n    customStyle: {\n      fontStyle: 'italic',\n      fontSize: '15px',\n    },\n  }, // EventObject\n]);\n\nconst timedEvent = calendar.getEvent('1', 'cal1'); // EventObject\ncalendar.on('clickEvent', ({ event }) => {\n  console.log(event); // EventObject\n});\n```\n\n## EventObject 객체\n\nEventObject는 아래와 같은 프로퍼티로 이루어져 있다. 일부 항목은 추가 설명이 있으며, 링크를 누르면 이동한다.\n\n```ts\ninterface EventObject {\n  id?: string;\n  calendarId?: string;\n  title?: string;\n  body?: string;\n  isAllday?: boolean;\n  start?: Date | string | number | TZDate;\n  end?: Date | string | number | TZDate;\n  goingDuration?: number;\n  comingDuration?: number;\n  location?: string;\n  attendees?: string[];\n  category?: 'milestone' | 'task' | 'allday' | 'time';\n  recurrenceRule?: string;\n  state?: 'Busy' | 'Free';\n  isVisible?: boolean;\n  isPending?: boolean;\n  isFocused?: boolean;\n  isReadOnly?: boolean;\n  isPrivate?: boolean;\n  color?: string;\n  backgroundColor?: string;\n  dragBackgroundColor?: string;\n  borderColor?: string;\n  customStyle?: JSX.CSSProperties;\n  raw?: any;\n}\n```\n\n| 프로퍼티                                    | 기본값                    | 설명                                                                                                                                                                                                              |\n| ------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| id                                          | <code>''</code>           | 일정 ID                                                                                                                                                                                                           |\n| [calendarId](#캘린더calendarid)             | <code>''</code>           | 캘린더 ID                                                                                                                                                                                                         |\n| title                                       | <code>''</code>           | 일정 제목                                                                                                                                                                                                         |\n| body                                        | <code>''</code>           | 일정 내용                                                                                                                                                                                                         |\n| [isAllday](#isallday)                       | <code>false</code>        | 종일 일정 여부                                                                                                                                                                                                    |\n| start                                       | <code>new TZDate()</code> | 일정이 시작하는 일시. 일정을 생성할 때는 <code>Date</code>, <code>string</code>, <code>number</code>, <code>TZDate</code>로 지정할 수 있으며, 캘린더 API 파라미터 또는 반환값에서는 <code>TZDate</code> 객체이다. |\n| end                                         | <code>new TZDate()</code> | 일정이 끝나는 일시. 일정을 생성할 때는 <code>Date</code>, <code>string</code>, <code>number</code>, <code>TZDate</code>로 지정할 수 있으며, 캘린더 API 파라미터 또는 반환값에서는 <code>TZDate</code> 객체이다.   |\n| goingDuration                               | <code>0</code>            | 일정 장소까지 이동하는 데 걸리는 시간. 분 단위의 숫자이다.                                                                                                                                                        |\n| comingDuration                              | <code>0</code>            | 일정 다음 장소까지 이동하는 데 걸리는 시간. 분 단위의 숫자이다.                                                                                                                                                   |\n| location                                    | <code>''</code>           | 일정 장소                                                                                                                                                                                                         |\n| attendees                                   | <code>[]</code>           | 일정 참석자 목록                                                                                                                                                                                                  |\n| [category](#category)                       | <code>'time'</code>       | 일정 카테고리. <code>milestone</code>, <code>task</code>, <code>allday</code>, <code>time</code> 중 하나이다.                                                                                                     |\n| dueDateClass                                | <code>''</code>           | task 일정 카테고리. 어떤 문자열도 가능하다.                                                                                                                                                                       |\n| recurrenceRule                              | <code>''</code>           | 일정 반복 규칙                                                                                                                                                                                                    |\n| state                                       | <code>'Busy'</code>       | 일정 상태. 바쁨(<code>Busy</code>), 한가함(<code>Free</code>) 중 하나이다.                                                                                                                                        |\n| isVisible                                   | <code>true</code>         | 일정 표시 여부                                                                                                                                                                                                    |\n| [isPending](#ispending-isfocused-isprivate) | <code>false</code>        | 미정인 일정 여부                                                                                                                                                                                                  |\n| [isFocused](#ispending-isfocused-isprivate) | <code>false</code>        | 일정 강조 여부                                                                                                                                                                                                    |\n| [isReadOnly](#isreadonly)                   | <code>false</code>        | 수정 가능한 일정 여부                                                                                                                                                                                             |\n| [isPrivate](#ispending-isfocused-isprivate) | <code>false</code>        | 개인적인 일정 여부                                                                                                                                                                                                |\n| [color](#스타일-관련-속성)                  | <code>'#000'</code>       | 일정 요소의 텍스트 색상                                                                                                                                                                                           |\n| [backgroundColor](#스타일-관련-속성)        | <code>'#a1b56c'</code>    | 일정 요소의 배경 색상                                                                                                                                                                                             |\n| [dragBackgroundColor](#스타일-관련-속성)    | <code>'#a1b56c'</code>    | 일정 요소를 드래그했을 때 배경 색상                                                                                                                                                                               |\n| [borderColor](#스타일-관련-속성)            | <code>'#000'</code>       | 일정 요소의 좌측 테두리 색상                                                                                                                                                                                      |\n| [customStyle](#스타일-관련-속성)            | <code>{}</code>           | 일정 요소에 적용할 스타일. [CSS 카멜케이스 프로퍼티를 가진 자바스크립트 객체](https://ko.reactjs.org/docs/dom-elements.html#style)이다.                                                                           |\n| raw                                         | <code>null</code>         | 실제 일정 데이터                                                                                                                                                                                                  |\n\n## 추가 설명\n\n### 캘린더(calendarId)\n\n캘린더는 이벤트를 그룹짓기 위해 사용하는 객체이다. 하나의 이벤트는 하나의 캘린더에 속하며, 고유의 ID, 이름, 색상값을 가진다.\n\n```ts\ninterface CalendarInfo {\n  id: string;\n  name: string;\n  color?: string;\n  backgroundColor?: string;\n  dragBackgroundColor?: string;\n  borderColor?: string;\n}\n```\n\n하나 이상의 캘린더를 설정하기 위해 인스턴스 생성 시 옵션 값으로 전달하여 지정하거나, `setCalendars` 메서드를 사용할 수 있다.\n\n```js\n// 인스턴스 생성 시\nconst calendar = new Calendar('#container', {\n  calendars: [\n    {\n      id: 'cal1',\n      name: 'My Calendar',\n    },\n    {\n      id: 'cal2',\n      name: 'Another Calendar',\n    },\n  ],\n});\n\n// 인스턴스 생성 후\ncalendar.setCalendars([\n  {\n    id: 'cal1',\n    name: 'My Calendar',\n  },\n  {\n    id: 'cal2',\n    name: 'Another Calendar',\n  },\n]);\n```\n\n캘린더에 컬러 값을 설정하거나 이벤트에 컬러 값을 설정한 경우 다음과 같은 우선순위로 컬러가 적용된다.\n\n1. 이벤트 고유의 컬러 값\n2. 캘린더의 컬러 값\n3. 둘 다 지정되지 않은 경우 기본값\n\n```js\ncalendar.setCalendars([\n  {\n    id: 'cal1',\n    name: 'My Calendar',\n    color: '#000',\n    backgroundColor: '#a1b56c',\n    dragBackgroundColor: '#a1b56c',\n    borderColor: '#000',\n  },\n  {\n    id: 'cal2',\n    name: 'Another Calendar',\n  },\n]);\n\ncalendar.createEvents([\n  {\n    id: '1',\n    calendarId: 'cal1',\n    title: 'Event 1',\n    isAllDay: true,\n    start: new Date('2018-08-01'),\n    end: new Date('2018-08-02'),\n    // 아래의 세 컬러는 cal1의 컬러 값을 무시한다.\n    color: '#fff',\n    backgroundColor: '#3c056d',\n    dragBackgroundColor: '#3c056d',\n    // borderColor: '#a73eaf' // 주석처리되었기 때문에 cal1의 '#000' 이 적용된다.\n  },\n  // 이 이벤트는 cal2에 속하지만, cal2가 아무런 컬러를 지정하지 않았기 때문에 기본값이 된다.\n  {\n    id: '2',\n    calendarId: 'cal2',\n    title: 'Event 2',\n    isAllDay: true,\n    start: new Date('2018-08-01'),\n    end: new Date('2018-08-02'),\n  },\n]);\n```\n\n### isAllday\n\n![isAllday](../../assets/EventObject_isAllday.png)\n\n종일 일정 여부를 나타낸다.\n\n종일 일정은 `isAllday` 외에도 여러 방법으로 적용할 수 있다. 아래의 경우 중 하나라도 해당하면 종일 일정 패널에 나타난다.\n\n- `isAllday`가 `true`인 경우\n- `category`가 `allday`인 경우\n- 일정 기간이 24시간 이상인 경우\n\n### category\n\n![category](../../assets/EventObject_category.png)\n\n카테고리에 맞는 패널에 일정이 그려진다. `milestone`, `task`, `allday`, `time` 중 하나이다.\n\n### isReadOnly\n\n![isReadOnly](../../assets/EventObject_isReadOnly.png)\n\n수정 가능한 일정 여부를 나타낸다. `isReadOnly`가 `true`인 경우 일정 이동, 일정 리사이징이 되지 않으며 일정 상세 팝업에서 편집 버튼이 노출되지 않는다.\n\n### isPending, isFocused, isPrivate\n\n`isPending`은 미정인 일정 여부를, `isFocused`는 일정 강조 여부를, `isPrivate`는 개인적인 일정 여부를 나타낸다. 기본적으로 렌더링에 영향을 주지 않으며, 해당 값에 따라 일정을 다르게 표시하고 싶을 때는 [템플릿](./template.md) 기능을 사용한다.\n\n```js\nconst calendar = new Calendar('#container', {\n  template: {\n    time({ title, isPending, isFocused, isPrivate }) {\n      if (isPending) {\n        return `Pending: ${title}`;\n      }\n\n      if (isFocused) {\n        return `Focused: ${title}`;\n      }\n\n      if (isPrivate) {\n        return `Private: ${title}`;\n      }\n\n      return title;\n    },\n  },\n});\n```\n\n자세한 예시는 [템플릿 문서](./template.md#time)를 참고한다.\n\n### 스타일 관련 속성\n\n![style](../../assets/EventObject_style.png)\n\n`color`, `backgroundColor`, `dragBackgroundColor`, `borderColor`, `customStyle`으로 일정 요소의 스타일을 커스터마이징 할 수 있다. `color`, `backgroundColor`, `dragBackgroundColor`, `borderColor`는 [CSS color 자료형](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value)으로 지정할 수 있으며, `customStyle`은 [CSS 카멜케이스 프로퍼티를 가진 자바스크립트 객체](https://ko.reactjs.org/docs/dom-elements.html#style)로 지정할 수 있다.\n"
  },
  {
    "path": "docs/ko/apis/options.md",
    "content": "# 옵션\n\n## 설명\n\n옵션을 통해 캘린더를 다양하게 커스터마이징할 수 있다. 인스턴스를 생성할 때 넘기는 옵션 객체를 통해 변경할 수도 있고 `setOptions` 메서드를 통해서도 자유롭게 옵션을 변경할 수 있다.\n\n```js\n// 인스턴스를 생성하며 옵션 설정하기\nconst calendar = new Calendar('#container', {\n  defaultView: 'month',\n  isReadOnly: true,\n  // ...\n});\n\n// 생성된 인스턴스의 옵션을 setOptions 메서드로 변경하기\ncalendar.setOptions({\n  defaultView: 'week',\n  isReadOnly: false,\n  // ...\n});\n```\n\n## 옵션 객체\n\n옵션 객체는 아래의 프로퍼티를 가진 중첩 객체이다. 해당 프로퍼티명을 클릭하면 자세한 설명으로 이동한다.\n\n| 프로퍼티                            | 기본값                                    | 설명                                                                                               |\n| ----------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------- |\n| [defaultView](#defaultview)         | <code>'week'</code>                       | 기본 뷰 타입                                                                                       |\n| [useFormPopup](#useformpopup)       | <code>false</code>                        | 기본으로 제공하는 일정 생성 팝업 사용 여부                                                         |\n| [useDetailPopup](#usedetailpopup)   | <code>false</code>                        | 기본으로 제공하는 일정 상세 팝업 사용 여부                                                         |\n| [isReadOnly](#isreadonly)           | <code>false</code>                        | 캘린더 전체의 읽기 전용 여부                                                                       |\n| [usageStatistics](#usagestatistics) | <code>true</code>                         | [Google Analytics(GA)](https://analytics.google.com/analytics/web/)를 위한 hostname 수집 허락 여부 |\n| [eventFilter](#eventfilter)         | <code>(event) => !!event.isVisible</code> | 캘린더 전체의 이벤트 필터링 조건                                                                   |\n| [week](#week)                       | <code>DEFAULT_WEEK_OPTIONS</code>         | 주간/일간뷰 관련 옵션                                                                              |\n| [month](#month)                     | <code>DEFAULT_MONTH_OPTIONS</code>        | 월간뷰 관련 옵션                                                                                   |\n| [gridSelection](#gridselection)     | <code>true</code>                         | 날짜/시간 선택의 클릭/더블 클릭 가능 여부                                                          |\n| [timezone](#timezone)               | <code>{ zones: [] }</code>                | 캘린더에서 사용하는 타임존 정보                                                                    |\n| [theme](#theme)                     | <code>DEFAULT_THEME</code>                | [테마](./theme.md)                                                                                 |\n| [template](#template)               | <code>DEFAULT_TEMPLATE</code>             | [템플릿](./template.md)                                                                            |\n| [calendars](#calendars)             | <code>[]</code>                           | 캘린더에서 사용하는 캘린더 목록                                                                    |\n\n## 사용 예시\n\n### defaultView\n\n- 타입: `'month' | 'week' | 'day'`\n- 기본값: `'week'`\n\n캘린더의 기본 뷰를 지정한다. 월간뷰, 주간뷰, 일간뷰를 지정할 수 있으며 각각 `'month'`, `'week'`, `'day'` 이다. 기본값은 `'week'`이다.\n\n```js\nconst calendar = new Calendar('#container', {\n  defaultView: 'month',\n});\n```\n\n[⬆ 목록으로 돌아가기](#옵션-객체)\n\n### useFormPopup\n\n- 타입: `boolean`\n- 기본값: `false`\n\n캘린더에서 기본으로 제공하는 일정 생성 팝업을 사용할지 여부를 지정한다. 기본값은 `false`이다.\n\n일정 생성 팝업을 사용할 때는 [`tui-date-picker`](https://github.com/nhn/tui.date-picker)와 [`tui-time-picker`](https://github.com/nhn/tui.time-picker)의 css 파일을 가져와야 스타일이 제대로 적용된다.\n\n```sh\nnpm install tui-date-picker tui-time-picker\n```\n\n```js\n// 일정 생성 팝업을 사용하기 위해 tui-date-picker와 tui-time-picker의 css 파일을 불러온다.\nimport 'tui-date-picker/dist/tui-date-picker.css';\nimport 'tui-time-picker/dist/tui-time-picker.css';\n\nconst calendar = new Calendar('#container', {\n  useFormPopup: true,\n});\n```\n\n[⬆ 목록으로 돌아가기](#옵션-객체)\n\n### useDetailPopup\n\n- 타입: `boolean`\n- 기본값: `false`\n\n캘린더에서 기본으로 제공하는 일정 상세 팝업을 사용할지 여부를 지정한다. 기본값은 `false`이다.\n\n```js\nconst calendar = new Calendar('#container', {\n  useDetailPopup: true,\n});\n```\n\n[⬆ 목록으로 돌아가기](#옵션-객체)\n\n### isReadOnly\n\n- 타입: `boolean`\n- 기본값: `false`\n\n캘린더를 읽기 전용으로 만들지 여부를 지정한다. 기본값은 `false`이며 `true`로 설정하면 사용자는 캘린더의 일정을 생성하거나 수정할 수 없다.\n\n```js\nconst calendar = new Calendar('#container', {\n  isReadOnly: true,\n});\n```\n\n[⬆ 목록으로 돌아가기](#옵션-객체)\n\n### usageStatistics\n\n- 타입: `boolean`\n- 기본값: `true`\n\n[Google Analytics(GA)](https://analytics.google.com/analytics/web/)를 위한 hostname 수집을 허용할지 여부를 지정한다. 기본값은 `true`이며 `false`로 설정하면 통계 수집을 하지 않는다.\n\n[TOAST UI 캘린더](https://github.com/nhn/tui.calendar)는 [GA](https://analytics.google.com/analytics/web/)를 적용하여 오픈 소스 사용에 대한 통계를 수집하여 전 세계에서 얼마나 널리 사용되는지 확인한다.\n이는 프로젝트의 향후 진행을 결정하는 중요한 지표 역할을 한다.\n`location.hostname`(예를 들어 \"ui.toast.com\")을 수집하며 사용량에 대한 통계를 측정하기 위해서만 사용된다.\n\n```js\nconst calendar = new Calendar('#container', {\n  usageStatistics: false,\n});\n```\n\n[⬆ 목록으로 돌아가기](#옵션-객체)\n\n### eventFilter\n\n- 타입: `(event: EventObject) => boolean`\n- 기본값: `(event) => !!event.isVisible`\n\n캘린더의 일정 필터링 조건을 지정한다. 기본값은 `(event) => !!event.isVisible`로 일정의 `isVisible` 속성이 `true`인 일정만 렌더링한다.\n\n커스텀 이벤트 필터를 적용할 때 `isVisible`을 필터링하지 않을 경우 `isVisible: false`인 이벤트가 캘린더에 나타날 수 있다.\n\n```js\nconst calendar = new Calendar('#container', {\n  eventFilter: (event) => event.isVisible && event.isAllday,\n});\n```\n\n[⬆ 목록으로 돌아가기](#옵션-객체)\n\n### week\n\n- 타입: `WeekOptions`\n- 기본값: `DEFAULT_WEEK_OPTIONS`\n\n```ts\ntype EventView = 'allday' | 'time';\ntype TaskView = 'milestone' | 'task';\ninterface CollapseDuplicateEvents {\n  getDuplicateEvents: (targetEvent: EventObject, events: EventObject[]) => EventObject[];\n  getMainEvent: (events: EventObject[]) => EventObject;\n};\n\ninterface WeekOptions {\n  startDayOfWeek?: number;\n  dayNames?: [string, string, string, string, string, string, string];\n  narrowWeekend?: boolean;\n  workweek?: boolean;\n  showNowIndicator?: boolean;\n  showTimezoneCollapseButton?: boolean;\n  timezonesCollapsed?: boolean;\n  hourStart?: number;\n  hourEnd?: number;\n  eventView?: boolean | EventView[];\n  taskView?: boolean | TaskView[];\n  collapseDuplicateEvents?: boolean | CollapseDuplicateEvents;\n}\n```\n\n```js\nconst DEFAULT_WEEK_OPTIONS = {\n  startDayOfWeek: 0,\n  dayNames: [],\n  narrowWeekend: false,\n  workweek: false,\n  showNowIndicator: true,\n  showTimezoneCollapseButton: false,\n  timezonesCollapsed: false,\n  hourStart: 0,\n  hourEnd: 24,\n  eventView: true,\n  taskView: true,\n  collapseDuplicateEvents: false,\n};\n```\n\n일간/주간뷰와 관련된 옵션을 지정한다.\n\n[⬆ 목록으로 돌아가기](#옵션-객체)\n\n#### week.startDayOfWeek\n\n- 타입: `number`\n- 기본값: `0`\n\n일간/주간뷰에서 주의 시작 요일을 지정한다. 기본값은 `0`으로 일요일부터 시작한다. `0`(일요일)부터 `6`(토요일)까지의 값을 지정할 수 있다.\n\n| 값  | 요일   |\n| --- | ------ |\n| 0   | 일요일 |\n| 1   | 월요일 |\n| 2   | 화요일 |\n| 3   | 수요일 |\n| 4   | 목요일 |\n| 5   | 금요일 |\n| 6   | 토요일 |\n\n```js\ncalendar.setOptions({\n  week: {\n    startDayOfWeek: 1,\n  },\n});\n```\n\n| 기본값 적용                                                                         | 예제 적용                                                                          |\n| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |\n| ![week-startDayOfWeek-default](../../assets/options_week-startDayOfWeek-before.png) | ![week-startDayOfWeek-example](../../assets/options_week-startDayOfWeek-after.png) |\n\n[⬆ 목록으로 돌아가기](#week)\n\n#### week.dayNames\n\n- 타입: `[string, string, string, string, string, string, string]`\n- 기본값: `[]`\n\n일간/주간뷰에서 주의 요일명을 변경할 수 있다. 기본값은 `[]`이며, [startDayOfWeek](#week.startDayOfWeek)로 설정한 요일부터 시작한다.\n\n이 옵션을 부여할 때는 반드시 일요일부터 월요일까지 모든 요일이 입력된 배열을 입력해야 한다. 각 요일의 인덱스는 `Date.prototype.getDay` 의 결과와 같다. ([참고](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-week-day))\n\n```js\ncalendar.setOptions({\n  week: {\n    dayNames: ['월', '화', '수', '목', '금', '토', '일'],\n  },\n});\n```\n\n| 기본값 적용                                                             | 예제 적용                                                              |\n| ----------------------------------------------------------------------- | ---------------------------------------------------------------------- |\n| ![week-daynames-default](../../assets/options_week-dayNames-before.png) | ![week-daynames-example](../../assets/options_week-dayNames-after.png) |\n\n[⬆ 목록으로 돌아가기](#week)\n\n#### week.narrowWeekend\n\n- 타입: `boolean`\n- 기본값: `false`\n\n일간/주간뷰에서 주말의 너비를 좁게(기존 너비의 1/2) 할 수 있다. 기본값은 `false`이며, 주말의 너비를 좁게 하려면 `true`로 지정한다.\n\n```js\ncalendar.setOptions({\n  week: {\n    narrowWeekend: true,\n  },\n});\n```\n\n| 기본값 적용                                                                       | 예제 적용                                                                        |\n| --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |\n| ![week-narrowWeekend-default](../../assets/options_week-narrowWeekend-before.png) | ![week-narrowWeekend-example](../../assets/options_week-narrowWeekend-after.png) |\n\n[⬆ 목록으로 돌아가기](#week)\n\n#### week.workweek\n\n- 타입: `boolean`\n- 기본값: `false`\n\n일간/주간뷰에서 주말을 제외할 수 있다. 기본값은 `false`이며, 주말을 제외하려면 `true`로 지정한다.\n\n```js\ncalendar.setOptions({\n  week: {\n    workweek: true,\n  },\n});\n```\n\n| 기본값 적용                                                             | 예제 적용                                                              |\n| ----------------------------------------------------------------------- | ---------------------------------------------------------------------- |\n| ![week-workweek-default](../../assets/options_week-workweek-before.png) | ![week-workweek-example](../../assets/options_week-workweek-after.png) |\n\n[⬆ 목록으로 돌아가기](#week)\n\n#### week.showNowIndicator\n\n- 타입: `boolean`\n- 기본값: `true`\n\n주간/일간뷰에서 현재 시간선을 표시할지 여부를 지정할 수 있다. 기본값은 `true`이며, 현재 시간선을 표시하지 않으려면 `false`로 지정한다.\n\n```js\ncalendar.setOptions({\n  week: {\n    showNowIndicator: false,\n  },\n});\n```\n\n| 기본값 적용                                                                             | 예제 적용                                                                              |\n| --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |\n| ![week-showNowIndicator-default](../../assets/options_week-showNowIndicator-before.png) | ![week-showNowIndicator-example](../../assets/options_week-showNowIndicator-after.png) |\n\n#### week.showTimezoneCollapseButton\n\n- 타입: `boolean`\n- 기본값: `false`\n\n주간/일간뷰에서 여러 타임존을 사용할 때, 서브 타임존을 접는 버튼을 표시할지 여부를 지정할 수 있다. 기본값은 `false`이며, 접기 버튼을 표시하려면 `true`로 지정한다.\n\n```js\ncalendar.setOptions({\n  week: {\n    showTimezoneCollapseButton: true,\n  },\n});\n```\n\n| 기본값 적용                                                                                                 | 예제 적용                                                                                                  |\n| ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |\n| ![week-showTimezoneCollapseButton-default](../../assets/options_week-showTimezoneCollapseButton-before.png) | ![week-showTimezoneCollapseButton-example](../../assets/options_week-showTimezoneCollapseButton-after.png) |\n\n[⬆ 목록으로 돌아가기](#week)\n\n#### week.timezonesCollapsed\n\n- 타입: `boolean`\n- 기본값: `false`\n\n주간/일간뷰에서 여러 타임존을 사용할 때, 서브 타임존들을 접힌 상태로 표시할지 여부를 지정한다. 기본값은 `false`이며, 접힌 상태로 표시하려면 `true`로 지정한다.\n\n```js\ncalendar.setOptions({\n  week: {\n    timezonesCollapsed: true,\n  },\n});\n```\n\n| 기본값 적용                                                                                 | 예제 적용                                                                                  |\n| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |\n| ![week-timezonesCollapsed-default](../../assets/options_week-timezonesCollapsed-before.png) | ![week-timezonesCollapsed-example](../../assets/options_week-timezonesCollapsed-after.png) |\n\n[⬆ 목록으로 돌아가기](#week)\n\n#### week.hourStart\n\n- 타입: `number`\n- 기본값: `0`\n\n주간/일간뷰에서 각 컬럼의 시작 시간을 지정한다. 기본값은 `0`이며, 원하는 시작 시간을 지정할 수 있다.\n\n```js\ncalendar.setOptions({\n  week: {\n    hourStart: 9,\n  },\n});\n```\n\n| 기본값 적용                                                               | 예제 적용                                                                |\n| ------------------------------------------------------------------------- | ------------------------------------------------------------------------ |\n| ![week-hourStart-default](../../assets/options_week-hourStart-before.png) | ![week-hourStart-example](../../assets/options_week-hourStart-after.png) |\n\n[⬆ 목록으로 돌아가기](#week)\n\n#### week.hourEnd\n\n- 타입: `number`\n- 기본값: `24`\n\n주간/일간뷰에서 각 컬럼의 끝 시간을 지정한다. 기본값은 `24`이며, 원하는 끝 시간을 지정할 수 있다.\n\n```js\ncalendar.setOptions({\n  week: {\n    hourEnd: 18,\n  },\n});\n```\n\n| 기본값 적용                                                           | 예제 적용                                                            |\n| --------------------------------------------------------------------- | -------------------------------------------------------------------- |\n| ![week-hourEnd-default](../../assets/options_week-hourEnd-before.png) | ![week-hourEnd-example](../../assets/options_week-hourEnd-after.png) |\n\n[⬆ 목록으로 돌아가기](#week)\n\n#### week.eventView\n\n- 타입: `boolean | ('allday' | 'time')[]`\n- 기본값: `true`\n\n주간/일간뷰에서 allday 패널과 time 패널의 표시 여부를 지정할 수 있다. 기본값은 `true`이며, allday 패널과 time 패널을 모두 표시한다. `false`인 경우엔 두 패널 모두 표시하지 않으며, `['allday']`인 경우엔 allday 패널만 표시한다. `['time']`인 경우엔 time 패널만 표시한다.\n\n```js\ncalendar.setOptions({\n  week: {\n    eventView: false,\n  },\n});\n```\n\n| 기본값 적용                                                               | 예제 적용                                                                |\n| ------------------------------------------------------------------------- | ------------------------------------------------------------------------ |\n| ![week-eventView-default](../../assets/options_week-eventView-before.png) | ![week-eventView-example](../../assets/options_week-eventView-after.png) |\n\n[⬆ 목록으로 돌아가기](#week)\n\n#### week.taskView\n\n- 타입: `boolean | ('milestone' | 'task')[]`\n- 기본값: `true`\n\n주간/일간뷰에서 milestone 패널과 task 패널의 표시 여부를 지정할 수 있다. 기본값은 `true`이며, milestone 패널과 task 패널을 모두 표시한다. `false`인 경우엔 두 패널 모두 표시하지 않으며, `['milestone']`인 경우엔 milestone 패널만 표시한다. `['task']`인 경우엔 task 패널만 표시한다.\n\n```js\ncalendar.setOptions({\n  week: {\n    taskView: false,\n  },\n});\n```\n\n| 기본값 적용                                                             | 예제 적용                                                              |\n| ----------------------------------------------------------------------- | ---------------------------------------------------------------------- |\n| ![week-taskView-default](../../assets/options_week-taskView-before.png) | ![week-taskView-example](../../assets/options_week-taskView-after.png) |\n\n[⬆ 목록으로 돌아가기](#week)\n\n#### week.collapseDuplicateEvents\n\n- 타입: `boolean | CollapseDuplicateEventsOptions`\n- 기본값: `false`\n\n```ts\ninterface CollapseDuplicateEventsOptions {\n  getDuplicateEvents: (targetEvent: EventObject, events: EventObject[]) => EventObject[];\n  getMainEvent: (events: EventObject[]) => EventObject;\n};\n```\n\n주간/일간뷰에서 중복된 일정을 겹치게 표시할 수 있다. 기본값은 `false`이며, 중복된 일정을 일반 일정과 동일하게 처리한다. `true`인 경우엔 **`title`, `start`, `end`가 같은 일정**을 중복된 일정으로 분류하고, 이 중 **마지막 일정**을 초기 렌더링 시 펼친다. 만약 자신만의 기준으로 중복된 일정을 필터링하고 싶다면 `getDuplicateEvents`를, 초기 렌더링 시 펼치고 싶은 일정을 정하고 싶다면 `getMainEvent`을 설정한다.\n\n`getDuplicateEvents`의 경우 **중복된 일정을 표시하고 싶은 순서대로 정렬하여 리턴**해야 한다. `getDuplicateEvents`의 리턴값이 `getMainEvent`의 파라미터로 사용된다.\n\n```js\ncalendar.setOptions({\n  week: {\n    collapseDuplicateEvents: {\n      getDuplicateEvents: (targetEvent, events) =>\n        events\n          .filter((event) =>\n            event.title === targetEvent.title &&\n            event.start.getTime() === targetEvent.start.getTime() &&\n            event.end.getTime() === targetEvent.end.getTime()\n          )\n          .sort((a, b) => (a.calendarId > b.calendarId ? 1 : -1)),\n      getMainEvent: (events) => events[events.length - 1], // events는 getDuplicateEvents()의 리턴값이다.\n    }\n  },\n});\n```\n\n| 기본값 적용                                                             | 예제 적용                                                              |\n| ----------------------------------------------------------------------- | ---------------------------------------------------------------------- |\n| ![week-collapseDuplicateEvents-default](../../assets/options_week-collapseDuplicateEvents-before.png) | ![week-collapseDuplicateEvents-example](../../assets/options_week-collapseDuplicateEvents-after.png) |\n\n[⬆ 목록으로 돌아가기](#week)\n\n### month\n\n- 타입: `MonthOptions`\n- 기본값: `DEFAULT_MONTH_OPTIONS`\n\n```ts\ninterface MonthOptions {\n  dayNames?: [string, string, string, string, string, string, string];\n  startDayOfWeek?: number;\n  narrowWeekend?: boolean;\n  visibleWeeksCount?: number;\n  isAlways6Weeks?: boolean;\n  workweek?: boolean;\n  visibleEventCount?: number;\n}\n```\n\n```js\nconst DEFAULT_MONTH_OPTIONS = {\n  dayNames: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'],\n  visibleWeeksCount: 0,\n  workweek: false,\n  narrowWeekend: false,\n  startDayOfWeek: 0,\n  isAlways6Weeks: true,\n  visibleEventCount: 6,\n};\n```\n\n월간뷰와 관련된 옵션을 지정한다.\n\n[⬆ 목록으로 돌아가기](#옵션-객체)\n\n#### month.dayNames\n\n- 타입: `[string, string, string, string, string, string, string]`\n- 기본값: `['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']`\n\n월간뷰에서 주의 요일명을 변경할 수 있다. 기본값은 `['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']`이며, [startDayOfWeek](#month.startDayOfWeek)로 설정한 요일부터 시작한다.\n\n이 옵션을 부여할 때는 반드시 일요일부터 월요일까지 모든 요일이 입력된 배열을 입력해야 한다. 각 요일의 인덱스는 `Date.prototype.getDay` 의 결과와 같다. ([참고](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-week-day))\n\n| 값  | 요일   |\n| --- | ------ |\n| 0   | 일요일 |\n| 1   | 월요일 |\n| 2   | 화요일 |\n| 3   | 수요일 |\n| 4   | 목요일 |\n| 5   | 금요일 |\n| 6   | 토요일 |\n\n```js\ncalendar.setOptions({\n  month: {\n    dayNames: ['일', '월', '화', '수', '목', '금', '토'],\n  },\n});\n```\n\n| 기본값 적용                                                               | 예제 적용                                                                |\n| ------------------------------------------------------------------------- | ------------------------------------------------------------------------ |\n| ![month-daynames-default](../../assets/options_month-dayNames-before.png) | ![month-daynames-example](../../assets/options_month-dayNames-after.png) |\n\n[⬆ 목록으로 돌아가기](#month)\n\n#### month.startDayOfWeek\n\n- 타입: `number`\n- 기본값: `0`\n\n월간뷰에서 주의 시작 요일을 지정한다. 기본값은 `0`으로 일요일부터 시작한다. `0`(일요일)부터 `6`(토요일)까지의 값을 지정할 수 있다.\n\n각 요일의 인덱스는 `Date.prototype.getDay` 의 결과와 같다. ([참고](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-week-day))\n\n| 값  | 요일   |\n| --- | ------ |\n| 0   | 일요일 |\n| 1   | 월요일 |\n| 2   | 화요일 |\n| 3   | 수요일 |\n| 4   | 목요일 |\n| 5   | 금요일 |\n| 6   | 토요일 |\n\n```js\ncalendar.setOptions({\n  month: {\n    startDayOfWeek: 1,\n  },\n});\n```\n\n| 기본값 적용                                                                           | 예제 적용                                                                            |\n| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |\n| ![month-startDayOfWeek-default](../../assets/options_month-startDayOfWeek-before.png) | ![month-startDayOfWeek-example](../../assets/options_month-startDayOfWeek-after.png) |\n\n[⬆ 목록으로 돌아가기](#month)\n\n#### month.narrowWeekend\n\n- 타입: `boolean`\n- 기본값: `false`\n\n월간뷰에서 주말의 너비를 좁게(기존 너비의 1/2) 할 수 있다. 기본값은 `false`이며, 주말의 너비를 좁게 하려면 `true`로 지정한다.\n\n```js\ncalendar.setOptions({\n  month: {\n    narrowWeekend: true,\n  },\n});\n```\n\n| 기본값 적용                                                                         | 예제 적용                                                                          |\n| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |\n| ![month-narrowWeekend-default](../../assets/options_month-narrowWeekend-before.png) | ![month-narrowWeekend-example](../../assets/options_month-narrowWeekend-after.png) |\n\n[⬆ 목록으로 돌아가기](#month)\n\n#### month.visibleWeeksCount\n\n- 타입: `number`\n- 기본값: `0`\n\n월간뷰에서 보여지는 주의 개수를 지정한다. 기본값은 `0`이며 6주를 표시한다. 다른 주의 개수를 지정하려면 `1`부터 `6`까지의 값을 지정할 수 있다.\n\n⚠️ 이 옵션을 설정하면 현재 날짜는 무조건 첫 주에 위치하게 된다.\n\n```js\ncalendar.setOptions({\n  month: {\n    visibleWeeksCount: 2,\n  },\n});\n```\n\n| 기본값 적용                                                                                 | 예제 적용                                                                                  |\n| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |\n| ![month-visibleWeeksCount-default](../../assets/options_month-visibleWeeksCount-before.png) | ![month-visibleWeeksCount-example](../../assets/options_month-visibleWeeksCount-after.png) |\n\n[⬆ 목록으로 돌아가기](#month)\n\n#### month.isAlways6Weeks\n\n- 타입: `boolean`\n- 기본값: `true`\n\n월간 뷰에서 항상 6주 단위로 캘린더를 표시할지 여부를 결정한다. 기본값은 `true`이며 표시하고 있는 월의 전체 주 수와 관계 없이 6주를 표시한다.\n\n`false`로 지정하면 해당 월의 표시 가능한 주 수에 따라 4~6주로 표시된다.\n\n```js\ncalendar.setOptions({\n  month: {\n    isAlways6Weeks: false,\n  },\n});\n```\n\n| 기본값 적용                                                                           | 예제 적용                                                                            |\n| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |\n| ![month-isAlways6Weeks-default](../../assets/options_month-isAlways6Weeks-before.png) | ![month-isAlways6Weeks-example](../../assets/options_month-isAlways6Weeks-after.png) |\n\n[⬆ 목록으로 돌아가기](#month)\n\n#### month.workweek\n\n- 타입: `boolean`\n- 기본값: `false`\n\n월간뷰에서 주말을 제외할 수 있다. 기본값은 `false`이며, 주말을 제외하려면 `true`로 지정한다.\n\n```js\ncalendar.setOptions({\n  month: {\n    workweek: true,\n  },\n});\n```\n\n| 기본값 적용                                                               | 예제 적용                                                                |\n| ------------------------------------------------------------------------- | ------------------------------------------------------------------------ |\n| ![month-workweek-default](../../assets/options_month-workweek-before.png) | ![month-workweek-example](../../assets/options_month-workweek-after.png) |\n\n[⬆ 목록으로 돌아가기](#month)\n\n#### month.visibleEventCount\n\n- 타입: `number`\n- 기본값: `6`\n\n월간뷰에서 각 날짜별 최대로 보여지는 일정의 갯수를 지정한다. 기본값은 `6` 이다.\n\n이 옵션에서 지정한 갯수만큼 일정을 표시하려 하지만 높이가 충분하지 못할 경우 자동으로 옵션이 무시된다.\n\n캘린더 전체 영역과 [month 테마의 gridCell 속성](./theme.md#month-gridcell)의 영향을 받는다.\n\n```js\ncalendar.setOptions({\n  month: {\n    visibleEventCount: 2,\n  },\n});\n```\n\n| 기본값 적용                                                                                 | 예제 적용                                                                                  |\n| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |\n| ![month-visibleEventCount-default](../../assets/options_month-visibleEventCount-before.png) | ![month-visibleEventCount-example](../../assets/options_month-visibleEventCount-after.png) |\n\n[⬆ 목록으로 돌아가기](#month)\n\n### gridSelection\n\n- 타입: `boolean | GridSelectionOptions`\n- 기본값: `true`\n\n```ts\ninterface GridSelectionOptions {\n  enableDblClick?: boolean;\n  enableClick?: boolean;\n}\n```\n\n캘린더의 날짜/시간을 선택할 때 클릭과 더블 클릭의 가능 여부를 지정한다. 기본값은 `true`이다. `true`나 `false`의 `boolean` 타입으로 지정하면 클릭과 더블 클릭이 둘 모두 가능하거나 불가능하게 만든다.\n`{ enableDblClick: boolean; enableClick: boolean }`로 지정하면 클릭과 더블 클릭을 각각 지정할 수도 있다.\n\n```js\nconst calendar = new Calendar('#container', {\n  gridSelection: {\n    enableDblClick: false,\n    enableClick: true,\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#옵션-객체)\n\n### timezone\n\n- 타입: `TimezoneOptions`\n- 기본값: `{ zones: [] }`\n\n⚠️ 타임존 기능을 이용하기 위해서는 `Intl.DateTimeFormat` API 뿐 아니라 IANA 타임존 데이터베이스를 지원하는 모던 브라우저가 필요하다.\n⚠️ Internet Explorer 11을 지원해야 한다면 폴리필을 적용하거나, 별도의 라이브러리와 함께 `customOffsetCalculator` 옵션을 사용해야 한다.\n\n- [지원 범위 (caniuse)](https://caniuse.com/mdn-javascript_builtins_date_tolocaletimestring_iana_time_zone_names)\n- [`Intl.DateTimeFormat` 폴리필](https://formatjs.io/docs/polyfills/intl-datetimeformat/)\n\n```ts\ninterface TimezoneConfig {\n  timezoneName: string;\n  displayLabel?: string;\n  tooltip?: string;\n}\n\ninterface TimezoneOptions {\n  zones?: TimezoneConfig[];\n  customOffsetCalculator?: (timezoneName: string, timestamp: number) => number;\n}\n```\n\n#### 기본 타임존 설정\n\n캘린더에서 사용되는 타임존 정보를 지정한다. 기본값은 `{ zones: [] }`이다. `zones`는 각 타임존 정보들의 배열이며 타임존 정보는 해당 타임존의 이름(`timezoneName`)과 주간/일간 뷰에서 사용되는 표시 라벨(`displayLabel`), 툴팁(`tooltip`)을 지정할 수 있다.\n\n타임존 정보 배열의 한 개 이상의 타임존 정보가 설정된 경우 해당 캘린더는 배열의 첫 번째 요소를 기본 타임존으로 설정한다.\n\n```js\n// 브라우저가 구동되는 시스템의 타임존과 상관 없이 기본 타임존을 런던으로 설정\nconst calendar = new Calendar('#container', {\n  timezone: {\n    zones: [\n      {\n        timezoneName: 'Europe/London',\n      },\n    ],\n  },\n});\n```\n\n타임존이 두 개 이상 설정되었고, 주간/일간 뷰를 표시하는 상태라면 좌측 시간 라인에 표시 라벨과 툴팁이 표시된다. 그 외에는 영향을 미치지 않는다.\n\n![타임존을 여러개 설정](../../assets/options_timezone-multiple-timezone.png)\n\n#### 사용자 정의 오프셋 계산\n\n`customOffsetCalculator`는 사용자가 직접 정의한 방식으로 주어진 타임존과 UTC와의 차이를 계산하여 분 단위로 리턴해야 한다. 예를 들어 타임존 이름이 `'Asia/Seoul'` 이라면 `UTC +9` 이므로 `540`이 리턴되어야 하는 것이다.\n\n만약 `customOffsetCalculator` 가 정의되어 있다면 캘린더는 타임존이 계산되는 모든 로직에 이 계산 함수를 사용한다. 그렇지 않다면 기본적으로 [`Intl.DateTimeFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat)를 활용하기 때문에 굳이 별도의 옵션을 정의할 필요가 없다.\n\n구형 브라우저를 지원하기 위해 별도의 라이브러리를 사용하는 등 특수한 경우가 아니라면 `customOffsetCalculator` 옵션을 사용하지 않는 것을 추천한다.\n\n```js\n// moment timezone을 사용하여 오프셋 계산\nfunction momentTZCalculator(timezoneName, timestamp) {\n  return moment.tz(timezoneName).utcOffset(timestamp);\n}\n\n// Luxon을 사용하여 오프셋 계산\nfunction luxonTZCalculator(timezoneName, timestamp) {\n  return DateTime.fromMillis(timestamp).setZone(timezoneName).offset;\n}\n\n// date-fns-tz를 사용하여 오프셋 계산\nfunction dateFnsTZCalculator(timezoneName, timestamp) {\n  return getTimezoneOffset(timezoneName, new Date(timestamp));\n}\n\n// ...\n\nconst calendar = new Calendar('#container', {\n  timezone: {\n    zones: [\n      {\n        timezoneName: 'Asia/Seoul',\n        displayLabel: 'Seoul',\n        tooltip: 'Seoul Time',\n      },\n      {\n        timezoneName: 'Asia/Tokyo',\n        displayLabel: 'Tokyo',\n        tooltip: 'Tokyo Time',\n      },\n    ],\n    customOffsetCalculator: momentTZCalculator, // 혹은 luxonTZCalculator, dateFnsTZCalculator\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#옵션-객체)\n\n### theme\n\n- 타입: `ThemeObject`\n- 기본값: `DEFAULT_THEME`\n\n캘린더의 테마를 지정한다. 캘린더 인스턴스 생성 시에 지정하거나, `setOptions` 메서드 혹은 `setTheme` 메서드로 변경 가능하며 자세한 내용은 [테마 문서](./theme.md)에서 확인할 수 있다.\n\n[⬆ 목록으로 돌아가기](#옵션-객체)\n\n### template\n\n- 타입: `TemplateObject`\n- 기본값: `DEFAULT_TEMPLATE`\n\n캘린더의 템플릿을 지정한다. 자세한 내용은 [템플릿 문서](./template.md)에서 확인할 수 있다.\n\n[⬆ 목록으로 돌아가기](#옵션-객체)\n\n### calendars\n\n- 타입: `CalendarInfo[]`\n- 기본값: `[]`\n\n캘린더에서 사용되는 캘린더 목록을 지정한다. 캘린더 목록의 각 캘린더 정보는 해당 캘린더의 `id`와 캘린더명, 색상 정보를 가지고 있다. 기본값은 `[]`이다. 캘린더 정보에 대한 자세한 설명은 [EventObject 문서](./event-object.md#캘린더calendarid)를 참고한다.\n\n```ts\ninterface CalendarInfo {\n  id: string;\n  name: string;\n  color?: string;\n  backgroundColor?: string;\n  dragBackgroundColor?: string;\n  borderColor?: string;\n}\n```\n\n[⬆ 목록으로 돌아가기](#옵션-객체)\n"
  },
  {
    "path": "docs/ko/apis/template.md",
    "content": "# 템플릿\n\n## 설명\n\n템플릿은 커스텀 렌더링을 지원하는 기능이다. 캘린더 인스턴스 생성 시 템플릿 옵션으로 커스텀 렌더링을 할 수 있으며, `setOptions`로 템플릿 옵션을 변경할 수 있다.\n\n```js\nconst calendar = new Calendar('#container', {\n  template: {\n    milestone(event) {\n      return `<span style=\"color: red;\">${event.title}</span>`;\n    },\n  },\n});\n\ncalendar.setOptions({\n  template: {\n    milestone(event) {\n      return `<span style=\"color: blue;\">${event.title}</span>`;\n    },\n  },\n});\n```\n\n## 템플릿 목록\n\n템플릿의 각 프로퍼티들은 문자열 또는 `preact`의 VNode를 반환하는 함수이며 파라미터는 템플릿의 종류에 따라 다르다. 아래는 전체 템플릿 목록이다.\n\n| 템플릿명                                                  | 파라미터                         | 설명                                                               |\n| --------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------ |\n| [milestone](#milestone)                                   | [EventObject](./event-object.md) | 주간/일간뷰의 milestone 이벤트                                     |\n| [milestoneTitle](#milestonetitle)                         | 없음                             | 주간/일간뷰의 milestone 패널의 왼쪽 영역                           |\n| [task](#task)                                             | [EventObject](./event-object.md) | 주간/일간뷰의 task 이벤트                                          |\n| [taskTitle](#tasktitle)                                   | 없음                             | 주간/일간뷰의 task 패널의 왼쪽 영역                                |\n| [allday](#allday)                                         | [EventObject](./event-object.md) | 주간/일간뷰의 allday 이벤트                                        |\n| [alldayTitle](#alldaytitle)                               | 없음                             | 주간/일간뷰의 allday 패널의 왼쪽 영역                              |\n| [time](#time)                                             | [EventObject](./event-object.md) | 주간/일간뷰의 timed 이벤트                                         |\n| [goingDuration](#goingduration)                           | [EventObject](./event-object.md) | 주간/일간뷰의 timed 이벤트의 일정 장소까지 이동 시간               |\n| [comingDuration](#comingduration)                         | [EventObject](./event-object.md) | 주간/일간뷰의 timed 이벤트의 복귀 시간                             |\n| [monthMoreTitleDate](#monthmoretitledate)                 | TemplateMoreTitleDate            | 월간뷰의 더 보기 팝업의 날짜                                       |\n| [monthMoreClose](#monthmoreclose)                         | 없음                             | 월간뷰의 더 보기 팝업의 닫기 버튼                                  |\n| [monthGridHeader](#monthgridheader)                       | TemplateMonthGrid                | 월간뷰의 셀의 헤더 영역                                            |\n| [monthGridHeaderExceed](#monthgridheaderexceed)           | <code>number</code>              | 월간뷰의 셀의 헤더 영역의 초과되는 이벤트 갯수를 표시하는 컴포넌트 |\n| [monthGridFooter](#monthgridfooter)                       | TemplateMonthGrid                | 월간뷰의 셀의 푸터 영역                                            |\n| [monthGridFooterExceed](#monthgridfooterexceed)           | <code>number</code>              | 월간뷰의 셀의 푸터 영역의 초과되는 이벤트 갯수를 표시하는 컴포넌트 |\n| [monthDayName](#monthdayname)                             | TemplateMonthDayName             | 월간뷰의 요일                                                      |\n| [weekDayName](#weekdayname)                               | TemplateWeekDayName              | 주간/일간뷰의 요일                                                 |\n| [weekGridFooterExceed](#weekgridfooterexceed)             | <code>number</code>              | 주간/일간뷰의 allday 패널의 초과된 이벤트 표시 컴포넌트            |\n| [collapseBtnTitle](#collapsebtntitle)                     | 없음                             | 주간/일간뷰의 allday 패널의 접기 버튼 컴포넌트                     |\n| [timezoneDisplayLabel](#timezonedisplaylabel)             | TemplateTimezone                 | 주간/일간뷰의 타임존 표시 컴포넌트                                 |\n| [timegridDisplayPrimaryTime](#timegriddisplayprimarytime) | TemplateNow                      | 주간/일간뷰의 primary 타임존 시간 표시                             |\n| [timegridDisplayTime](#timegriddisplaytime)               | TemplateNow                      | 주간/일간뷰의 primary 타임존 외의 타임존 시간 표시                 |\n| [timegridNowIndicatorLabel](#timegridnowindicatorlabel)   | TemplateNow                      | 주간/일간뷰의 현재 시간 표시                                       |\n| [popupIsAllday](#popupisallday)                           | 없음                             | 이벤트 폼 팝업에서 all day 텍스트                                  |\n| [popupStateFree](#popupstatefree)                         | 없음                             | 이벤트 폼 팝업에서 이벤트 한가함(free) 상태 텍스트                 |\n| [popupStateBusy](#popupstatebusy)                         | 없음                             | 이벤트 폼 팝업에서 이벤트 바쁨(busy) 상태 텍스트                   |\n| [titlePlaceholder](#titleplaceholder)                     | 없음                             | 이벤트 폼 팝업에서 이벤트명 placeholder                            |\n| [locationPlaceholder](#locationplaceholder)               | 없음                             | 이벤트 폼 팝업에서 이벤트 장소 placeholder                         |\n| [startDatePlaceholder](#startdateplaceholder)             | 없음                             | 이벤트 폼 팝업에서 이벤트 시작 날짜 placeholder                    |\n| [endDatePlaceholder](#enddateplaceholder)                 | 없음                             | 이벤트 폼 팝업에서 이벤트 종료 날짜 placeholder                    |\n| [popupSave](#popupsave)                                   | 없음                             | 이벤트 폼 팝업에서 저장 버튼 텍스트                                |\n| [popupUpdate](#popupupdate)                               | 없음                             | 이벤트 수정 팝업에서 수정 버튼 텍스트                              |\n| [popupEdit](#popupedit)                                   | 없음                             | 이벤트 상세 팝업에서 편집 버튼 텍스트                              |\n| [popupDelete](#popupdelete)                               | 없음                             | 이벤트 상세 팝업에서 삭제 버튼 텍스트                              |\n| [popupDetailTitle](#popupdetailtitle)                     | [EventObject](./event-object.md) | 이벤트 상세 팝업에서 이벤트명                                      |\n| [popupDetailDate](#popupdetaildate)                       | [EventObject](./event-object.md) | 이벤트 상세 팝업에서 이벤트의 기간                                 |\n| [popupDetailLocation](#popupdetaillocation)               | [EventObject](./event-object.md) | 이벤트 상세 팝업에서 이벤트의 장소                                 |\n| [popupDetailAttendees](#popupdetailattendees)             | [EventObject](./event-object.md) | 이벤트 상세 팝업에서 이벤트의 참석자                               |\n| [popupDetailState](#popupdetailstate)                     | [EventObject](./event-object.md) | 이벤트 상세 팝업에서 이벤트의 상태                                 |\n| [popupDetailRecurrenceRule](#popupdetailrecurrencerule)   | [EventObject](./event-object.md) | 이벤트 상세 팝업에서 이벤트의 반복 룰                              |\n| [popupDetailBody](#popupdetailbody)                       | [EventObject](./event-object.md) | 이벤트 상세 팝업에서 이벤트의 내용                                 |\n\n## 사용 예시\n\n### milestone 패널\n\n![milestone](../../assets/template_milestone.png)\n\n#### milestone\n\n[`EventObject`](./event-object.md) 파라미터를 이용해 주간/일간뷰의 milestone 이벤트를 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    milestone(event) {\n      return `<span style:\"color: blue;\">${event.title}</span>`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### milestoneTitle\n\n주간/일간뷰의 milestone 패널 왼쪽의 영역을 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    milestoneTitle() {\n      return `<span>Milestone events</span>`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n### task 패널\n\n![task](../../assets/template_task.png)\n\n#### task\n\n[`EventObject`](./event-object.md) 파라미터를 이용해 주간/일간뷰의 task 이벤트를 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    task(event) {\n      return `<span style=\"color: red;\">${event.title}</span>`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### taskTitle\n\n주간/일간뷰의 task 패널 왼쪽의 영역을 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    taskTitle() {\n      return `<span>Task events</span>`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n### allday 패널\n\n![allday](../../assets/template_allday.png)\n\n#### allday\n\n[`EventObject`](./event-object.md) 파라미터를 이용해 주간/일간뷰의 allday 이벤트를 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    allday(event) {\n      return `<span style=\"color: green;\">${event.title}</span>`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### alldayTitle\n\n주간/일간뷰의 allday 패널 왼쪽의 영역을 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    allday() {\n      return `<span>Allday events</span>`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n### timed 이벤트\n\n![time](../../assets/template_timed.png)\n\n#### time\n\n[`EventObject`](./event-object.md) 파라미터를 이용해 주간/일간뷰의 timed 이벤트를 커스터마이징할 수 있다. 이동 시간 및 복귀 시간을 제외한 부분이다.\n\n```js\ncalendar.setOptions({\n  template: {\n    time(event) {\n      return `<span style=\"color: black;\">${event.title}</span>`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### goingDuration\n\n[`EventObject`](./event-object.md) 파라미터를 이용해 주간/일간뷰에서 timed 이벤트의 일정 장소까지 이동 시간을 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    goingDuration(event) {\n      return `<span>${event.goingDuration}</span>`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### comingDuration\n\n[`EventObject`](./event-object.md) 파라미터를 이용해 주간/일간뷰에서 timed 이벤트의 복귀 시간을 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    comingDuration(event) {\n      return `<span>${event.comingDuration}</span>`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n### 월간뷰의 더보기 팝업\n\n![more-events-popup](../../assets/template_moreEventsPopup.png)\n\n#### monthMoreTitleDate\n\n```ts\ninterface TemplateMoreTitleDate {\n  ymd: string; // 해당 날짜의 `YYYY-MM-DD` 문자열 형식 데이터\n  date: number; // 해당 날짜의 일\n  day: number; // 해당 날짜의 요일\n}\n```\n\n더 보기 팝업의 날짜를 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    monthMoreTitleDate(moreTitle) {\n      const { date } = moreTitle;\n\n      return `<span>${date}</span>`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### monthMoreClose\n\n더 보기 팝업의 닫기 버튼을 커스터마이징할 수 있다. 기본적으로는 닫기 버튼이 표시되지 않는다.\n\n```js\ncalendar.setOptions({\n  template: {\n    monthMoreClose() {\n      return '';\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n### 월간뷰의 헤더와 푸터\n\n![grid-header-footer](../../assets/template_gridHeaderFooter.png)\n\n```ts\ninterface TemplateMonthGrid {\n  date: string; // 해당 날짜의 일\n  day: number; // 해당 날짜의 요일\n  hiddenEventCount: number; // 표시되지 않은 이벤트의 갯수\n  isOtherMonth: boolean; // 현재 월간뷰의 달과 다른 달의 날짜인지 여부\n  isToday: boolean; // 오늘 날짜인지 여부\n  month: number; // 해당 월\n  ymd: string; // 해당 날짜의 `YYYY-MM-DD` 문자열 형식 데이터\n}\n```\n\n#### monthGridHeader\n\n월간뷰 셀의 헤더 영역을 커스터마이징할 수 있다. `TemplateMonthGrid` 객체를 파라미터로 받는다.\n\n```js\ncalendar.setOptions({\n  template: {\n    monthGridHeader(model) {\n      const date = parseInt(model.date.split('-')[2], 10);\n\n      return `<span>${date}</span>`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### monthGridHeaderExceed\n\n월간뷰 셀의 헤더 영역의 초과되는 이벤트의 갯수를 표시하는 컴포넌트를 커스터마이징할 수 있다. 초과된 이벤트의 갯수를 파라미터로 받는다.\n\n```js\ncalendar.setOptions({\n  template: {\n    monthGridHeaderExceed(hiddenEvents) {\n      return `<span>${hiddenEvents} more</span>`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### monthGridFooter\n\n월간뷰 셀의 푸터 영역을 커스터마이징할 수 있다. `TemplateMonthGrid` 객체를 파라미터로 받는다.\n기본적으로는 아무 것도 표시되지 않는다.\n\n```js\ncalendar.setOptions({\n  template: {\n    monthGridFooter() {\n      return '';\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### monthGridFooterExceed\n\n월간뷰 셀의 푸터 영역의 초과되는 이벤트의 갯수를 표시하는 컴포넌트를 커스터마이징할 수 있다. 초과된 이벤트의 갯수를 파라미터로 받는다.\n기본적으로는 아무 것도 표시되지 않는다.\n\n```js\ncalendar.setOptions({\n  template: {\n    monthGridFooterExceed() {\n      return '';\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n### 요일 표시\n\n#### monthDayName\n\n![month-dayname](../../assets/template_monthDayName.png)\n\n```ts\ninterface TemplateMonthDayName {\n  day: number; // 해당 날짜의 요일\n  label: string; // 해당 요일의 기본적인 영문 축약형 문자열\n}\n```\n\n월간뷰의 요일을 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    monthDayName(model) {\n      return model.label;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### weekDayName\n\n![week-dayname](../../assets/template_weekDayName.png)\n\n```ts\ninterface TemplateWeekDayName {\n  date: number; // 해당 요일의 일\n  day: number; // 해당 요일\n  dayName: string; // 해당 요일의 기본적인 영문 축약형 문자열\n  isToday: boolean; // 해당 요일이 오늘인지 여부\n  renderDate: string; // 주간/일간뷰 렌더링의 기준 날짜\n  dateInstance: TZDate; // 해당 요일의 `Date` 객체\n}\n```\n\n주간/일간뷰의 요일을 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    weekDayName(model) {\n      return `<span>${model.date}</span>&nbsp;&nbsp;<span>${model.dayName}</span>`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n### weekGridFooterExceed\n\n![week-exceed](../../assets/template_weekExceed.png)\n\n주간/일간뷰의 allday 패널의 초과된 이벤트 표시 컴포넌트를 커스터마이징할 수 있다. 초과된 이벤트의 갯수를 파라미터로 받는다.\n\n```js\ncalendar.setOptions({\n  template: {\n    weekGridFooterExceed(hiddenEvents) {\n      return `+${hiddenEvents}`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n### collapseBtnTitle\n\n![collapse-btn](../../assets/template_collapseBtn.png)\n\n주간/일간뷰의 접기 버튼 컴포넌트를 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    collapseBtnTitle() {\n      return `<span>↑</span>`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n### timezoneDisplayLabel\n\n![timezone-display](../../assets/template_timezoneDisplay.png)\n\n2개 이상의 타임존을 사용하는 주간/일간뷰에서 타임존을 표시하는 컴포넌트를 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    timezoneDisplayLabel({ timezoneOffset }) {\n      const sign = timezoneOffset < 0 ? '-' : '+';\n      const hours = Math.abs(timezoneOffset / 60);\n      const minutes = Math.abs(timezoneOffset % 60);\n\n      return `GMT${sign}${hours}:${minutes}`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n### 시간 표시\n\n![timegrid-time](../../assets/template_timegridTime.png)\n\n```ts\ntype TimeUnit = 'second' | 'minute' | 'hour' | 'date' | 'month' | 'year';\n\ninterface TemplateNow {\n  unit: TimeUnit; // 시간의 단위\n  time: TZDate; // 해당 시간\n  format: string; // 해당 시간의 포맷\n}\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### timegridDisplayPrimaryTime\n\nprimary 타임존의 시간 표시를 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    timegridDisplayPrimaryTime({ time }) {\n      return `primary timezone: ${time}`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### timegridDisplayTime\n\nprimary 타임존을 제외한 타임존의 시간 표시를 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    timegridDisplayTime({ time }) {\n      return `sub timezone: ${time}`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### timegridNowIndicatorLabel\n\n현재 시간선에 표시되는 현재 시각 텍스트를 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    timegridNowIndicatorLabel({ time }) {\n      return `current time: ${time}`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n### 이벤트 폼 팝업\n\n![popup-create](../../assets/template_popupCreate.png)\n\n#### popupIsAllday\n\n이벤트 폼 팝업에서 all day 텍스트를 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupIsAllday() {\n      return 'All day';\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### popupStateFree\n\n이벤트 폼 팝업에서 이벤트의 한가함(free) 상태를 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupStateFree() {\n      return 'Free';\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### popupStateBusy\n\n이벤트 폼 팝업에서 이벤트의 바쁨(busy) 상태를 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupStateBusy() {\n      return 'Busy';\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### titlePlaceholder\n\n이벤트 폼 팝업에서 이벤트명의 placeholder를 커스터마이징할 수 있다. 무조건 문자열을 반환해야한다.\n\n```js\ncalendar.setOptions({\n  template: {\n    titlePlaceholder() {\n      return 'Title';\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### locationPlaceholder\n\n이벤트 폼 팝업에서 이벤트 장소의 placeholder를 커스터마이징할 수 있다. 무조건 문자열을 반환해야한다.\n\n```js\ncalendar.setOptions({\n  template: {\n    locationPlaceholder() {\n      return 'Location';\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### startDatePlaceholder\n\n이벤트 폼 팝업에서 이벤트의 시작 날짜 placeholder를 커스터마이징할 수 있다. 무조건 문자열을 반환해야한다.\n\n```js\ncalendar.setOptions({\n  template: {\n    startDatePlaceholder() {\n      return 'Start date';\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### endDatePlaceholder\n\n이벤트 폼 팝업에서 이벤트의 종료 날짜 placeholder를 커스터마이징할 수 있다. 무조건 문자열을 반환해야한다.\n\n```js\ncalendar.setOptions({\n  template: {\n    endDatePlaceholder() {\n      return 'End date';\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### popupSave\n\n이벤트 폼 팝업에서 저장 버튼의 텍스트를 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupSave() {\n      return 'Add';\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n### 이벤트 수정 팝업\n\n![popup-edit](../../assets/template_popupEdit.png)\n\n#### popupUpdate\n\n이벤트 수정 팝업에서 수정 버튼의 텍스트를 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupUpdate() {\n      return 'Update';\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n### 이벤트 상세 팝업\n\n![popup-detail](../../assets/template_popupDetail.png)\n\n#### popupEdit\n\n이벤트 상세 팝업에서 편집 버튼의 텍스트를 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupEdit() {\n      return 'Edit';\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### popupDelete\n\n이벤트 상세 팝업에서 삭제 버튼의 텍스트를 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupDelete() {\n      return 'Delete';\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### popupDetailTitle\n\n이벤트 상세 팝업에서 이벤트명을 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupDetailTitle({ title }) {\n      return title;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### popupDetailDate\n\n이벤트 상세 팝업에서 이벤트의 기간을 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupDetailDate({ start, end }) {\n      return `${start.toString()} - ${end.toString()}`;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### popupDetailLocation\n\n이벤트 상세 팝업에서 이벤트의 장소를 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupDetailLocation({ location }) {\n      return location;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### popupDetailAttendees\n\n이벤트 상세 팝업에서 이벤트의 참석자를 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupDetailAttendees({ attendees = [] }) {\n      return attendees.join(', ');\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### popupDetailState\n\n이벤트 상세 팝업에서 이벤트의 상태를 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupDetailState({ state }) {\n      return state || 'Busy';\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### popupDetailRecurrenceRule\n\n이벤트 상세 팝업에서 이벤트의 반복 룰을 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupDetailRecurrenceRule({ recurrenceRule }) {\n      return recurrenceRule;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n\n#### popupDetailBody\n\n이벤트 상세 팝업에서 이벤트의 내용을 커스터마이징할 수 있다.\n\n```js\ncalendar.setOptions({\n  template: {\n    popupDetailBody({ body }) {\n      return body;\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#템플릿-목록)\n"
  },
  {
    "path": "docs/ko/apis/theme.md",
    "content": "# 테마\n\n## 설명\n\n색상, 배경색 등을 변경하여 원하는 테마를 적용할 수 있다. `setTheme` 메서드를 통해 자유롭게 테마를 변경할 수 있다.\n\n```js\n// 인스턴스를 생성하며 테마 설정하기\nconst calendar = new Calendar('#container', {\n  theme: {\n    week: {\n      today: {\n        color: 'blue',\n      },\n    },\n  },\n});\n\n// 생성된 인스턴스의 테마를 setTheme 메서드로 변경하기\ncalendar.setTheme({\n  week: {\n    today: {\n      color: 'red',\n    },\n  },\n});\n```\n\n## 테마 객체\n\n테마 객체는 공통으로 적용되는 `common`, 주간/일간뷰를 위한 `week`, 월간뷰를 위한 `month` 세 부분으로 나뉘어진 중첩 객체다. 모든 값은 해당 속성에 대응되는 CSS 문자열 값이다.\n\n```ts\ninterface ThemeObject {\n  common: CommonTheme;\n  week: WeekTheme;\n  month: MonthTheme;\n}\n```\n\n### common 테마\n\n```ts\ninterface CommonTheme {\n  backgroundColor: string;\n  border: string;\n  gridSelection: {\n    backgroundColor: string;\n    border: string;\n  };\n  dayName: { color: string };\n  holiday: { color: string };\n  saturday: { color: string };\n  today: { color: string };\n}\n```\n\n| 테마                                       | 기본값                              | 설명            |\n| ------------------------------------------ | ----------------------------------- | --------------- |\n| [backgroundColor](#common-backgroundcolor) | <code>'white'</code>                | 캘린더의 배경색 |\n| [border](#common-border)                   | <code>'1px solid #e5e5e5'</code>    | 캘린더의 테두리 |\n| [gridSelection](#common-gridselection)     | <code>DEFAULT_GRID_SELECTION</code> | 날짜/시간 선택  |\n| [dayName](#common-dayname)                 | <code>{ color: '#333' }</code>      | 요일            |\n| [holiday](#common-holiday)                 | <code>{ color: '#ff4040' }</code>   | 휴일            |\n| [saturday](#common-saturday)               | <code>{ color: '#333' }</code>      | 토요일          |\n| [today](#common-today)                     | <code>{ color: '#fff' }</code>      | 오늘            |\n\n```ts\nconst DEFAULT_GRID_SELECTION = {\n  backgroundColor: 'rgba(81, 92, 230, 0.05)',\n  border: '1px solid #515ce6',\n};\n```\n\n### week 테마\n\n```ts\ninterface WeekTheme {\n  dayName: {\n    borderLeft: string;\n    borderTop: string;\n    borderBottom: string;\n    backgroundColor: string;\n  };\n  dayGrid: {\n    borderRight: string;\n    backgroundColor: string;\n  };\n  dayGridLeft: {\n    borderRight: string;\n    backgroundColor: string;\n    width: string;\n  };\n  timeGrid: { borderRight: string };\n  timeGridLeft: {\n    borderRight: string;\n    backgroundColor: string;\n    width: string;\n  };\n  timeGridLeftAdditionalTimezone: { backgroundColor: string };\n  timeGridHalfHour: { borderBottom: string };\n  nowIndicatorLabel: { color: string };\n  nowIndicatorPast: { border: string };\n  nowIndicatorBullet: { backgroundColor: string };\n  nowIndicatorToday: { border: string };\n  nowIndicatorFuture: { border: string };\n  pastTime: { color: string };\n  futureTime: { color: string };\n  weekend: { backgroundColor: string };\n  today: { color: string; backgroundColor: string };\n  pastDay: { color: string };\n  panelResizer: { border: string };\n  gridSelection: { color: string };\n}\n```\n\n| 테마                                                                   | 기본값                                             | 설명                                                                                   |\n| ---------------------------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------- |\n| [dayName](#week-dayname)                                               | <code>DEFAULT_WEEK_DAYNAME</code>                  | 요일                                                                                   |\n| [dayGrid](#week-daygrid)                                               | <code>DEFAULT_DAY_GRID</code>                      | 주간/일간뷰에서 패널의 각 셀                                                           |\n| [dayGridLeft](#week-daygridleft)                                       | <code>DEFAULT_DAY_GRID_LEFT</code>                 | 주간/일간뷰에서 패널 왼쪽 영역                                                         |\n| [timeGrid](#week-timegrid)                                             | <code>{ borderRight: '1px solid #e5e5e5' }</code>  | 주간/일간뷰에서 timed 이벤트 영역                                                      |\n| [timeGridLeft](#week-timegridleft)                                     | <code>DEFAULT_TIME_GRID_LEFT</code>                | 주간/일간뷰에서 timed 이벤트 영역의 왼쪽 영역                                          |\n| [timeGridLeftAdditionalTimezone](#week-timegridleftadditionaltimezone) | <code>{ backgroundColor: 'white' }</code>          | 주간/일간뷰에서 timed 이벤트 영역의 왼쪽 영역에 표시되는 서브 타임존                   |\n| [timeGridHalfHourLine](#week-timegridhalfhourline)                     | <code>{ borderBottom: '1px solid #e5e5e5' }</code> | 주간/일간뷰에서 timed 이벤트 영역에서 매 시간의 30분 선                                |\n| [timeGridHourLine](#week-timegridhourline)                             | <code>{ borderBottom: '1px solid #e5e5e5' }</code> | 주간/일간뷰에서 timed 이벤트 영역에서 매 시간의 정각 선                                |\n| [nowIndicatorLabel](#week-nowindicatorlabel)                           | <code>{ color: '#515ce6' }</code>                  | 현재 시간선에 표시되는 현재 시각 텍스트                                                |\n| [nowIndicatorPast](#week-nowindicatorpast)                             | <code>{ border: '1px dashed #515ce6' }</code>      | 현재 시간선에서 지난 날짜선                                                            |\n| [nowIndicatorBullet](#week-nowindicatorbullet)                         | <code>{ backgroundColor: '#515ce6' }</code>        | 현재 시간선에서 오늘 날짜                                                              |\n| [nowIndicatorToday](#week-nowindicatortoday)                           | <code>{ border: '1px solid #515ce6' }</code>       | 현재 시간선에서 오늘 날짜선                                                            |\n| [nowIndicatorFuture](#week-nowindicatorfuture)                         | <code>{ border: 'none' }</code>                    | 현재 시간선에서 미래 날짜선                                                            |\n| [pastTime](#week-pasttime)                                             | <code>{ color: '#bbb' }</code>                     | 주간/일간뷰에서 timed 이벤트 영역의 왼쪽 영역에 표시되는 지난 시간                     |\n| [futureTime](#week-futuretime)                                         | <code>{ color: '#333' }</code>                     | 주간/일간뷰에서 timed 이벤트 영역의 왼쪽 영역에 표시되는 미래 시간                     |\n| [weekend](#week-weekend)                                               | <code>{ backgroundColor: 'inherit' }</code>        | 주간/일간뷰에서 timed 이벤트 영역의 주말 컬럼                                          |\n| [today](#week-today)                                                   | <code>DEFAULT_TODAY</code>                         | 주간/일간뷰에서 timed 이벤트 영역의 오늘 컬럼(color는 dayName, backgroundColor는 컬럼) |\n| [pastDay](#week-pastday)                                               | <code>{ color: '#bbb' }</code>                     | 주간/일간뷰에서 과거 요일                                                              |\n| [panelResizer](#week-panelresizer)                                     | <code>{ border: '1px solid #e5e5e5' }</code>       | 패널 크기 조절 컴포넌트                                                                |\n| [gridSelection](#week-gridselection)                                   | <code>{ color: '#515ce6' }</code>                  | 주간/일간뷰에서 날짜/시간 선택                                                         |\n\n```ts\nconst DEFAULT_WEEK_DAYNAME = {\n  borderLeft: 'none',\n  borderTop: '1px solid #e5e5e5',\n  borderBottom: '1px solid #e5e5e5',\n  backgroundColor: 'inherit',\n};\n\nconst DEFAULT_DAY_GRID = {\n  borderRight: '1px solid #e5e5e5',\n  backgroundColor: 'inherit',\n};\n\nconst DEFAULT_DAY_GRID_LEFT = {\n  borderRight: '1px solid #e5e5e5',\n  backgroundColor: 'inherit',\n  width: '72px',\n};\n\nconst DEFAULT_TIME_GRID_LEFT = {\n  backgroundColor: 'inherit',\n  borderRight: '1px solid #e5e5e5',\n  width: '72px',\n};\n\nconst DEFAULT_TODAY = {\n  color: 'inherit',\n  backgroundColor: 'rgba(81, 92, 230, 0.05)',\n};\n```\n\n### month 테마\n\n```ts\ninterface MonthTheme {\n  dayExceptThisMonth: { color: string };\n  dayName: {\n    borderLeft: string;\n    backgroundColor: string;\n  };\n  holidayExceptThisMonth: { color: string };\n  moreView: {\n    backgroundColor: string;\n    border: string;\n    boxShadow: string;\n    width: number | null,\n    height: number | null,\n  };\n  moreViewTitle: {\n    backgroundColor: string;\n  };\n  weekend: { backgroundColor: string };\n  gridCell: {\n    headerHeight: number | null;\n    footerHeight: number | null;\n  };\n}\n```\n\n| 테마                                                    | 기본값                                                | 설명                                |\n| ------------------------------------------------------- | ----------------------------------------------------- | ----------------------------------- |\n| [dayExceptThisMonth](#month-dayexceptthismonth)         | <code>{ color: 'rgba(51, 51, 51, 0.4)' }</code>       | 다른 달인 날짜                      |\n| [holidayExceptThisMonth](#month-holidayexceptthismonth) | <code>{ color: 'rgba(255, 64, 64, 0.4)' }</code>      | 다른 달인 휴일                      |\n| [dayName](#month-dayname)                               | <code>DEFAULT_MONTH_DAYNAME</code>                    | 요일                                |\n| [moreView](#month-moreview)                             | <code>DEFAULT_MORE_VIEW</code>                        | 월간뷰의 더보기 팝업                |\n| [moreViewTitle](#month-moreviewtitle)                   | <code>{ backgroundColor: 'inherit' }</code>           | 월간뷰의 더보기 팝업의 헤더 영역    |\n| [weekend](#month-weekend)                               | <code>{ backgroundColor: 'inherit' }</code>           | 월간뷰의 주말 셀                    |\n| [gridCell](#month-gridcell)                             | <code>{ headerHeight: 31, footerHeight: null }</code> | 월간뷰의 모든 셀의 헤더와 푸터 높이 |\n\n```ts\nconst DEFAULT_MONTH_DAYNAME = {\n  borderLeft: 'none',\n  backgroundColor: 'inherit',\n};\n\nconst DEFAULT_MORE_VIEW = {\n  border: '1px solid #d5d5d5',\n  boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.1)',\n  backgroundColor: 'white',\n  width: null,\n  height: null,\n};\n```\n\n## 사용 예시\n\n### common\n\n#### common-backgroundColor\n\n배경색을 지정한다. 기본 값은 `'white'`다.\n\n```js\ncalendar.setTheme({\n  common: {\n    backgroundColor: 'black',\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#common-테마)\n\n#### common-border\n\n테두리를 지정한다. 기본 값은 `'1px solid #e5e5e5'`다.\n\n```js\ncalendar.setTheme({\n  common: {\n    border: '1px dotted #e5e5e5',\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#common-테마)\n\n#### common-gridSelection\n\n날짜/시간 선택의 배경색, 테두리를 지정한다. 기본 값은 `backgroundColor`의 경우 `'rgba(81, 92, 230, 0.05)'`, `border`의 경우 `'1px solid #515ce6'`이다.\n\n| 기본값 적용                                                                   | 예제 적용                                                                    |\n| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |\n| ![common-gridSelection-default](../../assets/common-gridSelection-before.png) | ![common-gridSelection-example](../../assets/common-gridSelection-after.png) |\n\n```js\ncalendar.setTheme({\n  common: {\n    gridSelection: {\n      backgroundColor: 'rgba(81, 230, 92, 0.05)',\n      border: '1px dotted #515ce6',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#common-테마)\n\n#### common-dayName\n\n요일 색상을 지정한다. 기본 값은 `'#333'`이다.\n\n| 기본값 적용                                                       | 예제 적용                                                        |\n| ----------------------------------------------------------------- | ---------------------------------------------------------------- |\n| ![common-dayname-default](../../assets/common-dayName-before.png) | ![common-dayname-example](../../assets/common-dayName-after.png) |\n\n```js\ncalendar.setTheme({\n  common: {\n    dayName: {\n      color: '#515ce6',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#common-테마)\n\n#### common-holiday\n\n휴일 색상을 지정한다. 기본 값은 `'#ff4040'`이다.\n\n| 기본값 적용                                                       | 예제 적용                                                        |\n| ----------------------------------------------------------------- | ---------------------------------------------------------------- |\n| ![common-holiday-default](../../assets/common-holiday-before.png) | ![common-holiday-example](../../assets/common-holiday-after.png) |\n\n```js\ncalendar.setTheme({\n  common: {\n    holiday: {\n      color: 'rgba(255, 64, 64, 0.5)',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#common-테마)\n\n#### common-saturday\n\n토요일 색상을 지정한다. 기본 값은 `'#333'`이다.\n\n| 기본값 적용                                                         | 예제 적용                                                          |\n| ------------------------------------------------------------------- | ------------------------------------------------------------------ |\n| ![common-saturday-default](../../assets/common-saturday-before.png) | ![common-saturday-example](../../assets/common-saturday-after.png) |\n\n```js\ncalendar.setTheme({\n  common: {\n    saturday: {\n      color: 'rgba(64, 64, 255, 0.5)',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#common-테마)\n\n#### common-today\n\n오늘 색상을 지정한다. 기본 값은 `'#fff'`이다.\n\n| 기본값 적용                                                   | 예제 적용                                                    |\n| ------------------------------------------------------------- | ------------------------------------------------------------ |\n| ![common-today-default](../../assets/common-today-before.png) | ![common-today-example](../../assets/common-today-after.png) |\n\n```js\ncalendar.setTheme({\n  common: {\n    today: {\n      color: 'grey',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#common-테마)\n\n### week\n\n#### week-dayName\n\n주간/일간뷰의 요일을 지정한다. `borderLeft`, `borderTop`, `borderBottom`, `backgroundColor`로 왼쪽, 위, 아래 테두리와 배경색을 지정할 수 있으며 각 기본 값은 `'none'`, `'1px solid #e5e5e5'`, `'1px solid #e5e5e5'`, `'inherit'`이다.\n\n| 기본값 적용                                                   | 예제 적용                                                    |\n| ------------------------------------------------------------- | ------------------------------------------------------------ |\n| ![week-dayname-default](../../assets/week-dayName-before.png) | ![week-dayname-example](../../assets/week-dayName-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    dayName: {\n      borderLeft: 'none',\n      borderTop: '1px dotted red',\n      borderBottom: '1px dotted red',\n      backgroundColor: 'rgba(81, 92, 230, 0.05)',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#week-테마)\n\n#### week-dayGrid\n\n주간/일간뷰의 각 패널의 셀을 지정한다. `borderRight`, `backgroundColor`로 오른쪽 테두리와 배경색을 지정할 수 있으며 각 기본 값은 `'1px solid #e5e5e5'`, `'inherit'`이다. 배경색 변경 시에 주말을 제외한 컬럼의 배경색도 변경된다.\n\n| 기본값 적용                                                   | 예제 적용                                                    |\n| ------------------------------------------------------------- | ------------------------------------------------------------ |\n| ![week-dayGrid-default](../../assets/week-dayGrid-before.png) | ![week-dayGrid-example](../../assets/week-dayGrid-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    dayGrid: {\n      borderRight: 'none',\n      backgroundColor: 'rgba(81, 92, 230, 0.05)',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#week-테마)\n\n#### week-dayGridLeft\n\n주간/일간뷰에서 각 패널의 왼쪽 영역을 지정한다. `borderRight`, `backgroundColor`, `width`로 오른쪽 테두리, 배경색, 너비를 지정할 수 있으며 각 기본 값은 `'1px solid #e5e5e5'`, `'inherit'`, `'72px'`이다.\n\n| 기본값 적용                                                           | 예제 적용                                                            |\n| --------------------------------------------------------------------- | -------------------------------------------------------------------- |\n| ![week-dayGridLeft-default](../../assets/week-dayGridLeft-before.png) | ![week-dayGridLeft-example](../../assets/week-dayGridLeft-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    dayGridLeft: {\n      borderRight: 'none',\n      backgroundColor: 'rgba(81, 92, 230, 0.05)',\n      width: '144px',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#week-테마)\n\n#### week-timeGrid\n\n주간/일간뷰에서 timed 이벤트 영역을 지정한다. `borderRight`로 오른쪽 테두리를 지정할 수 있으며 기본 값은 `'1px solid #e5e5e5'`다.\n\n| 기본값 적용                                                     | 예제 적용                                                      |\n| --------------------------------------------------------------- | -------------------------------------------------------------- |\n| ![week-timeGrid-default](../../assets/week-timeGrid-before.png) | ![week-timeGrid-example](../../assets/week-timeGrid-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    timeGrid: {\n      borderRight: '1px solid #e5e5e5',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#week-테마)\n\n#### week-timeGridLeft\n\n주간/일간뷰에서 timed 이벤트 영역의 왼쪽 영역을 지정한다. `borderRight`, `backgroundColor`, `width`로 오른쪽 테두리와 배경색, 너비를 지정할 수 있으며 각 기본 값은 `'1px solid #e5e5e5'`, `'inherit'`, `'72px'`이다.\n\n| 기본값 적용                                                             | 예제 적용                                                              |\n| ----------------------------------------------------------------------- | ---------------------------------------------------------------------- |\n| ![week-timeGridLeft-default](../../assets/week-timeGridLeft-before.png) | ![week-timeGridLeft-example](../../assets/week-timeGridLeft-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    timeGridLeft: {\n      borderRight: 'none',\n      backgroundColor: 'rgba(81, 92, 230, 0.05)',\n      width: '144px',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#week-테마)\n\n#### week-timeGridLeftAdditionalTimezone\n\n주간/일간뷰에서 timed 이벤트 영역의 왼쪽 영역에 표시되는 서브 타임존을 지정한다. `backgroundColor`로 배경색을 지정할 수 있으며 기본 값은 `'white'`이다.\n\n| 기본값 적용                                                                                                 | 예제 적용                                                                                                  |\n| ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |\n| ![week-timeGridLeftAdditionalTimezone-default](../../assets/week-timeGridLeftAdditionalTimezone-before.png) | ![week-timeGridLeftAdditionalTimezone-example](../../assets/week-timeGridLeftAdditionalTimezone-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    timeGridLeftAdditionalTimezone: {\n      backgroundColor: '#e5e5e5',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#week-테마)\n\n#### week-timeGridHalfHourLine\n\n주간/일간뷰에서 timed 이벤트 영역에서 매 시간의 30분 선을 지정한다. `borderBottom`으로 아래 테두리를 지정할 수 있으며 기본 값은 `'none'`이다.\n\n| 기본값 적용                                                                             | 예제 적용                                                                              |\n| --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |\n| ![week-timeGridHalfHourLine-default](../../assets/week-timeGridHalfHourLine-before.png) | ![week-timeGridHalfHourLine-example](../../assets/week-timeGridHalfHourLine-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    timeGridHalfHourLine: {\n      borderBottom: '1px dotted #e5e5e5',\n    },\n  },\n});\n```\n\n#### week-timeGridHourLine\n\n주간/일간뷰에서 timed 이벤트 영역에서 매 시간의 정각 선을 지정한다. `borderBottom`으로 아래 테두리를 지정할 수 있으며 기본 값은 `'none'`이다.\n\n| 기본값 적용                                                                     | 예제 적용                                                                      |\n| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |\n| ![week-timeGridHourLine-default](../../assets/week-timeGridHourLine-before.png) | ![week-timeGridHourLine-example](../../assets/week-timeGridHourLine-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    timeGridHourLine: {\n      borderBottom: '1px solid #f9f9f9',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#week-테마)\n\n#### week-nowIndicatorLabel\n\n현재 시간선에 표시되는 현재 시각 텍스트를 지정한다. `color`로 글자색을 지정할 수 있으며 기본 값은 `'#515ce6'`이다.\n\n| 기본값 적용                                                                       | 예제 적용                                                                        |\n| --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |\n| ![week-nowIndicatorLabel-default](../../assets/week-nowIndicatorLabel-before.png) | ![week-nowIndicatorLabel-example](../../assets/week-nowIndicatorLabel-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    nowIndicatorLabel: {\n      color: 'red',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#week-테마)\n\n#### week-nowIndicatorPast\n\n현재 시간선에서 지난 날짜선을 지정한다. `border`로 선의 테두리를 지정할 수 있으며 기본 값은 `'1px dashed #515ce6'`이다.\n\n| 기본값 적용                                                                     | 예제 적용                                                                      |\n| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |\n| ![week-nowIndicatorPast-default](../../assets/week-nowIndicatorPast-before.png) | ![week-nowIndicatorPast-example](../../assets/week-nowIndicatorPast-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    nowIndicatorPast: {\n      border: '1px dashed red',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#week-테마)\n\n#### week-nowIndicatorBullet\n\n현재 시간선에서 오늘 날짜에 표시되는 점을 지정한다. `backgroundColor`로 배경색을 지정할 수 있으며 기본 값은 `'#515ce6'`이다.\n\n| 기본값 적용                                                                         | 예제 적용                                                                          |\n| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |\n| ![week-nowIndicatorBullet-default](../../assets/week-nowIndicatorBullet-before.png) | ![week-nowIndicatorBullet-example](../../assets/week-nowIndicatorBullet-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    nowIndicatorBullet: {\n      backgroundColor: '#515ce6',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#week-테마)\n\n#### week-nowIndicatorToday\n\n현재 시간선에서 오늘 날짜선을 지정한다. `border`로 선의 테두리를 지정할 수 있으며 기본 값은 `'1px solid #515ce6'`이다.\n\n| 기본값 적용                                                                       | 예제 적용                                                                        |\n| --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |\n| ![week-nowIndicatorToday-default](../../assets/week-nowIndicatorToday-before.png) | ![week-nowIndicatorToday-example](../../assets/week-nowIndicatorToday-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    nowIndicatorToday: {\n      border: '1px solid red',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#week-테마)\n\n#### week-nowIndicatorFuture\n\n현재 시간선에서 미래 날짜선을 지정한다. `border`로 선의 테두리를 지정할 수 있으며 기본 값은 `'none'`이다.\n\n| 기본값 적용                                                                         | 예제 적용                                                                          |\n| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |\n| ![week-nowIndicatorFuture-default](../../assets/week-nowIndicatorFuture-before.png) | ![week-nowIndicatorFuture-example](../../assets/week-nowIndicatorFuture-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    nowIndicatorFuture: {\n      border: '1px solid red',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#week-테마)\n\n#### week-pastTime\n\n주간/일간뷰에서 timed 이벤트 영역의 왼쪽 영역에 표시되는 지난 시간을 지정한다. `color`로 글자색을 지정할 수 있으며 기본 값은 `'#bbb'`다.\n\n| 기본값 적용                                                     | 예제 적용                                                      |\n| --------------------------------------------------------------- | -------------------------------------------------------------- |\n| ![week-pastTime-default](../../assets/week-pastTime-before.png) | ![week-pastTime-example](../../assets/week-pastTime-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    pastTime: {\n      color: 'red',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#week-테마)\n\n#### week-futureTime\n\n주간/일간뷰에서 timed 이벤트 영역의 왼쪽 영역에 표시되는 미래 시간을 지정한다. `color`로 글자색을 지정할 수 있으며 기본 값은 `'#333'`다.\n\n| 기본값 적용                                                         | 예제 적용                                                          |\n| ------------------------------------------------------------------- | ------------------------------------------------------------------ |\n| ![week-futureTime-default](../../assets/week-futureTime-before.png) | ![week-futureTime-example](../../assets/week-futureTime-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    futureTime: {\n      color: 'red',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#week-테마)\n\n#### week-weekend\n\n주간/일간뷰에서 timed 이벤트 영역의 주말 컬럼을 지정한다. `backgroundColor`로 배경색을 지정할 수 있으며 기본 값은 `'inherit'`이다.\n\n| 기본값 적용                                                   | 예제 적용                                                    |\n| ------------------------------------------------------------- | ------------------------------------------------------------ |\n| ![week-weekend-default](../../assets/week-weekend-before.png) | ![week-weekend-example](../../assets/week-weekend-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    weekend: {\n      backgroundColor: 'rgba(255, 64, 64, 0.05)',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#week-테마)\n\n#### week-today\n\n주간/일간뷰에서 timed 이벤트 영역의 오늘 컬럼을 지정한다. `color`로 글자색, `backgroundColor`로 배경색을 지정할 수 있으며 각 기본 값은 `'inherit'`, `'rgba(81, 92, 230, 0.05)'`이다.\ncolor는 요일에 적용되고 backgroundColor는 컬럼에 적용된다.\n\n| 기본값 적용                                               | 예제 적용                                                |\n| --------------------------------------------------------- | -------------------------------------------------------- |\n| ![week-today-default](../../assets/week-today-before.png) | ![week-today-example](../../assets/week-today-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    today: {\n      color: '#e5e5e5',\n      backgroundColor: 'rgba(229, 229, 229, 0.05)',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#week-테마)\n\n#### week-pastDay\n\n주간/일간뷰에서 과거 요일을 지정한다. `color`로 글자색을 지정할 수 있으며 기본 값은 `'#bbb'`다.\n\n| 기본값 적용                                                   | 예제 적용                                                    |\n| ------------------------------------------------------------- | ------------------------------------------------------------ |\n| ![week-pastDay-default](../../assets/week-pastDay-before.png) | ![week-pastDay-example](../../assets/week-pastDay-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    pastDay: {\n      color: 'grey',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#week-테마)\n\n#### week-panelResizer\n\n패널 크기 조절 컴포넌트을 지정한다. `border`로 테두리를 지정할 수 있으며 기본 값은 `'1px solid #e5e5e5'`이다.\n\n| 기본값 적용                                                             | 예제 적용                                                              |\n| ----------------------------------------------------------------------- | ---------------------------------------------------------------------- |\n| ![week-panelResizer-default](../../assets/week-panelResizer-before.png) | ![week-panelResizer-example](../../assets/week-panelResizer-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    panelResizer: {\n      border: '1px dotted #e5e5e5',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#week-테마)\n\n#### week-gridSelection\n\n주간/일간뷰에서 날짜/시간 선택을 지정한다. `color`로 글자색을 지정할 수 있으며 기본 값은 `'#515ce6'`이다.\n\n| 기본값 적용                                                               | 예제 적용                                                                |\n| ------------------------------------------------------------------------- | ------------------------------------------------------------------------ |\n| ![week-gridSelection-default](../../assets/week-gridSelection-before.png) | ![week-gridSelection-example](../../assets/week-gridSelection-after.png) |\n\n```js\ncalendar.setTheme({\n  week: {\n    gridSelection: {\n      color: 'grey',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#week-테마)\n\n### month\n\n#### month-dayExceptThisMonth\n\n다른 달인 날짜를 지정한다. `color`로 글자색을 지정할 수 있으며 기본 값은 `'rgba(51, 51, 51, 0.4)'`이다.\n\n| 기본값 적용                                                                           | 예제 적용                                                                            |\n| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |\n| ![month-dayExceptThisMonth-default](../../assets/month-dayExceptThisMonth-before.png) | ![month-dayExceptThisMonth-example](../../assets/month-dayExceptThisMonth-after.png) |\n\n```js\ncalendar.setTheme({\n  month: {\n    dayExceptThisMonth: {\n      color: 'grey',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#month-테마)\n\n#### month-holidayExceptThisMonth\n\n다른 달인 휴일을 지정한다. `color`로 글자색을 지정할 수 있으며 기본 값은 `'rgba(255, 64, 64, 0.4)'`이다.\n\n| 기본값 적용                                                                                   | 예제 적용                                                                                    |\n| --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |\n| ![month-holidayExceptThisMonth-default](../../assets/month-holidayExceptThisMonth-before.png) | ![month-holidayExceptThisMonth-example](../../assets/month-holidayExceptThisMonth-after.png) |\n\n```js\ncalendar.setTheme({\n  month: {\n    holidayExceptThisMonth: {\n      color: 'blue',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#month-테마)\n\n#### month-dayName\n\n요일을 지정한다. `borderLeft`, `backgroundColor`로 왼쪽 테두리와 배경색을 지정할 수 있으며 각 기본 값은 `'none'`, `'inherit'`이다.\n\n| 기본값 적용                                                     | 예제 적용                                                      |\n| --------------------------------------------------------------- | -------------------------------------------------------------- |\n| ![month-dayname-default](../../assets/month-dayName-before.png) | ![month-dayname-example](../../assets/month-dayName-after.png) |\n\n```js\ncalendar.setTheme({\n  month: {\n    dayName: {\n      borderLeft: 'none',\n      backgroundColor: 'rgba(51, 51, 51, 0.4)',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#month-테마)\n\n#### month-moreView\n\n월간뷰의 더보기 팝업을 지정한다. `border`, `boxShadow`, `backgroundColor`로 테두리, 그림자, 배경색을 지정할 수 있으며 각 기본 값은 `'1px solid #d5e5e5'`, `'0 2px 6px 0 rgba(0, 0, 0, 0.1)'`, `'white'`다.\n\n또한 `width`, `height` 값을 지정하여 팝업의 크기를 지정할 수 있다. 팝업의 크기는 픽셀 값으로만 입력 가능하며, `number` 타입으로 입력되어야 한다.\n\n| 기본값 적용                                                       | 예제 적용                                                        |\n| ----------------------------------------------------------------- | ---------------------------------------------------------------- |\n| ![month-moreView-default](../../assets/month-moreView-before.png) | ![month-moreView-example](../../assets/month-moreView-after.png) |\n\n```js\ncalendar.setTheme({\n  month: {\n    moreView: {\n      border: '1px solid grey',\n      boxShadow: '0 2px 6px 0 grey',\n      backgroundColor: 'white',\n      width: 320,\n      height: 200,\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#month-테마)\n\n#### month-moreViewTitle\n\n월간뷰의 더보기 팝업의 헤더 영역을 지정한다. `backgroundColor`로 배경색을 지정할 수 있으며 기본 값은 `'inherit'`이다.\n\n| 기본값 적용                                                                 | 예제 적용                                                                  |\n| --------------------------------------------------------------------------- | -------------------------------------------------------------------------- |\n| ![month-moreViewTitle-default](../../assets/month-moreViewTitle-before.png) | ![month-moreViewTitle-example](../../assets/month-moreViewTitle-after.png) |\n\n```js\ncalendar.setTheme({\n  month: {\n    moreViewTitle: {\n      backgroundColor: 'grey',\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#month-테마)\n\n#### month-weekend\n\n월간뷰의 주말 셀을 지정한다. `backgroundColor`로 배경색을 지정할 수 있으며 기본 값은 `'inherit'`이다.\n\n| 기본값 적용                                                     | 예제 적용                                                      |\n| --------------------------------------------------------------- | -------------------------------------------------------------- |\n| ![month-weekend-default](../../assets/month-weekend-before.png) | ![month-weekend-example](../../assets/month-weekend-after.png) |\n\n```js\ncalendar.setTheme({\n  month: {\n    weekend: {\n      backgroundColor: 'rgba(255, 64, 64, 0.4)',\n    },\n  },\n});\n```\n\n#### month-gridCell\n\n월간뷰의 각 셀의 헤더와 푸터 높이를 지정한다. 기본적으로 푸터는 비활성화 상태이며, 푸터를 사용하기 위해서 임의의 `number` 타입의 값을 전달해야 한다.\n\n`headerHeight` 의 기본 값은 `31`이고, `footerHeight` 의 기본 값은 `null`이다.\n\n⚠️ 속성 값이 `null` 인 경우 헤더나 푸터가 표시되지 않는다.\n\n| 기본값 적용                                                      | 예제 적용                                                      |\n| ---------------------------------------------------------------- | -------------------------------------------------------------- |\n| ![month-gridCell-before](../../assets/month-gridCell-before.png) | ![month-gridCell-after](../../assets/month-gridCell-after.png) |\n\n```js\ncalendar.setTheme({\n  month: {\n    gridCell: {\n      footerHeight: 31,\n    },\n  },\n});\n```\n\n[⬆ 목록으로 돌아가기](#month-테마)\n"
  },
  {
    "path": "docs/ko/apis/tzdate.md",
    "content": "# TZDate\n\n## 설명\n\nTZDate는 타임존을 처리하기 위해 만든 커스텀 날짜 클래스이다. 일정 생성 시 일정 시작 일시 또는 끝나는 일시를 TZDate로 지정할 수 있으며, 캘린더 API 중 일시와 관련된 값은 TZDate로 반환된다.\n\n```js\nimport Calendar, { TZDate } from '@toast-ui/calendar';\n\nconst calendar = new Calendar('#container');\ncalendar.createEvents([\n  {\n    id: '1',\n    calendarId: 'cal1',\n    title: 'event',\n    start: new TZDate('2022-06-01T10:00:00'), // TZDate\n    end: new TZDate('2022-06-01T11:00:00'), // TZDate\n  },\n]);\n\nconsole.log(calendar.getDate()); // TZDate\nconsole.log(calendar.getEvent('1', 'cal1').start); // TZDate\n```\n\n## 인스턴스 생성\n\n- 타입\n  - `new TZDate(date?: number | string | Date | TZDate)`\n  - `new TZDate(year: number, monthIndex: number, day?: number, hours?: number, minutes?: number, seconds?: number, milliseconds?: number)`\n- 파라미터\n  - `date` 또는 개별 날짜 및 시각 구성 요소: 날짜와 시각을 나타내는 값\n\nTZDate는 아래 파라미터로 생성할 수 있다.\n\n1. 파라미터 없이: 파라미터가 없으면, 생성 순간의 날짜와 시각을 가지는 TZDate 인스턴스를 생성한다.\n2. UNIX 타임스탬프 값\n3. 타임스탬프 문자열\n4. 개별 날짜 및 시각 구성 요소\n5. Date 인스턴스\n6. TZDate 인스턴스\n\nTZDate 인스턴스를 제외한 나머지 파라미터는 `Date()` 생성자 파라미터와 동일하므로, 자세한 내용은 [`Date()` 생성자 파라미터](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#parameters)를 참고한다.\n\n```js\nimport { TZDate } from '@toast-ui/calendar';\n\nconst now = new TZDate(); // 파라미터 없음\nconst date = new TZDate(1654052400000); // UNIX 타임스탬프 값\nconst date1 = new TZDate('2022-06-01T12:00:00'); // 타임스탬프 문자열\nconst date2 = new TZDate(2022, 5, 1, 12, 0, 0, 0); // 개별 날짜 및 시각 구성 요소\nconst now1 = new TZDate(new Date()); // Date 인스턴스\nconst now2 = new TZDate(new TZDate()); // TZDate 인스턴스\n```\n\n## 인스턴스 메서드\n\n💡 메서드를 클릭하면 더 자세한 설명과 사용 예시를 볼 수 있다.\n\n| 메서드                                  | 설명                                                                                                      |\n| --------------------------------------- | --------------------------------------------------------------------------------------------------------- |\n| [toString](#tostring)                   | TZDate 인스턴스 일시를 문자열로 반환한다.                                                                 |\n| [toDate](#todate)                       | TZDate 인스턴스 일시를 Date 객체로 반환한다.                                                              |\n| [valueOf](#valueof)                     | 1970년 1월 1일 UTC 이후 TZDate 인스턴스 일시까지 경과 시간(밀리초)을 반환한다.                            |\n| [addFullYear](#addfullyear)             | 주어진 숫자 값만큼 연도를 더한다.                                                                         |\n| [addMonth](#addmonth)                   | 주어진 숫자 값만큼 월을 더한다.                                                                           |\n| [addDate](#adddate)                     | 주어진 숫자 값만큼 일자를 더한다.                                                                         |\n| [addHours](#addhours)                   | 주어진 숫자 값만큼 시를 더한다.                                                                           |\n| [addMinutes](#addminutes)               | 주어진 숫자 값만큼 분을 더한다.                                                                           |\n| [addSeconds](#addseconds)               | 주어진 숫자 값만큼 초를 더한다.                                                                           |\n| [addMilliseconds](#addmilliseconds)     | 주어진 숫자 값만큼 밀리초를 더한다.                                                                       |\n| [getTime](#gettime)                     | 1970년 1월 1일 UTC 이후 TZDate 인스턴스 일시까지 경과 시간(밀리초)을 반환한다.                            |\n| [getFullYear](#getfullyear)             | TZDate 인스턴스 일시의 연도를 반환한다.                                                                   |\n| [getMonth](#getmonth)                   | TZDate 인스턴스 일시의 월을 반환한다. 월은 0부터 시작하는 값이다. (ex. 3월의 경우 `2`)                    |\n| [getDate](#getdate)                     | TZDate 인스턴스 일시의 일자를 반환한다.                                                                   |\n| [getDay](#getday)                       | TZDate 인스턴스 일시의 요일에 해당하는 숫자 값을 반환한다. 0은 일요일을 나타낸다.                         |\n| [getHours](#gethours)                   | TZDate 인스턴스 일시의 시를 반환한다.                                                                     |\n| [getMinutes](#getminutes)               | TZDate 인스턴스 일시의 분을 반환한다.                                                                     |\n| [getSeconds](#getseconds)               | TZDate 인스턴스 일시의 초를 반환한다.                                                                     |\n| [getMilliseconds](#getmilliseconds)     | TZDate 인스턴스 일시의 밀리초를 반환한다.                                                                 |\n| [getTimezoneOffset](#gettimezoneoffset) | TZDate 인스턴스의 타임존 오프셋을 반환한다.                                                               |\n| [setWithRaw](#setwithraw)               | TZDate 인스턴스 일시를 개별 날짜 및 시각 구성 요소로 지정한다.                                            |\n| [setTime](#settime)                     | TZDate 인스턴스 일시를 1970년 1월 1일 UTC 이후 경과 시간(밀리초)으로 지정한다.                            |\n| [setFullYear](#setfullyear)             | TZDate 인스턴스 일시의 연도를 주어진 숫자 값으로 지정한다.                                                |\n| [setMonth](#setmonth)                   | TZDate 인스턴스 일시의 월을 주어진 숫자 값으로 지정한다. 월은 0부터 시작하는 값이다. (ex. 3월의 경우 `2`) |\n| [setDate](#setdate)                     | TZDate 인스턴스 일시의 일자를 주어진 숫자 값으로 지정한다.                                                |\n| [setHours](#sethours)                   | TZDate 인스턴스 일시의 시를 주어진 숫자 값으로 지정한다.                                                  |\n| [setMinutes](#setminutes)               | TZDate 인스턴스 일시의 분을 주어진 숫자 값으로 지정한다.                                                  |\n| [setSeconds](#setseconds)               | TZDate 인스턴스 일시의 초를 주어진 숫자 값으로 지정한다.                                                  |\n| [setMilliseconds](#setmilliseconds)     | TZDate 인스턴스 일시의 밀리초를 주어진 숫자 값으로 지정한다.                                              |\n| [tz](#tz)                               | 주어진 타임존을 따르는 새로운 TZDate 인스턴스를 반환한다.                                                 |\n| [local](#local)                         | 시스템 타임존을 따르는 새로운 TZDate 인스턴스를 반환한다.                                                 |\n\n### toString\n\n- 타입: `toString(): string`\n- 리턴: `string` - TZDate 인스턴스 일시를 나타내는 문자열\n\nTZDate 인스턴스 일시를 문자열로 반환한다. [Date 객체의 `toString()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toString) 함수와 동일한 포맷이다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### toDate\n\n- 타입: `toDate(): Date`\n- 리턴: `Date` - TZDate 인스턴스 일시를 나타내는 Date 객체\n\nTZDate 인스턴스 일시를 자바스크립트 표준 내장 객체인 [Date](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date)로 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### valueOf\n\n- 타입: `valueOf(): number`\n- 리턴: `number` - 1970년 1월 1일 UTC 이후 TZDate 인스턴스 일시까지 경과 시간(밀리초)\n\n1970년 1월 1일 UTC 이후 TZDate 인스턴스 일시까지 경과 시간(밀리초)을 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### addFullYear\n\n- 타입: `addFullYear(y: number): TZDate`\n- 파라미터\n  - `y` - 추가할 연도만큼의 숫자 값\n- 리턴: `TZDate` - 변경된 TZDate\n\n주어진 숫자 값만큼 연도를 더한다. 기존 TZDate 인스턴스를 변경한 다음, 이를 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### addMonth\n\n- 타입: `addMonth(m: number): TZDate`\n- 파라미터\n  - `m` - 추가할 월만큼의 숫자 값\n- 리턴: `TZDate` - 변경된 TZDate\n\n주어진 숫자 값만큼 월을 더한다. 기존 TZDate 인스턴스를 변경한 다음, 이를 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### addDate\n\n- 타입: `addDate(d: number): TZDate`\n- 파라미터\n  - `d` - 추가할 날짜만큼의 숫자 값\n- 리턴: `TZDate` - 변경된 TZDate\n\n주어진 숫자 값만큼 일자를 더한다. 기존 TZDate 인스턴스를 변경한 다음, 이를 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### addHours\n\n- 타입: `addHours(h: number): TZDate`\n- 파라미터\n  - `h` - 추가할 시만큼의 숫자 값\n- 리턴: `TZDate` - 변경된 TZDate\n\n주어진 숫자 값만큼 시를 더한다. 기존 TZDate 인스턴스를 변경한 다음, 이를 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### addMinutes\n\n- 타입: `addMinutes(M: number): TZDate`\n- 파라미터\n  - `M` - 추가할 분만큼의 숫자 값\n- 리턴: `TZDate` - 변경된 TZDate\n\n주어진 숫자 값만큼 분을 더한다. 기존 TZDate 인스턴스를 변경한 다음, 이를 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### addSeconds\n\n- 타입: `addSeconds(s: number): TZDate`\n- 파라미터\n  - `s` - 추가할 초만큼의 숫자 값\n- 리턴: `TZDate` - 변경된 TZDate\n\n주어진 숫자 값만큼 초를 더한다. 기존 TZDate 인스턴스를 변경한 다음, 이를 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### addMilliseconds\n\n- 타입: `addMilliseconds(ms: number): TZDate`\n- 파라미터\n  - `ms` - 추가할 밀리초만큼의 숫자 값\n- 리턴: `TZDate` - 변경된 TZDate\n\n주어진 숫자 값만큼 밀리초를 더한다. 기존 TZDate 인스턴스를 변경한 다음, 이를 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### getTime\n\n- 타입: `getTime(): number`\n- 리턴: `number` - 1970년 1월 1일 UTC 이후 TZDate 인스턴스 일시까지 경과 시간(밀리초)\n\n1970년 1월 1일 UTC 이후 TZDate 인스턴스 일시까지 경과 시간(밀리초)을 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### getFullYear\n\n- 타입: `getFullYear(): number`\n- 리턴: `number` - TZDate 인스턴스 일시의 연도\n\nTZDate 인스턴스 일시의 연도를 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### getMonth\n\n- 타입: `getMonth(): number`\n- 리턴: `number` - TZDate 인스턴스 일시의 월\n\nTZDate 인스턴스 일시의 월을 반환한다. 월은 0부터 시작하는 값이다. (ex. 3월의 경우 `2`)\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### getDate\n\n- 타입: `getDate(): number`\n- 리턴: `number` - TZDate 인스턴스 일시의 일자\n\nTZDate 인스턴스 일시의 일자를 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### getDay\n\n- 타입: `getDay(): number`\n- 리턴: `number` - TZDate 인스턴스 일시의 요일에 해당하는 숫자 값\n\nTZDate 인스턴스 일시의 요일에 해당하는 숫자 값을 반환한다. 0은 일요일을 나타낸다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### getHours\n\n- 타입: `getHours(): number`\n- 리턴: `number` - TZDate 인스턴스 일시의 시\n\nTZDate 인스턴스 일시의 시를 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### getMinutes\n\n- 타입: `getMinutes(): number`\n- 리턴: `number` - TZDate 인스턴스 일시의 분\n\nTZDate 인스턴스 일시의 분을 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### getSeconds\n\n- 타입: `getSeconds(): number`\n- 리턴: `number` - TZDate 인스턴스 일시의 초\n\nTZDate 인스턴스 일시의 초를 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### getMilliseconds\n\n- 타입: `getMilliseconds(): number`\n- 리턴: `number` - TZDate 인스턴스 일시의 밀리초\n\nTZDate 인스턴스 일시의 밀리초를 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### getTimezoneOffset\n\n- 타입: `getTimezoneOffset(): number`\n- 리턴: `number` - TZDate 인스턴스의 타임존 오프셋\n\nTZDate 인스턴스의 타임존 오프셋을 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### setWithRaw\n\n- 타입: `setWithRaw(y: number, m: number, d: number, h: number, M: number, s: number, ms: number): TZDate`\n- 파라미터\n  - `y`: 지정할 연도\n  - `m`: 지정할 월\n  - `d`: 지정할 일자\n  - `h`: 지정할 시\n  - `M`: 지정할 분\n  - `s`: 지정할 초\n  - `ms`: 지정할 밀리초\n- 리턴: `TZDate` - 변경된 TZDate\n\nTZDate 인스턴스 일시를 개별 날짜 및 시각 구성 요소로 지정한다. 기존 TZDate 인스턴스를 변경한 다음, 이를 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### setTime\n\n- 타입: `setTime(t: number): number`\n- 파라미터\n  - `t`: 지정할 1970년 1월 1일 UTC 이후 경과 시간(밀리초)\n- 리턴: `number` - 1970년 1월 1일 UTC 이후 변경된 TZDate 인스턴스 일시까지 경과 시간(밀리초)\n\nTZDate 인스턴스 일시를 1970년 1월 1일 UTC 이후 경과 시간(밀리초)으로 지정한다. 기존 TZDate 인스턴스를 변경한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### setFullYear\n\n- 타입: `setFullYear(y: number, m?: number, d?: number): number`\n- 파라미터\n  - `y`: 지정할 연도\n  - `m`: 지정할 월. 없다면 기존 TZDate 인스턴스 일시의 월이 유지된다.\n  - `d`: 지정할 일자. 없다면 기존 TZDate 인스턴스 일시의 일자가 유지된다.\n- 리턴: `number` - 1970년 1월 1일 UTC 이후 변경된 TZDate 인스턴스 일시까지 경과 시간(밀리초)\n\nTZDate 인스턴스 일시의 연도를 주어진 숫자 값으로 지정한다. 월과 일자도 추가로 지정할 수 있다. 기존 TZDate 인스턴스를 변경한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### setMonth\n\n- 타입: `setMonth(m: number, d?: number): number`\n- 파라미터\n  - `m`: 지정할 월\n  - `d`: 지정할 일자. 없다면 기존 TZDate 인스턴스 일시의 일자가 유지된다.\n- 리턴: `number` - 1970년 1월 1일 UTC 이후 변경된 TZDate 인스턴스 일시까지 경과 시간(밀리초)\n\nTZDate 인스턴스 일시의 월을 주어진 숫자 값으로 지정한다. 월은 0부터 시작하는 값이다. (ex. 3월의 경우 `2`) 일자도 추가로 지정할 수 있다. 기존 TZDate 인스턴스를 변경한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### setDate\n\n- 타입: `setDate(d: number): number`\n- 파라미터\n  - `d`: 지정할 일자\n- 리턴: `number` - 1970년 1월 1일 UTC 이후 변경된 TZDate 인스턴스 일시까지 경과 시간(밀리초)\n\nTZDate 인스턴스 일시의 일자를 주어진 숫자 값으로 지정한다. 기존 TZDate 인스턴스를 변경한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### setHours\n\n- 타입: `setHours(h: number, M?: number, s?: number, ms?: number): number`\n- 파라미터\n  - `h`: 지정할 시\n  - `M`: 지정할 분. 없다면 기존 TZDate 인스턴스 일시의 분이 유지된다.\n  - `s`: 지정할 초. 없다면 기존 TZDate 인스턴스 일시의 초가 유지된다.\n  - `ms`: 지정할 밀리초. 없다면 기존 TZDate 인스턴스 일시의 밀리초가 유지된다.\n- 리턴: `number` - 1970년 1월 1일 UTC 이후 변경된 TZDate 인스턴스 일시까지 경과 시간(밀리초)\n\nTZDate 인스턴스 일시의 시를 주어진 숫자 값으로 지정한다. 분, 초, 밀리초도 추가로 지정할 수 있다. 기존 TZDate 인스턴스를 변경한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### setMinutes\n\n- 타입: `setMinutes(M: number, s?: number, ms?: number): number`\n- 파라미터\n  - `M`: 지정할 분\n  - `s`: 지정할 초. 없다면 기존 TZDate 인스턴스 일시의 초가 유지된다.\n  - `ms`: 지정할 밀리초. 없다면 기존 TZDate 인스턴스 일시의 밀리초가 유지된다.\n- 리턴: `number` - 1970년 1월 1일 UTC 이후 변경된 TZDate 인스턴스 일시까지 경과 시간(밀리초)\n\nTZDate 인스턴스 일시의 분을 주어진 숫자 값으로 지정한다. 초, 밀리초도 추가로 지정할 수 있다. 기존 TZDate 인스턴스를 변경한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### setSeconds\n\n- 타입: `setSeconds(s: number, ms?: number): number`\n- 파라미터\n  - `s`: 지정할 초\n  - `ms`: 지정할 밀리초. 없다면 기존 TZDate 인스턴스 일시의 밀리초가 유지된다.\n- 리턴: `number` - 1970년 1월 1일 UTC 이후 변경된 TZDate 인스턴스 일시까지 경과 시간(밀리초)\n\nTZDate 인스턴스 일시의 초를 주어진 숫자 값으로 지정한다. 밀리초도 추가로 지정할 수 있다. 기존 TZDate 인스턴스를 변경한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### setMilliseconds\n\n- 타입: `setMilliseconds(ms: number): number`\n- 파라미터\n  - `ms`: 지정할 밀리초\n- 리턴: `number` - 1970년 1월 1일 UTC 이후 변경된 TZDate 인스턴스 일시까지 경과 시간(밀리초)\n\nTZDate 인스턴스 일시의 밀리초를 주어진 숫자 값으로 지정한다. 기존 TZDate 인스턴스를 변경한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### tz\n\n- 타입: `tz(tzValue: string | 'Local' | number): TZDate`\n- 파라미터\n  - `tzValue`: 지정할 타임존\n- 리턴: `TZDate` - 주어진 타임존을 따르는 새로운 TZDate 인스턴스\n\n주어진 타임존을 따르는 새로운 TZDate 인스턴스를 반환한다. `tzValue`은 [IANA 타임존 데이터베이스](https://www.iana.org/time-zones)의 타임존 이름, 타임존 오프셋 숫자 값, `'Local'`로 지정할 수 있다. `'Local'`로 지정할 경우 시스템 타임존을 따른다. 기존 TZDate 인스턴스를 변경하지 않고, 새로운 TZDate 인스턴스를 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n\n### local\n\n- 타입: `local(tzValue?: string | number): TZDate`\n- 파라미터\n  - `tzValue`: TZDate 인스턴스가 따르는 타임존. 없다면 TZDate 인스턴스의 타임존으로 계산한다.\n- 리턴: `TZDate` - 시스템 타임존을 따르는 새로운 TZDate 인스턴스\n\n시스템 타임존을 따르는 새로운 TZDate 인스턴스를 반환한다. `tzValue`는 TZDate 인스턴스가 자신에게 지정되어 있는 타임존이 아닌 다른 타임존을 따르고 있을 때 사용할 수 있다. 만약 `tzValue`가 없다면 TZDate 인스턴스의 타임존으로 계산한다. 기존 TZDate 인스턴스를 변경하지 않고, 새로운 TZDate 인스턴스를 반환한다.\n\n[⬆️ 목록으로 돌아가기](#인스턴스-메서드)\n"
  },
  {
    "path": "docs/ko/guide/getting-started.md",
    "content": "# 시작하기\n\n## 목차\n\n- [설치하기](#설치하기)\n  - [패키지 매니저 사용하기](#패키지-매니저-사용하기)\n    - [npm](#npm)\n  - [Contents Delivery Network (CDN) 사용하기](#contents-delivery-network-cdn-사용하기)\n  - [소스 파일 다운로드](#소스-파일-다운로드)\n- [사용하기](#사용하기)\n  - [HTML](#html)\n  - [자바스크립트](#자바스크립트)\n    - [불러오기](#불러오기)\n    - [레거시 브라우저용 번들 파일 불러오기](#레거시-브라우저용-번들-파일-불러오기)\n  - [CSS](#css)\n  - [인스턴스 만들기](#인스턴스-만들기)\n- [기본적인 사용 방법](#기본적인-사용-방법)\n  - [Google Analytics(GA)를 위한 hostname 수집 거부하기](#google-analyticsga를-위한-hostname-수집-거부하기)\n  - [일정 생성하기](#일정-생성하기)\n  - [팝업 사용하기](#팝업-사용하기)\n  - [테마 적용하기](#테마-적용하기)\n  - [템플릿 적용하기](#템플릿-적용하기)\n  - [인스턴스 이벤트 적용하기](#인스턴스-이벤트-적용하기)\n\n## 설치하기\n\nTOAST UI 제품들은 패키지 매니저를 이용하거나, 직접 소스 코드를 다운받아 사용할 수 있다. 하지만 패키지 매니저 사용을 권장한다.\n\n### 패키지 매니저 사용하기\n\nTOAST UI 제품들은 [npm](https://www.npmjs.com/) 패키지 매니저에 등록되어 있다.\n각 패키지 매니저가 제공하는 CLI 도구를 사용하면 쉽게 패키지를 설치할 수 있다. npm 사용을 위해선 [Node.js](https://nodejs.org)를 미리 설치해야 한다.\n\n#### npm\n\n```sh\nnpm install @toast-ui/calendar # 최신 버전\nnpm install @toast-ui/calendar@<version> # 2.0 이후 특정 버전\nnpm install tui-calendar@<version> # 1.x 특정 버전\n```\n\n### Contents Delivery Network (CDN) 사용하기\n\nTOAST UI Calendar는 CDN을 통해 사용할 수 있다.\n\n- 아래의 코드로 CDN을 사용할 수 있다.\n\n```html\n<link rel=\"stylesheet\" href=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css\" />\n<script src=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.min.js\"></script>\n\n<!-- 레거시 브라우저용 번들 파일 가져오기 -->\n<!-- <script src=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.ie11.min.js\"></script> -->\n\n<!-- 모듈로 가져오기 -->\n<!-- <script type=\"module\" src=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.mjs\"></script> -->\n```\n\n- CDN은 아래의 디렉토리 구조로 구성되어 있다.\n\n```\n- uicdn.toast.com/\n  ├─ calendar/\n  │  ├─ latest\n  │  │  ├─ toastui-calendar.css\n  │  │  ├─ toastui-calendar.js\n  │  │  ├─ toastui-calendar.min.css\n  │  │  ├─ toastui-calendar.min.js\n  │  │  ├─ toastui-calendar.ie11.js\n  │  │  ├─ toastui-calendar.ie11.min.js\n  │  │  │  toastui-calendar.mjs\n  │  ├─ v2.0.0/\n```\n\n### 소스 파일 다운로드\n\n- [각 버전의 소스코드 다운로드 하기](https://github.com/nhn/tui.calendar/releases)\n\n## 사용하기\n\n### HTML\n\nTOAST UI Calendar가 생성될 컨테이너 요소를 추가한다. **이 요소는 적절한 높이의 height 값을 가지고 있어야 한다. (최소 600px 이상 권장)**\n\n```html\n<div id=\"calendar\" style=\"height: 600px;\"></div>\n```\n\n### 자바스크립트\n\n#### 불러오기\n\nTOAST UI Calendar는 생성자 함수를 통해 인스턴스를 생성할 수 있다. 생성자 함수에 접근하기 위해서는 환경에 따라 접근할 수 있는 세 가지 방법이 존재한다.\n\n```js\n/* Node.js 환경에서 ES6 모듈 */\nimport Calendar from '@toast-ui/calendar';\n```\n\n```js\n/* Node.js 환경에서 CommonJS */\nconst Calendar = require('@toast-ui/calendar');\n```\n\n```js\n/* 브라우저 환경에서 namespace */\nconst Calendar = tui.Calendar;\n```\n\n#### 레거시 브라우저용 번들 파일 불러오기\n\nTOAST UI Calendar는 레거시 브라우저용 번들 파일을 따로 제공하고 있다. 기본 번들은 모던 브라우저의 최신 2개 버전을 안정적으로 지원한다. 하지만 기본 번들은 IE11을 위한 폴리필이 포함되어있지 않으므로 IE11 혹은 일정 수준 이하의 레거시 브라우저를 지원하기 위해서는 다음과 같이 폴리필이 포함된 IE11 번들을 추가해야 한다.\n\nIE11의 번들 크기는 기본 번들보다 30%가량 크기 때문에 반드시 지원 범위를 잘 고려하여 불필요하게 번들 사이즈를 늘리지 않도록 유의해야 한다.\n\n```js\n/* Node.js 환경에서 ES6 모듈 */\nimport Calendar from '@toast-ui/calendar/ie11';\n```\n\n```js\n/* Node.js 환경에서 CommonJS */\nconst Calendar = require('@toast-ui/calendar/ie11');\n```\n\n```html\n<!-- CDN과 브라우저 환경에서 namespace -->\n<script src=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.ie11.min.js\"></script>\n<script>\n  const Calendar = tui.Calendar;\n</script>\n```\n\n### CSS\n\nCalendar를 사용하기 위해서는 CSS 파일을 추가해야 한다. import, require를 통해 CSS 파일을 불러오거나, CDN을 통해 불러올 수 있다.\n\n```js\n/* Node.js 환경에서 ES6 모듈 */\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css'; // Calendar 스타일\n```\n\n```js\n/* Node.js 환경에서 CommonJS */\nrequire('@toast-ui/calendar/dist/toastui-calendar.min.css');\n```\n\n```html\n<!-- CDN -->\n<link rel=\"stylesheet\" href=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css\" />\n```\n\n### 인스턴스 만들기\n\n생성자 함수는 `container`, `options` 두 개를 인자로 갖는다.\n\n- `container`: TOAST UI Calendar를 자식 요소로 갖는 HTML 요소 또는 HTML 요소를 가져오기 위한 CSS 선택자 문자열\n- `options`: 기본 뷰 타입, 타임존, 테마, 템플릿 등 TOAST UI Calendar를 커스터마이징할 수 있는 옵션 객체. 자세한 정보는 [옵션 문서](../apis/options.md)를 참고한다.\n\n```js\nconst container = document.getElementById('calendar');\nconst options = {\n  defaultView: 'week',\n  timezone: {\n    zones: [\n      {\n        timezoneName: 'Asia/Seoul',\n        displayLabel: 'Seoul',\n      },\n      {\n        timezoneName: 'Europe/London',\n        displayLabel: 'London',\n      },\n    ],\n  },\n  calendars: [\n    {\n      id: 'cal1',\n      name: '개인',\n      backgroundColor: '#03bd9e',\n    },\n    {\n      id: 'cal2',\n      name: '직장',\n      backgroundColor: '#00a9ff',\n    },\n  ],\n};\n\nconst calendar = new Calendar(container, options);\n```\n\n![image](../../assets/gettingStarted_calendar.png)\n\n## 기본적인 사용 방법\n\n### Google Analytics(GA)를 위한 hostname 수집 거부하기\n\n[TOAST UI 캘린더](https://github.com/nhn/tui.calendar)는 [GA](https://analytics.google.com/analytics/web/)를 적용하여 오픈 소스 사용에 대한 통계를 수집하여 전 세계에서 얼마나 널리 사용되는지 확인한다.\n이는 프로젝트의 향후 진행을 결정하는 중요한 지표 역할을 한다.\n`location.hostname`(예를 들어 \"ui.toast.com\")을 수집하며 사용량에 대한 통계를 측정하기 위해서만 사용된다.\n\n만약 이를 거부하려면 [`usageStatistics` 옵션](/docs/ko/apis/options.md#usagestatistics)을 `false`로 설정한다.\n\n```js\nconst calendar = new Calendar('#calendar', {\n  usageStatistics: false\n});\n```\n\n### 일정 생성하기\n\n일정을 생성할 때는 Calendar 인스턴스의 [`createEvents` 메서드](../apis/calendar.md#createevents)를 사용한다.\n\n일정 정보는 [EventObject](../apis/event-object.md) 형태로 넘긴다.\n\n```js\ncalendar.createEvents([\n  {\n    id: 'event1',\n    calendarId: 'cal2',\n    title: '주간 회의',\n    start: '2022-06-07T09:00:00',\n    end: '2022-06-07T10:00:00',\n  },\n  {\n    id: 'event2',\n    calendarId: 'cal1',\n    title: '점심 약속',\n    start: '2022-06-08T12:00:00',\n    end: '2022-06-08T13:00:00',\n  },\n  {\n    id: 'event3',\n    calendarId: 'cal2',\n    title: '휴가',\n    start: '2022-06-08',\n    end: '2022-06-10',\n    isAllday: true,\n    category: 'allday',\n  },\n]);\n```\n\n![createEvents](../../assets/gettingStarted_createEvents.png)\n\n### 팝업 사용하기\n\nTOAST UI Calendar는 일정 생성 팝업과 일정 상세 팝업을 기본으로 제공한다. 이를 사용하려면 [`useFormPopup`](../apis/options.md#useformpopup)과 [`useDetailPopup`](../apis/options.md#usedetailpopup) 옵션을 `true`로 설정해야 한다. 옵션은 인스턴스 생성 시 설정하거나, 인스턴스 생성 후 [`setOptions`](../apis/calendar.md#setoptions) 메서드를 사용해서 변경할 수 있다.\n\n일정 생성 팝업을 사용할 때는 [`tui-date-picker`](https://github.com/nhn/tui.date-picker)와 [`tui-time-picker`](https://github.com/nhn/tui.time-picker)의 css 파일을 가져와야 스타일이 제대로 적용된다.\n\n```sh\nnpm install tui-date-picker tui-time-picker\n```\n\n```js\n// 일정 생성 팝업을 사용하기 위해 tui-date-picker와 tui-time-picker의 css 파일을 불러온다.\nimport 'tui-date-picker/dist/tui-date-picker.css';\nimport 'tui-time-picker/dist/tui-time-picker.css';\n\ncalendar.setOptions({\n  useFormPopup: true,\n  useDetailPopup: true,\n});\n```\n\n| 일정 생성 팝업                                    | 일정 상세 팝업                                        |\n| ------------------------------------------------ | ----------------------------------------------------- |\n| ![useFormPopup](../../assets/gettingStarted_useFormPopup.png) | ![useDetailPopup](../../assets/gettingStarted_useDetailPopup.png) |\n\n### 테마 적용하기\n\n색상, 배경색과 같은 스타일을 변경하고 싶을 때는 테마를 사용한다. 테마는 인스턴스 생성 시 [옵션 객체의 `theme` 프로퍼티](../apis/options.md#theme)에 명시하거나, 인스턴스 생성 후 [`setTheme`](../apis/calendar.md#settheme) 메서드를 사용해서 변경할 수 있다. 적용 가능한 테마는 [테마 문서](../apis/theme.md)를 참고한다.\n\n```js\ncalendar.setTheme({\n  common: {\n    gridSelection: {\n      backgroundColor: 'rgba(81, 230, 92, 0.05)',\n      border: '1px dotted #515ce6',\n    },\n  },\n});\n```\n\n![theme](../../assets/gettingStarted_theme.png)\n\n### 템플릿 적용하기\n\n템플릿은 커스텀 렌더링을 지원하는 기능이다. 인스턴스 생성 시 [옵션 객체의 `template` 프로퍼티](../apis/options.md#template)에 명시하거나, 인스턴스 생성 후 [`setOptions`](../apis/calendar.md#setoptions) 메서드를 사용해서 변경할 수 있다. 적용 가능한 템플릿은 [템플릿 문서](../apis/template.md)를 참고한다.\n\n```js\nfunction formatTime(time) {\n  const hours = `${time.getHours()}`.padStart(2, '0');\n  const minutes = `${time.getMinutes()}`.padStart(2, '0');\n\n  return `${hours}:${minutes}`;\n}\n\ncalendar.setOptions({\n  template: {\n    time(event) {\n      const { start, end, title } = event;\n\n      return `<span style=\"color: white;\">${formatTime(start)}~${formatTime(end)} ${title}</span>`;\n    },\n    allday(event) {\n      return `<span style=\"color: gray;\">${event.title}</span>`;\n    },\n  },\n});\n```\n\n![template](../../assets/gettingStarted_template.png)\n\n### 인스턴스 이벤트 적용하기\n\nTOAST UI Calendar는 인스턴스 이벤트를 제공한다. 필요에 따라 이벤트를 수신하도록 설정하여 원하는 동작을 실행시킬 수 있다. 또한 별도로 사용자가 자신만의 이벤트를 설정할 수도 있다.\n\n`on` 메서드를 사용하여 인스턴스 이벤트를 수신할 수 있다.\n\n자세한 내용은 [인스턴스 이벤트 문서](../apis/calendar.md#인스턴스-이벤트)를 참고한다.\n\n```js\ncalendar.on('clickEvent', ({ event }) => {\n  const el = document.getElementById('clicked-event');\n  el.innerText = event.title;\n});\n```\n\n![instance event](../../assets/gettingStarted_instanceEvent.gif)\n"
  },
  {
    "path": "docs/ko/guide/migration-guide-v2.md",
    "content": "# v2 마이그레이션 가이드\n\n## 목차\n\n- [개요](#개요)\n- [설치](#설치)\n  - [패키지 및 파일 이름 변경](#패키지-및-파일-이름-변경)\n  - [CDN 디렉토리 구조 변경](#cdn-디렉토리-구조-변경)\n- [브라우저 지원 범위(IE >= 11)](#브라우저-지원-범위ie--11)\n- [API 마이그레이션](#api-마이그레이션)\n  - [`schedule`에서 `event`로 용어 변경](#schedule에서-event로-용어-변경)\n  - [`currentTimeIndicator`, `currentTimeLine`에서 `nowIndicator`로 용어 변경](#currenttimeindicator-currenttimeline에서-nowindicator로-용어-변경)\n  - [기능 개선](#기능-개선)\n    - [렌더링 최적화](#렌더링-최적화)\n    - [테마 개선](#테마-개선)\n    - [view 관련 타입 개선](#view-관련-타입-개선)\n    - [taskView, eventView 타입 개선](#taskview-eventview-타입-개선)\n  - [변경](#변경)\n    - [옵션 변경사항](#옵션-변경사항)\n    - [인스턴스 메서드 변경사항](#인스턴스-메서드-변경사항)\n    - [인스턴스 이벤트 변경사항](#인스턴스-이벤트-변경사항)\n    - [템플릿 변경사항](#템플릿-변경사항)\n  - [제거](#제거)\n    - [렌더링 관련 파라미터 변경 사항](#렌더링-관련-파라미터-변경-사항)\n\n## 개요\n\n[preact](https://preactjs.com/)를 이용해 더 효율적으로 캘린더를 렌더링하는 TOAST UI Calendar v2.0이 출시되었다. v2에서는 번들 크기 개선 및 모던 개발 환경으로 업그레이드하여 다른 기능들을 추가하기 용이하게 만들기 위한 기반을 마련했다. 이를 이용해 캘린더를 사용하는 사용자들의 이해를 높일 수 있도록 마이그레이션을 가이드로 제공하고 있다.\n\n## 설치\n\n### 패키지 및 파일 이름 변경\n\n패키지명이 `tui-calendar`에서 `@toast-ui/calendar`로 변경되었다.\n\n```sh\n# v1\nnpm install tui-calendar@<version> # 1.x 특정 버전\n\n# v2\nnpm install @toast-ui/calendar # 최신 버전\nnpm install @toast-ui/calendar@<version> # 2.0 이후 특정 버전\n```\n\n파일명 또한 `tui-calendar`에서 `toastui-calendar`로 변경되었다.\n\n```js\n/* Node.js 환경에서 ES6 모듈 */\n// v1\nimport Calendar from 'tui-calendar';\nimport \"tui-calendar/dist/tui-calendar.min.css\";\n\n// v2\nimport Calendar from '@toast-ui/calendar';\nimport '@toast-ui/calendar/dist/toastui-calendar.min.css';\n```\n\n```html\n<!-- CDN -->\n<!-- v1 -->\n<link rel=\"stylesheet\" href=\"https://uicdn.toast.com/tui-calendar/latest/tui-calendar.min.css\" />\n<script src=\"https://uicdn.toast.com/tui-calendar/latest/tui-calendar.min.js\"></script>\n\n<!-- v2 -->\n<link rel=\"stylesheet\" href=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css\" />\n<script src=\"https://uicdn.toast.com/calendar/latest/toastui-calendar.min.js\"></script>\n```\n\n### CDN 디렉토리 구조 변경\n\nCDN의 디렉토리 구조와 번들 파일 이름이 변경되었다. v1에서는 `https://uicdn.toast.com/tui-calendar/latest/tui-calendar.js`와 같이 `tui-calendar` 라는 폴더 내에 `tui-calendar`라는 파일이 존재했다. 하지만 v2에서는 `https://uicdn.toast.com/calendar/latest/toastui-calendar.js`처럼 `calendar` 폴더 내에 `toastui-calendar`라는 파일이 존재한다.\n\nv1에서 사용하던 CDN 주소는 유지되지만, `/tui-calendar/latest/` 내부 파일은 TOAST UI Calendar의 최신버전이 아니라 v1의 최신 버전이다. 최신 버전을 사용하고 싶다면 `/calendar/latest/` 내부 파일을 사용해야 한다.\n\n```sh\n- uicdn.toast.com/\n  ├─ tui-calendar/ # v1\n  │  ├─ latest     # v1의 최신 버전\n  │  │  ├─ tui-calendar.css\n  │  │  ├─ tui-calendar.js\n  │  │  ├─ tui-calendar.min.css\n  │  │  ├─ tui-calendar.min.js\n  │  ├─ v1.0.0/    # v1의 특정 버전\n  │  │  ├─ ...\n  ├─ calendar/     # v2 이상\n  │  ├─ latest     # 최신 버전\n  │  │  ├─ toastui-calendar.css\n  │  │  ├─ toastui-calendar.js\n  │  │  ├─ toastui-calendar.min.css\n  │  │  ├─ toastui-calendar.min.js\n  │  │  ├─ toastui-calendar.ie11.js\n  │  │  ├─ toastui-calendar.ie11.min.js\n  │  │  │  toastui-calendar.mjs\n  │  ├─ v2.0.0/    # v2 이상 특정 버전\n  │  │  ├─ ...\n```\n\n## 브라우저 지원 범위(IE >= 11)\n\nv2부터 지원하는 브라우저 범위가 *인터넷 익스플로러 11 이상*으로 변경된다. v1에서는 인터넷 익스플로러 9 이상의 브라우저를 지원했지만 최신 개발 환경 및 [preact](https://preactjs.com/) X(10 버전)의 사용을 위해 지원 범위를 변경하게 되었다.\n\n기본 번들은 모던 브라우저의 최신 2개 버전을 안정적으로 지원한다. 하지만 기본 번들은 IE 11을 위한 폴리필이 포함되어있지 않으므로 IE 11 혹은 일정 수준 이하의 레거시 브라우저를 지원하기 위해서는 다음과 같이 폴리필이 포함된 IE 11 번들을 추가해야 한다.\nIE 11의 번들 크기는 기본 번들보다 30% 가량 크기 때문에 반드시 지원 범위를 잘 고려하여 불필요하게 번들 크기를 늘리지 않도록 유의해야 한다.\n\n```ts\nimport Calendar from '@toast-ui/calendar/ie11';\n```\n\n## API 마이그레이션\n\nv2를 사용하기 위해 API 마이그레이션이 필요한 API는 다음과 같다.\n\n- [옵션](../apis/options.md)\n- [테마](../apis/theme.md)\n- [인스턴스 이벤트](../apis/calendar.md#인스턴스-이벤트)\n- [인스턴스 메서드](../apis/calendar.md#인스턴스-메서드)\n\n마이그레이션 진행 단위는 크게 _기능 개선_, _변경_, _제거_ 로 구분된다.\n\n- [기능 개선](#기능-개선): 기능이 개선되거나 새로 추가된 API\n- [변경](#변경): 기능은 유지되나 이름, 타입 등이 변경된 API\n- [제거](#제거): 불필요하거나 스펙 아웃으로 제거된 API\n\nv2에서는 날짜나 시간을 선택할 때의 영역을 나타내는 `creationGuide`는 `gridSelection`으로 변경되었다. 각 패널을 조절하던 `vpanelSplitter`는 `panelResizer`로 용어가 변경되었다.\n`daygrid`나 `dayGridSchedule`처럼 통일되지 않은 용어들은 `dayGrid`나 `timeGrid`처럼 통일되었다.\n\n### `schedule`에서 `event`로 용어 변경\n\nv2에서는 일정이라는 의미에 맞게 기존 `schedule`에서 `event`로 네이밍이 변경되었다. 단순한 변수명 뿐만 아니라 `schedule`이 포함된 인스턴스 메서드, 인스턴스 이벤트 등의 관련된 API 모두가 `event`로 변경되었다.\n\n### `currentTimeIndicator`, `currentTimeLine`에서 `nowIndicator`로 용어 변경\n\nv1에서는 현재 시간선을 나타내는 용어로 `currentTimeIndicator`과 `currentTimeLine`이 혼용되었다. v2에서는 이를 `nowIndicator`로 통일했다.\n\n### 기능 개선\n\n#### 렌더링 최적화\n\nv1에서는 캘린더의 렌더링을 직접적인 DOM 조작으로 처리했다. 이에 따라 캘린더를 조작할 때마다 불필요한 렌더링이 일어날 수 있었다.\n\nv2에서는 가상 DOM을 이용해 불필요한 렌더링을 줄여 렌더링 속도를 개선하고 추후 서버 사이드 렌더링(SSR)을 지원하기 위해 [preact](https://preactjs.com/)를 도입하였다. 이에 따라 인스턴스 메서드 사용 시 렌더링을 제어하는 `force`, `silent` 등의 파라미터가 제거되었고 캘린더 내부의 상태에 따라 렌더링이 제어되어 인스턴스 메서드를 사용할 때 렌더링을 제어할 수 없게 되었다.\n\n#### 테마 개선\n\n[테마](../apis/theme.md)가 개선되었다. `.`으로 연결된 문자열 키 값으로 테마를 지정하는 방식에서 중첩 객체를 이용한 방식으로 개선되었다. 이에 따라 `setTheme` 메서드도 중첩 객체를 파라미터로 받아 처리하는 방식으로 개선되었다. 자세한 사항은 [테마](../apis/theme.md)에서 확인할 수 있다.\n\n```ts\n// v1\ncalendar.setTheme({\n  'common.dayName.color': '#333',\n});\n```\n\n```ts\n// v2\ncalendar.setTheme({\n  common: {\n    dayName: {\n      color: '#333',\n    },\n  },\n});\n```\n\nv1에서 사용하던 다음 프로퍼티들이 테마에서 제거되거나 이름이 변경되었다. 각종 색상 값들과 주간/일간뷰의 각 패널의 왼쪽 영역의 너비를 제외한 테마 값들은 전부 제거되었다.\n\n- `month.dayname.height`\n- `month.dayname.paddingLeft`\n- `month.dayname.paddingRight`\n- `month.dayname.fontSize`\n- `month.dayname.fontWeight`\n- `month.dayname.textAlign`\n- `month.day.fontSize`\n- `month.schedule` 전체\n- `month.moreView.paddingBottom`\n- `month.moreViewTitle.height`\n- `month.moreViewTitle.marginBottom`\n- `month.moreViewTitle.borderBottom`\n- `month.moreViewTitle.padding`\n- `month.moreViewList.padding`\n- `week.dayname.height`\n- `week.dayname.paddingLeft`\n- `week.dayname.textAlign`\n- `week.vpanelSplitter.height`\n- `week.vpanelSplitter.border` -> `week.panelResizer.border`\n- `week.daygridLeft.paddingRight`\n- `week.timegridLeft.fontSize`\n- `week.timegridLeftTimezoneLabel.height`\n- `week.timegridOneHour.height`\n- `week.timegridHalfHour.height`\n- `week.timegridHalfHour.borderBottom` -> `week.timeGridHalfHourLine.borderBottm`\n- `week.timegridHorizontalLine.borderBottom` -> `week.timeGridHourLine.borderBottom`\n- `week.timegrid.paddingRight`\n- `week.timegridSchedule` 전체\n- `week.currentTime` -> `week.nowIndicatorLabel`\n- `week.currentTime.fontSize`\n- `week.currentTime.fontWeight`\n- `week.currentTimeLinePast` -> `week.nowIndicatorPast`\n- `week.currentTimeLineBullet` -> `week.nowIndicatorBullet`\n- `week.currentTimeLineToday` -> `week.nowIndicatorToday`\n- `week.currentTimeLineFuture` -> `week.nowIndicatorFuture`\n- `week.pastTime.fontWeight`\n- `week.futureTime.fontWeight`\n- `week.creationGuide.fontSize`\n- `week.creationGuide.fontWeight`\n- `week.dayGridSchedule` 전체\n\n제거된 테마 값은 대신 CSS를 활용해 속성을 적용할 수 있다. 다음은 제거된 테마 값에 연관된 CSS 파일이다.\n\n| 제거된 테마 값                                           | 연관 파일 위치                                                   |\n| -------------------------------------------------------- | ---------------------------------------------------------------- |\n| <code>month.dayname</code> 관련                          | [dayNames.css](/apps/calendar/src/css/daygrid/dayNames.css)      |\n| <code>month.shedule</code> 관련                          | [dayGrid.css](/apps/calendar/src/css/daygrid/dayGrid.css)        |\n| <code>month.moreView</code> 관련                         | [seeMore.css](/apps/calendar/src/css/popup/seeMore.css)          |\n| <code>week.dayname</code> 관련                           | [dayNames.css](/apps/calendar/src/css/daygrid/dayNames.css)      |\n| <code>week.dayGridLeft</code> 관련                       | [allday.css](/apps/calendar/src/css/panel/allday.css)            |\n| <code>week.timeGridLeft</code> 관련                      | [timeColumn.css](/apps/calendar/src/css/timegrid/timeColumn.css) |\n| <code>week.timeGridSchedule</code> 관련                  | [time.css](/apps/calendar/src/css/events/time.css)               |\n| <code>week.gridSelection</code>(v1의 creationGuide) 관련 | [column.css](/apps/calendar/src/css/timegrid/column.css)         |\n| <code>week.dayGridSchedule</code> 관련                   | [dayGrid.css](/apps/calendar/src/css/daygrid/dayGrid.css)        |\n\n#### view 관련 타입 개선\n\nview와 관련된 인스턴스 메서드의 파라미터 및 반환 타입이 좀 더 명확히 나오게 되었다. 캘린더에서 사용하는 뷰의 종류는 월간뷰, 주간뷰 및 일간뷰의 3가지 뷰로 나눠지며 이는 캘린더에서 `'month'`, `'week'`, `'day'`의 타입을 가지도록 타입을 명확히 하였다.\n\n| 메서드명                 | 변경사항                                |\n| ------------------------ | --------------------------------------- |\n| <code>changeView</code>  | 변경하려는 뷰 이름 파라미터의 타입 개선 |\n| <code>getViewName</code> | 반환되는 뷰 이름의 타입 개선            |\n\n#### taskView, eventView 타입 개선\n\n주간/일간뷰에서 `milestone`, `task` 패널을 표시할지 여부를 나타내는 `taskView` 옵션과 `allday`, `time` 패널을 표시할지 여부를 나타내는 `eventView` 옵션의 타입이 명확하게 변경되었다.\n\n```ts\nconst calendar = new Calendar('#calendar', {\n  week: {\n    taskView: ['task'],\n    eventView: ['time'],\n  },\n});\n```\n\n### 변경\n\n#### 옵션 변경사항\n\n아래의 옵션이 옵션 객체 내에서 위치가 이동하거나 테마로 이동되었다.\n\n| 옵션                              | 변경사항                                         | 추가 설명                                                  |\n| --------------------------------- | ------------------------------------------------ | ---------------------------------------------------------- |\n| options.taskView                  | options.week.taskView                            |                                                            |\n| options.eventView                 | options.week.eventView                           |                                                            |\n| options.disableDblClick           | options.gridSelection.enableDblClick             | 기본값이 <code>false</code>에서 <code>true</code>로 변경됨 |\n| options.disableClick              | options.gridSelection.enableClick                | 기본값이 <code>false</code>에서 <code>true</code>로 변경됨 |\n| options.timezone.offsetCalculator | options.timezone.customOffsetCalculator          |                                                            |\n| options.month.grid                | [테마](../apis/theme.md)로 이동                  |                                                            |\n| options.month.moreLayerSize       | [테마](../apis/theme.md)로 이동                  |                                                            |\n| options.month.isAlways6Week       | <code>options.month.isAlways6Weeks</code>로 변경 |                                                            |\n| options.month.daynames            | <code>options.month.dayNames</code>로 변경       |                                                            |\n| options.week.daynames             | <code>options.week.dayNames</code>로 변경        |                                                            |\n\n#### 인스턴스 메서드 변경사항\n\nv1의 `creation popup`이라는 명칭이 v2에서 `form popup`이라는 명칭으로 변경됨에 따라 팝업을 띄우는 인스턴스 메서드 `openCreationPopup`도 `openFormPopup`으로 변경되었다.\n\n```ts\n// v1\ncalendar.openCreationPopup(schedule);\n```\n\n```ts\n// v2\ncalendar.openFormPopup(event);\n```\n\n특정 캘린더 ID를 가진 일정들을 보이지 않게 하거나 보이게 하는 v1의 `toggleSchedules` 인스턴스 메서드는 보다 정확한 의미를 지닌 `setCalendarVisibility`로 변경되었다. 다음은 캘린더 id가 `'1'`인 일정들을 보이지 않게 하는 예제다.\n\n```ts\n// v1\ncalendar.toggleSchedules('1', true);\n```\n\n```ts\n// v2\ncalendar.setCalendarVisibility('1', false);\n```\n\n#### 인스턴스 이벤트 변경사항\n\nv1의 `clickMore` 인스턴스 이벤트가 좀 더 명확한 의미를 가진 `clickMoreEventsBtn`으로 변경되었다.\n\n```ts\n// v1\ncalendar.on('clickMore', (event) => {\n  console.log(event.date, event.target);\n});\n```\n\n```ts\n// v2\ncalendar.on('clickMoreEventsBtn', (event) => {\n  console.log(event.date, event.target);\n});\n```\n\n#### 템플릿 변경사항\n\nv1의 `timegridCurrentTime`이 `timegridNowIndicatorLabel`로 이름이 변경되었다.\n\n### 제거\n\nv2에서는 [bower](https://bower.io/)에 대한 지원과 `jquery plugin`에 대한 지원을 중단했다.\n\n#### 렌더링 관련 파라미터 변경 사항\n\n다음 인스턴스 메서드들의 `force`, `silent` 또는 `immediately` 파라미터가 제거되었다. v2에서는 [preact](https://preactjs.com/)를 통한 렌더링 방식을 사용하므로 인위적으로 렌더링을 조절할 수 있는 해당 파라미터들은 제거되었다.\n\n- `changeView`\n- `clear`\n- `createEvents` (v1의 `createSchedules`)\n- `deleteEvent` (v1의 `deleteSchedule`)\n- `render`\n- `setCalendarColor`\n- `setOptions`\n- `updateEvent`\n"
  },
  {
    "path": "jest.config.js",
    "content": "module.exports = {\n  projects: ['<rootDir>/libs/date/jest.config.js', '<rootDir>/apps/calendar/jest.config.js'],\n};\n"
  },
  {
    "path": "libs/date/.eslintrc.js",
    "content": "module.exports = {\n  root: true,\n  env: {\n    browser: true,\n    es6: true,\n    node: true,\n    'jest/globals': true,\n  },\n  plugins: ['prettier', 'jest'],\n  extends: ['tui', 'plugin:prettier/recommended', 'plugin:jest/recommended'],\n  parserOptions: {\n    sourceType: 'module',\n  },\n};\n"
  },
  {
    "path": "libs/date/index.d.ts",
    "content": "// Type definitions for TOAST UI Date v0.0.3\nexport interface TuiDateConstructor {\n  new (...args: any[]): DateInterface;\n}\n\nexport class DateInterface {\n  constructor(...args: any[]);\n\n  setTimezoneOffset(offset: number): DateInterface;\n\n  setTimezoneName(zoneName: string): DateInterface;\n\n  clone(): DateInterface;\n\n  toDate(): Date;\n\n  getTime(): number;\n\n  getTimezoneOffset(): number;\n\n  getFullYear(): number;\n\n  getMonth(): number;\n\n  getDate(): number;\n\n  getHours(): number;\n\n  getMinutes(): number;\n\n  getSeconds(): number;\n\n  getMilliseconds(): number;\n\n  getDay(): number;\n\n  setTime(time: number): number;\n\n  setFullYear(year: number, month?: number, date?: number): number;\n\n  setMonth(month: number, date?: number): number;\n\n  setDate(date: number): number;\n\n  setHours(hours: number, min?: number, sec?: number, ms?: number): number;\n\n  setMinutes(min: number, sec?: number, ms?: number): number;\n\n  setSeconds(sec: number, ms?: number): number;\n\n  setMilliseconds(ms: number): number;\n\n  toString(): string;\n}\n\nexport class LocalDate extends DateInterface {}\nexport class UTCDate extends DateInterface {}\nexport class MomentDate extends DateInterface {\n  static setMoment(moment: any): TuiDateConstructor;\n}\n"
  },
  {
    "path": "libs/date/jest.config.js",
    "content": "module.exports = {\n  clearMocks: true,\n  testEnvironment: 'node'\n};\n"
  },
  {
    "path": "libs/date/package.json",
    "content": "{\n  \"name\": \"@toast-ui/date\",\n  \"version\": \"0.0.3\",\n  \"description\": \"TOAST UI Components: A Date interface  which is extensible. It provides UTCDate, LocalDate, MomentDate.\",\n  \"main\": \"dist/toastui-date.js\",\n  \"module\": \"src/\",\n  \"types\": \"index.d.ts\",\n  \"files\": [\n    \"dist/\",\n    \"src/\",\n    \"index.d.ts\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/nhn/toast-ui.date.git\"\n  },\n  \"scripts\": {\n    \"lint\": \"eslint ./src ./test\",\n    \"build\": \"webpack --mode=production && webpack --mode=production --env minify && node tsBannerGenerator.js\",\n    \"doc\": \"tuidoc\",\n    \"analyze\": \"NODE_ENV=production webpack --mode=production --minify --profile --json > stats.json && webpack-bundle-analyzer stats.json ./dist\",\n    \"prepare\": \"npm run build\"\n  },\n  \"keywords\": [\n    \"nhn\",\n    \"nhnent\",\n    \"toastui\",\n    \"toast-ui\",\n    \"tui\",\n    \"component\",\n    \"date\",\n    \"timezone\",\n    \"moment\"\n  ],\n  \"author\": \"NHN FE Development Lab <dl_javascript@nhn.com>\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"tui-jsdoc-template\": \"^1.2.2\"\n  },\n  \"dependencies\": {\n    \"moment\": \"^2.29.4\",\n    \"moment-timezone\": \"^0.5.35\"\n  }\n}\n"
  },
  {
    "path": "libs/date/src/index.js",
    "content": "import LocalDate from './localDate';\nimport UTCDate from './utcDate';\nimport MomentDate from './momentDate';\n\nexport default { LocalDate, UTCDate, MomentDate };\nexport { LocalDate, UTCDate, MomentDate };\n"
  },
  {
    "path": "libs/date/src/localDate.js",
    "content": "import isString from 'tui-code-snippet/type/isString';\n/**\n * datetime regex from https://www.regexpal.com/94925\n * timezone regex from moment\n */\nconst rISO8601 =\n  /^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\\.)?([0-9]+)?([+-]\\d\\d(?::?\\d\\d)?|\\s*Z)?$/;\n\nfunction throwNotSupported() {\n  throw new Error('This operation is not supported.');\n}\n\nfunction getDateTime(dateString) {\n  const match = rISO8601.exec(dateString);\n  if (match) {\n    const [, y, M, d, h, m, s, , ms, zoneInfo] = match;\n\n    return {\n      y: Number(y),\n      M: Number(M) - 1,\n      d: Number(d),\n      h: Number(h),\n      m: Number(m),\n      s: Number(s),\n      ms: Number(ms) || 0,\n      zoneInfo,\n    };\n  }\n\n  return null;\n}\n\nfunction createFromDateString(dateString) {\n  const info = getDateTime(dateString);\n  if (info && !info.zoneInfo) {\n    const { y, M, d, h, m, s, ms } = info;\n\n    return new Date(y, M, d, h, m, s, ms);\n  }\n\n  return null;\n}\n\nexport default class LocalDate {\n  constructor(...args) {\n    const [firstArg] = args;\n\n    if (firstArg instanceof Date) {\n      this.d = new Date(firstArg.getTime());\n    } else if (isString(firstArg) && args.length === 1) {\n      this.d = createFromDateString(firstArg);\n    }\n\n    if (!this.d) {\n      this.d = new Date(...args);\n    }\n  }\n\n  setTimezoneOffset() {\n    throwNotSupported();\n  }\n\n  setTimezoneName() {\n    throwNotSupported();\n  }\n\n  clone() {\n    return new LocalDate(this.d);\n  }\n\n  toDate() {\n    return new Date(this.d.getTime());\n  }\n\n  toString() {\n    return this.d.toString();\n  }\n}\n\nconst getterMethods = [\n  'getTime',\n  'getTimezoneOffset',\n  'getFullYear',\n  'getMonth',\n  'getDate',\n  'getHours',\n  'getMinutes',\n  'getSeconds',\n  'getMilliseconds',\n  'getDay',\n];\n\nconst setterMethods = [\n  'setTime',\n  'setFullYear',\n  'setMonth',\n  'setDate',\n  'setHours',\n  'setMinutes',\n  'setSeconds',\n  'setMilliseconds',\n];\n\ngetterMethods.forEach((methodName) => {\n  LocalDate.prototype[methodName] = function (...args) {\n    return this.d[methodName](...args);\n  };\n});\n\nsetterMethods.forEach((methodName) => {\n  LocalDate.prototype[methodName] = function (...args) {\n    return this.d[methodName](...args);\n  };\n});\n"
  },
  {
    "path": "libs/date/src/momentDate.js",
    "content": "let moment;\n\nexport default class MomentDate {\n  static setMoment(m) {\n    moment = m;\n\n    return MomentDate;\n  }\n\n  constructor(...args) {\n    if (!moment) {\n      throw new Error(\n        'MomentDate requires Moment constructor. Use \"MomentDate.setMoment(moment);\".'\n      );\n    }\n\n    this.m = moment(...args);\n  }\n\n  setTimezoneOffset(offset) {\n    this.m.utcOffset(-offset);\n\n    return this;\n  }\n\n  setTimezoneName(zoneName) {\n    if (this.m.tz) {\n      this.m.tz(zoneName);\n    } else {\n      throw new Error(\n        'It requires moment-timezone. Use \"MomentDate.setMoment()\" with moment-timezone'\n      );\n    }\n\n    return this;\n  }\n\n  clone() {\n    return new MomentDate(this.m);\n  }\n\n  toDate() {\n    return this.m.toDate();\n  }\n\n  toString() {\n    return this.m.format();\n  }\n\n  getTime() {\n    return this.m.valueOf();\n  }\n\n  getTimezoneOffset() {\n    const offset = -this.m.utcOffset();\n\n    return Math.abs(offset) ? offset : 0;\n  }\n\n  getFullYear() {\n    return this.m.year();\n  }\n\n  getMonth() {\n    return this.m.month();\n  }\n\n  getDate() {\n    return this.m.date();\n  }\n\n  getHours() {\n    return this.m.hours();\n  }\n\n  getMinutes() {\n    return this.m.minutes();\n  }\n\n  getSeconds() {\n    return this.m.seconds();\n  }\n\n  getMilliseconds() {\n    return this.m.milliseconds();\n  }\n\n  getDay() {\n    return this.m.day();\n  }\n\n  setTime(t) {\n    this.m = moment(t);\n\n    return this.getTime();\n  }\n\n  setFullYear(y, m = this.getMonth(), d = this.getDate()) {\n    this.m.year(y).month(m).date(d);\n\n    return this.getTime();\n  }\n\n  setMonth(m, d = this.m.date()) {\n    this.m.month(m).date(d);\n\n    return this.getTime();\n  }\n\n  setDate(d) {\n    this.m.date(d);\n\n    return this.getTime();\n  }\n\n  setHours(h, m = this.getMinutes(), s = this.getSeconds(), ms = this.getMilliseconds()) {\n    this.m.hours(h).minutes(m).seconds(s).milliseconds(ms);\n\n    return this.getTime();\n  }\n\n  setMinutes(m, s = this.getSeconds(), ms = this.getMilliseconds()) {\n    this.m.minutes(m).seconds(s).milliseconds(ms);\n\n    return this.getTime();\n  }\n\n  setSeconds(s, ms = this.getMilliseconds()) {\n    this.m.seconds(s).milliseconds(ms);\n\n    return this.getTime();\n  }\n\n  setMilliseconds(ms) {\n    this.m.milliseconds(ms);\n\n    return this.getTime();\n  }\n}\n"
  },
  {
    "path": "libs/date/src/utcDate.js",
    "content": "import LocalDate from './localDate';\n\nexport default class UTCDate extends LocalDate {\n  clone() {\n    return new UTCDate(this.d);\n  }\n\n  getTimezoneOffset() {\n    return 0;\n  }\n}\n\nconst getterProperties = [\n  'FullYear',\n  'Month',\n  'Date',\n  'Hours',\n  'Minutes',\n  'Seconds',\n  'Milliseconds',\n  'Day',\n];\n\nconst setterProperties = [\n  'FullYear',\n  'Month',\n  'Date',\n  'Hours',\n  'Minutes',\n  'Seconds',\n  'Milliseconds',\n];\n\ngetterProperties.forEach((prop) => {\n  const methodName = `get${prop}`;\n\n  UTCDate.prototype[methodName] = function (...args) {\n    return this.d[`getUTC${prop}`](...args);\n  };\n});\n\nsetterProperties.forEach((prop) => {\n  const methodName = `set${prop}`;\n\n  UTCDate.prototype[methodName] = function (...args) {\n    return this.d[`setUTC${prop}`](...args);\n  };\n});\n"
  },
  {
    "path": "libs/date/test/localDate.spec.js",
    "content": "import LocalDate from '../src/localDate';\n\ntest('LocalDate uses local date.', () => {\n  const date = new LocalDate('2020-01-29T19:20:00');\n\n  expect(date.toDate() instanceof Date).toBe(true);\n  expect(() => date.setTimezoneOffset(420)).toThrow();\n  expect(() => date.setTimezoneName('Asia/Seoul')).toThrow();\n\n  const cloned = date.clone();\n\n  expect(cloned instanceof LocalDate).toBe(true);\n  expect(cloned.getTime()).toBe(date.getTime());\n});\n\ntest('LocalDate uses local date getters', () => {\n  const nativeDate = new Date('2020-01-29T19:20:00');\n  const date = new LocalDate('2020-01-29T19:20:00');\n\n  expect(date.getTime()).toBe(nativeDate.getTime());\n  expect(date.getTimezoneOffset()).toBe(nativeDate.getTimezoneOffset());\n  expect(date.getFullYear()).toBe(nativeDate.getFullYear());\n  expect(date.getMonth()).toBe(nativeDate.getMonth());\n  expect(date.getDate()).toBe(nativeDate.getDate());\n  expect(date.getHours()).toBe(nativeDate.getHours());\n  expect(date.getMinutes()).toBe(nativeDate.getMinutes());\n  expect(date.getSeconds()).toBe(nativeDate.getSeconds());\n  expect(date.getMilliseconds()).toBe(nativeDate.getMilliseconds());\n  expect(date.getDay()).toBe(nativeDate.getDay());\n});\n\ntest('LocalDate uses local date setters', () => {\n  const nativeDate = new Date('2020-01-29T19:20:00');\n  const date = new LocalDate();\n  const time = 1580293200000;\n  const ONE_MINUTE = 60 * 1000;\n  const ONE_SECOND = 1 * 1000;\n\n  expect(date.setTime(time)).toBe(nativeDate.getTime());\n  expect(date.setFullYear(2020, 0, 29)).toBe(time);\n  expect(date.setMonth(0, 29)).toBe(time);\n  expect(date.setDate(29)).toBe(time);\n  expect(date.setHours(19, 20, 0, 0)).toBe(time);\n  expect(date.setMinutes(21, 0, 0)).toBe(time + ONE_MINUTE);\n  expect(date.setSeconds(1, 0)).toBe(time + ONE_MINUTE + ONE_SECOND);\n  expect(date.setMilliseconds(1)).toBe(time + ONE_MINUTE + ONE_SECOND + 1);\n});\n\ntest('If iso 8601 date string and no timezone info, LocalDate uses new Date(year, monthIndex[, day[, hour[, minutes[, seconds[, milliseconds]]]]]);', () => {\n  const nativeDate = new Date(2020, 0, 29, 19, 20, 0);\n  const date = new LocalDate('2020-01-29T19:20:00');\n\n  expect(date.getTime()).toBe(nativeDate.getTime());\n});\n\ntest('If iso 8601 date string and timezone info, LocalDate uses new Date(dateString);', () => {\n  const nativeDate = new Date('2020-01-29T19:20:00Z');\n  const date = new LocalDate('2020-01-29T19:20:00Z');\n\n  expect(date.getTime()).toBe(nativeDate.getTime());\n});\n\ntest('If dateString matches the regular expression, LocalDate can get a valid date', () => {\n  expect(new Date('2020-01-29T19:20:00.999').getTime()).toBe(\n    new LocalDate('2020-01-29T19:20:00.999').getTime()\n  );\n  expect(new Date('2020-01-29T19:20:00').getTime()).toBe(\n    new LocalDate('2020-01-29T19:20:00').getTime()\n  );\n  expect(new Date('2020-01-29T19:20:00.999+09:00').getTime()).toBe(\n    new LocalDate('2020-01-29T19:20:00.999+09:00').getTime()\n  );\n  expect(new Date('2020-01-29T19:20:00.999Z').getTime()).toBe(\n    new LocalDate('2020-01-29T19:20:00.999Z').getTime()\n  );\n  expect(new Date('2020-01-29T19:20:00+09:00').getTime()).toBe(\n    new LocalDate('2020-01-29T19:20:00+09:00').getTime()\n  );\n});\n"
  },
  {
    "path": "libs/date/test/momentDate.moment-timezone.spec.js",
    "content": "import moment from 'moment-timezone';\nimport MomentDate from '../src/momentDate';\n\ndescribe('MomentDate based on moment-timezone', () => {\n  beforeEach(() => {\n    MomentDate.setMoment(moment);\n  });\n\n  test('setTimezoneName() should set a timezone name if having moment-timezone', () => {\n    const timezoneName = 'US/Pacific';\n    const pst = 480;\n    const pdt = 420;\n    const jun = new MomentDate('2020-06-01T00:00:00').setTimezoneName(timezoneName);\n    const dec = new MomentDate('2020-12-01T00:00:00').setTimezoneName(timezoneName);\n    const utc = new MomentDate('2020-12-01T00:00:00').setTimezoneName('Etc/UTC');\n\n    expect(jun.getTimezoneOffset()).toBe(pdt);\n    expect(dec.getTimezoneOffset()).toBe(pst);\n    expect(utc.getTimezoneOffset()).toBe(0);\n  });\n});\n"
  },
  {
    "path": "libs/date/test/momentDate.moment.spec.js",
    "content": "import moment from 'moment';\nimport MomentDate from '../src/momentDate';\n\ndescribe('MomentDate should throw error', () => {\n  test('if there is no moment', () => {\n    expect(() => new MomentDate()).toThrow();\n  });\n\n  test('setTimezoneName() needs moment-timezone', () => {\n    MomentDate.setMoment(moment);\n\n    const timezoneName = 'US/Pacific';\n\n    expect(() => new MomentDate('2020-06-01T00:00:00').setTimezoneName(timezoneName)).toThrow();\n\n    MomentDate.setMoment();\n  });\n});\n\ndescribe('MomentDate based on moment', () => {\n  beforeEach(() => {\n    MomentDate.setMoment(moment);\n  });\n\n  test('toDate() return a native Date instance)', () => {\n    const date = new MomentDate('2020-01-29T19:20:00');\n\n    expect(date.toDate() instanceof Date).toBe(true);\n    expect(date.toDate().getTime()).toBe(date.getTime());\n  });\n\n  test('clone() returns a cloned instance', () => {\n    const date = new MomentDate('2020-01-29T19:20:00');\n    const cloned = date.clone();\n\n    expect(cloned instanceof MomentDate).toBe(true);\n    expect(cloned.getTime()).toBe(date.getTime());\n  });\n\n  test('getters should do same thing with native Date', () => {\n    const nativeDate = new Date('2020-01-29T19:20:00');\n    const date = new MomentDate('2020-01-29T19:20:00');\n\n    expect(date.getTime()).toBe(nativeDate.getTime());\n    expect(date.getTimezoneOffset()).toBe(nativeDate.getTimezoneOffset());\n    expect(date.getFullYear()).toBe(nativeDate.getFullYear());\n    expect(date.getMonth()).toBe(nativeDate.getMonth());\n    expect(date.getDate()).toBe(nativeDate.getDate());\n    expect(date.getHours()).toBe(nativeDate.getHours());\n    expect(date.getMinutes()).toBe(nativeDate.getMinutes());\n    expect(date.getSeconds()).toBe(nativeDate.getSeconds());\n    expect(date.getMilliseconds()).toBe(nativeDate.getMilliseconds());\n    expect(date.getDay()).toBe(nativeDate.getDay());\n  });\n\n  test('setters should do same thing with native Date', () => {\n    const nativeDate = new Date('2020-01-29T19:20:00');\n    const date = new MomentDate();\n    const time = nativeDate.getTime();\n    const ONE_MINUTE = 60 * 1000;\n    const ONE_SECOND = 1 * 1000;\n\n    expect(date.setTime(time)).toBe(time);\n    expect(date.setFullYear(2020, 0, 29)).toBe(time);\n    expect(date.setMonth(0, 29)).toBe(time);\n    expect(date.setDate(29)).toBe(time);\n    expect(date.setHours(19, 20, 0, 0)).toBe(time);\n    expect(date.setMinutes(21, 0, 0)).toBe(time + ONE_MINUTE);\n    expect(date.setSeconds(1, 0)).toBe(time + ONE_MINUTE + ONE_SECOND);\n    expect(date.setMilliseconds(1)).toBe(time + ONE_MINUTE + ONE_SECOND + 1);\n  });\n\n  test('setTimezoneOffset() should set utcOffset', () => {\n    const offset = 420;\n    const date = new MomentDate().setTimezoneOffset(offset);\n    const utc = new MomentDate().setTimezoneOffset(0);\n\n    expect(date.getTimezoneOffset()).toBe(offset);\n    expect(utc.getTimezoneOffset()).toBe(0);\n  });\n});\n"
  },
  {
    "path": "libs/date/test/utcDate.spec.js",
    "content": "import UTCDate from '../src/utcDate';\n\ntest('UTCDate uses utc date.', () => {\n  const date = new UTCDate('2020-01-29T19:20:00');\n\n  expect(date.d instanceof Date).toBe(true);\n  expect(date.toDate() instanceof Date).toBe(true);\n  expect(() => date.setTimezoneOffset(420)).toThrow();\n  expect(() => date.setTimezoneName('Asia/Seoul')).toThrow();\n\n  const cloned = date.clone();\n\n  expect(cloned instanceof UTCDate).toBe(true);\n  expect(cloned.getTime()).toBe(date.getTime());\n});\n\ntest('UTCDate uses utc date getters', () => {\n  const nativeDate = new Date('2020-01-29T19:20:00');\n  const date = new UTCDate('2020-01-29T19:20:00');\n\n  expect(date.getTime()).toBe(nativeDate.getTime());\n  expect(date.getTimezoneOffset()).toBe(0);\n  expect(date.getFullYear()).toBe(nativeDate.getUTCFullYear());\n  expect(date.getMonth()).toBe(nativeDate.getUTCMonth());\n  expect(date.getDate()).toBe(nativeDate.getUTCDate());\n  expect(date.getHours()).toBe(nativeDate.getUTCHours());\n  expect(date.getMinutes()).toBe(nativeDate.getUTCMinutes());\n  expect(date.getSeconds()).toBe(nativeDate.getUTCSeconds());\n  expect(date.getMilliseconds()).toBe(nativeDate.getUTCMilliseconds());\n  expect(date.getDay()).toBe(nativeDate.getUTCDay());\n});\n\ntest('UTCDate uses utc date setters', () => {\n  const nativeDate = new Date();\n  const date = new UTCDate();\n  const time = 1580293200000;\n\n  expect(date.setTime(time)).toBe(nativeDate.setTime(time));\n  expect(date.setFullYear(2020, 0, 29)).toBe(nativeDate.setUTCFullYear(2020, 0, 29));\n  expect(date.setMonth(0, 29)).toBe(nativeDate.setUTCMonth(0, 29));\n  expect(date.setDate(29)).toBe(nativeDate.setUTCDate(29));\n  expect(date.setHours(19, 20, 0, 0)).toBe(nativeDate.setUTCHours(19, 20, 0, 0));\n  expect(date.setMinutes(21, 0, 0)).toBe(nativeDate.setUTCMinutes(21, 0, 0));\n  expect(date.setSeconds(1, 0)).toBe(nativeDate.setUTCSeconds(1, 0));\n  expect(date.setMilliseconds(1)).toBe(nativeDate.setUTCMilliseconds(1));\n});\n"
  },
  {
    "path": "libs/date/tsBannerGenerator.js",
    "content": "/*eslint-disable*/\nconst fs = require('fs');\nconst path = require('path');\nconst pkg = require('./package.json');\n\nconst declareFilePath = path.join(__dirname, 'index.d.ts');\nconst declareRows = fs.readFileSync(declareFilePath).toString().split('\\n');\nconst TS_BANNER = ['// Type definitions for TOAST UI Date v' + pkg.version].join('\\n');\n\ndeclareRows.splice(0, 1, TS_BANNER);\nfs.writeFileSync(declareFilePath, declareRows.join('\\n'));\n"
  },
  {
    "path": "libs/date/tuidoc.config.json",
    "content": "{\n  \"header\": {\n    \"logo\": {\n      \"src\": \"https://uicdn.toast.com/toastui/img/tui-component-bi-white.png\"\n    },\n    \"title\": {\n      \"text\": \"Date\",\n      \"linkUrl\": \"https://github.com/nhn/toast-ui.date\"\n    },\n    \"version\": true\n  },\n  \"footer\": [\n    {\n      \"title\": \"NHN\",\n      \"linkUrl\": \"https://github.com/nhn\"\n    },\n    {\n      \"title\": \"FE Development Lab\",\n      \"linkUrl\": \"https://ui.toast.com/\"\n    }\n  ],\n  \"main\": {\n    \"filePath\": \"README.md\"\n  },\n  \"api\": {\n    \"filePath\": [\"src/js/**\"],\n    \"permalink\": {\n      \"repository\": \"https://github.com/nhn/tui.calendar\",\n      \"ref\": \"master\"\n    }\n  },\n  \"examples\": {\n    \"filePath\": \"examples\",\n    \"titles\": {\n      \"example01-basic\": \"1. Basic\",\n      \"example02-theme\": \"2. Theme\",\n      \"example03-custom-events\": \"3. Custom events\"\n    },\n    \"globalErrorLogVariable\": \"errorLogs\"\n  },\n  \"pathPrefix\": \"toast-ui.date\"\n}\n"
  },
  {
    "path": "libs/date/webpack.config.js",
    "content": "/* eslint-disable prefer-template */\nconst pkg = require('./package.json');\nconst path = require('path');\nconst webpack = require('webpack');\nconst ESLintPlugin = require('eslint-webpack-plugin');\n\nconst BANNER = [\n  'TOAST UI Date',\n  '@version ' + pkg.version + ' | ' + new Date().toDateString(),\n  '@author ' + pkg.author,\n  '@license ' + pkg.license,\n].join('\\n');\n\nmodule.exports = (env, args) => {\n  const { minify } = env;\n  const { mode = 'development' } = args;\n  const config = {\n    entry: './src/index.js',\n    output: {\n      library: ['toastui', 'Date'],\n      libraryTarget: 'umd',\n      libraryExport: 'default',\n      path: path.join(__dirname, 'dist'),\n      filename: 'toastui-date' + (minify ? '.min' : '') + '.js',\n      globalObject: 'this',\n    },\n    module: {\n      rules: [\n        {\n          test: /\\.js$/,\n          exclude: /node_modules/,\n          loader: 'babel-loader',\n          options: {\n            rootMode: 'upward',\n          },\n        },\n      ],\n    },\n    plugins: [\n      new webpack.BannerPlugin({\n        banner: BANNER,\n        entryOnly: true,\n      }),\n      new ESLintPlugin(),\n    ],\n    devtool: 'source-map',\n  };\n\n  if (mode === 'production') {\n    config.optimization = {\n      minimize: Boolean(minify),\n    };\n  }\n\n  return config;\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"tui-calendar\",\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.18.9\",\n    \"@babel/plugin-transform-react-jsx\": \"^7.18.6\",\n    \"@babel/preset-env\": \"^7.18.9\",\n    \"@babel/preset-typescript\": \"^7.18.6\",\n    \"@playwright/test\": \"^1.23.4\",\n    \"@rollup/plugin-commonjs\": \"^22.0.1\",\n    \"@testing-library/jest-dom\": \"^5.16.4\",\n    \"@testing-library/preact\": \"^3.2.2\",\n    \"@testing-library/user-event\": \"^14.3.0\",\n    \"@types/dompurify\": \"^2.3.3\",\n    \"@types/jest\": \"^27.4.1\",\n    \"@types/node\": \"^17.0.23\",\n    \"@types/webpack-env\": \"^1.17.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.30.7\",\n    \"@typescript-eslint/parser\": \"^5.30.7\",\n    \"babel-loader\": \"^8.2.5\",\n    \"concurrently\": \"^7.3.0\",\n    \"core-js\": \"^3.23.5\",\n    \"eslint\": \"^8.20.0\",\n    \"eslint-config-prettier\": \"^8.5.0\",\n    \"eslint-config-tui\": \"^5.1.0\",\n    \"eslint-plugin-jest\": \"^26.6.0\",\n    \"eslint-plugin-playwright\": \"^0.9.0\",\n    \"eslint-plugin-prettier\": \"^4.2.1\",\n    \"eslint-plugin-react\": \"^7.30.1\",\n    \"eslint-plugin-react-hooks\": \"^4.6.0\",\n    \"eslint-plugin-simple-import-sort\": \"^7.0.0\",\n    \"eslint-plugin-unused-imports\": \"^2.0.0\",\n    \"http-server\": \"^14.1.1\",\n    \"husky\": \"^8.0.1\",\n    \"jest\": \"^27.5.1\",\n    \"jest-date-mock\": \"^1.0.8\",\n    \"lint-staged\": \"^13.0.3\",\n    \"mini-css-extract-plugin\": \"^2.6.1\",\n    \"node-fetch\": \"^2.6.7\",\n    \"prettier\": \"^2.7.1\",\n    \"rimraf\": \"^3.0.2\",\n    \"timezone-mock\": \"^1.3.4\",\n    \"ts-essentials\": \"^9.2.0\",\n    \"ts-jest\": \"^27.1.4\",\n    \"ts-patch\": \"^2.0.1\",\n    \"typescript\": \"~4.7.4\",\n    \"typescript-transform-paths\": \"^3.3.1\",\n    \"vite\": \"^2.9.12\",\n    \"webpack\": \"^5.73.0\",\n    \"webpack-cli\": \"^4.10.0\",\n    \"webpack-merge\": \"^5.8.0\"\n  },\n  \"scripts\": {\n    \"build\": \"npm run build --workspaces\",\n    \"build:calendar\": \"npm run build --workspace=@toast-ui/calendar\",\n    \"build:react\": \"npm run build --workspace=@toast-ui/react-calendar\",\n    \"build:vue\": \"npm run build --workspace=@toast-ui/vue-calendar\",\n    \"build:date\": \"npm run build --workspace=@toast-ui/date\",\n    \"build:calendar-storybook\": \"npm run storybook:build --workspace=@toast-ui/calendar\",\n    \"build:calendar:docs\": \"npm run docs:build --workspace=@toast-ui/calendar\",\n    \"serve:storybook\": \"http-server ./apps/calendar/storybook-static\",\n    \"install:date\": \"cd libs/date && npm install\",\n    \"setup:date\": \"npm run install:date && npm run build:date\",\n    \"lint\": \"npm run lint --workspaces\",\n    \"develop\": \"npm run develop --workspace=@toast-ui/calendar\",\n    \"test\": \"jest\",\n    \"test:watch\": \"jest --watch\",\n    \"test:playwright\": \"playwright test\",\n    \"test:playwright:local\": \"playwright test --project=Chromium\",\n    \"test:playwright:inspect\": \"playwright test --project=Chromium --debug\",\n    \"prepare\": \"ts-patch install -s && husky install && ts-patch install -s && npm run build:date\",\n    \"update:readme\": \"node scripts/replaceLinkInReadme.js\"\n  },\n  \"workspaces\": [\n    \"apps/*\",\n    \"libs/*\"\n  ],\n  \"dependencies\": {\n    \"tui-code-snippet\": \"^2.3.3\"\n  }\n}\n"
  },
  {
    "path": "playwright.config.ts",
    "content": "import type { PlaywrightTestConfig } from '@playwright/test';\nimport { devices } from '@playwright/test';\n\nconst isCI = !!process.env.CI;\n\nconst config: PlaywrightTestConfig = {\n  testDir: 'apps/calendar/playwright',\n  testMatch: '*.e2e.ts',\n  fullyParallel: !isCI,\n  timeout: 30000,\n  forbidOnly: isCI,\n  // eslint-disable-next-line no-undefined\n  workers: isCI ? 2 : undefined,\n  use: {\n    trace: isCI ? 'off' : 'retain-on-failure',\n    viewport: {\n      width: 1600,\n      height: 900,\n    },\n    launchOptions: {\n      slowMo: 250,\n    },\n    timezoneId: 'Asia/Seoul',\n  },\n  webServer: {\n    command: isCI ? 'npm run serve:storybook' : 'npm run storybook --workspace=@toast-ui/calendar',\n    port: isCI ? 8080 : 6006,\n    timeout: 120 * 1000,\n    reuseExistingServer: !isCI,\n  },\n  projects: [\n    {\n      name: 'Chromium',\n      use: { ...devices['Desktop Chrome'] },\n    },\n    {\n      name: 'Safari',\n      use: { ...devices['Desktop Safari'] },\n    },\n    {\n      name: 'Firefox',\n      use: { ...devices['Desktop Firefox'] },\n    },\n  ],\n};\n\nexport default config;\n"
  },
  {
    "path": "scripts/replaceLinkInReadme.js",
    "content": "/* eslint-env node */\n/* eslint-disable @typescript-eslint/no-var-requires */\nconst path = require('path');\nconst fs = require('fs');\n\nconst packages = [];\nconst targets = (process.env.PACKAGES || 'all').split(',');\n\nif (targets.includes('core') || targets.includes('all')) {\n  packages.push('calendar');\n}\nif (targets.includes('react') || targets.includes('all')) {\n  packages.push('react-calendar');\n}\nif (targets.includes('vue') || targets.includes('all')) {\n  packages.push('vue-calendar');\n}\n\npackages.forEach((package) => {\n  const PACKAGE_JSON_PATH = path.join(__dirname, `../apps/${package}/package.json`);\n  const { version, repository } = require(PACKAGE_JSON_PATH);\n  const url = repository.url.slice(0, -4);\n\n  const README_PATH = path.join(__dirname, `../apps/${package}/README.md`);\n  const readme = fs.readFileSync(README_PATH).toString();\n\n  const newReadme = readme\n    .replaceAll('](/', `](${url}/blob/${package}@${version}/`)\n    .replaceAll('](.', `](${url}/blob/${package}@${version}/apps/${package}`);\n\n  fs.writeFileSync(README_PATH, newReadme);\n});\n"
  }
]